2024年11月底,Anthropic公司发布了全新的MCP(Model Context Protocol)协议,即模型上下文协议。该协议作为一种开放标准,旨在实现大型语言模型(LLM)应用程序与外部数据源和工具之间的无缝集成。无论是在开发AI驱动的集成开发环境(IDE)、增强聊天界面,还是创建自定义AI工作流程,MCP都提供了一种标准化的方式来连接LLM与所需的环境。
内容简介
MCP(模型上下文协议)是一种开放协议,它能够实现AI应用程序与本地或远程资源之间的安全、可控的交互。
不过由于协议刚刚发布,大部分的演示和视频都是基于Claude Desktop App来直接使用的。Claude Desktop的具体代码实现并未开源,因此今天我们将一起动手开发一个开源APP,接入MCP协议。如果你更倾向于直接查看项目代码,可以跳至文末收藏项目链接。
在开始开发之前,让我们先来分解一下MCP的工作原理:
总体架构
在其核心部分,MCP遵循client-server架构,其中主机应用程序可以连接到多个服务器:
MCP Hosts: Claude桌面程序、IDE或AI工具,通过MCP访问资源
MCP Clients: 用于与服务器保持1:1连接的协议客户端
MCP Servers: 轻量级程序,通过标准化的MCP协议暴露特定功能
Local Resources: 你的计算机资源(数据库、文件、服务)
Remote Resources: 可通过互联网访问的资源(例如通过API)
需要注意的是,尽管MCP server在名称中带有server字样,但在架构图中明确指出它应部署在本地个人电脑上。因此,我们可以直接从GitHub库中选择合适的server进行安装和使用,无需重复开发这部分功能。
开发阶段
我将用TypeScript基于Electron简要展示如何实现一个支持MCP的APP。
基本组件
在开发App之前,我们需要明确其核心组件,以确保系统架构的清晰和功能的完整性:
- 渲染器(Renderer):负责获取并展示所有工具(tools)提供的数据和功能,这些工具将直接与语言模型进行交互。
- 主进程(Main):作为App的核心控制中心,通过进程间通信(IPC)机制,将客户端(client)的请求和调用传递给渲染器,确保数据和操作的流畅传递。
- 客户端(Client):作为App与服务端(server)之间的桥梁,负责处理与服务器的通信,确保数据的准确传输和响应。
客户端(Client)
定义一个名为 initializeClient 的异步函数,用于初始化并连接一个服务端(server):
export async function initializeClient(name: String, config: ServerConfig) {
const transport = new StdioClientTransport({
command: config.command,
args: config.args,
});
const client_name = `${name}-client`;
const client = new Client({
name: client_name,
version: "1.0.0",
}, {
capabilities: {}
});
await client.connect(transport);
console.log(`${client_name} connected.`);
return client;
}
定义两个异步函数listTools和callTools,分别用于列出工具和调用工具。这两个函数都依赖于刚刚创建的client的对象:
export async function listTools(client: Client) {
const tools = await client.request(
{ method: "tools/list" },
ListToolsResultSchema
);
console.log('List Tools:', tools);
return tools;
}
export async function callTools(client: Client, params: any) {
const call_tools = await client.request(
{
method: "tools/call",
params: params
},
CallToolResultSchema
);
console.log('Call Tools:', call_tools);
return call_tools;
}
主进程(Main)
根据配置文件初始化多个客户端,并返回这些客户端的数组:
async function initClient(): Promise<ClientObj[]> {
const config = readConfig(configPath);
if (config) {
console.log('Config loaded:', config);
const clients = await Promise.all(
Object.entries(config.mcpServers).map(async ([name, serverConfig]) => {
console.log(`Initializing client for ${name} with config:`, serverConfig);
const client = await initializeClient(name, serverConfig);
console.log(`${name} initialized.`);
return { name, client };
})
);
console.log('All clients initialized.');
return clients
}
console.log('NO clients initialized.');
return []
}
其中config配置文件可以参考官方(https://github.com/modelcontextprotocol/servers)库中的JSON格式配置文件,所以initClient这里直接解析其内容即可。
接下来,使用 Electron 框架中的ipcMain模块来处理主进程和渲染进程之间的通信:
ipcMain.handle('list-clients', () => {
return clients.map(({ name }) => name);
});
clients.forEach(({ name, client }) => {
console.log(`IPC Main list-tools-${name}`)
ipcMain.handle(`list-tools-${name}`, async () => {
return await listTools(client);
});
console.log(`IPC Main call-tools-${name}`)
ipcMain.handle(`call-tools-${name}`, async (event, params) => {
return await callTools(client, params);
});
});
以上,我们已经为每个客户端注册两个 IPC 处理函数:
- list-tools-${name}: 用于列出该客户端的所有工具。
- call-tools-${name}: 用于调用该客户端的某个工具,并传递参数。
此外,还有一个全局的 list-clients 处理函数,用于列出所有客户端的名称。
预加载脚本(Preload)
主要功能是与 Electron 的 ipcRenderer 模块进行交互,并将结果暴露给渲染端(Renderer):
async function listClients(): Promise<string[]> {
return await ipcRenderer.invoke('list-clients');
}
async function exposeAPIs() {
const clients = await listClients();
const api: MCPAPI = {};
clients.forEach(client => {
api[client] = {
list: () => {
return ipcRenderer.invoke(`list-tools-${client}`);
},
call: (params : any) => {
return ipcRenderer.invoke(`call-tools-${client}`, params);
}
};
});
contextBridge.exposeInMainWorld('mcpServers', api);
}
为每个客户端创建一个对象,包含两个方法:
- list: 调用 ipcRenderer.invoke 方法,发送一个名为 list-tools-${client} 的 IPC 调用,返回该客户端的工具列表。
- call: 调用 ipcRenderer.invoke 方法,发送一个名为 call-tools-${client} 的 IPC 调用,并传递参数 params,用于调用该客户端的工具。
渲染端(Renderer)
我使用了自己的一个开源UI(https://github.com/AI-QL/chat-ui),只需一个HTML文件即可实现。在此过程中,我们需要特别关注渲染端如何调用工具:
listTools: async function (resourceName) {
const mcpServers = this.getServers
if (!mcpServers) {
return null
}
const mcpKeys = Object.keys(mcpServers)
const mcpTools = []
await Promise.all(mcpKeys.map(async (key) => {
const tools = await mcpServers[key].list();
for (const tool of tools.tools) {
mcpTools.push({
type: 'function',
function: {
name: tool.name,
description: tool.description,
parameters: tool.inputSchema,
}
})
}
}));
return mcpTools
}
这个 listTools 函数的主要功能是从多个服务端(server)中获取工具(tool)列表,并将这些工具的信息整理成统一的格式返回。返回的工具信息包含了工具的名称、描述和输入参数的结构。
最后我们将获取的 tools 列表添加到请求体中即可:
if (chatbotStore.mcp) {
body["tools"] = await mcpStore.listTools()
}
const request = {
headers: headers,
method: chatbotStore.method,
body: JSON.stringify(body),
};
const completion = await fetch(
chatbotStore.url + chatbotStore.path,
request
);
测试
消息请求
首先,发送一条基本chat completions请求,观察请求体payload中携带的tools:
可以看到每个tool就是一个详细的函数名称、参数和格式的定义,在TypeScript中,使用 Zod 来定义和验证工具的参数和格式是一种常见做法。
文件访问
然后,以filesystem为例,我们测试一下模型访问文件的能力:
我在此处使用了gpt-4o-mini模型,结果显示该模型能够顺利完成任务。需要注意的是,MCP server出于安全考虑,并不会真正删除文件。即使提出删除请求,server也只会对文件进行重命名操作。
网页访问
然后,我们测试一下模型访问网页的能力:
对于一般的文字搜索,直接利用搜索引擎通常能获得较高的成功率。然而,在处理特定项目的搜索时,例如天气状况查询,模型可能会因无法准确输入合适的天气网站而失败。在此情况下,我选择了国产的qwen-turbo模型,因为它在处理这类任务时表现更为稳定。相比之下,使用GPT模型时,由于其倾向于查询国外网站,可能会导致超时失败的问题。
多次实验后,这类复杂工具(如网页查询所用的Puppeteer)的调用功能稳定性较差,失败率较高。通常需要多次尝试才能成功,且在面对防机器人机制时,模型因缺乏类人的点击习惯,容易陷入死循环。因此,目前仅能将其视为一次体验性质的尝试。
总结
由于我在编写代码时,MCP 刚刚发布一周,因此仅完成了初步的开发尝试。许多功能和内容仍有待完善,这篇文章的内容也较为仓促,恳请各位读者见谅。
文中所用代码已经开源并上传至 GitHub,欢迎有兴趣的小伙伴们自行尝试实现。得益于 MCP 开源服务器的支持,开发类似的 AI 代理应用门槛已大幅降低。对于熟悉 Python 或 TypeScript 的开发者来说,只需稍加研究 SDK 库,便能快速构建出一个功能相近的原型应用。
然而,目前整个系统的瓶颈仍在于模型的能力尚不足以轻松应对如此复杂的多个服务端。当前不少小伙伴都在密切关注OpenAI正在进行的长达12天的马拉松发布会,期待能有更多创新性的内容涌现。
完整代码:https://github.com/AI-QL/chat-mcp
UI部分:https://github.com/AI-QL/chat-ui
标签:实战篇,Protocol,name,return,client,Context,const,tools,MCP From: https://blog.csdn.net/aiqlcom/article/details/144324496