MCP入门教程-第4章:MCP 工具 (Tools)

319 阅读5分钟

什么是MCP工具?

MCP工具是一种强大的原语,允许服务器向客户端暴露可执行功能。通过工具,大型语言模型(LLM)能够与外部系统交互、执行计算并在现实世界中采取行动。

核心特性

  • 模型控制:工具专为AI模型自动调用而设计(在人工监督下)
  • 动态发现:客户端可以通过tools/list端点列出可用工具
  • 灵活调用:使用tools/call端点调用工具,服务器执行操作并返回结果
  • 广泛适用:从简单计算到复杂API交互

TypeScript SDK环境搭建

首先,让我们建立开发环境:

# 安装MCP TypeScript SDK
npm install @modelcontextprotocol/sdk

# 安装类型定义
npm install --save-dev @types/node typescript

工具定义结构

每个MCP工具都遵循标准的JSON结构:

interface Tool {
  name: string;                    // 唯一标识符
  description?: string;            // 工具描述
  inputSchema: {                   // JSON Schema定义参数
    type: "object";
    properties: Record<string, any>;
    required?: string[];
  };
}

实战演示:构建多功能工具服务器

让我们创建一个功能丰富的MCP服务器,演示不同类型的工具实现:

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import * as fs from "fs/promises";
import * as path from "path";

class MCPToolServer {
  private server: Server;

  constructor() {
    this.server = new Server(
      {
        name: "advanced-tools-server",
        version: "1.0.0",
      },
      {
        capabilities: {
          tools: {},
        },
      }
    );

    this.setupTools();
  }

  private setupTools() {
    // 1. 系统操作工具 - 文件管理
    this.server.setRequestHandler(ListToolsRequestSchema, async () => {
      return {
        tools: [
          {
            name: "read_file",
            description: "读取指定路径的文件内容",
            inputSchema: {
              type: "object",
              properties: {
                filepath: {
                  type: "string",
                  description: "要读取的文件路径"
                }
              },
              required: ["filepath"]
            }
          },
          {
            name: "write_file", 
            description: "将内容写入指定文件",
            inputSchema: {
              type: "object",
              properties: {
                filepath: {
                  type: "string",
                  description: "目标文件路径"
                },
                content: {
                  type: "string", 
                  description: "要写入的内容"
                }
              },
              required: ["filepath", "content"]
            }
          },
          {
            name: "calculate",
            description: "执行数学计算表达式",
            inputSchema: {
              type: "object",
              properties: {
                expression: {
                  type: "string",
                  description: "数学表达式,如 '2 + 3 * 4'"
                }
              },
              required: ["expression"]
            }
          },
          {
            name: "fetch_weather",
            description: "获取指定城市的天气信息",
            inputSchema: {
              type: "object",
              properties: {
                city: {
                  type: "string",
                  description: "城市名称"
                },
                country: {
                  type: "string",
                  description: "国家代码,如 'CN', 'US'",
                  default: "CN"
                }
              },
              required: ["city"]
            }
          },
          {
            name: "process_data",
            description: "处理和分析JSON数据",
            inputSchema: {
              type: "object", 
              properties: {
                data: {
                  type: "array",
                  description: "要处理的数据数组"
                },
                operation: {
                  type: "string",
                  enum: ["sum", "average", "max", "min", "count"],
                  description: "要执行的操作类型"
                },
                field: {
                  type: "string",
                  description: "要操作的字段名(可选)"
                }
              },
              required: ["data", "operation"]
            }
          }
        ]
      };
    });

    // 工具调用处理器
    this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
      const { name, arguments: args } = request.params;

      try {
        switch (name) {
          case "read_file":
            return await this.handleReadFile(args);
          case "write_file":
            return await this.handleWriteFile(args);
          case "calculate": 
            return await this.handleCalculate(args);
          case "fetch_weather":
            return await this.handleFetchWeather(args);
          case "process_data":
            return await this.handleProcessData(args);
          default:
            throw new Error(`未知工具: ${name}`);
        }
      } catch (error) {
        return {
          content: [
            {
              type: "text",
              text: `工具执行错误: ${error.message}`
            }
          ],
          isError: true
        };
      }
    });
  }

  // 文件读取工具实现
  private async handleReadFile(args: any) {
    const { filepath } = args;
    
    // 输入验证
    if (!filepath || typeof filepath !== 'string') {
      throw new Error('文件路径无效');
    }

    // 安全检查 - 防止路径遍历攻击
    const normalizedPath = path.normalize(filepath);
    if (normalizedPath.includes('..')) {
      throw new Error('不允许访问上级目录');
    }

    try {
      const content = await fs.readFile(normalizedPath, 'utf8');
      return {
        content: [
          {
            type: "text",
            text: `文件内容 (${filepath}):\n${content}`
          }
        ]
      };
    } catch (error) {
      throw new Error(`读取文件失败: ${error.message}`);
    }
  }

  // 文件写入工具实现  
  private async handleWriteFile(args: any) {
    const { filepath, content } = args;
    
    if (!filepath || !content) {
      throw new Error('文件路径和内容都是必需的');
    }

    // 安全检查
    const normalizedPath = path.normalize(filepath);
    if (normalizedPath.includes('..')) {
      throw new Error('不允许访问上级目录');
    }

    try {
      await fs.writeFile(normalizedPath, content, 'utf8');
      return {
        content: [
          {
            type: "text", 
            text: `文件写入成功: ${filepath}`
          }
        ]
      };
    } catch (error) {
      throw new Error(`写入文件失败: ${error.message}`);
    }
  }

  // 计算工具实现
  private async handleCalculate(args: any) {
    const { expression } = args;
    
    if (!expression || typeof expression !== 'string') {
      throw new Error('表达式无效');
    }

    // 安全的数学表达式求值
    const sanitizedExpression = expression.replace(/[^0-9+-*/().\s]/g, '');
    
    try {
      // 使用Function构造器安全地执行数学运算
      const result = Function('"use strict"; return (' + sanitizedExpression + ')')();
      
      return {
        content: [
          {
            type: "text",
            text: `计算结果: ${expression} = ${result}`
          }
        ]
      };
    } catch (error) {
      throw new Error(`计算错误: 表达式无效`);
    }
  }

  // 天气API工具实现
  private async handleFetchWeather(args: any) {
    const { city, country = 'CN' } = args;
    
    if (!city) {
      throw new Error('城市名称是必需的');
    }

    // 模拟天气API调用
    const mockWeatherData = {
      city,
      country,
      temperature: Math.floor(Math.random() * 30) + 5,
      condition: ['晴天', '多云', '小雨', '阴天'][Math.floor(Math.random() * 4)],
      humidity: Math.floor(Math.random() * 50) + 30,
      timestamp: new Date().toLocaleString('zh-CN')
    };

    return {
      content: [
        {
          type: "text",
          text: `${city} 天气信息:\n温度: ${mockWeatherData.temperature}°C\n天气: ${mockWeatherData.condition}\n湿度: ${mockWeatherData.humidity}%\n更新时间: ${mockWeatherData.timestamp}`
        }
      ]
    };
  }

  // 数据处理工具实现
  private async handleProcessData(args: any) {
    const { data, operation, field } = args;
    
    if (!Array.isArray(data)) {
      throw new Error('数据必须是数组格式');
    }

    let result: any;
    const values = field ? data.map(item => item[field]).filter(val => typeof val === 'number') : data.filter(val => typeof val === 'number');

    switch (operation) {
      case 'sum':
        result = values.reduce((acc, val) => acc + val, 0);
        break;
      case 'average':
        result = values.length > 0 ? values.reduce((acc, val) => acc + val, 0) / values.length : 0;
        break;
      case 'max':
        result = values.length > 0 ? Math.max(...values) : null;
        break;
      case 'min':
        result = values.length > 0 ? Math.min(...values) : null;
        break;
      case 'count':
        result = values.length;
        break;
      default:
        throw new Error(`不支持的操作: ${operation}`);
    }

    return {
      content: [
        {
          type: "text",
          text: `数据处理结果:\n操作: ${operation}\n${field ? `字段: ${field}\n` : ''}结果: ${result}\n处理项数: ${values.length}`
        }
      ]
    };
  }

  // 启动服务器
  async run() {
    const transport = new StdioServerTransport();
    await this.server.connect(transport);
    console.error("高级工具服务器已启动");
  }
}

// 启动服务器
const server = new MCPToolServer();
server.run().catch(console.error);

工具注解(Tool Annotations)

MCP支持工具注解,提供关于工具行为的额外元数据:

// 带注解的工具定义示例
{
  name: "delete_file",
  description: "删除指定文件", 
  inputSchema: {
    type: "object",
    properties: {
      filepath: { type: "string" }
    },
    required: ["filepath"]
  },
  annotations: {
    title: "文件删除工具",
    readOnlyHint: false,      // 会修改环境
    destructiveHint: true,    // 可能造成破坏性更新
    idempotentHint: false,    // 重复调用有额外效果
    openWorldHint: false      // 封闭系统操作
  }
}

最佳实践

1. 安全考虑

// 输入验证示例
private validateInput(args: any, schema: any): boolean {
  // 实现JSON Schema验证
  // 清理文件路径
  // 验证URL和外部标识符
  // 检查参数大小和范围
  // 防止命令注入
  return true;
}

// 访问控制
private async checkPermissions(toolName: string, args: any): Promise<boolean> {
  // 实现身份验证
  // 使用适当的授权检查
  // 审计工具使用
  // 速率限制
  return true;
}

2. 错误处理

private handleToolError(error: Error, toolName: string) {
  // 记录安全相关错误
  // 不向客户端暴露内部错误
  // 适当处理超时
  // 清理资源
  
  return {
    content: [
      {
        type: "text",
        text: `工具 ${toolName} 执行失败: ${error.message}`
      }
    ],
    isError: true
  };
}

3. 性能优化

// 进度报告示例
private async longRunningOperation(onProgress: (progress: number) => void) {
  for (let i = 0; i < 100; i++) {
    // 执行操作
    await new Promise(resolve => setTimeout(resolve, 10));
    onProgress((i + 1) / 100);
  }
}

// 超时处理
private async withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
  const timeout = new Promise<never>((_, reject) =>
    setTimeout(() => reject(new Error('操作超时')), timeoutMs)
  );
  
  return Promise.race([promise, timeout]);
}

测试策略

// 测试示例
import { jest } from '@jest/globals';

describe('MCP工具测试', () => {
  let server: MCPToolServer;

  beforeEach(() => {
    server = new MCPToolServer();
  });

  test('计算工具 - 正常情况', async () => {
    const result = await server.handleCalculate({ expression: '2 + 3' });
    expect(result.content[0].text).toContain('= 5');
  });

  test('文件读取 - 安全检查', async () => {
    try {
      await server.handleReadFile({ filepath: '../../../etc/passwd' });
      fail('应该抛出安全错误');
    } catch (error) {
      expect(error.message).toContain('不允许访问上级目录');
    }
  });

  test('数据处理 - 求平均值', async () => {
    const result = await server.handleProcessData({
      data: [1, 2, 3, 4, 5],
      operation: 'average'
    });
    expect(result.content[0].text).toContain('结果: 3');
  });
});

部署和集成

客户端连接示例

import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";

async function connectToMCPServer() {
  const client = new Client(
    {
      name: "mcp-client",
      version: "1.0.0"
    },
    {
      capabilities: {}
    }
  );

  const transport = new StdioClientTransport({
    command: "node",
    args: ["./dist/server.js"]
  });

  await client.connect(transport);
  
  // 列出可用工具
  const tools = await client.request(
    { method: "tools/list" },
    {}
  );
  
  console.log("可用工具:", tools);
  
  // 调用工具
  const result = await client.request(
    { method: "tools/call" },
    {
      name: "calculate",
      arguments: { expression: "10 * 5 + 2" }
    }
  );
  
  console.log("计算结果:", result);
}

总结

MCP工具为AI应用提供了强大而灵活的扩展机制。通过TypeScript SDK,开发者可以轻松构建安全、高效的工具服务器,让AI模型能够与现实世界进行丰富的交互。

关键要点:

  • 标准化接口:MCP提供统一的工具发现和调用机制
  • 安全第一:实施严格的输入验证和访问控制
  • 灵活扩展:支持从简单计算到复杂API集成的各种场景
  • 开发友好:TypeScript SDK提供完整的类型支持和丰富的功能

随着AI技术的不断发展,MCP工具将成为构建智能、互联AI应用的重要基础设施。掌握MCP工具开发,就是掌握了AI时代的核心技能之一。