0

MCP Server开发实战:从零构建AI模型工具调用服务

2026.05.30 | youres | 4次围观

MCP到底解决了什么问题

如果你用过Claude Desktop、Cursor或者Windsurf,大概率已经接触过MCP了——当你让AI帮你查文件、搜代码、操作数据库时,背后跑的就是MCP Server。但大多数教程停留在"装个现成Server跑通demo"的阶段,真正动手从零写一个MCP Server的人少之又少。原因很简单:官方文档虽然完整,但缺少一条从需求分析到生产部署的完整路径。

我最近为公司内部AI助手开发了三个MCP Server——分别对接内部Wiki、JIRA工单和PostgreSQL数据查询,踩了不少坑。这篇文章把实战中最关键的经验整理出来,帮你避开我走过的弯路。

一、MCP协议核心概念:别被架构图吓到

MCP(Model Context Protocol)的本质就一句话:让AI模型能像调用函数一样调用外部工具。它定义了一套标准化的JSON-RPC 2.0通信协议,让任何LLM客户端都能用统一的方式发现和调用你的工具。

1.1 四个核心原语

原语方向作用典型场景
ToolsServer→Client暴露可调用的函数查询数据库、发送通知
ResourcesServer→Client暴露可读取的数据文件内容、API响应
PromptsServer→Client提供预设Prompt模板代码审查模板、报告生成
SamplingClient→ServerServer请求模型推理多步工具链中的中间决策

90%的MCP Server只需要实现Tools就够了。Resources和Prompts是锦上添花,Sampling在实际项目中几乎用不到。

1.2 通信方式选择

  • stdio:最简单,Server作为子进程启动,通过标准输入输出通信。适合本地桌面应用(Claude Desktop、Cursor)
  • SSE(Server-Sent Events):基于HTTP的长连接,适合远程部署和Web场景

建议:开发阶段用stdio快速调试,生产环境切SSE。两者只是传输层差异,业务逻辑完全复用。

二、从零搭建第一个MCP Server

我们用一个实际需求驱动:给AI助手提供查询公司内部知识库的能力。这是一个典型的Tools场景。

2.1 项目初始化

mkdir wiki-mcp-server && cd wiki-mcp-server
npm init -y
npm install @modelcontextprotocol/sdk zod

用TypeScript开发体验更好,但这里用纯JS保持简洁。zod是MCP SDK的依赖,用于参数校验。

2.2 最小可用Server代码

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: "wiki-search",
  version: "1.0.0",
});

// 注册工具:搜索Wiki
server.tool(
  "search_wiki",                          // 工具名
  "搜索公司内部Wiki知识库,返回匹配的文档标题和摘要",  // 描述(AI靠这个理解工具用途)
  {
    query: z.string().describe("搜索关键词"),
    limit: z.number().optional().describe("返回结果数量,默认5"),
  },
  async ({ query, limit = 5 }) => {
    // 实际项目中这里调你的Wiki API
    const results = await fetchWikiResults(query, limit);
    return {
      content: [
        {
          type: "text",
          text: JSON.stringify(results, null, 2),
        },
      ],
    };
  }
);

// 注册工具:获取Wiki文档详情
server.tool(
  "get_wiki_page",
  "根据文档ID获取Wiki页面完整内容",
  {
    page_id: z.string().describe("Wiki文档ID"),
  },
  async ({ page_id }) => {
    const page = await fetchWikiPage(page_id);
    return {
      content: [
        {
          type: "text",
          text: page.content,
        },
      ],
    };
  }
);

// 启动Server
const transport = new StdioServerTransport();
await server.connect(transport);

关键点:工具描述是给AI看的,写得越精准,AI调用时选错工具的概率越低。我见过有人写"搜索"两个字的描述,结果AI在任何场景都调这个工具。

2.3 模拟数据实现

async function fetchWikiResults(query, limit) {
  // 生产环境替换为真实API调用
  const mockData = [
    { id: "doc-001", title: "新人入职指南", snippet: "包含账号申请、环境配置..." },
    { id: "doc-002", title: "API网关配置规范", snippet: "路由规则、限流策略..." },
    { id: "doc-003", title: "数据库变更流程", snippet: "DDL审核、灰度发布..." },
  ];
  return mockData.filter(d =>
    d.title.includes(query) || d.snippet.includes(query)
  ).slice(0, limit);
}

async function fetchWikiPage(pageId) {
  return {
    id: pageId,
    title: "示例文档",
    content: "这是文档正文内容...",
    updatedAt: "2026-05-28",
  };
}

三、客户端配置:让AI发现你的Server

Server写好了,还需要在LLM客户端配置才能被调用。不同客户端的配置方式:

3.1 Claude Desktop配置

编辑 ~/AppData/Roaming/Claude/claude_desktop_config.json(Windows)或 ~/Library/Application Support/Claude/claude_desktop_config.json(macOS):

{
  "mcpServers": {
    "wiki-search": {
      "command": "node",
      "args": ["C:/path/to/wiki-mcp-server/index.mjs"]
    }
  }
}

3.2 Cursor配置

在Cursor设置中找到MCP选项,添加Server配置。Cursor也支持SSE模式连接远程Server,适合团队共享。

3.3 OpenClaw Agent配置

如果你使用OpenClaw,可以在Agent的配置文件中添加MCP Server,让Agent自动发现和调用工具。具体可参考OpenClaw Agent实战指南

四、进阶:生产级MCP Server的五个必备能力

Demo跑通只是起点,上生产还需要补齐这些能力:

4.1 认证与鉴权

MCP协议本身不包含认证机制,但你可以通过环境变量注入Token:

server.tool(
  "search_wiki",
  "搜索Wiki知识库",
  { query: z.string() },
  async ({ query }) => {
    const token = process.env.WIKI_API_TOKEN;
    if (!token) {
      return {
        content: [{ type: "text", text: "错误:未配置WIKI_API_TOKEN" }],
        isError: true,
      };
    }
    // 带Token调用真实API
    const res = await fetch("https://wiki.example.com/api/search", {
      headers: { Authorization: `Bearer ${token}` },
    });
    // ...
  }
);

4.2 错误处理与超时

外部API随时可能挂掉,必须有超时和降级策略:

async function fetchWithTimeout(url, options = {}, timeout = 10000) {
  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), timeout);
  try {
    const res = await fetch(url, { ...options, signal: controller.signal });
    return res;
  } finally {
    clearTimeout(timer);
  }
}

4.3 参数校验的边界情况

zod能做基础校验,但业务逻辑校验需要自己写。比如搜索关键词长度限制、SQL注入防护等:

server.tool(
  "query_database",
  "执行只读SQL查询",
  {
    sql: z.string().describe("SELECT语句"),
  },
  async ({ sql }) => {
    // 安全校验:只允许SELECT
    const normalized = sql.trim().toUpperCase();
    if (!normalized.startsWith("SELECT")) {
      return {
        content: [{ type: "text", text: "仅允许SELECT查询" }],
        isError: true,
      };
    }
    // 防止多语句注入
    if (sql.includes(";")) {
      return {
        content: [{ type: "text", text: "不允许包含分号" }],
        isError: true,
      };
    }
    // 执行查询...
  }
);

4.4 结果分页

AI模型的上下文窗口有限,一次返回太多数据反而降低效果。我的经验:单次工具返回内容控制在2000字以内,超过就分页。

4.5 日志与可观测性

给每个工具调用加上日志,方便排查问题:

server.tool(
  "search_wiki",
  "搜索Wiki",
  { query: z.string() },
  async ({ query }) => {
    const startTime = Date.now();
    console.error(`[MCP] search_wiki called: query="${query}"`);
    try {
      const results = await fetchWikiResults(query, 5);
      console.error(`[MCP] search_wiki done: ${results.length} results in ${Date.now() - startTime}ms`);
      return { content: [{ type: "text", text: JSON.stringify(results) }] };
    } catch (err) {
      console.error(`[MCP] search_wiki failed: ${err.message}`);
      return {
        content: [{ type: "text", text: `查询失败: ${err.message}` }],
        isError: true,
      };
    }
  }
);

注意:MCP的stdio模式下,console.log会干扰协议通信,日志必须用console.error输出到stderr。

五、SSE模式部署:让团队共享一个Server

当你的MCP Server需要被多人使用时,stdio模式(每客户端一个进程)就不合适了。切换到SSE模式:

import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";

const app = express();

app.get("/sse", async (req, res) => {
  const transport = new SSEServerTransport("/messages", res);
  await server.connect(transport);
});

app.post("/messages", async (req, res) => {
  // SSE transport自动处理
});

app.listen(3001, () => {
  console.error("MCP Server running on http://localhost:3001/sse");
});

客户端配置改为SSE URL即可:"url": "http://your-server:3001/sse"

部署建议

  • 用Docker容器化部署,方便扩缩容
  • 加一层Nginx做反向代理和TLS终止
  • 如果SSE连接受防火墙限制,考虑用WebSocket中继
  • 加上健康检查端点:GET /health 返回200

六、常见坑与解决方案

问题原因解决
Server启动后客户端找不到工具配置文件路径错误或JSON格式问题检查JSON语法,用绝对路径
工具调用返回空结果工具描述模糊,AI传了错误参数细化zod的describe,加示例
stdio模式客户端卡死console.log污染了MCP通道所有日志改用console.error
SSE连接频繁断开Nginx/proxy超时配置过短设置proxy_read_timeout 300s
工具返回内容AI无法理解返回格式非结构化统一返回JSON,加字段说明

总结

MCP Server的开发并不复杂——核心就是"定义工具+处理输入+返回结构化结果"三步。但生产级部署需要补齐认证、错误处理、日志、超时控制等工程能力。从stdio本地调试起步,验证通过后再切SSE做团队共享,这是最稳妥的路径。

我的实践建议:先从一个最简单的只读工具开始(比如查询类),跑通后再加写入类工具。写操作一定要加确认机制——AI调用删除工具时没有"你确定吗"的弹窗,后果自负。更多AI自动化实战,可参考OpenClaw本地部署指南AI OCR免安装教程

版权声明

本文仅代表个人观点。
本文系AI辅助作者原创,未经许可,转载请保留原文链接。

发表评论