AI探索 | 基于 Node.js 开发 MCP 客户端+服务端及优秀项目分享

发布于:2025-07-22 ⋅ 阅读:(16) ⋅ 点赞:(0)

🤔什么是 MCP?

官网原文:MCP is an open protocol that standardizes how applications provide context to LLMs. Think of MCP like a USB-C port for AI applications. Just as USB-C provides a standardized way to connect your devices to various peripherals and accessories, MCP provides a standardized way to connect AI models to different data sources and tools.

MCP是一个开放协议,它规范了应用程序如何向大型语言模型(LLM)提供上下文。你可以将MCP想象成AI应用程序的USB-C端口。就像USB-C为设备与各种外围设备和配件的连接提供了标准化方式一样,MCP也为AI模型与不同数据源和工具的连接提供了标准化方式。

MCP的意义

在 MCP 推出之前,AI 应用依赖于各种方法与外部工具交互,例如手动 API 连接、基于插件的接口以及代理框架。如图 1 所示,这些方法需要将每个外部服务与特定的 API 集成,这增加了复杂性并限制了可扩展性。MCP 通过提供标准化协议来应对这些挑战,该协议支持与多种工具进行无缝且灵活的交互。

MCP 架构

MCP 架构由三个核心组件组成:MCP 主机、MCP 客户端和 MCP 服务器。这些组件协同工作,实现 AI 应用、外部工具和数据源之间的无缝通信,确保操作安全且得到妥善管理。

以上两张图片来自:MCP Research Paper

🥇优秀项目

awesome-mcp-servers

开源地址:https://github.com/punkpeye/awesome-mcp-servers

Github 上必须出现的,又必然很火的 awesome-xx 系列🤭,收藏精品 MCP 服务端。

playwright-mcp

开源地址:https://github.com/microsoft/playwright-mcp

微软出品,使用 Playwright 提供浏览器自动化功能,能够通过结构化可访问性快照与网页进行交互,无需截图或视觉调优模型。

github-mcp-server

开源地址:https://github.com/github/github-mcp-server

Github 官方出品的 MCP 服务,提供与GitHub API的无缝集成,为开发人员和工具赋予高级自动化和交互能力。

Awesome-MCP-ZH

开源地址:https://github.com/yzfly/Awesome-MCP-ZH

一个专为中文用户打造的 MCP(模型上下文协议)资源合集! 这里有 MCP 的基础介绍、玩法、客户端、服务器和社区资源,帮你快速上手这个 AI 界的“万能插头”。

MCP Server资源站

  • Mcp.so:收录超多的 MCP Server

👨‍💻一套简单的MCP交互程序

其实对程序员来说,MCP 交互不是什么新玩意,都可以简单拿 HTTP 协议套过来进行理解😄。一个支持发送 MCP 请求的客户端,向 MCP 服务器发起调用。

MCP 官方代码库为 https://github.com/modelcontextprotocol,里面有文档以及主流语言的 SDK,这里我们选择 typescript-sdk 进行客户端/服务端的开发。里面有一个inspector的库,是用于测试 MCP Server 的。

注意:MCP 需要 node.js v18+

开始做 MCP 开发前,有些术语需要了解下。

术语概要

  • resource(资源):是向 LLM 公开数据的方式。它们类似于 REST API 中的 GET 端点——它们提供数据,但不应执行大量计算或对服务端产生影响;
  • tool(工具):允许 LLM 通过你的服务器执行操作。与资源不同,工具需要执行计算并产生副作用;
  • prompt(提示):可重复使用的模板,帮助 LLM 有效地与服务器交互。

参考资料

  • pentest-mcp:适用于专业渗透测试人员的 MCP 服务器,包括 STDIO/HTTP/SSE 支持、nmap、go/dirbuster、nikto、JtR、hashcat、单词表构建等。
  • fastify-mcp:用于运行 MCP 服务器的 Fastify 插件

🖥️ 服务端

使用 node.js 自带的 http 模块处理请求

import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"
import { z } from "zod"
import dayjs from "dayjs"
import { randomUUID } from "node:crypto"

import { createServer, ServerResponse } from 'node:http'
import pc from 'picocolors'

const log = (msg, level="INFO")=> console.debug(`${pc.cyan(dayjs().format("MM-DD HH:mm:ss:SSS"))} ${pc.gray(level)} ${msg}`)

const buildServer = ()=>{
    const mcpServer = new McpServer({ name: "demo-mcp-server", version:"1.0.0" })

    mcpServer.registerTool(
        "add",
        {
            title: "数值相加",
            description: "将参数累加",
            inputSchema: {  a: z.number(), b: z.number() }
        },
        async ({ a, b })=>{
            log(`${pc.magenta("[TOOL]")} add a=${a} b=${b}`)
            return {
                content: [{ type:'text', text: `${a+b}` }]
            }
        }
    )

    mcpServer.registerResource(
        "greeting",
        new ResourceTemplate("greeting://{name}", { list: null }),
        {
            title: "Greeting Resource",
            description: "Dynamic greeting generator"
        },
        async (uri, { name })=>{
            log(`${pc.magenta("[RESOURCE]")} GREETING ${name}`)
            return {
                contents:[{ uri: uri.href, text:`Hello ${name}` }]
            }
        }
    )
    return mcpServer
}


/**
 * @type {Map<String, StreamableHTTPServerTransport>}
 */
const transports = new Map()

/**
 *
 * @param {ServerResponse} res
 * @param {String} message - 描述信息
 * @param {Number} code - 错误代码,默认-32000
 */
const sendError = (res, statusCode=400, message, code=-32000)=>{
    res.writeHead(statusCode, { 'content-type':'application/json' })
    res.end(JSON.stringify({
        jsonrpc: "2.0",
        error: { code, message },
        id: null,
    }))
}

const httpServer = createServer(async (req, res)=>{
    const { url, method, headers } = req
    if(url == '/favicon.ico')
        return

    const sessionId = headers['mcp-session-id']

    // 请求体(需要手动拼接)
    let body = '';
    req.on('data', chunk =>  body += chunk)

    req.on('end', async ()=>{
        log(`${pc.magenta(method.padEnd(4, " "))} | ${req.url} SESSION=${sessionId} TYPE=${headers['content-type']}`)

        if(method == 'POST'){
            /**@type {StreamableHTTPServerTransport} */
            let transport = transports.get(sessionId)

            if(transport == null || transport.closed == true){
                let server = buildServer()
                transport = new StreamableHTTPServerTransport({
                    sessionIdGenerator: ()=> randomUUID(),
                    onsessioninitialized: id=> {
                        log(`MCP Session 已创建,ID=${id}`)
                        transports.set(id, transport)
                    }
                })
                transport.onclose = function(){
                    log(`transport 关闭 onclose...`)
                    if(this.sessionId)
                        transports.delete(this.sessionId)
                    else
                        this.closed = true
                }

                await server.connect(transport)
                log(`StreamableHTTPServerTransport 创建成功...`)
            }

            const postBody = JSON.parse(body)
            await transport.handleRequest(req, res, postBody)
            log("处理 POST 请求...")
            console.debug(postBody)
        }
        else {
            sendError(res, 405, "Method not allowed.")
        }
    })
})

const PORT = 8080
httpServer.listen(PORT, ()=>{
    log(`SERVER RUNNING ON ${PORT} ...`)
})

🤖 客户端

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

const client = new Client({
    name: "demo-mcp-client",
    version: "1.0.0"
})

let transport = new StreamableHTTPClientTransport("http://localhost:8080")
await client.connect(transport)

console.debug("MCP Server 连接成功...")

try{
    //列出全部的 resource
    const resources = await client.listResources()
    console.debug("资源列表", resources)

    console.debug(await client.readResource({uri:"greeting://0604hx"}))

    const tools = await client.listTools()
    console.debug("工具列表", tools)

    console.debug(await client.callTool({
        name: 'add',
        arguments:{
            a: 100,
            b: 300
        }
    }))
}catch(e){
    console.debug("出错啦", e)
}

await transport.close()
await client.close()

StreamableHTTP客户端初始化流程

  1. 发起初始化
{
  method: 'initialize',
  params: {
    protocolVersion: '2025-06-18',
    capabilities: {},
    clientInfo: { name: 'demo-mcp-client', version: '1.0.0' }
  },
  jsonrpc: '2.0',
  id: 0
}
  1. 第二次请求
{ method: 'notifications/initialized', jsonrpc: '2.0' }

📷 运行截图

服务端

客户端执行结果

🛠️ 使用 MCP Inspector

后续会单独写一篇博文介绍如何使用😄

❓ 问题集锦

  1. MCP error -32603: keyValidator._parse is not a function
    经排查,是由于 zod 版本过高(一开始用的是 4.x),改成 3.x 就正常了。参考:McpError: MCP error -32603: keyValidator._parse is not a function

  2. "Bad Request: Server not initialized
    Server 并未初始化,请先调用server.connect(transport)

  3. Invalid Request: Server already initialized
    Server 已经初始化,sessionId 已经重复。

📚 推荐书籍


《MCP协议与AI Agent开发:标准、应用与实现》清华大学出版社出版的图书,系统地阐述了MCP的技术原理、协议机制与工程应用,提供了从底层协议设计到项目部署的全流程的应用指南。全书从结构上分为基础理论、协议规范、开发工具链、应用构建4部分,共9章。具体内容包括大模型基础、MCP基本原理、MCP标准与规范体系、MCP与LLM的互联机制、MCP开发环境与工具链、MCP与多模态大模型集成,以及MCP的状态流转机制、Prompt构建规范、上下文调度策略及其与模型推理引擎的协同工作,同时涉及流式响应、函数调用、模块化Prompt设计等前沿技术,并结合基于DeepSeek平台的真实应用项目(人格共创AI剧本工坊、自演化智能议程会议系统与深梦编导器),助力读者理解MCP在多元领域的可拓展性与工程实践


网站公告

今日签到

点亮在社区的每一天
去签到