Code execution with MCP: Building more efficient agents

MCPとコード実行で効率的なAIエージェントを構築する

このブログ記事を初学者にもわかりやすく解説します。かなり長くなりますが、丁寧に説明していきます。


1. この記事の概要

一言で言うと: AIエージェントが外部ツール(Google Drive、Salesforceなど)と連携する際、従来の方法だと大量のトークン(≒ 処理コスト)を消費してしまう。コード実行という手法を使えば、これを劇的に削減できる、という内容です。

この記事の核心は、 「AI が得意なことは AI に、プログラムが得意なことはプログラムに」 という考え方です。

2. 前提知識:MCPとは何か?

MCPの基本概念

MCP(Model Context Protocol) は、AI エージェントと外部システムを接続するための「共通規格」です。

たとえ話で説明すると:

従来のやり方は、家電製品ごとに専用のリモコンが必要な状態に似ています。テレビ用、エアコン用、照明用…とバラバラでした。MCP は「学習リモコン」のようなもので、一度設定すれば様々な機器を操作できます。

【従来】
AIエージェント ─専用コネクタ→ Google Drive
AIエージェント ─専用コネクタ→ Salesforce
AIエージェント ─専用コネクタ→ Slack
(それぞれ個別に開発が必要)

【MCP導入後】
AIエージェント ─MCP→ Google Drive
              ├MCP→ Salesforce
              └MCP→ Slack
(共通プロトコルで全部つながる)

3. 問題点:従来のツール呼び出しの課題

MCP は便利ですが、接続するツールが増えると2つの大きな問題が発生します。

問題① ツール定義がコンテキストウィンドウを圧迫する

コンテキストウィンドウとは: AIモデルが一度に処理できるテキストの量(トークン数)の上限です。

AIエージェントがツールを使うには、まず「どんなツールがあるか」をモデルに教える必要があります。これがツール定義です。

例:Google Driveのツール定義
────────────────────────────
gdrive.getDocument
  説明: Google Driveからドキュメントを取得する
  パラメータ:
    - documentId(必須、文字列): 取得するドキュメントのID
    - fields(任意、文字列): 返す特定のフィールド
  戻り値: タイトル、本文、メタデータなどを含むドキュメントオブジェクト

1つのツール定義だけでも結構な量のテキストです。これが数百〜数千ツールになると、ユーザーの質問を読む前に何十万トークンも消費してしまいます。

具体的なイメージ:

【コンテキストウィンドウの使われ方】
┌─────────────────────────────────────┐
│ ツール定義 1,000個分               │ ← 150,000トークン(コストと時間がかかる)
│ (Google Drive, Salesforce,        │
│  Slack, GitHub, Notion...)        │
├─────────────────────────────────────┤
│ ユーザーの質問                     │ ← 100トークン
├─────────────────────────────────────┤
│ AIの回答                           │
└─────────────────────────────────────┘

問題② 中間結果がトークンを消費する

もう一つの問題は、ツールの実行結果(中間結果)がすべてモデルを通過することです。

例:会議の議事録をGoogle DriveからSalesforceに転記する

ユーザーの依頼:
「Google Driveの会議議事録をダウンロードして、Salesforceのリードに添付して」

【従来のやり方】
──────────────────────────────────────────────────────────

ステップ1: Google Driveからドキュメント取得
TOOL CALL: gdrive.getDocument(documentId: "abc123")
    → 結果: "Q4の目標について議論...[議事録全文]"
    → この全文がAIのコンテキストに読み込まれる(例:50,000トークン)

ステップ2: Salesforceに書き込み
TOOL CALL: salesforce.updateRecord(...)
    → AIが議事録全文を「もう一度書き出す」必要がある(また50,000トークン)

合計:100,000トークン消費!

2 時間の会議の議事録だと、これだけで10万トークン近く使ってしまう可能性があります。

図解:従来のアーキテクチャ

┌─────────────────────────────────────────────────────────┐
│                     LLM(AIモデル)                      │
│  ┌─────────────────────────────────────────────────┐   │
│  │ コンテキストウィンドウ                          │   │
│  │ ・全ツール定義                                  │   │
│  │ ・ユーザーの質問                                │   │
│  │ ・ツール実行結果(全文)← これが問題!          │   │
│  └─────────────────────────────────────────────────┘   │
└────────────────────┬────────────────────────────────────┘
                     │ ↑↓ すべてのデータが通過
            ┌────────┴────────┐
            │   MCPクライアント │
            └────────┬────────┘
          ┌──────────┼──────────┐
          ↓          ↓          ↓
    ┌──────────┐ ┌──────────┐ ┌──────────┐
    │ Google   │ │Salesforce│ │  Slack   │
    │  Drive   │ │   MCP    │ │   MCP    │
    │   MCP    │ │  Server  │ │  Server  │
    └──────────┘ └──────────┘ └──────────┘

4. 解決策:コード実行と MCP の組み合わせ

基本アイデア

従来は「AIが直接ツールを呼び出す」方式でしたが、新しいアプローチでは「AIがコードを書いて、そのコードがツールを呼び出す」方式を採用します。

従来の方式(直接ツール呼び出し):

AI → [ツール1を呼ぶ] → 結果をAIに返す → [ツール2を呼ぶ] → 結果をAIに返す

新しい方式(コード実行):

AI → [コードを書く] → コード実行環境 → [ツール1→ツール2] → 最終結果だけAIに返す

実装方法:ツールをファイルシステムとして提示

MCPサーバーのツールを、ファイルシステム上のコードファイルとして表現します。

servers/(ディレクトリ)
├── google-drive/
│   ├── getDocument.ts    ← ドキュメント取得の関数
│   ├── getSheet.ts       ← スプレッドシート取得の関数
│   └── index.ts
├── salesforce/
│   ├── updateRecord.ts   ← レコード更新の関数
│   ├── query.ts          ← クエリ実行の関数
│   └── index.ts
└── slack/
    ├── postMessage.ts
    └── index.ts

各ファイルには、ツールを呼び出すためのTypeScript関数が定義されています。

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

interface GetDocumentInput {
  documentId: string;
}

interface GetDocumentResponse {
  content: string;
}

/* Google Driveからドキュメントを読み取る */
export async function getDocument(
  input: GetDocumentInput,
): Promise<GetDocumentResponse> {
  return callMCPTool<GetDocumentResponse>("google_drive__get_document", input);
}

実際の使用例

先ほどの「議事録を Google Drive から Salesforce に転記」の例が、こうなります。

// AIが生成するコード
import * as gdrive from "./servers/google-drive";
import * as salesforce from "./servers/salesforce";

// Google Driveから議事録を取得
const transcript = (await gdrive.getDocument({ documentId: "abc123" })).content;

// Salesforceに書き込み(変数を渡すだけ!)
await salesforce.updateRecord({
  objectType: "SalesMeeting",
  recordId: "00Q5f000001abcXYZ",
  data: { Notes: transcript },
});

重要なポイント:

  • 議事録のデータ(transcript)はコード実行環境の中だけで流れる
  • AI のコンテキストには議事録全文が入らない
  • AI は「どういうコードを書くか」だけ考えればいい

効果:トークン消費を98.7%削減

【従来の方式】
全ツール定義をロード:150,000トークン

【コード実行方式】
必要なツールのファイルだけ読む:2,000トークン

削減率:98.7%

5. コード実行方式のメリット

メリット① プログレッシブディスクロージャー(段階的な情報開示)

AI はファイルシステムの探索が得意です。必要なツールだけを「必要なときに」読み込めます。

【従来】起動時に全ツール定義をロード
─────────────────────────────
1,000個のツール定義 → 一気にコンテキストへ

【コード実行方式】必要なときに必要なものだけ
─────────────────────────────
1. ./servers/ ディレクトリを一覧表示
2. 「今回はGoogle DriveとSalesforceが必要だな」
3. その2つのフォルダだけ読む
4. さらにその中で必要な関数のファイルだけ読む

また、search_toolsというツール検索機能を追加することで、さらに効率的にツールを見つけられます。

メリット② コンテキスト効率的なデータ処理

大量のデータを扱う場合、コード実行環境内でフィルタリングしてから結果を返せます。

例:10,000行のスプレッドシートから「保留中」の注文だけ取得

// 従来方式:10,000行すべてがAIのコンテキストに入る
TOOL CALL: gdrive.getSheet(sheetId: 'abc123')10,000行がコンテキストに... 💀

// コード実行方式:フィルタリングしてから返す
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)); // 最初の5件だけ表示

// AIは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)); // 5秒待機
}
console.log("Deployment notification received");

従来方式だと、このループの各反復で AI モデルを呼び出す必要があり、非効率でした。

メリット④ プライバシー保護

中間データはコード実行環境内に留まるため、AIに見せたくないデータを保護できます。

例:顧客の個人情報をGoogle Driveから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クライアントが自動的に個人情報をトークン化(匿名化)できます。

// AIが見るデータ(トークン化済み)
[
  { salesforceId: '00Q...', email: '[EMAIL_1]', phone: '[PHONE_1]', name: '[NAME_1]' },
  { salesforceId: '00Q...', email: '[EMAIL_2]', phone: '[PHONE_2]', name: '[NAME_2]' },
  ...
]

実際のデータはGoogle Drive → Salesforceに流れますが、AIのコンテキストには入りません。

メリット⑤ 状態の永続化とスキル化

コード実行環境ではファイルシステムにアクセスできるため、中間結果を保存して後で使うことができます。

// データを取得してCSVに保存
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);

// 後で再開するときに読み込む
const saved = await fs.readFile("./workspace/leads.csv", "utf-8");

さらに、よく使う処理を「スキル」として保存し、再利用できます。

// スキルとして保存:./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`;
}

// 後で簡単に使える
import { saveSheetAsCsv } from "./skills/save-sheet-as-csv";
const csvPath = await saveSheetAsCsv("abc123");