MCP 入门实践

仓库代码:github.com/hahala2333/...

共有两个分支,以下实践代码在dev_QwQ-32B分支

概览

MCP,全称 Model Context Protocol(模型上下文协议),是一种由 Anthropic(Claude 母公司)于去年 11 月正式提出的技术协议,其核心目标是大幅提升大模型(Agent)在调用外部工具时的开发效率。

大模型本身主要依靠内部预训练获得的知识和语言生成能力,这就使它在没有外部工具的情况下,只能依赖内置的信息来对话,无法主动查询实时数据、执行复杂计算或调用特定服务。

Function Calling

Function Calling 是 OpenAI 提出的一种机制,这种机制一方面解决了大模型无法直接访问实时数据或外部资源的限制,另一方面也为构建更加智能和多功能的 agent 提供了基础架构。

目的是:

让大模型能够"决定"调用某个函数/工具,并生成适当的参数,让外部系统来执行这个函数,然后再把结果返回给大模型继续对话。

Function Calling 的工作机制可以分为以下几步:

  1. 定义函数:
    开发者提前定义好模型可以调用的一些"函数",包括名称、参数、用途等(用 JSON Schema 来描述。JSON Schema 是大模型了解"有哪些工具"和"每个工具怎么用"的说明书。
  2. 大模型"选择"函数:
    当用户输入一个请求后,大模型判断是否需要调用某个函数,并"自动生成"相应函数的名称和参数。大模型之所以能"决定"要用哪个函数,很大程度上就是靠 JSON Schema(函数描述)来进行意图匹配和参数构造的!
  3. 外部系统执行函数:
    模型本身不能执行这些函数,所以 Function Calling 的输出(函数名+参数)由你的程序或平台(比如 OpenAI 的 API 调用器)接收并实际执行这个函数,调用外部工具或服务。
  4. 把结果喂回给模型:
    执行完函数后的结果会被再次输入给模型,模型再基于这个结果继续生成回答。

🧠 比喻一下:

把大模型想象成一个"聪明的助理",但ta不能动手,只能说话。

你给ta一个"工具箱",每个工具(函数)上贴有标签(功能说明)。

ta会在需要时说:"我想用这个工具,它需要这些材料(参数)。"

然后你(平台/程序)帮它执行,ta再根据结果给你回答。


举个 Function Calling 的例子

假设我们想让模型查天气,我们先定义一个"函数"叫 get_weather

json 复制代码
{
  "name": "get_weather",
  "description": "获取指定城市的天气",
  "parameters": {
    "type": "object",
    "properties": {
      "city": {
        "type": "string",
        "description": "城市名称"
      }
    },
    "required": ["city"]
  }
}

你输入:"帮我查一下北京今天的天气"

模型分析后生成:

json 复制代码
{
  "function_call": {
    "name": "get_weather",
    "arguments": {
      "city": "北京"
    }
  }
}

然后由你的程序调用真实的天气 API,拿到结果后再返回给模型,比如:

json 复制代码
{
  "city": "北京",
  "weather": "晴",
  "temperature": "20°C"
}

模型最后回复你:"今天北京晴,气温20度。"


MCP(Model Context Protocol)

MCP(Model Context Protocol) 是一种通信协议,用于定义模型(如大语言模型)与其外部环境(如用户、工具、系统状态)之间的标准化交互方式。

MCP 就像智能体系统的"中枢神经协议",它让模型能知道有哪些工具可用、如何调用、如何理解返回的结果,并保持整个思维链的连续性。

✅ MCP 出现的目的:

  1. 统一接口规范:
    不同团队、不同模型、不同工具都在实现自己的"Function Calling",缺乏统一标准。MCP 提出一套开放、统一的协议,让所有调用具备"共通语言"。
  2. 支持更复杂的 Agent 场景:
    多轮对话、状态追踪、多个工具协作、模型嵌套调用等,Function Calling 无法胜任,MCP 抽象了"上下文协议"来管理整个会话生命周期。
  3. 支持模型互换、平台互通:
    MCP 让模型成为一个"可插拔组件",不同平台上的模型可以共用一套调用机制和工具集。

概念

MCP 是一种开放协议,它标准化了应用程序向 LLM 提供上下文的方式。可以将 MCP 视为 AI 应用程序的 USB-C 端口。正如 USB-C 提供了一种将设备连接到各种外围设备和配件的标准化方式一样,MCP 提供了一种将 AI 模型连接到不同数据源和工具的标准化方式。

架构

MCP 的核心遵循客户端-服务器架构,其中主机应用程序可以连接到多个服务器

  • MCP 主机(MCP Hosts):希望通过 MCP 访问数据的 Claude Desktop、IDE 或 AI 工具等程序
  • MCP 客户端(MCP Clients):与服务器保持 1:1 连接的协议客户端, 把 MCP Server 提供的能力(函数/工具)暴露给大模型选择,当模型决定调用工具时,它负责把调用请求发送给 MCP Server,再把结果返回给模型
  • MCP 服务器(MCP Servers) :轻量级程序,每个程序都通过标准化的 Model Context Protocol 公开特定功能,比如:
    • Server A → 访问本地数据库 A(CSV、SQLite 等)
    • Server B → 访问本地文件、插件等
    • Server C → 访问远程 Web API(比如搜索引擎、天气、网页分析等)

举例:

json 复制代码
sequenceDiagram
    participant User
    participant LLM
    participant MCP Client
    participant MCP Server
    participant Weather API

    User->>LLM: 查一下北京天气
    LLM->>MCP Client: function_call = get_weather(city="北京")
    MCP Client->>MCP Server: 调用 get_weather(city="北京")
    MCP Server->>Weather API: 请求天气数据(调用外部 API)
    Weather API-->>MCP Server: 返回天气数据(JSON)
    MCP Server-->>MCP Client: 返回执行结果
    MCP Client-->>LLM: 调用结果(插入模型上下文)
    LLM-->>User: 北京今天晴,气温20°C

MCP Client 实践

开始构建可以与所有 MCP 服务器集成的自己的客户端。

本次实践使用的是 Qwen/QwQ-32B 当然其他支持Tool Calling的模型也可以

json 复制代码
#Create MCP文件    
mkdir MCP    
cd MCP    

# Create project directory    
mkdir mcp-client-typescript    
cd mcp-client-typescript    

# Initialize npm project    
npm init -y    

# Install dependencies    
npm install openai @modelcontextprotocol/sdk dotenv    

# Install dev dependencies    
npm install -D @types/node typescript    

# Create source file    
touch index.ts    

**将 package.json更新为 设置****type: "module"**和build:

javascript 复制代码
{
  "type": "module",
    "scripts": {
    "build": "tsc && chmod 755 build/index.js"
  }
}

在项目的根目录中创建 ****tsconfig.json

json 复制代码
{
  "compilerOptions": {  
    "target": "ES2022",  
    "module": "Node16",  
    "moduleResolution": "Node16",  
    "outDir": "./build",  
    "rootDir": "./",  
    "strict": true,  
    "esModuleInterop": true,  
    "skipLibCheck": true,  
    "forceConsistentCasingInFileNames": true  
  },
  "include": ["index.ts"],  
  "exclude": ["node_modules"]  
}

创建一个 .env 文件来存储变量

javascript 复制代码
OPENAI_API_KEY=''  
BASE_URL=''  
MODEL=''  

在MCP文件夹下创建一个 .gitignore 文件

javascript 复制代码
.env
node_modules  

核心代码 index.ts负责:

  • 连接 MCP 工具服务器(支持 .js 或 .py)
  • 自动注册并列出支持的工具
  • 与 OpenAI 交互,自动触发工具调用
  • 基于工具结果生成最终对话内容
json 复制代码
// 引入所需模块
import { Client } from "@modelcontextprotocol/sdk/client/index.js"; // MCP 客户端核心类
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; // 用于通过 stdio 通信的传输层
import dotenv from "dotenv"; // 用于加载 .env 配置文件
import { OpenAI } from "openai"; // OpenAI 官方 SDK
import { ChatCompletionMessageParam } from "openai/resources/chat/completions"; // 聊天消息类型
import readline from "readline/promises"; // 用于命令行交互

// 加载环境变量
dotenv.config();

// 读取环境变量
const OPENAI_API_KEY = process.env.OPENAI_API_KEY!;
const BASE_URL = process.env.BASE_URL || undefined;
const MODEL = process.env.MODEL || "Qwen/QwQ-32B";

// 如果未设置 OpenAI API Key,则报错
if (!OPENAI_API_KEY) {
  throw new Error("❌ 请在 .env 文件中设置 OPENAI_API_KEY");
}

// MCP + OpenAI 客户端类
class MCPClient {
  mcp: Client; // MCP 客户端实例
  openai: OpenAI; // OpenAI 客户端实例
  transport: any = null; // 通信传输层
  tools: Array<{ name: string; description: string; input_schema: any }> = []; // 工具列表

  constructor() {
    // 初始化 MCP 客户端
    this.mcp = new Client({ name: "mcp-client-openai", version: "1.0.0" });

    // 初始化 OpenAI 客户端
    this.openai = new OpenAI({
      apiKey: OPENAI_API_KEY,
      baseURL: BASE_URL,
    });
  }

  // 连接 MCP 服务器(支持 .js 或 .py)
  async connectToServer(serverScriptPath: string) {
    const isJs = serverScriptPath.endsWith(".js");
    const isPy = serverScriptPath.endsWith(".py");

    if (!isJs && !isPy) {
      throw new Error("❌ 服务器脚本必须是 .js 或 .py 文件");
    }

    // 判断平台,选择启动命令(Windows 用 python,Unix 用 python3)
    const command = isPy
      ? process.platform === "win32"
        ? "python"
        : "python3"
      : process.execPath; // 如果是 JS 脚本,则使用 Node 运行

    // 启动服务器脚本,并建立 stdio 通信
    this.transport = new StdioClientTransport({
      command,
      args: [serverScriptPath],
    });

    // 使用 MCP 客户端连接服务器
    this.mcp.connect(this.transport);

    // 获取服务器暴露的工具信息
    const toolsResult = await this.mcp.listTools();
    this.tools = toolsResult.tools.map((tool: any) => ({
      name: tool.name,
      description: tool.description,
      input_schema: tool.inputSchema,
    }));

    console.log(
      "✅ 已连接服务器,支持工具:",
      this.tools.map((t) => t.name)
    );
  }

  // 处理用户提问
  async processQuery(query: string): Promise<string> {
    const messages: ChatCompletionMessageParam[] = [
      { role: "system", content: "你是一个智能助手。" },
      { role: "user", content: query },
    ];

    // 构造 tools 数组,供 OpenAI 调用
    const tools: any = this.tools.map((tool) => ({
      type: "function",
      function: {
        name: tool.name,
        description: tool.description,
        parameters: tool.input_schema,
        strict: false, // Qwen 特有字段,OpenAI 可忽略
      },
    }));

    try {
      // 第一次请求 OpenAI,看是否需要调用工具
      const response = await this.openai.chat.completions.create({
        model: MODEL,
        messages,
        tools,
        tool_choice: "auto", // 让模型自动决定是否调用工具
        max_tokens: 1000,
        temperature: 0.7,
      });

      const message = response.choices[0].message;

      // 如果模型返回了工具调用请求
      if (message.tool_calls?.length) {
        const toolCall = message.tool_calls[0];
        const toolName = toolCall.function.name;
        const toolArgs = JSON.parse(toolCall.function.arguments);

        console.log(`\n🔧 调用工具:${toolName}`);
        console.log(`📦 参数:${JSON.stringify(toolArgs)}`);

        // 使用 MCP 调用本地工具
        const toolResult = await this.mcp.callTool({
          name: toolName,
          arguments: toolArgs,
        });

        // 获取工具的文本返回结果
        const resultText =
          (toolResult as any)?.content?.[0]?.text ?? "[工具无结果返回]";

        // 把调用过程加入对话上下文
        messages.push({
          role: "assistant",
          content: null,
          tool_calls: [toolCall],
        });

        messages.push({
          role: "tool",
          tool_call_id: toolCall.id,
          content: resultText,
        });

        // 再次向 OpenAI 发送请求,获取基于工具结果的最终回复
        const finalResponse = await this.openai.chat.completions.create({
          model: MODEL,
          messages,
          max_tokens: 1000,
        });

        return finalResponse.choices[0].message?.content || "[无返回内容]";
      }

      // 如果没有工具调用,直接返回模型回复
      return message?.content || "[无返回内容]";
    } catch (err: any) {
      return `❌ OpenAI 请求出错: ${err.message}`;
    }
  }

  // 启动命令行对话循环
  async chatLoop() {
    const rl = readline.createInterface({
      input: process.stdin,
      output: process.stdout,
    });

    console.log("✅ MCP Client 启动完成");
    console.log("💬 输入你的问题,或输入 'quit' 退出");

    while (true) {
      const input = await rl.question("\nQuery: ");
      if (input.toLowerCase() === "quit") break;

      const result = await this.processQuery(input);
      console.log("\n🧠 回复:\n" + result);
    }

    rl.close();
  }

  // 关闭 MCP 连接
  async cleanup() {
    await this.mcp.close();
  }
}

// 主函数入口
async function main() { 
  // 检查是否提供了服务器脚本路径
  if (process.argv.length < 3) { 
    console.log("用法: node build/index.js <path_to_server_script>");

    return; 
  }

  const mcpClient = new MCPClient(); 

  try {
    // 连接 MCP 工具服务器
    await mcpClient.connectToServer(process.argv[2]); 
    // 启动交互循环

    await mcpClient.chatLoop(); 
  } finally { 
    // 清理并退出


    await mcpClient.cleanup();  
    process.exit(0);  
  }
}

// 启动程序


main();  

打包****客户端

json 复制代码
npm run build 

以 GitHub官方发布的 MCP Server 示例验证Client

json 复制代码
# 在MCP文件夹下   
git clone [email protected]:modelcontextprotocol/servers.git 
#进入github文件夹
cd servers/src/github 
npm install
#打包
npm run build 
# 进入mcp-client-typescript文件夹

cd mcp-client-typescript 
#执行
node build/index.js ../servers/src/github/dist/index.js 

问题:搜索包含 "docker" 的 GitHub 仓库,按关注数排序,结果参考如图所示

MCP Server 实践

开始构建您自己的服务器,以便在客户端中使用。

在本次,我们将构建一个简单的 MCP 天气服务器并将其连接到客户端。

json 复制代码
# Create a new directory for our project
mkdir weather
cd weather

# Initialize a new npm project
npm init -y

# Install dependencies
npm install @modelcontextprotocol/sdk zod
npm install -D @types/node typescript

# Create our files
mkdir src
touch src/index.ts

**将 package.json更新为 设置****type: "module"**和build:

json 复制代码
{
  "type": "module",
  "bin": {
    "weather": "./build/index.js"
  },
  "scripts": {
    "build": "tsc && chmod 755 build/index.js"
  },
  "files": [
    "build"
  ],
}

在项目的根目录中创建 ****tsconfig.json

json 复制代码
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

创建src/index.ts

这段代码是一个 MCP 天气工具服务端,基于 美国国家气象局(NWS)API 实现,暴露了两个工具:

  • get-alerts: 根据州名(两位缩写)获取天气警报
  • get-forecast: 根据经纬度获取天气预报
json 复制代码
// 引入 MCP Server 和 Stdio 传输模块
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod"; // 用于参数校验的库

// 设置 NWS(美国国家气象局)API 基础信息
const NWS_API_BASE = "https://api.weather.gov";
const USER_AGENT = "weather-app/1.0";

// 创建 MCP 服务器实例
const server = new McpServer({
  name: "weather",        // 工具服务名称
  version: "1.0.0",       // 版本
  capabilities: {
    resources: {},        // 无资源
    tools: {},            // 工具将在后续注册
  },
});

// 通用请求封装:调用 NWS API 接口并解析 JSON 返回
async function makeNWSRequest<T>(url: string): Promise<T | null> {
  const headers = {
    "User-Agent": USER_AGENT,
    Accept: "application/geo+json",
  };

  try {
    const response = await fetch(url, { headers });
    if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
    return (await response.json()) as T;
  } catch (error) {
    console.error("Error making NWS request:", error);
    return null;
  }
}

// 定义 NWS 返回的告警数据结构
interface AlertFeature {
  properties: {
    event?: string;
    areaDesc?: string;
    severity?: string;
    status?: string;
    headline?: string;
  };
}

// 格式化单条警报内容为字符串
function formatAlert(feature: AlertFeature): string {
  const props = feature.properties;
  return [
    `Event: ${props.event || "Unknown"}`,
    `Area: ${props.areaDesc || "Unknown"}`,
    `Severity: ${props.severity || "Unknown"}`,
    `Status: ${props.status || "Unknown"}`,
    `Headline: ${props.headline || "No headline"}`,
    "---",
  ].join("\n");
}

// 定义接口返回类型
interface ForecastPeriod {
  name?: string;
  temperature?: number;
  temperatureUnit?: string;
  windSpeed?: string;
  windDirection?: string;
  shortForecast?: string;
}
interface AlertsResponse {
  features: AlertFeature[];
}
interface PointsResponse {
  properties: { forecast?: string };
}
interface ForecastResponse {
  properties: { periods: ForecastPeriod[] };
}

// 工具 1️⃣:根据州名获取天气警报
server.tool(
  "get-alerts",
  "Get weather alerts for a state",
  {
    state: z.string().length(2).describe("Two-letter state code (e.g. CA, NY)"),
  },
  async ({ state }) => {
    const stateCode = state.toUpperCase();
    const alertsUrl = `${NWS_API_BASE}/alerts?area=${stateCode}`;
    const alertsData = await makeNWSRequest<AlertsResponse>(alertsUrl);

    if (!alertsData) {
      return { content: [{ type: "text", text: "Failed to retrieve alerts data" }] };
    }

    const features = alertsData.features || [];
    if (features.length === 0) {
      return { content: [{ type: "text", text: `No active alerts for ${stateCode}` }] };
    }

    const formattedAlerts = features.map(formatAlert);
    const alertsText = `Active alerts for ${stateCode}:\n\n${formattedAlerts.join("\n")}`;

    return { content: [{ type: "text", text: alertsText }] };
  }
);

// 工具 2️⃣:根据经纬度获取天气预报
server.tool(
  "get-forecast",
  "Get weather forecast for a location",
  {
    latitude: z.number().min(-90).max(90).describe("Latitude of the location"),
    longitude: z.number().min(-180).max(180).describe("Longitude of the location"),
  },
  async ({ latitude, longitude }) => {
    // 先获取当前坐标所对应的 forecast URL
    const pointsUrl = `${NWS_API_BASE}/points/${latitude.toFixed(4)},${longitude.toFixed(4)}`;
    const pointsData = await makeNWSRequest<PointsResponse>(pointsUrl);

    if (!pointsData) {
      return {
        content: [
          {
            type: "text",
            text: `Failed to retrieve grid point data for coordinates: ${latitude}, ${longitude}. This location may not be supported by the NWS API (only US locations are supported).`,
          },
        ],
      };
    }

    const forecastUrl = pointsData.properties?.forecast;
    if (!forecastUrl) {
      return {
        content: [
          {
            type: "text",
            text: "Failed to get forecast URL from grid point data",
          },
        ],
      };
    }

    // 请求天气预报数据
    const forecastData = await makeNWSRequest<ForecastResponse>(forecastUrl);
    if (!forecastData) {
      return {
        content: [{ type: "text", text: "Failed to retrieve forecast data" }],
      };
    }

    const periods = forecastData.properties?.periods || [];
    if (periods.length === 0) {
      return { content: [{ type: "text", text: "No forecast periods available" }] };
    }

    // 格式化天气预报信息
    const formattedForecast = periods.map((period: ForecastPeriod) =>
      [
        `${period.name || "Unknown"}:`,
        `Temperature: ${period.temperature || "Unknown"}°${period.temperatureUnit || "F"}`,
        `Wind: ${period.windSpeed || "Unknown"} ${period.windDirection || ""}`,
        `${period.shortForecast || "No forecast available"}`,
        "---",
      ].join("\n")
    );

    const forecastText = `Forecast for ${latitude}, ${longitude}:\n\n${formattedForecast.join("\n")}`;

    return { content: [{ type: "text", text: forecastText }] };
  }
);

// 主函数:通过 stdio 启动 MCP 服务器
async function main() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("Weather MCP Server running on stdio");
}

main().catch((error) => {
  console.error("Fatal error in main():", error);
  process.exit(1);
});

打包MCP Servers

json 复制代码
npm run build

MCP 客户端调用

json 复制代码
cd mcp-client-typescript
node build/index.js ../weather/build/index.js

问题:请使用 get-alerts 工具查询 CA 的天气预警, 结果参考如图所示:

产考文档

modelcontextprotocol.io/quickstart/...

github.com/modelcontex...

相关推荐
355984268550559 小时前
医保服务平台 Webpack逆向
前端·webpack·node.js
不能只会打代码12 小时前
六十天前端强化训练之第三十一天之Webpack 基础配置 大师级讲解(接下来几天给大家讲讲工具链与工程化)
前端·webpack·node.js
还是鼠鼠15 小时前
Node.js 路由 - 初识 Express 中的路由
前端·vscode·前端框架·npm·node.js·express
神影天初16 小时前
安装node,配置npm, yarn, pnpm, bun
node.js
9527!到!16 小时前
nvm 命令的实际意义讲解
npm·node.js
QC七哥17 小时前
picgo的vscode插件支持easyimage图床
node.js·visual studio code
知否技术1 天前
Node登陆认证实战!10分钟手把手教会你!
后端·node.js
斯~内克1 天前
深入探索Node.js Koa框架:构建现代化Web应用的2000字实践指南
前端·node.js
mercyT1 天前
npm i 出现的网络问题
前端·npm·node.js