从零开始:构建简易 MCP 系统全攻略👽

1,717 阅读8分钟

在当今人工智能飞速发展的时代,大型语言模型(LLMs)已广泛应用于各种场景。为了让这些模型更高效地与外部工具和上下文提供者通信,模型上下文协议(Model Context Protocol,MCP)应运而生。MCP 是一种标准化协议,旨在允许语言模型以标准化方式执行函数,并在不同模型和提供者之间保持一致的接口。本文将带你从零开始构建一个简易的 MCP 系统,深入理解其核心组件的设计。

一、MCP 系统

MCP 采用客户端 - 服务器(C/S)架构,主要包含三个关键角色:

MCP Host:作为交互入口,是发起请求的应用,如 IDE、AI 工具等。像 Claude Desktop 和 JetBrains IDE 等工具已集成了 MCP Host 功能。它受控本地资源,通过安全通道访问应用入口层,发起上下文请求,是用户与 AI 交互的直接界面,相当于 AI 的 “大本营”。

MCP Client:负责维护与服务器的连接,并处理请求与响应的转换。它支持 TypeScript 和 Python SDK,能实现协议层的消息封装。其作为协议转换层,是连接主机和服务器的桥梁,确保通信顺畅,通常无需用户直接操作,在 LLM 和 MCP 服务器之间传递信息。

MCP Server:对接数据源或工具,提供三类核心服务:

  • 资源(Resources) :包括静态数据(如文件内容、数据库记录)和动态模板(支持 RFC 6570 格式的 URI 模板)。它可以是文档解析服务、API 网关服务等,为 AI 提供所需的数据支持。

  • 工具(Tools) :允许 LLM 调用外部操作,如更新数据库、触发 API 等,通过 JSON - RPC 2.0 协议传递参数。例如,服务器可以提供计算工具、文件操作工具等,拓展 AI 的功能边界。

  • 提示(Prompts) :预定义交互模板,标准化模型处理特定任务的流程。通过这些模板,能让模型更规范地完成特定任务。

其通信协议分为两层:

  • 协议层:基于 JSON - RPC 2.0 封装请求、通知、响应等消息类型,支持错误码规范,如 MethodNotFound=-32601 。这样的规范确保了在不同系统间交互时,消息格式和错误处理的一致性。

  • 传输层:提供 Stdio(本地进程通信)和 HTTP + SSE(远程通信)两种模式,确保跨平台兼容性。

    • stdio(标准输入输出)模式:MCP 客户端通过子进程启动 MCP 服务器,通过标准输入(stdin)向服务器发送请求,通过标准输出(stdout)接收服务器的响应,适合在本地开发环境中使用,无需额外的网络配置,设置简单。

    • SSE(Server - Sent Events)模式:MCP 服务器以独立进程运行,监听 HTTP 请求,服务器可以持续向客户端推送事件和数据,适合在分布式环境中使用,支持多客户端连接,可被多个客户端同时访问。

典型的交互流程如下:MCP Host 发起请求,MCP Client 将请求转换为标准协议消息,MCP Server 接收请求并调用相应的资源或工具,处理结果通过 MCP Client 返回给 MCP Host,完成一次交互。通过这种设计,MCP 让 AI 助手从单纯的对话工具,进化成能操作现实世界的强大助手,突破了传统 AI 助手(如 Deepseek 等预训练模型)存在的知识截止日期限制,使 AI 能够连接并访问外部资源,获取更多实时信息和功能。

二、搭建开发环境

在开始构建 MCP 系统之前,需要搭建好开发环境。这里我们以 Node.js 为例,因为它在网络应用开发方面非常便捷,且有丰富的库可以使用。

创建项目目录:在你喜欢的位置创建一个新目录,比如 my - mcp - server 。这个目录将作为我们整个项目的根目录,用于存放所有相关的代码和文件。

初始化 Node.js 项目:进入项目目录,在终端执行 npm init -y,这会生成一个 package.json 文件,用于管理项目的依赖和脚本等信息。package.json 文件记录了项目的名称、版本、作者、依赖包等关键信息,方便后续对项目进行维护和管理。

安装必要依赖:我们需要安装一些库来帮助我们构建 MCP 服务器。执行以下命令安装所需依赖:

npm install express body - parser json - rpc - 2.0

其中,express 是一个流行的 Node.js web 应用框架,用于搭建服务器,它提供了简洁的路由系统和中间件机制,方便我们处理各种 HTTP 请求;body - parser 用于解析 HTTP 请求体,确保我们能够正确获取客户端发送过来的数据;json - rpc - 2.0 则是实现 JSON - RPC 2.0 协议的库,MCP 的协议层正是基于此,它帮助我们在服务器端处理符合 JSON - RPC 2.0 规范的请求和响应。

三、构建服务器骨架

安装好依赖后,我们来创建服务器的基本结构。在项目目录下创建一个 src 目录,并在其中添加一个名为 server.js 的文件。

首先,引入所需的模块:

const express = require('express');
const bodyParser = require('body - parser');
const JsonRpcServer = require('json - rpc - 2.0').Server;

这里,我们通过 require 函数引入了 express、body - parser 和 json - rpc - 2.0 模块,为后续搭建服务器和处理 JSON - RPC 请求做准备。

然后,创建一个 Express 应用实例,并使用 body - parser 中间件来解析 JSON 格式的请求体:

const app = express();
app.use(bodyParser.json());

通过 express() 创建了一个 Express 应用实例 app,接着使用 app.use(bodyParser.json()) ,这行代码告诉 Express 应用在处理请求时,使用 body - parser 中间件来解析 JSON 格式的请求体,以便我们后续能够从请求中获取到正确的数据。

接下来,创建一个 JSON - RPC 2.0 服务器实例,并定义一个简单的方法,用于测试服务器是否正常工作:

const jsonRpcServer = new JsonRpcServer();
jsonRpcServer.addMethod('ping', (params, done) => {
    done(null, 'pong');
});

通过 new JsonRpcServer() 创建了一个 JSON - RPC 2.0 服务器实例 jsonRpcServer ,然后使用 addMethod 方法为服务器添加了一个名为 ping 的方法。当客户端调用 ping 方法时,服务器会执行传入的回调函数,这里回调函数直接通过 done 函数返回一个 'pong' 结果,用于表明服务器正常响应。

最后,将 JSON - RPC 服务器集成到 Express 应用中,并启动服务器监听指定端口(这里以 3000 为例):

app.use('/mcp', (req, res) => {
    jsonRpcServer.receive(req.body, (err, response) => {
        if (err) {
            console.error(err);
            res.status(500).send('Internal Server Error');
        } else {
            res.json(response);
        }
    });
});
const port = process.env.PORT || 3000;
app.listen(port, () => {
    console.log(`MCP Server is running on port ${port}`);
});

通过 app.use('/mcp', (req, res) => {}) 将 JSON - RPC 服务器集成到 Express 应用中,当接收到以 /mcp 为路径的请求时,会调用 jsonRpcServer.receive 方法来处理请求。如果处理过程中出现错误,会将错误信息打印到控制台,并返回一个 500 Internal Server Error 响应给客户端;如果处理成功,则将响应结果以 JSON 格式返回给客户端。最后,通过 app.listen(port, () => {}) 启动服务器,监听指定端口 port,如果环境变量中没有设置 PORT,则默认使用 3000 端口,并在服务器启动成功后打印日志信息。

至此,一个简单的服务器骨架就搭建好了。你可以在终端运行 node src/server.js 启动服务器,然后通过一些工具(如 Postman)发送 JSON - RPC 请求到 http://localhost:3000/mcp 来测试 ping 方法是否正常工作。请求体如下:

{
    "jsonrpc": "2.0",
    "method": "ping",
    "params": [],
    "id": 1
}

如果一切正常,你应该能收到如下响应:

{
    "jsonrpc": "2.0",
    "result": "pong",
    "id": 1
}

四、实现核心 MCP 组件

(一)定义 MCP 请求和响应类型

在 src 目录下创建一个 types.js 文件,用于定义 MCP 请求和响应的类型。

首先,定义 MCP 请求类型:

// MCP请求类型
exports.MCPRequest = class MCPRequest {
    constructor(version, tools = [], context = []) {
        this.type ='mcp.Request';
        this.version = version;
        this.tools = tools;
        this.context = context;
    }
};
exports.ToolRequest = class ToolRequest {
    constructor(id, name, input) {
        this.id = id;
        this.name = name;
        this.input = input;
    }
};
exports.ContextRequest = class ContextRequest {
    constructor(id, name, input = null) {
        this.id = id;
        this.name = name;
        this.input = input;
    }
};

这里,MCPRequest 包含版本号、工具请求数组和上下文请求数组。它是整个 MCP 请求的封装,用于向服务器传达客户端的需求,包括使用哪些工具以及获取哪些上下文信息。ToolRequest 表示对某个工具的请求,包含请求 ID、工具名称和输入参数,每个工具请求都有唯一的 ID 以便跟踪和响应,工具名称用于服务器识别要调用的具体工具,输入参数则是该工具执行所需的数据。ContextRequest 表示对上下文的请求,包含请求 ID、上下文名称和可选的输入参数,类似地,通过 ID 跟踪请求,上下文名称指定要获取的上下文类型,输入参数可用于更精确地筛选或定制上下文内容。

然后,定义 MCP 响应类型:

// MCP响应类型
exports.MCPResponse = class MCPResponse {
    constructor(version, status, tools = [], context = [], error = null) {
        this.type ='mcp.Response';
        this.version = version;
        this.status = status;
        this.tools = tools;
        this.context = context;
        this.error = error;
    }
};
exports.ToolResult = class ToolResult {
    constructor(id, status, output = null, error = null) {
        this.id = id;
        this.status = status;
        this.output = output;
        this.error = error;
    }
};
exports.ContextResult = class ContextResult {
    constructor(id, status, output = null, error = null) {
        this.id = id;
        this.status = status;
        this.output = output;
        this.error = error;
    }
};
exports.ErrorInfo = class ErrorInfo {
    constructor(message, details = null) {
        this.message = message;
        this.details = details;
    }
};

MCPResponse 包含版本号、状态(成功或失败)、工具执行结果数组、上下文处理结果数组和错误信息(如果有)。它是服务器对 MCP 请求的整体响应,版本号用于保持协议版本一致,状态表明整个请求处理的结果,工具执行结果数组和上下文处理结果数组分别存储对应请求的处理结果,错误信息则在出现问题时记录详细情况。ToolResult 表示工具执行的结果,包含请求 ID、执行状态、输出结果和错误信息,通过 ID 与请求对应,执行状态表明工具执行是否成功,输出结果是工具执行后的返回数据,错误信息在执行出错时记录原因。ContextResult 表示上下文处理的结果,类似 ToolResult,也是用于记录上下文请求处理的结果。ErrorInfo 用于存储错误的详细信息,包括错误消息和具体细节,方便调试和排查问题。

(二)工具管理器

在 src 目录下创建一个 tools.js 文件,用于管理服务器提供的工具。

首先,创建一个 Map 来存储可用的工具及其处理函数:

const toolRegistry = new Map();

这里使用 Map 数据结构创建了一个 toolRegistry ,它将用于存储工具名称和对应的处理函数,方便后续根据工具名称快速找到并调用相应的处理逻辑。

然后,定义一个函数用于注册新工具:

exports.registerTool = (name, handler) => {
    toolRegistry.set(name, handler);
};

这个 registerTool 函数接收两个参数,name 表示工具的名称,handler 是处理该工具请求的函数。通过 toolRegistry.set(name, handler) 将工具名称和处理函数存储到 toolRegistry 中,完成工具的注册过程。

接着,定义一个函数用于处理工具请求:

const { ToolRequest, ToolResult, ErrorInfo } = require('./types');
exports.processToolRequests = async (toolRequests) => {
    return Promise.all(toolRequests.map(async (req) => {
        try {
            const handler = toolRegistry.get(req.name);
            if (!handler) {
                return new ToolResult(
                    req.id,
                    'error',
                    null,
                    new ErrorInfo(`找不到工具'${req.name}'`)
                );
            }
            const output = await handler(req.input);
            return new ToolResult(req.id,'success', output, null);
        } catch (error) {
            return new ToolResult(
                req.id,
                'error',
                null,
                new ErrorInfo(
                    error instanceof Error? error.message : '未知错误',
                    error
                )
            );
        }
    }));
};

这个 processToolRequests 函数接收一个工具请求数组 toolRequests 。它首先使用 Promise.all 和 map 方法对每个工具请求进行处理。对于每个请求,先从 toolRegistry 中获取对应的处理函数 handler,如果找不到对应的处理函数,则返回一个包含错误信息的 ToolResult,表明找不到该工具。如果找到了处理函数,则调用该函数并传入请求的输入参数 req.input,等待处理函数执行完成并获取输出结果 output,最后返回一个状态为'success' 且包含输出结果的 ToolResult。如果在处理过程中出现错误,会捕获该错误并返回一个包含错误信息的 ToolResult,错误信息包括错误消息和具体的错误对象(如果有)。

(三)上下文提供者管理器

在 src 目录下创建一个 context.js 文件,用于管理上下文提供者。

类似工具管理器,先创建一个 Map 来存储可用的上下文提供者及其处理函数:

const contextRegistry = new Map();

这里创建的 contextRegistry Map 用于存储上下文提供者的名称和对应的处理函数,与工具管理器中的 toolRegistry 类似,都是为了方便管理和查找相关的处理逻辑。

然后,定义一个函数用于注册新的上下文提供者:

exports.registerContextProvider = (name, handler) => {
    contextRegistry.set(name, handler);
};

这个 registerContextProvider 函数与工具注册函数类似,接收上下文提供者的名称 name 和处理函数 handler,通过 contextRegistry.set(name, handler) 将其存储到 contextRegistry 中,完成上下文提供者的注册。

接着,定义一个函数用于处理上下文请求:

const { ContextRequest, ContextResult, ErrorInfo } = require('./types');
exports.processContextRequests = async (contextRequests) => {
    return Promise.all(contextRequests.map(async (req) => {
        try {
            const handler = contextRegistry.get(req.name);
            if (!handler) {
                return new ContextResult(
                    req.id,
                    'error',
                    null,
                    new ErrorInfo(`找不到上下文提供者'${req.name}'`)
                );
            }
            const output = await handler(req.input);
            return new ContextResult(req.id,'success', output, null);
        } catch (error) {
            return new ContextResult(
                req.id,
                'error',
                null,
                new ErrorInfo(
                    error instanceof Error? error.message : '未知错误',
                    error
                )
            );
        }
    }));
};

processContextRequests 函数接收一个上下文请求数组 contextRequests 。同样使用 Promise.all 和 map 方法,对每个上下文请求进行处理。通过检索 contextRegistry ,找到对应的处理函数并执行,根据执行结果返回包含成功或错误信息的 ContextResult ,以此实现对上下文请求的统一管理与处理 。

五、总结

通过以上步骤,我们成功构建了一个简易的 MCP 系统。从认识 MCP 系统的架构、角色及通信协议,到搭建开发环境、构建服务器骨架,再到实现核心 MCP 组件,每一个环节都紧密相连,共同构成了一个完整可用的系统。

最后欢迎大家点赞关注加收藏~