1. 项目概述:一个连接AI与工具的“翻译官”

最近在折腾AI应用开发,特别是想让大语言模型(LLM)能更“接地气”地操作我们日常用的各种软件和API时,遇到了一个挺有意思的项目: sthan-io/mcp-server 。这名字听起来有点技术范儿,简单来说,它是一个实现了 Model Context Protocol (MCP) 协议的服务器。你可以把它理解为一个“翻译官”或者“适配器”,专门负责让像 Claude、ChatGPT 这类不懂“人间烟火”的AI大脑,能够理解并安全地调用我们人类世界里的工具,比如读取数据库、查询天气、操作文件系统,甚至是控制智能家居。

MCP 这个概念,最早是由 Anthropic 公司提出并推动的,目的是为 AI 助手与外部工具、数据源之间建立一个标准化、安全可靠的通信桥梁。 sthan-io/mcp-server 这个开源项目,就是一个具体的 MCP 服务器实现。它不是一个最终用户直接使用的软件,而更像是一个“脚手架”或“参考实现”,为开发者提供了一个清晰的模板,告诉他们如何按照 MCP 的规范,把自己手头的工具(我们称之为“资源”)包装起来,暴露给 AI 使用。

对于开发者、AI应用构建者,或者任何想让自己的工作流更智能化的技术爱好者来说,理解并上手 MCP 服务器开发,意味着你能够为 AI 助手“赋能”,让它从只能聊天的“顾问”,升级为能帮你实际干活的“助理”。这个项目就是一个绝佳的起点。

2. MCP协议核心思想与项目定位拆解

在深入代码之前,我们得先搞明白 MCP 到底想解决什么问题,以及 sthan-io/mcp-server 在这个生态里的角色。

2.1 为什么需要MCP?从“插件混乱”到“标准接口”

早期让AI使用工具,基本是各显神通。每个AI应用(比如某个定制化的ChatGPT)想要连接一个新工具,都需要写一段特定的、硬编码的集成代码。这带来了几个明显问题:

  1. 开发效率低 :每对接一个工具就要重写一遍通信、认证、错误处理的逻辑。
  2. 安全性难以保障 :工具权限控制分散,AI可能被诱导执行危险操作。
  3. 体验割裂 :用户在不同AI助手间切换,工具能力无法继承。
  4. 生态封闭 :开发者写的工具适配器很难被其他AI项目复用。

MCP 的提出,就是为了定义一套统一的“语言”。在这套协议下:

  • 工具提供方(即 MCP 服务器) :只需要按照固定格式声明自己有哪些“工具”(Tools)和“资源”(Resources),以及如何调用它们。
  • 工具使用方(即 MCP 客户端,通常是AI助手) :只需要学会 MCP 这一种“语言”,就能自动发现、理解并请求调用所有兼容 MCP 的服务器提供的工具,无需为每个工具单独开发适配器。

sthan-io/mcp-server 就是一个工具提供方的标准范例。它告诉你,一个合格的 MCP 服务器应该长什么样,如何组织代码,如何响应客户端的标准请求。

2.2 项目架构与核心组件

浏览该项目的代码仓库,我们可以清晰地看到其作为参考实现的模块化结构,这为我们自建服务器提供了蓝图:

  1. 协议层实现 :这是核心。项目包含了处理 MCP 标准消息(如 initialize , tools/list , tools/call , resources/list , resources/read 等)的完整逻辑。它使用 JSON-RPC over STDIO/SSE 作为传输层,这是 MCP 的典型通信方式,保证了进程间通信的通用性和简单性。
  2. 工具(Tools)封装示例 :项目会演示如何将一段具体的功能(比如“获取当前时间”、“执行一个计算”)包装成一个 MCP “工具”。一个工具需要明确定义输入参数(名称、类型、描述)和输出格式,这样 AI 客户端才能理解如何调用它。
  3. 资源(Resources)定义示例 :除了主动调用的工具,MCP 还有“资源”的概念,可以理解为被动访问的数据源,比如一个只读的配置文件、一个数据库视图。项目会展示如何将一块数据声明为资源,并实现读取方法。
  4. 配置与生命周期管理 :如何读取配置文件来动态加载不同的工具集?服务器启动、关闭时如何进行资源初始化和清理?这些工程化细节在项目中都有体现。
  5. 错误处理与日志 :健壮的服务器必须能妥善处理无效请求、工具执行失败等异常,并给出结构化的错误信息反馈给客户端,同时记录日志便于调试。

注意 :作为参考实现, sthan-io/mcp-server 可能不会实现非常复杂的业务工具,它的主要价值在于展示 如何正确地遵循协议 。你需要做的是借鉴其架构,填充进你自己的业务逻辑。

3. 从零开始构建一个自定义MCP服务器

理解了项目的定位,我们动手实践一下。假设我们要构建一个“项目管理MCP服务器”,让AI能查询任务状态、创建新任务。

3.1 环境准备与项目初始化

首先,你需要一个合适的开发环境。由于 MCP 服务器可以用多种语言实现(官方推荐 TypeScript/Python,社区也有 Go、Rust 等版本), sthan-io/mcp-server 很可能是基于 TypeScript 的。我们以此为例。

# 1. 创建项目目录
mkdir my-project-mcp-server
cd my-project-mcp-server

# 2. 初始化Node.js项目
npm init -y

# 3. 安装TypeScript和类型定义
npm install typescript ts-node @types/node --save-dev

# 4. 安装MCP核心SDK(如果存在)或必要的通信库
# 例如,Anthropic 官方提供了 @modelcontextprotocol/sdk
npm install @modelcontextprotocol/sdk

# 5. 初始化tsconfig.json
npx tsc --init

修改 tsconfig.json ,确保 target ES2020 或更高, module commonjs NodeNext ,并设置 outDir ./dist

3.2 定义工具(Tools)清单

src/tools 目录下,我们创建第一个工具: getTaskStatus

// src/tools/getTaskStatus.ts
import { Tool } from '@modelcontextprotocol/sdk'; // 假设SDK中有此类型

export const getTaskStatusTool: Tool = {
  name: 'get_task_status',
  description: '根据任务ID获取当前任务的状态、负责人和截止日期。',
  inputSchema: {
    type: 'object',
    properties: {
      taskId: {
        type: 'string',
        description: '项目的唯一任务标识符',
      },
    },
    required: ['taskId'],
  },
};

// 工具的执行函数
export async function executeGetTaskStatus(args: { taskId: string }): Promise<any> {
  const { taskId } = args;
  // 这里应该是你的业务逻辑,例如查询数据库
  // 模拟返回数据
  const mockTask = {
    id: taskId,
    title: '实现用户登录模块',
    status: '进行中',
    assignee: '张三',
    dueDate: '2023-10-27',
  };
  
  // 确保返回结构清晰,AI易于理解
  return {
    content: [
      {
        type: 'text',
        text: `任务 #${taskId} 的详情如下:\n` +
              `标题:${mockTask.title}\n` +
              `状态:${mockTask.status}\n` +
              `负责人:${mockTask.assignee}\n` +
              `截止日期:${mockTask.dueDate}`
      }
    ]
  };
}

关键点解析

  • name :工具的唯一标识,调用时使用。建议用下划线命名。
  • description :至关重要!AI 客户端(如 Claude)依靠这个描述来理解工具用途,并决定在何时调用它。描述应清晰、准确。
  • inputSchema :使用 JSON Schema 定义输入参数。这相当于给 AI 一份“填写说明书”,告诉它需要提供什么信息,以及信息的类型。
  • 执行函数:这里是真正的业务逻辑。返回格式应遵循 MCP 对于工具调用结果的约定,通常包含一个 content 数组,里面可以是文本( text )、图片( image )或其他类型。

3.3 实现服务器主循环与协议处理

src/server.ts 中,我们需要建立服务器的主框架,处理来自客户端的连接和请求。

// src/server.ts
import { Server } from '@modelcontextprotocol/sdk/server.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/stdio.js';
import { getTaskStatusTool, executeGetTaskStatus } from './tools/getTaskStatus.js';
// 导入其他工具...

async function main() {
  // 1. 创建Server实例
  const server = new Server(
    {
      name: 'my-project-mcp-server',
      version: '0.1.0',
    },
    {
      capabilities: {
        tools: {}, // 声明我们支持工具功能
        resources: {}, // 声明我们支持资源功能(可选)
      },
    }
  );

  // 2. 注册工具列表
  server.setRequestHandler('tools/list', async () => {
    return {
      tools: [getTaskStatusTool], // 返回所有已定义的工具描述
    };
  });

  // 3. 注册工具调用处理器
  server.setRequestHandler('tools/call', async (request) => {
    const { name, arguments: args } = request.params;
    
    switch (name) {
      case 'get_task_status':
        return await executeGetTaskStatus(args as { taskId: string });
      // 处理其他工具调用...
      default:
        throw new Error(`未知的工具: ${name}`);
    }
  });

  // 4. 设置传输层(这里使用标准输入输出,是最常见的方式)
  const transport = new StdioServerTransport();
  await server.connect(transport);
  
  console.error('MCP服务器已启动,等待客户端连接...');
}

main().catch((error) => {
  console.error('服务器运行失败:', error);
  process.exit(1);
});

为什么使用 Stdio(标准输入输出)? 这是 MCP 推荐的方式,因为它简单、通用,不依赖网络端口。AI 客户端(如 Claude Desktop)会以子进程的方式启动你的 MCP 服务器,并通过管道进行通信。这避免了网络权限、防火墙等复杂问题,安全性也更高——服务器只对启动它的客户端进程可见。

3.4 配置与客户端集成

服务器写好了,如何让 AI 客户端(比如 Claude Desktop)知道并使用它呢?这需要通过客户端的配置文件来实现。

以 Claude Desktop 为例,你需要在其配置目录(如 ~/Library/Application Support/Claude/claude_desktop_config.json 在 macOS 上)添加你的服务器配置:

{
  "mcpServers": {
    "my-project-server": {
      "command": "node",
      "args": ["/绝对路径/to/your/server/dist/server.js"],
      "env": {
        "YOUR_API_KEY": "your_secret_key_here"
      }
    }
  }
}

配置要点

  • command :启动你服务器的命令,这里是 node
  • args :传递给命令的参数,第一个是你的编译后的 JS 文件路径。
  • env :可以设置环境变量,用于传递敏感信息(如 API 密钥), 切忌 将密钥硬编码在代码中。

保存配置并重启 Claude Desktop,它就会在启动时加载你的服务器。之后,你在和 Claude 对话时,它就能自动“知道”可以使用 get_task_status 这个工具了。

4. 高级主题与最佳实践

构建一个能用的服务器只是第一步,要让它健壮、安全、易维护,还需要考虑更多。

4.1 错误处理与用户反馈

工具执行可能会失败(网络错误、无效输入、权限不足)。必须向客户端返回结构化的错误信息,而不是直接崩溃或输出晦涩的日志。

export async function executeGetTaskStatus(args: { taskId: string }): Promise<any> {
  try {
    const { taskId } = args;
    if (!taskId.match(/^TASK-\d+$/)) {
      // 返回一个对AI和用户都友好的错误
      return {
        content: [{
          type: 'text',
          text: `错误:任务ID格式无效。应为 'TASK-数字' 格式。`
        }],
        isError: true // MCP响应中可能包含错误标识
      };
    }
    // ... 正常业务逻辑
  } catch (error: any) {
    console.error(`查询任务失败:`, error); // 服务器端记录详细日志
    return {
      content: [{
        type: 'text',
        text: `抱歉,查询任务状态时遇到系统错误:${error.message}`
      }],
      isError: true
    };
  }
}

心得 :给AI的错误信息应该是 自然语言 ,可以被AI直接复述给用户。同时,在服务器日志中记录详细的调试信息(如堆栈跟踪),方便开发者排查。

4.2 资源(Resources)的实现

工具是“主动操作”,资源是“被动数据”。例如,你可以将一个项目配置文件 /project/roadmap.md 声明为资源。

// 在server.ts中注册资源
server.setRequestHandler('resources/list', async () => {
  return {
    resources: [
      {
        uri: 'file:///project/roadmap.md',
        name: '项目路线图文档',
        description: '本项目的主要功能规划和里程碑',
        mimeType: 'text/markdown',
      },
    ],
  };
});

server.setRequestHandler('resources/read', async (request) => {
  const { uri } = request.params;
  if (uri === 'file:///project/roadmap.md') {
    const content = await fs.promises.readFile('./roadmap.md', 'utf-8');
    return {
      contents: [{
        uri,
        mimeType: 'text/markdown',
        text: content,
      }],
    };
  }
  throw new Error(`资源未找到: ${uri}`);
});

这样,AI 客户端就可以在需要时读取 roadmap.md 的内容作为上下文,而无需通过工具调用来获取。

4.3 安全性考量

这是 MCP 服务器设计的重中之重。

  1. 最小权限原则 :服务器进程本身应仅拥有执行其功能所需的最小系统权限。不要用 root 或管理员权限运行。
  2. 输入验证与净化 :对所有来自客户端的输入(如 taskId )进行严格验证,防止注入攻击。
  3. 敏感信息管理 :API 密钥、数据库密码等 永远不要 硬编码或通过命令行参数传递。使用环境变量或安全的配置管理服务。
  4. 工具作用域限制 :仔细设计每个工具的能力。一个“读取日志”的工具不应该拥有“删除文件”的权限。在工具执行函数内部进行二次权限检查。
  5. 审计日志 :记录所有工具调用请求和关键操作,包括调用者(客户端)、参数、时间戳和结果状态,便于事后审计和问题追踪。

5. 调试、测试与常见问题排查

开发过程中,你肯定会遇到各种问题。这里分享一套实用的调试流程。

5.1 独立测试你的服务器

在集成到 AI 客户端之前,最好先独立测试服务器的协议响应。你可以写一个简单的测试脚本模拟客户端。

// test-client.ts
import { spawn } from 'child_process';

const serverProcess = spawn('node', ['dist/server.js']);

serverProcess.stdin.write(JSON.stringify({
  jsonrpc: '2.0',
  id: 1,
  method: 'tools/list',
  params: {}
}) + '\n');

serverProcess.stdout.on('data', (data) => {
  console.log('服务器响应:', data.toString());
});

serverProcess.stderr.on('data', (data) => {
  console.error('服务器错误:', data.toString());
});

也可以使用像 @modelcontextprotocol/sdk 包中可能提供的测试工具或客户端进行更规范的测试。

5.2 与Claude Desktop集成时的常见问题

问题现象 可能原因 排查步骤
Claude 完全“看不到”新工具 1. 配置文件路径错误。
2. 配置文件格式错误(JSON语法)。
3. 服务器启动失败。
1. 检查 Claude Desktop 的配置目录是否正确。
2. 使用 jsonlint 验证配置文件。
3. 在终端手动运行配置中的 command args ,看服务器能否正常启动并打印日志。
Claude 能看到工具但调用失败 1. 工具执行函数内部报错。
2. 返回格式不符合 MCP 规范。
3. 权限问题(如文件无法读取)。
1. 查看服务器日志 。这是最重要的!确保服务器进程的标准错误输出被捕获(例如重定向到文件)。
2. 对照 MCP 协议文档,检查工具调用返回的 JSON 结构。
3. 在工具函数内添加详细的 try-catch 和日志。
工具调用缓慢或超时 1. 工具执行的业务逻辑本身很慢(如网络请求)。
2. 服务器进程阻塞。
1. 在工具实现中优化性能,考虑异步操作。
2. 检查是否有同步的耗时操作(如大型文件同步读取)。
3. AI客户端可能有调用超时设置,需确保工具在超时前返回。
重启 Claude 后配置不生效 客户端缓存了旧的服务器信息。 完全退出 Claude Desktop 进程(不仅仅是关闭窗口),再重新启动。有时还需要清除客户端缓存,具体位置参考客户端文档。

最重要的调试心得 始终监控你的服务器进程日志 。因为通信是通过 Stdio 静默进行的,只有日志能告诉你服务器内部发生了什么。在开发时,可以将日志详细输出到控制台或一个文件。

5.3 性能与可扩展性思考

当你的工具越来越多,或者单个工具负载很重时,需要考虑:

  • 无状态设计 :尽量将 MCP 服务器设计为无状态的。每次工具调用都是独立的,这便于未来水平扩展。
  • 连接池与缓存 :对于数据库查询、第三方 API 调用等,使用连接池和适当的缓存策略(注意缓存失效)可以极大提升性能。
  • 异步处理 :对于耗时较长的操作,可以考虑实现异步工具调用模式(如果 MCP 协议支持),即立即返回一个“任务已接收”的响应,再通过其他方式(如资源更新)通知结果。

构建 sthan-io/mcp-server 这样的项目,不仅仅是实现一个协议,更是开启了一种新的 AI 交互范式。它把 AI 从封闭的聊天框里解放出来,通过标准化接口连接到无限的工具生态。从这个小项目出发,你可以将公司内部的 CRM、CMS,或者你个人常用的脚本、硬件设备都封装成 MCP 工具,打造一个真正懂你、能帮你处理实际事务的智能工作伙伴。过程中最关键的,除了技术实现,就是对工具边界的谨慎定义和安全性的持续关注。

Logo

码道开发者社区,聚焦华为云码道 CodeArts 代码智能体,沉淀 Agent、Skill、鸿蒙开发实战内容,供开发者查阅资料、交流技术、分享工程实践

更多推荐