Skip to main content

概述

MCP(Model Context Protocol)是一个开放协议,允许AI模型通过标准化的方式与外部工具和数据源交互。LangChat Pro实现了MCP客户端和服务端,为AI助手提供强大的扩展能力。

协议概述

MCP核心概念

MCP协议定义了模型与工具之间的标准化通信方式:
  1. 服务器(Server): 提供工具和资源的组件
  2. 客户端(Client): 调用服务器工具的组件
  3. 工具(Tools): 可被模型调用的功能
  4. 资源(Resources): 可访问的数据或内容
  5. 提示词模板(Prompts): 预定义的提示词模板

MCP通信模型

┌──────────────┐
│   LLM       │
│  (模型层)     │
└──────┬───────┘


┌──────────────┐
│ MCP Client  │
│  (客户端)     │
└──────┬───────┘


┌──────────────┐
│ MCP Server  │
│  (服务端)     │
└──────────────┘

核心组件

McpClientManager

路径: langchat-mcp/src/main/java/cn/langchat/mcp/factory/McpClientManager.java 职责: 管理MCP客户端连接和生命周期
@Service
public class McpClientManager {
    
    @Resource
    private AigcMcpService mcpService;
    
    /**
     * 客户端实例缓存
     */
    private final Map<String, McpClient> clientCache = new ConcurrentHashMap<>();
    
    /**
     * 获取MCP客户端
     */
    public McpClient getClient(String serverId) {
        // 检查缓存
        if (clientCache.containsKey(serverId)) {
            return clientCache.get(serverId);
        }
        
        // 创建新客户端
        AigcMcp serverConfig = mcpService.getById(serverId);
        McpClient client = createClient(serverConfig);
        
        // 缓存客户端
        clientCache.put(serverId, client);
        
        return client;
    }
    
    /**
     * 创建MCP客户端
     */
    private McpClient createClient(AigcMcp serverConfig) {
        McpClient client = McpClient.newClient()
            .serverConfig(serverConfig.getConfig())
            .transport(getTransport(serverConfig))
            .build();
        
        // 连接服务器
        client.connect();
        
        return client;
    }
    
    /**
     * 获取所有工具
     */
    public List<Tool> getTools(String serverId) {
        McpClient client = getClient(serverId);
        return client.listTools();
    }
    
    /**
     * 调用工具
     */
    public String callTool(String serverId, String toolName, Map<String, Object> args) {
        McpClient client = getClient(serverId);
        return client.callTool(toolName, args);
    }
    
    /**
     * 关闭客户端
     */
    public void closeClient(String serverId) {
        McpClient client = clientCache.remove(serverId);
        if (client != null) {
            client.close();
        }
    }
    
    /**
     * 关闭所有客户端
     */
    public void closeAll() {
        clientCache.values().forEach(McpClient::close);
        clientCache.clear();
    }
}

McpToolFactory

职责

将MCP服务器提供的工具注册到AI Service

实现

@Component
public class McpToolFactory implements DynamicToolFactory {
    
    @Resource
    private McpClientManager mcpClientManager;
    
    @Resource
    private AigcMcpService mcpService;
    
    @Override
    public void dynamicTools(LcChatReq req, AiServices<LangChatAgent> aiServices) {
        log.debug("开始构建MCP工具: userId={}, appId={}", 
                req.getUserId(), req.getAppId());
        
        // 1. 获取启用的MCP服务器
        var mcpServers = mcpService.getEnabledServers(req.getUserId(), req.getAppId());
        
        if (CollUtil.isEmpty(mcpServers)) {
            log.debug("未启用MCP服务器: userId={}, appId={}", 
                    req.getUserId(), req.getAppId());
            return;
        }
        
        // 2. 获取所有工具
        var tools = new ArrayList<Tool>();
        for (var server : mcpServers) {
            try {
                List<Tool> serverTools = mcpClientManager.getTools(server.getId());
                tools.addAll(serverTools);
            } catch (Exception e) {
                log.error("获取MCP工具失败: serverId={}", server.getId(), e);
            }
        }
        
        if (CollUtil.isEmpty(tools)) {
            log.debug("MCP工具为空: userId={}, appId={}", 
                    req.getUserId(), req.getAppId());
            return;
        }
        
        // 3. 添加到AiServices
        aiServices.tools(tools);
        
        log.info("MCP工具构建完成: userId={}, appId={}, toolCount={}", 
                req.getUserId(), req.getAppId(), tools.size());
    }
}

MCP服务端

FileSystemMcpServer

路径: langchat-mcp-server/mcp-server-filesystem/src/main/java/cn/langchat/mcp/server/fs/FileSystemMcpServer.java 职责: 提供文件系统操作工具 实现示例:
public class FileSystemMcpServer {
    
    /**
     * 读取文件
     */
    @McpTool(name = "read_file", description = "读取文件内容")
    public String readFile(
        @McpParameter(name = "path", description = "文件路径", required = true)
        String path
    ) {
        try {
            return Files.readString(Paths.get(path));
        } catch (IOException e) {
            throw new RuntimeException("读取文件失败", e);
        }
    }
    
    /**
     * 写入文件
     */
    @McpTool(name = "write_file", description = "写入文件内容")
    public String writeFile(
        @McpParameter(name = "path", description = "文件路径", required = true)
        String path,
        @McpParameter(name = "content", description = "文件内容", required = true)
        String content
    ) {
        try {
            Files.writeString(Paths.get(path), content);
            return "写入成功";
        } catch (IOException e) {
            throw new RuntimeException("写入文件失败", e);
        }
    }
    
    /**
     * 列出目录
     */
    @McpTool(name = "list_directory", description = "列出目录内容")
    public String listDirectory(
        @McpParameter(name = "path", description = "目录路径", required = true)
        String path
    ) {
        try (Stream<Path> stream = Files.list(Paths.get(path))) {
            return stream
                .map(Path::getFileName)
                .map(Path::toString)
                .collect(Collectors.joining("\n"));
        } catch (IOException e) {
            throw new RuntimeException("列出目录失败", e);
        }
    }
    
    /**
     * 搜索文件
     */
    @McpTool(name = "search_files", description = "搜索文件")
    public String searchFiles(
        @McpParameter(name = "path", description = "搜索路径", required = true)
        String path,
        @McpParameter(name = "pattern", description = "文件名模式", required = true)
        String pattern
    ) {
        try (Stream<Path> stream = Files.walk(Paths.get(path))) {
            return stream
                .filter(p -> p.getFileName().toString().matches(pattern))
                .map(Path::toString)
                .collect(Collectors.joining("\n"));
        } catch (IOException e) {
            throw new RuntimeException("搜索文件失败", e);
        }
    }
}

配置模型

AigcMcp

public class AigcMcp {
    private String id;
    private String name;           // MCP服务器名称
    private String description;    // 描述
    private String type;           // 类型: stdio, sse
    private String config;         // 配置(JSON)
    private Boolean enabled;       // 是否启用
}

配置示例

{
  "id": "mcp-001",
  "name": "filesystem",
  "type": "stdio",
  "description": "文件系统MCP服务器",
  "config": {
    "command": "python",
    "args": ["/path/to/server.py"],
    "env": {
      "PYTHONPATH": "/path/to/dependencies"
    }
  },
  "enabled": true
}

传输方式

1. Stdio(标准输入输出)

适用场景: 本地进程通信
private Transport getStdioTransport(AigcMcp serverConfig) {
    McpConfig config = JsonUtil.toBean(serverConfig.getConfig(), McpConfig.class);
    
    return Transport.stdio()
        .command(config.getCommand())
        .args(config.getArgs())
        .env(config.getEnv())
        .build();
}

2. SSE(Server-Sent Events)

适用场景: HTTP通信
private Transport getSseTransport(AigcMcp serverConfig) {
    McpConfig config = JsonUtil.toBean(serverConfig.getConfig(), McpConfig.class);
    
    return Transport.sse()
        .url(config.getUrl())
        .headers(config.getHeaders())
        .build();
}

MCP调用流程

1. 工具调用

用户提问:"读取文件 /path/to/file.txt"

LLM分析问题

识别需要调用MCP工具: "read_file"

McpToolFactory提供工具

McpClientManager调用工具

McpServer执行工具

返回文件内容

LLM根据结果生成回答

2. 资源访问

用户提问:"获取最新文档"

LLM识别需要访问资源

McpClientManager列出资源

读取资源内容

LLM根据资源生成回答

扩展点

1. 自定义MCP服务器

步骤1: 创建MCP服务器
public class CustomMcpServer {
    
    @McpTool(name = "custom_tool", description = "自定义工具")
    public String customTool(
        @McpParameter(name = "input", description = "输入参数", required = true)
        String input
    ) {
        // 自定义逻辑
        return "处理结果: " + input;
    }
    
    @McpResource(name = "custom_resource", description = "自定义资源")
    public String customResource() {
        // 返回资源内容
        return "资源内容";
    }
}
步骤2: 注册服务器 在数据库中添加MCP服务器配置:
INSERT INTO aigc_mcp (id, name, type, description, enabled)
VALUES (
    'mcp-custom',
    'custom_server',
    'stdio',
    '自定义MCP服务器',
    1
);

2. 添加新传输方式

private Transport getCustomTransport(AigcMcp serverConfig) {
    McpConfig config = JsonUtil.toBean(serverConfig.getConfig(), McpConfig.class);
    
    return Transport.custom()
        .url(config.getUrl())
        .timeout(config.getTimeout())
        .build();
}

最佳实践

1. 工具命名

  • 使用下划线分隔
  • 名称要清晰描述功能
  • 示例: read_file, write_file, search_files

2. 参数定义

  • 明确参数类型
  • 标注必需参数
  • 提供参数说明
  • 设置合理的默认值

3. 错误处理

  • 捕获所有异常
  • 返回友好的错误信息
  • 记录详细日志
  • 考虑重试机制

4. 性能优化

  • 缓存客户端实例
  • 使用连接池
  • 设置超时时间
  • 异步执行耗时操作

5. 安全考虑

  • 验证文件路径
  • 限制访问范围
  • 过滤敏感操作
  • 记录调用日志

配置说明

application.yml

langchat:
  mcp:
    enabled: true                    # 是否启用MCP
    timeout: 30                      # 超时时间(秒)
    max-retries: 3                    # 最大重试次数
    pool-size: 10                    # 连接池大小

mcp:
  servers:
    filesystem:
      type: stdio
      command: python
      args: ["/path/to/server.py"]
    custom:
      type: sse
      url: http://localhost:8080/mcp

参考文档