关于我
MCP 全解:从理解到深度开发从零开发一款 MCP Server

进阶:使用 Low Level Server 接口

McpServer 之外的底层 Server 类——直接面向 MCP 协议原语开发,厘清 Resources/Prompt/Tools/Sampling/Notification 等核心概念,并给出各类服务的完整实现模式。

McpServer 是经过高度封装的顶层接口,简单易用但无法实现更复杂的效果(例如订阅 resource 变更事件等),因此复杂项目中推荐使用更低级的基础接口:Server 类。

1. 简介

在前面的示例中,均采用高度封装的 McpServer 做服务开发,优点是接口体系相对简单易用,但该类型隐藏了底层诸多基础接口,只能实现 Resource/Tools/Prompt 等基本能力,无法实现 MCP 协议底层的 Logging、双向通讯、Sampling 等高阶能力。因此有必要向下探索更底层的 Server 接口,实现更复杂的 MCP 效果。

先看个简单例子:

// 导入所需的模块
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import {
  ListToolsRequestSchema,
  CallToolRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";

const printLog = (...args: any[]) => {
  console.error(...args);
};

const WeatherParamsSchema = z.object({
  city: z.string().describe('城市名称,如"北京"、"上海"等'),
});

// 1. 创建服务器实例
const server = new Server(
  {
    name: "SimpleToolServer",
    version: "1.0.0",
  },
  {
    capabilities: {
      tools: {}, // 支持工具调用功能
    },
    instructions: "这是一个MCP服务器示例",
  }
);

// 2. 注册请求处理器 - 工具列表处理器
server.setRequestHandler(ListToolsRequestSchema, async () => {
  printLog("收到工具列表请求");
  return {
    tools: [
      {
        id: "weather",
        name: "天气查询",
        description: "获取指定城市的天气信息",
        inputSchema: zodToJsonSchema(WeatherParamsSchema),
      },
    ],
  };
});

// 3. 注册请求处理器 - 工具调用处理器
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { toolId, arguments: rawArgs = {} } = request.params;
  // 实现处理逻辑
});

// 4. 设置各类服务器事件处理器
server.oninitialized = () => {
  printLog("服务器已完成初始化,准备处理请求");
};
server.onclose = () => {
  printLog("服务器连接已关闭");
};
server.onerror = (error: Error) => {
  printLog("服务器错误:", error);
};

// 5. 创建并连接 STDIO 传输层
async function startServer(): Promise<void> {
  const transport = new StdioServerTransport();
  await server.connect(transport);

  // 优雅关闭的处理
  process.on("SIGINT", async () => {
    await server.close();
    process.exit(0);
  });
}

startServer().catch((error) => printLog("启动失败:", error));

相比前面的示例,Server 的初始化逻辑相对复杂一些:创建服务器对象时需要配置 capabilities 属性(这是一个重要协议字段,用于告知 Client 端该 Server 支持哪些 MCP 能力);之后注册请求处理回调;必要时可注册各类 Server 事件回调(例如上例中的 oninitialized);最后创建 transport 对象,启动服务。

对比之下,McpServer 的初始化只需三步:创建 McpServer 对象、通过 tools/resources/prompts 等接口注册服务能力、创建 transport 对象启动服务。

换句话说,McpServer 是在 Server 基础上的高级封装形态,只需调用 tools 等接口注册能力即可;而使用 Server 类时,我们需要直接面向 MCP 协议底层的 tools/prompt/sampling 等基础概念,使用 setRequestHandler 为每一类可能的 MCP 通讯类型注册相应处理回调。

2. 核心概念

在展开 MCP 的高阶开发技巧之前,有必要先捋清楚几个关键概念:

  • Resources(资源):适用于需要向 LLM 呈现静态或动态数据的场景。例如你希望 LLM 基于一份产品目录进行问答,那么这份目录就可以作为 Resource 公开;又或者需要 LLM 根据最新新闻报道进行摘要,这些报道也可以通过 URI 的形式作为 Resource 提供。关键在于,Resources 提供的是"数据",是 LLM 理解和处理的基础。
  • Prompt(提示):更进一步,它不仅仅是数据,更是一种"指令模板"。当你需要反复使用类似的提问方式、只是每次具体内容略有不同时,Prompt 就非常有用。例如可以创建一个"产品推荐"的 Prompt 模板,包含产品类型、用户偏好等动态参数,每次只需填充参数,无需从头编写整个 Prompt。Prompt 还可以包含 Resources 的上下文。
  • Tools(工具):赋予 LLM "行动"的能力。如果说 Resources 和 Prompt 是"输入",那么 Tools 就是"输出"。当你希望 LLM 不仅仅生成文本,而是执行某些操作时,就需要用到 Tools,例如"发送邮件""查询数据库"。Tools 的设计强调 AI 模型控制,但通常需要人类监督和批准,以确保安全性和可控性。
  • Sampling(采样):一种特殊的"反向"能力。通常是客户端请求 LLM 完成任务,但某些场景下,服务器可能需要借助 LLM 的能力来完成某些复杂的智能代理行为。例如服务器根据用户输入动态生成多个候选方案,再让 LLM 评估排序。Sampling 允许服务器"借用"客户端的 LLM 能力,同时通过人机协作确保用户对输入输出的控制。
  • Notification(通知):用于实现双向通信中的状态同步。复杂交互中可能需要告知对方当前的状态、进度或发生的事件,例如耗时任务的进度事件、任务取消事件。
  • Roots(根):定义服务器的工作范围。通过 Roots,服务器可以明确知道哪些资源是它需要关注的,以及这些资源的位置,这对组织资源访问、提高效率和安全性都很重要。
  • Transport(传输):所有原语的底层支撑,负责处理客户端和服务器之间的通信,包括连接管理、消息收发和错误处理。MCP 内置了 stdio 和 SSE 两种实现,也支持自定义 Transport。

这些能力共同协作,完成 MCP 协议定义的完整工作流程:

🖼️ 配图待补:VPxlb70qKo0SRsxd2MrcE0nlnEe

深入理解这些原语并根据实际场景灵活运用,是构建强大、灵活的 MCP 应用的关键。这不仅是学习 API 的使用,更是理解 LLM 交互模式、设计高效协作流程的过程。

3. 基本开发模式

使用 Server 类型开发 MCP 服务,至少包含如下步骤。

第一步,创建 Server 实例对象:

import { Server } from "@modelcontextprotocol/sdk/server";

const server = new Server({
  name: "MyServer",
  version: "1.0.0"
}, {
  capabilities: {
    sampling: {},   // 支持 LLM 采样功能
    resources: {},  // 支持资源管理
    logging: {},    // 支持日志功能
    prompts: {},    // 支持提示管理
    tools: {},      // 支持工具调用
    roots: {},      // 支持根目录列表
    experimental: { // 实验性功能
      myCustomFeature: true,
    }
  },
  instructions: "服务器使用说明..."
});

其中 capabilities 是一个非常核心的概念,用于声明服务器或客户端支持的功能特性。初始化阶段,客户端和服务器会交换各自的 capabilities,协商确定双方都支持的功能集合,确保通信兼容性。instructions 则用于说明该 MCP 的作用,以便决策何时调用该服务。

第二步,注册各类事件回调:

import {
  ListToolsRequestSchema,
  CallToolRequestSchema
} from "@modelcontextprotocol/sdk/types";

server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        id: "weather",
        name: "天气查询",
        description: "获取指定城市的天气信息",
        inputSchema: zodToJsonSchema(WeatherParamsSchema),
      },
    ],
  };
});

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { toolId, arguments: rawArgs = {} } = request.params;
  // 实现处理逻辑
});

这是一个比较经典的案例,为了实现 tool 能力,需要同时注册两类事件处理器:ListToolsRequestSchema 用于获取可用工具列表,CallToolRequestSchema 用于处理工具请求回调。Prompt/Resource 也遵循类似规则,需要同时实现对应的 List 与 Call 函数。

第三步,创建 Transport 对象并关联到 Server:

const transport = new StdioServerTransport();
await server.connect(transport);

McpServer 类似,这里同样可使用 STDIO 与 SSE 等通讯协议,前面已有详细介绍,此处不再赘述。真正的难点在于消息事件回调,接下来分场景展开说明。

4. 开发 Tool 服务

关键接口:

  • ListToolsRequestSchema:用于声明该 MCP 具备哪些 Tools 工具能力;
  • CallToolRequestSchema:用于处理具体的工具请求;
  • server.sendToolListChanged:发送工具列表变更通知。

Tools 是 MCP 服务器向客户端暴露的可调用函数,允许 LLM 客户端在对话中触发实际操作,例如查询数据库、调用外部 API、读写文件或触发工作流。借助 MCP Tools,LLM 不再只是被动响应的"回答机器",而是能够通过标准协议执行动作、产生副作用,从而完成更复杂的任务。

实现 Tool 需要注册两类事件回调。ListToolsRequestSchema 用于声明该 MCP 具备哪些 Tools,回调中返回的 tools 声明包含 name(工具名称)、description(工具描述,LLM 会通过分析它确定在什么场景下调用哪个工具)、inputSchema(声明这个 tool 接收的参数结构,要求传入 zod 类型):

server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "天气查询",
        description: "获取指定城市的天气信息",
        inputSchema: zodToJsonSchema(z.object({
          city: z.string().describe('城市名称,如"北京"、"上海"等'),
        })),
      },
    ],
  };
});

CallToolRequestSchema 用于处理具体的工具请求。注意,MCP Server 没有路由概念,所有对具体 Tool 的调用最终都会走到 CallToolRequestSchema 请求,因此需要在回调中通过 toolId 参数判断当前正在调用的工具:

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

  switch (toolId) {
    case 'weather': {
      // ...
      return {
        result: `${city}的天气: ${weather}`,
      };
    }
    default:
      return {
        result: `未找到ID为"${toolId}"的工具`,
      };
  }
});

与 Prompt 类似,MCP Client 调用时首先调用 ListToolsRequestSchema 获取所有可用 Tools,之后根据场景需求调用 CallToolRequestSchema 触发工具逻辑。重点在于工具的 Description 描述——越准确,调用精确度越高。此外,还可以通过 Server 类的 sendToolListChanged 方法发送工具列表变更事件。

5. 开发 Resource 服务

关键接口:

  • ListResourcesRequestSchema:声明该 MCP 支持输出的完整资源列表;
  • ReadResourceRequestSchema:处理具体的资源请求;
  • server.sendResourceListChanged:发送资源列表变更通知;
  • server.sendResourceUpdated:发送资源内容变更通知。

需要读取某类资源、且明确没有任何副作用时,可优先使用 Resource 类型,例如读入文件、读入数据库字段等。与 Tools 不同,读操作被 MCP 默认为安全操作,不需要用户审批即可执行。

实现 Resource 服务主要用到两类事件。ListResourcesRequestSchema 用于声明该 MCP 支持输出的完整资源列表:

server.setRequestHandler(ListResourcesRequestSchema, async (request) => {
  return {
    resources: [
      {
        name: 'foo',
        type: 'file',
        uri: 'mcp://root-docs/README.md',
        description: 'readme',
      },
    ],
  };
});

ReadResourceRequestSchema 用于处理具体的资源请求,回调中可通过 request.params.uri 获取请求的资源链接:

server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
  const { uri } = request.params;
  if (uri !== 'mcp://root-docs/README.md') {
    throw new Error(`资源 ${uri} 不存在`);
  }
  return {
    contents: [
      {
        type: 'text',
        uri: 'mcp://root-docs/README.md',
        text: '# 示例文档\n\n这是一个示例Markdown文档,用于演示MCP资源管理功能。',
        contentType: 'text/markdown',
      },
    ],
  };
});

此外,还可以通过 server.sendResourceListChangedserver.sendResourceUpdated 接口发送资源列表/内容变更通知。这一特性特别适用于动态内容场景,例如 Browser MCP 中页面接口发生变动后,即可通过这两类事件通知 MCP Client 做出响应。

6. 开发 Prompt 服务

关键接口:

  • ListPromptsRequestSchema:获取所有可用的提示模板列表;
  • GetPromptRequestSchema:获取特定提示模板的详细信息,包括模板内容和所需参数;
  • server.sendPromptListChanged:发送 Prompt 列表变更事件。

Prompt 是当代与 LLM 交互的重要技巧。过去我们需要 case by case 地思考,或到处复制各种被验证有效的 Prompt;现在则可以将一些可复用的 Prompt 封装到 MCP 中,让 LLM 根据上下文信息生成相关 Prompt,进而正确执行任务。

实现 Prompt 需要注册两类事件回调。ListPromptsRequestSchema 用于获取所有可用的提示模板列表(参数 filter 可选,用于过滤;返回 prompts 数组,需包含 id、name、description 字段):

server.setRequestHandler(ListPromptsRequestSchema, async (request) => {
  const { filter } = request.params || {};
  let prompts = [...promptTemplates];

  if (filter && typeof filter === 'string') {
    const lowerFilter = filter.toLowerCase();
    prompts = prompts.filter(prompt =>
      prompt.name.toLowerCase().includes(lowerFilter) ||
      prompt.description.toLowerCase().includes(lowerFilter)
    );
  }

  return {
    prompts: prompts.map(({ id, name, description }) => ({
      id, name, description: description || ''
    })),
  };
});

GetPromptRequestSchema 用于获取特定提示模板的详细信息(参数 name 必填;返回 prompt 对象):

server.setRequestHandler(GetPromptRequestSchema, async (request) => {
  const { promptId } = request.params;
  if (!promptId) {
    throw new Error('缺少提示ID');
  }
  const prompt = promptTemplates.find(p => p.id === promptId);
  if (!prompt) {
    throw new Error(`提示 ${promptId} 不存在`);
  }
  return { prompt };
});

除此之外,还需要定义一系列 Prompt 模板:

const promptTemplates: Prompt[] = [
  {
    name: '电子邮件模板',
    description: '生成一封专业的电子邮件',
    template: `请为我写一封发给{{recipient}}的电子邮件...`,
    templateParameters: {
      recipient: {
        description: '收件人名称',
        required: true,
      },
      // 其他参数...
    },
  },
];

其中 description 非常重要,LLM 需要根据这个字段判断使用哪个 Prompt Template;template 使用 {{参数名}} 标记参数位置;templateParameters 定义每个参数的描述、是否必填、默认值等。

串联起来,MCP Client 消费时会首先调用 ListPromptsRequestSchema 获取所有可用 Prompt,之后根据 description 判断当前任务适合哪个模板;找到后进一步调用 GetPromptRequestSchema 并传入参数,拼接出最终 Prompt 内容。MCP Client(如 Cursor、Cline 等)拿到 Prompt 后,会进一步调用 LLM 执行这段 Prompt。这种方式特别适用于管理可复用的 Prompt 模板,例如针对某些库的使用说明,或 Plan => Design => Implement 这类复合流程。

7. 使用 Sampling 调用 Client LLM

这是一个比较特别的特性,Sampling 允许 MCP Server 通过 MCP Client(例如 Cursor)请求 LLM 完成任务。执行流程:

  1. MCP Server 端发送 sampling/createMessage 请求,内容中包含 Prompt 字符串:

    {
      "messages": [
        {
          "role": "user",
          "content": {
            "type": "text",
            "text": "Please summarize this log file."
          }
        }
      ],
      "systemPrompt": "You are a helpful developer assistant.",
      "includeContext": "thisServer",
      "maxTokens": 300
    }
  2. MCP Host 端检测 Prompt,并调用 LLM 完成请求:

    {
      "model": "claude-3-sonnet",
      "role": "assistant",
      "content": {
        "type": "text",
        "text": "The log file contains several timeout errors and warnings related to database connections."
      }
    }
  3. 将经过改造的 Prompt 返回给 MCP Server。

在此基础上,你不需要在 MCP 中调用各种 LLM 服务,只需通过 Sampling 消息获取 LLM 完善内容即可。

不过这个特性目前还处于比较早期的阶段,官方文档并没有过多解释,相关 SDK 实现中也没有给出正确示例,学习难度有点高。Sampling 本质上是一种 Server 调用 LLM 完善内容的过程,这意味着 MCP Client 并不会主动调用任何 Sampling 服务,而是需要将 Sampling 隐藏在 Prompt/Resource/Tools 请求中。构造一个例子:

第一步,先开发一个 Prompt 服务,核心代码:

server.setRequestHandler(ListPromptsRequestSchema, async (request) => {
  let prompts = [...promptTemplates];
  return {
    prompts: prompts.map(({ id, name, description }) => ({
      id, name, description: description || '',
    })),
  };
});

server.setRequestHandler(GetPromptRequestSchema, async (request) => {
  const { name } = request.params;
  const prompt = promptTemplates.find((p) => p.name === name);
  return { prompt, message };
});

第二步,在某类请求中调用 Sampling 能力,改造上述 GetPromptRequestSchema 回调:

server.setRequestHandler(GetPromptRequestSchema, async (request) => {
  const { name } = request.params;
  const prompt = promptTemplates.find((p) => p.name === name);
  const message = await server.createMessage({
    prompt: prompt?.template as string,
    promptParameters: {
      recipient: '范文杰',
      topic: '关于MCP的介绍',
    },
    messages: [],
    maxTokens: 100,
  });
  return { prompt, message };
});

在回调中,需要调用 server.createMessage 函数并传入 Prompt 及其他核心参数,这个 Message 最终会反向传输到 MCP Client,由 Client 根据参数内容调用 LLM 获得最终结果,之后将结果返回给 MCP Server 完成整体调用链路。

Sampling 能力在某些情况下会特别有用,例如 Figma MCP 中,可以将取到的 Selection Node 或设计图通过 Sampling 传入 LLM 进行详细分析,拆解出 UI 分层、布局、配色等要素,再整理成 Prompt 返回给 MCP Client,从而提升 D2C 的生成质量。不过这个能力目前还不是很成熟,使用率并不高,后续大家可以关注一下。

8. 总结

最后再总结下各项原语的作用。MCP Server 可对外暴露 Resources/Prompt/Tools 类型的能力,务必注意在具体场景中选择正确的服务类型:

类型语义副作用行为
Resources用于读取资源(读文件、读数据库)直接返回数据
Prompt语义上代表读取一种特殊资源:PromptMCP Client 获取结果后会根据 Prompt 内容进一步执行操作
Tools执行某些可能产生副作用的操作(写文件、写数据库)MCP 协议规定执行 Tool 前需要向用户发出申请,审核通过后才能执行

在 MCP TS SDK 中,这些能力都通过注册一系列事件回调实现。此外,Server 端还可以通过 Logging 向对方发送日志,通过 Sampling 主动调用 Client 的 LLM 能力,完成更复杂的任务。

On this page