MCP理论和实战,然后做个MCP脚手架吧

引言: 本文介绍了目前MCP Server的开发方式和原理,包括streamable HTTP和STDIO两种。并提供了一个npm脚手架工具帮你创建项目,每个模板项目都是可运行的。

streamable HTTP

原理分析

抓包「握手」

MCP Client总共发了三次请求,MCP Server响应2次。实际的握手流程是4次握手,第5次请求是为了通知后续的信息(比如进度,日志等。 目前规范实现来看,第5次握手不影响正常功能)

使用wiresshark抓包结果如下:

从官网的「initialization」流程来看,也就是4次(第5次未来应该会被普遍实现)

第1次 Post请求,initialize 方法

json 复制代码
{
  "jsonrpc": "2.0",
  "id": 0,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "sampling": {},
      "elicitation": {},
      "roots": {
        "listChanged": true
      }
    },
    "clientInfo": {
      "name": "inspector-client",
      "version": "0.17.2"
    }
  }
}

第2次 :200 OK,响应体如下

json 复制代码
{
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "tools": {
        "listChanged": true
      }
    },
    "serverInfo": {
      "name": "weather",
      "version": "0.0.1"
    }
  },
  "jsonrpc": "2.0",
  "id": 0
}

第3次 :Post请求,notifications/initialized方法

json 复制代码
{"jsonrpc":"2.0","method":"notifications/initialized"}

第4次 :202 Accepted,无响应体

第5次 :Get请求,此时要求服务端一定是SSE传输了-accept: text/event-stream

vbnet 复制代码
GET /mcp HTTP/1.1
accept: text/event-stream

总结「握手」流程

  1. POST /mcp (initialize)

    • 客户端:你好,我是 Inspector Client,我想初始化。
    • 服务器:收到,这是我的能力列表(200 OK)。
    • 状态:JSON-RPC 会话开始。
  2. POST /mcp (notifications/initialized)

    • 客户端:我已经收到你的能力了,初始化完成。
    • 服务器:收到 (202 Accepted)。
    • 状态:逻辑握手完成。
  3. GET /mcp (Header: accept: text/event-stream)

    • 目的 :客户端现在试图建立长连接通道,以便在未来能收到服务器发来的通知(比如 notifications/message 或 roots/listChanged)。如果没有这个通道,服务器就变成了"哑巴",无法主动联系客户端。

后续通信

tools/list (列出工具)

client->server 请求

请求头:

http 复制代码
POST /mcp HTTP/1.1
accept: application/json, text/event-stream
accept-encoding: gzip, deflate, br
content-length: 85
content-type: application/json
mcp-protocol-version: 2025-06-18
user-agent: node-fetch
Host: localhost:3000
Connection: keep-alive

请求数据:

json 复制代码
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list",
  "params": {
    "_meta": {
      "progressToken": 1
    }
  }
}

P.S. params中的progressToken是可以用于后续的进度通知的(通过SSE)

server->client 响应

响应头:

http 复制代码
HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json
Date: Thu, 27 Nov 2025 11:52:31 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Transfer-Encoding: chunked

响应体:

json 复制代码
{
  "result": {
    "tools": [
      {
        "name": "get_weather_now",
        "title": "Get Weather Now",
        "description": "Get current weather for a location (city name)",
        "inputSchema": {
          "$schema": "http://json-schema.org/draft-07/schema#",
          "type": "object",
          "properties": {
            "location": {
              "description": "Location name or city (e.g. beijing, shanghai, new york, tokyo)",
              "type": "string"
            }
          },
          "required": [
            "location"
          ]
        }
      }
    ]
  },
  "jsonrpc": "2.0",
  "id": 1
}

这里列出一个工具:

  • get_weather_now,我们自己定义/注册的工具。我们可以拿到它的titledescriptioninputSchema,这些语义信息可以帮助LLM理解这个工具。
tools/call (调用tool)

这里通过 mcp inspector 工具调用了get_weather_now,请求体如下:

json 复制代码
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "_meta": {
      "progressToken": 2
    },
    "name": "get_weather_now",
    "arguments": {
      "location": "北京"
    }
  }
}

响应体:

json 复制代码
{
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Weather for 北京, CN:\nCondition: 晴\nTemperature: 3°C\nLast Update: 2025-11-27T19:50:14+08:00"
      }
    ]
  },
  "jsonrpc": "2.0",
  "id": 2
}
方法小总结

上面我们列出了两种常见的方法

  • tools/list。MCP Client在向LLM发请求携带列出的tool,LLM会告诉客户端调用的tool name,然后由MCP client来触发tool调用。
  • tools/call。MCP Client告诉MCP Server 调用哪个tool。

可以结合官网的这张示意图,调用tool就是一次request/response。如果是长任务,可以通过_meta.progressToken作为关联,通过SSE持续通知进度(还记得「握手」流程的第5次握手吗)

代码实战 - 天气工具

准备天气API

这里我使用了心知天气的API,然后自己封装一个node API。 src/core/seniverse.ts

ts 复制代码
import * as crypto from 'node:crypto';
import * as querystring from 'node:querystring';
/**
 * 查询天气接口
 */
const API_URL = 'https://api.seniverse.com/v3/';
export class SeniverseApi {
    publicKey;
    secretKey;
    constructor(publicKey, secretKey) {
        this.publicKey = publicKey;
        this.secretKey = secretKey;
    }
    async getWeatherNow(location) {
        const params = {
            ts: Math.floor(Date.now() / 1000), // Current timestamp (seconds)
            ttl: 300, // Expiration time
            public_key: this.publicKey,
            location: location
        };
        // Step 2: Sort keys and construct the string for signature
        // "key=value" joined by "&", sorted by key
        const sortedKeys = Object.keys(params).sort();
        const str = sortedKeys.map(key => `${key}=${params[key]}`).join('&');
        // Step 3: HMAC-SHA1 signature
        const signature = crypto
            .createHmac('sha1', this.secretKey)
            .update(str)
            .digest('base64');
        // Step 4 & 5: Add sig to params and encode for URL
        // querystring.encode will handle URL encoding of the signature and other params
        params.sig = signature;
        const queryString = querystring.encode(params);
        const url = `${API_URL}weather/now.json?${queryString}`;
        try {
            const response = await fetch(url);
            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }
            return await response.json();
        }
        catch (error) {
            console.error("Error making Seniverse request:", error);
            return null;
        }
    }
}

src/core/index.ts

ts 复制代码
import { SeniverseApi } from './seniverse.js';

export const seniverseApi = new SeniverseApi(
  process.env.SENIVERSE_PUBLIC_KEY || '',
  process.env.SENIVERSE_SECRET_KEY || '',
);

搭建streamable HTTP类型的MCP

1.使用express提供后端服务,然后设置/mcp endpoint(一般来说MCP client默认就是访问这个endpoint). 2.在MCP协议中,握手/工具调用等都是通过这个一个endpoint来完成的。

3.封装逻辑 封装了一个MyServer

  • run方法启动HTTP服务
  • init方法注册工具

4.核心是McpServerStreamableHTTPServerTransport两个API

  • McpServer: 负责注册tool.
  • StreamableHTTPServerTransport: 接管了/mcp endpoint的通信逻辑
json 复制代码
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import express from "express";
import { z } from "zod";
import "dotenv/config";
import { seniverseApi } from "./core/index.js";

export class MyServer {
  private mcpServer: McpServer;
  private app: express.Express
  constructor() {
    this.mcpServer = new McpServer({
      name: "weather",
      version: "0.0.1",
    });

    // Set up Express and HTTP transport
    this.app = express();
    this.app.use(express.json());

    this.app.use('/mcp', async (req: express.Request, res: express.Response) => {
        // Create a new transport for each request to prevent request ID collisions
        const transport = new StreamableHTTPServerTransport({
            sessionIdGenerator: undefined,
            enableJsonResponse: true
        });

        res.on('close', () => {
            transport.close();
        });

        await this.mcpServer.connect(transport);
        await transport.handleRequest(req, res, req.body);
    });

  }

  /**
   * 在端口运行Server, 通过HTTP stream传输数据
   */
  async run(): Promise<void> {
    const port = parseInt(process.env.PORT || '3000');
    this.app.listen(port, () => {
        console.log(`Demo MCP Server running on http://localhost:${port}/mcp`);
    }).on('error', error => {
        console.error('Server error:', error);
        process.exit(1);
    });
    
  }

  /**
   * 初始化,注册工具
   */
  async init(): Promise<void> {
    // Register weather tool
    this.mcpServer.registerTool(
      "get_weather_now",
      {
        title: "Get Weather Now",
        description: "Get current weather for a location (city name)",
        inputSchema: {
          location: z.string().describe("Location name or city (e.g. beijing, shanghai, new york, tokyo)")
        }
      },
      async ({ location }) => {
        
        const weatherData = await seniverseApi.getWeatherNow(location);
        if (!weatherData || !weatherData.results || weatherData.results.length === 0) {
          return {
            content: [
              {
                type: "text",
                text: `Failed to retrieve weather data for location: ${location}. Please check the location name and try again.`,
              },
            ],
          };
        }

        const result = weatherData.results[0];
        const weatherText = `Weather for ${result.location.name}, ${result.location.country}:\n` +
                            `Condition: ${result.now.text}\n` +
                            `Temperature: ${result.now.temperature}°C\n` +
                            `Last Update: ${result.last_update}`;
        return {
          content: [
            {
              type: "text",
              text: weatherText,
            },
          ],
        };
      },
    );
  }
}

效果如下:

注意左侧侧边栏:

  • Transport Type选择Streamable HTTP
  • URL 填写你的express 服务地址和endpoint。

stdio

原理分析

我在项目中,通过监听process.stdin,查看通信Message

ts 复制代码
// 监听 stdin 输入,可以在inspector面板的"notifications/message"中看到(作为debug用)
    process.stdin.on("data", async (data) => {
      const input = data.toString().trim();
      console.error(input);
    });

通过mcp-inspector工具就可以观察到通信信息了,往下看👁

tools/list

tools/call

结合官网的stdio通信原理图

可以总结如下:

  • 连接一个stdio MCP服务,不同于streamable HTTP MCP服务需要进行「握手」,只要开启一个子进程(subprocess),就表示连接成功。
  • 后续的通信的信息格式遵循json-rpc:2.0,通过读写process.stdinprocess.stdout完成通信。

代码实战 - 统计文件数

比较简单,可以参考我的这篇博客 Node写MCP入门教程,基于StdioServerTransport实现的统计目录下文件夹的MCP Server,并且介绍了mcp inspector的调试和Trae安装使用。

创建MCP项目的脚手架

每次写个新MCP Server都要搭建项目模板,这种重复的工作当然该做成工具辣! 我自己写了一个create-mcp脚手架 Githubcreate-mcp cli工具已经发布在npm上了,可以npm安装使用。

cli 原理

1.脚手架原理,首先准备两个模板项目

  • template-stdio 模板
  • template-streamable 模板

2.然后用Node写一个cli工具,使用了以下依赖,通过命令行交互的方式创建项目

bash 复制代码
pnpm i minimist prompts fs-extra chalk

3.根据你选择的项目名称和模板,帮你拷贝模板,修改为你的「项目名称」

觉得这个cli项目不错的话,给个免费的star吧~ 👉 Github

使用 cli

使用@caikengren-cli/create-mcp创建项目

bash 复制代码
npx @caikengren-cli/create-mcp

然后依次分别运行下面两个命令

bash 复制代码
# 编译ts/运行node
pnpm dev

# 打开 mcp-inspector工具调试
pnpm inspect

参考

mcp官网
mcp中文网
mcp: typescript-sdk

相关推荐
想用offer打牌7 小时前
MCP (Model Context Protocol) 技术理解 - 第二篇
后端·aigc·mcp
崔庆才丨静觅8 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅10 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊10 小时前
jwt介绍
前端