MCP Fusion/Security and governance/マルチテナントコネクタ

マルチテナントコネクタ

VinkiusについてAIに質問

1 つのコネクタで複数のテナントを扱えます。特別なモジュールは不要です。リクエストごとのテナント解決、タグでフィルターされた機能の可視性、ロールベースの Presenter により、各テナントが見てよい内容だけを表示します。

1 つのコネクタで複数の顧客に対応することは、後から追加する機能ではありません。コンテキスト、可視性、認識がどのように構築されるかという性質です。MCP Fusion にテナンシーモジュールがないのは、既に使っている部品が 1 つに組み合わさるためです。このページではその方法を説明します。

1. リクエストごとにテナントを解決する

すべてのリクエストは新しい ctx から始まるため、ハンドラーの前にテナントを一度だけ決定します。

typescript
interface AppContext {
  db: PrismaClient;
  tenantId: string;
  role: 'viewer' | 'admin';
}

registry.attachToServer(server, {
  contextFactory: async (extra) => {
    const claims = await verify(extra.session?.authToken);
    return {
      db: pool.forTenant(claims.tenant_id),
      tenantId: claims.tenant_id,
      role: claims.role,
    };
  },
});

安全にするルールは 2 つあります。ファクトリーはリクエストごとに新しいオブジェクトを返すため、あるリクエストが別のリクエストのコンテキストを観測できません。ミドルウェアはプロトタイプ汚染対策付きでそのオブジェクトに書き込み、悪意のある引数名が __proto__ に到達できないようにします。ハンドラーはデータへの唯一の入口として ctx.db または ctx.tenantId を使い、テナント ID がツール引数から来ることはありません。

2. タグで機能をスコープする

テナントが同じ面を持つことはほとんどありません。タグとフィルターで、誰に何が存在するかを決めます。

typescript
const filterFor = (role: string) =>
  role === 'admin'
    ? { anyTag: ['public', 'admin'] }
    : { tags: ['public'], exclude: ['internal'] };

attachToServer(server, { filter: filterFor(ctx.role) });

この形では、無料デプロイメントは公開ツールを、エンタープライズデプロイメントは管理ツールを公開し、ハンドラーのコードベースは同一です。フィルターは、リクエスト自身のコンテキストの下でツールリストが構築される場所にて評価されます。Tool exposition を参照してください。

3. ロールで認識を形作る

可視性はロールが見るツールを決め、認識はツールが表示する内容を決めます。確認済みのパターンは、ステップ 2 のフィルターが選択する 1 つのハンドラー上の 2 つのツールです。

typescript
const listPeople = async (input, ctx) => ctx.db.people.findMany();

f.query('people.list_public')
  .describe('List people in the tenant, without contact details')
  .tags('public')
  .returns(ViewerPresenter)          // schema without email or phone
  .handle(listPeople);

f.query('people.list_admin')
  .describe('List people in the tenant with full records')
  .tags('admin')
  .returns(AdminPresenter)
  .handle(listPeople);               // same handler function

viewer ロールのエージェントには people.list_public だけが届き、連絡先フィールドは見えません。admin エージェントには完全な Presenter とともに両方が届きます。フィールドルールとレダクションは対象者ごとに 1 か所にあり、コントラクトダイジェストは両方の面を記録します。(data, ctx) => string[] として書くコンテキストルールは、ロールに応じたガイダンスを追加します。Models and Presenters を参照してください。

4. テナントごとにクレデンシャルを分離する

ベンダーキーもテナント間で共有してはいけません。defineCredentials で宣言してリクエストごとに読み取ります。Vinkius Cloud ではランタイムが購入者ごとのシークレットを各リクエストに注入し、ローカルでは同じアクセサーが環境を読み取ります。Credentials を参照してください。

5. コストとトラフィックを帰属させる

すべての呼び出しを帰属させられます。ctx.tenantId をレート制限キーとして渡し、あるテナントが別のテナントの予算を使い切らないようにします。また、監査イベントとテレメトリにテナント ID を出力します。Vinkius コンソールでは同じコネクタがサーバーごとのコストと信頼性を表示します。AI spendTool reliability に記載されています。

パターンを 1 つの表に

ConcernMechanismWhere it lives
IdentityJWT, API key or OAuth tokencontextFactory + auth middleware
Data isolationper-tenant client from the contextcontextFactory
Capability visibilitytags + filtertool list construction
Field isolationrole-based PresentersPresenter per audience
Secret isolationBYOC credentialsruntime injection
Fair userate-limit key from ctxrate limiter middleware
Proofcontract digest + audit eventslockfile and audit sink

忘れてしまうランタイムスイッチも、誤設定するクロステナントキャッシュもありません。明示的に共有しない限り何も共有されないためです。これが、テナント列を持つだけのシングルテナントコネクタとマルチテナントコネクタの違いです。

次のステップ