MCP 服务器开发

本页给出 MCP 工具的最小 Python/TypeScript 实现(工具注册 + 输入 schema + 返回格式)、API key vs Bearer token 的鉴权对比代码、三种返回状态(success/error/pending)的标准格式,以及工具 JSON Schema 的完整设计示例。

每个 tool 应有稳定名称、明确的参数 schema 与可机器解析的错误;返回值避免超大 payload,必要时用资源 URI 或分页。启动时声明能力,版本升级保持向后兼容或显式破坏说明。

日志与可观测:记录 tool 调用 id、耗时与失败原因,不回显密钥或完整用户隐私字段。

  • 副作用工具:要求幂等键或确认参数,避免 Agent 误触发生产变更。
  • 与宿主权限对齐:文件、网络、子进程最小授权。
  • 联调清单:示例请求、边界参数、超时与取消行为。

MCP 工具最小实现

以下是一个完整的 MCP 工具注册示例(Python,使用 mcp SDK),包含工具注册、输入 schema 校验和三种标准返回格式:

# Python — 使用官方 mcp SDK 的最小 MCP 服务器
# pip install mcp

from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent, CallToolResult
import json

app = Server("my-mcp-server")

# 1. 注册工具列表(tools/list 响应)
@app.list_tools()
async def list_tools() -> list[Tool]:
    return [
        Tool(
            name="search_docs",
            description="按关键词搜索内部文档,返回标题与摘要。只读操作,不修改任何数据。",
            inputSchema={
                "type": "object",
                "properties": {
                    "query": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 200,
                        "description": "搜索关键词,支持中英文,勿拼接 SQL 或路径遍历字符"
                    },
                    "limit": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 20,
                        "default": 5,
                        "description": "返回结果条数上限"
                    },
                    "category": {
                        "type": "string",
                        "enum": ["api", "guide", "faq", "all"],
                        "default": "all",
                        "description": "文档分类过滤"
                    }
                },
                "required": ["query"],
                "additionalProperties": False
            }
        )
    ]

# 2. 实现工具 handler(tools/call 响应)
@app.call_tool()
async def call_tool(name: str, arguments: dict) -> CallToolResult:
    if name == "search_docs":
        return await handle_search_docs(arguments)
    return CallToolResult(
        content=[TextContent(type="text", text=json.dumps({
            "status": "error",
            "error": {"code": "UNKNOWN_TOOL", "message": f"未知工具: {name}"}
        }))],
        isError=True
    )

async def handle_search_docs(args: dict) -> CallToolResult:
    query = args["query"]
    limit = args.get("limit", 5)
    category = args.get("category", "all")

    # 服务端二次校验(不信任模型传入的参数)
    if len(query) > 200:
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps({
                "status": "error",
                "error": {"code": "VALIDATION_ERROR", "message": "query 超过 200 字符限制"}
            }))],
            isError=True
        )

    try:
        results = await search_engine.query(query, limit=limit, category=category)
        # 成功返回格式
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps({
                "status": "success",
                "data": {"results": results, "total": len(results), "query": query}
            }))]
        )
    except TimeoutError:
        # 可重试错误
        return CallToolResult(
            content=[TextContent(type="text", text=json.dumps({
                "status": "error",
                "error": {"code": "TIMEOUT", "message": "搜索超时,可稍后重试", "retryable": True}
            }))],
            isError=True
        )

# 3. 启动 stdio 传输
async def main():
    async with stdio_server() as streams:
        await app.run(*streams)

if __name__ == "__main__":
    import asyncio
    asyncio.run(main())

TypeScript 等价实现(使用 @modelcontextprotocol/sdk):

// TypeScript — 最小 MCP 工具实现
// npm install @modelcontextprotocol/sdk

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";

const server = new Server({ name: "my-mcp-server", version: "1.0.0" }, {
  capabilities: { tools: {} },
});

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [{
    name: "search_docs",
    description: "按关键词搜索内部文档,只读",
    inputSchema: {
      type: "object",
      properties: {
        query: { type: "string", minLength: 1, maxLength: 200 },
        limit: { type: "integer", minimum: 1, maximum: 20, default: 5 }
      },
      required: ["query"],
    },
  }],
}));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === "search_docs") {
    const { query, limit = 5 } = request.params.arguments as { query: string; limit?: number };
    const results = await searchDocs(query, limit);
    return {
      content: [{ type: "text", text: JSON.stringify({ status: "success", data: results }) }],
    };
  }
  throw new Error(`Unknown tool: ${request.params.name}`);
});

const transport = new StdioServerTransport();
await server.connect(transport);

传输(stdio / 远程)

stdio:子进程与宿主同机通信,适合 CLI 与桌面 Agent;注意工作目录、环境变量继承、标准输出仅用于协议行(日志应走 stderr 或专用通道),避免污染 JSON-RPC 流。

HTTP / SSE / Streamable HTTP:适合远程部署与多客户端;需 TLS、可观测的关闭与背压、以及与会话/路由匹配的 URL 设计。无论哪种传输,都应定义超时、最大消息体与并发上限,并在文档中写明宿主如何启动与回收进程。

鉴权:API key vs Bearer token 实现对比

# 方式 1:API key 在 Header(适合内部服务、简单场景)
# 客户端请求头:X-API-Key: sk-xxxx
# 服务端校验(FastAPI 示例,HTTP/SSE 传输)

from fastapi import FastAPI, Request, HTTPException
from fastapi.middleware.base import BaseHTTPMiddleware
import hmac, os

API_KEY = os.environ["MCP_API_KEY"]  # 从环境变量读取,禁止硬编码

class ApiKeyMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        key = request.headers.get("X-API-Key", "")
        # 使用 hmac.compare_digest 防止时序攻击
        if not hmac.compare_digest(key.encode(), API_KEY.encode()):
            raise HTTPException(status_code=401, detail={
                "status": "error",
                "error": {"code": "UNAUTHORIZED", "message": "无效的 API Key"}
            })
        return await call_next(request)


# 方式 2:Bearer token(OAuth 2.0,适合多租户、短期授权)
# 客户端请求头:Authorization: Bearer eyJhbGc...

import jwt  # pip install PyJWT

JWT_SECRET = os.environ["JWT_SECRET"]
JWT_ALGORITHM = "HS256"

def verify_bearer_token(authorization: str) -> dict:
    """解析并校验 JWT Bearer token,返回 payload。"""
    if not authorization.startswith("Bearer "):
        raise HTTPException(status_code=401, detail={
            "status": "error",
            "error": {"code": "INVALID_AUTH_SCHEME", "message": "需要 Bearer token"}
        })
    token = authorization[7:]
    try:
        payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
        return payload  # 包含 sub(用户ID)、exp(过期时间)、scope 等
    except jwt.ExpiredSignatureError:
        raise HTTPException(status_code=401, detail={
            "status": "error",
            "error": {"code": "TOKEN_EXPIRED", "message": "Token 已过期,请刷新"}
        })
    except jwt.InvalidTokenError as e:
        raise HTTPException(status_code=401, detail={
            "status": "error",
            "error": {"code": "INVALID_TOKEN", "message": str(e)}
        })

# 在 tool handler 中使用 token payload 隔离租户数据
async def handle_search_docs_with_auth(args: dict, token_payload: dict) -> dict:
    tenant_id = token_payload["sub"]       # 从 token 中提取租户 ID
    scope = token_payload.get("scope", "") # 检查权限范围
    if "docs:read" not in scope:
        raise HTTPException(status_code=403, detail={
            "status": "error",
            "error": {"code": "FORBIDDEN", "message": "缺少 docs:read 权限"}
        })
    # 用 tenant_id 隔离数据查询,禁止全局查询
    return await search_engine.query(args["query"], tenant_id=tenant_id)

工具 JSON Schema 完整设计示例

完整的 JSON Schema 应包含参数类型、required、enum、description 和格式约束:

{
  "name": "create_ticket",
  "description": "创建工单。写操作,会产生数据库记录;需确认 environment 参数后再调用。",
  "inputSchema": {
    "type": "object",
    "additionalProperties": false,
    "required": ["title", "priority", "environment"],
    "properties": {
      "title": {
        "type": "string",
        "minLength": 5,
        "maxLength": 200,
        "description": "工单标题,5-200 字符,禁止 HTML 标签"
      },
      "description": {
        "type": "string",
        "maxLength": 5000,
        "description": "详细描述(可选),支持 Markdown"
      },
      "priority": {
        "type": "string",
        "enum": ["low", "medium", "high", "critical"],
        "description": "优先级。critical 会触发 on-call 通知"
      },
      "environment": {
        "type": "string",
        "enum": ["staging", "production"],
        "description": "目标环境。生产环境工单需要人工确认"
      },
      "assignee_id": {
        "type": ["string", "null"],
        "pattern": "^usr_[a-z0-9]{8}$",
        "default": null,
        "description": "负责人 ID,格式 usr_xxxxxxxx;null 表示未分配"
      },
      "due_date": {
        "type": ["string", "null"],
        "format": "date",
        "description": "截止日期,ISO 8601 格式(YYYY-MM-DD)"
      },
      "idempotency_key": {
        "type": "string",
        "minLength": 16,
        "maxLength": 64,
        "description": "幂等键,客户端生成,相同 key 不重复创建工单"
      }
    }
  }
}

三种标准返回状态(success / error / pending)的完整格式:

# success:操作完成,data 包含结果
{
  "status": "success",
  "data": {
    "ticket_id": "tkt_a1b2c3d4",
    "url": "https://app.example.com/tickets/tkt_a1b2c3d4",
    "created_at": "2026-04-11T10:30:00Z"
  }
}

# error:操作失败,Agent 根据 retryable 决定是否重试
{
  "status": "error",
  "error": {
    "code": "VALIDATION_ERROR",      # 机器可读错误码
    "message": "due_date 格式错误,需要 YYYY-MM-DD",  # 人类可读说明
    "fields": ["due_date"],           # 有问题的字段(校验类错误)
    "retryable": false                # false=不可重试(需修正参数)
  }
}

# error(可重试):临时故障,Agent 可退避后重试
{
  "status": "error",
  "error": {
    "code": "UPSTREAM_TIMEOUT",
    "message": "下游服务超时,请在 5 秒后重试",
    "retryable": true,
    "retry_after_ms": 5000
  }
}

# pending:异步操作已接受,需轮询或等待回调
{
  "status": "pending",
  "data": {
    "job_id": "job_x9y8z7",
    "poll_url": "/api/jobs/job_x9y8z7",
    "estimated_seconds": 30
  }
}

MCP 请求主流程

下列为常见宿主与服务器之间的消息顺序示意;具体方法名以你所用 SDK 与协议版本为准。重点是:能力协商在前,列举工具在中,执行与资源访问在后

  [ 宿主启动 MCP 服务器进程或建立远程连接 ]
        │
        ▼
  ┌─────────────────┐     交换协议版本、能力与 serverInfo;约定后续特性
  │  initialize      │     (如是否支持 tools、resources、采样等)
  └─────────────────┘
        │
        ▼
  ┌─────────────────┐     返回 prompts / resources / tools 等能力子集
  │  能力发现消息     │     (依实现可能分多条或合并)
  └─────────────────┘
        │
        ▼
  ┌─────────────────┐     模型侧需要调用工具时,宿主下发 name + 校验后参数
  │  tools/call      │──── 服务器:校验 inputSchema → 执行业务 → 结构化结果或错误
  └─────────────────┘
        │
        ▼
  ┌─────────────────┐     可选:读取资源、拉取提示模板、进度/取消通知
  │  资源 / 其它扩展  │
  └─────────────────┘
        │
        ▼
  [ 连接关闭或进程退出;清理句柄与临时凭证 ]

工具名规范化预览

工具名在清单与 tools/call 中必须一致。下方预览将输入转为小写、把非 [a-z0-9_] 转为下划线并合并连续下划线;若以数字开头则加前缀 t_。规则为教学用示例,发布前应以团队规范与宿主限制为准。

规范化结果


              

---
name: mcp-server-dev
description: 设计或实现 MCP 服务器工具与资源;输入:工具需求描述;产出:完整工具定义 + handler 代码;禁止:硬编码凭证或记录敏感数据
version: "1.1.0"
triggers:
  - "实现.*MCP|开发.*MCP 服务"
  - "给 Agent 添加工具|tool.*registration"
steps:
  1. 用 @app.list_tools() 注册工具,每个工具写 name/description/inputSchema
  2. inputSchema 中写全 required、enum、minLength/maxLength、additionalProperties: false
  3. 在 handler 内用 assert 或 pydantic 二次校验参数(不信任模型输入)
  4. 返回格式统一为 {status, data} 或 {status, error:{code,message,retryable}}
  5. 区分三种状态:success/error(不可重试)/error(可重试+retry_after_ms)
  6. 异步操作返回 pending + job_id + poll_url
  7. 鉴权:从环境变量读取密钥,用 hmac.compare_digest 防时序攻击
  8. 多租户:从 JWT payload 提取 tenant_id,所有查询加 tenant 过滤
  9. 日志:记录 tool_name/call_id/耗时/status,禁止记录完整请求 payload
  10. 大结果:超 10KB 返回 resource URI,不直接嵌入 tool 响应
  11. 工具名用 snake_case,动词开头(search_/create_/delete_/get_)
  12. 写操作必须有 idempotency_key 参数,服务端检查去重
  13. 高风险工具(delete_/bulk_)添加 environment 参数(staging/production)
  14. 在 tools/list 的 description 中写明是否只读、是否幂等
  15. 用 yamllint 和 JSON Schema validator 对工具定义做 CI 校验
constraints:
  - 禁止在工具结果中回显 API key、连接串或用户密码
  - 禁止 handler 直接执行模型传入的字符串(SQL 注入、命令注入)
  - 工具名在 list 和 call 中必须完全一致(区分大小写)

返回技能库 更多技能入口