MCP Fusion/Core concepts/ミドルウェアとコンテキスト

ミドルウェアとコンテキスト

VinkiusについてAIに質問

型付きコンテキスト導出で認証、テナント分離、レート制限を組み合わせます。defineMiddleware の 1 回の呼び出しで下流のすべてのハンドラーの ctx が拡張され、ビルド時に O(1) でコンパイルされます。

ミドルウェアは、コネクターが関数の集合からサービスへ変わる場所です。アイデンティティを解決し、テナントを分離し、トラフィックを制限し、すべての呼び出しを監査します。MCP Fusion のミドルウェアモデルは小さく、エンドツーエンドで型付けされ、最初のリクエストの前にコンパイルされます。

2 つの形、1 つの概念

コンテキスト導出が慣用的な形です。ミドルウェアは ctx に追加する部分を返します。

typescript
import { initMCPFusion } from '@mcpfusion/core';

interface AppContext {
  db: PrismaClient;
  user?: { id: string; role: 'viewer' | 'admin' };
}

const f = initMCPFusion<AppContext>();

const withUser = f.middleware(async (ctx) => {
  const token = ctx.headers?.authorization;
  const user = await verify(token);
  if (!user) throw f.error('UNAUTHORIZED', 'Sign in first');
  return { user };
});

ツールでの利用で接続します。

typescript
f.query('billing.list_invoices')
  .use(withUser)
  .handle(async (input, ctx) => {
    return ctx.db.invoices.findMany({
      where: { userId: ctx.user.id },
    });
  });

型システムは導出結果を引き継ぎます。.use(withUser) によってビルダーのコンテキスト型は AppContext & { user } に変わるため、ハンドラー内の ctx.user はコンパイルされ、ミドルウェアを削除するとビルドが失敗します。本番で初めて見つかる実行時キャストはありません。

従来の形も利用できます

前後で処理したり、途中で短絡したりする必要があるミドルウェアは (ctx, args, next) を使います。

typescript
import type { MiddlewareFn } from '@mcpfusion/core';

const audit: MiddlewareFn<AppContext> = async (ctx, args, next) => {
  const started = Date.now();
  const result = await next();
  ctx.audit?.({ tool, args, ms: Date.now() - started });
  return result;
};

ミドルウェアから ToolResponse を返すと短絡します。next() は呼び出されず、そのレスポンスが回答になります。これにより inputFirewallrateLimit はハンドラーに触れずに呼び出しを拒否できます。return next() を忘れると、すべての開発者が一度はその間違いをするため、コンソールに一度だけ警告が表示されます。

例外を投げる方法も使えます。throw toolError('NOT_FOUND', ...) はコードと復旧情報を保ったままパイプラインを通過します。それ以外の throw 値は INTERNAL_ERROR としてラップされます。

順序とスコープ

3 つのスコープがアクションごとに 1 つのチェーンへ組み合わされます。

スコープ宣言場所位置
グローバルf.middleware() とレジストリレベル最外部
グループActionGroupBuilder.use()中間
ツールまたはアクションビルダーの .use()最内部

グローバルミドルウェアが最初に実行され、認証がすべてに先行します。アクション単位のミドルウェアは最後に実行され、ハンドラーに最も近い位置になります。チェーンは buildToolDefinition() でアクションごとの 1 つのクロージャへコンパイルされるため、リクエストパスは配列をループするのではなく呼び出し 1 回です。

組み込み機能

フレームワークには 3 つのミドルウェアが付属します。特別な魔法ではなく、通常のミドルウェアです。

  • inputFirewall({ judge }): 引数に対する LLM-as-Judge、fail-closed で INPUT_REJECTED を返します
  • rateLimit({ windowMs, limit, keyFn }): タイムスタンプによるスライディングウィンドウ。拒否されたリクエストは記録されないため、悪用するクライアントが自分のロックアウトを延長することはできません
  • auditTrail({ sink, hashArgs }): 呼び出しごとに引数の SHA-256 ダイジェストを含むイベントを出力します。引数そのものは出力しません

認証ミドルウェアは専用パッケージの requireJwtrequireApiKeyrequireAuth から提供されます。認証をご覧ください。

構造による分離

contextFactory はリクエストごとに実行され、新しいオブジェクトを返します。ミドルウェアはそのオブジェクトに導出したキーを書き込みますが、__proto__constructorprototype は保護機構によってスキップされます。そのため、同じ長寿命プロセスに到達する場合でも、並行する 2 つのリクエストがテナント ID、ユーザー、データベースハンドルを共有することはありません。ステートレスなトランスポートではリクエストごとに新しいサーバーが割り当てられ、stdio ではリクエストごとのコンテキストオブジェクトによって同じ保証が得られます。

これが マルチテナントコネクター の基盤です。分離はハンドラーが覚えておく規律ではなく、ランタイムの性質です。

次のステップ