TypeScriptでカスタムMCPサーバーを構築する:完全なアーキテクチャとデプロイメントガイド

目次(18 項目)
Model Context Protocol (MCP) は、AI モデルが外部ツール、リソース、およびコンテキストプロバイダーと対話するための標準化されたインターフェースを定義します。このガイドでは、TypeScript と公式の @modelcontextprotocol/sdk を使用してカスタム MCP サーバーを構築する方法を詳しく説明し、堅牢なアーキテクチャ、スキーマ検証、トランスポートメカニズム、およびクラウドデプロイメントに焦点を当てます。
コア MCP サーバーアーキテクチャ
MCP サーバーは基本的に受信 MCPRequest オブジェクトを処理し、MCPResponse オブジェクトを生成します。サーバーの責任には以下が含まれます。
- トランスポート層: 通信の処理(例: HTTP、stdio)。
- リクエストの逆直列化と検証: 定義されたスキーマに対して
MCPRequestペイロードを解析および検証します。 - ツール/リソースのオーケストレーション: 要求されたツールを実行したり、指定されたリソースを取得したりします。
- レスポンスの直列化:
MCPResponseオブジェクトのフォーマット。
Zod をスキーマ検証に、Google Cloud Run をデプロイに活用し、stdio と Server-Sent Events (SSE) の両方のトランスポートをサポートするサーバーを実装します。
プロジェクトのセットアップ
新しい TypeScript プロジェクトを初期化します。
mkdir mcp-server-ts
cd mcp-server-ts
npm init -y
npm install typescript @types/node ts-node zod @modelcontextprotocol/sdk express @types/express @modelcontextprotocol/zod
npm install --save-dev @types/ws ws
npx tsc --init
厳密性と ESNext モジュール用に tsconfig.json を設定します。
// tsconfig.json
{
"compilerOptions": {
"target": "es2021",
"module": "commonjs",
"rootDir": "./src",
"outDir": "./dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"resolveJsonModule": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}
Zod を使用したスキーマ定義と検証
@modelcontextprotocol/zod パッケージは、MCP 型の Zod スキーマを提供し、厳密な型安全性と検証を保証します。
// src/schemas.ts
import {
MCPRequestSchema,
MCPResponseSchema,
ToolDefinitionSchema,
ResourceDefinitionSchema,
PromptTemplateDefinitionSchema,
ToolCallSchema,
ToolResultSchema,
ResourceFetchSchema,
ResourceDataSchema,
PromptTemplateRenderSchema,
PromptTemplateOutputSchema,
} from '@modelcontextprotocol/zod';
// Export all necessary schemas for use throughout the server
export {
MCPRequestSchema,
MCPResponseSchema,
ToolDefinitionSchema,
ResourceDefinitionSchema,
PromptTemplateDefinitionSchema,
ToolCallSchema,
ToolResultSchema,
ResourceFetchSchema,
ResourceDataSchema,
PromptTemplateRenderSchema,
PromptTemplateOutputSchema,
};
// Define custom tool/resource schemas if needed, extending base MCP types
// Example: A custom tool that takes a 'query' string
import { z } from 'zod';
export const SearchToolInputSchema = z.object({
query: z.string().describe("The search query to execute."),
});
export const SearchToolOutputSchema = z.object({
results: z.array(z.string()).describe("A list of search results."),
});
export const SearchToolDefinition = ToolDefinitionSchema.extend({
name: z.literal("search"),
description: z.literal("A tool for performing web searches."),
input_schema: SearchToolInputSchema,
output_schema: SearchToolOutputSchema,
});
export type SearchToolInput = z.infer<typeof SearchToolInputSchema>;
export type SearchToolOutput = z.infer<typeof SearchToolOutputSchema>;
ツール、リソース、プロンプトテンプレートの実装
MCP サーバーは、tools、resources、および prompt_templates を介して機能を提供します。
// src/handlers.ts
import {
MCPRequest,
MCPResponse,
ToolCall,
ResourceFetch,
PromptTemplateRender,
ToolDefinition,
ResourceDefinition,
PromptTemplateDefinition,
} from '@modelcontextprotocol/sdk';
import {
SearchToolInput,
SearchToolOutput,
SearchToolDefinition,
} from './schemas';
// --- Tool Implementations ---
async function executeSearchTool(input: SearchToolInput): Promise<SearchToolOutput> {
console.log(`Executing search for: ${input.query}`);
// Simulate an external API call
await new Promise(resolve => setTimeout(resolve, 500));
const results = [
`Result 1 for "${input.query}"`,
`Result 2 for "${input.query}"`,
];
return { results };
}
// Map tool names to their execution functions
const toolExecutors: Record<string, (input: any) => Promise<any>> = {
'search': executeSearchTool,
};
// --- Resource Implementations ---
async function fetchDocumentationResource(id: string): Promise<string> {
console.log(`Fetching documentation for ID: ${id}`);
// Simulate fetching from a database or file system
await new Promise(resolve => setTimeout(resolve, 300));
switch (id) {
case 'mcp-overview':
return "The Model Context Protocol (MCP) standardizes AI model interaction with external systems.";
case 'sdk-usage':
return "The @modelcontextprotocol/sdk provides utilities for building MCP clients and servers.";
default:
throw new Error(`Resource with ID '${id}' not found.`);
}
}
// Map resource names to their fetch functions
const resourceFetchers: Record<string, (id: string) => Promise<any>> = {
'documentation': fetchDocumentationResource,
};
// --- Prompt Template Implementations ---
async function renderSummaryTemplate(variables: Record<string, any>): Promise<string> {
console.log(`Rendering summary template with variables: ${JSON.stringify(variables)}`);
const { document, length } = variables;
if (!document) throw new Error("Missing 'document' variable for summary template.");
// Simulate a complex templating engine
await new Promise(resolve => setTimeout(resolve, 100));
return `Here is a ${length || 'brief'} summary of the document: "${document.substring(0, 50)}..."`;
}
// Map prompt template names to their render functions
const promptTemplateRenderers: Record<string, (variables: Record<string, any>) => Promise<string>> = {
'summary': renderSummaryTemplate,
};
// --- MCP Request Handler ---
export async function handleMCPRequest(request: MCPRequest): Promise<MCPResponse> {
const response: MCPResponse = {
request_id: request.request_id,
tool_results: [],
resource_data: [],
prompt_template_outputs: [],
error: undefined,
};
try {
// Handle tool calls
if (request.tool_calls) {
for (const call of request.tool_calls) {
const executor = toolExecutors[call.name];
if (!executor) {
response.tool_results.push({
call_id: call.call_id,
error: `Tool '${call.name}' not found.`,
});
continue;
}
try {
const output = await executor(call.input);
response.tool_results.push({
call_id: call.call_id,
output: output,
});
} catch (toolError: any) {
response.tool_results.push({
call_id: call.call_id,
error: toolError.message || 'Tool execution failed.',
});
}
}
}
// Handle resource fetches
if (request.resource_fetches) {
for (const fetch of request.resource_fetches) {
const fetcher = resourceFetchers[fetch.name];
if (!fetcher) {
response.resource_data.push({
fetch_id: fetch.fetch_id,
error: `Resource '${fetch.name}' not found.`,
});
continue;
}
try {
const data = await fetcher(fetch.id);
response.resource_data.push({
fetch_id: fetch.fetch_id,
data: data,
});
} catch (resourceError: any) {
response.resource_data.push({
fetch_id: fetch.fetch_id,
error: resourceError.message || 'Resource fetch failed.',
});
}
}
}
// Handle prompt template renders
if (request.prompt_template_renders) {
for (const render of request.prompt_template_renders) {
const renderer = promptTemplateRenderers[render.name];
if (!renderer) {
response.prompt_template_outputs.push({
render_id: render.render_id,
error: `Prompt template '${render.name}' not found.`,
});
continue;
}
try {
const output = await renderer(render.variables);
response.prompt_template_outputs.push({
render_id: render.render_id,
output: output,
});
} catch (templateError: any) {
response.prompt_template_outputs.push({
render_id: render.render_id,
error: templateError.message || 'Prompt template rendering failed.',
});
}
}
}
} catch (e: any) {
console.error("Unhandled error in MCP request handler:", e);
response.error = e.message || 'Internal server error.';
}
return response;
}
// --- Server Capabilities (for /mcp/capabilities endpoint) ---
export const serverCapabilities = {
tools: [
SearchToolDefinition.parse({
name: 'search',
description: 'A tool for performing web searches.',
input_schema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] },
output_schema: { type: 'object', properties: { results: { type: 'array', items: { type: 'string' } } }, required: ['results'] },
}),
] as ToolDefinition[],
resources: [
ResourceDefinition.parse({
name: 'documentation',
description: 'Provides access to internal documentation articles.',
id_schema: { type: 'string' },
data_schema: { type: 'string' },
}),
] as ResourceDefinition[],
prompt_templates: [
PromptTemplateDefinition.parse({
name: 'summary',
description: 'Generates a summary of a given document.',
variables_schema: {
type: 'object',
properties: {
document: { type: 'string', description: 'The document content to summarize.' },
length: { type: 'string', enum: ['brief', 'detailed'], description: 'Desired length of the summary.' }
},
required: ['document']
},
output_schema: { type: 'string' },
}),
] as PromptTemplateDefinition[],
};
トランスポート層: stdio vs. SSE
MCP はさまざまなトランスポートをサポートしています。stdio(ローカル開発/CLI ツール用)と SSE(HTTP ベースのストリーミングインタラクション用)の両方を実装します。
stdio トランスポート
stdio トランスポートは MCPRequest から stdin を読み取り、stdout に MCPResponse を書き込みます。各メッセージにはその長さがプレフィックスとして付加されます。
// src/stdioServer.ts
import { MCPRequestSchema, MCPResponseSchema } from './schemas';
import { handleMCPRequest } from './handlers';
import {
readMCPMessage,
writeMCPMessage,
MCPMessage,
} from '@modelcontextprotocol/sdk/stdio';
async function startStdioServer() {
console.log("MCP stdio server started. Waiting for input...");
process.stdin.on('data', async (chunk) => {
try {
const messages = readMCPMessage(chunk);
for (const message of messages) {
if (message.type === 'request') {
const parsedRequest = MCPRequestSchema.parse(message.payload);
console.log(`Received MCP Request: ${parsedRequest.request_id}`);
const response = await handleMCPRequest(parsedRequest);
writeMCPMessage(process.stdout, { type: 'response', payload: response });
} else {
console.warn(`Received unexpected MCP message type: ${message.type}`);
}
}
} catch (error: any) {
console.error("Error processing stdio input:", error);
// Attempt to send an error response if possible
if (error.request_id) { // If we can extract request_id from a partially parsed request
writeMCPMessage(process.stdout, {
type: 'response',
payload: {
request_id: error.request_id,
error: `Invalid MCP request: ${error.message}`,
},
});
} else {
// Fallback for unparseable requests
console.error("Failed to parse incoming MCP request. Cannot send specific error response.");
}
}
});
process.stdin.on('end', () => {
console.log("MCP stdio server stdin ended.");
});
process.stdin.on('error', (err) => {
console.error("MCP stdio server stdin error:", err);
});
}
if (require.main === module) {
startStdioServer();
}
Server-Sent Events (SSE) トランスポート
SSE は、ストリーミングレスポンスのための永続的な HTTP 接続を提供します。これはウェブベースのクライアントに最適です。
// src/sseServer.ts
import express from 'express';
import { v4 as uuidv4 } from 'uuid';
import { MCPRequestSchema, MCPResponseSchema } from './schemas';
import { handleMCPRequest, serverCapabilities } from './handlers';
import {
MCPRequest,
MCPResponse,
MCPCapabilities,
} from '@modelcontextprotocol/sdk';
const app = express();
app.use(express.json()); // For parsing application/json
const PORT = process.env.PORT || 8080;
// Middleware to set SSE headers
const setSSEHeaders = (res: express.Response) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('X-Accel-Buffering', 'no'); // Disable Nginx buffering
};
// Endpoint for server capabilities
app.get('/mcp/capabilities', (req, res) => {
res.json(serverCapabilities);
});
// SSE endpoint for MCP requests
app.post('/mcp/stream', async (req, res) => {
setSSEHeaders(res);
const requestId = uuidv4(); // Generate a unique ID for this request stream
try {
// Validate incoming request body
const mcpRequest: MCPRequest = MCPRequestSchema.parse({
request_id: requestId, // Override or ensure request_id is present
...req.body,
});
console.log(`Received SSE MCP Request: ${mcpRequest.request_id}`);
// Send initial "request_received" event
res.write(`event: request_received\n`);
res.write(`data: ${JSON.stringify({ request_id: mcpRequest.request_id })}\n\n`);
// Process the request
const mcpResponse = await handleMCPRequest(mcpRequest);
// Send the final "response" event
res.write(`event: response\n`);
res.write(`data: ${JSON.stringify(mcpResponse)}\n\n`);
} catch (error: any) {
console.error("Error processing SSE MCP request:", error);
const errorResponse: MCPResponse = {
request_id: requestId,
error: `Invalid MCP request or internal server error: ${error.message}`,
};
res.write(`event: error\n`);
res.write(`data: ${JSON.stringify(errorResponse)}\n\n`);
} finally {
res.end(); // Close the connection after sending response or error
}
});
// Health check endpoint
app.get('/healthz', (req, res) => {
res.status(200).send('OK');
});
export function startSseServer() {
app.listen(PORT, () => {
console.log(`MCP SSE server listening on port ${PORT}`);
});
}
if (require.main === module) {
startSseServer();
}
メインサーバーのエントリポイント
環境変数に基づいてサーバータイプを選択する単一のエントリポイント。
// src/index.ts
import { startStdioServer } from './stdioServer';
import { startSseServer } from './sseServer';
const SERVER_TYPE = process.env.MCP_SERVER_TYPE || 'sse'; // Default to SSE
if (SERVER_TYPE === 'stdio') {
startStdioServer();
} else if (SERVER_TYPE === 'sse') {
startSseServer();
} else {
console.error(`Unknown MCP_SERVER_TYPE: ${SERVER_TYPE}. Must be 'stdio' or 'sse'.`);
process.exit(1);
}
トランスポートの比較
| 機能 | stdio | SSE (HTTP) |
|---|---|---|
| プロトコル | カスタム長プレフィックス付きバイナリ/テキスト | HTTP/1.1、テキストベース |
| 接続 | 永続的 (stdin/stdout) | 永続的 (単一のリクエスト/レスポンスストリーム) |
| ストリーミング | 双方向 (別々のパイプ経由) | 単方向 (サーバーからクライアントへ) |
| オーバーヘッド | 最小 | HTTP ヘッダー、イベントフレーミング |
| 複雑さ | 低 (SDK がフレーミングを処理) | 中程度 (HTTP サーバー、ヘッダー、イベント形式) |
| ユースケース | CLI ツール、ローカルエージェント、コンテナ内部 | ウェブクライアント、クラウド関数、外部サービス |
| 認証 | OS レベルの権限 | HTTP ヘッダー (Bearer トークン、API キー) |
| エラー処理 | アプリケーションレベルのメッセージ | HTTP ステータスコード、アプリケーションレベルのイベント |
| スケーラビリティ | 単一プロセス | 水平スケーラブル (ロードバランサー) |
Docker を使用したコンテナ化
一貫したデプロイのためにアプリケーションをコンテナ化します。
# Dockerfile
# Use a slim Node.js image for smaller size
FROM node:20-slim AS builder
WORKDIR /app
# Copy package.json and package-lock.json first to leverage Docker cache
COPY package*.json ./
RUN npm install --omit=dev
# Copy source code
COPY . .
# Build TypeScript code
RUN npm run build
# --- Production Stage ---
FROM node:20-slim
WORKDIR /app
# Copy only necessary files from the builder stage
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
# Expose the port for the SSE server
EXPOSE 8080
# Set environment variable for SSE server type
ENV MCP_SERVER_TYPE=sse
# Command to run the SSE server
CMD ["node", "dist/index.js"]
Docker イメージをビルドします。
docker build -t mcp-server-ts .
ローカルで実行 (SSE):
docker run -p 8080:8080 mcp-server-ts
curl でテストします。
curl -X POST -H "Content-Type: application/json" \
-d '{
"tool_calls": [
{ "call_id": "call-1", "name": "search", "input": { "query": "latest AI research" } }
],
"resource_fetches": [
{ "fetch_id": "fetch-1", "name": "documentation", "id": "mcp-overview" }
],
"prompt_template_renders": [
{ "render_id": "render-1", "name": "summary", "variables": { "document": "This is a very long document about the history of computing.", "length": "brief" } }
]
}' \
http://localhost:8080/mcp/stream
期待される出力 (ストリーミングイベント):
event: request_received
data: {"request_id":"<uuid>"}
event: response
data: {"request_id":"<uuid>","tool_results":[{"call_id":"call-1","output":{"results":["Result 1 for \"latest AI research\"","Result 2 for \"latest AI research\""]}}],"resource_data":[{"fetch_id":"fetch-1","data":"The Model Context Protocol (MCP) standardizes AI model interaction with external systems."}],"prompt_template_outputs":[{"render_id":"render-1","output":"Here is a brief summary of the document: \"This is a very long document about the history of compu...\""}]}
Google Cloud Run へのデプロイ
Google Cloud Run は、コンテナ化されたアプリケーションに最適なサーバーレスプラットフォームであり、自動スケーリングと従量課金制を提供します。
前提条件
- Google Cloud プロジェクトが設定されていること。
gcloudCLI がインストールされ、認証されていること。- Cloud Run API が有効になっていること。
デプロイ手順
-
Docker イメージをビルドして Google Container Registry (GCR) または Artifact Registry にプッシュします。
bash# For GCR (older, but widely used) docker tag mcp-server-ts gcr.io/<YOUR_PROJECT_ID>/mcp-server-ts:latest docker push gcr.io/<YOUR_PROJECT_ID>/mcp-server-ts:latest # For Artifact Registry (recommended) # Enable Artifact Registry API: gcloud services enable artifactregistry.googleapis.com # Create a repository: gcloud artifacts repositories create mcp-repo --repository-format=docker --location=us-central1 --description="MCP Docker repository" docker tag mcp-server-ts us-central1-docker.pkg.dev/<YOUR_PROJECT_ID>/mcp-repo/mcp-server-ts:latest docker push us-central1-docker.pkg.dev/<YOUR_PROJECT_ID>/mcp-repo/mcp-server-ts:latest -
Cloud Run にデプロイします。
bashgcloud run deploy mcp-server-ts \ --image gcr.io/<YOUR_PROJECT_ID>/mcp-server-ts:latest \ --platform managed \ --region us-central1 \ --allow-unauthenticated \ --port 8080 \ --min-instances 0 \ --max-instances 10 \ --memory 512Mi \ --cpu 1 \ --set-env-vars MCP_SERVER_TYPE=sse \ --project <YOUR_PROJECT_ID>--allow-unauthenticated: 公開アクセス用。本番環境では--no-allow-unauthenticatedを検討し、IAM を使用してください。--port 8080:DockerfileのEXPOSEと一致します。Cloud Run はこのポートにトラフィックを自動的にルーティングします。--set-env-vars MCP_SERVER_TYPE=sse: SSE サーバーが起動することを保証します。
-
IAM 認証 (本番環境向け推奨):
--no-allow-unauthenticatedが使用されている場合、クライアントは認証する必要があります。GCP 内のサービス間通信には、サービスアカウントを使用します。-
クライアントサービスアカウント: クライアント(例: AI モデルサービス)用のサービスアカウントを作成します。
-
Invoker ロールの付与: Cloud Run サービスでクライアントサービスアカウントに
roles/run.invokerロールを付与します。bashgcloud run services add-iam-policy-binding mcp-server-ts \ --member="serviceAccount:<CLIENT_SERVICE_ACCOUNT_EMAIL>" \ --role="roles/run.invoker" \ --region us-central1 \ --platform managed -
クライアント側の認証: GCP サービスからリクエストを行う場合、
gcloudクライアントライブラリまたはcurlとgcloud auth print-identity-tokenを使用すると、認証を自動的に処理できます。bash# Example curl with authenticated token SERVICE_URL=$(gcloud run services describe mcp-server-ts --platform managed --region us-central1 --format 'value(status.url)') TOKEN=$(gcloud auth print-identity-token) curl -X POST -H "Content-Type: application/json" \ -H "Authorization: Bearer ${TOKEN}" \ -d '{ "tool_calls": [ { "call_id": "call-1", "name": "search", "input": { "query": "cloud run deployment" } } ] }' \ "${SERVICE_URL}/mcp/stream"
-
本番環境での注意点とトラブルシューティング
-
Cloud Run での
403 Forbidden:- 原因: デプロイ中に
--no-allow-unauthenticatedが使用されましたが、クライアントが Cloud Run invoker トークンを含む有効なAuthorizationヘッダーを提供していません。 - 修正:
- 呼び出し元のサービスアカウントが Cloud Run サービスで
roles/run.invokerを持っていることを確認します。 - クライアントが ID トークンを正しく生成して添付していることを確認します(例:
gcloud auth print-identity-tokenまたは Google Auth Libraries を使用)。 - 公開アクセスを意図している場合は、
--allow-unauthenticatedで再デプロイします。
- 呼び出し元のサービスアカウントが Cloud Run サービスで
- 原因: デプロイ中に
-
500 Internal Server Error/Container instance crashed:- 原因: 起動中またはリクエスト処理中にアプリケーションがクラッシュしました。一般的な問題には、誤った
PORT環境変数、未処理の例外、メモリ不足エラーなどがあります。 - 修正:
- Cloud Run のログ (
gcloud run services logs read mcp-server-ts --limit 100) を確認します。Error: listen EADDRINUSE(Cloud Run では発生しにくいポート競合)、UnhandledPromiseRejectionWarning、またはMemory limit exceededを探します。 - アプリケーションが
process.env.PORTでリッスンしていることを確認します(Cloud Run がこれを挿入します)。私たちのsseServer.tsはprocess.env.PORT || 8080を正しく使用しています。 - ログがリソース枯渇を示している場合は、メモリ (
--memory) または CPU (--cpu) を増やします。 - 非同期操作の周りに、より堅牢な
try...catchブロックを追加します。
- Cloud Run のログ (
- 原因: 起動中またはリクエスト処理中にアプリケーションがクラッシュしました。一般的な問題には、誤った
-
400 Bad Requestfrom/mcp/stream:- 原因: 入力 JSON ペイロードが
MCPRequestSchemaに準拠していません。これは、必須フィールドの欠落やデータ型の誤りによってよく発生します。 - 修正:
- クライアントのリクエストボディを
MCPRequestSchema(およびカスタムスキーマ)と照合して確認します。 - Zod 検証エラーを出力するために、サーバーの
catchブロックにMCPRequestSchema.parseのより詳細なログを追加します。 - 例:
typescript
import { ZodError } from 'zod'; // ... inside try/catch for MCPRequestSchema.parse } catch (error: any) { if (error instanceof ZodError) { console.error("Zod validation error:", JSON.stringify(error.errors, null, 2)); // ... send detailed error response } else { console.error("Non-Zod error:", error); } }
- クライアントのリクエストボディを
- 原因: 入力 JSON ペイロードが
-
応答の遅延 / タイムアウト:
- 原因: ツール実行、リソース取得、またはプロンプトテンプレートのレンダリングに時間がかかっています。Cloud Run にはデフォルトのリクエストタイムアウト(例: 5 分)があります。
- 修正:
- ハンドラーロジックを最適化します。
- 非常に長いタスクには非同期パターンを実装します(例: Cloud Tasks または Pub/Sub にオフロードし、MCP サーバーが結果をポーリングするか、Webhook を受信するようにします)。
- Cloud Run のリクエストタイムアウトを増やします (
--timeout)。 - 外部 API 呼び出しに適切なタイムアウトが設定されていることを確認します。
-
SSE 接続が途中で切断される:
- 原因: プロキシ(Nginx や Cloud Load Balancer など)が応答をバッファリングし、即時ストリーミングを妨げることがあります。Cloud Run 自体は SSE をうまく処理しますが、中間プロキシが干渉する可能性があります。
- 修正:
X-Accel-Buffering: noヘッダーが設定されていることを確認します(私たちのsetSSEHeaders関数がこれを行います)。- Cloud Run の前に他のプロキシがバッファリングしていないことを確認します。
- キープアライブメカニズム: SSE は本質的にキープアライブですが、サーバーが長時間アイドル状態になると、一部のネットワークコンポーネントが接続を閉じる可能性があります。実際のデータイベント間にサーバーが長時間アイドル状態になる可能性がある場合は、定期的な「ハートビート」コメント (
: comment\n\n) を送信することを検討してください。
よくある質問
Q1: MCP サーバーに新しいカスタムツールやリソースを追加するにはどうすればよいですか?
A1:
- スキーマの定義:
src/schemas.tsでツールの入力と出力(またはリソースの ID とデータ)の Zod スキーマを作成します。ToolDefinitionSchemaまたはResourceDefinitionSchemaを拡張します。 - ロジックの実装:
src/handlers.tsで、解析された入力を受け取り、出力を返す非同期関数を作成します。 - Executor の登録:
src/handlers.tsのtoolExecutorsまたはresourceFetchersマップに新しい関数を追加します。 - 機能の更新: クライアントが検出できるように、
src/handlers.tsのserverCapabilities配列にToolDefinitionまたはResourceDefinitionを追加します。 - 再ビルドとデプロイ: Docker イメージを再ビルドし、Cloud Run に再デプロイします。
Q2: 双方向ストリーミングに SSE の代わりに WebSockets を使用できますか?
A2: MCP 自体はトランスポートに依存しませんが、@modelcontextprotocol/sdk は現在 stdio と SSE に特化したヘルパーを提供しています。WebSockets を使用するには、トランスポート層のカスタム実装が必要です。次のことを行う必要があります。
- WebSocket サーバーをセットアップします(例: Express と
wsライブラリを使用)。 - WebSocket 上でメッセージフレーミング(例:
typeとpayloadフィールドを持つ JSON メッセージ)を実装します。 - 受信 WebSocket メッセージを処理し、同じ接続を介して応答を返すように
handleMCPRequestを適応させます。 これは可能ですが、提供されている SSE/stdio の例よりも手作業が多くなります。
Q3: Cloud Run MCP サーバーでシークレット(例: 外部ツール用の API キー)を管理するにはどうすればよいですか?
A3:
- Cloud Secret Manager: シークレットを Google Cloud Secret Manager に保存します。
- アクセス権の付与: Cloud Run サービスアカウント(デフォルトでは
<YOUR_PROJECT_ID>@appspot.gserviceaccount.com)に、特定のシークレットに対するSecret Manager Secret Accessorロールを付与します。 - 環境変数としてマウント: デプロイ時にシークレットを環境変数としてマウントするように Cloud Run を構成します。
アプリケーションはbash
gcloud run deploy mcp-server-ts ... \ --set-secrets=EXTERNAL_API_KEY=EXTERNAL_API_KEY:latest \ ...process.env.EXTERNAL_API_KEYにアクセスできるようになります。 - 直接アクセス(あまり一般的ではない): または、アプリケーションは Google Cloud クライアントライブラリを使用して Secret Manager API を直接呼び出すこともできますが、一般的には環境変数の方が設定が簡単です。
Q4: ツール実行が Cloud Run のリクエストタイムアウトよりも長くかかる場合はどうなりますか?
A4: 長時間実行される操作(例: 複雑な ML モデルの推論、大規模なデータ処理)の場合、MCP リクエスト内での直接同期実行は適切ではありません。次のパターンを検討してください。
- 非同期タスクキュー: MCP サーバーは、長時間実行されるタスクを開始します(例: Google Cloud Pub/Sub にメッセージを公開したり、Cloud Tasks エントリを作成したりすることによって)。その後、タスクが受け入れられたことを示す
MCPResponseをすぐに返し、場合によってはtask_idを含めます。 - ポーリング: クライアントは、完了を確認し、結果を取得するために、
task_idを使用して MCP サーバー(または別のサービス)の別のエンドポイントを定期的にポーリングできます。 - Webhook: 長時間実行されるタスクは、完了後、MCP サーバー(または別のサービス)の専用エンドポイントに Webhook 通知を送信して、クライアントが永続的な接続を維持している場合、または非同期更新を受信する手段がある場合に、結果をクライアントにプッシュできます。 これにより、リクエスト/レスポンスサイクルと実際の実行時間が分離されます。
Q5: stdio サーバーをローカルでテストするにはどうすればよいですか?
A5: JSON データをその stdin にパイプできます。
- サーバーをコンパイルします:
npm run build - リクエストファイルを作成します:
json
// request.json { "request_id": "test-stdio-1", "tool_calls": [ { "call_id": "call-stdio-1", "name": "search", "input": { "query": "stdio transport" } } ] } @modelcontextprotocol/sdkからstdio-clientを使用してリクエストを送信します:または、長さプレフィックス付きメッセージを手動で構築することもできます。bash# Install stdio-client globally or locally npm install -g @modelcontextprotocol/sdk # Run the server in one terminal node dist/index.js # In another terminal, send the request stdio-client --request-file request.json --server-command "node dist/index.js"bash# In one terminal: node dist/index.js # In another terminal: # Get the length of the JSON string # echo '{"request_id":"test-stdio-1","tool_calls":[{"call_id":"call-stdio-1","name":"search","input":{"query":"stdio transport"}}]}' | wc -c # (Let's say it's 123 bytes) # Then send: echo -n -e '\x00\x00\x00\x7B{"request_id":"test-stdio-1","tool_calls":[{"call_id":"call-stdio-1","name":"search","input":{"query":"stdio transport"}}]}' | node dist/index.js # Note: The \x00\x00\x00\x7B represents the 4
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

カスタムMCPクライアントの構築:あらゆるLLMを複数Model Context Protocolサーバーに接続する
あらゆるLLMを複数Model Context Protocolサーバーに接続するカスタムmcpクライアントを、本番環境レベルのアーキテクチャとコード例で構築するための包括的なガイド。
Read more
PythonとClaudeでゼロから始めるMCPサーバー構築:完全ガイド
Python、FastMCP、型付きツール、リソース、Claude Desktop連携を用いて、本番環境向けModel Context Protocol (MCP)サーバーを構築するステップバイステップガイドです。
Read more
ソフトウェアエンジニアリングにおけるAIエージェント:実際に機能するアーキテクチャパターン
コパイロットから自律エージェントへの移行:ツール呼び出しループ、MCP統合、メモリアーキテクチャ、マルチエージェント連携、そしてエージェントをデプロイする前にすべてのエンジニアリングチームが定義すべき安全境界について解説します。
Read more