MCP
前言
重新整理了上篇文章,主要修正了错误的地方,加上了正确的截图和代码!感谢大家的积极指正!
这篇文章记录一下我用 MCPTypeScriptSDK 实现一个自包含的 AI 聊天应用的过程:内部包含 MCP 服务器提供上下文,客户端拿上下文再去调 LLM 接口拿回答!
正文MCP 是什么?简单说,MCP 是一个给 AI 应用提供上下文的标准协议。你可以把它理解成一个服务标准,它规定了“资源”和“工具”的接口规范,然后通过客户端连接这些接口,就可以组合出丰富的上下文数据。比如说资源可以是“当前时间”、“用户历史记录”,工具可以是“数据库搜索”、“调用外部 API”。
它采用的是客户端-服务器架构,Server 暴露上下文能力,Client 拉取这些上下文,再拿去调语言模型生成回答,而 Transport 负责 Server 和 Client 的通信部分!

MCP 架构
(AI 帮我画的图)
其中图片中的 Transport 层还分为:
StdioServerTransport:用于 CLI 工具对接 stdin/stdoutSSEServerTransport:用于HTTP通信StdioClientTransport:客户端以子进程方式拉起服务端,这个不常用另外,Server 层分为:
Server 基本类:原始的类,适合自己定制功能!McpServer基于Server 封装好了可以快速使用的方法!安装依赖用的是官方的 TypeScriptSDK:
仓库:https://github.com/modelcontextprotocol/typescript-sdk
官网:https://modelcontextprotocol.io
代码语言:javascript代码运行次数:0运行复制npm install @modelcontextprotocol/sdk axios
DeepSeek 没有官方 SDK,用的是 HTTP API,所以需要 axios!
记得把 API Key 放到 .env 或直接配置成环境变量,我用的 DEEPSEEK_API_KEY。
实现一个 McpServer我们先实现一个本地 McpServer,实现两个东西:
当前时间(资源)本地“知识库”搜索(工具)代码如下:
代码语言:javascript代码运行次数:0运行复制// src/server.jsimport { McpServer, ResourceTemplate,} from"@modelcontextprotocol/sdk/server/mcp.js";import { StdioServerTransport } from"@modelcontextprotocol/sdk/server/stdio.js";import { z } from"zod";const facts = ["公理1: 生存是文明的第一需要.","公理2: 文明不断增长和扩张,但宇宙中的物质总量保持不变.",].map((f) => f.toLowerCase());try {const server = new McpServer({ name: "mcp-cli-server", version: "1.0.0", });// 使用 Zod 定义工具的输入模式 server.tool( "search_local_database", { query: z.string(), }, async ({ query }) => { console.log("Tool called with query:", query); const queryTerms = query.toLowerCase().split(/\s+/); const results = facts.filter((fact) => queryTerms.some((term) => fact.includes(term)) ); return { content: [ { type: "text", text: results.length === 0 ? "未找到相关公理" : results.join(" "), }, ], }; } );// 定义资源 server.resource( "current_time", new ResourceTemplate("time://current", { list: undefined }), async (uri) => ({ contents: [{ uri: uri.href, text: newDate().toLocaleString() }], }) );await server.connect(new StdioServerTransport());console.log("Server is running...");} catch (err) {console.error("Server connection failed:", err);}
这样一来,我们的服务端就能通过 MCP 协议对外暴露两个上下文能力了。
配置 MCP ClientMCP 的客户端用来连接服务器并获取资源或调用工具:
代码语言:javascript代码运行次数:0运行复制// src/client.js;import { Client } from"@modelcontextprotocol/sdk/client/index.js";import { StdioClientTransport } from"@modelcontextprotocol/sdk/client/stdio.js";exportasyncfunction createClient() {const client = new Client({ name: "Demo", version: "1.0.0", });const transport = new StdioClientTransport({ command: "node", args: ["src/server.js"], });try { await client.connect(transport); console.log("Client connected successfully"); } catch (err) { console.error("Client connection failed:", err); throw err; }// 可选:添加客户端方法调用后的调试return client;}
连上之后,我们就可以开始调用服务端的资源和工具了。
获取上下文我们设定一个简单的逻辑:每次用户提问,客户端都会获取当前时间;如果问题里包含 公理,那就调用搜索工具查一下本地知识库:
代码语言:javascript代码运行次数:0运行复制async function getContext(client, question) {let currentTime = "";let additionalContext = "";try { const resources = await client.readResource( { uri: "time://current" }, { timeout: 15000 } ); // 增加超时时间 console.log("Resources response:", resources); currentTime = resources.contents[0]?.text || newDate().toLocaleString(); // 注意:resources 直接包含 contents } catch (err) { console.error("Resource read error:", err); currentTime = newDate().toLocaleString(); }if (question.toLowerCase().includes("公理")) { console.log("Searching for axioms...", question); try { const result = await client.getPrompt({ name: "search_local_database", arguments: { query: question }, }); console.log("Tool result:", result); additionalContext = result?.[0]?.text || "No results found."; } catch (err) { console.error("Tool call error:", err); additionalContext = "Error searching database."; } }return { currentTime, additionalContext };}
DeepSeek 使用的是标准 OpenAI 接口风格,HTTP POST 请求即可。这里我们用 axios 调用:
代码语言:javascript代码运行次数:0运行复制import axios from"axios";asyncfunction askLLM(prompt) {try { console.log("Calling LLM with prompt:", prompt); const res = await axios.post( "https://api.deepseek.com/chat/completions", { model: "deepseek-chat", messages: [{ role: "user", content: prompt }], max_tokens: 2048, stream: false, temperature: 0.7, }, { headers: { Authorization: `Bearer ${process.env.DEEPSEEK_API_KEY}`, "Content-Type": "application/json", }, timeout: 1000000, } ); console.log("LLM response:", res.data); return res.data.choices[0].message.content; } catch (err) { console.error("LLM error:", err); return"Error calling LLM."; }}
完整的代码,包含用命令行做一个简单的交互界面:
代码语言:javascript代码运行次数:0运行复制// src/index.jsimport readline from"readline";import axios from"axios";import { createClient } from"./client.js";import { DEEPSEEK_API_KEY } from"./config.js";asyncfunction askLLM(prompt) {try { console.log("Calling LLM with prompt:", prompt); const res = await axios.post( "https://api.deepseek.com/chat/completions", { model: "deepseek-chat", messages: [{ role: "user", content: prompt }], max_tokens: 2048, stream: false, temperature: 0.7, }, { headers: { Authorization: `Bearer ${DEEPSEEK_API_KEY}`, "Content-Type": "application/json", }, timeout: 1000000, } ); return res.data.choices[0].message.content; } catch (err) { console.error("LLM error:", err); return"Error calling LLM."; }}asyncfunction getContext(client, question) {let currentTime = "";let additionalContext = "";try { const resources = await client.readResource( { uri: "time://current" }, { timeout: 15000 } ); // 增加超时时间 currentTime = resources.contents[0]?.text || newDate().toLocaleString(); // 注意:resources 直接包含 contents } catch (err) { console.error("Resource read error:", err); currentTime = newDate().toLocaleString(); }if (question.toLowerCase().includes("公理")) { try { // const result = await client.getPrompt({ // name: "search_local_database", // arguments: { query: question }, // }); const toolResult = await client.callTool({ name: "search_local_database", arguments: { query: question }, }); console.log("Tool result:", toolResult); additionalContext = toolResult?.content?.[0]?.text || "No results found."; } catch (err) { console.error("Tool call error:", err); additionalContext = "Error searching database."; } }return { currentTime, additionalContext };}const rl = readline.createInterface({ input: process.stdin, output: process.stdout,});const client = await createClient();while (true) {const question = awaitnewPromise((resolve) => rl.question("You: ", resolve) );if (question.toLowerCase() === "exit") { console.log("Exiting..."); rl.close(); process.exit(0); }// 使用上下文const context = await getContext(client, question);// 不使用上下文// const context = {};const prompt = `Time: ${context.currentTime} Context: ${context.additionalContext} Q: ${question} A:`;console.log("Prompt:", prompt);const answer = await askLLM(prompt);console.log('Assistant:', answer);}
接着在终端运行:
代码语言:javascript代码运行次数:0运行复制# 启动服务器node src/server.js
# 启动客户端node src/index.js
运行结果:可以看到识别到关键字之后,答案更加集中在特定领域!

未命中关键词

命中了关键词
一些注意点这个项目虽然小,但也踩了些坑,顺便分享几点:
MCP SDK 的 server 和 client 都是异步启动的,别忘了加上 await connect()。工具的入参和 schema 必须严格匹配,否则会抛错。下面是我的目录结构,做个参考吧!
代码语言:javascript代码运行次数:0运行复制mcp-mini/├── package.json├── src/│ ├── client.js│ ├── server.js│ └── index.js
总的来说,MCP TypeScriptSDK 用起来还是挺顺的,适合做一些轻量、模块化、支持上下文的 AI 应用。这种服务 + 客户端 + LLM 的组合模式挺适合本地测试,也方便后续扩展别的服务。
今天的分享就到这了,如果文章中有啥错误,欢迎指正!
希望天晴下载这一宝藏平台能持续成为您探索数字世界的得力助手。未来若有任何需求或疑问,别忘了这里是您的首选解答站!