首页 › tutorials › Claude Code MCP服务器开发完全指南

概述

本教程面向希望为 Claude Code 构建自定义 MCP(Model Context Protocol)服务器的开发者。学完本教程后,你将掌握 MCP 协议的三大原语(Tools、Resources、Prompts)、2025-06-18 规范新增特性(Elicitation、Structured Output、OAuth 2.1)、以及如何用 TypeScript 和 Python(FastMCP 3.x)两种语言实现完整的 MCP 服务器,并将其接入 Claude Code 的完整工作流。

适用人群:有基本编程经验、已使用过 Claude Code、希望扩展 Claude Code 能力边界的开发者。

前置条件


一、MCP 协议核心概念

1.1 什么是 MCP?

Model Context Protocol(模型上下文协议)是 Anthropic 发布的开放协议,定义了 AI 模型与外部工具、数据源之间的标准化通信方式。MCP 的设计理念类似"AI 的 USB-C"——一种通用的连接标准,让任何 AI 客户端都能与任何工具服务器对接。

图表1

图表 1

1.2 三大核心原语

MCP 服务器暴露三种类型的能力给客户端:

原语 用途 特征 典型场景
Tools 可调用的函数 模型控制、有副作用 查询数据库、调用API、执行命令
Resources 可读取的数据源 只读、应用控制 文件内容、数据库记录、配置信息
Prompts 可复用的提示模板 用户控制 标准化的工作流模板、角色设定

何时用 Tools vs Resources? - 如果操作会改变状态(写入、删除、发送)→ 用 Tool - 如果只是读取数据(查询、获取、列举)→ 用 Resource - 如果需要标准化一段提示让用户选择使用 → 用 Prompt

1.3 传输机制

MCP 支持两种传输方式:

  1. stdio:本地进程间通信,Claude Code 启动 MCP 服务器作为子进程
  2. Streamable HTTP:远程通信,支持 HTTP POST + SSE 流式响应(2025-03-26 规范替代了旧的 HTTP+SSE)

1.4 协议版本演进

图表2

图表 2

2025-06-18 规范的重大更新: - Elicitation:服务器可暂停工具执行,向用户请求输入(人机协作) - Structured Output:工具可返回结构化内容(文本 + 图片 + 音频等) - Tool Annotations:工具可声明 readOnlyHint、destructiveHint 等元数据 - OAuth 2.1 增强:远程服务器的标准化认证流程


二、用 TypeScript SDK 构建 MCP 服务器

2.1 项目初始化

# 创建项目目录
mkdir my-mcp-server && cd my-mcp-server

# 初始化 npm 项目
npm init -y

# 安装 MCP TypeScript SDK(当前最新 v1.29.x)
npm install @modelcontextprotocol/sdk zod
npm install -D typescript @types/node tsx

# 初始化 TypeScript 配置
npx tsc --init

在 tsconfig.json 中确认以下配置:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  }
}

2.2 最小化 MCP 服务器

创建 src/index.ts:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

// 创建服务器实例
const server = new McpServer({
  name: "my-first-mcp-server",
  version: "1.0.0",
});

// 注册一个 Tool
server.tool(
  "get_weather",                          // 工具名称
  "获取指定城市的天气信息",                  // 工具描述
  { city: z.string().describe("城市名称") }, // 参数 schema
  async ({ city }) => {
    // 这里是工具的实际逻辑
    const weatherData = {
      city,
      temperature: "22°C",
      condition: "晴",
      humidity: "45%",
    };
    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(weatherData, null, 2),
        },
      ],
    };
  }
);

// 注册一个 Resource
server.resource(
  "config",                    // 资源名称
  "config://app",              // 资源 URI
  async (uri) => {
    return {
      contents: [
        {
          uri: uri.href,
          text: JSON.stringify({ version: "1.0", env: "development" }),
        },
      ],
    };
  }
);

// 注册一个 Prompt
server.prompt(
  "code-review",                // Prompt 名称
  "代码审查模板",                // 描述
  { code: z.string().describe("待审查的代码") },
  async ({ code }) => {
    return {
      messages: [
        {
          role: "user",
          content: {
            type: "text",
            text: `请审查以下代码,关注安全性、性能和可维护性:\n\n${code}`,
          },
        },
      ],
    };
  }
);

// 启动服务器
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("MCP Server started on stdio"); // 注意:用 stderr 输出日志
}

main().catch(console.error);

更新 package.json:

{
  "type": "module",
  "scripts": {
    "build": "tsc",
    "dev": "tsx src/index.ts",
    "start": "node dist/index.js"
  }
}

2.3 关键实现要点

stdout vs stderr:MCP 协议通过 stdio 传输 JSON-RPC 消息,所以 console.log() 会干扰协议通信。所有日志必须输出到 stderr:

console.error("这条日志不会干扰 MCP 协议"); // ✓ 正确
console.log("这条会破坏 MCP 协议");          // ✗ 错误

Zod Schema 自动转换:SDK 会自动将 Zod schema 转换为 JSON Schema,Claude 端会据此理解参数含义和类型约束。

Tool 返回格式:所有 Tool 必须返回 { content: [...] } 结构,content 数组中可包含多个内容块(text、image、resource)。

2.4 本地调试

使用 MCP Inspector(官方调试工具)进行测试:

# 安装并运行 Inspector
npx @modelcontextprotocol/inspector tsx src/index.ts

Inspector 会打开一个浏览器界面,你可以: - 查看 Tools/Resources/Prompts 列表 - 手动调用 Tool 并查看返回结果 - 检查 JSON-RPC 请求/响应 - 验证 Schema 定义是否正确


三、用 Python FastMCP 3.x 构建 MCP 服务器

3.1 为什么选择 FastMCP?

Python 生态有两个层次的 MCP SDK: - mcp 官方 SDK:底层、灵活但代码量大 - fastmcp 3.x:高层封装,用装饰器快速定义 Tools/Resources/Prompts

对于大多数场景,FastMCP 的开发效率更高。

3.2 安装与初始化

# 创建项目
mkdir my-python-mcp && cd my-python-mcp

# 创建虚拟环境
python -m venv .venv
source .venv/bin/activate  # Windows: .venv\Scripts\activate

# 安装 FastMCP
pip install fastmcp

3.3 FastMCP 服务器实现

创建 server.py:

from fastmcp import FastMCP
import json
from datetime import datetime

# 创建服务器实例
mcp = FastMCP("my-python-server")

# ============ Tools ============

@mcp.tool()
def search_docs(query: str, max_results: int = 5) -> str:
    """搜索技术文档

    Args:
        query: 搜索关键词
        max_results: 最大返回结果数(默认5)
    """
    # 模拟搜索逻辑
    results = [
        {"title": f"文档{i}: {query} 相关内容", "relevance": 0.95 - i * 0.1}
        for i in range(min(max_results, 10))
    ]
    return json.dumps(results, ensure_ascii=False, indent=2)


@mcp.tool()
def calculate(expression: str) -> str:
    """安全计算数学表达式

    Args:
        expression: 数学表达式字符串,如 "2 + 3 * 4"
    """
    # 安全的数学计算 - 只允许基本运算符
    allowed_chars = set("0123456789+-*/().% ")
    if not all(c in allowed_chars for c in expression):
        return "错误:表达式包含不允许的字符"
    try:
        result = eval(expression)  # 在输入已验证的情况下使用
        return f"{expression} = {result}"
    except Exception as e:
        return f"计算错误:{e}"


@mcp.tool()
def create_task(title: str, description: str = "", priority: str = "medium") -> str:
    """创建新任务

    Args:
        title: 任务标题
        description: 任务描述
        priority: 优先级 (low/medium/high)
    """
    task = {
        "id": f"TASK-{datetime.now().strftime('%Y%m%d%H%M%S')}",
        "title": title,
        "description": description,
        "priority": priority,
        "status": "created",
        "created_at": datetime.now().isoformat(),
    }
    return json.dumps(task, ensure_ascii=False, indent=2)


# ============ Resources ============

@mcp.resource("config://app")
def get_app_config() -> str:
    """获取应用配置"""
    config = {
        "app_name": "My Python MCP Server",
        "version": "1.0.0",
        "environment": "development",
        "features": ["search", "calculate", "task_management"],
    }
    return json.dumps(config, ensure_ascii=False, indent=2)


@mcp.resource("docs://api/{endpoint}")
def get_api_docs(endpoint: str) -> str:
    """获取 API 文档"""
    docs = {
        "search_docs": "搜索技术文档,参数: query(str), max_results(int)",
        "calculate": "计算数学表达式,参数: expression(str)",
        "create_task": "创建任务,参数: title(str), description(str), priority(str)",
    }
    return json.dumps(
        docs.get(endpoint, f"未找到 {endpoint} 的文档"),
        ensure_ascii=False
    )


# ============ Prompts ============

@mcp.prompt()
def debug_helper(error_message: str, language: str = "python") -> str:
    """调试辅助提示模板

    Args:
        error_message: 错误信息
        language: 编程语言
    """
    return f"""你是一个 {language} 调试专家。请分析以下错误并给出解决方案:

错误信息:
{error_message}

请按以下格式回答:
1. 错误原因分析
2. 可能的修复方案(从最可能到最不可能排序)
3. 预防措施"""


@mcp.prompt()
def code_review_template(code: str, focus: str = "general") -> str:
    """代码审查提示模板"""
    focus_areas = {
        "general": "整体代码质量、可读性、最佳实践",
        "security": "安全漏洞、输入验证、权限控制",
        "performance": "性能瓶颈、时间复杂度、内存使用",
    }
    return f"""请审查以下代码,重点关注:{focus_areas.get(focus, focus_areas['general'])}

```{focus}
{code}

请提供: 1. 发现的问题(按严重程度排序) 2. 改进建议 3. 修改后的代码示例"""

============ 启动服务器 ============

if name == "main": mcp.run()

### 3.4 运行 FastMCP 服务器

```bash
# stdio 模式(Claude Code 默认使用此模式)
fastmcp run server.py

# 或者开发调试模式
fastmcp dev server.py

# 指定传输方式
fastmcp run server.py --transport stdio

四、将 MCP 服务器接入 Claude Code

4.1 使用 claude mcp add 命令

Claude Code 提供了专用的 MCP 管理命令:

# 添加 stdio 模式的本地 MCP 服务器(TypeScript)
claude mcp add my-tools -- npx tsx /path/to/src/index.ts

# 添加 stdio 模式的本地 MCP 服务器(Python)
claude mcp add my-python-tools -- fastmcp run /path/to/server.py

# 添加远程 MCP 服务器(Streamable HTTP)
claude mcp add --transport http http://localhost:3000/mcp

# 指定作用域
claude mcp add --scope project my-tools -- npx tsx ./src/index.ts
claude mcp add --scope user my-tools -- npx tsx ~/mcp-servers/my-server/src/index.ts

4.2 三种作用域

图表3

图表 3

作用域选择指南: - user:个人通用工具(如搜索引擎、个人日历) - project:项目特定工具(如项目数据库、CI/CD 工具) - 优先使用 project scope,让项目配置可移植

4.3 手动编辑配置文件

你也可以直接编辑配置文件。用户级配置在 ~/.claude/settings.json,项目级配置在项目根目录的 .mcp.json:

{
  "mcpServers": {
    "my-local-server": {
      "command": "npx",
      "args": ["tsx", "/path/to/src/index.ts"],
      "env": {
        "API_KEY": "your-api-key"
      }
    },
    "my-remote-server": {
      "url": "http://localhost:3000/mcp",
      "headers": {
        "Authorization": "Bearer your-token"
      }
    },
    "my-python-server": {
      "command": "fastmcp",
      "args": ["run", "/path/to/server.py"],
      "env": {
        "PYTHONUNBUFFERED": "1"
      }
    }
  }
}

4.4 管理已添加的 MCP 服务器

# 列出所有 MCP 服务器
claude mcp list

# 查看某个服务器的详细信息
claude mcp get my-tools

# 移除服务器
claude mcp remove my-tools

# 重置服务器连接
claude mcp reset

4.5 在 Claude Code 中使用 MCP 工具

添加服务器后,在 Claude Code 对话中直接使用:

> 帮我搜索关于 React hooks 的技术文档
# Claude 会自动调用 search_docs tool

> 当前应用配置是什么?
# Claude 会读取 config://app resource

> 用代码审查模板帮我检查这段代码
# Claude 会使用 code-review prompt

Claude Code 的 /mcp 面板可以查看所有已连接的服务器状态和可用工具数量。


五、进阶特性

5.1 Tool Annotations(工具注解)

2025-06-18 规范引入了工具注解,让你的工具声明自身的行为特征:

// TypeScript SDK 示例
server.tool(
  "delete_database",
  "删除数据库",
  { db_name: z.string() },
  { annotations: {
    readOnlyHint: false,       // 非只读,会修改状态
    destructiveHint: true,     // 破坏性操作
    idempotentHint: false,     // 非幂等
    openWorldHint: false,      // 不访问外部网络
  }},
  async ({ db_name }) => {
    // 删除逻辑
    return { content: [{ type: "text", text: `已删除数据库: ${db_name}` }] };
  }
);
# FastMCP Python 示例
from fastmcp import FastMCP
from mcp.types import ToolAnnotations

mcp = FastMCP("annotated-server")

@mcp.tool(annotations=ToolAnnotations(
    readOnlyHint=True,
    destructiveHint=False,
    idempotentHint=True,
    openWorldHint=False,
))
def query_user(user_id: str) -> str:
    """查询用户信息(只读操作)"""
    return json.dumps({"user_id": user_id, "name": "张三"})

注解帮助 Claude 在调用前评估风险,对破坏性操作会额外请求用户确认。

5.2 Progress Notifications(进度通知)

长时间运行的操作可以报告进度:

server.tool(
  "process_large_file",
  "处理大文件",
  { path: z.string() },
  async ({ path }, { sendNotification }) => {
    const totalSteps = 100;
    for (let i = 0; i < totalSteps; i++) {
      // 执行处理步骤...
      await sendNotification({
        method: "notifications/progress",
        params: {
          progress: i + 1,
          total: totalSteps,
          progressToken: "file-processing",
        },
      });
    }
    return { content: [{ type: "text", text: "处理完成" }] };
  }
);

5.3 Structured Output(结构化输出)

2025-06-18 规范允许工具返回多种类型的内容:

server.tool(
  "analyze_chart",
  "分析图表并返回描述和图片",
  { chart_type: z.string() },
  async ({ chart_type }) => {
    return {
      content: [
        {
          type: "text",
          text: `分析结果:这是一个${chart_type}图表,显示上升趋势...`,
        },
        {
          type: "image",
          data: base64EncodedImage,
          mimeType: "image/png",
        },
        {
          type: "resource",
          resource: {
            uri: "analysis://full-report",
            text: "完整分析报告内容...",
          },
        },
      ],
    };
  }
);

5.4 Elicitation(人机交互)

Elicitation 是 2025-06-18 规范的重磅特性,允许服务器在工具执行过程中暂停并请求用户输入:

// 概念示例:删除前确认
server.tool(
  "safe_delete",
  "安全删除(需要用户确认)",
  { file_path: z.string() },
  async ({ file_path }, { elicitInput }) => {
    // 请求用户确认
    const confirmation = await elicitInput({
      message: `确认删除文件 ${file_path}?此操作不可撤销。`,
      requestedSchema: {
        type: "object",
        properties: {
          confirm: { type: "boolean", title: "确认删除" },
          reason: { type: "string", title: "删除原因(可选)" },
        },
        required: ["confirm"],
      },
    });

    if (!confirmation.confirm) {
      return { content: [{ type: "text", text: "操作已取消" }] };
    }

    // 执行删除...
    return { content: [{ type: "text", text: `已删除: ${file_path}` }] };
  }
);

图表4

图表 4


六、实战案例:Markdown 知识库 MCP 服务器

下面构建一个完整的、可运行的 MCP 服务器,管理本地 Markdown 知识库。

6.1 项目结构

md-knowledge-mcp/
├── package.json
├── tsconfig.json
├── src/
│   └── index.ts
└── knowledge/              # Markdown 知识库目录
    ├── getting-started.md
    └── api-reference.md

6.2 完整实现

src/index.ts:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import fs from "fs/promises";
import path from "path";

// 知识库根目录(可通过环境变量配置)
const KNOWLEDGE_DIR = process.env.KNOWLEDGE_DIR || "./knowledge";

const server = new McpServer({
  name: "md-knowledge-server",
  version: "1.0.0",
});

// ============ Tools ============

server.tool(
  "search_notes",
  "在知识库中搜索包含关键词的笔记",
  {
    query: z.string().describe("搜索关键词"),
    max_results: z.number().optional().default(10).describe("最大返回数量"),
  },
  async ({ query, max_results }) => {
    try {
      const files = await fs.readdir(KNOWLEDGE_DIR);
      const mdFiles = files.filter((f) => f.endsWith(".md"));
      const results: Array<{ file: string; matches: string[]; preview: string }> = [];

      for (const file of mdFiles) {
        if (results.length >= max_results) break;
        const content = await fs.readFile(
          path.join(KNOWLEDGE_DIR, file),
          "utf-8"
        );
        const lines = content.split("\n");
        const matches = lines.filter((line) =>
          line.toLowerCase().includes(query.toLowerCase())
        );
        if (matches.length > 0) {
          results.push({
            file,
            matches: matches.slice(0, 3),
            preview: lines.slice(0, 5).join("\n"),
          });
        }
      }

      return {
        content: [
          {
            type: "text",
            text:
              results.length > 0
                ? JSON.stringify(results, null, 2)
                : `未找到包含 "${query}" 的笔记`,
          },
        ],
      };
    } catch (error: any) {
      return {
        content: [{ type: "text", text: `搜索失败: ${error.message}` }],
        isError: true,
      };
    }
  }
);

server.tool(
  "create_note",
  "创建新的 Markdown 笔记",
  {
    title: z.string().describe("笔记标题(将作为文件名)"),
    content: z.string().describe("笔记内容(Markdown 格式)"),
    tags: z.array(z.string()).optional().describe("标签列表"),
  },
  { annotations: { readOnlyHint: false, destructiveHint: false } },
  async ({ title, content, tags }) => {
    try {
      const fileName = title.replace(/[^a-zA-Z0-9\u4e00-\u9fa5-_]/g, "-") + ".md";
      const filePath = path.join(KNOWLEDGE_DIR, fileName);

      const frontmatter = [
        "---",
        `title: ${title}`,
        `created: ${new Date().toISOString()}`,
        tags ? `tags: [${tags.join(", ")}]` : "",
        "---",
        "",
      ].join("\n");

      await fs.writeFile(filePath, frontmatter + content, "utf-8");

      return {
        content: [{ type: "text", text: `笔记已创建: ${fileName}` }],
      };
    } catch (error: any) {
      return {
        content: [{ type: "text", text: `创建失败: ${error.message}` }],
        isError: true,
      };
    }
  }
);

server.tool(
  "update_note",
  "更新已有笔记(追加内容)",
  {
    filename: z.string().describe("文件名"),
    new_content: z.string().describe("要追加的内容"),
  },
  { annotations: { readOnlyHint: false, destructiveHint: false } },
  async ({ filename, new_content }) => {
    try {
      const filePath = path.join(KNOWLEDGE_DIR, filename);
      const existing = await fs.readFile(filePath, "utf-8");
      const updated = existing + "\n\n" + new_content;
      await fs.writeFile(filePath, updated, "utf-8");
      return {
        content: [{ type: "text", text: `笔记已更新: ${filename}` }],
      };
    } catch (error: any) {
      return {
        content: [{ type: "text", text: `更新失败: ${error.message}` }],
        isError: true,
      };
    }
  }
);

// ============ Resources ============

server.resource("notes://list", async (uri) => {
  try {
    const files = await fs.readdir(KNOWLEDGE_DIR);
    const mdFiles = files.filter((f) => f.endsWith(".md"));
    const listing = mdFiles.map(async (f) => {
      const stat = await fs.stat(path.join(KNOWLEDGE_DIR, f));
      return { name: f, size: stat.size, modified: stat.mtime.toISOString() };
    });
    return {
      contents: [
        {
          uri: uri.href,
          text: JSON.stringify(await Promise.all(listing), null, 2),
        },
      ],
    };
  } catch (error: any) {
    return {
      contents: [{ uri: uri.href, text: `读取失败: ${error.message}` }],
    };
  }
});

server.resource("notes://{filename}", async (uri, { filename }) => {
  try {
    const filePath = path.join(KNOWLEDGE_DIR, filename as string);
    const content = await fs.readFile(filePath, "utf-8");
    return {
      contents: [{ uri: uri.href, text: content }],
    };
  } catch (error: any) {
    return {
      contents: [{ uri: uri.href, text: `读取失败: ${error.message}` }],
    };
  }
});

// ============ Prompts ============

server.prompt(
  "summarize-notes",
  "总结知识库笔记",
  { topic: z.string().describe("要总结的主题") },
  async ({ topic }) => {
    return {
      messages: [
        {
          role: "user",
          content: {
            type: "text",
            text: `请搜索知识库中关于「${topic}」的所有笔记,并生成一份结构化的总结报告。包括:1) 核心概念 2) 关键要点 3) 相关引用`,
          },
        },
      ],
    };
  }
);

// ============ 启动 ============

async function main() {
  // 确保知识库目录存在
  await fs.mkdir(KNOWLEDGE_DIR, { recursive: true });
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error(`Knowledge MCP Server started, dir: ${KNOWLEDGE_DIR}`);
}

main().catch(console.error);

6.3 注册到 Claude Code

# 构建项目
npm run build

# 注册到 Claude Code(项目级)
claude mcp add --scope project knowledge-base -- node /path/to/md-knowledge-mcp/dist/index.js

# 或使用 tsx 直接运行
claude mcp add --scope project knowledge-base -- npx tsx /path/to/md-knowledge-mcp/src/index.ts

# 指定知识库目录
claude mcp add --scope project knowledge-base -e KNOWLEDGE_DIR=/path/to/my-notes -- npx tsx /path/to/md-knowledge-mcp/src/index.ts

6.4 使用示例

在 Claude Code 中:

> 搜索知识库中关于 "API设计" 的笔记
# 调用 search_notes tool

> 创建一个新笔记,标题是"RESTful API 最佳实践",内容是关于 HTTP 方法的使用
# 调用 create_note tool

> 列出知识库中的所有笔记
# 读取 notes://list resource

> 帮我总结关于"数据库"主题的所有笔记
# 使用 summarize-notes prompt

七、错误处理与日志最佳实践

7.1 错误返回格式

MCP 工具的错误应通过结构化的错误响应返回,而非抛出异常:

// ✓ 正确:返回错误信息
return {
  content: [{ type: "text", text: `查询失败: ${error.message}` }],
  isError: true,  // 标记为错误
};

// ✗ 错误:直接抛出异常可能导致服务器崩溃
throw new Error("查询失败");

7.2 日志规范

// 所有日志输出到 stderr
function log(level: string, message: string, data?: any) {
  console.error(JSON.stringify({
    level,
    message,
    timestamp: new Date().toISOString(),
    ...(data && { data }),
  }));
}

server.tool("my-tool", "示例工具", { input: z.string() }, async ({ input }) => {
  log("info", "工具调用开始", { input });
  try {
    const result = await doSomething(input);
    log("info", "工具调用成功", { result });
    return { content: [{ type: "text", text: result }] };
  } catch (error: any) {
    log("error", "工具调用失败", { error: error.message });
    return { content: [{ type: "text", text: error.message }], isError: true };
  }
});

7.3 输入验证

import { z } from "zod";

// Zod 自动处理类型验证
server.tool(
  "query_database",
  "查询数据库",
  {
    table: z.string().min(1).max(64).regex(/^[a-zA-Z_][a-zA-Z0-9_]*$/),
    limit: z.number().int().min(1).max(1000).default(100),
    offset: z.number().int().min(0).default(0),
  },
  async ({ table, limit, offset }) => {
    // 参数已经过 Zod 验证
    // ...
  }
);

八、生产部署

8.1 Docker 容器化

# Dockerfile
FROM node:20-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-slim
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./

# 非 root 用户
USER node
EXPOSE 3000

CMD ["node", "dist/index.js"]

8.2 远程部署(Streamable HTTP 模式)

将 stdio 服务器改造为支持远程访问:

import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";

const app = express();
app.use(express.json());

app.post("/mcp", async (req, res) => {
  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: undefined,
  });
  await server.connect(transport);
  await transport.handleRequest(req, res, req.body);
});

app.listen(3000, () => {
  console.error("MCP Server listening on port 3000");
});

然后在 Claude Code 中注册:

claude mcp add --transport http http://your-server:3000/mcp

8.3 安全考量

图表5

图表 5

关键安全原则: 1. 永远不要信任 AI 生成的参数——在工具内部进行完整的输入验证 2. 破坏性操作使用 Tool Annotations——让 Claude 在调用前提示用户确认 3. 敏感数据通过环境变量传递——不要在代码中硬编码 API Key 4. 远程服务器必须使用 HTTPS——防止中间人攻击 5. 限制文件系统访问范围——只允许访问指定目录


九、完整工作流程总结

图表6

图表 6

关键命令速查

操作 命令
创建 TS 服务器 npm init -y && npm i @modelcontextprotocol/sdk zod
创建 Python 服务器 pip install fastmcp
调试 TS 服务器 npx @modelcontextprotocol/inspector npx tsx src/index.ts
调试 Python 服务器 fastmcp dev server.py
注册到 Claude Code claude mcp add <name> -- <command>
查看已注册服务器 claude mcp list
查看服务器详情 claude mcp get <name>
移除服务器 claude mcp remove <name>
查看 MCP 面板 Claude Code 中输入 /mcp

十、常见问题与排错

Q1:MCP 服务器启动后 Claude Code 无法连接

症状:claude mcp list 显示服务器但工具不可用。

排查步骤: 1. 手动运行服务器命令确认无报错:npx tsx src/index.ts 2. 检查 stderr 输出是否有错误信息 3. 确认 console.log() 没有被使用(必须用 console.error()) 4. 检查 Node.js 版本 >= 18

Q2:Tool 参数验证失败

原因:Zod schema 与实际参数不匹配。

解决:确保 Zod schema 描述完整,.describe() 为每个字段添加描述:

{
  // ✓ 好
  name: z.string().min(1).describe("用户名,不能为空"),

  // ✗ 差:缺少描述和约束
  name: z.string(),
}

Q3:远程 MCP 服务器连接超时

排查: 1. 确认服务器使用 Streamable HTTP 传输(非旧的 SSE) 2. 检查防火墙和网络连通性 3. 确认 URL 路径正确(通常是 /mcp) 4. 使用 curl -X POST http://server:port/mcp 测试连通性

Q4:环境变量无法传递

解决:使用 -e 参数或 env 配置:

claude mcp add -e API_KEY=xxx -e DB_URL=xxx my-server -- npx tsx src/index.ts

或配置文件中:

{
  "mcpServers": {
    "my-server": {
      "command": "npx",
      "args": ["tsx", "src/index.ts"],
      "env": { "API_KEY": "xxx" }
    }
  }
}

延伸阅读

训练营适用性评估

维度 评估
适合训练营类型 AI 工具开发、DevOps 自动化、企业 AI 集成
建议教学时长 4-6 小时(含动手实验)
难度等级 中级(需要编程基础,不需要 AI/ML 背景)
实验环境要求 Node.js 18+ 或 Python 3.10+、Claude Code CLI、Docker(可选)
可拆分模块 基础概念(1h)+ TypeScript 实战(1.5h)+ Python 实战(1.5h)+ 部署与安全(1h)+ 综合练习(1h)

[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。

51学通信知识星球海报-扫码加入