mcp-server-development

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese
This skill provides comprehensive guidance for building robust MCP servers, with specific focus on the unity-mcp-server architecture and patterns.
本技能为构建稳定的MCP服务器提供全面指导,重点关注unity-mcp-server的架构与模式。

Core Philosophy

核心设计理念

MCP servers bridge AI assistants to external systems. They must be:
  • Reliable: Handle errors gracefully, never crash unexpectedly
  • Discoverable: Tools should have clear, self-documenting schemas
  • Performant: Minimize latency, especially for stdio transport
  • Protocol-compliant: Follow JSON-RPC 2.0 and MCP spec exactly
MCP服务器是AI助手与外部系统之间的桥梁。它们必须具备以下特性:
  • 可靠性:优雅处理错误,避免意外崩溃
  • 可发现性:工具应具备清晰、自文档化的schema
  • 高性能:尽可能降低延迟,尤其是对于stdio传输
  • 协议合规性:严格遵循JSON-RPC 2.0和MCP规范

Architecture Patterns

架构模式

Handler-Based Design

基于处理器的设计

ALWAYS use a handler class per tool. Each handler encapsulates:
  • Input validation (Zod schema)
  • Business logic execution
  • Error handling and response formatting
javascript
// Pattern: One handler per tool
export class SystemPingToolHandler extends BaseToolHandler {
  constructor(unityConnection) {
    super({
      name: 'system_ping',
      description: 'Check Unity Editor connectivity',
      inputSchema: { type: 'object', properties: {} }
    });
    this.unityConnection = unityConnection;
  }

  async execute(params) {
    const result = await this.unityConnection.send({ command: 'ping' });
    return { status: 'ok', ...result };
  }
}
务必为每个工具单独使用一个处理器类。每个处理器封装以下内容:
  • 输入验证(Zod schema)
  • 业务逻辑执行
  • 错误处理与响应格式化
javascript
// Pattern: One handler per tool
export class SystemPingToolHandler extends BaseToolHandler {
  constructor(unityConnection) {
    super({
      name: 'system_ping',
      description: 'Check Unity Editor connectivity',
      inputSchema: { type: 'object', properties: {} }
    });
    this.unityConnection = unityConnection;
  }

  async execute(params) {
    const result = await this.unityConnection.send({ command: 'ping' });
    return { status: 'ok', ...result };
  }
}

BaseToolHandler Contract

BaseToolHandler 契约要求

All handlers MUST:
  1. Call
    super()
    with tool metadata
  2. Implement
    execute(params)
    method
  3. Return plain objects (framework handles JSON-RPC wrapping)
  4. Throw errors with descriptive messages
所有处理器必须满足以下要求:
  1. 传入工具元数据调用
    super()
  2. 实现
    execute(params)
    方法
  3. 返回普通对象(框架会处理JSON-RPC封装)
  4. 抛出带有描述性信息的错误

Input Validation with Zod

使用Zod进行输入验证

ALWAYS validate inputs before processing:
javascript
import { z } from 'zod';

const inputSchema = z.object({
  name: z.string().min(1).describe('GameObject name'),
  primitiveType: z.enum(['cube', 'sphere', 'cylinder']).optional()
});

validate(input) {
  return inputSchema.parse(input);
}
务必在处理前验证输入:
javascript
import { z } from 'zod';

const inputSchema = z.object({
  name: z.string().min(1).describe('GameObject name'),
  primitiveType: z.enum(['cube', 'sphere', 'cylinder']).optional()
});

validate(input) {
  return inputSchema.parse(input);
}

Transport Layer

传输层

Content-Length Framing (Standard)

Content-Length 帧格式(标准)

MCP uses LSP-style Content-Length framing for stdio:
Content-Length: 123\r\n
\r\n
{"jsonrpc":"2.0","id":1,"method":"tools/call"...}
ALWAYS use Content-Length for output. NEVER mix framing formats in a session.
MCP针对stdio采用LSP风格的Content-Length帧格式:
Content-Length: 123\r\n
\r\n
{"jsonrpc":"2.0","id":1,"method":"tools/call"...}
务必在输出时使用Content-Length。绝对不要在一个会话中混合使用多种帧格式。

Hybrid Input (Compatibility)

混合输入(兼容性)

Accept both Content-Length and NDJSON input for client compatibility:
  • Claude Desktop: Content-Length
  • Some CLI tools: NDJSON (newline-delimited)
为了兼容客户端,同时支持Content-Length和NDJSON输入:
  • Claude Desktop:使用Content-Length
  • 部分CLI工具:使用NDJSON(换行分隔)

Error Handling

错误处理

Structured Errors

结构化错误

Use MCP error codes from the spec:
javascript
const McpError = {
  ParseError: -32700,
  InvalidRequest: -32600,
  MethodNotFound: -32601,
  InvalidParams: -32602,
  InternalError: -32603
};

throw new Error(JSON.stringify({
  code: McpError.InvalidParams,
  message: 'primitiveType must be one of: cube, sphere, cylinder'
}));
使用规范中定义的MCP错误码:
javascript
const McpError = {
  ParseError: -32700,
  InvalidRequest: -32600,
  MethodNotFound: -32601,
  InvalidParams: -32602,
  InternalError: -32603
};

throw new Error(JSON.stringify({
  code: McpError.InvalidParams,
  message: 'primitiveType must be one of: cube, sphere, cylinder'
}));

Error Response Format

错误响应格式

javascript
{
  jsonrpc: '2.0',
  id: requestId,
  error: {
    code: -32602,
    message: 'Invalid params',
    data: { field: 'name', issue: 'required' }
  }
}
javascript
{
  jsonrpc: '2.0',
  id: requestId,
  error: {
    code: -32602,
    message: 'Invalid params',
    data: { field: 'name', issue: 'required' }
  }
}

Unity-Specific Patterns

Unity专属模式

Command Protocol

命令协议

Unity communication uses a simple request/response pattern:
javascript
const result = await this.unityConnection.send({
  command: 'gameobject_create',
  params: { name: 'Cube', primitiveType: 'cube' }
});
与Unity的通信采用简单的请求/响应模式:
javascript
const result = await this.unityConnection.send({
  command: 'gameobject_create',
  params: { name: 'Cube', primitiveType: 'cube' }
});

Workspace Root Resolution

工作区根目录解析

ALWAYS include
workspaceRoot
in commands requiring file paths:
javascript
execute(params) {
  return this.unityConnection.send({
    command: 'screenshot_capture',
    workspaceRoot: config.workspaceRoot,
    ...params
  });
}
务必在需要文件路径的命令中包含
workspaceRoot
javascript
execute(params) {
  return this.unityConnection.send({
    command: 'screenshot_capture',
    workspaceRoot: config.workspaceRoot,
    ...params
  });
}

Testing Patterns

测试模式

TDD for Handlers

处理器的测试驱动开发(TDD)

  1. Contract test first: Define expected input/output schema
  2. Mock Unity connection: Isolate handler logic
  3. Test error paths: Invalid input, connection failures
  4. Test happy path last: After contracts are verified
javascript
describe('CreateGameObjectHandler', () => {
  it('validates primitiveType enum', async () => {
    const handler = new CreateGameObjectHandler(mockConnection);
    await assert.rejects(
      () => handler.handle({ name: 'Cube', primitiveType: 'invalid' }),
      /primitiveType must be one of/
    );
  });
});
  1. 先做契约测试:定义预期的输入/输出schema
  2. Mock Unity连接:隔离处理器逻辑
  3. 测试错误路径:无效输入、连接失败等场景
  4. 最后测试正常路径:在契约验证通过后进行
javascript
describe('CreateGameObjectHandler', () => {
  it('validates primitiveType enum', async () => {
    const handler = new CreateGameObjectHandler(mockConnection);
    await assert.rejects(
      () => handler.handle({ name: 'Cube', primitiveType: 'invalid' }),
      /primitiveType must be one of/
    );
  });
});

Integration Testing

集成测试

Test full request/response cycle including JSON-RPC framing:
javascript
const stdin = new PassThrough();
const stdout = new PassThrough();
const transport = new HybridStdioServerTransport(stdin, stdout);

stdin.write('Content-Length: 50\r\n\r\n{"jsonrpc":"2.0","id":1,"method":"ping"}');
// Assert stdout contains Content-Length response
测试包含JSON-RPC帧格式的完整请求/响应周期:
javascript
const stdin = new PassThrough();
const stdout = new PassThrough();
const transport = new HybridStdioServerTransport(stdin, stdout);

stdin.write('Content-Length: 50\r\n\r\n{"jsonrpc":"2.0","id":1,"method":"ping"}');
// 断言stdout包含Content-Length格式的响应

Common Mistakes

常见错误

Mixing framing formats:
  • NEVER: Output NDJSON after receiving Content-Length input
  • ALWAYS: Output Content-Length regardless of input format
Swallowing errors:
  • NEVER:
    catch (e) { return null; }
  • ALWAYS: Propagate errors with context
Missing validation:
  • NEVER: Trust raw input from clients
  • ALWAYS: Validate with Zod before processing
Blocking stdio:
  • NEVER: Synchronous operations on transport
  • ALWAYS: Use async/await throughout
混合帧格式
  • 禁止:在接收Content-Length格式的输入后输出NDJSON
  • 务必:无论输入格式如何,输出都使用Content-Length
吞掉错误
  • 禁止:
    catch (e) { return null; }
  • 务必:携带上下文传播错误
缺少验证
  • 禁止:信任客户端的原始输入
  • 务必:处理前使用Zod进行验证
阻塞stdio
  • 禁止:在传输层使用同步操作
  • 务必:全程使用async/await

Handler Registration

处理器注册

Register all handlers in a central index:
javascript
// src/handlers/index.js
export function createHandlers(unityConnection) {
  return [
    new SystemPingToolHandler(unityConnection),
    new CreateGameObjectHandler(unityConnection),
    new ScreenshotHandler(unityConnection),
    // ...
  ];
}
在中央索引文件中注册所有处理器:
javascript
// src/handlers/index.js
export function createHandlers(unityConnection) {
  return [
    new SystemPingToolHandler(unityConnection),
    new CreateGameObjectHandler(unityConnection),
    new ScreenshotHandler(unityConnection),
    // ...
  ];
}

Remember

注意事项

  • Content-Length always: Output framing must be consistent
  • Validate everything: Never trust client input
  • One handler, one tool: Keep handlers focused
  • Test error paths: Most bugs hide in error handling
  • Protocol compliance: Follow JSON-RPC 2.0 exactly
  • 始终使用Content-Length:输出的帧格式必须保持一致
  • 验证所有输入:永远不要信任客户端输入
  • 一个处理器对应一个工具:保持处理器职责单一
  • 测试错误路径:大多数bug隐藏在错误处理逻辑中
  • 遵循协议规范:严格遵守JSON-RPC 2.0