取り組みの背景 — MCP がAIエージェント開発を変える理由
Model Context Protocol(MCP)は、LLM アプリケーションと外部ツール・データソースを接続するための標準プロトコルです。Anthropic が提唱し、現在では Google の Gemini CLI や多くの AI ツールが対応を進めています。
従来、AI エージェントに外部ツール連携能力を持たせるには、各ツールごとに独自のアダプターを実装する必要がありましました。MCP はこの課題を解決し、「ツール提供側が MCP サーバーを公開すれば、MCP 対応のあらゆる AI クライアントから利用できる」というエコシステムを構築します。
MCP アーキテクチャの全体像
MCP は JSON-RPC 2.0 ベースのプロトコルで、以下の3つの主要コンポーネントで構成されます。
MCP ホスト : AI アプリケーション本体(Gemini CLI、Claude Code など)
MCP クライアント : ホスト内でサーバーと通信するコンポーネント
MCP サーバー : ツール・リソース・プロンプトを提供するサービス
通信はstdio(標準入出力)または SSE(Server-Sent Events)/ Streamable HTTP で行われます。stdio はローカル実行に、SSE / HTTP はリモートサーバーに適しています。
MCP が提供する3つのプリミティブ
Tools(ツール) — LLM が呼び出せる関数(API 呼び出し、DB クエリ等)
Resources(リソース)— LLM が読み取れるデータソース(ファイル、DB レコード等)
Prompts(プロンプト)— 再利用可能なプロンプトテンプレート
AI エージェントが最も頻繁に使うのは Tools です。Gemini API の Function Calling と概念的に近く、MCP サーバーが提供するツール定義は、Gemini の functionDeclarations にマッピングできます。Function Calling の基礎をまだ押さえていない方は、先に Gemini API Function Calling 実践ガイド を読んでおくとスムーズです。
開発環境のセットアップ
必要なパッケージ
# プロジェクト初期化
mkdir gemini-mcp-server && cd gemini-mcp-server
npm init -y
# MCP SDK と依存パッケージ
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx
# Gemini API クライアント(テスト用)
npm install @google/genai
TypeScript 設定
// tsconfig.json
{
"compilerOptions" : {
"target" : "ES2022" ,
"module" : "ESNext" ,
"moduleResolution" : "bundler" ,
"outDir" : "./dist" ,
"rootDir" : "./src" ,
"strict" : true ,
"esModuleInterop" : true ,
"declaration" : true ,
"sourceMap" : true
},
"include" : [ "src/**/*" ]
}
MCP サーバーの基本実装
まず、最小限の MCP サーバーを構築します。ここでは「天気情報を取得するツール」を例に、サーバーの骨格を理解しましょう。
サーバーの骨格
// src/server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" ;
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" ;
import { z } from "zod" ;
// サーバーインスタンスの作成
const server = new McpServer ({
name: "weather-tools" ,
version: "1.0.0" ,
capabilities: {
tools: {}, // ツールを提供
resources: {} // リソースも提供可能
}
});
// ツールの定義
server. tool (
"get_weather" , // ツール名
"指定された都市の現在の天気情報を取得します" , // 説明
{
city: z. string (). describe ( "都市名(例: Tokyo, New York)" ),
unit: z. enum ([ "celsius" , "fahrenheit" ])
. default ( "celsius" )
. describe ( "温度の単位" )
},
async ({ city , unit }) => {
// 実際の API 呼び出し(ここではモック)
const weatherData = await fetchWeatherAPI (city, unit);
return {
content: [{
type: "text" ,
text: JSON . stringify (weatherData, null , 2 )
}]
};
}
);
// サーバーの起動
async function main () {
const transport = new StdioServerTransport ();
await server. connect (transport);
console. error ( "[weather-tools] MCP server running on stdio" );
}
main (). catch (console.error);
ツールのレスポンス設計
MCP ツールのレスポンスは content 配列で返します。テキスト、画像、埋め込みリソースなど複数のコンテンツタイプを含めることができます。
// 成功時のレスポンス
return {
content: [{
type: "text" ,
text: JSON . stringify ({
city: "Tokyo" ,
temperature: 22 ,
condition: "晴れ" ,
humidity: 45 ,
wind_speed: 12
}, null , 2 )
}]
};
// エラー時のレスポンス
return {
isError: true ,
content: [{
type: "text" ,
text: "都市名が見つかりません。正しい都市名を指定してください。"
}]
};
実践:複数ツールを持つ本番品質サーバーの構築
ここからが本記事の核心です。「プロジェクト管理ツールサーバー」を例に、本番環境で使える MCP サーバーを構築します。
プロジェクト構成
src/
├── server.ts # サーバーエントリポイント
├── tools/
│ ├── index.ts # ツール登録
│ ├── tasks.ts # タスク管理ツール
│ ├── calendar.ts # カレンダーツール
│ └── analytics.ts # 分析ツール
├── middleware/
│ ├── auth.ts # 認証ミドルウェア
│ ├── rateLimit.ts # レート制限
│ └── logging.ts # ロギング
├── db/
│ └── client.ts # データベースクライアント
└── types/
└── index.ts # 型定義
型安全なツール定義パターン
// src/types/index.ts
import { z } from "zod" ;
// タスクのスキーマ定義
export const TaskSchema = z. object ({
id: z. string (). uuid (),
title: z. string (). min ( 1 ). max ( 200 ),
description: z. string (). optional (),
status: z. enum ([ "todo" , "in_progress" , "done" , "blocked" ]),
priority: z. enum ([ "low" , "medium" , "high" , "critical" ]),
assignee: z. string (). optional (),
dueDate: z. string (). datetime (). optional (),
tags: z. array (z. string ()). default ([]),
createdAt: z. string (). datetime (),
updatedAt: z. string (). datetime ()
});
export type Task = z . infer < typeof TaskSchema>;
// ツールの入力パラメータスキーマ
export const CreateTaskInput = z. object ({
title: z. string (). describe ( "タスクのタイトル" ),
description: z. string (). optional (). describe ( "タスクの詳細説明" ),
priority: z. enum ([ "low" , "medium" , "high" , "critical" ])
. default ( "medium" )
. describe ( "優先度" ),
assignee: z. string (). optional (). describe ( "担当者のユーザーID" ),
dueDate: z. string (). optional (). describe ( "期限(ISO 8601形式)" ),
tags: z. array (z. string ()). default ([]). describe ( "タグのリスト" )
});
export const QueryTasksInput = z. object ({
status: z. enum ([ "todo" , "in_progress" , "done" , "blocked" ])
. optional ()
. describe ( "ステータスでフィルタ" ),
priority: z. enum ([ "low" , "medium" , "high" , "critical" ])
. optional ()
. describe ( "優先度でフィルタ" ),
assignee: z. string (). optional (). describe ( "担当者でフィルタ" ),
limit: z. number (). min ( 1 ). max ( 100 ). default ( 20 )
. describe ( "取得件数の上限" )
});
ツール実装(タスク管理)
// src/tools/tasks.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" ;
import { CreateTaskInput, QueryTasksInput } from "../types/index.js" ;
import { db } from "../db/client.js" ;
import { randomUUID } from "crypto" ;
export function registerTaskTools ( server : McpServer ) {
// タスク作成
server. tool (
"create_task" ,
"新しいタスクを作成します。タイトルは必須です。" ,
CreateTaskInput.shape,
async ( params ) => {
const task = {
id: randomUUID (),
... params,
status: "todo" as const ,
createdAt: new Date (). toISOString (),
updatedAt: new Date (). toISOString ()
};
await db.tasks. insert (task);
return {
content: [{
type: "text" as const ,
text: `タスクを作成しました: \n ${ JSON . stringify ( task , null , 2 ) }`
}]
};
}
);
// タスク検索
server. tool (
"query_tasks" ,
"条件に一致するタスクを検索します。フィルタなしで全タスクを取得可能。" ,
QueryTasksInput.shape,
async ( params ) => {
const tasks = await db.tasks. find ({
... (params.status && { status: params.status }),
... (params.priority && { priority: params.priority }),
... (params.assignee && { assignee: params.assignee })
}, { limit: params.limit });
return {
content: [{
type: "text" as const ,
text: tasks. length > 0
? `${ tasks . length }件のタスクが見つかりました: \n ${ JSON . stringify ( tasks , null , 2 ) }`
: "条件に一致するタスクは見つかりませんでした。"
}]
};
}
);
// タスクステータス更新
server. tool (
"update_task_status" ,
"タスクのステータスを更新します。" ,
{
taskId: CreateTaskInput.shape.title, // reuse string schema
newStatus: QueryTasksInput.shape.status. unwrap ()
},
async ({ taskId , newStatus }) => {
const updated = await db.tasks. updateStatus (taskId, newStatus);
if ( ! updated) {
return {
isError: true ,
content: [{
type: "text" as const ,
text: `タスクID「${ taskId }」が見つかりません。`
}]
};
}
return {
content: [{
type: "text" as const ,
text: `タスク「${ updated . title }」のステータスを「${ newStatus }」に更新しました。`
}]
};
}
);
}
認証とセキュリティの実装
本番環境では認証が不可欠です。MCP は SSE / HTTP トランスポート使用時に OAuth 2.0 ベースの認証をサポートしています。
Bearer Token 認証ミドルウェア
// src/middleware/auth.ts
import { Request, Response, NextFunction } from "express" ;
interface AuthConfig {
apiKeys : Set < string >;
rateLimitPerMinute : number ;
}
const requestCounts = new Map < string , { count : number ; resetAt : number }>();
export function createAuthMiddleware ( config : AuthConfig ) {
return ( req : Request , res : Response , next : NextFunction ) => {
// API キーの検証
const authHeader = req.headers.authorization;
if ( ! authHeader?. startsWith ( "Bearer " )) {
return res. status ( 401 ). json ({
error: "Authorization header with Bearer token required"
});
}
const apiKey = authHeader. slice ( 7 );
if ( ! config.apiKeys. has (apiKey)) {
return res. status ( 403 ). json ({ error: "Invalid API key" });
}
// レート制限チェック
const now = Date. now ();
const record = requestCounts. get (apiKey);
if (record && record.resetAt > now) {
if (record.count >= config.rateLimitPerMinute) {
return res. status ( 429 ). json ({
error: "Rate limit exceeded" ,
retryAfter: Math. ceil ((record.resetAt - now) / 1000 )
});
}
record.count ++ ;
} else {
requestCounts. set (apiKey, {
count: 1 ,
resetAt: now + 60_000
});
}
next ();
};
}
SSE トランスポートでのリモート公開
// src/remote-server.ts
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js" ;
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js" ;
import express from "express" ;
import { createAuthMiddleware } from "./middleware/auth.js" ;
const app = express ();
const server = new McpServer ({
name: "project-manager" ,
version: "1.0.0"
});
// ツール登録(前述の registerTaskTools 等)
// registerTaskTools(server);
// 認証ミドルウェア
app. use ( "/mcp" , createAuthMiddleware ({
apiKeys: new Set (process.env. MCP_API_KEYS ?. split ( "," ) ?? []),
rateLimitPerMinute: 60
}));
// SSE エンドポイント
app. get ( "/mcp/sse" , async ( req , res ) => {
const transport = new SSEServerTransport ( "/mcp/messages" , res);
await server. connect (transport);
});
app. post ( "/mcp/messages" , async ( req , res ) => {
// メッセージハンドリング
});
app. listen ( 3001 , () => {
console. log ( "[project-manager] MCP server listening on port 3001" );
});
Gemini API との統合パターン
MCP サーバーを構築したら、次は Gemini API と連携させます。Gemini CLI は MCP にネイティブ対応していますが、API 経由で利用する場合はブリッジ層が必要です。
パターン1: Gemini CLI 経由の直接接続
最もシンプルな方法は、settings.json に MCP サーバーを登録することです。
{
"mcpServers" : {
"project-manager" : {
"command" : "npx" ,
"args" : [ "tsx" , "/path/to/src/server.ts" ],
"env" : {
"DATABASE_URL" : "postgresql://localhost:5432/projects"
}
}
}
}
Gemini CLI を起動すると、MCP サーバーのツールが自動的に認識され、Function Calling として利用可能になります。
パターン2: API 経由の MCP ツールブリッジ
Gemini API をプログラムから呼び出す場合は、MCP ツールを Gemini の functionDeclarations に変換するブリッジを実装します。
// src/bridge/gemini-mcp-bridge.ts
import { GoogleGenAI } from "@google/genai" ;
import { Client } from "@modelcontextprotocol/sdk/client/index.js" ;
import { StdioClientTransport } from
"@modelcontextprotocol/sdk/client/stdio.js" ;
class GeminiMCPBridge {
private genai : GoogleGenAI ;
private mcpClient : Client ;
constructor ( apiKey : string ) {
this .genai = new GoogleGenAI ({ apiKey });
this .mcpClient = new Client (
{ name: "gemini-bridge" , version: "1.0.0" }
);
}
async connect ( command : string , args : string []) {
const transport = new StdioClientTransport ({ command, args });
await this .mcpClient. connect (transport);
}
// MCP ツールを Gemini Function Declarations に変換
async getGeminiFunctionDeclarations () {
const { tools } = await this .mcpClient. listTools ();
return tools. map ( tool => ({
name: tool.name,
description: tool.description ?? "" ,
parameters: this . convertJsonSchemaToGemini (tool.inputSchema)
}));
}
// エージェントループの実行
async run ( userMessage : string ) {
const functions = await this . getGeminiFunctionDeclarations ();
const model = "gemini-2.5-pro" ;
let messages = [{ role: "user" as const , parts: [{ text: userMessage }] }];
const tools = [{ functionDeclarations: functions }];
while ( true ) {
const response = await this .genai.models. generateContent ({
model,
contents: messages,
config: { tools }
});
const candidate = response.candidates?.[ 0 ];
if ( ! candidate) break ;
// Function Call があれば MCP サーバーに転送
const functionCalls = candidate.content.parts?. filter (
( p : any ) => p.functionCall
);
if ( ! functionCalls || functionCalls. length === 0 ) {
// テキスト応答 — ループ終了
const text = candidate.content.parts
?. map (( p : any ) => p.text)
. join ( "" );
return text;
}
// MCP ツール呼び出しと結果の収集
messages. push ({ role: "model" as const , parts: candidate.content.parts });
const functionResponses = [];
for ( const part of functionCalls) {
const { name , args } = (part as any ).functionCall;
const result = await this .mcpClient. callTool ({
name,
arguments: args ?? {}
});
functionResponses. push ({
functionResponse: {
name,
response: {
content: result.content
. map (( c : any ) => c.text)
. join ( " \n " )
}
}
});
}
messages. push ({
role: "user" as const ,
parts: functionResponses
});
}
}
private convertJsonSchemaToGemini ( schema : any ) {
// JSON Schema → Gemini パラメータ形式への変換
// 型マッピング、required フィールドの処理等
return {
type: "object" ,
properties: schema.properties ?? {},
required: schema.required ?? []
};
}
}
// 使用例
async function main () {
const bridge = new GeminiMCPBridge ( "YOUR_GEMINI_API_KEY" );
await bridge. connect ( "npx" , [ "tsx" , "./src/server.ts" ]);
const result = await bridge. run (
"高優先度のタスクを3つ作成して、それぞれにAIタグを付けてください"
);
console. log (result);
}
このブリッジパターンにより、Gemini API が MCP ツールをシームレスに呼び出すエージェントループが実現します。
エラーハンドリングとリトライ戦略
本番環境では、ネットワーク障害やタイムアウトへの対策が不可欠です。
堅牢なエラーハンドリング
// src/middleware/errorHandler.ts
import { z } from "zod" ;
// ツールラッパー:エラーを安全にキャッチ
function withErrorHandling < T extends z . ZodRawShape >(
handler : ( params : z . infer < z . ZodObject < T >>) => Promise < any >
) {
return async ( params : z . infer < z . ZodObject < T >>) => {
try {
return await handler (params);
} catch (error) {
// Zod バリデーションエラー
if (error instanceof z . ZodError ) {
return {
isError: true ,
content: [{
type: "text" as const ,
text: `入力パラメータが不正です: \n ${
error . errors . map ( e =>
` - ${ e . path . join ( "." ) }: ${ e . message }`
). join ( " \n " )
}`
}]
};
}
// データベースエラー
if ((error as any ).code === "ECONNREFUSED" ) {
return {
isError: true ,
content: [{
type: "text" as const ,
text: "データベースに接続できません。管理者に連絡してください。"
}]
};
}
// 予期しないエラー
console. error ( "[MCP Server Error]" , error);
return {
isError: true ,
content: [{
type: "text" as const ,
text: "内部エラーが発生しました。しばらく待ってから再試行してください。"
}]
};
}
};
}
指数バックオフ付きリトライ
// src/utils/retry.ts
async function withRetry < T >(
fn : () => Promise < T >,
options : {
maxRetries ?: number ;
baseDelay ?: number ;
maxDelay ?: number ;
} = {}
) : Promise < T > {
const { maxRetries = 3 , baseDelay = 1000 , maxDelay = 10000 } = options;
for ( let attempt = 0 ; attempt <= maxRetries; attempt ++ ) {
try {
return await fn ();
} catch (error) {
if (attempt === maxRetries) throw error;
const delay = Math. min (
baseDelay * Math. pow ( 2 , attempt) + Math. random () * 1000 ,
maxDelay
);
console. error (
`[Retry] Attempt ${ attempt + 1 }/${ maxRetries },` +
` waiting ${ Math . round ( delay ) }ms`
);
await new Promise ( resolve => setTimeout (resolve, delay));
}
}
throw new Error ( "Unreachable" );
}
テストとデバッグ
MCP Inspector を使ったインタラクティブテスト
MCP SDK にはデバッグ用の Inspector ツールが付属しています。
# MCP Inspector の起動
npx @modelcontextprotocol/inspector npx tsx src/server.ts
ブラウザで http://localhost:5173 を開くと、ツール一覧の確認、パラメータを入力してのテスト実行、レスポンスの検証が GUI で行えます。
ユニットテスト
// tests/tools/tasks.test.ts
import { describe, it, expect, beforeEach } from "vitest" ;
import { Client } from "@modelcontextprotocol/sdk/client/index.js" ;
import { InMemoryTransport } from
"@modelcontextprotocol/sdk/inMemory.js" ;
import { createServer } from "../src/server.js" ;
describe ( "Task Tools" , () => {
let client : Client ;
beforeEach ( async () => {
const server = createServer ();
const [ clientTransport , serverTransport ] =
InMemoryTransport. createLinkedPair ();
await server. connect (serverTransport);
client = new Client ({ name: "test" , version: "1.0.0" });
await client. connect (clientTransport);
});
it ( "should create a task" , async () => {
const result = await client. callTool ({
name: "create_task" ,
arguments: {
title: "テスト用タスク" ,
priority: "high"
}
});
expect (result.isError). toBeFalsy ();
const text = (result.content[ 0 ] as any ).text;
expect (text). toContain ( "テスト用タスク" );
});
it ( "should return error for invalid input" , async () => {
const result = await client. callTool ({
name: "create_task" ,
arguments: { title: "" } // 空文字列は不正
});
expect (result.isError). toBeTruthy ();
});
});
本番デプロイのベストプラクティス
Docker コンテナ化
# Dockerfile
FROM node:22-slim AS builder
WORKDIR /app
COPY package*.json tsconfig.json ./
RUN npm ci
COPY src/ src/
RUN npm run build
FROM node:22-slim
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package.json ./
ENV NODE_ENV=production
EXPOSE 3001
CMD [ "node" , "dist/remote-server.js" ]
ヘルスチェックとモニタリング
// ヘルスチェックエンドポイント
app. get ( "/health" , async ( req , res ) => {
const checks = {
server: "ok" ,
database: "unknown" ,
uptime: process. uptime ()
};
try {
await db. ping ();
checks.database = "ok" ;
res. json (checks);
} catch {
checks.database = "error" ;
res. status ( 503 ). json (checks);
}
});
環境変数の管理
# .env.production
MCP_SERVER_PORT = 3001
MCP_API_KEYS = key1,key2,key3
DATABASE_URL = postgresql://user:pass@db:5432/projects
LOG_LEVEL = info
RATE_LIMIT_PER_MINUTE = 120
個人開発者の視点から(実体験メモ)
まとめ
MCP サーバーの自作は、AI エージェント開発における最も強力な拡張手段の一つです。本記事で解説した内容を振り返ります。
MCP のアーキテクチャ(ホスト・クライアント・サーバー)と3つのプリミティブ(Tools・Resources・Prompts)
TypeScript + Zod による型安全なツール定義
認証・レート制限・エラーハンドリングの本番パターン
Gemini API との統合(CLI 直接接続とブリッジパターン)
テスト戦略と Docker デプロイ
MCP エコシステムは急速に拡大しており、カスタムサーバーを構築できるスキルは今後ますます重要になるでしょう。MCP の基本的な接続方法は Gemini × MCP サーバー連携ガイド で、マルチエージェント構成の応用は Google ADK × Python マルチエージェント開発ガイド でそれぞれ詳しく解説しています。まずは小さなツールサーバーから始めて、段階的に機能を拡張していくことをお勧めします。