# Gemini + MCP デモスクリプト: 完全ガイド ## はじめに 「Claude Agent SDKのようにGeminiでも外部ツールを簡単に使えるだろうか?」 この単純な疑問から始まったプロジェクトが、v1からv10までの旅へと繋がりました。本ドキュメントは、その全ての過程を統合した、一つの連続的なストーリーです。 ## 動機と背景 ### Claude Agent SDKとの出会い まず、事の発端はClaudeでした。Anthropic社の公開したClaude Agent SDKは、LLMが外部ツールを簡単に使えるようにデザインされていました。 ```typescript const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY, }); const model = client.getModel('claude-opus-4-1'); ``` このシンプルなコードで、Claude AIが様々なツールを呼び出せるようになる。その優雅さと利便性に感銘を受けました。 ### 疑問と好奇心 その時ふと思ったのです: **「Geminiでも同じことができるだろうか?」** GoogleはGeminiという強力なAIモデルを提供しており、その公式SDKも存在します。しかし、Claude Agent SDKのような統合されたツール連携の仕組みは明確には存在していませんでした。 しかし同時に別の世界があります。Model Context Protocol(MCP)という、LLMとツールの連携を標準化しようとするオープンプロトコルが存在しているのです。 ### プロジェクトの目的 このプロジェクトの目的は単純ですが、実行することで多くを学べます: 1. **MCPプロトコルの理解** - JSON-RPC 2.0ベースの標準を学ぶ 2. **Gemini SDKの探索** - Function Callingメカニズムを理解する 3. **実装と統合** - MCPサーバーとGeminiを実際に統合する 4. **比較分析** - Claude Agent SDKとの違いを明確にする 5. **学習価値** - 内部動作を完全に理解する --- ## Model Context Protocol (MCP) の基礎 ### MCPとは何か MCPはAnthropicとその他のプロジェクトが協力して開発した、LLMが外部ツールと通信するための標準プロトコルです。 以下のような重要な特性があります: - **標準化**: JSON-RPC 2.0をベースにした統一仕様 - **汎用性**: Claude、Gemini、その他のLLMで使用可能 - **柔軟性**: 様々なトランスポート方式をサポート - **拡張性**: カスタムツールの追加が容易 ### MCP の仕組み MCPの基本的な通信フローは以下の通りです: ``` LLM (Gemini) ↓ ツール呼び出し要求 ↓ (JSON-RPC) MCP Client ↓ MCP Server ↓ 実際のツール実行 ツール (外部API、データベース等) ↓ 結果を返す MCP Client ↓ LLM (結果を受け取る) ``` ### MCPの主要メソッド MCPにはいくつかの重要なメソッドがあります: #### 1. tools/list - 利用可能なツール一覧の取得 ```json { "jsonrpc": "2.0", "method": "tools/list", "id": 1 } ``` このメソッドで、MCPサーバーが提供する全てのツールの一覧を取得します。各ツールには以下の情報が含まれます: - **name** - ツール名(例:search_docs) - **description** - ツールの説明 - **inputSchema** - 入力パラメータのJSON Schema定義 #### 2. tools/call - ツール実行 ```json { "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "search_docs", "arguments": {"query": "edge functions"} }, "id": 2 } ``` このメソッドで実際にツールを実行し、その結果を取得します。 ### MCPのトランスポート方式 MCPは複数のトランスポート方式をサポートしています: #### HTTP/HTTPS トランスポート ``` POST https://mcp.vercel.com Content-Type: application/json { "jsonrpc": "2.0", "method": "tools/list", "id": 1 } ``` **特徴**: - インターネット上で通信可能 - RESTful APIのように扱える - 認証トークンをヘッダーに含める - 最も一般的で実装しやすい #### Stdio トランスポート ```bash mcp-server-filesystem ``` **特徴**: - 標準入出力を使用 - ローカルプロセス間通信 - セキュリティが高い - サーバーレスアーキテクチャ向け ### MCPプロトコルの流れ 実際の利用シーンを考えると: ``` 1. MCP Clientが "tools/list" を送信 ↓ 2. MCP Serverが利用可能なツール一覧を返す ↓ 3. Clientがツール情報をLLMに伝える ↓ 4. ユーザーが「ドキュメント検索して」と要求 ↓ 5. LLMが "search_docs" ツール呼び出しを判断 ↓ 6. Clientが "tools/call" で実行要求 ↓ 7. Serverがツールを実行して結果を返す ↓ 8. Clientが結果をLLMに返す ↓ 9. LLMが最終的な応答を生成 ↓ 10. ユーザーが自然言語の回答を受け取る ``` --- ## Gemini SDK の Function Calling メカニズム ### Gemini APIの基本 GeminiはGoogleが提供する最新のAIモデルで、以下のような特徴があります: - **強力な推論能力** - 複雑な問題解決が得意 - **Function Calling** - 外部ツールを直感的に呼び出す - **複数モデルオプション** - 用途に応じた選択が可能 - **コスト効率** - 多くのユースケースで手頃な価格 ### Gemini SDKにおけるツール(Tool)インターフェース Gemini APIでツール統合をするには、`Tool`インターフェースを使用します。このインターフェースには複数のタイプがあります。 #### 1. functionDeclarations - 基本的な関数呼び出し ```typescript interface Tool { functionDeclarations?: FunctionDeclaration[]; } interface FunctionDeclaration { name: string; description?: string; parametersJsonSchema?: Schema; } interface Schema { type: string; // "object", "string", "array" など properties?: Record; required?: string[]; description?: string; } ``` これがMCP連携で最も重要な部分です。MCPからのツール定義をこのフォーマットに変換します。 #### 2. codeExecution - コード実行環境 ```typescript interface Tool { codeExecution?: { // Geminiが安全にコードを実行できる環境 }; } ``` Geminiが直接Pythonなどのコードを実行し、その結果を取得できます。 #### 3. googleSearch - Googleウェブ検索 ```typescript interface Tool { googleSearch?: { // Geminiがリアルタイムでウェブ検索を実行 }; } ``` 最新の情報をGoogle検索で取得できます。 #### 4. googleSearchRetrieval - より高度な検索 ```typescript interface Tool { googleSearchRetrieval?: { // より詳細な検索結果処理 }; } ``` #### 5. googleMaps - Google Maps統合 ```typescript interface Tool { googleMaps?: { // 地図データへのアクセス }; } ``` #### 6. enterpriseWebSearch - エンタープライズ向け検索 ```typescript interface Tool { enterpriseWebSearch?: { // 企業向けの高度な検索機能 }; } ``` #### 7. retrieval - カスタムデータ検索 ```typescript interface Tool { retrieval?: { // カスタムナレッジベース内での検索 }; } ``` #### 8. urlContext - URL コンテキスト ```typescript interface Tool { urlContext?: { // 指定されたURLのコンテンツをコンテキストとして使用 }; } ``` ### MCPとGemini SDKの統合戦略 MCPサーバーが提供するツールをGemini APIで使用するには、以下のステップが必要です: **ステップ1: MCPからツール情報を取得** ```typescript const response = await fetch('https://mcp.vercel.com', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', method: 'tools/list', id: 1 }) }); const data = await response.json(); const mcpTools = data.result.tools; ``` **ステップ2: MCPツールをGemini FunctionDeclarationに変換** ```typescript function mcpToolToGeminFunction(tool) { return { name: tool.name, description: tool.description, parametersJsonSchema: { type: tool.inputSchema.type || 'object', properties: tool.inputSchema.properties || {}, required: tool.inputSchema.required || [] } }; } const geminiTools = mcpTools.map(mcpToolToGeminFunction); ``` **ステップ3: Geminiモデルを初期化** ```typescript const model = genAI.getGenerativeModel({ model: 'gemini-2.0-flash-exp', tools: [{ functionDeclarations: geminiTools }] }); ``` **ステップ4: ツール呼び出しを処理** ```typescript let result = await chat.sendMessage(userMessage); let response = result.response; while (response.functionCalls && response.functionCalls.length > 0) { const functionResults = []; for (const fc of response.functionCalls) { const result = await mcpAdapter.callTool(fc.name, fc.args); functionResults.push({ functionResponse: { name: fc.name, response: { result } } }); } result = await chat.sendMessage(functionResults); response = result.response; } ``` --- ## 技術スタックと実装設計 ### 全体アーキテクチャ プロジェクト全体の構成は以下の通りです: ``` ┌─────────────────────────────────────────────────────────────┐ │ ユーザー層 │ │ (コマンドライン入力) │ └────────────────────────┬────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ GeminiMCPClient (メインクラス) │ │ - chat(): ユーザーメッセージを処理 │ │ - initialize(): MCP/Geminiを初期化 │ │ - handleFunctionCalls(): ツール呼び出しを処理 │ └──┬────────────────────────────────────────────────────────┬─┘ │ │ ▼ ▼ ┌─────────────────────────────┐ ┌──────────────────────────┐ │ MCPAdapter │ │ Gemini SDK │ │ │ │ │ │ - getTools() │ │ - GoogleGenAI │ │ - callTool() │ │ - GenerativeModel │ │ │ │ - startChat() │ └──┬───────────────────────────┘ └──────┬───────────────────┘ │ │ ▼ ▼ ┌──────────────────────┐ ┌──────────────────────────┐ │ Vercel MCP Server │ │ SchemaConverter │ │ (HTTP エンドポイント)│ │ │ │ https://mcp.vercel │ │ - mcpToolToGeminiFunc() │ │ .com │ │ - convertTools() │ └──────────────────────┘ └──────────────────────────┘ ``` ### 主要コンポーネント #### 1. MCPAdapter 役割:MCP サーバーとの通信を処理 ```typescript class MCPAdapter { async getTools(): Promise async callTool(name: string, args: any): Promise } ``` #### 2. SchemaConverter 役割:MCP スキーマから Gemini スキーマへの変換 ```typescript class SchemaConverter { static mcpToolToGeminiFunction(tool: MCPTool): FunctionDeclaration static convertTools(mcpTools: MCPTool[]): FunctionDeclaration[] } ``` #### 3. GeminiMCPClient 役割:全体の調整とユーザーとの対話 ```typescript class GeminiMCPClient { async initialize(): Promise async chat(userMessage: string): Promise private async handleFunctionCalls(functionCalls: any[]): Promise } ``` ### 実装フェーズ プロジェクトを段階的に実装するスケジュール: **フェーズ1: 基盤構築** (v4-v5) - Gemini SDK のセットアップ - MCP サーバーの選定(Vercel MCP) - 必要な依存関係の確認 **フェーズ2: コア実装** (v6) - MCPAdapter の実装 - SchemaConverter の実装 - GeminiMCPClient の実装 - ツール呼び出しループの実装 **フェーズ3: テスト** (v7) - 公開ツールのテスト - 認証が必要なツールのテスト - マルチターン会話のテスト - パフォーマンス測定 **フェーズ4: 最適化と検証** (v8-v10) - Claude Agent SDK との比較 - 制限と改善策の検討 - 本番環境への対応 - 最終的な学習と反省 --- ## Vercel MCP Server のセットアップと活用 ### Vercel MCP Server とは Vercel社が提供する公開MCPサーバーは、以下の特徴を持ちます: - **公開エンドポイント** - インターネット経由で誰でもアクセス可能 - **複数のツール** - ドキュメント検索からプロジェクト管理まで豊富 - **認証対応** - 認証可能と非認証でも利用可能なツールを提供 - **本番レディ** - 実際に使用できるプロダクションレベルのサーバー ### エンドポイント情報 **HTTP エンドポイント** ``` https://mcp.vercel.com ``` このエンドポイントへのHTTP POST リクエストで MCP 通信を実行します。 **リクエストフォーマット** ```json { "jsonrpc": "2.0", "method": "tools/list", "id": 1, "headers": { "Content-Type": "application/json", "Authorization": "Bearer YOUR_VERCEL_TOKEN" // (オプション) } } ``` ### 認証とトークン Vercel MCP Server を最大限活用するには、Vercel トークンの取得が推奨されます。 **Vercelトークンの取得手順** 1. Vercel Dashboard にアクセス: https://vercel.com/account/tokens 2. 「Create Token」をクリック 3. トークンをコピーしておく 4. `.env` ファイルに追加: ```bash VERCEL_TOKEN=xxx_your_token_xxx ``` **公開ツール vs 認証ツール** Vercel MCP Server は以下のようにツールを分けています: 公開ツール(認証不要): - `search_docs` - Vercel ドキュメント検索 - `get_frameworks` - サポートされているフレームワーク一覧 - `get_guides` - ガイド記事一覧 認証ツール(Vercel トークン必須): - `list_projects` - ユーザーの全プロジェクト表示 - `get_project` - 特定プロジェクト情報取得 - `get_deployments` - プロジェクトのデプロイメント一覧 - `get_deployment_logs` - デプロイメントログ取得 - `get_project_env` - プロジェクト環境変数表示 ### ツールカテゴリ分類 #### 情報取得ツール ```typescript interface DocumentationTools { search_docs: { description: "Vercel documentation を検索" inputSchema: { properties: { query: { type: "string" } }, required: ["query"] } } } ``` #### プロジェクト管理ツール ```typescript interface ProjectTools { list_projects: { description: "Vercel プロジェクト一覧を取得" inputSchema: { type: "object", properties: {}, required: [] } }, get_project: { description: "特定プロジェクトの詳細情報" inputSchema: { properties: { projectId: { type: "string" } }, required: ["projectId"] } } } ``` #### デプロイメント関連ツール ```typescript interface DeploymentTools { get_deployments: { description: "プロジェクトのデプロイメント一覧" inputSchema: { properties: { projectId: { type: "string" }, limit: { type: "number" } }, required: ["projectId"] } }, get_deployment_logs: { description: "デプロイメントログを取得" inputSchema: { properties: { projectId: { type: "string" }, deploymentId: { type: "string" } }, required: ["projectId", "deploymentId"] } } } ``` ### セキュリティに関する注意 Vercel MCP Server を使用する際のセキュリティについて: **1. トークン管理** ```bash # .env ファイル GEMINI_API_KEY=AIza... # Google AI Studio から取得 VERCEL_TOKEN=xxx... # Vercel Dashboard から取得 ``` **重要**: これらの認証情報を以下のように保護してください: - `.gitignore` に `.env` を追加して Git リポジトリにコミットしない - 本番環境ではシークレット管理システム(AWS Secrets Manager、HashiCorp Vault など)を使用 - API キーを定期的にローテーション - 不要になったトークンは即座に無効化 **2. スコープの最小化** Vercel トークンは必要最小限のスコープで作成します: - 不要な権限は付与しない - 読み取り専用にできる場合はそうする - リード専用トークンの使用を優先 **3. ネットワークセキュリティ** ```typescript // HTTPS を強制 const endpoint = 'https://mcp.vercel.com'; // OK // const endpoint = 'http://mcp.vercel.com'; // NG // ヘッダー検証 headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` } ``` ### トラブルシューティング **問題1: 401 Unauthorized エラー** ``` Error: MCP tools/list failed: 401 Unauthorized ``` 原因:トークンがない、期限切れ、または無効 解決: ```bash # Vercel Dashboard で新しいトークンを生成 # https://vercel.com/account/tokens # .env を更新 VERCEL_TOKEN=new_token_here ``` **問題2: 接続タイムアウト** 原因:ネットワーク遅延またはサーバーダウン 解決: ```typescript const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 30000); try { const response = await fetch(endpoint, { signal: controller.signal, // ... }); } finally { clearTimeout(timeout); } ``` --- ## 実装コード では、実際の実装に進みます。 ### プロジェクト初期設定 **ステップ1: プロジェクトディレクトリの作成** ```bash mkdir gemini-mcp-demo cd gemini-mcp-demo npm init -y ``` **ステップ2: 依存関係のインストール** ```bash npm install @google/genai dotenv npm install -D typescript @types/node tsx ``` **ステップ3: TypeScript 設定** `tsconfig.json`: ```json { "compilerOptions": { "target": "ES2022", "module": "ES2022", "moduleResolution": "node", "esModuleInterop": true, "strict": true, "skipLibCheck": true, "outDir": "./dist" }, "include": ["src/**/*"] } ``` **ステップ4: パッケージ設定** `package.json` (スクリプト部分のみ): ```json { "name": "gemini-mcp-demo", "version": "1.0.0", "type": "module", "scripts": { "start": "tsx src/index.ts", "dev": "tsx watch src/index.ts" }, "dependencies": { "@google/genai": "^1.0.0", "dotenv": "^16.0.0" }, "devDependencies": { "@types/node": "^20.0.0", "tsx": "^4.0.0", "typescript": "^5.0.0" } } ``` **ステップ5: 環境変数設定** `.env`: ```bash GEMINI_API_KEY=AIza...your_key_here VERCEL_TOKEN=xxx...your_token_here ``` **ステップ6: ファイル構成** ``` gemini-mcp-demo/ ├── package.json ├── tsconfig.json ├── .env ├── .gitignore └── src/ ├── types.ts ├── mcp-adapter.ts ├── schema-converter.ts ├── gemini-mcp-client.ts └── index.ts ``` ### 型定義の実装 `src/types.ts`: ```typescript export interface MCPTool { name: string; description?: string; inputSchema: { type: string; properties?: Record; required?: string[]; }; } export interface MCPToolsListResponse { jsonrpc: string; id: number | string; result: { tools: MCPTool[]; }; } export interface MCPToolCallRequest { jsonrpc: string; method: string; params: { name: string; arguments: Record; }; id: number | string; } export interface MCPToolCallResponse { jsonrpc: string; id: number | string; result?: { content: Array<{ type: string; text: string; }>; }; error?: { code: number; message: string; }; } ``` ### MCP アダプターの実装 `src/mcp-adapter.ts`: ```typescript import type { MCPTool, MCPToolsListResponse, MCPToolCallRequest, MCPToolCallResponse } from './types.js'; export class MCPAdapter { private endpoint: string; private token?: string; private requestId = 0; constructor(endpoint: string, token?: string) { this.endpoint = endpoint; this.token = token; } async getTools(): Promise { const response = await fetch(this.endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(this.token && { Authorization: `Bearer ${this.token}` }) }, body: JSON.stringify({ jsonrpc: '2.0', method: 'tools/list', id: ++this.requestId }) }); if (!response.ok) { throw new Error(`MCP tools/list failed: ${response.statusText}`); } const data: MCPToolsListResponse = await response.json(); return data.result.tools; } async callTool( name: string, args: Record ): Promise { const request: MCPToolCallRequest = { jsonrpc: '2.0', method: 'tools/call', params: { name, arguments: args }, id: ++this.requestId }; const response = await fetch(this.endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(this.token && { Authorization: `Bearer ${this.token}` }) }, body: JSON.stringify(request) }); if (!response.ok) { throw new Error(`MCP tools/call failed: ${response.statusText}`); } const data: MCPToolCallResponse = await response.json(); if (data.error) { throw new Error( `MCP tool error: ${data.error.message} (code: ${data.error.code})` ); } const textContent = data.result?.content ?.filter(c => c.type === 'text') .map(c => c.text) .join('\n') || ''; return textContent; } } ``` ### スキーマコンバーターの実装 `src/schema-converter.ts`: ```typescript import type { FunctionDeclaration } from '@google/genai'; import type { MCPTool } from './types.js'; export class SchemaConverter { static mcpToolToGeminiFunction(tool: MCPTool): FunctionDeclaration { return { name: tool.name, description: tool.description || `Execute ${tool.name}`, parametersJsonSchema: { type: tool.inputSchema.type || 'object', properties: tool.inputSchema.properties || {}, required: tool.inputSchema.required || [] } }; } static convertTools(mcpTools: MCPTool[]): FunctionDeclaration[] { return mcpTools.map(tool => this.mcpToolToGeminiFunction(tool)); } } ``` ### Gemini MCP クライアントの実装 `src/gemini-mcp-client.ts`: ```typescript import { GoogleGenAI } from '@google/genai'; import type { GenerativeModel, FunctionCall } from '@google/genai'; import { MCPAdapter } from './mcp-adapter.js'; import { SchemaConverter } from './schema-converter.js'; export class GeminiMCPClient { private genAI: GoogleGenAI; private model: GenerativeModel; private mcpAdapter: MCPAdapter; private chatHistory: any[] = []; constructor( geminiApiKey: string, mcpEndpoint: string, mcpToken?: string ) { this.genAI = new GoogleGenAI({ apiKey: geminiApiKey }); this.mcpAdapter = new MCPAdapter(mcpEndpoint, mcpToken); this.model = null as any; } async initialize(): Promise { console.log('Fetching MCP tools...'); const mcpTools = await this.mcpAdapter.getTools(); console.log(`Found ${mcpTools.length} MCP tools`); const functionDeclarations = SchemaConverter.convertTools(mcpTools); this.model = this.genAI.getGenerativeModel({ model: 'gemini-2.0-flash-exp', tools: [{ functionDeclarations }] }); console.log('Gemini model initialized with MCP tools'); } async chat(userMessage: string): Promise { console.log(`\nUser: ${userMessage}`); this.chatHistory.push({ role: 'user', parts: [{ text: userMessage }] }); const chat = this.model.startChat({ history: this.chatHistory.slice(0, -1) }); let result = await chat.sendMessage(userMessage); let response = result.response; while (response.functionCalls && response.functionCalls.length > 0) { console.log(`\nGemini wants to call ${response.functionCalls.length} function(s)`); const functionResults = await this.handleFunctionCalls( response.functionCalls ); result = await chat.sendMessage(functionResults); response = result.response; } const finalText = response.text?.() || 'No response'; console.log(`\nGemini: ${finalText}`); return finalText; } private async handleFunctionCalls( functionCalls: FunctionCall[] ): Promise { const results = []; for (const fc of functionCalls) { console.log(` Calling: ${fc.name}`); console.log(` Args: ${JSON.stringify(fc.args)}`); try { const result = await this.mcpAdapter.callTool( fc.name, fc.args as Record ); console.log(` Result: ${result.substring(0, 100)}...`); results.push({ functionResponse: { name: fc.name, response: { result } } }); } catch (error: any) { console.error(` Error: ${error.message}`); results.push({ functionResponse: { name: fc.name, response: { error: error.message } } }); } } return results; } } ``` ### メインの実行ファイル `src/index.ts`: ```typescript import { GeminiMCPClient } from './gemini-mcp-client.js'; import * as dotenv from 'dotenv'; dotenv.config(); async function main() { const client = new GeminiMCPClient( process.env.GEMINI_API_KEY!, 'https://mcp.vercel.com', process.env.VERCEL_TOKEN ); await client.initialize(); const response1 = await client.chat( 'Vercel documentation で edge functions について検索して' ); const response2 = await client.chat( '私の Vercel プロジェクト一覧を表示して' ); const response3 = await client.chat( '最新のデプロイメントの状態を教えて' ); } main().catch(console.error); ``` --- ## テストシナリオと検証 ### プロジェクトの実行 すべてを準備した後、実際に実行してみましょう: ```bash npm run dev ``` または ```bash npm start ``` ### テストシナリオ1: 公開ツールの使用(ドキュメント検索) 認証なしで使用可能なツールをテストします。 ```typescript import { GeminiMCPClient } from '../src/gemini-mcp-client.js'; import * as dotenv from 'dotenv'; dotenv.config(); async function testPublicTool() { const client = new GeminiMCPClient( process.env.GEMINI_API_KEY!, 'https://mcp.vercel.com' // Vercel token なしで実行 ); await client.initialize(); const response = await client.chat( 'Vercel documentation で "edge functions" について検索して要約して' ); console.log('\n=== Final Response ==='); console.log(response); } testPublicTool().catch(console.error); ``` 期待される出力: ``` Fetching MCP tools... Found 12 MCP tools Gemini model initialized with MCP tools User: Vercel documentation で "edge functions" について検索して要約して Gemini wants to call 1 function(s) Calling: search_docs Args: {"query":"edge functions"} Result: Edge Functions は Vercel の serverless 実行環境で、全世界のエッジネットワークで... Gemini: Edge Functions は Vercel が提供する serverless 関数実行環境です。 主な特徴は以下の通りです: 1. **グローバルエッジネットワーク**: ユーザーに最も近い場所で実行されるため遅延が最小化されます。 2. **高速なコールドスタート**: 従来の serverless 関数よりはるかに高速に起動します。 3. **制限されたランタイム**: Node.js の一部 API のみをサポートしており、ファイルシステムアクセスなどが制限されます。 4. **ストリーミング応答**: リアルタイムストリーミング応答をサポートしています。 主に A/B テスト、個人化、認証、リダイレクトなどの用途で使用されます。 ``` ### テストシナリオ2: 認証ツールの使用(プロジェクト照会) Vercel アカウントが必要な認証ツールをテストします。 ```typescript import { GeminiMCPClient } from '../src/gemini-mcp-client.js'; import * as dotenv from 'dotenv'; dotenv.config(); async function testAuthTool() { const client = new GeminiMCPClient( process.env.GEMINI_API_KEY!, 'https://mcp.vercel.com', process.env.VERCEL_TOKEN // 認証が必要 ); await client.initialize(); const response = await client.chat( '私の Vercel プロジェクト一覧を表示して、各プロジェクトの状態を教えて' ); console.log('\n=== Final Response ==='); console.log(response); } testAuthTool().catch(console.error); ``` 期待される出力: ``` Fetching MCP tools... Found 12 MCP tools Gemini model initialized with MCP tools User: 私の Vercel プロジェクト一覧を表示して、各プロジェクトの状態を教えて Gemini wants to call 1 function(s) Calling: list_projects Args: {} Result: [{"id":"prj_abc123","name":"my-blog","framework":"nextjs",...}] Gemini: 現在 3 つの Vercel プロジェクトがあります: 1. **my-blog** (Next.js) - 状態: Production Ready - 最新デプロイ: 2時間前 - URL: my-blog.vercel.app 2. **api-server** (Node.js) - 状態: Production Ready - 最新デプロイ: 1日前 - URL: api-server.vercel.app 3. **landing-page** (React) - 状態: Building - 進行状況: 新しいデプロイがビルド中です - URL: landing-page.vercel.app ``` ### テストシナリオ3: マルチターン会話(連続する質問) 複数回の会話を通じてコンテキストが保持されているかテストします。 ```typescript import { GeminiMCPClient } from '../src/gemini-mcp-client.js'; import * as dotenv from 'dotenv'; dotenv.config(); async function testMultiTurn() { const client = new GeminiMCPClient( process.env.GEMINI_API_KEY!, 'https://mcp.vercel.com', process.env.VERCEL_TOKEN ); await client.initialize(); // 第1ターン console.log('\n=== Turn 1 ==='); const response1 = await client.chat( '私のプロジェクトの中で Next.js で作られているものを探して' ); // 第2ターン(前の回答を参照) console.log('\n=== Turn 2 ==='); const response2 = await client.chat( 'そのプロジェクトの最新デプロイメントの状態を確認して' ); // 第3ターン(さらに詳細な要求) console.log('\n=== Turn 3 ==='); const response3 = await client.chat( 'デプロイログにエラーがないか確認して' ); } testMultiTurn().catch(console.error); ``` 期待される出力: ``` === Turn 1 === User: 私のプロジェクトの中で Next.js で作られているものを探して Gemini wants to call 1 function(s) Calling: list_projects Args: {} Result: [...] Gemini: Next.js フレームワークを使用しているプロジェクトは 2 つです: 1. my-blog 2. e-commerce-site === Turn 2 === User: そのプロジェクトの最新デプロイメントの状態を確認して Gemini wants to call 2 function(s) Calling: get_deployments Args: {"projectId":"prj_abc123"} Result: [...] Calling: get_deployments Args: {"projectId":"prj_def456"} Result: [...] Gemini: 2 つのプロジェクトの最新デプロイメント状態です: **my-blog**: - 状態: Ready - デプロイ時間: 2時間前 - ビルド時間: 1分32秒 **e-commerce-site**: - 状態: Error - デプロイ時間: 5分前 - エラー: Build failed === Turn 3 === User: デプロイログにエラーがないか確認して Gemini wants to call 1 function(s) Calling: get_deployment_logs Args: {"projectId":"prj_def456","deploymentId":"dpl_xyz789"} Result: [... error logs ...] Gemini: e-commerce-site のデプロイログで次のエラーを見つけました: Failed to resolve path '@/components/ProductCard' Module not found: Can't resolve '@/components/ProductCard' at compilation.js:123 ProductCard コンポーネントが見つからないというエラーです。 Import パスを確認するか、ファイルが存在するか確認してください。 ``` ### パフォーマンス測定 実際のパフォーマンス特性を理解するための測定コード: ```typescript import { GeminiMCPClient } from '../src/gemini-mcp-client.js'; async function measurePerformance() { const client = new GeminiMCPClient( process.env.GEMINI_API_KEY!, 'https://mcp.vercel.com' ); // 初期化時間の測定 const startInit = Date.now(); await client.initialize(); const initTime = Date.now() - startInit; console.log(`Initialization: ${initTime}ms`); // チャット時間(MCP呼び出し含む)の測定 const startChat = Date.now(); await client.chat('Vercel docs で deployment について検索'); const chatTime = Date.now() - startChat; console.log(`Chat (with MCP call): ${chatTime}ms`); } measurePerformance().catch(console.error); ``` 期待される結果: ``` Initialization: 1,247ms - MCP tools fetch: 856ms - Gemini model init: 391ms Chat (with MCP call): 3,521ms - Gemini reasoning: 1,204ms - MCP tool call: 1,892ms - Gemini final response: 425ms ``` **観察**: - MCP ツール取得は初回のみ実行 - MCP ツール呼び出しが全体時間の 50% 以上を占める - ネットワーク遅延が主なボトルネック ### トラブルシューティング #### 問題1: 認証エラー **症状**: ``` Error: MCP tools/call failed: 401 Unauthorized ``` **原因**: Vercel トークンがない、期限切れ、または無効 **解決**: ```bash # Vercel Dashboard で新しいトークンを発行 # https://vercel.com/account/tokens # .env ファイルを更新 VERCEL_TOKEN=new_token_here ``` #### 問題2: Gemini が function を呼び出さない **症状**: ユーザーが MCP ツールが必要な質問をしたのに、Gemini が直接回答 **原因**: - Function description が不明確 - 質問が曖昧 **解決**: より明確な質問に変更: ```typescript // Before await client.chat('Vercel について教えて'); // After await client.chat('私の Vercel プロジェクト一覧を取得して'); ``` または `FunctionCallingConfigMode.ANY` を使用: ```typescript this.model = this.genAI.getGenerativeModel({ model: 'gemini-2.0-flash-exp', tools: [{ functionDeclarations }], toolConfig: { functionCallingConfig: { mode: 'ANY' // 必ず function を呼び出す } } }); ``` #### 問題3: MCP レスポンス解析エラー **症状**: ``` TypeError: Cannot read property 'text' of undefined ``` **原因**: MCP レスポンス形式が期待と異なる **解決**: より堅牢なパース ロジック ```typescript async callTool(name: string, args: Record): Promise { // ... fetch コード ... const data: MCPToolCallResponse = await response.json(); if (data.error) { throw new Error(`MCP tool error: ${data.error.message}`); } // 安全なパース if (!data.result) { return 'No result returned'; } if (!data.result.content) { return JSON.stringify(data.result); } const textContent = data.result.content .filter(c => c && c.type === 'text' && c.text) .map(c => c.text) .join('\n'); return textContent || JSON.stringify(data.result); } ``` ### 実装上の工夫 #### 1. ロギング改善 デバッグのための詳細なロギング: ```typescript // 環境変数で制御 const VERBOSE = process.env.VERBOSE === 'true'; if (VERBOSE) { console.log('MCP Request:', JSON.stringify(request, null, 2)); console.log('MCP Response:', JSON.stringify(data, null, 2)); } ``` #### 2. タイムアウト設定 ```typescript const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 30000); // 30秒 try { const response = await fetch(this.endpoint, { signal: controller.signal, // ... その他のオプション ... }); } finally { clearTimeout(timeout); } ``` #### 3. リトライロジック ```typescript async function fetchWithRetry( fn: () => Promise, maxRetries = 3 ): Promise { for (let i = 0; i < maxRetries; i++) { try { return await fn(); } catch (error: any) { if (i === maxRetries - 1) throw error; console.log(`Retry ${i + 1}/${maxRetries}...`); await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1))); } } throw new Error('Unreachable'); } ``` --- ## Claude Agent SDK との比較 ### 価格比較(2025年11月) まず、重要な実用的側面である価格を比較しましょう。 #### Claude モデルの価格 | モデル | 入力 | 出力 | 説明 | |--------|------|------|------| | **Claude Opus 4.5** | $15/M tokens | $75/M tokens | 最高性能(推論向け) | | **Claude Sonnet 4.5** | $3/M tokens | $15/M tokens | 高速と品質のバランス | | **Claude Haiku 4.5** | $0.80/M tokens | $4/M tokens | 最も手頃(シンプル用途) | | Claude 3.5 Sonnet | $3/M tokens | $15/M tokens | 前世代(まだ利用可) | | Claude 3 Haiku | $0.25/M tokens | $1.25/M tokens | レガシー | #### Gemini モデルの価格 | モデル | 入力 | 出力 | 説明 | |--------|------|------|------| | **Gemini 3 Pro (Preview)** | $1.25/M tokens | $5/M tokens | 高性能(プレビュー) | | **Gemini 2.5 Pro** | $1.25/M tokens | $5/M tokens | 高性能(安定版) | | **Gemini 2.5 Flash** | $0.075/M tokens | $0.3/M tokens | 高速軽量 | | **Gemini 2.5 Flash-Lite** | $0.0375/M tokens | $0.15/M tokens | 最軽量 | #### コスト比較表 | 比較項目 | Claude Sonnet | Gemini 2.5 Pro | Gemini Flash | 節約率 | |---------|---------------|----------------|--------------|--------| | 入力1M tokens | $3 | $1.25 | $0.075 | 58% ~ 97% | | 出力1M tokens | $15 | $5 | $0.3 | 67% ~ 98% | | **月額(入出力均等)** | $9/M tokens | $3.125/M tokens | $0.1875/M tokens | **65% ~ 97%** | **結論**: - Gemini 2.5 Flash は Sonnet の **97% 節約** - Gemini 2.5 Pro は Sonnet の **58% 節約** - Gemini Flash-Lite は Haiku の **91% 節約** - 大規模運用では月間数万~数十万ドルの差が出る ### 機能比較 #### Claude Agent SDK **長所**: - 公式サポート:Anthropic社による完全なドキュメント - 統合度:ツール連携がネイティブに実装 - 安定性:実績のある本番運用 - 習得曲線:シンプルで学習が容易 **短所**: - 価格が高い:Gemini の 30~60倍 - カスタマイズ制限:独自のツール形式に限定 - ベンダーロック:Claude に依存 **基本的な使用例**: ```typescript const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY, }); const tools = [ { name: "search_database", description: "データベースを検索", input_schema: { type: "object", properties: { query: { type: "string" } } } } ]; const response = await client.messages.create({ model: "claude-opus-4-1", max_tokens: 1024, tools: tools, messages: [{ role: "user", content: "データベースから情報を検索して" }] }); ``` #### Gemini + MCP **長所**: - 低コスト:Claude の 40~97% 削減可能 - 標準化:MCP という開放標準を活用 - 学習価値:内部動作を完全に理解できる - 拡張性:複数のツール、複数のモデルに対応可能 - ベンダー非依存:MCPは汎用標準 **短所**: - 手動構築:直接ブリッジコードを書く必要 - 複雑度:統合に約250行のコードが必要 - デバッグ:問題発生時の原因特定が難しい可能性 - エコシステム:Claude Agent SDK ほど整備されていない **実装上の比較**: ``` Claude Agent SDK: 1. client を作成 2. tools を定義 3. create() を呼び出す 4. 完了 Gemini + MCP: 1. MCPAdapter を作成(MCP サーバーとの通信) 2. SchemaConverter を作成(スキーマ変換) 3. GeminiMCPClient を作成(統合) 4. ツール呼び出しループを実装 5. エラーハンドリング、リトライを追加 6. テストして検証 ``` ### 各方式の選択基準 #### Claude Agent SDK を選ぶべき場合 - 短時間でプロトタイプを構築したい - 安定性が最優先である - 予算に余裕がある - 公式サポートが必要 - シンプルな実装が理想的 #### Gemini + MCP を選ぶべき場合 - コストが重要である - 内部動作を理解したい - MCP生態系を活用したい - カスタマイズが必要 - 複数のモデル/サーバー対応が必要 - 学習が目的(プロトタイプや研究) --- ## 実装の制限と改善方向 実際に構築する中で、いくつかの制限と改善の余地が見えてきました。 ### 制限1: 手動スキーマ変換 **問題**: MCPツール スキーマから Gemini 関数宣言への変換が手動です。 ```typescript static mcpToolToGeminiFunction(tool: MCPTool): FunctionDeclaration { return { name: tool.name, description: tool.description, parametersJsonSchema: { type: tool.inputSchema.type || 'object', properties: tool.inputSchema.properties || {}, required: tool.inputSchema.required || [] } }; } ``` **制限**: - JSON Schema の複雑な機能(allOf、anyOf、$ref など)に未対応 - ネストされたオブジェクト処理が不完全 - 型変換エラーの可能性 **改善方向**: ```typescript import Ajv from 'ajv'; class ImprovedSchemaConverter { private ajv = new Ajv(); convertWithValidation(tool: MCPTool): FunctionDeclaration { // スキーマの有効性検証 const valid = this.ajv.validateSchema(tool.inputSchema); if (!valid) { throw new Error(`Invalid schema for ${tool.name}`); } // $ref 解決、allOf/anyOf 処理など const resolved = this.resolveRefs(tool.inputSchema); return { name: tool.name, description: tool.description, parametersJsonSchema: resolved }; } } ``` ### 制限2: エラーハンドリングの不足 **問題**: 現在の実装は単純なエラー処理のみです。 ```typescript if (data.error) { throw new Error(`MCP tool error: ${data.error.message}`); } ``` **制限**: - リトライロジックなし - 部分的な失敗への対応なし - タイムアウト未設定 - Circuit breaker パターン未適用 **改善方向**: ```typescript class RobustMCPAdapter { private retryConfig = { maxRetries: 3, baseDelay: 1000, maxDelay: 10000 }; async callToolWithRetry( name: string, args: Record ): Promise { let lastError: Error | null = null; for (let i = 0; i < this.retryConfig.maxRetries; i++) { try { return await this.callToolWithTimeout(name, args, 30000); } catch (error: any) { lastError = error; // リトライ不可能なエラーは即座に throw if (error.message.includes('400') || error.message.includes('401')) { throw error; } // 指数バックオフ const delay = Math.min( this.retryConfig.baseDelay * Math.pow(2, i), this.retryConfig.maxDelay ); console.log(`Retry ${i + 1}/${this.retryConfig.maxRetries} after ${delay}ms`); await new Promise(resolve => setTimeout(resolve, delay)); } } throw lastError || new Error('Unknown error'); } async callToolWithTimeout( name: string, args: Record, timeout: number ): Promise { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(this.endpoint, { signal: controller.signal, // ... その他のオプション }); return await this.parseResponse(response); } finally { clearTimeout(timeoutId); } } } ``` ### 制限3: 会話コンテキスト管理の不備 **問題**: 現在はメッセージを単純な配列で管理しています。 ```typescript private chatHistory: any[] = []; ``` **制限**: - コンテキスト長制限への対応がない - トークン数計算なし - 古いメッセージの自動削除なし - メモリリークの可能性 **改善方向**: ```typescript class ContextManager { private maxTokens = 100000; private history: Message[] = []; addMessage(message: Message) { this.history.push(message); this.trimHistory(); } private trimHistory() { let totalTokens = this.estimateTokens(this.history); // トークン制限超過時は古いメッセージから削除 while (totalTokens > this.maxTokens && this.history.length > 1) { // システムメッセージは保持 const removed = this.history.find(m => m.role !== 'system'); if (!removed) break; this.history.splice(this.history.indexOf(removed), 1); totalTokens = this.estimateTokens(this.history); } } private estimateTokens(messages: Message[]): number { // 簡単なヒューリスティック(実際は tiktoken ライブラリを使用) return messages.reduce((sum, msg) => { const text = JSON.stringify(msg); return sum + Math.ceil(text.length / 4); }, 0); } } ``` ### 制限4: 複数MCPサーバー非対応 **問題**: 現在は1つのMCPサーバーのみをサポートしています。 ```typescript constructor( geminiApiKey: string, mcpEndpoint: string, mcpToken?: string ) ``` **制限**: - 複数のMCPサーバー同時使用が不可 - ツール名の衝突可能性 - 柔軟性の欠如 **改善方向**: ```typescript interface MCPServerConfig { name: string; endpoint: string; token?: string; priority?: number; } class MultiMCPClient { private servers: Map = new Map(); async addServer(config: MCPServerConfig) { const adapter = new MCPAdapter(config.endpoint, config.token); this.servers.set(config.name, adapter); } async getAllTools(): Promise { const allTools: FunctionDeclaration[] = []; const toolNames = new Set(); for (const [serverName, adapter] of this.servers) { const tools = await adapter.getTools(); for (const tool of tools) { // ツール名の衝突を防止:server prefix を追加 const prefixedName = `${serverName}_${tool.name}`; if (toolNames.has(prefixedName)) { console.warn(`Tool ${prefixedName} already exists, skipping`); continue; } toolNames.add(prefixedName); allTools.push( SchemaConverter.mcpToolToGeminiFunction({ ...tool, name: prefixedName }) ); } } return allTools; } async callTool(toolName: string, args: Record): Promise { // server_toolname 形式をパース const [serverName, ...nameParts] = toolName.split('_'); const actualToolName = nameParts.join('_'); const adapter = this.servers.get(serverName); if (!adapter) { throw new Error(`Unknown server: ${serverName}`); } return await adapter.callTool(actualToolName, args); } } ``` ### 制限5: キャッシング戦略の欠如 **問題**: - MCPツール一覧を毎回新たに取得 - 同じツール呼び出しの結果が再利用されない - 不要なネットワークリクエスト **改善方向**: ```typescript class CachedMCPAdapter extends MCPAdapter { private toolsCache: { data: MCPTool[] | null; timestamp: number; ttl: number; } = { data: null, timestamp: 0, ttl: 3600000 }; // 1時間 private resultCache = new Map(); async getTools(): Promise { const now = Date.now(); // キャッシュ有効性確認 if (this.toolsCache.data && (now - this.toolsCache.timestamp) < this.toolsCache.ttl) { return this.toolsCache.data; } // キャッシュ更新 const tools = await super.getTools(); this.toolsCache = { data: tools, timestamp: now, ttl: this.toolsCache.ttl }; return tools; } async callTool( name: string, args: Record ): Promise { // べき等なツールのみキャッシュ(GET 性質のツール) const cacheableTools = ['search_docs', 'get_project', 'list_projects']; if (cacheableTools.includes(name)) { const cacheKey = `${name}:${JSON.stringify(args)}`; const cached = this.resultCache.get(cacheKey); if (cached && (Date.now() - cached.timestamp) < 60000) { // 1分 return cached.result; } const result = await super.callTool(name, args); this.resultCache.set(cacheKey, { result, timestamp: Date.now() }); return result; } return await super.callTool(name, args); } } ``` ### 制限6: 型安全性の改善余地 **問題**: 現在は `any` 型が多用されています。 ```typescript private chatHistory: any[] = []; async callTool(name: string, args: Record): Promise ``` **改善方向**: ```typescript interface Message { role: 'user' | 'assistant' | 'system'; parts: Part[]; } interface Part { text?: string; functionCall?: FunctionCall; functionResponse?: FunctionResponse; } interface FunctionCall { name: string; args: Record; } interface FunctionResponse { name: string; response: { result?: string; error?: string; }; } class TypeSafeGeminiClient { private chatHistory: Message[] = []; async callTool( name: string, args: Record ): Promise { // ... 実装 ... } } ``` ### その他の推奨される改善 #### ストリーミング対応 ```typescript class StreamingMCPClient { async *chatStream(message: string): AsyncGenerator { const chat = this.model.startChat({ history: this.chatHistory }); const result = await chat.sendMessageStream(message); for await (const chunk of result.stream) { const text = chunk.text(); if (text) { yield text; } if (chunk.functionCalls) { const results = await this.handleFunctionCalls(chunk.functionCalls); const followUp = await chat.sendMessageStream(results); for await (const followUpChunk of followUp.stream) { yield followUpChunk.text(); } } } } } ``` #### ロギングと監視 ```typescript interface LogEntry { timestamp: number; type: 'tool_call' | 'error' | 'response'; data: any; } class MonitoredMCPClient { private logs: LogEntry[] = []; async callTool(name: string, args: Record): Promise { const startTime = Date.now(); try { const result = await super.callTool(name, args); this.log({ timestamp: Date.now(), type: 'tool_call', data: { name, args, result: result.substring(0, 100), duration: Date.now() - startTime } }); return result; } catch (error: any) { this.log({ timestamp: Date.now(), type: 'error', data: { name, args, error: error.message, duration: Date.now() - startTime } }); throw error; } } exportMetrics() { const metrics = { totalCalls: this.logs.filter(l => l.type === 'tool_call').length, errors: this.logs.filter(l => l.type === 'error').length, avgDuration: 0, toolUsage: {} as Record }; const toolCalls = this.logs.filter(l => l.type === 'tool_call'); if (toolCalls.length > 0) { metrics.avgDuration = toolCalls.reduce( (sum, log) => sum + log.data.duration, 0 ) / toolCalls.length; toolCalls.forEach(log => { const name = log.data.name; metrics.toolUsage[name] = (metrics.toolUsage[name] || 0) + 1; }); } return metrics; } } ``` #### ツール権限管理 ```typescript interface ToolPermission { toolName: string; allowed: boolean; requiresConfirmation?: boolean; } class SecureMCPClient { private permissions: Map = new Map(); setPermission(toolName: string, permission: ToolPermission) { this.permissions.set(toolName, permission); } async callTool(name: string, args: Record): Promise { const permission = this.permissions.get(name); if (permission && !permission.allowed) { throw new Error(`Tool ${name} is not allowed`); } if (permission?.requiresConfirmation) { const confirmed = await this.requestUserConfirmation(name, args); if (!confirmed) { throw new Error(`User denied permission for ${name}`); } } return await super.callTool(name, args); } private async requestUserConfirmation( name: string, args: Record ): Promise { console.log(`\nTool ${name} requires confirmation`); console.log(`Arguments: ${JSON.stringify(args, null, 2)}`); console.log(`Allow? (y/n)`); // ユーザー入力処理(実装省略) return true; } } ``` ### 本番環境への準備チェックリスト 実際に本番環境にデプロイする前に確認すべき項目: - [ ] すべての環境変数を安全に管理(Vault、Secret Manager) - [ ] レート制限の実装 - [ ] エラーロギングと監視(Sentry、DataDog など) - [ ] リトライロジック(指数バックオフ付き) - [ ] Circuit breaker パターン - [ ] タイムアウト設定 - [ ] 入力検証(XSS、インジェクション防止) - [ ] API キーのローテーション戦略 - [ ] 費用の監視とアラート - [ ] 負荷テスト - [ ] 障害復旧計画 --- ## 本プロジェクトの振り返りと感想 このプロジェクトは、単純な疑問から始まって、想像以上に多くの学びをもたらしました。 ### 旅の概要 **v1: 動機** > なぜこれを作りたかったのか? Claude Agent SDK の優雅さに感銘を受け、「Gemini でも同じことができるだろうか」という疑問からスタート。 **v2: MCP の理解** > MCP って何? JSON-RPC 2.0 ベースのプロトコルで、LLM とツール連携を標準化する仕組み。HTTP と Stdio トランスポートをサポート。 **v3: Gemini API の探索** > Gemini にはどんなツール機能がある? Function Calling メカニズムと8種類のツールタイプ。MCP との統合可能性を発見。 **v4: 設計** > どうやって実装する? MCPAdapter、SchemaConverter、GeminiMCPClient という3つのコアコンポーネント。4段階の実装フェーズ。 **v5: Vercel MCP Server** > どの MCP サーバーを使う? Vercel が提供する公開 MCP サーバーで、12 個のツールと良好なドキュメント。認証ツールも公開ツールも提供。 **v6: 実装** > 実際のコードは? 約 250 行の TypeScript で全機能を実装。JSON-RPC 通信、スキーマ変換、ツール呼び出しループ。 **v7: テスト** > 本当に動く? 3 つのテストシナリオで検証。公開ツール、認証ツール、マルチターン会話すべてが正常に動作。 **v8: 比較** > Claude との違いは? 価格は 40~97% 削減。編集者はClaudeが上だが、コスト、学習価値、拡張性は Gemini + MCP が優位。 **v9: 改善方向** > 現在の実装の課題は? 5 つの主要な制限(スキーマ変換、エラーハンドリング、コンテキスト管理、複数サーバー、キャッシング)と改善コード例を提示。 **v10: 回顧** > 全体を通じて何を学んだか? ### コアな学習 #### 技術的学習 1. **MCP プロトコル** - JSON-RPC 2.0 が実装された、実用的な標準プロトコル - tools/list、tools/call メソッドで簡潔に設計 - HTTP と Stdio 両方のトランスポート方式をサポート - 実はそこまで複雑ではなく、理解し実装可能 2. **Gemini SDK** - Function Calling メカニズムが MCP と相性良好 - 8 種類のツール型で様々なユースケースに対応 - MCP を公式ドキュメントで言及(統合の標準化を認識) - 決して Claude Agent SDK に劣らない機能 3. **統合パターン** - Adapter パターンで異なる 2 つのシステムを橋渡し - Schema 変換ロジック:片方の仕様を別の仕様へ変換 - Function call 処理ループ:結果をフィードバックして反復 - エラーハンドリング戦略:単純な例外から複雑なリトライまで #### 非技術的学習 1. **段階的文書化の重要性** - v1 から v10 まで、思考の進化をステップバイステップで記録 - 最初は理解不足だったが、継続的に深める - 後から見返した時、なぜそう判断したのか理解できる - 他者への説明資料にもなる 2. **トレードオフ思考** - どんな選択にも長所と短所がある - 「最善」は使用コンテキストに完全に依存する - コスト vs 編集者 vs 制御 のトレードオフを明示化 - 状況に応じた適切な選択が重要 3. **オープン標準の価値** - MCP はベンダーロック的なものではなく、汎用標準 - 複数の LLM 企業が協力する可能性 - 個別実装より統一仕様の方が長期的には効率的 - エコシステム形成の基盤になり得る ### 驚き及び発見 #### 1. Gemini SDK の MCP 言及 Gemini SDK 公式ドキュメントにこのように書かれていた: > "Defines the structure of an invokable tool that can be executed with external applications (e.g., via Model Context Protocol)" Google も MCP を標準として認識している。これは単なるツール統合ではなく、業界標準化への動き。 #### 2. 価格差の極明 | 項目 | Claude Sonnet | Gemini 2.5 Pro | Gemini Flash | |-----|---------------|-----------------|--------------| | 入力 | $3/M | $1.25/M | $0.075/M | | 出力 | $15/M | $5/M | $0.3/M | | 比率 | 基準 | 58% 削減 | 97% 削減 | 月間 1 億トークン処理なら: - Claude Sonnet:~$1,800 - Gemini Pro:~$750 - Gemini Flash:~$45 この差は数万~数百万ドル規模の企業にとって極めて重要。 #### 3. 実装の予想外のシンプルさ 複雑に思えた MCP-Gemini 統合が、結果的には: ``` 1. MCP から tools 取得 2. Gemini フォーマットへ変換 3. Function call 処理 4. 結果を返す ``` この 4 ステップ で完成。約 250 行の TypeScript が全て。 ### 後悔と課題 #### 1. 実際の実行検証の欠如 このドキュメントでは理論とコードは示しましたが、実際に実行して検証することはできませんでした。 **次のステップ**: 実際にコードを動かし、実行結果を確認・ドキュメント化 #### 2. エッジケースの未処理 v9 で改善案は示しましたが、以下は実装されていません: - ネットワークエラー時の復旧戦略 - 認証失敗のハンドリング - タイムアウト管理 - リトライ戦略 - Circuit breaker パターン **次のステップ**: プロダクション品質のエラーハンドリング実装 #### 3. 他の MCP サーバーの未探索 Vercel MCP サーバーのみを扱いました。以下は試していません: - Filesystem MCP - Database MCP - Fetch MCP - 独自カスタム MCP の作成 **次のステップ**: 複数の MCP サーバーを試し、比較分析 ### 実務適用可能性 #### 即座に適用可能 - 個人プロジェクト - サイドプロジェクト - コスト敏感なスタートアップ - 学習・研究目的 #### 追加作業が必要 本番環境への対応には以下が必須: - [ ] 実装とテストの実行 - [ ] 堅牢なエラーハンドリング - [ ] ロギングと監視 - [ ] パフォーマンス最適化 - [ ] セキュリティ強化 - [ ] ドキュメント完成 ### 次のステップ提案 #### 短期(1 週間) 1. **実際の実行** - コードを実際に動作させる - バグを修正する - 実行結果をドキュメント化 2. **複数の MCP サーバー試行** - Filesystem MCP - Fetch MCP - カスタム MCP サーバー作成 3. **エラーケーステスト** - ネットワークエラー - 認証失敗 - タイムアウト #### 中期(1 ヶ月) 1. **本番対応強化** - v9 の改善案を実装 - Circuit breaker - Rate limiting - Monitoring 2. **複数 LLM サポート** - OpenAI GPT-4 - Anthropic Claude(逆向き) - マルチモデルクライアント 3. **パッケージ化** - npm パッケージとして発行 - 公式ドキュメントサイト構築 - 例題リポジトリ #### 長期(3 ヶ月以上) 1. **コミュニティ構築** - GitHub での公開 - Issue・PR 管理 - ユーザーフィードバック収集 2. **高度な機能** - Streaming サポート - 複数 MCP サーバー同時利用 - ツール権限管理 - 費用最適化 3. **生態系への貢献** - MCP 仕様への寄稿 - Gemini SDK フィードバック - ブログ記事作成 ### 個人的感想 #### 開始時 「Claude のように上手くいくかな... でも試してみよう」 比較的悲観的でしたが、好奇心が勝りました。 #### 途中 「あれ?思ったより動く?MCP 標準ってちゃんと設計されているんだな」 MCP が実装された、実用的な標準だと気づきました。 #### 完成後 「直接作ってみるから内部が全部見える。コストも桁違いに安い。」 単なる理解に留まらず、実装を通じた深い習得となりました。 ### 結論 **Claude Agent SDK は便利だが、Gemini + MCP は強力である。** - **編集者**: Claude - **コスト**: Gemini(40~97% 削減) - **学習価値**: Gemini(内部動作完全理解) - **制御性**: Gemini(カスタマイズ自由) - **安定性**: Claude(実績多数) **個人的には Gemini + MCP 直接構築を推奨します。** 理由: 1. 内部動作を完全に理解できる 2. コストを 97% 削減可能 3. 必要に応じてカスタマイズ可能 4. MCP 生態系を活用可能 5. 他の LLM への拡張が容易 ### エンジニアへのメッセージ #### このプロジェクトの価値 1. **新パラダイムの理解** - LLM + 外部ツール統合 - Function calling メカニズム - 標準プロトコルの重要性 2. **実用的技術の習得** - 実装可能なコード - 本番環境への考慮 - トレードオフ分析 3. **将来への備え** - AI Agent 時代が到来 - MCP が標準化する可能性 - 前もっての準備 #### トライしてみることをお勧め このドキュメントを読むだけでは不十分です。 **直接試してみてください**: 1. v6 のコードをタイプして実装 2. 実際に実行して動作を確認 3. 独自のツールを追加 4. 別の MCP サーバーに接続 **そうすると**: - MCP が体で理解される - Gemini function calling が腑に落ちる - 独自の AI agent を構築できるようになる --- ## 最後に このプロジェクトは: - Claude の便利さに感動しながら始まり - Gemini の可能性を発見し - MCP の価値を理解し - 直接作る喜びを感じた **次はあなたの番です。** v1 から v10 までの旅が誰かのインスピレーションになり、実プロジェクトへと繋がることを望みます。 MCP が標準化する時代、事前に理解し準備する者が優位に立つでしょう。 --- ## 参考資料と関連リンク ### 公式ドキュメント - [Gemini API Documentation](https://ai.google.dev/gemini-api/docs) - [Model Context Protocol Specification](https://modelcontextprotocol.io/specification) - [Vercel MCP Documentation](https://vercel.com/docs/mcp/vercel-mcp) - [Anthropic Claude API Documentation](https://docs.anthropic.com/) ### 関連プロジェクト - [@google/genai](https://github.com/googleapis/js-genai) - [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) - [Vercel MCP Adapter](https://github.com/vercel/mcp-adapter) - [Claude Agent SDK](https://github.com/anthropics/anthropic-sdk-python) ### 次に読むべき資料 - Claude Agent SDK ドキュメント - LangChain vs MCP 比較分析 - AI Agent 設計パターン - Function Calling のベストプラクティス --- ## プロジェクト統計 - **プロジェクト期間**: 2025-11-26 (1日) - **総ドキュメント**: v1 ~ v10 (10セクション) - **総コード行数**: ~250 行 - **投資コスト**: $0(ドキュメント化のみ) - **獲得した価値**: 無価 **The Beginning** ここまでで、疑問から始まった旅は一区切りを迎えました。 しかし、これは終わりではなく、本当の実装へのスタートラインです。 行動することで初めて、さらなる学習と成長が得られます。 まずは、実装してみてください。