自定义MCP服务器开发指南

从零构建自己的MCP服务器

一、MCP SDK概述

MCP(Model Context Protocol)是Anthropic推出的开放协议,旨在标准化AI模型与外部工具/数据源的交互方式。MCP服务器是实现了该协议的独立服务,可以被任何MCP客户端(如Claude Desktop、Claude Code)动态发现和调用。MCP官方提供了两套主流SDK,分别面向TypeScript/Node.js和Python生态系统。

TypeScript SDK

TypeScript SDK (@modelcontextprotocol/sdk) 提供了完整的类型定义和服务器/客户端类,适合在Node.js运行时中构建MCP服务器。它天然支持异步流式处理,完美适配现代JS开发范式。安装方式如下:

npm install @modelcontextprotocol/sdk # 或 yarn add @modelcontextprotocol/sdk # 或 pnpm add @modelcontextprotocol/sdk

Python SDK

Python SDK (mcp) 提供了同样完整的MCP协议实现,支持asyncio异步编程模型,适合AI/ML开发者快速搭建MCP服务。安装方式如下:

pip install mcp # 或使用 uv(更快的包管理器) uv add mcp
核心类 (TypeScript)
Server、Client、McpServer、McpClient。Server用于提供服务,Client用于消费服务。StdioServerTransport、SSEServerTransport用于传输层配置。
核心类 (Python)
Server、Session。提供装饰器风格注册工具和资源,支持async/await异步编程,与流行的Python框架深度集成。
核心类型定义
Tool、Resource、Prompt、CallToolResult、ReadResourceResult、GetPromptResult。这些类型定义了MCP三种原语的标准接口格式。
注意: MCP SDK遵循JSON-RPC 2.0协议规范。所有服务器与客户端之间的通信均通过格式化的JSON消息进行。理解JSON-RPC的消息结构(jsonrpc、method、params、id字段)对调试和开发MCP服务器非常有帮助。

二、项目初始化

创建一个新的MCP服务器项目需要从项目初始化、依赖安装到基本结构搭建。以下是TypeScript和Python两种方案的完整初始化步骤。

TypeScript 项目初始化

mkdir my-mcp-server cd my-mcp-server npm init -y npm install @modelcontextprotocol/sdk zod npm install -D typescript @types/node ts-node # 创建 tsconfig.json npx tsc --init --target ES2022 --module NodeNext --moduleResolution NodeNext --outDir ./dist --rootDir ./src --declaration true

推荐的项目目录结构如下:

my-mcp-server/ ├── src/ │ ├── index.ts # 入口文件,创建服务器实例 │ ├── tools/ # 工具实现 │ │ ├── weather.ts │ │ └── calculator.ts │ ├── resources/ # 资源实现 │ │ └── files.ts │ └── prompts/ # 提示模板 │ └── templates.ts ├── package.json ├── tsconfig.json └── README.md

Python 项目初始化

mkdir my-mcp-server cd my-mcp-server python -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate pip install mcp httpx # 或使用 pyproject.toml # [project] # name = "my-mcp-server" # version = "0.1.0" # dependencies = ["mcp", "httpx"]

推荐的Python项目目录结构:

my-mcp-server/ ├── src/ │ ├── __init__.py │ ├── server.py # 入口文件 │ ├── tools/ # 工具模块 │ │ ├── __init__.py │ │ ├── weather.py │ │ └── search.py │ └── resources/ # 资源模块 │ ├── __init__.py │ └── docs.py ├── pyproject.toml └── README.md
点击复制 MCP Server 的核心设计原则:每个服务器可以独立提供 Tools(工具)、Resources(资源)和 Prompts(提示模板)三种原语中的任意组合。客户端通过 capabilities 协商来发现服务器支持哪些功能。开发者可以根据需要只实现其中一种或全部三种。

三、实现一个简单的Tool(工具)

Tool(工具)是MCP服务器中最常用的原语,它允许AI模型执行外部操作——查询数据库、调用API、进行计算等。每个Tool需要定义名称、描述和输入参数模式(inputSchema),并通过处理函数返回结果。

TypeScript 实现

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: "weather-server", version: "1.0.0", }); // 定义工具:获取天气信息 server.tool( "get-weather", { city: z.string().describe("城市名称"), days: z.number().optional().describe("预报天数"), }, async ({ city, days = 1 }) => { // 这里可以调用真实天气API const result = `查询城市:${city},未来${days}天天气:晴,温度15-25℃`; return { content: [{ type: "text", text: result }] }; } ); // 启动服务器 const transport = new StdioServerTransport(); await server.connect(transport);

Python 实现

from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import TextContent # 创建服务器实例 server = Server("weather-server") @server.tool("get-weather") async def get_weather(city: str, days: int = 1) -> list[TextContent]: """查询指定城市的天气信息""" # 这里可以调用真实天气API result = f"查询城市:{city},未来{days}天天气:晴,温度15-25℃" return [TextContent(type="text", text=result)] async def main(): async with stdio_server() as (read_stream, write_stream): await server.run(read_stream, write_stream, server.create_initialization_options())

Tool 设计要点

设计Tool时需注意以下几点:第一,name使用小写字母和连字符的kebab-case命名,便于客户端调用;第二,description清晰描述工具用途,帮助AI模型理解何时使用;第三,inputSchema参数应尽可能精确,使用必选参数而非全可选,降低模型误调用概率;第四,返回结果使用标准化的content数组格式,支持text、image、resource等多种类型。

最佳实践: 为每个Tool编写详细的description。AI模型(如Claude)通过description来判断何时调用哪个Tool。一个模糊的description会导致模型在错误的场景下调用工具,或在正确的场景下忽略调用。好的description应当描述"什么情况下使用此工具"以及"返回什么类型的数据"。

四、实现Resources和Prompts

除Tools之外,MCP协议还定义了Resources(资源)和Prompts(提示模板)两种原语。Resources允许AI模型读取结构化的外部数据,如文件内容、数据库记录;Prompts则为常见任务提供可复用的提示模板。

实现 Resources

Resources采用类似文件系统的URI方案进行标识。每个Resource通过唯一的URI进行访问,支持静态资源和模板资源两种模式。静态资源对应固定URI(如file:///docs/readme),模板资源使用URI Template语法支持参数化访问。

// TypeScript 实现 Resources // 静态资源 server.resource( "config", "config://app", async (uri) => ({ contents: [{ uri: uri.href, text: JSON.stringify({ version: "1.0", debug: false }) }] }) ); // 模板资源 - 支持参数化 server.resource( "user-profile", new ResourceTemplate("users://{userId}/profile", { list: undefined }), async (uri, { userId }) => ({ contents: [{ uri: uri.href, text: `用户 ${userId} 的配置信息...` }] }) );
# Python 实现 Resources @server.resource("config://app") async def get_config(uri: str) -> str: """返回应用配置信息""" return json.dumps({"version": "1.0", "debug": False}) @server.resource("users://{userId}/profile") async def get_user_profile(uri: str, userId: str) -> str: """返回指定用户的配置信息""" return f"用户 {userId} 的配置信息..."

实现 Prompts

Prompts是可复用的提示模板,支持参数化占位符。当客户端需要执行某种固定模式的任务时,可以通过Prompts获取预定义的提示内容,填充参数后发送给AI模型。

// TypeScript 实现 Prompts server.prompt( "code-review", { language: z.string().describe("编程语言"), code: z.string().describe("代码内容"), }, ({ language, code }) => ({ messages: [{ role: "user", content: { type: "text", text: `请审查以下${language}代码,指出潜在的问题和改进建议:\n\n${code}` } }] }) ); server.prompt( "translate", { text: z.string(), targetLang: z.string() }, ({ text, targetLang }) => ({ messages: [{ role: "user", content: { type: "text", text: `请将以下内容翻译成${targetLang}:\n\n${text}` } }] }) );
# Python 实现 Prompts @server.prompt("code-review") async def code_review(language: str, code: str) -> list[Message]: return [ Message( role="user", content=TextContent( type="text", text=f"请审查以下{language}代码,指出潜在的问题和改进建议:\n\n{code}" ) ) ]
重要: 三种原语的选择策略:如果你需要模型执行操作(写数据库、发邮件、算数学),使用Tool;如果你需要模型读取数据(查文档、看文件、获取配置),使用Resource;如果你需要模型按照固定格式处理重复任务,使用Prompt。一个服务器可以同时提供三种原语,客户端在connect时自动发现所有能力。

五、配置传输层

传输层决定了MCP服务器如何与客户端进行通信。MCP协议定义了两种标准传输方式:stdio(标准输入输出)和SSE(Server-Sent Events),各自适用于不同的部署场景。

stdio 传输

stdio传输是最简单的传输方式,服务器通过标准输入接收JSON-RPC请求,通过标准输出发送响应。客户端(如Claude Desktop)以子进程方式启动服务器,通过管道进行双向通信。

// TypeScript - stdio 传输 import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; const transport = new StdioServerTransport(); await server.connect(transport); console.error("MCP Server running on stdio");
# Python - stdio 传输 from mcp.server.stdio import stdio_server async def main(): async with stdio_server() as (read_stream, write_stream): await server.run(read_stream, write_stream, server.create_initialization_options())

使用stdio传输的场景:本地开发工具、Claude Desktop插件、与CI/CD流水线集成。stdio的优势在于零配置、低延迟、无需网络端口占用。但缺点是无法远程访问,服务器生命周期与客户端绑定。

SSE 传输

SSE传输使MCP服务器作为HTTP服务运行,支持远程访问和多个客户端连接。服务器通过HTTP POST接收客户端请求,通过SSE连接向客户端推送事件。

// TypeScript - SSE 传输(使用 Express) import express from "express"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; const app = express(); let transport = null; app.get("/sse", (req, res) => { transport = new SSEServerTransport("/messages", res); server.connect(transport); }); app.post("/messages", (req, res) => { transport.handlePostMessage(req, res); }); app.listen(3000, () => { console.error("MCP Server running on http://localhost:3000/sse"); });
# Python - SSE 传输(使用 Starlette) from starlette.applications import Starlette from starlette.routing import Route from mcp.server.sse import SseServerTransport sse = SseServerTransport("/messages") async def handle_sse(request): async with sse.connect_sse( request.scope, request.receive, request._send ) as (read_stream, write_stream): await server.run(read_stream, write_stream, server.create_initialization_options()) async def handle_messages(request): await sse.handle_post_message(request.scope, request.receive) routes = [ Route("/sse", endpoint=handle_sse), Route("/messages", endpoint=handle_messages, methods=["POST"]), ] app = Starlette(routes=routes)

使用SSE传输的场景:云端部署的MCP服务器、需要多客户端共享的服务、与现有Web服务集成的场景。SSE支持跨网络访问,服务器可独立运行,客户端无需本地安装。缺点是增加了网络延迟,需要处理认证和安全性。

点击复制 配置Claude Desktop连接MCP服务器(claude_desktop_config.json): { "mcpServers": { "my-weather-server": { "command": "node", "args": ["path/to/dist/index.js"] }, "my-python-server": { "command": "uv", "args": ["run", "path/to/server.py"] } } } 对于SSE远程服务器,使用"url"字段替代"command"/"args"。

六、测试与调试

MCP服务器的测试和调试需要特殊工具和方法,因为其通信基于JSON-RPC协议而不是传统的REST API。MCP官方提供了Inspector工具,同时也推荐编写自动化测试来确保服务器稳定性。

MCP Inspector 交互式调试

MCP Inspector是官方提供的Web-based调试工具,可以连接到任何MCP服务器并交互式测试其Tools、Resources和Prompts。它提供了可视化的请求-响应面板,方便开发者观察原始JSON-RPC消息。

# 全局安装 Inspector(方式一:npx) npx @modelcontextprotocol/inspector node dist/index.js # 方式二:直接连接已运行的SSE服务器 npx @modelcontextprotocol/inspector --connect http://localhost:3000/sse # 安装后打开浏览器访问 http://localhost:5173

Inspector提供的主要功能包括:查看服务器信息和capabilities声明、测试每个Tool的调用并查看原始JSON响应、浏览Resources列表并读取指定资源内容、测试Prompts模板并查看生成的消息内容、检查服务器日志输出、检查连接状态和错误信息。

编写自动化测试

推荐使用SDK提供的测试工具编写自动化测试,验证每个Tool和Resource的行为是否符合预期。

// TypeScript 自动化测试示例 import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; async function testWeatherTool() { const server = new McpServer({ name: "test", version: "1.0" }); server.tool("get-weather", { city: z.string() }, async ({ city }) => { return { content: [{ type: "text", text: `天气:${city} 晴` }] }; } ); // 使用内存传输进行测试 const [clientTransport, serverTransport] = InMemoryTransport.createPair(); const client = new Client({ name: "test-client", version: "1.0" }); await Promise.all([ server.connect(serverTransport), client.connect(clientTransport), ]); const result = await client.request( { method: "tools/call", params: { name: "get-weather", arguments: { city: "北京" } }}, CallToolResultSchema ); console.assert(result.content[0].text === "天气:北京 晴"); console.log("测试通过!"); }
# Python 自动化测试示例 import pytest from mcp import ClientSession, StdioServerParameters @pytest.mark.asyncio async def test_get_weather(): async with ClientSession( StdioServerParameters( command="python", args=["src/server.py"] ) ) as session: await session.initialize() result = await session.call_tool( "get-weather", {"city": "北京"} ) assert "北京" in result.content[0].text print("测试通过!")

常见错误排查

开发MCP服务器时常见的错误及解决方法:

1. 传输层未正确连接:检查transport是否正确创建并connect;stdio模式确保子进程路径正确;SSE模式检查端口是否被占用。

2. Tool参数校验失败:检查inputSchema定义是否与处理函数的参数签名一致;使用zod(TS)或Pydantic(Python)进行严格校验。

3. 返回格式错误:检查返回的content数组是否符合CallToolResult格式;确保JSON-RPC消息的id字段正确匹配请求。

4. 服务器崩溃/意外退出:检查未捕获的异常;在关键处添加try/catch;使用console.error(TS)或logging(Python)记录日志到stderr。

调试技巧: stdio传输模式下,所有console.log输出会被误认为JSON-RPC响应发送给客户端,导致协议解析错误。应始终使用console.error(输出到stderr)打印调试日志,因为stderr不会被MCP传输层捕获。在开发过程中,Inspector的"Server Logs"面板可以实时查看stderr输出。

七、发布MCP服务器

完成开发后,可以将MCP服务器发布到公共包管理器供他人使用。根据SDK的选择,分别发布到npm(TypeScript)或PyPI(Python)。良好的文档和配置示例对于用户采用至关重要。

发布到 npm

# 构建 TypeScript 项目 npm run build # tsc 编译 # 确保 package.json 包含正确配置 # { # "name": "@your-org/my-mcp-server", # "type": "module", # "main": "./dist/index.js", # "bin": { # "my-mcp-server": "./dist/index.js" # }, # "files": ["dist/"] # } # 登录并发布 npm login npm publish --access public

发布到 PyPI

# 使用 pyproject.toml 配置 # [build-system] # requires = ["setuptools>=68.0", "wheel"] # build-backend = "setuptools.build_meta" # # [project] # name = "my-mcp-server" # version = "0.1.0" # description = "My custom MCP server" # requires-python = ">=3.10" # dependencies = ["mcp", "httpx"] # 构建并发布 python -m build twine upload dist/* # 或使用 uv uv build uv publish

编写用户配置文档

发布后,用户需要将你的MCP服务器配置到他们的客户端中。在README中提供清晰的配置示例至关重要,包括Claude Desktop的claude_desktop_config.json配置示例、Claude Code的CLAUUDE.md或settings.json配置方式以及使用SSE传输时的远程连接URL。

点击复制 完整的README文档应包含: 1. 项目简介:说明MCP服务器的功能和适用场景 2. 快速开始:从安装到运行的最简步骤 3. 支持的Tools列表:每个Tool的名称、参数说明和返回值 4. 支持的Resources列表:每个Resource的URI模式和说明 5. 支持的Prompts列表:每个Prompt的参数和示例 6. 配置示例:Claude Desktop和Claude Code的配置JSON 7. 开发指南:本地开发、测试和构建命令 8. 许可证和贡献指南:MIT或Apache-2.0许可证,PR和Issue流程 一个好的README是项目被广泛采用的关键。

在GitHub上开源

将MCP服务器开源到GitHub可以吸引社区贡献,形成良性发展循环。推荐在仓库中配置好GitHub Actions CI/CD流水线,自动运行测试、构建和发布。在Description中添加"MCP Server"标签,便于其他开发者搜索发现。可以在MCP官方Awesome列表(github.com/modelcontextprotocol/awesome-mcp-servers)中提交你的服务器,获得更广泛的曝光。

安全提示: MCP服务器具有执行代码和访问数据的权限,在发布前务必进行安全审查。特别注意:不要硬编码API密钥或敏感凭证,应通过环境变量注入;实现用户认证机制(对于SSE远程服务器);对用户输入进行验证和清理,防止注入攻击;在文档中明确告知用户服务器需要哪些权限和访问哪些资源。