Site
全記事

公開日 2026年9月21日約11分で読了

自前のMCPコネクタを組む: なぜMCP Fusionなのか

あなたのデータとエージェントの知覚の間にあるもの、そしてその間隔がなぜアーキテクチャであるのか: egress を閉じる MVA の分離、エージェントが見るものを決める presenter、自己修復するエラー、エージェントが感じられる状態、そしてそれをすべて一つのハッシュ可能なバンドルとして届ける deploy。

Renato Marinho

著者 Renato Marinho

Founder · Vinkius

The anatomy of an MCP connector: source of truth on the left, the MVA stack in the middle with the model as egress boundary, the presenter as perception and the tools as verbs, and the perception package the agent receives on the right; undeclared fields are stripped at the model layer.

MCP コネクタは約束である。あなたの文脈も、履歴も、意図も共有しないシステムに対して、billing.void_invoice の呼び出しは、あなたが意図した意味と確かに一致することを約束している。DB の行は答えではない。それは素材にすぎない。素材とエージェントの間に一つアーキテクチャの層があり、手作りのサーバーはたいていそこを抜けている。

エージェントは、構造上確率的だ。パラメータを幻覚し、入力を書き間違え、考えずにリトライし、呼び出しと调用のあいだで文脈を落とす。素のサーバーは各ツール呼び出しを独立として扱う。その一つの抜けが、動くデモを、データを壊し、トークンを浪費し、誰も追跡できない形で失敗するシステムに変える。生 JSON をエージェントに送り込むと、構造的な障害モードが4つ生まれる。文脈不足、行動の盲、知覚の不整合、情報漏洩。これらはプロンプトエンジニアリングを積み重ねても埋まらない欠落だ。

それがこの記事の主張だ。障害モードが構造的なら、直し方も構的になければならない。MCP Fusion は MVA というパターンで、その直しを扱う。このパターンは、アプリケーションコードがもともと知っている責任の分離だ。変るのは向ける先だけ:

  • Model が、データの意味と、プロセスの外へ出せる範囲を所有する。
  • Presenter が、エージェントへの知覚を所有する。
  • Tools が動詞を所有する。クエリ、変更、アクション。

MVA パイプライン: 素の行がモデル境界をくぐり、未宣言フィールドは剥がされ、プレゼンタが実際にエージェントへ届く知覚パッケージを組み立てる

この分離を誠実にするのは一つの規則であり、それはスタイル規則ではなくセキュリティ特性だ。向こうは、方向である。Tools は Presenter を import し、Presenter は Model を import し、Model はコアを import する。逆向きへの import は一切ない。データに触れるレイヤーは、絶対にエージェントが操れるレイヤーであってはならない。

Model: ワイアが切れる場所

通常のアプリケーションでは、スキーマは入力を検証する。コネクタでは出力も閉じなければならない。ワイアの向こうの消費側は、コードを読む同僚ではない。渡されたものすべてを実行する言語モデルだからだ。defineModel でその境界が引かれる。四つの宣言は、それぞれ別の仕事を背負う:

m.casts がフィールド、その型、説明を宣言する。その説明は人間向けのドキュメントではない。ちょうど必要な時、各レスポンスと添えられる解釈規則としてコンパイルされる。m.hidden がワイアへ一切届かないフィールドを宣言する。パスワードハッシュ、内部フラグ、テナントマーカーだ。m.guarded がエージェント側から絶対に入れないフィールドを宣言する。m.fillable が入力プロファイル、create、update、filter を宣言する。ツールのパラメータは、手打ちし直すのではなく、そのプロファイルから導出される。

これが本番で効いてくる帰結だ。マイグレーションがテーブルにカラムを増やしても、そのカラムは漏れない。誰かがモデルに入れ、さらに誰かが意図して Presenter に入れるまで、ワイアを渡さない。素のサーバーにはこの性質がない。出力は序列化された行そのものだ。新フィールドがスキーマに増えた瞬間、パスワードハッシュ、内部フラグ、テナント ID がすべてエージェントの文脈窓に届いてしまう。

Presenter: 見せない知覚

MVA の V は、人間の目用ではない。エージェントが見るパッケージであり、4層から組み立てる。

防火壁を生き延びたデータ。 serialize する前に、presenter は生結果に対してスキーマを strip モードで実行する。DB が何を返しても、エージェントが見るのは宣言された表面だけだ。これは RAM レベルでの egress 制御であり、view レイヤーでもテンプレートでも規約でもない。スキーマが知らないフィールドは、渡れない。

届ける瞬間の規則。 モデル上のフィールド説明は、このレスポンス、このエンティティに添えるシステム規則としてコンパイルされる。エージェントは何千トークンものグローバルプロンプトを背負っていない。実際に求めたものへの解釈規則だけを受け取る。ドメイン知識は1箇所に住み、そのドメインがかかわる時だけ添えられる。

実働の上限。 presenter が、このエージェントが1レスポンスで見えてよい項目数を宣言し、リストが切れた時に何があるかを伝える。1万行のリストは、エージェントにとってデータではない。データ姿で出ているサービス妨害だ。上限は知覚の一部であり、切られた通知こそが、エージェントが実際より多く見たふりをすることを防ぐ。

アフォーダンス。 suggestActions が、先ほど見たものに基づいて次に取れることをエージェントに伝える。ドキュメントではこれをエージェント向け HATEOAS と呼ぶ。行動の盲が死ぬのはここだ。レスポンスはペイロードではなく、ワークフロー中の位置である。サーバーサイドで描画されるチャートや図形ブロックも同じパッケージの一部であり、決定的だ。描画するのはフレームワーク、読むのはエージェントであり、ループ内にいるモデルがピクセルを生成することはない。

Tools: 意図を持つ動詞

このフレームワークでは、ツールはスキーマ付きの名前付き関数ではない。既定の意図を持つ意味的な動詞だ。f.query は読み取り専用。f.mutation は既定で破壊的。f.action は中立。それはメタデータラベルではない。プラットフォームがリトライを安全とみなすもの、observability パイプラインがマークするもの、読みが書きに変わったらガバナンスツールが旗を立てるものを、この分類が導く。

動詞からは、チェーンは意図的に短い。.fromModel は入力の形をモデルの fillable プロファイルから引き出す。ツールのパラメータは、egress を閉じたのと同じ宣言から導出される。.returns がレスポンスに presenter を付加する。.proxy がハンドラを書き上げる。動詞から HTTP メソッドを推論し、入力からパスパラメータを解決し、レスポンスのエノベロープを展開する。.with ステップは、モデルでは表せないドメイン固有の入力用だ。

f.router が動詞をプレフィックスで集約し、ミドルウェアやタグを各に継承させる。f.middleware は下流で型付き context を導く。テナント識別子は、その context 内の検証済み証書から来る。だからこそドキュメントは「エージェントは上書きできない」と断定的に言える。並行数の上限も、egress のバイト上限も、同じチェーンに付く。ワークフローがツールではなくプロンプトを必要とする時、definePrompt は同じ presenter から組み立てる。規則がシステムメッセージに、データと UI がユーザーブロックになる。真実の一つの源、二つの表面だ。

方向付けするエラー、エージェントが感じられる状態

素のサーバーは、誤った呼び出しに平坦な文字列で答える。平坦な文字列へのエージェントの反応は、同じ入力でリトライすることだ。もう一度、繰り返す。そのループは、トークン予算が死ぬ場所だ。間違った返金が4回も試行されるのもそこだ。

フレームワークの答えは、自己修復するエノベロープだ。f.error は、具体的な code、メッセージ、提案、代わりに取れるアクションのリスト、任意の details、リトライウィンドウから組み立てる:

<tool_error code="InvoiceNotFound">
  <message>Invoice "INV-999" does not exist.</message>
  <recovery>Call billing.list_invoices first to find valid IDs.</recovery>
  <available_actions>billing.list_invoices</available_actions>
</tool_error>

具体的な code は汎用より強い。AlreadyPaidBAD_REQUEST が伝えられないことをエージェントに伝える。recovery 行が、リトライループを高くさせる勘定を消す。

state は、言語モデルにないもう一つの感覚だ。mutation の後でも、エージェントは mutation 前に取得したリストが最新だと信じたままになる。フレームワークの答えは、レスポンスの state 同期シグナルで、HTTP のキャッシュ機構から借用するものだ。ツールが結果を immutable、volatile、または causal とマークする。immutable な結果は信頼してよい。volatile は「再クエリせよ」を言う。causal のマークは、この mutation 後に、これらの他動詞も再クエリが必要だと告げる。因果であり、時間ではない。時計を持たないエージェントが本当に必要なのは、まさにそこだ。

Deploy: バンドルとしてのコネクタ

この作り方で組んだコネクタは、単一の成果物として出荷し、全工程を CLI が担う。mcpfusion deploy はサーバーを自己完結の1ファイルにまとめる。依存はすべてインライン化し、Node builtins は bundler を満たすためにあるスタブに置き換える(決して呼ばれない)。transport 自体もプラットフォームが供給する以上、スタブになる。バンドルは生サイズ 1.5 MB というゲートを通過するが、それは仕様ではなく予算だ。圧縮し、ハッシュを取り、edge へ届ける。ハッシュがデプロイ済みと一致すれば、プラットフォームは instant restore を報告する。同じバイト列、再読み込みなしだ。

アーキテクチャを監査可能にするのは、この2つの工程だ。CLI はバンドルを introspect しながら2度目のコンパイルを実行する。ツール契約、プロンプト、証書スキーマを抽出し、capability lockfile に書き出す。決定的なスナップショットで、コネクタの振る舞い表面そのものだ。その lockfile は git で diff できる。CI での fusion lock --check がゲートだ。コードが実際に見せる表面と、コミットした表面を照合し、差分を breaking、risky、safe、cosmetic のいずれかに分類する。プロトコルにはドリフト検出の仕組みがない。フレームワークが与える。監査できないデプロイと、レビュアーが読めるデプロイの違いがそこにある。

同じバンドルは、プロトコルの現行期を話す。stateless、リクエストごとの処理、ロードバランサの後ろで動作する。同じレジストリは、stdio と、2025 世代の HTTP でも変更なしで走る。ローカル開発も本番も同じコードの上にある。その仕組みだ。

edge デプロイには3つの制約があり、いずれも設計判断だ。ツールセットは明示的に register する(そこには filesystem が存在しないため、discovery は走らない)。native addon は使えない。バンドルはプロセスには触れない。import も登録も明示的であることで、サーバーが edge で合法になる。

成果物としての deploy: 左にソースツリーと lockfile、中央に CLI パイプライン、右に同じレジストリが stdio、HTTP、stateless な edge で動作している

なぜ MCP Fusion で組むのか

上記全体の裏にある問いに、直接答えられるのはこれだ。なぜ素の MCP サーバーを書いて、痛い所にだけ安全性を足さないのか。

障害モードが構造的だからだ。構造的な直しはフレームワークの中にあり、ハンドラの中にはない。素のサーバーには6つの「ない」がある。こちらには6つの「ある」がある:

  • 素のサーバーは、返すものを漏らす。こちらはモデル層で egress を閉じ、新カラムが誰かが宣言するまで、エージェントへ届かない。
  • 素のサーバーは何も強制しない。こちらは attach 後にレジストリを凍結し、パイプラインの順序を構造的に保つ。安全性は強制であり、規約ではない。
  • 素のサーバーは自身のドリフトが見えない。こちらは振る舞い表面を lockfile にハッシュし、merge 前に各変更を分類する。
  • 素のサーバーはエラーに文字列で答える。こちらは回復手順と、次に取れるアクションで答える。
  • 素のサーバーは時間に盲である。こちらはレスポンスに causal な無効化シグナルを載せる。
  • 素のサーバーはどこでホストされても表面が一つである。こちらは stdio、HTTP、Vinkius edge、serverless の対象にわたるレジストリが一つであり、observability は SOC 2 コントロールに写り、SIEM へ転送できる。

2つの乗数が、仕事そのものの経済を変える。既存の契約からコネクタを生成できる。OpenAPI 仕様や Prisma スキーマを、1コマンドで完全なサーバーにする。egress、テナント分離、メモリ保護は、生成コードに焼き込まれている。テストが本物の pipeline だ。testing パッケージは、コネクタを RAM 上で、本番と同じ validation、ミドルウェア、ハンドラ、presenter、egress を経由させて実行する。トークンゼロ、完全決定。データに秘密フィールドがないこと、規則が届いたこと、エラーが分類されたことをアサートできる。

正直なコストは、これがヘルパーライブラリではなくアーキテクチャであることだ。MVA の分離が内面化するまで、数日はかかる。バンドルの予算が、依存を軽量のまま保つ。買うのは、自分のデータとエージェントの知覚の間の境界で、レビューではなくフレームワークが強制する側だ。ワイアの向こうに誰かのエージェントが向き、本番で回すものなら、それが肝だ。

1つのコネクタ: 仕様から edge まで

全工程は、最初から最後まで:

mcpfusion create invoices --vector openapi
mcpfusion remote --server-id <uuid from the dashboard>
mcpfusion deploy

生成ではなく素の開始なら、同じ3コマンドに --vector vanilla を付ける。次に最低限の3つの宣言:

defineModel('Invoice', m => {
  m.casts({
    id: m.uuid().label('Invoice ID, a UUID'),
    total: m.number().label('Total, integer cents'),
    status: m.string().label('open, paid, or voided'),
  });
  m.hidden(['webhookSecret', 'internalFlags']);
  m.fillable({ create: ['id', 'total'], filter: ['status'] });
});

const router = f.router('billing');
router.mutation('void_invoice')
  .withString('id')
  .returns(invoiceUI)
  .invalidates('billing.*')
  .proxy('invoices/:id/void');

const tester = createMCPFusionTester(registry, {
  contextFactory: () => ({ tenantId: 't_777' }),
});

const result = await tester.callAction('billing', 'void_invoice', { id: 'INV-7' });

expect(result.data).not.toHaveProperty('webhookSecret');
expect(result.uiBlocks.length).toBeGreaterThan(0);

テストは本番と同じパイプラインを走らせ、1 トークンも使わず、egress が保たれていたことを示す。デプロイし、CI で lockfile をチェックする。コネクタは、リポジトリの中のハッシュで、pull request 中の差分で、edge 上の隔離されたプログラムだ。同じ 1 つの物が、この 3 つのすべてだ。

シーラされ、スナップショットから復元される形でそのプログラムを実行する runtime は、V8 isolates の記事 の主題だ。

トピックmcpfusionconnectorsmcpagentsedge