前端的AI路其之六 - MCP实现一个流式HTTP服务端

0 阅读5分钟

一、前言

其实前文已经讲过了MCP Server开发以及相关的调试。但是只是一个基础的MCP Tools,说穿了就是运行在Client的一段脚本。本次的主要内容是开发一个远程HTTP服务,这样我们可以将各类Tools都集成到远程服务上,既能动态的实现MCP功能的增减,也能提供更多更强大的功能,比如远程处理各种文件、爬取各种资源等等。

本文所用到的完整代码见文章结尾

二、准备

node 22.14.0

@modelcontextprotocol/sdk 1.21.1

Cursor (Windsurf 暂时不支持这种远程服务形式,因此以Cursor作为Client进行实验)

三、实现

我们可以先想一下如果你想实现这样一个远程服务,该如何实现?我的大概思路如下

1. 首先它得是个可运行的服务

2. 它需要提供可以被AI识别的Tools

3. Client 知道有这个服务并能访问

4. Client访问后能感知到并能够调用这些功能

看起来其实很简单,前后就这几步,那么我们就按照步骤来实现

3.1、 可运行的服务

作为前端,首选的,快捷的,比较快捷的方式肯定是启一个node服务啦

import express from 'express';

// 创建并配置 HTTP 服务
const app = express();
app.use(express.json());

const PORT = 3020;
app.listen(PORT, () =>
    console.error(`MCP server listening on http://localhost:${PORT}/mcp`)
);

3.2、可以被AI识别的Tools

回想前文,我们其实已经实现过MCP Tools,这里无需再做赘述传送门,我们以一个自定义加减映射来做Demo

import express from 'express';
import { randomUUID } from 'node:crypto';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { z } from 'zod'; 

// 创建 MCP Server 并在实例上注册工具
function getServer(): McpServer {
    // 首先,创建 McpServer 实例
    const server = new McpServer({
        serverId: randomUUID(),
        name: 'Local Calculator MCP',
        version: '1.0.0',
    });

    // 然后,在 server 实例上调用 registerTool 方法来定义和注册工具
    server.registerTool('add', 
        {
            description: '计算两个数字的LS映射和',
            // 使用 zod 定义输入参数的结构和类型
            inputSchema: {
                a: z.number().describe('第一个数字'),
                b: z.number().describe('第二个数字'),
            },
        },
        // 第三个参数是工具的实际处理函数
        // 第三个参数是工具的实际处理函数
        async (params) => {
            const sum = params.a + params.b * 33;
            console.log(`Executing 'add' tool: ${params.a} + ${params.b} = ${sum}`);
            
            // 修复返回值:结果必须包装在一个带有 `content` 数组的对象中
            return {
                content: [
                    {
                        type: 'text',
                        text: JSON.stringify({ result: sum })
                    }
                ]
            };
        }
    );

    // 如果有更多工具,可以在这里继续调用 server.registerTool(...)

    return server;
}

...

其实这两步感觉可以不用写,对大家来说太基础了。 接下来的步骤才是重头戏

3.3、给Client 配置访问

根据 MCP官方给的demo,需要提供一个POST方法给Client访问

image.png

所以我们代码里首先需要增加一个POST方法就行,如下

...
app.post('/mcp', async (req, res) => {
    console.log('post MCP')
    console.log('Received a POST request to /mcp, responding with health check OK.');
    res.status(200).json({ status: 'ok', message: 'MCP server is alive and listening for POST requests.' });
});
...

注意 因为 Windsurf现在还只支持 stdiosse两种形式,作为例子

我们在mcp.json中做如下修改

{
  "mcpServers": {
    "test_mcp_server": {
      "type": "streamable-http",
      "url": "http://localhost:3020/mcp",
      "note": "For Streamable HTTP connections, add this URL directly in your MCP Client"
    }
  }
}

注意 我们的服务要先启动 也就是运行

npm run build

node build/index.js // 这个看你build后文件叫啥就是啥

当我们在mcp.json配置好后,看到终端有打印这个 说明Cursor已经访问了我们的代码

(其实我们也可以通过MCP Inspector来调试,具体见下文)

image.png

3.4、Client访问后能感知到并能够调用这些功能

这里我们要用到一个方法 StreamableHTTPServerTransport

StreamableHTTPServerTransport 在 MCP SDK 中的作用一般是作为一个“流式 HTTP 服务端传输层”,用于处理和管理通过 HTTP 协议与 MCP server 进行的流式数据通信。

具体来说,它的主要功能包括:

  1. 作为 MCP server 的传输层,负责接收和发送 HTTP 请求和响应,支持流式数据处理(如大文件、长连接、实时数据等场景)。
  2. 封装了 HTTP 协议的细节,让上层 server(如 getServer() 返回的对象)能够专注于业务逻辑,而不用关心底层 HTTP 连接的管理。
  3. 支持 session(会话)管理,比如通过 sessionIdGenerator 可以自定义会话 ID 的生成方式。
  4. 通常配合 server.connect(transport) 以及 transport.handleRequest(req, res, req.body) 一起使用,实现 HTTP 请求的接入、处理和响应。

简单理解:
StreamableHTTPServerTransport 是 MCP server 和 HTTP 客户端之间的“桥梁”,负责把 HTTP 请求转化为 MCP server 能理解的格式,并将 server 的响应通过 HTTP 返回给客户端,且支持流式、长连接等高级用法。

修改POST方法

app.post('/mcp', async (req, res) => {
    console.log('post MCP')
    console.log('Received a POST request to /mcp, responding with health check OK.');
    
    const server = getServer();
    const transport = new StreamableHTTPServerTransport({
        sessionIdGenerator: undefined,
    });

    await server.connect(transport);

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

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

这样 当Client 访问到/mcp接口的时候,就会获取到 Tools信息,然后进行调用。

其实这里我们可以使用MCP Inspector进行调试,看一下在我们配置之后,是经历了哪些流程

PS 关于调试基本内容 我们这里不做赘述,有兴趣可以看我前面的这篇文章

3.5、 MCP Inspector 查看调用流程

1、启动你的服务

node  build/index.js

2、启动 MCP Inspector

npx @modelcontextprotocol/inspector

3、打开 Inspector的页面,并做如下配置后点击Connect

image.png

4、 这时候我们就可以看到连接情况以及对应的HistoryTools

image.png

我们可以看到它首先初始化了一次请求 initialize

initialize的请求和返回如下

image.png

5、 我们再点击Tools 就会再发起请求 获取到服务所提供的Tools

image.png

6、 所以我总结了一下,我们在Client配置完我们的服务之后,一个大概的流程就是 Client首先发送初始化请求,确认服务器状态,然后获取到有哪些Tools

四、总结

本文主要是探索了一下 MCP HTTP Server形式下是的实现流程。对于其中的原理,我其实没有做过多的关注。一方面是我觉得 这个并不是我写这篇文章的主旨,另一方面,也是感觉自己没有对它的原理有多深入的了解,我还是在使用阶段。也许等后面了解深入了,我会再回过来阐述一下原理部分。

我是走在探索AI使用的一个小前端,文章的内容可能有失偏颇,也有可能不准确,写出来也是希望能和大家讨论,欢迎各位指正。

祝好。

五、完整代码

import express from 'express';
import { randomUUID } from 'node:crypto';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { z } from 'zod'; 

// 创建 MCP Server 并在实例上注册工具
function getServer(): McpServer {
    // 首先,创建 McpServer 实例
    const server = new McpServer({
        serverId: randomUUID(),
        name: 'Local Calculator MCP',
        version: '1.0.0',
    });

    // 然后,在 server 实例上调用 registerTool 方法来定义和注册工具
    server.registerTool('add', 
        {
            description: '计算两个数字的LS映射和',
            // 使用 zod 定义输入参数的结构和类型
            inputSchema: {
                a: z.number().describe('第一个数字'),
                b: z.number().describe('第二个数字'),
            },
        },
        // 第三个参数是工具的实际处理函数
        // 第三个参数是工具的实际处理函数
        async (params) => {
            const sum = params.a + params.b * 33;
            console.log(`Executing 'add' tool: ${params.a} + ${params.b} = ${sum}`);
            
            // 修复返回值:结果必须包装在一个带有 `content` 数组的对象中
            return {
                content: [
                    {
                        type: 'text',
                        text: JSON.stringify({ result: sum })
                    }
                ]
            };
        }
    );

    // 如果有更多工具,可以在这里继续调用 server.registerTool(...)

    return server;
}

// 创建并配置 HTTP 服务 (这部分保持不变)
const app = express();
app.use(express.json());

app.post('/mcp', async (req, res) => {
    console.log('post MCP')
    console.log('Received a POST request to /mcp, responding with health check OK.');
    
    const server = getServer();
    const transport = new StreamableHTTPServerTransport({
        sessionIdGenerator: undefined,
    });

    await server.connect(transport);

    res.on('close', () => {
        console.log('Received a close event to /mcp, responding with health check OK.');
        transport.close();
        server.close();
    });

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

const PORT = 3020;
app.listen(PORT, () =>
    console.error(`MCP server listening on http://localhost:${PORT}/mcp`)
);