📖

Code execution with MCP: Building more efficient agentsを読んでみる

に公開

本投稿について

本記事は、Anthropic社が公開されているEngineering at Anthropic: Inside the team building reliable AI systemsのガイド記事を読んでAI Agent開発の知見を高め、共有していくための記事となります。

記事中のわかりにくい用語に関しては、脚注を加えております。
本投稿記事で誤りがございましたら、コメントいただけると幸いです。

サマリー

MCPはエージェントが多くのツールやシステムに接続するための基礎プロトコルを提供します。しかし、接続されたサーバーが多すぎると、ツールの定義や結果が過剰なトークンを消費し、エージェントの効率が低下することがあります。

ここでの多くの問題は、コンテキスト管理、ツール構成、状態持続性など(技術的に)新しく感じられますが、ソフトウェア工学から既知の解決策が存在します。コード実行はこれらの確立されたパターンをエージェントに適用し、馴染みのあるプログラミング構造を使ってMCPサーバーとより効率的にやり取りできるようにします。

イントロ

モデルコンテキストプロトコル(MCP)は、AIエージェントを外部システムに接続するためのオープン標準です。エージェントをツールやデータに接続するには、伝統的に各ペアリングごとにカスタム統合が必要であり、断片化や重複作業を生み出し、真に接続されたシステムのスケールを難しくしています。MCPはユニバーサルプロトコルを提供しており、開発者はエージェント内で一度MCPを実装すれば、統合のエコシステム全体が解放されます。

2024年11月のMCP開始以来、採用は急速に進みました。コミュニティは数千台のMCPサーバーを構築し、主要なプログラミング言語すべてに対応したSDKが利用可能であり、業界全体でもMCPをエージェントとツールやデータ接続の事実上の標準として採用しています。

現在、開発者は数十台のMCPサーバーで数百から数千のツールにアクセスできるエージェントを日常的に構築しています。しかし、接続されたツールの数が増えるにつれて、すべてのツール定義を最初に読み込み、中間結果[1]をコンテキストウィンドウ[2]に渡すことでエージェントの動作が遅くなりコストが増加します。

本ブログでは、コード実行によってエージェントがMCPサーバーとより効率的にやり取りし、より多くのツールを扱いながらトークン数を減らす方法を探っていきます。

ツールからの過剰なトークン消費はエージェントの効率を低下させる

MCPの使用が拡大するにつれて、エージェントコストやレイテンシを増加させる2つの一般的なパターンがあります。

  1. ツール定義はコンテキストウィンドウを過負荷にします
  2. 中間ツールの結果は追加のトークンを消費します

1. ツール定義はコンテキストウィンドウを過負荷にします

ほとんどのMCPクライアントはすべてのツール定義を最初に直接コンテキストに読み込み、ツール呼び出し構文でモデルに露出させます。これらのツールの定義は以下のようになります:

gdrive.getDocument
     Description: Retrieves a document from Google Drive
     Parameters:
                documentId (required, string): The ID of the document to retrieve
                fields (optional, string): Specific fields to return
     Returns: Document object with title, body content, metadata, permissions, etc.
salesforce.updateRecord
    Description: Updates a record in Salesforce
    Parameters:
               objectType (required, string): Type of Salesforce object (Lead, Contact,      Account, etc.)
               recordId (required, string): The ID of the record to update
               data (required, object): Fields to update with their new values
     Returns: Updated record object with confirmation

ツール説明はコンテキストウィンドウのスペースを大きく占有し、応答時間とコストを増加させます。エージェントが数千のツールに接続されている場合、リクエストを読む前に数十万のトークンを処理する必要があります。

2. 中間ツールの結果は追加のトークンを消費します

ほとんどのMCPクライアントはモデルが直接MCPツールを呼び出すことを許可しています。例えば、エージェントに「Googleドライブから会議の書き起こしをダウンロードして、Salesforceリードに添付してください」と頼むかもしれません。

モデルは以下の判断を行います:

TOOL CALL: gdrive.getDocument(documentId: "abc123")
        → returns "Discussed Q4 goals...\n[full transcript text]"
           (loaded into model context)

TOOL CALL: salesforce.updateRecord(
			objectType: "SalesMeeting",
			recordId: "00Q5f000001abcXYZ",
  			data: { "Notes": "Discussed Q4 goals...\n[full transcript text written out]" }
		)
		(model needs to write entire transcript into context again)

すべての中間結果はモデルを通過しなければなりません。この例では、通話の全記録が2回流れます。2時間の営業会議なら、追加で50,000トークンを処理することになるかもしれません。さらに大きな文書でもコンテキストウィンドウの制限を超え、ワークフローが壊れてしまうことがあります。

大規模な文書や複雑なデータ構造では、ツール呼び出し間でデータをコピーする際にモデルが誤りを犯しやすくなります。


MCPクライアントはツール定義をモデルのコンテキストウィンドウに読み込み、各ツール呼び出しと結果がモデルを通過するメッセージループをオーケストレーションします。

MCPによるコード実行はコンテキスト効率を向上させる

エージェントにとってコード実行環境が一般的になる中で、解決策としてMCPサーバーを直接のツール呼び出しではなくコードAPIとして提示することが挙げられます。エージェントはその後、MCPサーバーとやり取りするためのコードを書くことができます。このアプローチは両方の課題に対応しています。エージェントは必要なツールだけを読み込み、実行環境でデータを処理してから結果をモデルに返すことができるのです。

これを実現する方法はいくつかあります。一つの方法は、接続されたMCPサーバーから利用可能なすべてのツールのファイルツリーを生成することです。こちらはTypeScriptを使った実装です:

servers
├── google-drive
│   ├── getDocument.ts
│   ├── ... (other tools)
│   └── index.ts
├── salesforce
│   ├── updateRecord.ts
│   ├── ... (other tools)
│   └── index.ts
└── ... (other servers)

そして各ツールはファイルに対応しています。例えば:

// ./servers/google-drive/getDocument.ts
import { callMCPTool } from "../../../client.js";

interface GetDocumentInput {
  documentId: string;
}

interface GetDocumentResponse {
  content: string;
}

/* Read a document from Google Drive */
export async function getDocument(input: GetDocumentInput): Promise<GetDocumentResponse> {
  return callMCPTool<GetDocumentResponse>('google_drive__get_document', input);
}

Google Driveのドキュメントを取得するためのMCPツール呼び出しラッパー関数

上記の Google Drive to Salesforce の例は、次のコードになります。

// Read transcript from Google Docs and add to Salesforce prospect
import * as gdrive from './servers/google-drive';
import * as salesforce from './servers/salesforce';

const transcript = (await gdrive.getDocument({ documentId: 'abc123' })).content;
await salesforce.updateRecord({
  objectType: 'SalesMeeting',
  recordId: '00Q5f000001abcXYZ',
  data: { Notes: transcript }
});

エージェントがコード実行環境を持つ場合、このようなコードを生成して実行することで、複雑なワークフローを構築可能

エージェントはファイルシステムを探索することでツールを発見します。具体的には、./servers/ ディレクトリをリストして利用可能なサーバー(google-drive や salesforce など)を見つけ、必要な特定のツールファイル(getDocument.ts や updateRecord.ts など)を読み込んでそれぞれのツールのインターフェースを理解します。これにより、エージェントは現在のタスクに必要な定義だけを読み込むことができます。この方法により、トークン使用量は 150,000 トークンから 2,000 トークンに削減され、時間とコストの節約率は 98.7% となります。

Cloudflare[3]は同様の発見を発表し、MCP でのコード実行を「コードモード」と呼びました。核心的な洞察は同じで、LLM はコードを書くのが得意であり、開発者はこの強みを活かして MCP サーバーとより効率的にやり取りするエージェントを構築すべきだということです。

MCP を使用したコード実行の利点

MCP を使用したコード実行により、オンデマンドでツールを読み込み、モデルに到達する前にデータをフィルタリングし、複雑なロジックを 1 つのステップで実行することで、エージェントはコンテキストをより効率的に使用できるようになります。このアプローチを使用すると、セキュリティと状態管理の利点もあります。

段階的な開示

モデルはファイルシステムをナビゲートするのが得意です。ツールをファイルシステム上のコードとして提示すると、モデルはツール定義をすべて前もって読み取るのではなく、オンデマンドで読み取ることができます。

あるいは、search_tools ツールをサーバーに追加して、関連する定義を見つけることもできます。たとえば、上で使用した仮想の Salesforce サーバーを操作する場合、エージェントは「salesforce」を検索し、現在のタスクに必要なツールのみをロードします。 search_tools ツールに詳細レベル パラメーターを含めると、エージェントが必要な詳細レベル (名前のみ、名前と説明、スキーマを含む完全な定義など) を選択できるようになり、エージェントがコンテキストを保存してツールを効率的に検索できるようになります。

コンテキスト効率の良いツールの結果

大規模なデータセットを操作する場合、エージェントは結果を返す前にコードで結果をフィルタリングおよび変換できます。 10,000 行のスプレッドシートをフェッチすることを検討してください:

// Without code execution - all rows flow through context
TOOL CALL: gdrive.getSheet(sheetId: 'abc123')
        → returns 10,000 rows in context to filter manually

// With code execution - filter in the execution environment
const allRows = await gdrive.getSheet({ sheetId: 'abc123' });
const pendingOrders = allRows.filter(row => 
  row["Status"] === 'pending'
);
console.log(`Found ${pendingOrders.length} pending orders`);
console.log(pendingOrders.slice(0, 5)); // Only log first 5 for review

MCPサーバーに直接「pendingだけ返して」とは頼まず、取得後にコード実行環境で処理

エージェントには 10,000 行ではなく 5 行が表示されます。同様のパターンは、コンテキスト ウィンドウを肥大化させることなく、集計、複数のデータ ソース間の結合、または特定のフィールドの抽出に対して機能します。

より強力でコンテキスト効率の高い制御フロー

ループ、条件分岐、およびエラー処理は、個々のツール呼び出しを連鎖させるのではなく、使い慣れたコード パターンを使用して実行できます。たとえば、Slack での展開通知が必要な場合、エージェントは次のように記述できます。

let found = false;
while (!found) {
  const messages = await slack.getChannelHistory({ channel: 'C123456' });
  found = messages.some(m => m.text.includes('deployment complete'));
  if (!found) await new Promise(r => setTimeout(r, 5000));
}
console.log('Deployment notification received');

このアプローチは、エージェント ループを通じて MCP ツールの呼び出しとスリープ コマンドを交互に行うよりも効率的です。

さらに、実行される条件ツリーを書き出すことができるため、「最初のトークンまでの時間」のレイテンシーも節約されます。モデルが if ステートメントを評価するのを待つのではなく、エージェントはコード実行環境にこれを行わせることができます。

プライバシー保護操作

エージェントが MCP でコード実行を使用する場合、デフォルトでは中間結果が実行環境に残ります。この方法では、エージェントはユーザーが明示的にログに記録したもの、または返したもののみを参照します。つまり、モデルと共有したくないデータは、モデルのコンテキストに入ることなくワークフローを流れることができます。

さらに機密性の高いワークロードの場合、エージェント ハーネス(実行環境の制御レイヤー)は機密データを自動的にトークン化(機密データ(PIIなど)をモデルに渡す前に安全なプレースホルダーに置き換える処理)できます。たとえば、顧客の連絡先の詳細をスプレッドシートから Salesforce にインポートする必要があるとします。エージェントは次のように書いています。

const sheet = await gdrive.getSheet({ sheetId: 'abc123' });
for (const row of sheet.rows) {
  await salesforce.updateRecord({
    objectType: 'Lead',
    recordId: row.salesforceId,
    data: { 
      Email: row.email,
      Phone: row.phone,
      Name: row.name
    }
  });
}
console.log(`Updated ${sheet.rows.length} leads`);

MCP クライアントは、モデルに到達する前にデータをインターセプトし、PII[4]をトークン化します。

// What the agent would see, if it logged the sheet.rows:
[
  { salesforceId: '00Q...', email: '[EMAIL_1]', phone: '[PHONE_1]', name: '[NAME_1]' },
  { salesforceId: '00Q...', email: '[EMAIL_2]', phone: '[PHONE_2]', name: '[NAME_2]' },
  ...
]

その後、データが別の MCP ツール呼び出しで共有されると、MCP クライアントでのルックアップによってトークン化が解除されます。実際のメールアドレス、電話番号、名前は Google スプレッドシートから Salesforce に流れますが、モデルを経由することはありません。これにより、エージェントが機密データを誤って記録したり処理したりすることが防止されます。これを使用して、データの送受信先を選択する決定的なセキュリティ ルールを定義することもできます。

状態の永続性とスキル

ファイルシステムにアクセスしてコードを実行すると、エージェントは操作全体にわたって状態を維持できます。エージェントは中間結果をファイルに書き込むことができ、作業を再開して進捗状況を追跡できるようになります。

const leads = await salesforce.query({ 
  query: 'SELECT Id, Email FROM Lead LIMIT 1000' 
});
const csvData = leads.map(l => `${l.Id},${l.Email}`).join('\n');
await fs.writeFile('./workspace/leads.csv', csvData);

// Later execution picks up where it left off
const saved = await fs.readFile('./workspace/leads.csv', 'utf-8');

エージェントは、独自のコードを再利用可能な関数として永続化することもできます。エージェントがタスク用に機能するコードを開発したら、その実装を将来の使用のために保存できます。

// In ./skills/save-sheet-as-csv.ts
import * as gdrive from './servers/google-drive';
export async function saveSheetAsCsv(sheetId: string) {
  const data = await gdrive.getSheet({ sheetId });
  const csv = data.map(row => row.join(',')).join('\n');
  await fs.writeFile(`./workspace/sheet-${sheetId}.csv`, csv);
  return `./workspace/sheet-${sheetId}.csv`;
}

// Later, in any agent execution:
import { saveSheetAsCsv } from './skills/save-sheet-as-csv';
const csvPath = await saveSheetAsCsv('abc123');

これは、スキル、再利用可能な命令、スクリプト、および特殊なタスクのパフォーマンスを向上させるモデルのリソースのフォルダーの概念と密接に結びついています。これらの保存された関数に SKILL.md ファイルを追加すると、モデルが参照して使用できる構造化されたスキルが作成されます。これにより、時間の経過とともに、エージェントはより高いレベルの機能のツールボックスを構築し、最も効果的に機能するために必要な足場を進化させることができます。


関係図を簡単に表してみました

コードの実行には独自の複雑さが伴うことに注意してください。エージェントが生成したコードを実行するには、適切なサンドボックス、リソース制限、監視を備えた安全な実行環境が必要です。これらのインフラストラクチャ要件により、直接ツール呼び出しでは回避できる操作上のオーバーヘッドとセキュリティに関する考慮事項が追加されます。コード実行のメリット (トークン コストの削減、レイテンシの短縮、ツール構成の改善) は、これらの実装コストと比較検討する必要があります。

所感

MCPサーバーとLLMモデルを直接繋げないで、間にコード実行環境を挟むことでLLMモデルに対して必要最低限のコンテキストを渡すことができるというアイデアはとても良いと思います。

セキュリティ面や実行進捗の状態が保存できるのもとても魅力的です。

この仕組みを作るうえで、実行環境の設計力が問われるのかなと感じました。

脚注
  1. MCPサーバーが外部ツールやサービスに問い合わせた後に返すレスポンス(結果データ) ↩︎

  2. LLMモデルが一度に覚えておける記憶範囲で、単位はトークン数で表される ↩︎

  3. CDN(コンテンツデリバリーネットワーク)やDDoS攻撃対策、SSL証明書の提供など、Webサイトの快適さと安全性を保つための機能を幅広く提供している企業 ↩︎

  4. Personally Identifiable Information(個人を特定できる情報) の略 ↩︎

ヘッドウォータース

Discussion