🚀🚀🚀MCP SDK 快速接入 DeepSeek 并添加工具!万万没想到MCP这么简单好用!

前言

本文主要是纠正上篇文章的错误地方,补全了一些后续的步骤!感谢各位同学的指正,十分感谢🙏!

这篇文章记录一下我用 MCP TypeScript SDK 实现一个自包含的 AI 聊天应用的过程:内部包含 MCP 服务器提供上下文,客户端拿上下文再去调 LLM 接口拿回答!

往期精彩推荐

正文

MCP 是什么?

简单说,MCP 是一个给 AI 应用提供上下文的标准协议。你可以把它理解成一个服务标准,它规定了"资源"和"工具"的接口规范,然后通过客户端连接这些接口,就可以组合出丰富的上下文数据。比如说资源可以是"当前时间"、"用户历史记录",工具可以是"数据库搜索"、"调用外部 API"。

它采用的是客户端-服务器架构,Server 暴露上下文能力,Client 拉取这些上下文,再拿去调语言模型生成回答,而 Transport 负责 ServerClient 的通信部分!

(AI 帮我画的图)

其中图片中的 Transport 层还分为:

  • StdioServerTransport:用于 CLI 工具对接 stdin/stdout
  • SSEServerTransport:用于HTTP通信
  • StdioClientTransport:客户端以子进程方式拉起服务端,这个不常用

另外,Server 层分为:

  • Server 基本类:原始的类,适合自己定制功能!
  • McpServer基于Server 封装好了可以快速使用的方法!

注意:基本类和封装类的接口有很大不同,具体请参看 README 文件!

安装依赖

用的是官方的 TypeScript SDK

仓库:github.com/modelcontex...

官网:modelcontextprotocol.io

bash 复制代码
npm install @modelcontextprotocol/sdk axios

DeepSeek 没有官方 SDK,用的是 HTTP API,所以需要 axios

记得把 API Key 放到 .env 或直接配置成环境变量,我用的 DEEPSEEK_API_KEY

实现一个 McpServer

我们先实现一个本地 McpServer,实现两个东西:

  • 当前时间(资源)
  • 本地"知识库"搜索(工具)

代码如下:

ts 复制代码
// src/server.js
import {
  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("\n"),
          },
        ],
      };
    }
  );


  // 定义资源
  server.resource(
    "current_time",
    new ResourceTemplate("time://current", { list: undefined }),
    async (uri) => ({
      contents: [{ uri: uri.href, text: new Date().toLocaleString() }],
    })
  );

  await server.connect(new StdioServerTransport());
  console.log("Server is running...");
} catch (err) {
  console.error("Server connection failed:", err);
}

这样一来,我们的服务端就能通过 MCP 协议对外暴露两个上下文能力了。

配置 MCP Client

MCP 的客户端用来连接服务器并获取资源或调用工具:

ts 复制代码
// src/client.js;
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

export async function 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;
}

连上之后,我们就可以开始调用服务端的资源和工具了。

获取上下文

我们设定一个简单的逻辑:每次用户提问,客户端都会获取当前时间;如果问题里包含 公理,那就调用搜索工具查一下本地知识库:

ts 复制代码
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 ||
      new Date().toLocaleString(); // 注意:resources 直接包含 contents
  } catch (err) {
    console.error("Resource read error:", err);
    currentTime = new Date().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,开始问答

DeepSeek 使用的是标准 OpenAI 接口风格,HTTP POST 请求即可。这里我们用 axios 调用:

js 复制代码
import axios from "axios";

async function 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.";
  }
}

完整的代码,包含用命令行做一个简单的交互界面:

ts 复制代码
// src/index.js
import readline from "readline";
import axios from "axios";
import { createClient } from "./client.js";
import { DEEPSEEK_API_KEY } from "./config.js";

async function 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.";
  }
}

async function getContext(client, question) {
  let currentTime = "";
  let additionalContext = "";

  try {
    const resources = await client.readResource(
      { uri: "time://current" },
      { timeout: 15000 }
    ); // 增加超时时间
    currentTime = resources.contents[0]?.text || new Date().toLocaleString(); // 注意:resources 直接包含 contents
  } catch (err) {
    console.error("Resource read error:", err);
    currentTime = new Date().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 = await new Promise((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}\nContext: ${context.additionalContext}\nQ: ${question}\nA:`;
  console.log("Prompt:", prompt);
  const answer = await askLLM(prompt);
  console.log('Assistant:', answer);
}

接着在终端运行:

bash 复制代码
# 启动服务器
node src/server.js
bash 复制代码
# 启动客户端
node src/index.js

运行结果:

一些注意点

这个项目虽然小,但也踩了些坑,顺便分享几点:

  • MCP SDK 的 server 和 client 都是异步启动的,别忘了加上 await connect()
  • 工具的入参和 schema 必须严格匹配,否则会抛错。

下面是我的目录结构,做个参考吧!

css 复制代码
mcp-mini/
├── package.json
├── src/
│   ├── client.js
│   ├── server.js
│   └── index.js

最后

总的来说,MCP TypeScript SDK 用起来还是挺顺的,适合做一些轻量、模块化、支持上下文的 AI 应用。这种服务 + 客户端 + LLM 的组合模式挺适合本地测试,也方便后续扩展别的服务。

今天的分享就到这了,如果文章中有啥错误,欢迎指正!

往期精彩推荐

相关推荐
Kx…………4 分钟前
Day2—3:前端项目uniapp壁纸实战
前端·css·学习·uni-app·html
BillKu2 小时前
Vue3 + TypeScript中provide和inject的用法示例
javascript·vue.js·typescript
培根芝士2 小时前
Electron打包支持多语言
前端·javascript·electron
Baoing_2 小时前
Next.js项目生成sitemap.xml站点地图
xml·开发语言·javascript
mr_cmx2 小时前
Nodejs数据库单一连接模式和连接池模式的概述及写法
前端·数据库·node.js
东部欧安时2 小时前
研一自救指南 - 07. CSS面向面试学习
前端·css
沉默是金~3 小时前
Vue+Notification 自定义消息通知组件 支持数据分页 实时更新
javascript·vue.js·elementui
涵信3 小时前
第十二节:原理深挖-React Fiber架构核心思想
前端·react.js·架构
在下千玦3 小时前
#去除知乎中“盐选”付费故事
javascript
ohMyGod_1233 小时前
React-useRef
前端·javascript·react.js