掘金 人工智能 04月29日 10:52
从零搭建MCP服务:基于Stdio的实践指南
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文从工程师角度出发,深入探讨了如何构建MCP(Model Context Protocol)服务,并以Stdio作为实现方式。文章首先介绍了MCP的基本架构,包括Host、MCP Protocol、MCP Server和Local等组件。随后,作者详细讲解了MCP Server的搭建过程,包括Tools和Resources的定义与使用,并提供了基于TypeScript-SDK的示例代码,演示了如何读取package.json文件中的信息。最后,文章通过DeepChat验证了服务的可用性,并展望了未来SSE的实现。

💡 **MCP架构解析**: 文章首先介绍了MCP的核心组成部分,包括Host(如Claude Desktop)、MCP Protocol(通信协议)、MCP Server(核心服务)和Local(数据资源),为读者构建MCP服务打下基础。

🛠️ **Tools的定义与实现**: 详细阐述了如何在MCP Server中定义Tools,包括工具的名称、描述和输入/输出Schema。文章通过示例展示了如何创建工具来获取package.json中的name和version信息。

📚 **Resources的设计与应用**: 介绍了Resources的概念,它作为LLM的上下文信息来源。文章讲解了如何声明资源以及定义读取资源的逻辑,并提供了一个读取package.json中devDependencies信息的例子。

💻 **Stdio Server的搭建与验证**: 演示了如何使用StdioServerTransport实现消息的输入和输出。文章还介绍了如何使用DeepChat验证MCP服务的可用性,并展示了工具被调用的效果。

最近 MCP 的概念很火,还有 A2A。但是发现很多文章都停留在了概念层面,没有深入去说如何构建一个 MCP 的服务,以及我觉得很多文章对 MCP 的理解其实也是有问题的,所以这篇文章算是站在工程师的角度,如何去构建一个 MCP 的服务。

会大概写三个系列,当前为第一系列 主要介绍 Stdio 的搭建。

在正式开始代码之前,先看一下官方给的架构图:

包含了以下部分:

    HostMCP ProtocolMCP ServerLocal ...

在很多文章中还会提及到 MCP Client,其实可以简单理解成就是主应用(Host)中的一部分,在一个问题中可能需要调用多个 MCP Server,所以需要多个 MCP Client。它负责发起 MCP 请求和接收 MCP 的响应。

MCP Server 相关概念

在编写一个正式的 MCP Server 之前,我们需要了解一些概念:

上面是官网文档中列举的相关概念,不过重点关注 Resources 和 Tools。

Tools 这个比较好理解就是工具,定义一系列工具等待被调用。而 Resources 就是资源,可以简单理解成就是给 LLM 的上下文填充使用的,让它更好的理解问题。

MCP Server 搭建

目前官网已经支持了 typescript-sdk 这里直接使用即可。

mkdir mcp-servercd mcp-serverpnpm initpnpm add @modelcontextprotocol/sdk zod zod-to-json-schema

编写的示例就是读取当前的 package.json 内容,然后返回给 LLM,可以让用户提问当前 package.json 的 name 和 version 是多少的时候正确回答。

先创建一个 server 实例

import { Server } from "@modelcontextprotocol/sdk/server/index.js";import packageData from "../package.json";const server = new Server(  {    name: "get-package",    version: packageData.version,  },  {    capabilities: {      tools: {},      resources: {},    },  });

上面的 name 应当保持唯一,下面定义两个工具用于返回 name 和 version。

Tools

const emptyInputSchema = z.object({});const emptyInputJsonSchema = zodToJsonSchema(emptyInputSchema);server.setRequestHandler(ListToolsRequestSchema, async () => {  return {    tools: [      {        name: "get-name",        description: "获取package.json中的name",        inputSchema: emptyInputJsonSchema,      },      {        name: "get-version",        description: "获取package.json中的version",        inputSchema: emptyInputJsonSchema,      },    ],  };});

这里因为我们并不需要 inputSchema 输入,所以直接使用了 zodToJsonSchema 来生成一个空的 schema。但是如果你的工具依赖相关的字段,比如你有一个 add 的工具,计算两个数的相加,可能你就要定义一下 args 的 schema 了。

定义完成之后只是在 server 上声明了工具,但是当 MCP Client 发起请求调用工具的时候,我们还要写相关的逻辑来处理相对应的工具请求。

server.setRequestHandler(CallToolRequestSchema, async (request) => {  const { name } = request.params;  switch (name) {    case "get-name": {      return {        content: [          {            type: "text",            text: packageData.name,          },        ],      };    }    case "get-version": {      return {        content: [          {            type: "text",            text: packageData.version,          },        ],      };    }    default:      throw new Error(`Unknown tool: ${name}`);  }});

CallToolRequestSchema 表示调用工具的请求,这里我们根据 name 来返回不同的内容。这样对于工具的编写就完成了,但是最初的时候提到除了工具,还有 Resources 资源,我们还没有定义。

Resources

Resources 用于让 MCP client 读取,然后作为 LLM 上下文的内容。因为 LLM 的训练是有一个截止时间的,对于很多不公开或者最新的信息是没有的,现在比较火的 RAG 其实也是为了让 LLM 可以理解信息。

资源的定义和 Tools 类似,需要先声明资源,然后再定义资源的读取逻辑。

不过这里需要注意,因为不同的主应用(Host)实现不同,所以导致资源的读取逻辑并不相通,截取官方的一段话介绍。

server.setRequestHandler(ListResourcesRequestSchema, async () => {  return {    resources: [      {        name: "package devDependencies依赖",        description: "返回 package.json 中的 devDependencies 依赖",        // "返回整个 package.json 文件的内容,包括 name, version, dependencies, devDependencies, scripts 等。",        uri: "package://devDependencies",      },    ],  };});server.setRequestHandler(ReadResourceRequestSchema, async (request) => {  const { uri } = request.params;  switch (uri) {    case "package://devDependencies":      return {        contents: [          {            uri,            text: JSON.stringify(packageData, null, 2),          },        ],      };    default:      throw new Error(`Unknown resource: ${uri}`);  }});

最后我们使用 Stdio 来完成消息标准输入和输出。

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

验证服务

客户端的话可以使用 DeepChat,安装好之后点击设置,找到 MCP 设置,新建一个自定义的 MCP 服务。

然后填写下面的内容用于配置解析

mcpServers: {"read-package": {"descriptions": "读取package.json文件相关内容并返回","icons": "📁","autoApprove": ["read"],"type": "stdio","command": "node","args": ["/Users/yliu/Desktop/my/mcp-ts/dist/src/index.js"],"env": {},"baseUrl": ""}    }

提交之后启动新建的 MCP 服务。

之后新建对话框输入对话就可以看到工具被调用的效果了。

因为目前 DeepChat 并不支持 Resources 的读取,导致我们上面定义的 package 的 info 没有被返回,如果支持 package 的信息读取的话,就可以延伸问一下这个包的协议是什么之类的提问了。

完整代码

点击查看完整代码
#!/usr/bin/env nodeimport { Server } from "@modelcontextprotocol/sdk/server/index.js";import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";import { z } from "zod";import { zodToJsonSchema } from "zod-to-json-schema";import packageData from "../package.json";import {  CallToolRequestSchema,  ListResourcesRequestSchema,  ListToolsRequestSchema,  ReadResourceRequestSchema,} from "@modelcontextprotocol/sdk/types.js";const emptyInputSchema = z.object({});const emptyInputJsonSchema = zodToJsonSchema(emptyInputSchema);const server = new Server(  {    name: "get-package",    version: packageData.version,  },  {    capabilities: {      tools: {},      resources: {},    },  });server.setRequestHandler(ListToolsRequestSchema, async () => {  return {    tools: [      {        name: "get-name",        description: "获取package.json中的name",        inputSchema: emptyInputJsonSchema,      },      {        name: "get-version",        description: "获取package.json中的version",        inputSchema: emptyInputJsonSchema,      },    ],  };});server.setRequestHandler(CallToolRequestSchema, async (request) => {  const { name } = request.params;  switch (name) {    case "get-name": {      return {        content: [          {            type: "text",            text: packageData.name,          },        ],      };    }    case "get-version": {      return {        content: [          {            type: "text",            text: packageData.version,          },        ],      };    }    default:      throw new Error(`Unknown tool: ${name}`);  }});server.setRequestHandler(ListResourcesRequestSchema, async () => {  return {    resources: [      {        name: "package devDependencies依赖",        description: "返回 package.json 中的 devDependencies 依赖",        // "返回整个 package.json 文件的内容,包括 name, version, dependencies, devDependencies, scripts 等。",        uri: "package://devDependencies",      },    ],  };});server.setRequestHandler(ReadResourceRequestSchema, async (request) => {  const { uri } = request.params;  switch (uri) {    case "package://devDependencies":      return {        contents: [          {            uri,            text: JSON.stringify(packageData, null, 2),          },        ],      };    default:      throw new Error(`Unknown resource: ${uri}`);  }});const transport = new StdioServerTransport();server.connect(transport);

最后

如果文章有什么问题或者建议,欢迎讨论,下面一篇会介绍 SSE 的实现。

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

MCP Stdio LLM TypeScript
相关文章