动手构建 MCP Server:Python & TypeScript 实战指南

0 阅读6分钟

合集:MCP(模型上下文协议)系列 · 中级篇(一)


前言

安装现成的 MCP Server 很简单,但真正的威力在于:你能构建任何你需要的 MCP Server,让 AI 连接到你特有的系统、数据库或内部工具。

本篇带你从零开始,用 Python 和 TypeScript 分别构建一个完整的 MCP Server,包含工具、资源和提示模板三类能力。国内使用Claud Code 访问 ccAiHub.com


一、理解 MCP Server 的生命周期

主机启动 MCP Server 进程
    ↓
握手(Handshake):交换能力列表
    ↓
主机发现 Server 提供的 Tools/Resources/Prompts
    ↓
用户发起请求
    ↓
AI 决定调用某个 Tool
    ↓
主机通过 JSON-RPC 调用 ServerServer 执行逻辑,返回结果
    ↓
AI 将结果纳入推理,生成最终回复

二、Python 实战:使用 FastMCP

2.1 环境准备

# 推荐使用 uv 管理 Python 项目
pip install uv

# 创建项目
mkdir my-mcp-server && cd my-mcp-server
uv init
uv add mcp

# 或使用 pip
pip install mcp

2.2 最简单的 MCP Server

server.py

from mcp.server.fastmcp import FastMCP

# 创建 Server 实例
mcp = FastMCP("my-first-server")

# 定义一个工具
@mcp.tool()
def hello(name: str) -> str:
    """向指定的人打招呼"""
    return f"你好,{name}!来自 MCP Server 的问候。"

if __name__ == "__main__":
    mcp.run()

就这样!FastMCP 会自动处理:

  • JSON Schema 生成(从函数签名推断)
  • 参数验证
  • 错误处理
  • 协议握手

2.3 完整示例:代码质量分析 Server

code_analysis_server.py

import subprocess
import json
from pathlib import Path
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("code-analysis")

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

@mcp.tool()
def run_linter(file_path: str, fix: bool = False) -> str:
    """
    对指定文件运行 ESLint 检查
    
    Args:
        file_path: 要检查的文件路径
        fix: 是否自动修复可修复的问题
    """
    if not Path(file_path).exists():
        return f"❌ 文件不存在:{file_path}"
    
    cmd = ["npx", "eslint", "--format", "json"]
    if fix:
        cmd.append("--fix")
    cmd.append(file_path)
    
    result = subprocess.run(cmd, capture_output=True, text=True)
    
    try:
        lint_results = json.loads(result.stdout)
        errors = sum(r["errorCount"] for r in lint_results)
        warnings = sum(r["warningCount"] for r in lint_results)
        
        if errors == 0 and warnings == 0:
            return f"✅ {file_path} 通过检查,无问题"
        
        issues = []
        for file_result in lint_results:
            for msg in file_result.get("messages", []):
                severity = "❌" if msg["severity"] == 2 else "⚠️"
                issues.append(
                    f"{severity}{msg['line']} 行:{msg['message']} ({msg['ruleId']})"
                )
        
        return f"发现 {errors} 个错误,{warnings} 个警告:\n" + "\n".join(issues)
    except json.JSONDecodeError:
        return result.stdout or result.stderr

@mcp.tool()
def calculate_complexity(file_path: str) -> dict:
    """
    计算文件的圈复杂度
    
    Returns:
        包含函数名和复杂度的字典列表
    """
    if not Path(file_path).exists():
        return {"error": f"文件不存在:{file_path}"}
    
    result = subprocess.run(
        ["npx", "complexity-report", "--format", "json", file_path],
        capture_output=True, text=True
    )
    
    try:
        data = json.loads(result.stdout)
        functions = []
        for func in data.get("functions", []):
            complexity = func.get("complexity", {}).get("cyclomatic", 0)
            functions.append({
                "name": func.get("name", "anonymous"),
                "complexity": complexity,
                "risk": "高" if complexity > 10 else "中" if complexity > 5 else "低"
            })
        return {"functions": functions, "file": file_path}
    except:
        return {"error": "无法计算复杂度", "output": result.stdout}

@mcp.tool()
def find_duplicate_code(directory: str, min_lines: int = 5) -> str:
    """
    在目录中查找重复代码块
    
    Args:
        directory: 要扫描的目录
        min_lines: 最小重复行数阈值
    """
    result = subprocess.run(
        ["npx", "jscpd", "--min-lines", str(min_lines), "--output", "json", directory],
        capture_output=True, text=True
    )
    # 处理结果...
    return f"扫描完成,结果见 report/ 目录"

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

@mcp.resource("analysis://reports/{report_id}")
def get_analysis_report(report_id: str) -> str:
    """获取历史分析报告"""
    report_path = Path(f"reports/{report_id}.json")
    if not report_path.exists():
        return f"报告 {report_id} 不存在"
    return report_path.read_text()

@mcp.resource("analysis://config")
def get_analysis_config() -> str:
    """获取当前分析配置"""
    config_path = Path(".eslintrc.json")
    if config_path.exists():
        return config_path.read_text()
    return "{}"

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

@mcp.prompt()
def code_review_prompt(file_path: str, focus: str = "全面") -> str:
    """
    生成代码审查提示
    
    Args:
        file_path: 要审查的文件
        focus: 审查重点(安全/性能/规范/全面)
    """
    return f"""
请对 {file_path} 进行代码审查,重点关注:{focus}

审查维度:
1. 先运行 run_linter 工具检查语法和规范问题
2. 用 calculate_complexity 评估代码复杂度
3. 根据结果提出具体的改进建议

请以 Markdown 格式输出审查报告,包含:
- 总体评分(1-10)
- 主要问题列表(按优先级排序)
- 具体修改建议(带代码示例)
"""

if __name__ == "__main__":
    mcp.run()

2.4 连接到 Claude Desktop

claude_desktop_config.json

{
  "mcpServers": {
    "code-analysis": {
      "command": "python",
      "args": ["/absolute/path/to/code_analysis_server.py"]
    }
  }
}

三、TypeScript 实战:使用官方 SDK

3.1 环境准备

mkdir my-ts-mcp-server && cd my-ts-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk
npm install -D typescript @types/node
npx tsc --init

3.2 完整示例:项目管理助手 Server

src/index.ts

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  ListToolsRequestSchema,
  CallToolRequestSchema,
  ListResourcesRequestSchema,
  ReadResourceRequestSchema,
  ListPromptsRequestSchema,
  GetPromptRequestSchema,
  Tool,
  Resource,
  Prompt,
} from "@modelcontextprotocol/sdk/types.js";
import fs from "fs";
import path from "path";

// ==================== 初始化 Server ====================

const server = new Server(
  {
    name: "project-manager",
    version: "1.0.0",
  },
  {
    capabilities: {
      tools: {},
      resources: {},
      prompts: {},
    },
  }
);

// ==================== 定义工具列表 ====================

const TOOLS: Tool[] = [
  {
    name: "create_task",
    description: "在 tasks.json 中创建新任务",
    inputSchema: {
      type: "object",
      properties: {
        title: { type: "string", description: "任务标题" },
        description: { type: "string", description: "任务描述" },
        priority: {
          type: "string",
          enum: ["low", "medium", "high"],
          description: "优先级",
          default: "medium",
        },
        assignee: { type: "string", description: "负责人" },
      },
      required: ["title"],
    },
  },
  {
    name: "list_tasks",
    description: "列出所有任务,支持按状态和优先级过滤",
    inputSchema: {
      type: "object",
      properties: {
        status: {
          type: "string",
          enum: ["todo", "in_progress", "done"],
          description: "按状态过滤",
        },
        priority: {
          type: "string",
          enum: ["low", "medium", "high"],
          description: "按优先级过滤",
        },
      },
    },
  },
  {
    name: "update_task_status",
    description: "更新任务状态",
    inputSchema: {
      type: "object",
      properties: {
        task_id: { type: "string", description: "任务 ID" },
        status: {
          type: "string",
          enum: ["todo", "in_progress", "done"],
          description: "新状态",
        },
      },
      required: ["task_id", "status"],
    },
  },
];

// ==================== 工具处理逻辑 ====================

const TASKS_FILE = "tasks.json";

function loadTasks(): any[] {
  if (!fs.existsSync(TASKS_FILE)) return [];
  return JSON.parse(fs.readFileSync(TASKS_FILE, "utf-8"));
}

function saveTasks(tasks: any[]): void {
  fs.writeFileSync(TASKS_FILE, JSON.stringify(tasks, null, 2));
}

server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  switch (name) {
    case "create_task": {
      const tasks = loadTasks();
      const newTask = {
        id: `task-${Date.now()}`,
        title: args.title as string,
        description: args.description as string || "",
        priority: args.priority as string || "medium",
        assignee: args.assignee as string || "未分配",
        status: "todo",
        createdAt: new Date().toISOString(),
      };
      tasks.push(newTask);
      saveTasks(tasks);
      return {
        content: [
          {
            type: "text",
            text: `✅ 任务创建成功!\nID: ${newTask.id}\n标题: ${newTask.title}\n优先级: ${newTask.priority}`,
          },
        ],
      };
    }

    case "list_tasks": {
      let tasks = loadTasks();
      if (args.status) tasks = tasks.filter((t: any) => t.status === args.status);
      if (args.priority) tasks = tasks.filter((t: any) => t.priority === args.priority);

      if (tasks.length === 0) {
        return { content: [{ type: "text", text: "没有找到符合条件的任务" }] };
      }

      const table = tasks
        .map((t: any) => `- [${t.status}] ${t.title} (${t.priority}) - ${t.assignee}`)
        .join("\n");

      return { content: [{ type: "text", text: `找到 ${tasks.length} 个任务:\n${table}` }] };
    }

    case "update_task_status": {
      const tasks = loadTasks();
      const task = tasks.find((t: any) => t.id === args.task_id);
      if (!task) {
        return { content: [{ type: "text", text: `❌ 未找到任务:${args.task_id}` }] };
      }
      const oldStatus = task.status;
      task.status = args.status;
      task.updatedAt = new Date().toISOString();
      saveTasks(tasks);
      return {
        content: [
          {
            type: "text",
            text: `✅ 任务状态已更新:${task.title}\n${oldStatus}${args.status}`,
          },
        ],
      };
    }

    default:
      throw new Error(`未知工具:${name}`);
  }
});

// ==================== 资源处理 ====================

server.setRequestHandler(ListResourcesRequestSchema, async () => ({
  resources: [
    {
      uri: "tasks://all",
      name: "所有任务",
      description: "完整的任务列表(JSON 格式)",
      mimeType: "application/json",
    },
    {
      uri: "tasks://summary",
      name: "任务摘要",
      description: "按状态统计的任务摘要",
      mimeType: "text/plain",
    },
  ] as Resource[],
}));

server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  const { uri } = request.params;

  if (uri === "tasks://all") {
    const tasks = loadTasks();
    return {
      contents: [
        { uri, mimeType: "application/json", text: JSON.stringify(tasks, null, 2) },
      ],
    };
  }

  if (uri === "tasks://summary") {
    const tasks = loadTasks();
    const summary = {
      todo: tasks.filter((t: any) => t.status === "todo").length,
      in_progress: tasks.filter((t: any) => t.status === "in_progress").length,
      done: tasks.filter((t: any) => t.status === "done").length,
      total: tasks.length,
    };
    return {
      contents: [
        {
          uri,
          mimeType: "text/plain",
          text: `任务摘要:\n待办:${summary.todo}\n进行中:${summary.in_progress}\n已完成:${summary.done}\n共计:${summary.total}`,
        },
      ],
    };
  }

  throw new Error(`未知资源:${uri}`);
});

// ==================== 提示模板 ====================

server.setRequestHandler(ListPromptsRequestSchema, async () => ({
  prompts: [
    {
      name: "daily_standup",
      description: "生成每日站会报告",
      arguments: [
        { name: "team", description: "团队名称", required: false },
      ],
    },
  ] as Prompt[],
}));

server.setRequestHandler(GetPromptRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  if (name === "daily_standup") {
    const team = args?.team || "我们的团队";
    return {
      description: "每日站会报告模板",
      messages: [
        {
          role: "user",
          content: {
            type: "text",
            text: `请为 ${team} 生成今日站会报告。
            
步骤:
1. 读取 tasks://summary 资源了解整体进度
2. 列出所有 in_progress 状态的任务
3. 列出所有 done 状态且今天更新的任务
4. 识别可能的阻塞项

输出格式:
## 昨日完成
## 今日计划  
## 阻塞项(如有)`,
          },
        },
      ],
    };
  }

  throw new Error(`未知提示模板:${name}`);
});

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

async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("项目管理 MCP Server 已启动");
}

main().catch(console.error);

3.3 编译和配置

# 编译
npx tsc

# 配置到 Claude Desktop
{
  "mcpServers": {
    "project-manager": {
      "command": "node",
      "args": ["/absolute/path/to/my-ts-mcp-server/dist/index.js"]
    }
  }
}

四、调试技巧

4.1 MCP Inspector(官方调试工具)

# 启动调试界面
npx @modelcontextprotocol/inspector python server.py
# 或
npx @modelcontextprotocol/inspector node dist/index.js

打开 http://localhost:5173,可以:

  • 查看 Server 暴露的所有工具/资源/提示
  • 直接调用工具测试
  • 查看请求/响应的原始 JSON

4.2 日志输出

# Python:写入 stderr(不干扰 stdio 协议)
import sys
print("Debug info", file=sys.stderr)
// TypeScript:写入 stderr
console.error("Debug info");

五、本篇小结

语言框架特点
PythonFastMCP装饰器语法,最简洁,推荐入门
TypeScript官方 SDK类型安全,生态更完善
Go/Java/...社区 SDK适合已有技术栈

核心原则:最小化 Server 的职责——一个 Server 专注一件事,通过组合多个 Server 实现复杂能力。

下一篇深入 MCP 的三大原语,学习更高级的 Resources 订阅、Tools 注解和动态 Prompts。


系列导航

  • 初级篇(三):MCP 生态地图:工具、数据库、搜索全覆盖
  • 中级篇(一):动手构建 MCP Server:Python & TypeScript 实战 ← 当前
  • 中级篇(二):深入三大原语:Resources、Tools 和 Prompts
  • 中级篇(三):MCP + RAG:构建企业知识库问答系统
  • 高级篇(一):企业级 MCP 架构:安全、认证与高可用
  • 高级篇(二):MCP OAuth 2.1 实战:标准化身份认证
  • 高级篇(三):MCP + 多智能体编排:下一代 AI 工作流