•24 min read

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

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

Model Context Protocol (MCP) は、AI モデルが外部ツール、リソース、およびコンテキストプロバイダーと対話するための標準化されたインターフェースを定義します。このガイドでは、TypeScript と公式の @modelcontextprotocol/sdk を使用してカスタム MCP サーバーを構築する方法を詳しく説明し、堅牢なアーキテクチャ、スキーマ検証、トランスポートメカニズム、およびクラウドデプロイメントに焦点を当てます。

Audio Briefing
0:00 / 0:00

コア MCP サーバーアーキテクチャ

MCP サーバーは基本的に受信 MCPRequest オブジェクトを処理し、MCPResponse オブジェクトを生成します。サーバーの責任には以下が含まれます。

  1. トランスポート層: 通信の処理(例: HTTP、stdio)。
  2. リクエストの逆直列化と検証: 定義されたスキーマに対して MCPRequest ペイロードを解析および検証します。
  3. ツール/リソースのオーケストレーション: 要求されたツールを実行したり、指定されたリソースを取得したりします。
  4. レスポンスの直列化: 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);
}

トランスポートの比較

機能stdioSSE (HTTP)
プロトコルカスタム長プレフィックス付きバイナリ/テキストHTTP/1.1、テキストベース
接続永続的 (stdin/stdout)永続的 (単一のリクエスト/レスポンスストリーム)
ストリーミング双方向 (別々のパイプ経由)単方向 (サーバーからクライアントへ)
オーバーヘッド最小HTTP ヘッダー、イベントフレーミング
複雑さ低 (SDK がフレーミングを処理)中程度 (HTTP サーバー、ヘッダー、イベント形式)
ユースケースCLI ツール、ローカルエージェント、コンテナ内部ウェブクライアント、クラウド関数、外部サービス
認証OS レベルの権限HTTP ヘッダー (Bearer トークン、API キー)
エラー処理アプリケーションレベルのメッセージHTTP ステータスコード、アプリケーションレベルのイベント
スケーラビリティ単一プロセス水平スケーラブル (ロードバランサー)
Advertisement

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 プロジェクトが設定されていること。
  • gcloud CLI がインストールされ、認証されていること。
  • Cloud Run API が有効になっていること。

デプロイ手順

  1. Docker イメージをビルドして Google Container Registry (GCR) または Artifact Registry にプッシュします。

    # 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
    
  2. Cloud Run にデプロイします。

    gcloud 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 サーバーが起動することを保証します。
  3. IAM 認証 (本番環境向け推奨):

    --no-allow-unauthenticated が使用されている場合、クライアントは認証する必要があります。GCP 内のサービス間通信には、サービスアカウントを使用します。

    • クライアントサービスアカウント: クライアント(例: AI モデルサービス)用のサービスアカウントを作成します。

    • Invoker ロールの付与: Cloud Run サービスでクライアントサービスアカウントに roles/run.invoker ロールを付与します。

      gcloud 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 を使用すると、認証を自動的に処理できます。

      # 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"
      

本番環境での注意点とトラブルシューティング

  1. 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 で再デプロイします。
  2. 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 ブロックを追加します。
  3. 400 Bad Request from /mcp/stream:

    • 原因: 入力 JSON ペイロードが MCPRequestSchema に準拠していません。これは、必須フィールドの欠落やデータ型の誤りによってよく発生します。
    • 修正:
      • クライアントのリクエストボディを MCPRequestSchema(およびカスタムスキーマ)と照合して確認します。
      • Zod 検証エラーを出力するために、サーバーの catch ブロックに MCPRequestSchema.parse のより詳細なログを追加します。
      • 例:
        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);
          }
        }
        
  4. 応答の遅延 / タイムアウト:

    • 原因: ツール実行、リソース取得、またはプロンプトテンプレートのレンダリングに時間がかかっています。Cloud Run にはデフォルトのリクエストタイムアウト(例: 5 分)があります。
    • 修正:
      • ハンドラーロジックを最適化します。
      • 非常に長いタスクには非同期パターンを実装します(例: Cloud Tasks または Pub/Sub にオフロードし、MCP サーバーが結果をポーリングするか、Webhook を受信するようにします)。
      • Cloud Run のリクエストタイムアウトを増やします (--timeout)。
      • 外部 API 呼び出しに適切なタイムアウトが設定されていることを確認します。
  5. SSE 接続が途中で切断される:

    • 原因: プロキシ(Nginx や Cloud Load Balancer など)が応答をバッファリングし、即時ストリーミングを妨げることがあります。Cloud Run 自体は SSE をうまく処理しますが、中間プロキシが干渉する可能性があります。
    • 修正:
      • X-Accel-Buffering: no ヘッダーが設定されていることを確認します(私たちの setSSEHeaders 関数がこれを行います)。
      • Cloud Run の前に他のプロキシがバッファリングしていないことを確認します。
      • キープアライブメカニズム: SSE は本質的にキープアライブですが、サーバーが長時間アイドル状態になると、一部のネットワークコンポーネントが接続を閉じる可能性があります。実際のデータイベント間にサーバーが長時間アイドル状態になる可能性がある場合は、定期的な「ハートビート」コメント (: comment\n\n) を送信することを検討してください。
Advertisement

よくある質問

Q1: MCP サーバーに新しいカスタムツールやリソースを追加するにはどうすればよいですか?

A1:

  1. スキーマの定義: src/schemas.ts でツールの入力と出力(またはリソースの ID とデータ)の Zod スキーマを作成します。ToolDefinitionSchema または ResourceDefinitionSchema を拡張します。
  2. ロジックの実装: src/handlers.ts で、解析された入力を受け取り、出力を返す非同期関数を作成します。
  3. Executor の登録: src/handlers.ts の toolExecutors または resourceFetchers マップに新しい関数を追加します。
  4. 機能の更新: クライアントが検出できるように、src/handlers.ts の serverCapabilities 配列に ToolDefinition または ResourceDefinition を追加します。
  5. 再ビルドとデプロイ: Docker イメージを再ビルドし、Cloud Run に再デプロイします。

Q2: 双方向ストリーミングに SSE の代わりに WebSockets を使用できますか?

A2: MCP 自体はトランスポートに依存しませんが、@modelcontextprotocol/sdk は現在 stdio と SSE に特化したヘルパーを提供しています。WebSockets を使用するには、トランスポート層のカスタム実装が必要です。次のことを行う必要があります。

  1. WebSocket サーバーをセットアップします(例: Express と ws ライブラリを使用)。
  2. WebSocket 上でメッセージフレーミング(例: type と payload フィールドを持つ JSON メッセージ)を実装します。
  3. 受信 WebSocket メッセージを処理し、同じ接続を介して応答を返すように handleMCPRequest を適応させます。 これは可能ですが、提供されている SSE/stdio の例よりも手作業が多くなります。

Q3: Cloud Run MCP サーバーでシークレット(例: 外部ツール用の API キー)を管理するにはどうすればよいですか?

A3:

  1. Cloud Secret Manager: シークレットを Google Cloud Secret Manager に保存します。
  2. アクセス権の付与: Cloud Run サービスアカウント(デフォルトでは <YOUR_PROJECT_ID>@appspot.gserviceaccount.com)に、特定のシークレットに対する Secret Manager Secret Accessor ロールを付与します。
  3. 環境変数としてマウント: デプロイ時にシークレットを環境変数としてマウントするように Cloud Run を構成します。
    gcloud run deploy mcp-server-ts ... \
      --set-secrets=EXTERNAL_API_KEY=EXTERNAL_API_KEY:latest \
      ...
    
    アプリケーションは process.env.EXTERNAL_API_KEY にアクセスできるようになります。
  4. 直接アクセス(あまり一般的ではない): または、アプリケーションは Google Cloud クライアントライブラリを使用して Secret Manager API を直接呼び出すこともできますが、一般的には環境変数の方が設定が簡単です。

Q4: ツール実行が Cloud Run のリクエストタイムアウトよりも長くかかる場合はどうなりますか?

A4: 長時間実行される操作(例: 複雑な ML モデルの推論、大規模なデータ処理)の場合、MCP リクエスト内での直接同期実行は適切ではありません。次のパターンを検討してください。

  1. 非同期タスクキュー: MCP サーバーは、長時間実行されるタスクを開始します(例: Google Cloud Pub/Sub にメッセージを公開したり、Cloud Tasks エントリを作成したりすることによって)。その後、タスクが受け入れられたことを示す MCPResponse をすぐに返し、場合によっては task_id を含めます。
  2. ポーリング: クライアントは、完了を確認し、結果を取得するために、task_id を使用して MCP サーバー(または別のサービス)の別のエンドポイントを定期的にポーリングできます。
  3. Webhook: 長時間実行されるタスクは、完了後、MCP サーバー(または別のサービス)の専用エンドポイントに Webhook 通知を送信して、クライアントが永続的な接続を維持している場合、または非同期更新を受信する手段がある場合に、結果をクライアントにプッシュできます。 これにより、リクエスト/レスポンスサイクルと実際の実行時間が分離されます。

Q5: stdio サーバーをローカルでテストするにはどうすればよいですか?

A5: JSON データをその stdin にパイプできます。

  1. サーバーをコンパイルします: npm run build
  2. リクエストファイルを作成します:
    // request.json
    {
      "request_id": "test-stdio-1",
      "tool_calls": [
        { "call_id": "call-stdio-1", "name": "search", "input": { "query": "stdio transport" } }
      ]
    }
    
  3. @modelcontextprotocol/sdk から stdio-client を使用してリクエストを送信します:
    # 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"
    
    または、長さプレフィックス付きメッセージを手動で構築することもできます。
    # 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
    
Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement