•22 min read

Xây dựng máy chủ MCP tùy chỉnh bằng TypeScript: Hướng dẫn kiến trúc & triển khai hoàn chỉnh

Xây dựng máy chủ MCP tùy chỉnh bằng TypeScript: Hướng dẫn kiến trúc & triển khai hoàn chỉnh

Giao thức Ngữ cảnh Mô hình (MCP) định nghĩa một giao diện tiêu chuẩn để các mô hình AI tương tác với các công cụ, tài nguyên bên ngoài và nhà cung cấp ngữ cảnh. Hướng dẫn này trình bày chi tiết việc xây dựng một máy chủ MCP tùy chỉnh bằng TypeScript và @modelcontextprotocol/sdk chính thức, tập trung vào kiến trúc mạnh mẽ, xác thực lược đồ, cơ chế truyền tải và triển khai đám mây.

Audio Briefing
0:00 / 0:00

Kiến trúc Máy chủ MCP cốt lõi

Một máy chủ MCP về cơ bản xử lý các đối tượng MCPRequest đến và tạo ra các đối tượng MCPResponse. Trách nhiệm của máy chủ bao gồm:

  1. Lớp Truyền tải: Xử lý giao tiếp (ví dụ: HTTP, stdio).
  2. Giải mã & Xác thực Yêu cầu: Phân tích cú pháp và xác thực các tải trọng MCPRequest dựa trên các lược đồ đã định nghĩa.
  3. Điều phối Công cụ/Tài nguyên: Thực thi các công cụ được yêu cầu hoặc tìm nạp các tài nguyên được chỉ định.
  4. Tuần tự hóa Phản hồi: Định dạng các đối tượng MCPResponse.

Chúng ta sẽ triển khai một máy chủ hỗ trợ cả truyền tải stdio và Server-Sent Events (SSE), tận dụng Zod để xác thực lược đồ và Google Cloud Run để triển khai.

Thiết lập Dự án

Khởi tạo một dự án TypeScript mới:

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

Cấu hình tsconfig.json cho tính nghiêm ngặt và các mô-đun ESNext:

// 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"]
}

Định nghĩa và Xác thực Lược đồ với Zod

Gói @modelcontextprotocol/zod cung cấp các lược đồ Zod cho các loại MCP, đảm bảo an toàn và xác thực kiểu nghiêm ngặt.

// 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>;

Triển khai Công cụ, Tài nguyên và Mẫu Lời nhắc

Các máy chủ MCP hiển thị các khả năng thông qua tools, resources và 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[],
};

Lớp Truyền tải: stdio so với SSE

MCP hỗ trợ nhiều loại truyền tải khác nhau. Chúng ta sẽ triển khai cả stdio (cho các công cụ phát triển cục bộ/CLI) và SSE (cho các tương tác dựa trên HTTP, truyền trực tuyến).

Truyền tải stdio

Truyền tải stdio đọc MCPRequest từ stdin và ghi MCPResponse vào stdout. Mỗi tin nhắn được đặt tiền tố bằng độ dài của nó.

// 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();
}

Truyền tải Server-Sent Events (SSE)

SSE cung cấp một kết nối HTTP liên tục để truyền trực tuyến các phản hồi. Điều này lý tưởng cho các máy khách dựa trên web.

// 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();
}

Điểm vào Máy chủ Chính

Một điểm vào duy nhất để chọn loại máy chủ dựa trên các biến môi trường.

// 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);
}

So sánh Truyền tải

Tính năngstdioSSE (HTTP)
Giao thứcNhị phân/văn bản có tiền tố độ dài tùy chỉnhHTTP/1.1, dựa trên văn bản
Kết nốiLiên tục (stdin/stdout)Liên tục (luồng yêu cầu/phản hồi đơn)
Truyền trực tuyếnHai chiều (qua các đường ống riêng biệt)Một chiều (máy chủ đến máy khách)
Chi phíTối thiểuTiêu đề HTTP, đóng khung sự kiện
Độ phức tạpThấp (SDK xử lý đóng khung)Trung bình (máy chủ HTTP, tiêu đề, định dạng sự kiện)
Trường hợp sử dụngCông cụ CLI, tác nhân cục bộ, nội bộ containerMáy khách web, hàm đám mây, dịch vụ bên ngoài
Xác thựcQuyền cấp hệ điều hànhTiêu đề HTTP (mã thông báo Bearer, khóa API)
Xử lý lỗiThông báo cấp ứng dụngMã trạng thái HTTP, sự kiện cấp ứng dụng
Khả năng mở rộngMột tiến trìnhCó thể mở rộng theo chiều ngang (bộ cân bằng tải)
Advertisement

Đóng gói với Docker

Đóng gói ứng dụng để triển khai nhất quán.

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

Xây dựng ảnh Docker:

docker build -t mcp-server-ts .

Chạy cục bộ (SSE):

docker run -p 8080:8080 mcp-server-ts

Kiểm tra với 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

Đầu ra dự kiến (các sự kiện được truyền trực tuyến):

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...\""}]}

Triển khai lên Google Cloud Run

Google Cloud Run là một nền tảng phi máy chủ lý tưởng cho các ứng dụng đóng gói, cung cấp khả năng tự động mở rộng và thanh toán theo mức sử dụng.

Điều kiện tiên quyết

  • Dự án Google Cloud đã được cấu hình.
  • CLI gcloud đã được cài đặt và xác thực.
  • API Cloud Run đã được bật.

Các bước Triển khai

  1. Xây dựng và Đẩy ảnh Docker lên Google Container Registry (GCR) hoặc 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. Triển khai lên 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: Để truy cập công khai. Đối với sản xuất, hãy cân nhắc --no-allow-unauthenticated và sử dụng IAM.
    • --port 8080: Khớp với EXPOSE trong Dockerfile. Cloud Run tự động định tuyến lưu lượng truy cập đến cổng này.
    • --set-env-vars MCP_SERVER_TYPE=sse: Đảm bảo máy chủ SSE khởi động.
  3. Xác thực IAM (Khuyến nghị cho Sản xuất):

    Nếu --no-allow-unauthenticated được sử dụng, máy khách phải xác thực. Đối với giao tiếp giữa các dịch vụ trong GCP, hãy sử dụng tài khoản dịch vụ.

    • Tài khoản Dịch vụ Máy khách: Tạo một tài khoản dịch vụ cho máy khách (ví dụ: một dịch vụ mô hình AI).

    • Cấp vai trò Invoker: Cấp vai trò roles/run.invoker cho tài khoản dịch vụ máy khách trên dịch vụ Cloud Run của bạn.

      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
      
    • Xác thực phía Máy khách: Khi thực hiện yêu cầu từ một dịch vụ GCP, các thư viện máy khách gcloud hoặc curl với gcloud auth print-identity-token có thể tự động xử lý xác thực.

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

Các vấn đề và Khắc phục sự cố trong Sản xuất

  1. 403 Forbidden trên Cloud Run:

    • Nguyên nhân: --no-allow-unauthenticated đã được sử dụng trong quá trình triển khai, nhưng máy khách không cung cấp tiêu đề Authorization hợp lệ với mã thông báo invoker của Cloud Run.
    • Khắc phục:
      • Đảm bảo tài khoản dịch vụ gọi có roles/run.invoker trên dịch vụ Cloud Run.
      • Xác minh máy khách đang tạo và đính kèm mã thông báo ID một cách chính xác (ví dụ: sử dụng gcloud auth print-identity-token hoặc Thư viện Xác thực của Google).
      • Nếu dự định truy cập công khai, hãy triển khai lại với --allow-unauthenticated.
  2. 500 Internal Server Error / Container instance crashed:

    • Nguyên nhân: Ứng dụng gặp sự cố trong quá trình khởi động hoặc xử lý yêu cầu. Các vấn đề thường gặp bao gồm biến môi trường PORT không chính xác, các ngoại lệ không được xử lý hoặc lỗi hết bộ nhớ.
    • Khắc phục:
      • Kiểm tra nhật ký Cloud Run (gcloud run services logs read mcp-server-ts --limit 100). Tìm kiếm Error: listen EADDRINUSE (xung đột cổng, không có khả năng xảy ra trong Cloud Run), UnhandledPromiseRejectionWarning hoặc Memory limit exceeded.
      • Đảm bảo ứng dụng của bạn lắng nghe trên process.env.PORT (Cloud Run chèn cái này). sseServer.ts của chúng ta sử dụng process.env.PORT || 8080 một cách chính xác.
      • Tăng bộ nhớ (--memory) hoặc CPU (--cpu) nếu nhật ký cho thấy cạn kiệt tài nguyên.
      • Thêm các khối try...catch mạnh mẽ hơn xung quanh các hoạt động không đồng bộ.
  3. 400 Bad Request từ /mcp/stream:

    • Nguyên nhân: Tải trọng JSON đến không tuân thủ MCPRequestSchema. Điều này thường xảy ra do thiếu các trường bắt buộc hoặc kiểu dữ liệu không chính xác.
    • Khắc phục:
      • Xem lại nội dung yêu cầu của máy khách so với MCPRequestSchema (và bất kỳ lược đồ tùy chỉnh nào).
      • Thêm nhật ký chi tiết hơn trong khối catch của máy chủ cho MCPRequestSchema.parse để xuất lỗi xác thực Zod.
      • Ví dụ:
        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. Phản hồi chậm / Hết thời gian chờ:

    • Nguyên nhân: Thực thi công cụ, tìm nạp tài nguyên hoặc hiển thị mẫu lời nhắc kéo dài. Cloud Run có thời gian chờ yêu cầu mặc định (ví dụ: 5 phút).
    • Khắc phục:
      • Tối ưu hóa logic xử lý.
      • Triển khai các mẫu không đồng bộ cho các tác vụ rất dài (ví dụ: chuyển sang Cloud Tasks hoặc Pub/Sub, và để máy chủ MCP thăm dò kết quả hoặc nhận webhook).
      • Tăng thời gian chờ yêu cầu của Cloud Run (--timeout).
      • Đảm bảo các cuộc gọi API bên ngoài có thời gian chờ thích hợp.
  5. Kết nối SSE bị ngắt sớm:

    • Nguyên nhân: Các proxy (như Nginx hoặc Bộ cân bằng tải đám mây) có thể đệm các phản hồi, ngăn chặn việc truyền trực tuyến ngay lập tức. Bản thân Cloud Run xử lý SSE tốt, nhưng các proxy trung gian có thể gây nhiễu.
    • Khắc phục:
      • Đảm bảo tiêu đề X-Accel-Buffering: no được đặt (hàm setSSEHeaders của chúng ta làm điều này).
      • Xác minh không có proxy nào khác phía trước Cloud Run đang đệm.
      • Cơ chế giữ kết nối: Mặc dù SSE vốn đã giữ kết nối, nhưng nếu máy chủ không hoạt động quá lâu, một số thành phần mạng có thể đóng kết nối. Cân nhắc gửi các bình luận "nhịp tim" định kỳ (: comment\n\n) nếu máy chủ có thể không hoạt động trong thời gian dài giữa các sự kiện dữ liệu thực tế.
Advertisement

Các câu hỏi thường gặp

Q1: Làm cách nào để thêm một công cụ hoặc tài nguyên tùy chỉnh mới vào máy chủ MCP của tôi?

A1:

  1. Định nghĩa Lược đồ: Tạo một lược đồ Zod cho đầu vào và đầu ra của công cụ (hoặc ID và dữ liệu của tài nguyên) trong src/schemas.ts. Mở rộng ToolDefinitionSchema hoặc ResourceDefinitionSchema.
  2. Triển khai Logic: Viết một hàm không đồng bộ trong src/handlers.ts nhận đầu vào đã phân tích cú pháp và trả về đầu ra.
  3. Đăng ký Trình thực thi: Thêm hàm mới của bạn vào bản đồ toolExecutors hoặc resourceFetchers trong src/handlers.ts.
  4. Cập nhật Khả năng: Thêm ToolDefinition hoặc ResourceDefinition của bạn vào mảng serverCapabilities trong src/handlers.ts để máy khách có thể khám phá nó.
  5. Xây dựng lại và Triển khai: Xây dựng lại ảnh Docker của bạn và triển khai lại lên Cloud Run.

Q2: Tôi có thể sử dụng WebSockets thay vì SSE để truyền trực tuyến hai chiều không?

A2: Mặc dù bản thân MCP không phụ thuộc vào giao thức truyền tải, @modelcontextprotocol/sdk hiện cung cấp các công cụ hỗ trợ cụ thể cho stdio và SSE. WebSockets sẽ yêu cầu triển khai tùy chỉnh lớp truyền tải. Bạn sẽ cần:

  1. Thiết lập một máy chủ WebSocket (ví dụ: sử dụng thư viện ws với Express).
  2. Triển khai đóng khung tin nhắn (ví dụ: tin nhắn JSON với các trường type và payload) qua WebSocket.
  3. Điều chỉnh handleMCPRequest để xử lý các tin nhắn WebSocket đến và gửi phản hồi trở lại qua cùng một kết nối. Điều này khả thi nhưng đòi hỏi nhiều công việc thủ công hơn so với các ví dụ SSE/stdio được cung cấp.

Q3: Làm cách nào để quản lý các bí mật (ví dụ: khóa API cho các công cụ bên ngoài) trong máy chủ MCP của Cloud Run?

A3:

  1. Cloud Secret Manager: Lưu trữ bí mật trong Google Cloud Secret Manager.
  2. Cấp quyền truy cập: Cấp cho tài khoản dịch vụ Cloud Run (mặc định là <YOUR_PROJECT_ID>@appspot.gserviceaccount.com) vai trò Secret Manager Secret Accessor trên các bí mật cụ thể.
  3. Gắn làm biến môi trường: Cấu hình Cloud Run để gắn các bí mật làm biến môi trường trong quá trình triển khai:
    gcloud run deploy mcp-server-ts ... \
      --set-secrets=EXTERNAL_API_KEY=EXTERNAL_API_KEY:latest \
      ...
    
    Ứng dụng của bạn sau đó có thể truy cập process.env.EXTERNAL_API_KEY.
  4. Truy cập trực tiếp (ít phổ biến hơn): Ngoài ra, ứng dụng của bạn có thể trực tiếp gọi API Secret Manager bằng cách sử dụng các thư viện máy khách của Google Cloud, nhưng các biến môi trường thường đơn giản hơn cho cấu hình.

Q4: Điều gì sẽ xảy ra nếu việc thực thi công cụ của tôi mất nhiều thời gian hơn thời gian chờ yêu cầu của Cloud Run?

A4: Đối với các hoạt động kéo dài (ví dụ: suy luận mô hình ML phức tạp, xử lý dữ liệu lớn), việc thực thi đồng bộ trực tiếp trong yêu cầu MCP không phù hợp. Hãy xem xét các mẫu sau:

  1. Hàng đợi tác vụ không đồng bộ: Máy chủ MCP khởi tạo một tác vụ kéo dài (ví dụ: bằng cách xuất bản một tin nhắn lên Google Cloud Pub/Sub hoặc tạo một mục Cloud Tasks). Sau đó, nó ngay lập tức trả về một MCPResponse cho biết tác vụ đã được chấp nhận, có thể kèm theo một task_id.
  2. Thăm dò: Máy khách sau đó có thể định kỳ thăm dò một điểm cuối riêng biệt trên máy chủ MCP của bạn (hoặc một dịch vụ khác) với task_id để kiểm tra hoàn thành và truy xuất kết quả.
  3. Webhooks: Tác vụ kéo dài, sau khi hoàn thành, có thể gửi thông báo webhook đến một điểm cuối chuyên dụng trên máy chủ MCP của bạn (hoặc một dịch vụ khác) để đẩy kết quả trở lại máy khách nếu máy khách duy trì kết nối liên tục hoặc có cách để nhận các cập nhật không đồng bộ. Điều này tách rời chu trình yêu cầu-phản hồi khỏi thời gian thực thi thực tế.

Q5: Làm cách nào để kiểm tra máy chủ stdio của tôi cục bộ?

A5: Bạn có thể chuyển dữ liệu JSON vào stdin của nó.

  1. Biên dịch máy chủ: npm run build
  2. Chuẩn bị tệp yêu cầu:
    // request.json
    {
      "request_id": "test-stdio-1",
      "tool_calls": [
        { "call_id": "call-stdio-1", "name": "search", "input": { "query": "stdio transport" } }
      ]
    }
    
  3. Gửi yêu cầu bằng stdio-client từ @modelcontextprotocol/sdk:
    # 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"
    
    Ngoài ra, bạn có thể tự xây dựng tin nhắn có tiền tố độ dài:
    # Trong một terminal:
    node dist/index.js
    
    # Trong một terminal khác:
    # Lấy độ dài của chuỗi JSON
    # echo '{"request_id":"test-stdio-1","tool_calls":[{"call_id":"call-stdio-1","name":"search","input":{"query":"stdio transport"}}]}' | wc -c
    # (Giả sử là 123 byte)
    # Sau đó gửi:
    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
    # Lưu ý: \x00\x00\x00\x7B đại diện cho 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