🚀 MCP协议深度解析:从代码出发,理解AI工具的"万能接口"

32 阅读6分钟

🚀 MCP协议深度解析:从代码出发,理解AI工具的"万能接口"

别急着造火箭,我们先把手上的螺丝刀研究透 🔧

🤔 开篇:一个直击灵魂的问题

假设你是个AI大模型,用户问你:

"帮我查一下用户001的信息"

你知识渊博,但你对这个"数据库"一无所知。怎么办?

传统做法是:开发者给你写个函数,你调用它。

// 传统方案:硬编码
function queryUser(userId) {
    return database.users[userId];  // 只能在这个项目用
}

问题来了

  1. ❌ 换个项目,这个函数就废了(项目绑定
  2. ❌ Node写的工具,Python项目怎么用?(语言绑定
  3. ❌ 工具和AI在同一个进程,想分开部署?没门!(进程绑定

这就好比你有个万能工具箱,但每个工具都焊死在特定的工作台上——换个地方干活就得重买工具!


💡 MCP:给AI的"USB-C接口"

MCP (Model Context Protocol) 就是来解决这个问题的。它让AI和工具之间有了标准化的通信协议,就像USB-C一样——不管是什么设备,插上就能用!

MCP的三大核心特点

graph LR
    A[AI Agent] -->|MCP协议| B{传输方式}
    B -->|本地| C[stdio<br/>标准输入输出]
    B -->|远程| D[HTTP<br/>网络通信]
    C --> E[Node工具]
    C --> F[Python工具]
    C --> G[Java工具]
    D --> H[云端工具]
    
    style A fill:#4A90D9,color:#fff
    style B fill:#FF6B6B,color:#fff
    style C fill:#4ECDC4,color:#fff
    style D fill:#45B7D1,color:#fff

关键理解

  • MCP不是API调用:API是拿数据,MCP是扩展能力(让AI能做事)
  • MCP不是函数库:函数库要引入代码,MCP是跨进程调用
  • MCP是协议:就像HTTP一样,谁实现都能通信

🔍 深度解剖:MCP代码

第一步:创建MCP服务器

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';

这三行导入了什么?

导入作用通俗理解
McpServerMCP服务器核心类工具箱本体
StdioServerTransportstdio传输层通过"对讲机"(标准输入输出)通信
z (zod)参数校验工具检查工具收到的"指令"是否合规

第二步:准备数据

const database = {
    users: {
        '001': {id: '001', name: '张三', email: '111@qq.com', role: 'admin'},
        '002': {id: '002', name: '李四', email: '222@qq.com', role: 'user'},
        '003': {id: '003', name: '王五', email: '333@qq.com', role: 'user'}
    }
}

💡 注意:这里是模拟数据,实际项目可以换成:

  • 真实数据库(MySQL/PostgreSQL)
  • 第三方API(GitHub/Notion)
  • 本地文件系统
  • 甚至另一个MCP服务器!

第三步:实例化服务器

const server = new McpServer({
    name: 'my-mcp-server',
    version: '1.0.0',
});

关键理解

  • name:服务器的唯一标识,客户端通过它来识别
  • version:版本号,便于升级管理
  • 这个对象就像一个工具箱的铭牌,告诉别人"我是谁"

第四步:注册工具(核心!)

server.registerTool(
    'query_user',  // ① 工具名称
    {              // ② 工具描述和参数定义
        description: '查询数据库中的用户信息。输入用户ID,返回该用户详细信息(姓名、邮箱、角色)',
        inputSchema: z.object({
            id: z.string().describe('用户ID, 例如: 001, 002, 003'),
        }),
    },
    async ({ id }) => {  // ③ 工具执行函数(注意:这里是解构参数!)
        // 业务逻辑...
    }
);
🎯 深入理解这三个部分

① 工具名称 'query_user'

  • 客户端调用时的唯一标识
  • 命名规范:动词+名词,如 get_user, create_order, search_product
  • 可以理解为工具的"身份证号"

② 工具描述(元数据)

{
    description: '...',  // 给AI看的!AI根据这个决定是否调用
    inputSchema: z.object({...})  // 参数校验,防止AI"胡言乱语"
}

重点理解 description

  • 这不是给程序员看的注释!
  • 这是给LLM看的工具说明
  • LLM会根据你的描述判断"什么时候该用这个工具"
  • 写得好:AI精准调用 🎯
  • 写得差:AI乱调用或不用 ❌

③ 执行函数

async ({ id }) => {  // 注意:参数是一个对象,需要解构
    const user = database.users[id];
    if (!user) {  // 用户不存在
        return {
            content: [
                {
                    type: 'text', 
                    text: `用户${id}不存在。可用ID: 001, 002, 003`
                }
            ]
        }
    }
    // 返回固定格式的结果
    return {
        content: [
            {
                type: 'text',
                text: `用户${id}的信息如下:姓名: ${user.name} 邮箱: ${user.email} 角色: ${user.role}`
            }
        ]
    };
}

⚠️ 常见错误:很多初学者会写成 async (id) =>,但正确写法是 async ({ id }) =>,因为参数是以对象形式传入的!

返回值格式解析

{
    content: [          // 内容数组,可以包含多种类型
        {
            type: 'text',  // 文本类型(也支持 image, audio 等)
            text: `用户${id}的信息如下:姓名: ${user.name} 邮箱: ${user.email} 角色: ${user.role}`
        }
    ]
}

这个格式是MCP协议规定的,所有工具都必须遵守!

第五步:启动服务器

const transport = new StdioServerTransport();
await server.connect(transport);
🎯 这行代码到底做了什么?

它让你的MCP服务器准备好通过stdio通信

  1. StdioServerTransport():创建一个stdio传输通道
  2. server.connect(transport):将服务器连接到这个通道
  3. 服务器开始监听 process.stdin(标准输入)
  4. 准备通过 process.stdout(标准输出)发送响应

就像:你打开对讲机,调到"stdio频道",然后说"我准备好了,谁呼叫我都能应答"📻


🔄 完整的通信流程

我的代码(作为子进程)

// server.js - 这是你的MCP服务器代码
const transport = new StdioServerTransport();
await server.connect(transport);

这段代码的作用

  1. 创建一个stdio传输通道
  2. 让服务器监听 process.stdin(等待指令)
  3. 准备通过 process.stdout(返回结果)
  4. 你的程序现在是一个"被调用方"

谁来调用你?(客户端视角)

// client.js - 这是另一个文件(AI Agent)
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';

const transport = new StdioClientTransport({
    command: 'node',           // 执行命令
    args: ['server.js']        // 你的文件
});

await client.connect(transport);

这段代码的作用

  1. 通过 child_process.spawn() 创建子进程
  2. 子进程运行的就是你的 server.js
  3. 通过子进程的 stdin 发送请求
  4. 通过子进程的 stdout 接收响应

📊 完整的通信流程图

sequenceDiagram
    participant Client as AI Agent (父进程)
    participant Server as MCP Server (子进程)
    
    Client->>Server: child_process.spawn('node server.js')
    activate Server
    Note over Server: 启动,监听stdin
    
    Client->>Server: 写入stdin: 调用query_user
    Note over Server: 解析请求
    Server->>Server: 执行query_user逻辑
    Server->>Client: 写入stdout: 返回结果
    deactivate Server
    
    Note over Client: 解析结果,返回给用户

💡 关键理解:为什么是"stdio"?

stdio的三条"管道"

stdin  ← 父进程写,子进程读 (请求)
stdout → 子进程写,父进程读 (响应)
stderr → 子进程写,父进程读 (错误日志)

好处

  1. 零依赖:任何编程语言都支持标准输入输出
  2. 简单可靠:不需要配置端口、IP
  3. 进程隔离:工具崩溃不影响AI主进程
  4. 调试方便:可以手动输入测试

🎯 代码中的"精髓"

1. Zod参数校验

inputSchema: z.object({
    id: z.string().describe('用户ID, 例如: 001, 002, 003'),
})
  • 为什么需要校验? AI可能会"胡说八道"传错参数
  • Zod的作用:在工具执行前就拦截错误参数
  • describe:告诉AI这个参数应该传什么

2. 错误处理

if (!user) {
    return {
        content: [
            {
                type: 'text',
                text: `用户${id}不存在。可用ID: 001, 002, 003`
            }
        ]
    };
}

优雅的错误返回

  • 不要 throw error(会中断通信)
  • 返回友好的错误信息(AI能理解并告诉用户)
  • 提供可用选项(AI可以重新尝试)

3. 返回格式规范

return {
    content: [
        {
            type: 'text',
            text: `用户${id}的信息如下:姓名: ${user.name} ...`
        }
    ]
};
  • 统一格式:所有工具返回同样结构
  • 内容数组:支持返回多个内容(文本、图片等)
  • AI友好:返回的文本AI能直接理解

🔧 扩展思考:工具还能怎么改进?

改进1:支持批量查询

server.registerTool(
    'query_users_batch',
    {
        description: '批量查询用户信息,一次查询多个用户',
        inputSchema: z.object({
            ids: z.array(z.string()).describe('用户ID数组,例如: ["001", "002"]')
        })
    },
    async ({ ids }) => {
        const results = ids.map(id => database.users[id]).filter(Boolean);
        if (results.length === 0) {
            return {
                content: [{ type: 'text', text: '没有找到任何用户' }]
            };
        }
        return {
            content: [{
                type: 'text',
                text: `找到 ${results.length} 个用户:\n${results.map(u => `${u.id}: ${u.name}(${u.role})`).join('\n')}`
            }]
        };
    }
);

改进2:添加缓存

const cache = new Map();

async ({ id }) => {
    if (cache.has(id)) {
        return {
            content: [{ 
                type: 'text', 
                text: `[缓存] ${cache.get(id)}` 
            }]
        };
    }
    const user = database.users[id];
    if (user) {
        const userStr = `姓名: ${user.name} 邮箱: ${user.email} 角色: ${user.role}`;
        cache.set(id, userStr);
        return {
            content: [{ type: 'text', text: userStr }]
        };
    }
    // ... 错误处理
}

改进3:支持多种输出格式

return {
    content: [
        {
            type: 'text',
            text: `用户信息:${JSON.stringify(user)}`
        },
        {
            type: 'resource',  // 也可以是 image, audio 等
            resource: {
                uri: `data:user/${id}`,
                mimeType: 'application/json',
                text: JSON.stringify(user)
            }
        }
    ]
};

改进4:添加工具执行日志

async ({ id }) => {
    console.error(`[${new Date().toISOString()}] 查询用户: ${id}`); // stderr输出
    
    // ... 业务逻辑
    
    console.error(`[${new Date().toISOString()}] 查询完成`);
    return { content: [...] };
}

🆚 MCP vs 传统方式:本质区别

传统方式(函数调用)

// 在同一进程内
function queryUser(id) { return database.users[id]; }
const result = queryUser('001');  // 直接调用

❌ 必须在同一进程
❌ 必须用同一种语言
❌ 工具和业务耦合

MCP方式(协议调用)

// 跨进程,跨语言
// Node.js 写的工具
server.registerTool('query_user', ...);

// 任何语言写的客户端都能调用
// Python客户端:
# client.call_tool('query_user', {'id': '001'})
// Java客户端:
// client.callTool("query_user", Map.of("id", "001"))

✅ 可以跨进程
✅ 可以跨语言
✅ 工具独立部署


🌟 让代码"活"起来:实际运行流程

1. 启动MCP服务器(调试模式)

# 可以直接运行测试
node my-mcp-server.js
# 服务器启动,等待stdin输入

2. 手动测试(通过stdio)

# 在另一个终端,向stdin发送JSON请求
echo '{"method":"call_tool","params":{"name":"query_user","arguments":{"id":"001"}}}' | node my-mcp-server.js

3. AI Agent连接服务器

// AI Agent(客户端)
const transport = new StdioClientTransport({
    command: 'node',
    args: ['my-mcp-server.js']
});
await client.connect(transport);

4. AI调用工具

// AI看到用户问题"查一下001"
// AI判断:需要调用 query_user 工具
const result = await client.callTool({
    name: 'query_user',
    arguments: { id: '001' }
});

📊 总结:在MCP生态中的位置

graph TB
    subgraph &#34;你的MCP服务器&#34;
        A[McpServer实例] --> B[query_user工具]
        B --> C[Zod参数校验]
        B --> D[业务逻辑]
        D --> E[返回格式化结果]
    end
    
    subgraph &#34;MCP协议层&#34;
        F[StdioServerTransport]
        F -->|stdin| B
        B -->|stdout| F
    end
    
    subgraph &#34;客户端&#34;
        G[AI Agent]
        G -->|启动子进程| F
        F -->|返回结果| G
    end
    
    style A fill:#4A90D9,color:#fff
    style B fill:#FF6B6B,color:#fff
    style F fill:#4ECDC4,color:#fff
    style G fill:#45B7D1,color:#fff

核心要点回顾

  1. MCP让工具"独立"了:不再绑定特定项目/语言/进程
  2. stdio是桥梁:通过标准输入输出实现跨进程通信
  3. 描述很重要:description是给AI看的,决定AI会不会用你的工具
  4. 格式要规范:返回格式必须符合MCP协议
  5. 错误要友好:返回错误信息而不是抛出异常
  6. 参数需解构async ({ id }) 而不是 async (id)

🚀 下一步你可以做什么?

  1. 改造现有工具:把你项目里的工具函数改成MCP Server
  2. 多语言尝试:用Python/Java/Rust也实现一个MCP Server
  3. 组合工具:一个MCP Server里注册多个工具
  4. 添加资源:除了Tool,还可以注册Resource(让AI获取知识)
  5. 生产部署:使用HTTP Transport部署到云端,实现远程调用

📝 完整代码(可直接运行)

// mcp-server.js
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';

// 模拟数据库
const database = {
    users: {
        '001': {id: '001', name: '张三', email: '111@qq.com', role: 'admin'},
        '002': {id: '002', name: '李四', email: '222@qq.com', role: 'user'},
        '003': {id: '003', name: '王五', email: '333@qq.com', role: 'user'}
    }
};

// 创建服务器
const server = new McpServer({
    name: 'my-mcp-server',
    version: '1.0.0',
});

// 注册工具
server.registerTool(
    'query_user',
    {
        description: '查询数据库中的用户信息。输入用户ID,返回该用户详细信息(姓名、邮箱、角色)',
        inputSchema: z.object({
            id: z.string().describe('用户ID, 例如: 001, 002, 003'),
        }),
    },
    async ({ id }) => {
        const user = database.users[id];
        if (!user) {
            return {
                content: [
                    {
                        type: 'text',
                        text: `用户${id}不存在。可用ID: ${Object.keys(database.users).join(', ')}`
                    }
                ]
            };
        }
        return {
            content: [
                {
                    type: 'text',
                    text: `用户${id}的信息如下:姓名: ${user.name} 邮箱: ${user.email} 角色: ${user.role}`
                }
            ]
        };
    }
);

// 启动服务器
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('MCP服务器已启动,等待请求...');

记住:MCP不是魔法,它只是把"调用函数"这件事标准化了。但就是这个标准化,让AI从"只会聊天"变成了"什么都能干"!🌟