MCP协议 进阶 MCP 工具注册 工具管理 Server开发

MCP工具注册与管理:构建可扩展的工具系统

AIEng Hub
阅读约 16 分钟

引言

随着MCP Server提供的工具数量增长,如何高效地注册、组织和管理工具成为关键问题。本文将介绍从单一工具到大规模工具系统的架构演进。

工具注册基础

静态注册模式

最基本的工具注册方式——在代码中硬编码工具定义:

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";

const server = new Server(
  { name: "tool-server", version: "1.0.0" },
  { capabilities: { tools: {} } }
);

// 静态工具定义
const toolDefinitions = [
  {
    name: "search_docs",
    description: "搜索文档内容",
    inputSchema: {
      type: "object",
      properties: {
        query: { type: "string" },
      },
      required: ["query"],
    },
  },
  {
    name: "get_file",
    description: "读取文件内容",
    inputSchema: {
      type: "object",
      properties: {
        path: { type: "string" },
      },
      required: ["path"],
    },
  },
];

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: toolDefinitions,
}));

适用场景: 工具数量固定、规模较小(< 5个)的Server。

注册器模式

引入工具注册中心,集中管理工具生命周期:

// src/registry/tool-registry.ts
import { Server } from "@modelcontextprotocol/sdk/server/index.js";

interface ToolDefinition {
  name: string;
  description: string;
  inputSchema: object;
  handler: (args: any) => Promise<ToolResult>;
}

class ToolRegistry {
  private tools: Map<string, ToolDefinition> = new Map();

  // 注册一个工具
  register(tool: ToolDefinition): void {
    if (this.tools.has(tool.name)) {
      throw new Error(`工具 '${tool.name}' 已注册`);
    }
    this.tools.set(tool.name, tool);
  }

  // 批量注册工具
  registerAll(tools: ToolDefinition[]): void {
    for (const tool of tools) {
      this.register(tool);
    }
  }

  // 获取所有工具定义
  getDefinitions(): ToolDefinition[] {
    return Array.from(this.tools.values()).map(({ handler, ...def }) => def);
  }

  // 执行工具
  async execute(name: string, args: any): Promise<ToolResult> {
    const tool = this.tools.get(name);
    if (!tool) {
      throw new Error(`工具 '${name}' 未找到`);
    }
    return tool.handler(args);
  }

  // 注销工具
  unregister(name: string): void {
    this.tools.delete(name);
  }

  // 检查工具是否存在
  has(name: string): boolean {
    return this.tools.has(name);
  }
}

export const toolRegistry = new ToolRegistry();

装饰器模式(TypeScript)

// 使用装饰器进行声明式注册
function tool(config: {
  name: string;
  description: string;
  inputSchema: object;
}) {
  return function (
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    toolRegistry.register({
      ...config,
      handler: descriptor.value.bind(target),
    });
    return descriptor;
  };
}

class SearchTools {
  @tool({
    name: "search_docs",
    description: "搜索文档",
    inputSchema: {
      type: "object",
      properties: {
        query: { type: "string" },
      },
      required: ["query"],
    },
  })
  async searchDocs(args: { query: string }) {
    return {
      content: [{ type: "text", text: `搜索结果: ${args.query}` }],
      isError: false,
    };
  }

  @tool({
    name: "search_code",
    description: "搜索代码",
    inputSchema: {
      type: "object",
      properties: {
        pattern: { type: "string" },
      },
      required: ["pattern"],
    },
  })
  async searchCode(args: { pattern: string }) {
    return {
      content: [{ type: "text", text: `代码搜索结果: ${args.pattern}` }],
      isError: false,
    };
  }
}

动态注册与热加载

文件系统驱动的动态注册

import fs from "fs/promises";
import path from "path";

class DynamicToolLoader {
  private toolsDir: string;

  constructor(toolsDir: string) {
    this.toolsDir = toolsDir;
  }

  async loadToolsFromFiles(): Promise<ToolDefinition[]> {
    const files = await fs.readdir(this.toolsDir);
    const tools: ToolDefinition[] = [];

    for (const file of files) {
      if (!file.endsWith(".tool.js") && !file.endsWith(".tool.ts")) {
        continue;
      }

      try {
        const module = await import(path.join(this.toolsDir, file));
        if (module.default && module.default.tool) {
          tools.push(module.default.tool);
        }
      } catch (error) {
        console.error(`加载工具文件 ${file} 失败:`, error);
      }
    }

    return tools;
  }
}

// 工具文件示例: search.tool.ts
export default {
  tool: {
    name: "search",
    description: "搜索内容",
    inputSchema: {
      type: "object",
      properties: {
        query: { type: "string" },
      },
      required: ["query"],
    },
    handler: async (args: { query: string }) => ({
      content: [{ type: "text", text: `搜索: ${args.query}` }],
      isError: false,
    }),
  },
};

工具分组与命名空间

命名空间策略

为大型工具集引入命名空间管理:

// 工具命名空间
class NamespacedRegistry {
  private namespaces: Map<string, ToolRegistry> = new Map();

  // 创建命名空间
  createNamespace(name: string): ToolRegistry {
    if (this.namespaces.has(name)) {
      throw new Error(`命名空间 '${name}' 已存在`);
    }
    const registry = new ToolRegistry();
    this.namespaces.set(name, registry);
    return registry;
  }

  // 获取命名空间下的所有工具
  getTools(namespace?: string): ToolDefinition[] {
    if (namespace) {
      const registry = this.namespaces.get(namespace);
      return registry ? registry.getDefinitions() : [];
    }

    // 返回所有工具
    const allTools: ToolDefinition[] = [];
    for (const [, registry] of this.namespaces) {
      allTools.push(...registry.getDefinitions());
    }
    return allTools;
  }

  // 执行命名空间下的工具
  async execute(fullName: string, args: any): Promise<ToolResult> {
    const parts = fullName.split(".");
    if (parts.length === 2) {
      const [namespace, toolName] = parts;
      const registry = this.namespaces.get(namespace);
      if (registry) {
        return registry.execute(toolName, args);
      }
    }
    throw new Error(`工具 '${fullName}' 未找到`);
  }
}

// 使用示例
const registry = new NamespacedRegistry();

// 文件系统工具组
const fsNs = registry.createNamespace("fs");
fsNs.register({
  name: "read",
  description: "读取文件",
  inputSchema: { /* ... */ },
  handler: async (args) => { /* ... */ },
});
fsNs.register({
  name: "write",
  description: "写入文件",
  inputSchema: { /* ... */ },
  handler: async (args) => { /* ... */ },
});

// 数据库工具组
const dbNs = registry.createNamespace("db");
dbNs.register({
  name: "query",
  description: "执行查询",
  inputSchema: { /* ... */ },
  handler: async (args) => { /* ... */ },
});

// 工具列表: "fs.read", "fs.write", "db.query"

参数校验中间件

实现参数校验层

import { z } from "zod";

// 参数校验中间件
function withValidation(schema: z.ZodSchema) {
  return (handler: Function) => {
    return async (args: unknown) => {
      try {
        const validated = schema.parse(args);
        return await handler(validated);
      } catch (error) {
        if (error instanceof z.ZodError) {
          return {
            content: [{
              type: "text",
              text: `参数验证失败:\n${error.errors
                .map(e => `  - ${e.path.join(".")}: ${e.message}`)
                .join("\n")}`
            }],
            isError: true,
          };
        }
        throw error;
      }
    };
  };
}

// 使用
const searchSchema = z.object({
  query: z.string().min(1, "查询不能为空").max(200, "查询太长"),
  limit: z.number().int().min(1).max(100).default(20),
  offset: z.number().int().min(0).default(0),
});

const searchHandler = withValidation(searchSchema)(async (args) => {
  // args 已通过Zod校验,类型安全
  return {
    content: [{ type: "text", text: `搜索 ${args.query}` }],
    isError: false,
  };
});

运行时管理

工具统计与监控

// 工具调用统计
class ToolStats {
  private calls: Map<string, {
    count: number;
    errors: number;
    totalDuration: number;
    lastCalled: Date;
  }> = new Map();

  recordCall(name: string, duration: number, isError: boolean): void {
    const stats = this.calls.get(name) || {
      count: 0,
      errors: 0,
      totalDuration: 0,
      lastCalled: new Date(),
    };

    stats.count++;
    stats.totalDuration += duration;
    if (isError) stats.errors++;
    stats.lastCalled = new Date();

    this.calls.set(name, stats);
  }

  getReport(): string {
    let report = "工具调用统计:\n";
    report += "=".repeat(40) + "\n";

    for (const [name, stats] of this.calls) {
      const avgDuration = stats.totalDuration / stats.count;
      report += `${name}:\n`;
      report += `  调用次数: ${stats.count}\n`;
      report += `  错误次数: ${stats.errors}\n`;
      report += `  平均耗时: ${avgDuration.toFixed(2)}ms\n`;
      report += `  最后调用: ${stats.lastCalled.toISOString()}\n`;
    }

    return report;
  }
}

限流控制

// 简单的令牌桶限流器
class RateLimiter {
  private tokens: Map<string, {
    count: number;
    resetAt: number;
  }> = new Map();

  constructor(
    private maxTokens: number = 10,
    private windowMs: number = 60000
  ) {}

  checkLimit(toolName: string): boolean {
    const now = Date.now();
    let entry = this.tokens.get(toolName);

    if (!entry || now > entry.resetAt) {
      entry = { count: 0, resetAt: now + this.windowMs };
      this.tokens.set(toolName, entry);
    }

    entry.count++;
    return entry.count <= this.maxTokens;
  }
}

// 在调用处理中使用
const rateLimiter = new RateLimiter(10, 60000); // 每分钟10次

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name } = request.params;

  if (!rateLimiter.checkLimit(name)) {
    return {
      content: [{
        type: "text",
        text: `工具 '${name}' 调用频率过高,请稍后再试`,
      }],
      isError: true,
    };
  }

  // 正常处理...
});

最佳实践

工具组织架构

src/
├── tools/
│   ├── index.ts          # 统一注册入口
│   ├── file-tools/       # 文件工具组
│   │   ├── read.ts
│   │   ├── write.ts
│   │   └── search.ts
│   ├── db-tools/         # 数据库工具组
│   │   ├── query.ts
│   │   └── schema.ts
│   └── api-tools/        # API工具组
│       ├── http.ts
│       └── websocket.ts
├── registry/
│   ├── tool-registry.ts
│   └── validation.ts
├── middleware/
│   ├── rate-limit.ts
│   └── logging.ts
└── index.ts

工具管理清单

场景推荐方案
< 5个工具静态注册,代码硬编码
5-20个工具注册器模式 + 文件组织
20+个工具命名空间 + 动态加载
需要权限控制中间件链模式
需要监控调用统计 + 日志
需要热更新文件系统监听 + 动态注册

总结

工具注册与管理是构建生产级MCP Server的基础:

模式适用场景优势
静态注册简单Server简单直接
注册器模式中型Server集中管理
命名空间大型Server逻辑清晰
动态加载插件化体系高度灵活

下一步学习建议:


本文最后更新于 2024-07-06。