MCP从理解到实现

1. MCP协议简介

Model Context Protocol (MCP)是Anthropic在2024年推出的开放标准,旨在统一大型语言模型(LLM)与外部数据源和工具之间的通信。它解决了AI模型因数据孤岛限制而无法充分发挥潜力的问题,使AI应用能够访问和操作本地及远程数据。

MCP 是让不同 AI 模型和外部工具通过统一接口协作的通信协议,类似给所有设备统一用USB-C 接口

2. 三层架构设计

MCP遵循清晰的三层架构模式:

Host层

● 与用户直接交互的应用程序(如Claude Desktop、Cursor、Cline等)

● 提供用户界面,处理用户查询到大模型,大模型通过Client路由到指定server

Client层

● 实现MCP的客户端标准,作为大模型和Server的桥梁

● 业界主流方案是一个Client与多个MCP Server建立连接

● 当大模型需要外部工具时,构造请求并将响应传回模型

Server层

● 实现MCP服务端标准,将不同数据源格式统一为MCP标准化结构,支持本地部署与远程连接

● 提供核心功能:定义API和Description,表示服务可提供的功能

● 处理Client的请求并返回结果

3. 通信机制与工作流程

通信协议

● 本地通讯:通过标准输入输出(stdin/stdout)传输,子进程方式通信

● 远程通讯:使用SSE或WebSockets

● 数据格式:采用JSON-RPC 2.0格式

注册与初始化过程

MCP服务器注册通过手动配置完成,目前不存在自动化的服务发现机制:

  1. 服务器创建与配置:开发者创建符合MCP协议的服务器程序,准备连接信息

  2. 在Host中添加配置:

bash 复制代码
"mcpServers": {
  "dev-tools": {
    "command": "python",
    "args": ["/absolute/path/to/my-mcp-server/server.py"]
  },
  "weather": {      
    "transport": "sse",      
    "url": "https://weather-mcp.example.com/api/mcp",
    "description": "Weather data service",      
    "auth": {        
      "type": "api-key",        
      "headerName": "X-API-Key",        
      "key": "${WEATHER_API_KEY}"
    }    
  }
}
  1. 初始化与功能协商:Host启动时初始化MCP Client组件

Client连接所有MCP Server并获取它们提供的Tools能力进行存储,用户每次提问时Host将这些能力信息作为系统提示词传到大模型的上下文中

实际工作流程

  1. 用户发起查询:Host调用大模型,携带Server工具信息

  2. 需求分析:大模型识别需要外部工具时,基于Server描述决定调用哪个Server和方法

  3. 数据交换:Client向Server发送请求,Server处理后返回数据

  4. 结果整合:Client将数据返回给大模型,模型完成最终响应

4. 实现方案

Client和Server连接模式

● 基本模式:一个Client对应一个Server

● 高级模式(LangChain / LangGraph的实现):一个Client管理多个Server连接

实现选项

  1. 使用开源Client:如Open MCP Client (open-mcp-client.vercel.app/)

  2. 使用官方SDK:Anthropic提供的@modelcontextprotocol/sdk自行实现

4. 智能运维助手场景

需求分析

智能运维助手需要访问服务器日志数据,辅助AI进行分析和诊断。可以根据MCP架构实现以下组件:

  1. MCP Server (必须实现):访问服务器日志、系统状态和监控数据

  2. MCP Client (自行实现/社区实现):大模型与Server的桥梁,有开源库,比如Langchain等库已经实现好了

  3. MCP Host (可选实现):提供用户交互界面

Client的实现方案

  1. 使用现有开源Client, 一个client管理多个server:

Open MCP Client (open-mcp-client.vercel.app/)

  1. 使用官方SDK自行实现:

Anthropic提供的@modelcontextprotocol/sdk

5. 代码实现示例

5.1 MCP Server实现

typescript 复制代码
// mcp_server.ts
import express from 'express';
import bodyParser from 'body-parser';
import { exec } from 'child_process';
import fs from 'fs/promises';
import path from 'path';
import childProcess from 'child_process';
 
const app = express();
app.use(bodyParser.json());
 
// 工具定义
const TOOLS = [
  {
    name: "diagnose_service",
    description: "Diagnose a service status",
    parameters: {
      type: "object",
      properties: {
        service_name: {
          type: "string",
          description: "Name of the service to diagnose"
        }
      },
      required: ["service_name"]
    }
  }
];
 
// MCP协议处理函数
async function handleMcpRequest(request: any) {
  const method = request.method;
  const params = request.params || {};
  const requestId = request.id;
 
  console.log(`Received MCP request: ${method}`, params);
 
  // 列出工具
  if (method === 'list_tools') {
    return {
      jsonrpc: '2.0',
      result: {
        tools: TOOLS
      },
      id: requestId
    };
  }
    
  // 调用工具
  if (method === 'call_tool') {
    const toolName = params.name;
    const toolArgs = params.arguments || {};
    
    console.log(`Calling tool: ${toolName} with arguments:`, toolArgs);
    
    // 服务诊断工具
    if (toolName === 'diagnose_service') {
      const serviceName = toolArgs.service_name;
      return new Promise((resolve) => {
        exec(`systemctl status ${serviceName}`, (error, stdout, stderr) => {
          const result = {
            jsonrpc: '2.0',
            result: {
              content: [
                {
                  type: "text",
                  text: error ? `Error: ${stderr}` : stdout
                }
              ]
            },
            id: requestId
          };
          resolve(result);
        });
      });
    }
  }
  
  // 方法不存在
  console.error(`Method not found: ${method}`);
  return {
    jsonrpc: '2.0',
    error: { code: -32601, message: 'Method not found' },
    id: requestId
  };
}
 
// 标准输入/输出模式的处理函数
async function handleStdioMode() {
  process.stdin.setEncoding('utf-8');
  process.stdin.on('data', async (data) => {
    const lines = data.toString().split('\n');
    
    for (const line of lines) {
      if (!line.trim()) continue;
      
      try {
        const request = JSON.parse(line);
        const response = await handleMcpRequest(request);
        
        // 发送响应到标准输出
        process.stdout.write(JSON.stringify(response) + '\n');
      } catch (error) {
        console.error('Error handling request:', error);
        const errorResponse = {
          jsonrpc: '2.0',
          error: { code: -32603, message: `Internal error: ${(error as Error).message}` },
          id: null
        };
        process.stdout.write(JSON.stringify(errorResponse) + '\n');
      }
    }
  });
  
  console.error('MCP Server running in stdio mode');
}
 
// HTTP接口
app.post('/mcp-server', async (req, res) => {
  try {
    const response = await handleMcpRequest(req.body);
    res.json(response);
  } catch (error) {
    console.error('Error handling request:', error);
    res.status(500).json({
      jsonrpc: '2.0',
      error: { code: -32603, message: 'Internal error' },
      id: req.body.id
    });
  }
});
 
// 启动服务器 - 支持HTTP和stdio两种模式
if (process.argv.includes('--stdio')) {
  handleStdioMode();
} else {
  const PORT = process.env.PORT || 3000;
  app.listen(PORT, () => {
    console.log(`MCP Server running on port ${PORT}`);
  });
}
 
export default app;

5.2 一个MCP Client管理多个MCP Server的实现

typescript 复制代码
// multi_server_mcp_client.ts
import { spawn, ChildProcess } from 'child_process';
import fetch from 'node-fetch';
import { EventEmitter } from 'events';
import * as path from 'path';
 
// 定义连接类型
type StdioConnection = {
  command: string;
  args: string[];
  transport: 'stdio';
};
 
type SSEConnection = {
  url: string;
  transport: 'sse';
};
 
// MCP配置类型
type MCPConfig = Record<string, StdioConnection | SSEConnection>;
 
// 工具类型定义
type ToolParameter = {
  type: string;
  description?: string;
  properties?: Record<string, any>;
  required?: string[];
};
 
type Tool = {
  name: string;
  description: string;
  parameters: ToolParameter;
  returns?: any;
};
 
// 传输层基类
abstract class Transport extends EventEmitter {
  abstract connect(): Promise<void>;
  abstract send(request: any): Promise<any>;
  abstract disconnect(): Promise<void>;
}
 
// Stdio传输层实现
class StdioTransport extends Transport {
  private process: ChildProcess | null = null;
  private responseQueue: Array<{
    resolve: (value: any) => void;
    reject: (reason: any) => void;
    id: string;
  }> = [];
  private buffer: string = '';
  
  constructor(private command: string, private args: string[]) {
    super();
  }
  
  async connect(): Promise<void> {
    return new Promise((resolve, reject) => {
      try {
        this.process = spawn(this.command, this.args);
        
        this.process.stdout?.on('data', (data: Buffer) => {
          this.buffer += data.toString();
          this.processBuffer();
        });
        
        this.process.stderr?.on('data', (data: Buffer) => {
          console.error(`[StdioTransport] Error: ${data.toString()}`);
        });
        
        this.process.on('error', (error) => {
          console.error('[StdioTransport] Process error:', error);
          reject(error);
        });
        
        this.process.on('exit', (code) => {
          console.log(`[StdioTransport] Process exited with code ${code}`);
          if (code !== 0) {
            this.rejectAllPending(`Process exited with code ${code}`);
          }
        });
        
        // 给进程一些启动时间
        setTimeout(resolve, 100);
      } catch (error) {
        reject(error);
      }
    });
  }
  
  private processBuffer() {
    const lines = this.buffer.split('\n');
    
    // 保留最后一行,可能不完整
    this.buffer = lines.pop() || '';
    
    for (const line of lines) {
      if (!line.trim()) continue;
      
      try {
        const response = JSON.parse(line);
        const pending = this.responseQueue.find(p => p.id === response.id);
        
        if (pending) {
          const index = this.responseQueue.indexOf(pending);
          this.responseQueue.splice(index, 1);
          
          if (response.error) {
            pending.reject(response.error);
          } else {
            pending.resolve(response);
          }
        } else {
          console.warn('[StdioTransport] Received response with no matching request:', response);
        }
      } catch (error) {
        console.error('[StdioTransport] Error parsing response:', error, line);
      }
    }
  }
  
  async send(request: any): Promise<any> {
    if (!this.process || this.process.killed) {
      throw new Error('Process not running');
    }
    
    return new Promise((resolve, reject) => {
      this.responseQueue.push({
        resolve,
        reject,
        id: request.id
      });
      
      const requestStr = JSON.stringify(request) + '\n';
      this.process!.stdin!.write(requestStr);
    });
  }
  
  private rejectAllPending(reason: string) {
    for (const pending of this.responseQueue) {
      pending.reject(new Error(reason));
    }
    this.responseQueue = [];
  }
  
  async disconnect(): Promise<void> {
    if (this.process && !this.process.killed) {
      this.process.kill();
    }
  }
}
 
// HTTP/SSE传输层实现
class HttpTransport extends Transport {
  constructor(private url: string) {
    super();
  }
  
  async connect(): Promise<void> {
    // HTTP传输不需要特别的连接逻辑
  }
  
  async send(request: any): Promise<any> {
    const response = await fetch(this.url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(request)
    });
    
    if (!response.ok) {
      throw new Error(`HTTP error: ${response.status} ${response.statusText}`);
    }
    
    return await response.json();
  }
  
  async disconnect(): Promise<void> {
    // HTTP传输不需要特别的断开逻辑
  }
}
 
// 多服务器MCP客户端
class MultiServerMCPClient {
  private services: Map<string, { 
    transport: Transport, 
    tools: Tool[] 
  }> = new Map();
  private isConnected: boolean = false;
  
  constructor(private config: MCPConfig) {}
  
  async connect(): Promise<void> {
    // 创建和连接所有服务器客户端
    for (const [serviceName, serviceConfig] of Object.entries(this.config)) {
      try {
        // 创建传输层
        let transport: Transport;
        if (serviceConfig.transport === 'stdio') {
          transport = new StdioTransport(serviceConfig.command, serviceConfig.args);
        } else if (serviceConfig.transport === 'sse') {
          transport = new HttpTransport(serviceConfig.url);
        } else {
          throw new Error(`Unsupported transport: ${(serviceConfig as any).transport}`);
        }
        
        // 连接到服务
        await transport.connect();
        
        // 初始化服务
        const response = await transport.send({
          jsonrpc: '2.0',
          method: 'list_tools',
          params: {},
          id: `init_${serviceName}_${Date.now()}`
        });
        
        if (response.error) {
          throw new Error(`Initialization error: ${response.error.message}`);
        }
        
        // 保存服务信息和工具
        this.services.set(serviceName, {
          transport,
          tools: response.result?.tools || []
        });
        
        console.log(`Connected to MCP service: ${serviceName} with ${response.result?.tools?.length || 0} tools`);
      } catch (error) {
        console.error(`Failed to connect to service ${serviceName}:`, error);
        // 继续连接其他服务,不因一个服务失败而终止
      }
    }
    
    this.isConnected = true;
    console.log(`Connected to ${this.services.size} MCP services`);
  }
  
  getTools(): Tool[] {
    if (!this.isConnected) {
      throw new Error('Not connected to MCP services');
    }
    
    // 收集所有服务的工具,添加服务名前缀
    const allTools: Tool[] = [];
    
    for (const [serviceName, service] of this.services.entries()) {
      for (const tool of service.tools) {
        allTools.push({
          ...tool,
          name: `${serviceName}.${tool.name}`,
          description: `[${serviceName}] ${tool.description}`
        });
      }
    }
    
    return allTools;
  }
  
  async callTool(fullToolName: string, parameters: any): Promise<any> {
    if (!this.isConnected) {
      throw new Error('Not connected to MCP services');
    }
    
    // 解析服务名和工具名
    const [serviceName, toolName] = fullToolName.split('.');
    
    if (!serviceName || !toolName) {
      throw new Error(`Invalid tool name format: ${fullToolName}. Expected: service.tool`);
    }
    
    const service = this.services.get(serviceName);
    if (!service) {
      throw new Error(`Service not found: ${serviceName}`);
    }
    
    // 调用工具
    const response = await service.transport.send({
      jsonrpc: '2.0',
      method: 'call_tool',
      params: {
        name: toolName,
        arguments: parameters
      },
      id: `call_${serviceName}_${toolName}_${Date.now()}`
    });
    
    if (response.error) {
      throw new Error(`Tool call error: ${response.error.message}`);
    }
    
    return response.result;
  }
  
  async disconnect(): Promise<void> {
    // 断开所有服务连接
    for (const [serviceName, service] of this.services.entries()) {
      try {
        await service.transport.disconnect();
        console.log(`Disconnected from service: ${serviceName}`);
      } catch (error) {
        console.error(`Error disconnecting from ${serviceName}:`, error);
      }
    }
    
    this.services.clear();
    this.isConnected = false;
    console.log('Disconnected from all MCP services');
  }
}
export { MultiServerMCPClient, MCPConfig, Tool, using };

5.3 简易MCP Host实现

typescript 复制代码
import express from 'express';
import path from 'path';
import { MultiServerMCPClient, MCPConfig } from './multi_server_mcp_client';
 
const app = express();
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));
 
// 模拟LLM接口
class ChatModel {
  private config: { apiKey: string, model: string };
  
  constructor(config: { apiKey: string, model: string }) {
    this.config = config;
  }
  
  async chat(options: { messages: any[] }): Promise<any> {
    console.log('Sending chat request to LLM');
    console.log('Messages:', JSON.stringify(options.messages, null, 2));
    
    // 检查是否需要调用工具 - 实际应该由LLM API返回
    const lastMessage = options.messages[options.messages.length - 1];
    if (lastMessage.role === 'user') {
      const content = lastMessage.content.toLowerCase();
      
      if (content.includes('service') || content.includes('status')) {
        // 模拟LLM决定使用服务诊断工具
        return {
          id: 'resp_' + Date.now(),
          content: JSON.stringify({
            thoughts: "用户询问服务状态。我应该使用服务诊断工具获取信息。",
            tool: "logs.diagnose_service",
            parameters: {
              service_name: "nginx"
            }
          }),
          model: this.config.model
        };
      }
     }
    
    // 普通回复
    return {
      id: 'resp_' + Date.now(),
      content: '我已经处理了您的请求。您可以尝试询问关于日志、服务状态或文件内容的问题,我可以使用工具来帮助您。',
      model: this.config.model
    };
  }
}
 
// MCP服务配置
const MCP_CONFIG: MCPConfig = {
  "logs": {
    url: "http://localhost:3000/mcp",
    transport: "sse",
  },
  "filesystem": {
    command: "node",
    args: [path.join(__dirname, "mcp_server.js"), "--stdio"],
    transport: "stdio",
  }
};
 
// 初始化客户端
const mcpClient = new MultiServerMCPClient(MCP_CONFIG);
 
// 初始化模型
const model = new ChatModel({
  apiKey: process.env.AI_API_KEY || 'dummy-key',
  model: 'claude-3.7-sonnet'
});
 
let toolsMetadata: any[] = [];
 
// 连接到服务器
(async () => {
  try {
    await mcpClient.connect();
    toolsMetadata = mcpClient.getTools();
    // 在这种模式下,工具名称会添加服务器前缀(例如logs.search_logs)以避免命名冲突
    console.log('MCP Client connected successfully');
    console.log('Available tools:', JSON.stringify(toolsMetadata, null, 2));
  } catch (error) {
    console.error('Failed to connect MCP Client:', error);
  }
})();
 
// 处理聊天请求
app.post('/api/chat', async (req, res) => {
  try {
    const { message, conversation_id } = req.body;
    const newConversationId = conversation_id || Date.now().toString();
    
    // 从数据库或内存中获取历史消息
    let conversationHistory = await getConversationHistory(newConversationId) || [];
    
    // 构建带有工具调用能力的提示
    const systemPrompt = `You are an AI operations assistant with many tools. 
    When you need to use external tools, use the appropriate tool.
    Available Tools: ${JSON.stringify(toolsMetadata)}
    
    When you want to use external tool, respond with a JSON object containing:
    {
      "thoughts": "your reasoning",
      "tool": "service.tool_name",
      "parameters": { param1: value1, ... }
    }`;
    
    // 创建完整的消息历史,包括系统提示和新消息
    const messages = [
      { role: 'system', content: systemPrompt },
      ...conversationHistory,
      { role: 'user', content: message }
    ];
    
    // 开始agent循环,允许多次工具调用
    let maxIterations = 5; // 防止无限循环
    let finalResponse = null;
    let toolUsed = null;
    
    for (let i = 0; i < maxIterations; i++) {
      // 调用LLM
      const llmResponse = await model.chat({ messages });
      
      try {
        // 尝试解析JSON工具调用
        const toolCall = JSON.parse(llmResponse.content);
        
        if (toolCall.tool && toolCall.parameters) {
          console.log(`LLM requested tool (iteration ${i+1}): ${toolCall.tool}`);
          toolUsed = toolCall.tool;
          
          // 执行工具调用
          const toolResult = await mcpClient.callTool(
            toolCall.tool, 
            toolCall.parameters
          );
          
          // 提取工具返回的文本内容
          let resultText = '';
          if (toolResult.content && Array.isArray(toolResult.content)) {
            resultText = toolResult.content
              .filter((item) => item.type === 'text')
              .map((item) => item.text)
              .join('\n');
          } else {
            resultText = JSON.stringify(toolResult);
          }
          
          // 将LLM响应和工具结果添加到历史中
          messages.push({ role: 'assistant', content: llmResponse.content });
          messages.push({ role: 'system', content: `Tool result: ${resultText}` });
          
          // 继续循环,让LLM处理工具结果
          continue;
        }
        
        // 如果没有工具调用,这是最终响应
        finalResponse = llmResponse.content;
        break;
        
      } catch (parseError) {
        // 不是JSON,是普通回复,保存为最终响应
        finalResponse = llmResponse.content;
        break;
      }
    }
    
    // 保存对话历史
    const updatedHistory = [
      ...conversationHistory,
      { role: 'user', content: message },
      { role: 'assistant', content: finalResponse }
    ];
    await saveConversationHistory(newConversationId, updatedHistory);
    
    // 返回最终结果
    res.json({
      message: finalResponse,
      conversation_id: newConversationId,
      tool_used: toolUsed
    });
    
  } catch (error) {
    console.error('Chat error:', error);
    res.status(500).json({ error: 'Failed to process chat request' });
  }
});
 
// 辅助函数用于管理对话历史
async function getConversationHistory(conversationId) {
  // 实现从数据库或内存中获取对话历史
  // 简单实现可以使用内存缓存或Redis
  // 这里假设有一个全局对象存储对话历史
  return global.conversations[conversationId] || [];
}
 
async function saveConversationHistory(conversationId, history) {
  // 保存对话历史到数据库或内存
  if (!global.conversations) global.conversations = {};
  global.conversations[conversationId] = history;
}
 
// 获取工具列表API
app.get('/api/tools', (req, res) => {
  res.json({ tools: toolsMetadata });
});
 
// 启动服务器
const PORT = process.env.HOST_PORT || 8080;
app.listen(PORT, () => {
  console.log(`MCP Host running on port ${PORT}`);
});
 
// 确保在进程退出时断开MCP连接
process.on('exit', async () => {
  await mcpClient.disconnect();
});
 
process.on('SIGINT', async () => {
  await mcpClient.disconnect();
  process.exit();
});
 
export default app;

总结

● 对开发者:MCP减少 70% 的重复接口开发工作, 增强AI能力

● 对公司:快速连接现有系统与 AI 能力

相关推荐
大龄大专大前端16 分钟前
JavaScript闭包的认识/应用/原理
前端·javascript·ecmascript 6
字节源流19 分钟前
【SpringMVC】常用注解:@SessionAttributes
java·服务器·前端
肥肠可耐的西西公主29 分钟前
前端(vue)学习笔记(CLASS 4):组件组成部分与通信
前端·vue.js·学习
烛阴35 分钟前
JavaScript 函数绑定:从入门到精通,解锁你的代码超能力!
前端·javascript
泫凝1 小时前
使用 WebP 优化 GPU 纹理占用
前端·javascript
magic 2451 小时前
CSS块元素、行内元素、行内块元素详解
前端·css
returnShitBoy1 小时前
前端面试:React hooks 调用是可以写在 if 语句里面吗?
前端·javascript·react.js
love黄甜心1 小时前
Sass:深度解析与实战应用
前端·css·sass
goto_w2 小时前
使用elementplus的table表格遇到的问题
前端·javascript·vue.js
Moment2 小时前
如果你想找国外远程,首先让我先给你推荐这几个流行的技术栈 🤪🤪🤪
前端·后端·github