MCP Fusion/Core concepts/FSMステートゲーティング

FSMステートゲーティング

VinkiusについてAIに質問

時間的なハルシネーション対策です。ツールをワークフロー状態に紐付け、順序外のアクションをエージェントが試した後に拒否するのではなく、ツール一覧から物理的に消します。

エージェントワークフローで最も高くつくハルシネーションは、間違った値ではなく、ステップ 3 より前にステップ 4 を呼び出すことです。エージェントにはツールが見えるため呼び出し、サーバーは拒否し、ターンが失われます。MCP Fusion はそのステップをメニューから取り除きます。

ツールを状態に紐付ける

typescript
export default f.action('invoice.discharge')
  .describe('Discharge an approved invoice')
  .bindState('approved', 'DISCHARGE')
  .handle(async (input, ctx) => discharge(input.id));

bindState(states, transition) は、ワークフローが指定された状態のいずれかにある間だけこのツールが tools/list に表示され、呼び出しが成功すると transition が発火することを意味します。状態は一度設定したマシンから取得されます。

typescript
const fsm = f.fsm({
  id: 'invoice',
  initial: 'draft',
  states: {
    draft: { on: { SUBMIT: 'review' } },
    review: { on: { APPROVE: 'approved', REJECT: 'draft' } },
    approved: { on: { DISCHARGE: 'discharged' } },
    discharged: { type: 'final' },
  },
});

この形は XState v5 と互換性があります。XState はオプションの peer です。インストールされている場合は実際の actor を実行し、ない場合は組み込みの手動トランジションテーブルを使うため、起動に失敗せず自然に機能を縮退できます。

エージェントからの見え方

  • draft では invoice.discharge は一覧にありません。見えないものを呼び出すことはできません。
  • approved へ進むツールの呼び出しが成功するとマシンが進み、サーバーは notifications/tools/list_changed を送信します。
  • クライアントが一覧を更新すると、独自の説明付きで invoice.discharge が表示されます。

これはガードより強力です。ガードは試行後に拒否しますが、ゲーティングは誘惑そのものを除去します。順序に関する競合を表現できないため、順序のバリデーションエラーはログから消えます。

ゲーティングは一覧だけでなく呼び出し経路でも強制されます。ツール名を推測したクライアントにも、許可されたアクションの一覧付きで FORBIDDEN が返るため、セキュリティがクライアントのメニュー更新に依存することはありません。

スキーマでの段階的開示

FSM に紐付いたコネクターは、コンテキスト自体を小さくすることもできます。マシンが初期状態にある間、compactDescription() はツールに短い説明を与え、後の状態では完全な .describe() が提供されます。2 回目のデプロイなしで、draft のエージェントメニューは approved の同じツールより小さく明確になります。

サーバーレスとステートレスの状態

状態マシンは永続的な状態を持ちますが、MCP 2.0 のステートレスなトランスポートには状態を保持するセッションがありません。3 つの仕組みで対応します。

  1. stateHandleKey: ツール呼び出しに明示的なハンドル引数を含め、モデルが返す方法です。ステートレスなフローで仕様が推奨するパターンです
  2. セッション ID: トランスポートに Mcp-Session-Id がある場合、ゲートはセッションごとに状態を保存します
  3. アタッチメント単位のフォールバック: どちらのキーもないシングルテナントサーバーでは、プロセス内の ID を使います

永続化には、実装していただく FsmStateStore インターフェースを使います。

typescript
interface FsmStateStore {
  load(handle: string): Promise<{ state: string; updatedAt: string } | undefined>;
  save(handle: string, snapshot: { state: string; updatedAt: string }): Promise<void>;
}

単一プロセスではメモリーストアを使い、Redis、DynamoDB、エッジ KV、データベースの行に接続することもできます。フレームワークはリクエストごとにゲートをクローンして復元するため、並行するリクエストが誤って同じマシンを共有することはありません。

使う場面

  • 承認とレビューのフロー。承認前に払い出せないようにする場合
  • 支払いとフルフィルメント。認可後にキャプチャし、前倒ししない場合
  • 複数ステップの移行と破壊的なメンテナンス期間
  • 順序を誤ると費用やデータが失われ、拒否された呼び出しでもターンを消費する場合

FSM ゲーティングは ツール公開 と組み合わせられます。状態によって公開サーフェスが変わるためです。また ルーティング とも組み合わせられます。状態に紐付いたツールを独自のファイルに置けるためです。ハンドラーは変わりません。もともと世界の 1 つの状態向けに書かれているからです。

次のステップ