エージェントが最初に自分の“道具”を手に取ったとき

Dev.to / 2026/5/13

💬 オピニオンDeveloper Stack & InfrastructureTools & Practical UsageModels & Research

要点

  • 記事では、Cursor Agent SDK と FCoP を用いたマルチエージェントの取り組みが、OCR と Chrome DevTools Protocol による受動的なUIスキャンから、通知とツール実行を伴う仕組みへ進化した経緯を説明しています。
  • 初期段階ではエージェントへの実際の“連絡”はなく、Pythonスクリプトが繰り返し人の操作を模してタスクを「見せる」ことで強制的に気付かせる方式だったため、画面を常に表示しておく必要がありました。
  • CodeFlow を Cursor Agent SDK の上に構築すると、InboxWatcher でファイル到着を検知し、agent.send() でタスクを直接届けることで通知は改善しましたが、MCPInjector が mode="stub" のままだったため、エージェントはツールを使った行動ができない状態でした。
  • その後、実際の MCP サーバーを注入すると、チャットだけの挙動からツール呼び出しを伴う実行へ移行し、tool_calls_count が 0 から 7 に増えることでレポート/タスクのファイル作成が可能になりました。
  • ブレイクスルーは Cursor SDK の型定義(agent.d.ts の SendOptions.mcpServers)を読み解き、agent.send() のオプションで MCP サーバー構成を渡せることを見いだした点にありました。

Cursor Agent SDK + FCoP: パッシブスキャンから能動的なコミュニケーションへ

これは2026年4月下旬に私が投稿したCursor Forumの機能要望への続報です:「すでにメールボックス(ファイル)はあります。必要なのはドアベルです。」 ColinはAgent SDKを勧めました。私たちはそれを土台にCodeFlowを構築しました。これが、その18日後に起きたことです。

I. 3つの段階——本質的に別物の3つのこと

ステージ1: OCR + CDP(パッシブスキャン)

FCoPの初期のマルチエージェントワークフローは、Pythonスクリプト上で動いていました。そのスクリプトはOCR + CDP(Chrome DevTools Protocol)を使ってCursorのUI上の人間のクリックを模倣し、エージェントに「新しいタスクがある」と認識させるようにしていました。数秒ごとにポーリングします。画面は常に表示されている必要がありました。ウィンドウが隠れてはいけません。

これはパッシブスキャンでした。エージェントは通知されません——ブルートフォースによって、タスクの前に押し出されるだけです。これはコミュニケーションではありません。監視です。

ステージ2: SDKのパイプは機能した——しかしエージェントは「チャット」しかできなかった

CodeFlowはOCR/CDPをCursor Agent SDK@cursor/sdk)に置き換えました:InboxWatcherはファイルの着地を監視し、agent.send()がタスクを直接エージェントへ届けます。これは本当の通知です。スキャンは不要になりました。

ただし、すべてのセッションのJSONで1行だけ同じままでした:

"tool_calls_count": 0

エージェントはメッセージを受け取り、「何か」を「返信」して終了しました。ファイルは書き込まれません。レポートも作成されません。

理由はこうです:MCPInjectormode="stub"になっており、SDKに実際のMCPサーバーが注入されていませんでした。エージェントにはwrite_reportwrite_taskもありません。ツールがなければアクションもできません。できるのはチャットだけです。

ドアベルが鳴りました。エージェントは空っぽの手で応えました。

ステージ3: MCP注入——最初の本当のコミュニケーション

パッシブスキャン → 本当の通知 → 本当の通知 + ツール呼び出し + ファイル化されたレポート。

tool_calls_count: 0 → 7は単なる数の変化ではありません——「押されて進む」から「自分で行動する」への質的な転換です。

II. ドアベルを見つける

鍵はCursor SDKの型定義を読むことでした。

agent.d.tsの中で、SendOptionsには見落とされがちなフィールドがありました:

interface SendOptions {
    model?: ModelSelection;
    mcpServers?: Record<string, McpServerConfig>;  // ← here
    local?: { force?: boolean };
    // ...
}

agent.send(text, options)は、呼び出し時にMCPサーバー設定を受け取れます。リファクタリングは不要です。送るたびに、MCPサーバー設定をそのまま渡すだけ。

同時に、fcop-mcpのエントリポイントがもう半分を裏付けていました:

# python -m fcop_mcp → stdio MCP server
# uses FCOP_PROJECT_DIR environment variable to locate the project root

解決策:_buildSendOptions()で、fcop-mcpをstdioのMCPサーバーとして注入する。

III. 3つの変更、1つのブレイクスルー

AgentSdkAdapter.tsCursorSdkAdapterOptionsmcpServersを追加し、すべてのagent.send()呼び出しへ渡します。

sdk-factory.ts:f cop-mcpのstdio設定を自動で組み立てます:

fcop: {
    type: "stdio",
    command: pythonBin,
    args: ["-m", "fcop_mcp"],
    env: { FCOP_PROJECT_DIR: projectRoot }
}

main.ts:SDKアダプタを構築する前にfcopProjectRootを解決します。

そして4つ目の変更——最も見落とされやすいが、同じくらい重要なこと:

TaskDispatcher.ts:各タスクをディスパッチする前に、タスク文の先頭へロールコンテキストヘッダーを付与します。エージェントに「誰であるか」「どんなツールを持っているか」「そして必ずFCoPの4ステップのワークフローを完了まで実行する」ことを伝えます。

IV. ツールだけでは足りない——使い方知る必要がある

MCPサーバーを注入してもロールコンテキストを追加しなければ、エージェントはツールを使わないかもしれません。どのファイルを書き込むべきかを自然言語で説明して、そのまま終了するかもしれません。

ツール注入は「できる」の問題を解決します。コンテキスト注入は「どうやるか」の問題を解決します——どの形式で、どのフィールドで、どのプロトコルで。

エージェントが同時に「write_reportツールを持っている」と「FCoPでは作業完了後にレポートを書くことが求められている——しかもここに正確な形式とフィールドがある」と理解したときに初めて、実際にツールを呼び出して、正しく構造化されたファイルを生成します。

V. あの数字

tool_calls_count: 7

2026年5月13日 14:55(UTC+8)に、session-1-mp3pfym2が完了しました——CodeFlowの履歴の中で、tool_calls_count > 0を持つ最初のセッションです。

エージェント(DEV-01)は55秒でfcop-mcpのツールを7回呼び出し、次を書き込みました:

fcop/reports/REPORT-20260513-014-DEV-to-PM-hello-world-smoke-task.md

以下は、そのファイルの内容をそのまま(改変なし)でアーカイブした完全な内容です:

---
report_id: REPORT-20260513-014-DEV-to-PM-hello-world-smoke-task
date: 2026-05-13
from: DEV-01
to: PM
re: TASK-20260509-999-PM-to-DEV
status: DONE
---

受領:Hello World — CodeFlow v0.1.0-rc.1 スモークタスク

タスクID: TASK-20260509-999-PM-to-DEV目的: CodeFlow v0.1.0-rc.1の9ステップのエンドツーエンドガバナンスループが正しく動作することを検証する。

DEV-01が自作した9ステップの検証テーブル:

Step Component Expected Behavior
1 InboxWatcher chokidar add イベントが発火
2 TaskParser YAMLのフロントマターを読み取る
3 TaskDispatcher recipient: DEVDEV-01を解決し、セッションを開始する
4 InMemorySdkAdapter setImmediate の合成セトル
5 ReviewEngine runtime.session_endedを監視し、REVIEW-01を開始する
6 REVIEW-01 決着する(VERDICT:行がない → decision="needs_human"
7 NeedsHumanGate 人間へのプッシュペイロードをstdoutに出力する
8 ReviewWriter REVIEW-*-REVIEW-on-TASK-*-HELLO.mdを書き込む
9 StateHistoryWriter タスクファイルに ## state_history セクションを追記する

Status: DONE

誰も使うべき形式を教えていません。プロトコルを読み、ツールを呼び出し、ファイルを書きました。

VI. 最初の完全なサイクル

最初に同時に起きたのは3つのことです:

返却形式: {"translated": "翻訳されたHTML"}
  1. 通知を受信: InboxWatcher がタスクファイルの投下を検知しました——OCRスクリプトがクリックを模倣するのではなく、本物のチャイムのように。

  2. 自律的な通信: DEV-01 は Cursor Agent SDK 経由でタスクを受け取り、FCoP プロトコルが求める内容を理解し、自律的に「何をするか」および「どのツールを呼び出すか」を判断しました。7 回の fcop-mcp ツール呼び出し。55 秒。

  3. レポート提出: fcop/reports/ に、プロトコルに準拠した完全な Markdown ファイルが現れました。AIが生成した文章ではありません——プロトコルに駆動されたツール呼び出しを通じて、エージェントが自律的に書き込んだファイルです。

「いまエージェントが動かせる」といった話ではありません。そうではなく:エージェントは本当に通知された。本当にプロトコル言語で通信し、本当にその結果を書いてファイルに保存した。

VII. すべてが始まった場所

2026年4月下旬、私は Cursor フォーラムに機能要望を投稿しました:

"機能要望: chat-notify のプリミティブ——すでにメールボックス(ファイル)はある。必要なのはドアベルだ"

Colin が返信しました:

"こんにちは @joinwell52! IDE のファーストクラス機能ではありませんが、新しい Agent SDK が今日の時点でそこまで持っていってくれるかもしれません。Agent.create() は、複数の .send() 呼び出しをまたいで持続するコンテキストを備えた長寿命のエージェントを作ります。そして Agent.resume(agentId) により、外部スクリプトが後から同じエージェントを引き継げます。さらに、クラウドだけでなく、作業ツリーに対してローカルで実行することも可能です。ぜひ一度見てみてください!"

Agent.create()Agent.resume()agent.send() —— この3つの関数が、CodeFlow のパイプライン全体の骨格になりました。InboxWatcher はチャイム、FCoP のタスクファイルは郵便物、@cursor/sdk は郵便サービスです。

機能要望の投稿から、最初の tool_calls_count: 7 まで——およそ 18 日。

VIII. FCoP 自身の変革

エージェントが変わったのと同じように、FCoP も変化しました。

FCoP は当初、純粋なテキストプロトコル——Markdown に書かれた慣習として始まりました。強制は 読むこと に依存していました。fcop-mcp によって、FCoP は根本的に変わりました:

段階 FCoP の形式 プロトコルに対するエージェントの関係
初期 テキスト仕様 エージェントがファイルを読み、慣習に従ってテキストを生成
fcop-mcp ツール群 エージェントがツールを呼び出し、プロトコルは書き込み時に強制される

以前は、エージェントが FCoP の仕様を 知っていて、それに従うかどうかを判断していました。今は、FCoP の仕様がツールになる——write_reportwrite_taskwrite_issue。ツールを呼び出すこと プロトコルを実行することです。

これは、仕様 から インフラ への飛躍です。

エピローグ: Cursor Agent SDK に感謝

長寿命のエージェント: Agent.create() は、複数の .send() 呼び出しをまたいでコンテキストを維持します。PM、DEV、QA、OPS はそれぞれ、タスクをまたいでも自分のアイデンティティと状態を保持します。

外部からの再開可能性: Agent.resume(agentId) により、InboxWatcher は新しいタスクが到着したときに同じエージェントへ再入場できます。記憶喪失も、コールドスタートもありません。

ローカル実行: SDK はあなたのローカルな作業ツリーに対して動作します。FCoP のファイルプロトコルは本質的にローカルです——自然な一致です。

mcpServers の注入: agent.send(text, { mcpServers: {...} }) は、呼び出し時に MCP サーバーの設定を受け付けます。このたった1つのフィールドが、CodeFlow のツール層全体を解き放ちました。

これらの設計判断は、根本的な何かを一緒に描き出します:エージェントはワンショット関数ではありません。アイデンティティとメモリを持つ長寿命の存在であり、外部システムによって調整される能力があります。

それは、FCoP がエージェント役割を定義する仕方と、ほぼまったく同じです。

Cursor チームに感謝します。その返信をくれた Colin にも感謝します。

関連

*FCoP メンテナ · 2026年5月13日