Skip to content

MCP Server

md
文档:
1. 中文:https://mcpcn.com
2. 官网:https://modelcontextprotocol.io
3. vscode(github copilot):https://docs.github.com/en/copilot/how-tos/provide-context/use-mcp/use-the-github-mcp-server?tool=vscode
4. 高德地图:https://lbs.amap.com/api/mcp-server/gettingstarted#s0
5. VScode 插件:https://code.visualstudio.com/mcp
6. MCP魔塔:https://www.modelscope.cn/mcp/servers/@modelscope/modelscope-mcp-server
7. 建议学习:https://mp.weixin.qq.com/s/ohHHXl4VNIkbPpFPAnqQhQ

MCP Server 概念

在编写代码之前,让我们用简单的语言回顾一下 MCP 的主要组成部分。

服务器 (Server)

一个 McpServer 是核心对象,负责管理:

  1. 能力 (Capabilities): 声明是否支持工具、资源和提示。

  2. 注册表 (Registry): 保存已注册的工具、资源和提示。

  3. 协议合规性: 处理传入的 JSON-RPC 消息 (initialize, callTool, readResource 等)。

在 Node.js (TypeScript/JavaScript) 中,你可以这样创建它:

点击展开代码
js
const server = new McpServer({
  name: 'my-mcp-server',
  version: '1.0.0',
  capabilities: {
    tools:     { listChanged: true },
    resources: { listChanged: true },
    prompts:   { listChanged: true }
  }
});

工具 (Tool)

工具是 AI 可以调用来执行某些操作(计算或产生副作用)的函数。 注册工具时,你需要提供:

  1. 名称 (字符串), 例如:"calculate-bmi".

  2. 其参数的 Zod 模式(以便 MCP 可以自动验证)。

  3. 一个异步处理函数,接收解析后的参数并返回结果或错误。 示例:

点击展开代码
js
server.registerTool({
  name: 'calculate-bmi',
  schema: z.object({
    weight: z.number().positive(),
    height: z.number().positive()
  }),
  handler: async (params) => {
    const { weight, height } = params;
    return weight / (height * height);
  }
});

注册后,AI客户端可以进行JSON- RPC调用

点击展开代码
json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "callTool",
  "params": {
    "toolName": "calculate-bmi",
    "arguments": {
      "weight": 70,
      "height": 1.75
    }
  }
}

资源 (Resources)

资源向 AI 暴露数据。它们类似于 "GET" 端点:

点击展开代码
js
server.resource(
  'user-profile',
new ResourceTemplate('users://{userId}/profile', { list: undefined }),
async (uri, { userId }) => {
    // 例如,从你的数据库获取
    return {
      contents: [{
        uri: uri.href,
        text: `Profile data for user ${userId}`
      }]
    };
  }
);

那么客户端则进行如下调用:

点击展开代码
json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "readResource",
  "params": {
    "resourceName": "user-profile",
    "uri": "users://123/profile"
  }
}

提示 (Prompts)

提示是可重用的消息模板。它们帮助为 LLM 格式化请求:

点击展开代码
js
server.prompt(
  'review-code',
  { code: z.string() },
  ({ code }) => ({
    messages: [{
      role: 'user',
      content: {
        type: 'text',
        text: `Please review this code:\n\n${code}`
      }
    }]
  })
);

然后,客户端可以进行如下调用:

点击展开代码
json
{
 "jsonrpc": "2.0",
 "id": 5,
 "method": "mcp/getPrompt",
 "params": { "name": "review-code", "arguments": { "code": "const a = 1;" } }
}

demo如下

点击展开代码
js

/**
 * server.js
 *
 * Express MCP Server (Streamable HTTP, Stateful)
 *
 *  - 声明 MCP 能力(tools/resources/prompts)
 *  - 资源:config://app(静态)、users://{userId}/profile(动态)
 *  - 工具:calculate-bmi
 *  - 提示:review-code
 *  - 会话内持久化 McpServer 实例
 */

const express = require('express');
const { randomUUID } = require('crypto');
const { McpServer, ResourceTemplate } = require('@modelcontextprotocol/sdk/server/mcp.js');
const { StreamableHTTPServerTransport } = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
const { isInitializeRequest } = require('@modelcontextprotocol/sdk/types.js');
const { z } = require('zod');

const app = express();
// 便于 Express 解析所有 JSON 请求体。
app.use(express.json());

// 会话存储:我们维护一个内存中的对象 sessions,以 sessionId 为键,存储 { server, transport }。
// 这确保了每个客户端保持相同的 McpServer 实例,因此工具在多次调用之间保持注册状态
const sessions = {};

/* 
初始化流程createMcpServer():
我们传递 capabilities: { tools, resources, prompts } 并将 listChanged 设为 true,以便在初始化握手期间,服务器明确通告可用的工具/资源/提示。
注册两个资源 (config://app 和 users://{userId}/profile)、一个工具 (calculate-bmi) 和一个提示 (review-code)。
 */
function createMcpServer() {
  const server = new McpServer({
    name: 'example-server',
    version: '1.0.0',
    capabilities: {
      tools:     { listChanged: true },
      resources: { listChanged: true },
      prompts:   { listChanged: true }
    }
  });

  // 静态资源 config://app
  server.resource(
    'config',
    'config://app',
    async (uri) => {
      return {
        contents: [
          { uri: uri.href, text: 'App configuration here' }
        ]
      };
    }
  );

  // 动态资源 users://{userId}/profile
  server.resource(
    'user-profile',
    new ResourceTemplate('users://{userId}/profile', { list: undefined }),
    async (uri, { userId }) => {
      return {
        contents: [
          {
            uri: uri.href,
            text: `Profile data for user ${userId}`
          }
        ]
      };
    }
  );

  // 工具 calculate-bmi
  server.tool(
    'calculate-bmi',
    { weightKg: z.number(), heightM: z.number() },
    async ({ weightKg, heightM }) => {
      const bmi = weightKg / (heightM * heightM);
      return {
        content: [
          { type: 'text', text: String(bmi) }
        ]
      };
    }
  );

  // 提示 review-code
  server.prompt(
    'review-code',
    { code: z.string() },
    ({ code }) => {
      return {
        messages: [
          {
            role: 'user',
            content: {
              type: 'text',
              text: `Please review this code:\n\n${code}`
            }
          }
        ]
      };
    }
  );

return server;
}

// POST /mcp:初始化或复用会话
app.post('/mcp', async (req, res) => {
  const sessionIdHeader = req.headers['mcp-session-id'];
let sessionEntry = null;

  // 情况1:复用已存在的会话
if (sessionIdHeader && sessions[sessionIdHeader]) {
    sessionEntry = sessions[sessionIdHeader];

  // 情况2:无会话但这是初始化请求 → 建立新会话
  } else if (!sessionIdHeader && isInitializeRequest(req.body)) {
    const newSessionId = randomUUID();

    // StreamableHTTPServerTransport 是一种支持流式数据传输的 HTTP 服务器传输层实现,其核心作用是在 HTTP 协议基础上
    // ,为服务器与客户端(尤其是 LLM 模型或需要实时交互的客户端)提供持续、高效的流式数据交换能力。
    // 1. 支持流式响应
    // 2. 维持长链接
    // 3. 兼容HTTP协议
    // 4. 标准化的数据传输格式
    const transport = new StreamableHTTPServerTransport({
      sessionIdGenerator: () => newSessionId,
      onsessioninitialized: (sid) => {
        sessions[sid] = { server, transport };
      }
    });

    transport.onclose = () => {
      if (transport.sessionId && sessions[transport.sessionId]) {
        delete sessions[transport.sessionId];
      }
    };

    // 调用 createMcpServer() 来注册工具/资源/提示。
    const server = createMcpServer();
    // 在发送任何响应之前调用 await server.connect(transport)。
    // server.connect 通常用于建立或管理服务器与客户端之间的连接
    await server.connect(transport);

    sessions[newSessionId] = { server, transport };
    sessionEntry = sessions[newSessionId];

  } else {
    res.status(400).json({
      jsonrpc: '2.0',
      error: { code: -32000, message: 'Bad Request: No valid session ID provided' },
      id: null
    });
    return;
  }

  // 将请求转交给本会话的 transport
  await sessionEntry.transport.handleRequest(req, res, req.body);
});

// GET/DELETE /mcp:SSE 下行与关闭会话
async function handleSessionRequest(req, res) {
  const sessionIdHeader = req.headers['mcp-session-id'];
if (!sessionIdHeader || !sessions[sessionIdHeader]) {
    res.status(400).send('Invalid or missing session ID');
    return;
  }
  const { transport } = sessions[sessionIdHeader];
  await transport.handleRequest(req, res);
}

app.get('/mcp', handleSessionRequest);
app.delete('/mcp', handleSessionRequest);

// 启动
const PORT = 7171;
app.listen(PORT, () => {
  console.log(`MCP Server listening on port ${PORT}`);
});

/* 测试:初始化 */
/* 
curl -i -X POST http://localhost:7171/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Authorization: Bearer my-secret-token" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "initialize",
    "params": {
      "protocolVersion": "2024-11-05",
      "capabilities": { "interactive": true },
      "clientInfo": { "name": "example-client", "version": "1.0.0" }
    }
  }'

*/

/* 测试:调用 calculate-bmi 工具 */
/* 

curl -X POST http://localhost:7171/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "mcp-session-id: 68be30c9-73cd-42fd-9260-e4591c0735a2【需与返回的sessionId一致】" \
  -d '{
    "jsonrpc": "2.0",
    "id": 2,
    "method": "mcp/callTool",
    "params": {
      "name": "calculate-bmi",
      "arguments": { "weightKg": 70, "heightM": 1.75 }
    }
  }'

*/