MCP 从入门到实践:用 TypeScript 实现第一个 MCP Server

本文是「从零搭建私人 RAG 知识库」专栏的基础篇。 MCP 是连接 AI 应用与外部工具、数据源的标准协议。本文暂时不引入数据库、RAG 或业务系统,而是从一个最简单的问候工具开始,跑通 MCP Server 的完整流程。

前言

大模型擅长理解和生成文本,但它不能天然读取本地文件、查询数据库或调用我们编写的函数。

假设我们有一个普通函数:

ts 复制代码
function greet(name: string): string {
    return `你好,${name}!`;
}

它可以接收名字并返回问候语,但 AI 客户端并不知道:

  • 这个函数是否存在;
  • 它有什么作用;
  • 调用时需要哪些参数;
  • 如何获得执行结果。

MCP 要解决的,就是让 AI 客户端能够用统一方式发现并调用这样的外部能力。

本文将完成一个最小示例:

text 复制代码
用户:请向小明问好
  ↓
AI 客户端选择 greet 工具
  ↓
MCP Server 执行工具
  ↓
返回:你好,小明!

一、什么是 MCP

MCP 的全称是 Model Context Protocol,中文通常翻译为"模型上下文协议"。

它是一套开放协议,用于规范 AI 应用与外部工具、数据源之间的通信方式。

通过 MCP,AI 应用可以:

  • 调用计算工具;
  • 读取本地文件;
  • 查询数据库;
  • 请求第三方 API;
  • 获取业务系统中的数据;
  • 执行经过授权的操作。

可以把 MCP 理解成 AI 应用与外部能力之间的"通用接口标准"。

MCP 不是什么

理解 MCP 时,需要先区分几个概念:

  1. MCP 不是大模型:它不负责理解问题和生成回答。
  2. MCP 不是数据库:它不负责长期保存数据。
  3. MCP 不是 RAG 框架:它不负责文档分块、向量化和语义检索。
  4. MCP 不会自动保证安全:权限控制、参数校验和敏感数据保护仍然需要开发者负责。

MCP 主要解决的是:

AI 应用如何发现、调用外部能力,并获得调用结果。


二、MCP 的基本架构

一个典型的 MCP 系统包含三个角色:

text 复制代码
用户
  ↓
MCP Host
  └── MCP Client
          ↓ MCP
      MCP Server
          ↓
       外部能力

1. MCP Host

Host 是用户实际使用的 AI 应用,例如:

  • 桌面 AI 助手;
  • IDE;
  • 支持工具调用的聊天应用;
  • 自己开发的 Agent 应用。

Host 通常负责管理对话、调用大模型以及展示工具调用过程。

2. MCP Client

MCP Client 位于 Host 内部,负责与 MCP Server 通信,包括:

  • 建立连接;
  • 获取工具列表;
  • 发送工具调用请求;
  • 接收工具执行结果。

一个 Host 可以连接多个 MCP Server。

3. MCP Server

MCP Server 是外部能力的提供方。

它负责:

  • 声明自己提供了哪些能力;
  • 描述工具的用途和参数;
  • 校验客户端传入的参数;
  • 执行对应逻辑;
  • 按 MCP 格式返回结果。

本文将实现的 greet 工具就运行在 MCP Server 中。


三、MCP Server 的三类核心能力

MCP Server 主要可以提供 Tools、Resources 和 Prompts 三类能力。

1. Tools:可执行工具

Tool 可以理解为模型能够调用的函数,例如:

text 复制代码
greet(name)
add(a, b)
get_weather(city)

Tool 适合完成计算、查询或操作任务。

本文使用的 greet 就是一个 Tool。

2. Resources:可读取资源

Resource 是 Server 提供的可读取数据,通常通过 URI 标识,例如:

text 复制代码
file:///notes/today.md
config://application

它适合向客户端提供文档、配置或其他内容。

3. Prompts:提示词模板

Prompt 是 Server 提供的可复用提示词模板,例如:

  • 总结一篇文章;
  • 审查一段代码;
  • 根据固定格式生成报告。

4. 如何选择

可以先使用下面的简单判断方式:

  • 需要执行函数或操作:使用 Tool
  • 需要读取数据:使用 Resource
  • 需要复用任务模板:使用 Prompt

初学 MCP 时,先掌握 Tool 即可。


四、准备一个最小 TypeScript 项目

1. 前置环境

本文示例基于 Node.js 和 npm。在开始之前,请先准备以下环境:

  • Node.js 18 或更高版本,建议使用当前的 LTS 版本;
  • npm,安装 Node.js 时通常会一并安装;
  • 一个代码编辑器,例如 Visual Studio Code;
  • 一个可以执行命令的终端。

可以在终端中执行以下命令,确认 Node.js 和 npm 已正确安装:

bash 复制代码
node --version
npm --version

如果命令能够输出版本号,说明环境已经可用。例如:

text 复制代码
v22.17.0
10.9.2

如果终端提示找不到 nodenpm,请先安装 Node.js。建议从 Node.js 官方网站下载安装 LTS 版本,也可以使用 nvm 等版本管理工具进行安装。

不同操作系统的安装方式可能不同,但后续示例使用的代码和 npm 命令基本一致。

2. 创建项目

创建项目并安装依赖:

bash 复制代码
mkdir mcp-hello
cd mcp-hello
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript tsx @types/node

package.json 中启用 ESM,并添加启动命令:

json 复制代码
{
  "type": "module",
  "scripts": {
    "start": "tsx src/index.ts"
  }
}

项目只需要一个源文件:

text 复制代码
mcp-hello/
├── package.json
└── src/
    └── index.ts

五、实现第一个 MCP Server

src/index.ts 中写入以下代码:

ts 复制代码
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: "hello-server",
    version: "1.0.0",
});

server.registerTool(
    "greet",
    {
        description: "向指定的人发送一句中文问候",
        inputSchema: {
            name: z.string().describe("需要问候的人名"),
        },
    },
    async ({ name }) => ({
        content: [
            {
                type: "text",
                text: `你好,${name}!`,
            },
        ],
    }),
);

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

这就是一个可以运行的最小 MCP Server。


六、逐段理解示例代码

1. 创建 Server

ts 复制代码
const server = new McpServer({
    name: "hello-server",
    version: "1.0.0",
});

这里创建了一个 MCP Server,并提供名称和版本信息。

2. 注册 Tool

ts 复制代码
server.registerTool(
    "greet",
    // 工具定义
    // 工具处理函数
);

registerTool() 的三个主要部分是:

  1. 工具名称;
  2. 工具定义;
  3. 工具处理函数。

3. 描述工具和参数

ts 复制代码
{
    description: "向指定的人发送一句中文问候",
    inputSchema: {
        name: z.string().describe("需要问候的人名"),
    },
}

description 告诉模型这个工具有什么作用。

inputSchema 定义调用工具时需要传入的参数。这里表示:

  • 参数名为 name
  • 参数类型必须是字符串;
  • 参数含义是"需要问候的人名"。

工具描述和参数描述不仅是给开发者看的,模型也会根据它们判断何时调用工具以及如何生成参数。

4. 执行工具

ts 复制代码
async ({ name }) => ({
    content: [
        {
            type: "text",
            text: `你好,${name}!`,
        },
    ],
})

当客户端调用 greet 时,处理函数会接收通过校验的 name,然后返回 MCP Tool 结果。

常见的文本结果格式为:

ts 复制代码
{
    content: [
        {
            type: "text",
            text: "工具执行结果",
        },
    ],
}

5. 建立 stdio 连接

ts 复制代码
const transport = new StdioServerTransport();
await server.connect(transport);

Transport 负责传输 MCP 消息。

StdioServerTransport 使用进程的标准输入和标准输出通信,适合本地 MCP Server。


七、启动 MCP Server

执行:

bash 复制代码
npm start

程序启动后可能不会显示任何内容,而是一直等待输入。这通常不是程序卡住,而是 Server 正在等待 MCP Client 发送协议消息。

MCP Server 不是普通的命令行交互程序,不应该直接在终端中输入自然语言进行测试。通常需要由支持 MCP 的客户端启动并连接它。


八、在 MCP 客户端中配置 Server

不同客户端的配置文件位置不同,但核心配置通常包括:

  • Server 名称;
  • 启动命令;
  • 命令参数。

示例配置如下:

json 复制代码
{
  "mcpServers": {
    "hello": {
      "command": "npx",
      "args": [
        "tsx",
        "/绝对路径/mcp-hello/src/index.ts"
      ]
    }
  }
}

请将示例路径替换为本机的真实绝对路径。

保存配置并重启客户端后,可以尝试输入:

text 复制代码
请向小明问好。

如果客户端正确连接 Server,大致会发生以下过程:

  1. 客户端发现 greet 工具;
  2. 模型根据工具描述判断它适合当前请求;
  3. 模型生成参数:
json 复制代码
{
  "name": "小明"
}
  1. MCP Client 调用 greet
  2. MCP Server 返回:
text 复制代码
你好,小明!
  1. 模型将工具结果展示给用户。

九、一次 Tool 调用是怎样完成的

完整流程可以概括为:

text 复制代码
用户提出请求
  ↓
大模型读取可用工具列表
  ↓
大模型选择 greet
  ↓
大模型生成 { "name": "小明" }
  ↓
MCP Client 发送调用请求
  ↓
MCP Server 校验参数
  ↓
工具处理函数生成问候语
  ↓
MCP Server 返回结果
  ↓
大模型组织最终回答

这里需要区分三项职责:

  • 大模型:理解用户意图,决定是否调用工具;
  • MCP Client:按照协议发送请求并接收结果;
  • MCP Server:校验参数并可靠地执行工具。

MCP Server 本身不会理解"请向小明问好"这句话,也不会自主决定调用哪个工具。这个判断通常由 Host 中的大模型完成。


十、stdio 使用时的注意事项

1. 不要向 stdout 输出日志

stdio MCP Server 使用标准输出传输协议消息。

下面的代码可能破坏通信:

ts 复制代码
console.log("Server started");

因为 console.log() 默认写入 stdout,客户端可能把日志误认为 MCP 协议数据。

需要输出日志时,应写入 stderr:

ts 复制代码
console.error("Server started");

可以简单记成:

text 复制代码
stdout:传输 MCP 消息
stderr:输出运行日志

2. 客户端配置尽量使用绝对路径

MCP Host 启动 Server 时,工作目录不一定是项目目录。相对路径可能导致脚本找不到,因此建议在客户端配置中使用绝对路径。

3. Server 由客户端启动

stdio 模式下,通常由 MCP Host 创建 Server 子进程,并通过该进程的标准输入和标准输出通信。

因此,不需要为这个最小示例额外监听 HTTP 端口。


十一、MCP Tool 开发建议

1. 工具名称要表达用途

推荐:

text 复制代码
greet
get_weather
search_document

不推荐:

text 复制代码
run
do_it
process

清晰的名称能帮助模型选择正确工具。

2. 工具描述要具体

不够清晰:

ts 复制代码
description: "处理名字"

更清晰:

ts 复制代码
description: "向指定的人发送一句中文问候"

描述应说明工具做什么,而不是简单重复工具名称。

3. 参数要有类型和说明

ts 复制代码
inputSchema: {
    name: z.string().describe("需要问候的人名"),
}

参数 Schema 一方面帮助模型生成正确参数,另一方面可以阻止不符合要求的数据进入处理函数。

4. 在系统边界校验输入

不要默认信任模型生成的参数。涉及以下内容时尤其需要严格校验:

  • 文件路径;
  • URL;
  • SQL;
  • Shell 命令;
  • 用户身份;
  • 写入或删除操作。

5. 高风险操作需要确认

读取信息和删除数据的风险完全不同。

对于发送消息、修改文件、删除数据等具有副作用的工具,应考虑:

  • 要求用户确认;
  • 使用最小权限;
  • 限制操作范围;
  • 记录执行结果;
  • 避免返回敏感信息。

这个原则与 MCP 无关,而是所有外部工具调用都应遵守的安全边界。


十二、从最小示例继续扩展

理解 greet 工具后,可以按照同样的结构增加更多能力。

例如增加一个加法工具:

ts 复制代码
server.registerTool(
    "add",
    {
        description: "计算两个数字之和",
        inputSchema: {
            a: z.number().describe("第一个数字"),
            b: z.number().describe("第二个数字"),
        },
    },
    async ({ a, b }) => ({
        content: [
            {
                type: "text",
                text: String(a + b),
            },
        ],
    }),
);

虽然工具功能不同,但基本结构没有变化:

text 复制代码
定义名称
  ↓
编写描述
  ↓
定义参数 Schema
  ↓
实现处理函数
  ↓
返回 MCP 结果

后续无论接入天气 API、文件系统、数据库还是知识库,本质上都是把已有能力包装成 MCP Tool。

初学时不必立即引入复杂业务。先确认下面这条链路能够正常工作:

text 复制代码
AI 客户端
  ↓
发现 Tool
  ↓
生成参数
  ↓
MCP Server 执行
  ↓
返回结果

十三、总结

MCP 的核心价值可以概括为一句话:

MCP 使用标准协议,把 AI 应用与外部工具和数据连接起来。

本文实现的最小 MCP Server 只做了三件事:

  1. 创建 McpServer
  2. 使用 registerTool() 注册 greet 工具;
  3. 使用 StdioServerTransport 与客户端通信。

核心代码只有以下几部分:

ts 复制代码
const server = new McpServer({
    name: "hello-server",
    version: "1.0.0",
});

server.registerTool(
    "greet",
    {
        description: "向指定的人发送一句中文问候",
        inputSchema: {
            name: z.string().describe("需要问候的人名"),
        },
    },
    async ({ name }) => ({
        content: [{ type: "text", text: `你好,${name}!` }],
    }),
);

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

掌握这个最小结构后,再逐步接入文件、API、数据库和知识库,会更容易理解每个组件在 MCP 系统中的职责。

相关推荐
Undoom1 小时前
用搜索引擎 API 搭一个 SEO 关键词监控工具:定时追踪排名、广告和相关搜索
后端
程序大爆炸1 小时前
gcsfuse的TypeCache
后端
朕的剑还未配妥1 小时前
CSS 实现渐变毛玻璃:从 backdrop-filter 到多层 mask
前端·css
心运软件1 小时前
Python实战:中国大学排行榜数据采集与可视化大屏
后端·python
程序员黑豆1 小时前
鸿蒙开发实战:使用 List 组件构建新闻列表
前端·华为·harmonyos
xiaobaoyu2 小时前
谈谈你对iframe的了解
前端
GISer_Jing2 小时前
前端转全栈须知后端知识
前端·后端·ai·前端框架
爱勇宝2 小时前
我做了一个排版器,也踩了一遍富文本复制的坑
前端·后端·微信
布朗克1682 小时前
Go 入门到精通-33-unsafe 与 CGO
开发语言·后端·golang·unsafe·cgo