仓库代码:github.com/hahala2333/...
共有两个分支,以下实践代码在dev_QwQ-32B分支
概览
MCP,全称 Model Context Protocol(模型上下文协议),是一种由 Anthropic(Claude 母公司)于去年 11 月正式提出的技术协议,其核心目标是大幅提升大模型(Agent)在调用外部工具时的开发效率。
大模型本身主要依靠内部预训练获得的知识和语言生成能力,这就使它在没有外部工具的情况下,只能依赖内置的信息来对话,无法主动查询实时数据、执行复杂计算或调用特定服务。
Function Calling
Function Calling 是 OpenAI 提出的一种机制,这种机制一方面解决了大模型无法直接访问实时数据或外部资源的限制,另一方面也为构建更加智能和多功能的 agent 提供了基础架构。
目的是:
让大模型能够"决定"调用某个函数/工具,并生成适当的参数,让外部系统来执行这个函数,然后再把结果返回给大模型继续对话。
Function Calling 的工作机制可以分为以下几步:
- 定义函数:
开发者提前定义好模型可以调用的一些"函数",包括名称、参数、用途等(用 JSON Schema 来描述。JSON Schema 是大模型了解"有哪些工具"和"每个工具怎么用"的说明书。 - 大模型"选择"函数:
当用户输入一个请求后,大模型判断是否需要调用某个函数,并"自动生成"相应函数的名称和参数。大模型之所以能"决定"要用哪个函数,很大程度上就是靠 JSON Schema(函数描述)来进行意图匹配和参数构造的! - 外部系统执行函数:
模型本身不能执行这些函数,所以 Function Calling 的输出(函数名+参数)由你的程序或平台(比如 OpenAI 的 API 调用器)接收并实际执行这个函数,调用外部工具或服务。 - 把结果喂回给模型:
执行完函数后的结果会被再次输入给模型,模型再基于这个结果继续生成回答。
🧠 比喻一下:
把大模型想象成一个"聪明的助理",但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 出现的目的:
- 统一接口规范:
不同团队、不同模型、不同工具都在实现自己的"Function Calling",缺乏统一标准。MCP 提出一套开放、统一的协议,让所有调用具备"共通语言"。 - 支持更复杂的 Agent 场景:
多轮对话、状态追踪、多个工具协作、模型嵌套调用等,Function Calling 无法胜任,MCP 抽象了"上下文协议"来管理整个会话生命周期。 - 支持模型互换、平台互通:
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 的天气预警, 结果参考如图所示: