MCP Fusion/Protocol and runtime/プロンプトエンジン

プロンプトエンジン

VinkiusについてAIに質問

プロンプトはツールと並ぶ第一級の存在です。型付き引数スキーマ、プロンプトハンドラーからのループバック実行、署名付きカーソルによるページネーション、インターセプター、ハイドレーションの締切を備えています。

ツールは実行するもの、プロンプトは種をまくものです。プロンプトは、会話全体を再利用できる形でサーバーサイドに置くテンプレートです。クライアントがプロンプトの一覧を取得し、ユーザーがそのうちの一つを選び、サーバーがメッセージの一式を返します。MCP Fusion はプロンプトをツールと同じエンジニアリングで扱います。型付き入力、ミドルウェア、ページネーション、そして自前のツールパイプラインへのループバックを備えています。

プロンプトを定義する

typescript
import { z } from 'zod';

export default f.prompt('issue.review')
  .describe('Review a pull request with the team checklist')
  .input(z.object({
    repo: z.string().describe('Repository slug'),
    pr: z.number().describe('Pull request number'),
  }))
  .handler(async (ctx, args) => ({
    description: 'Review acme/product#42 against the checklist',
    messages: [
      { role: 'user', content: `Review PR ${args.pr} in ${args.repo}. Use the checklist.` },
      { role: 'assistant', content: 'Sharing the checklist first, then the diff.' },
    ],
  }));

Fluent 形式はツールと同じ作りです: .describe().input()(Zod オブジェクト、またはプリミティブ引数のフラットなディスクリプターマップ)、.use(middleware).title().icons().tags()、そして終端の .handler((ctx, args) => PromptResult)。返り値は { description?, messages } で、ロールは userassistant を使え、複数ターンの会話も可能です。テキスト以外のコンテンツブロックも扱え、imageaudioresource_linkresource、さらに PromptMessage のファクトリーヘルパーが用意されています(.system()user としてエンコードされます。MCP には system ロールがないためです)。

宣言的な definePrompt や、f.presenter() と同じスタイルの f.prompt(name, config) も、設定をコードとして書く構成のために用意されています。引数は Zod にコンパイルされ、厳格なフラットバリデーションが適用されます。使えるのはプリミティブだけで、配列や入れ子オブジェクトは対象外です。これは MCP の制約で、ワイヤーの時点ではなく定義の時点で、明確なエラーとして強制されます。

ループバック: ツールを呼び出すプロンプト

プロンプトハンドラーのコンテキストには invokeTool(name, args) が含まれます。これはミドルウェア、バリデーション、Presenter を通る同じツールパイプラインへのディスパッチャです。したがってプロンプトは静的な文字列ではありません。ユーザー自身のツールで PR のライブな diff を取得し、チェックリストをハイドレートしたうえで、種をまいたメッセージを返すことができます。RBAC はループバックの中でも強制されます(呼び出し元のコンテキストは呼び出し元のコンテキストのままです)。さらにリクエストの AbortSignal も伝播するため、キャンセルされたプロンプトは自分が引き金にしたツールの処理もキャンセルされます。

ハイドレーションには上限が設けられています。Registry ごとの締切(.timeout(ms) をビルダーに設定するか setDefaultHydrationTimeout を使います)が、プロンプトがツールを呼び戻すために費やせる時間を抑えます。締切を過ぎると、クライアントをハングさせる代わりに、ハイドレーションのアラートブロックを添えたレスポンスが返されます。

ページネーションとライフサイクル

prompts/list はサーバー側でページネーションされます。CursorCodec は "after" の名前をカーソルに変換します。カーソルは HMAC-SHA256 で署名されるか、AES-GCM で暗号化されます(32 バイトのシークレットで設定します。指定しない場合は、プロセスごとに生成される一時的なキーが使われます)。デフォルトのページサイズは 50 で、tag による絞り込みもサポートされています。カーソルは設計上、クライアントから見て不透明なものです。

notifyPromptListChanged()(または実行時の f.prompt の変更に対する list_changed イベントの経路)は notifications/prompts/list_changed を 100ms のデバウンスで送信します。セッション中に Registry が成長しても、クライアントに無駄な通知を連打させることはありません。

インターセプター

registry.useInterceptor(fn) はハンドラーが返ったあと、クライアントが結果を見るに実行されます。<compliance_notice> を追加する、ユーザーのターンを先頭に付ける、タグで囲んだコンテキストブロックを挿入する、といったことができます。インターセプトを使えば、すべてのプロンプトを編集せずに、組織全体のガイダンスを安全に追加できます。

なぜこれが重要なのか

MCP 上に構築するチームにとって、統合のコストはたいてい「クライアントにはツールしかない」という点に表れます。プロンプトはもう半分を埋めます。機能だけを渡すのではなく、ワークフローそのものを届けられるのです。ToolsResourcesWire format と組み合わせれば、コネクター 1 つで、あなたのデータをどう扱うかの全体像を、エージェントのネイティブプロトコルのまま渡せます。

次のステップ

  • Tools: 実行を担う半分
  • Runtime architecture: Registry が置かれる場所
  • Skills: SKILL.md をエージェントへ届けるのもプロンプトです