随着大语言模型和原生人工智能应用程序的发展,一种新型的应用程序正在出现。这种应用程序运行在人工智能聊天工具中(比如ChatGPT),而不是作为独立的网页或移动应用存在。
在本教程中,你将学习如何从零开始构建MCP(模型上下文协议)服务器,包括开发可用于与ChatGPT结合使用的带认证功能的用户界面以及数据库系统。
你将经历构建、测试、将ChatGPT应用程序作为插件添加到系统中,以及将其提交到应用发布目录的全过程。通过这些步骤,你可以分三个阶段来完成这个项目的开发:
-
第一阶段:构建能够返回文本数据的基本MCP服务器。
-
第二阶段:为你的MCP服务器开发用户界面,使其能够在大语言模型的用户界面中使用。
-
第三阶段:为MCP服务器添加认证功能和数据库系统。
要完全理解本文的内容,你需要具备以下基础知识:
-
网页开发
-
JavaScript
-
React与React Native
-
SQL与数据库
我们将涵盖的内容:
什么是MCP服务器?
模型上下文协议(MCP)服务器是一种通过标准协议为AI应用程序提供工具、资源及提示的程序。MCP服务器可以提供只读的上下文信息、可调用的工具,或是可重复使用的提示模板,这些都能帮助扩展AI应用程序的功能。
开发者负责构建或配置MCP服务器,而主机应用程序中的MCP客户端会与之连接。这样一来,该应用程序就能让模型识别出可用的功能,并在适当的时候通过MCP协议调用相关工具或获取资源,从而协助完成特定任务。
使用MCP服务器能做什么?
MCP服务器使AI应用程序能够与模型本身之外的信息和系统进行交互。例如,它可以帮助模型查询当前信息、保存和检索用户数据、搜索文档,或触发其他应用程序中的操作。
在实际应用中,某个MCP服务器可能会连接到一个在线数据库,而另一个服务器则可能处理本地机器上的文件。这样的设计使得构建更加实用、且能与真实工具相结合的AI工作流程成为可能。
入门级:如何搭建自己的MCP服务器
在本教程中,你将学习如何使用Node.js自带的HTTP服务器、用于数据库管理和身份验证的Supabase工具,以及官方的MCP服务器SDK来构建自己的MCP服务器。之后你还会将其部署到DigitalOcean平台上,并在ChatGPT平台上发布你的应用程序。
具体来说,你需要完成以下两个步骤:
-
第一步:将搭建好的MCP服务器作为应用程序/连接器连接到ChatGPT平台,使其能在ChatGPT中得到使用。
-
第二步:提交应用程序进行审核,如果通过审核,就将其发布到ChatGPT的应用程序目录中。
虽然MCP服务器SDK并不是构建自己MCP服务器的唯一工具或框架,但为了简化入门流程,这里我选择了使用这些较为直观、易于使用的工具。
步骤0:准备项目
你将在这里创建一个完整的项目,因此首先需要创建项目包并初始化项目。具体操作如下:
-
创建一个以项目名称命名的新文件夹。在这个例子中,你可以使用
mcp_todo作为文件夹名。 -
进入这个新文件夹。
-
在该文件夹中打开终端。
-
使用
npm init --init-type=module -y命令初始化npm项目,这样就能生成JavaScript项目包文件,并为项目添加支持ES6规范的依赖包。 -
在项目中使用
git init命令初始化Git版本控制系统,以便跟踪代码变更。 -
安装项目中会用到的相关依赖包:
-
这些依赖包包括Supabase、MCP SDK(我们将在第二步中详细介绍),以及用于验证大语言模型输入数据的zod验证库。
npm install @modelcontextprotocol/sdk zod @supabase/supabase-js
-
-
创建一个
.gitignore文件,将node_modules目录添加到该文件中,这样Git就不会跟踪这个目录了。 -
使用以下命令将项目当前的状态添加到Git版本控制系统中:
-
git add . -
git commit -m "初始化项目"
-
通过这些步骤,你为自己创建了一个新的项目,这个项目可以作为管理和后续开发该项目的起点。
步骤1:创建一个Node.js服务器
要启动这个项目,你需要先创建一个简单的Node.js服务器。你可以创建一个名为server.js的新文件,然后写入以下代码来实现这一目标:
import { createServer } from "node:http";
const port = Number(process.env.PORT ?? 8787);
const httpServer = createServer(async (req, res) => {
console.log(`${req.method} ${req.url}`);
if (!req.url) {
res.writeHead(400).end("缺少URL地址");
return;
}
const url = new URL(req.url, `http://${req.headers.host ?? "localhost"});
res.writeHead(404).end("未找到相应资源");
});
httpServer.listen(port, () => {
console.log(`Todo MCP服务器正在监听http://localhost:${port}地址,按Ctrl+C可停止服务器运行`);
});
这个简单的Node.js服务器将作为你构建MCP服务器的基础。
要继续开发MCP服务器,你需要使用MCP Server SDK来进行相关配置。之后,你需要定义两样内容:一是要让大型语言模型使用的工具,二是大型语言模型用于渲染的用户界面及所需资源。
定义这些工具和用户界面的具体内容时,同样需要借助MCP Server SDK来完成。
步骤2:配置MCP Server SDK
要配置并启动MCP服务器,你需要具备以下条件:
-
工具:MCP服务器为大型语言模型提供的功能接口,这些接口使大型语言模型能够与服务器及外部系统进行交互。例如调用API、执行计算任务或查询数据库等。
-
资源(可选):MCP服务器与大型语言模型共享的数据。比如文件、数据库结构,或者可以在大型语言模型的聊天界面中作为嵌入组件使用的HTML用户界面等。
你可以在server.js文件的顶部添加以下代码来启动服务器:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
function createTodoServer() {
const server = new McpServer({ name: "todo-app", version: "0.1.0" });
return server;
}
接下来,你可以使用以下函数格式来添加所需的工具和资源:
server.registerTool(
"名称",
{},
async (args, meta) => { }
);
可以将这个工具注册功能视为连接MCP服务器的接口。大型语言模型会通过这个接口,根据提供的名称和元数据信息,来使用这些工具中的参数和结果进行处理。
今天,你将创建三个简单的工具:
-
添加待办事项
-
更新待办事项
-
列出所有待办事项
这些工具的功能看起来有些相似,但在后续的内容中,你会了解到如何编写它们,从而真正理解其中涉及的概念。
步骤3:添加MCP服务器工具——创建并添加待办事项
首先,在添加待办事项时,你需要一个简单的内存数组来存储这些数据。你可以将这个数组创建在`createTodoServer`函数之外,这样在整个服务器中都可以使用它。
let todos = []; // 创建在createTodoServer函数块之外
let nextId = 1; // 创建在createTodoServer函数块之外(这是一个用于标识待办事项的临时ID)
除了这个数组之外,你还需要另外两个辅助函数:首先是用于验证输入数据的函数,它能够指定大语言模型应该接收什么样的输入类型。
import { z } from "zod";
const addTodoInputSchema = {
title: z.string().min(1),
}; // 创建在createTodoServer函数块之外
接下来,你需要编写一个返回函数,这样就可以将其与其他函数一起使用,从而为这些功能提供统一的返回结果格式。
const replyWithTodos = (message) => ({
content: message ? [{ type: 'text', text: message }] : [],
structuredContent: { tasks: todos },
}); // 创建在createTodoServer函数块之外
最后,在`createTodoServer`函数内部,在`return server`之前,你需要注册这个添加待办事项的功能:
server.registerTool(
'add_todo',
{
title: '添加待办事项',
description: '根据输入的标题创建一个新的待办事项。",
inputSchema: addTodoInputSchema,
_meta: {
'openai/toolInvocation/invoking': '添加待办事项',
'openai/toolInvocation/invoked': '待办事项已添加',
},
},
async (args) => {
const title = args?.title?.trim().() ?? '';
if (!title) return replyWithTodos('标题不能为空。');
const todo = { id: `todo-${nextId++}`, title, completed: false };
todos = [...todos, todo];
return replyWithTodos(`${todo.title}`);
},
); // 创建在createTodoServer函数块之内
在上述代码中,你为这个功能指定了名称,并采用了一种简单的方法将待办事项添加到已经定义好的内存数组中。关键在于在添加数据之前先对其进行验证,并为其创建相应的对象。
import { z } from "zod";
const addTodoInputSchema = {
title: z.string().min(1),
}; // 创建在createTodoServer函数块之外
在元数据中,你提供了标题、描述、输入格式以及OpenAI所需的其他信息。这样,在AI处理这些数据时,就能正确地生成结果。当AI完成待办事项的添加操作后,你就能得到最新的待办事项列表。
const replyWithTodos = (message) => ({
content: message ? [{ type: 'text', text: message }] : [],
structuredContent: { tasks: todos },
}); // 创建在createTodoServer函数块之外
同时,你还定义了输入格式,这样大语言模型在调用你的服务器时就知道应该提供什么信息。此外,你还编写了一个辅助函数来处理待办事项的数据,这个函数会将待办事项以结构化的方式呈现出来,以便AI能够理解。
步骤4:从MCP服务器中获取待办事项列表
要列出所有的待办事项,你可以使用一个简单的列表函数来直接显示这些待办事项,而无需进行任何修改。在下面的代码中,你仍然采用了之前使用的命名规则、元数据设置以及描述信息格式。同时,你也再次使用了之前的辅助函数来获取内存中存储的待办事项列表。这段代码应该被写入`createTodoServer`函数内部。
server.registerTool(
'list_todos',
{
title: '列出所有待办事项',
description: '会显示所有的待办事项。",
_meta: {
'openai/toolInvocation/invoking': '正在列出待办事项',
'openai/toolInvocation/invoked': '待办事项已列出',
},
},
async () => {
return replyWithTodos();
},
);
步骤5:添加完成待办事项的功能
为了能够完成或编辑待办事项,你可以创建一个名为“complete_todo”的新工具,这个工具会接收待办事项的ID作为参数,并返回更新后的待办事项列表。为此,你需要在`createTodoServer`函数之外添加用于验证请求内容的辅助函数:
const completeTodoInputSchema = {
id: z.string().min(1),
};
然后在`createTodoServer`函数内部,添加以下代码:
server.registerTool(
'complete_todo',
{
title: '完成待办事项',
description: '根据ID将待办事项标记为已完成',
inputSchema: completeTodoInputSchema,
_meta: {
'openai/toolInvocation/invoking': '正在完成待办事项',
'openai/toolInvocation/invoked': '待办事项已完成',
},
},
async (args) => {
const id = args?.id;
if (!id) return replyWithTodos('缺少待办事项ID。');
const todo = todos.find((task) =&> task.id === id);
if (!todo) {
return replyWithTodos(`未找到ID为${id}的待办事项。`);
}
todos = todos.map((task) =&>
task.id === id ? { ...task, completed: true } : task,
);
return replyWithTodos(`待办事项“${todo.title}”已完成。`);
},
);
在这个工具中,你使用了与“list_todos”函数相同的定义结构,但额外添加了检查逻辑,以确保大语言模型返回的ID是有效的,并且该ID确实对应于某个待办事项。在处理数据之前,你始终应该手动验证这些数据的准确性,因为大语言模型可能会产生错误信息,而它们本身并不需要验证输入内容的正确性。
现在你可以将代码提交到Git中,并在版本控制系统中跟踪它的变化过程:
-
git add . -
git commit -m "新增MCP待办事项处理功能"
步骤6:将你的MCP服务器与Node.js服务器连接起来
既然你已经编写好了MCP服务器的主要功能代码,接下来就需要将这些功能连接到Node.js的HTTP服务器上了。
为此,你需要编写相应的流处理函数及相关代码。这些代码将会替代第一步中创建的服务器代码,因为它们包含了更多用于管理MCP服务器的功能。
首先,需要导入`StreamableHTTPServerTransport`函数:
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
然后,你可以将下面的代码复制过来,并用它替换掉项目中原有的服务器代码,这样就可以在项目中使用了。
const port = Number(process.env.PORT ?? 8787);
const MCP_PATH = '/mcp';
const httpServer = createServer(async (req, res) => {
if (!req.url) {
res.writeHead(400).end('缺少URL地址');
return;
}
const url = new URL(req.url, `http://${req.headers.host ?? 'localhost'}`);
// 处理针对/mcp端点的OPTIONS请求
if (req.method === 'OPTIONS' && url.pathname === MCP_PATH) {
res.writeHead(204, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
'Access-Control-Allow-Headers': 'content-type, mcp-session-id',
'Access-Control-Expose-Headers': 'Mcp-Session-Id',
});
res.end();
return;
}
// 处理针对主链接的GET请求
if (req.method === 'GET' && url.pathname === '/') {
res.writeHead(200, { 'content-type': 'text/plain' }).end('Todo MCP服务器');
return;
}
// 在这里使用Streamable HTTP来处理与MCP相关的请求
const MCP_METHODS = new Set(['POST', 'GET', 'DELETE']);
if (url.pathname === MCP_PATH && req.method && MCP_METHODS.has(req.method)) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id');
const server = createTodoServer();
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined, // 无状态模式
enableJsonResponse: true,
});
res.on('close', () => {
transport.close();
server.close();
});
try {
await server.connect(transport);
await transport.handleRequest(req, res);
} catch (error) {
console.error('处理MCP请求时出现错误:', error);
if (!res.headersSent) {
res.writeHead(500).end('内部服务器错误');
}
}
return;
}
res.writeHead(404).end('未找到相应资源');
});
httpServer.listen(port, () => {
console.log(
`Todo MCP服务器正在监听地址:http://localhost:${port}${MCP_PATH}`
);
});
在这段代码中,我们创建了一个主要的HTTP服务器来处理各种请求。该服务器为MCP客户端提供了/mcp端点,并通过Streamable HTTP将每个请求转发给一个无状态的MCP服务器进行处理。
现在你可以将这段代码提交到Git中,并在版本控制系统中对其进行跟踪了:
-
git add . -
git commit -m "新增MCP服务器相关功能"
如何测试您的MCP服务器
现在,你可以通过运行以下代码来测试你的MCP服务器的基本结构:
node server.js
通过执行这条命令,你将启动在前几步中创建的服务器。该服务器会处于活跃状态,并开始监听http://localhost:8787/mcp地址上的请求变化。在运行server.js之后,你需要打开“检查器”工具——这个工具能帮助你查看MCP服务器的注册信息,以及那些需要在安全环境中使用和运行的端点及工具。
npx @modelcontextprotocol/inspector@latest --server-url http://localhost:8787/mcp --transport http
当你运行上述命令后,就会发现自己已经与MCP服务器建立了连接。接下来,你需要通过“检查器”用户界面来使用这个服务器。利用“检查器”界面,你可以在不连接任何外部服务的情况下测试你的MCP服务器,并在本地验证输入和输出结果。

要测试你的工具,首先需要连接到服务器,之后才能查看和操作这些工具。
写完这段代码后,你可能会想:究竟可以通过什么样的用户界面来向用户展示信息呢?如果你现在立即运行你的项目,得到的只会是LLM生成的文本回复。但如果你构建一个用户界面,就能显著提升用户的体验。在下一节中,我们将会讨论如何实现这一点。
第二级:如何构建用户界面
通过之前编写的代码,你已经创建了一个简单的MCP服务器,该服务器能够将待办事项添加到列表中,并允许应用程序将这些事项标记为已完成。现在,你将学习如何使用`registerResource`工具来注册你自己设计的用户界面资源,这样ChatGPT就能使用这些资源了。
“资源”是指由你的MCP服务器提供的、专门用于LLM处理的数据。你可以将自己的用户界面分享给LLM,让它能够在聊天过程中使用这些界面来显示额外的数据或组件。
要共享用户界面,你需要准备一个HTML文件,这个文件需要依赖MCP服务器提供的数据,并且能够与MCP服务器进行交互。因此,你需要创建一个新的HTML文件。
第一步:创建用于展示用户界面的HTML文件
你之前准备的`TodoHTML`文件应该是一个能够与服务器以及ChatGPT的用户界面进行交互的HTML文件。这样的用户界面看起来会像下面的图片所示:

要构建这样一个用户界面,你需要创建一个名为`public/todo-widget.html`的文件,并在其中编写如下结构的代码:
<!doctype html>
<html lang="en">
待办事项列表
:273785004