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

Mục lục bài viết(18 mục)
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.
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:
- Lớp Truyền tải: Xử lý giao tiếp (ví dụ: HTTP, stdio).
- 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
MCPRequestdựa trên các lược đồ đã định nghĩa. - Đ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.
- 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ăng | stdio | SSE (HTTP) |
|---|---|---|
| Giao thức | Nhị phân/văn bản có tiền tố độ dài tùy chỉnh | HTTP/1.1, dựa trên văn bản |
| Kết nối | Liê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ến | Hai 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ểu | Tiêu đề HTTP, đóng khung sự kiện |
| Độ phức tạp | Thấ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ụng | Công cụ CLI, tác nhân cục bộ, nội bộ container | Máy khách web, hàm đám mây, dịch vụ bên ngoài |
| Xác thực | Quyền cấp hệ điều hành | Tiêu đề HTTP (mã thông báo Bearer, khóa API) |
| Xử lý lỗi | Thông báo cấp ứng dụng | Mã trạng thái HTTP, sự kiện cấp ứng dụng |
| Khả năng mở rộng | Một tiến trình | Có thể mở rộng theo chiều ngang (bộ cân bằng tải) |
Đó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
-
Xây dựng và Đẩy ảnh Docker lên Google Container Registry (GCR) hoặc 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 -
Triển khai lên 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: Để truy cập công khai. Đối với sản xuất, hãy cân nhắc--no-allow-unauthenticatedvà sử dụng IAM.--port 8080: Khớp vớiEXPOSEtrongDockerfile. 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.
-
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.invokercho tài khoản dịch vụ máy khách trên dịch vụ Cloud Run của bạn.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 -
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
gcloudhoặccurlvớigcloud auth print-identity-tokencó thể tự động xử lý xác thực.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"
-
Các vấn đề và Khắc phục sự cố trong Sản xuất
-
403 Forbiddentrê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 đềAuthorizationhợ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.invokertrê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-tokenhoặ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.
- Đảm bảo tài khoản dịch vụ gọi có
- Nguyên nhân:
-
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
PORTkhô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ếmError: listen EADDRINUSE(xung đột cổng, không có khả năng xảy ra trong Cloud Run),UnhandledPromiseRejectionWarninghoặcMemory 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.tscủa chúng ta sử dụngprocess.env.PORT || 8080mộ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...catchmạnh mẽ hơn xung quanh các hoạt động không đồng bộ.
- Kiểm tra nhật ký Cloud Run (
- 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
-
400 Bad Requesttừ/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
catchcủa máy chủ choMCPRequestSchema.parseđể xuất lỗi xác thực Zod. - Ví dụ:
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); } }
- Xem lại nội dung yêu cầu của máy khách so với
- Nguyên nhân: Tải trọng JSON đến không tuân thủ
-
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.
-
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àmsetSSEHeaderscủ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ế.
- Đảm bảo tiêu đề
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:
- Đị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ộngToolDefinitionSchemahoặcResourceDefinitionSchema. - Triển khai Logic: Viết một hàm không đồng bộ trong
src/handlers.tsnhận đầu vào đã phân tích cú pháp và trả về đầu ra. - Đăng ký Trình thực thi: Thêm hàm mới của bạn vào bản đồ
toolExecutorshoặcresourceFetcherstrongsrc/handlers.ts. - Cập nhật Khả năng: Thêm
ToolDefinitionhoặcResourceDefinitioncủa bạn vào mảngserverCapabilitiestrongsrc/handlers.tsđể máy khách có thể khám phá nó. - 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:
- Thiết lập một máy chủ WebSocket (ví dụ: sử dụng thư viện
wsvới Express). - Triển khai đóng khung tin nhắn (ví dụ: tin nhắn JSON với các trường
typevàpayload) qua WebSocket. - Đ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:
- Cloud Secret Manager: Lưu trữ bí mật trong Google Cloud Secret Manager.
- 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 Accessortrên các bí mật cụ thể. - 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:
Ứng dụng của bạn sau đó có thể truy cậpbash
gcloud run deploy mcp-server-ts ... \ --set-secrets=EXTERNAL_API_KEY=EXTERNAL_API_KEY:latest \ ...process.env.EXTERNAL_API_KEY. - 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:
- 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
MCPResponsecho biết tác vụ đã được chấp nhận, có thể kèm theo mộttask_id. - 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ả. - 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ó.
- Biên dịch máy chủ:
npm run build - Chuẩn bị tệp yêu cầu:
json
// request.json { "request_id": "test-stdio-1", "tool_calls": [ { "call_id": "call-stdio-1", "name": "search", "input": { "query": "stdio transport" } } ] } - Gửi yêu cầu bằng
stdio-clienttừ@modelcontextprotocol/sdk:Ngoài ra, bạn có thể tự xây dựng tin nhắn có tiền tố độ dài: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# 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
Free In-Browser Developer Tools
Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.
Related Articles

Xây dựng một MCP Client tùy chỉnh: Kết nối bất kỳ LLM nào với nhiều Model Context Protocol Server
Hướng dẫn toàn diện về xây dựng một mcp client tùy chỉnh: kết nối bất kỳ llm nào với nhiều model context protocol server với kiến trúc cấp độ sản xuất và các ví dụ mã.
Read more
Xây dựng máy chủ MCP đầu tiên của bạn từ đầu: Hướng dẫn Python & Claude đầy đủ
Hướng dẫn từng bước để xây dựng các máy chủ Model Context Protocol (MCP) sản xuất với Python, FastMCP, typed tools, resources và tích hợp Claude Desktop.
Read more
DeepSeek-R1 & Các Mô Hình Suy Luận Chưng Cất: Triển Khai vLLM Cục Bộ, Lượng Tử Hóa & Kiến Trúc
Hướng dẫn toàn diện về deepseek-r1 và các mô hình suy luận chưng cất: triển khai vLLM cục bộ, lượng tử hóa và kiến trúc với các ví dụ về kiến trúc và mã nguồn cấp độ sản xuất.
Read more