MCP服务器开发指南 (4)

这是关于构建MCP服务器的四部分教程的最后一部分。在第一部分中,我们创建了一个具有基本资源的MCP服务器。在第二部分中,我们添加了资源模板并改进了代码组织,在第三部分中,我们添加了提示并进一步优化了结构。现在我们将通过添加工具来完成我们的服务器。

1、什么是MCP工具?

工具是可以调用以执行操作或检索动态信息的可执行函数。与只读的资源和结构化LLM交互的提示不同,工具允许LLM主动执行一些事情,如计算值、进行API调用或修改数据。

2、为什么要使用工具?

工具使LLM能够与系统交互并执行操作。以下是一些实际示例:

文件操作:

name: "write-file"  
arguments: {  
  path: "/logs/report.txt",  
  content: "每日总结..."  
}

用户:

将此报告保存到文件中

AI:

我将使用write-file工具……文件已成功保存。

API交互:

name: "fetch-weather"  
arguments: {  
  location: "旧金山",  
  units: "摄氏度"  
}

用户:

旧金山的天气如何?

AI:

让我查一下……根据天气API,气温为18°C,天气晴朗。

数据处理:

name: "analyze-data"  
arguments: {  
  dataset: "sales_2024_q1",  
  operation: "summary_stats"  
}

用户:

计算第一季度销售的汇总统计

AI:

正在运行分析……平均销售额为342,中位数为342,……

3、添加工具

为我们的新工具创建一个新文件,并添加一个“create-message”工具:

// src/tools.ts  
  
// 允许的值  
const messageTypes = ['greeting', 'farewell', 'thank-you'] as const;  
const tones = ['formal', 'casual', 'playful'] as const;  
  
// 工具定义  
export const tools = {  
  'create-message': {  
    name: 'create-message',  
    description: '生成带有各种选项的自定义消息',  
    inputSchema: {  
      type: 'object',  
      properties: {  
        messageType: {  
          type: 'string',  
          enum: messageTypes,  
          description: '要生成的消息类型',  
        },  
        recipient: {  
          type: 'string',  
          description: '消息的接收者姓名',  
        },  
        tone: {  
          type: 'string',  
          enum: tones,  
          description: '消息的语气',  
        },  
      },  
      required: ['messageType', 'recipient'],  
    },  
  },  
};

到目前为止,我们添加的只是一个描述,它将使使用它的模型理解其功能以及期望的信息类型。

现在让我们添加实际的处理器:

// src/tools.ts  
  
// 允许的值  
const messageTypes = ['greeting', 'farewell', 'thank-you'] as const;  
const tones = ['formal', 'casual', 'playful'] as const;  
  
// 工具定义  
export const tools = {  
  // ... 现有的定义  
};  
  
type CreateMessageArgs = {  
  messageType: typeof messageTypes[number];  
  recipient: string;  
  tone?: typeof tones[number];  
};  
  
// 各种消息组合的简单模板  
const messageFns = {  
  greeting: {  
    formal: (recipient: string) =>  
      `亲爱的${recipient},希望这封信能让你一切安好`,  
    playful: (recipient: string) => `嘿嘿 ${recipient}!🎉 有什么新鲜事吗?`,  
    casual: (recipient: string) => `嗨 ${recipient}!你好吗?`,  
  },  
  farewell: {  
    formal: (recipient: string) =>  
      `祝好,${recipient}。期待再次相见。`,  
    playful: (recipient: string) =>  
      `再见啦,${recipient}!👋 保持精彩!`,  
    casual: (recipient: string) => `再见 ${recipient},保重!`,  
  },  
  "thank-you": {  
    formal: (recipient: string) =>  
      `亲爱的${recipient},我真诚地感谢你的帮助。`,  
    playful: (recipient: string) =>  
      `你是最棒的,${recipient}!🌟 谢谢你一百万次!`,  
    casual: (recipient: string) =>  
      `非常感谢你,${recipient}!真的很感激!`,  
  },  
};  
  
const createMessage = (args: CreateMessageArgs) => {  
  if (!args.messageType) throw new Error("必须提供消息类型。");  
  if (!args.recipient) throw new Error("必须提供接收者。");  
  
  const { messageType, recipient } = args;  
  const tone = args.tone || "casual";  
  if (!messageTypes.includes(messageType)) {  
    throw new Error(  
      `消息类型必须是以下之一:${messageTypes.join(", ")}`,  
    );  
  }  
  if (!tones.includes(tone)) {  
    throw new Error(  
      `如果提供了语气,则必须是以下之一:${  
        tones.join(", ")  
      }`,  
    );  
  }  
  
  const message = messageFns[messageType][tone](recipient);  
  
  return {  
    content: [  
      {  
        type: "text",  
        text: message,  
      },  
    ],  
  };  
};  
  
export const toolHandlers = {  
  "create-message": createMessage,  
};

现在让我们更新处理器:

// src/handlers.ts  
import {  
  CallToolRequestSchema, // <-- 添加这个  
  GetPromptRequestSchema,  
  ListPromptsRequestSchema,  
  ListResourcesRequestSchema,  
  ListResourceTemplatesRequestSchema,  
  ListToolsRequestSchema, // <-- 并添加这个  
  ReadResourceRequestSchema,  
} from "@modelcontextprotocol/sdk/types.js";  
import { resourceHandlers, resources } from "./resources.js";  
import {  
  getResourceTemplate,  
  resourceTemplates,  
} from "./resource-templates.js";  
import { promptHandlers, prompts } from "./prompts.js";  
import { toolHandlers, tools } from "./tools.js"; // <-- 导入我们的工具  
import type { Server } from "@modelcontextprotocol/sdk/server/index.js";  
  
export const setupHandlers = (server: Server): void => {  
  // 当客户端请求时列出可用资源
  
  // ... 之前创建的处理器在这里
  
  // 工具  
  server.setRequestHandler(ListToolsRequestSchema, async () => ({  
    tools: Object.values(tools),  
  }));  
  
  server.setRequestHandler(CallToolRequestSchema, async (request) => {  
    type ToolHandlerKey = keyof typeof toolHandlers;  
    const { name, arguments: params } = request.params ?? {};  
    const handler = toolHandlers[name as ToolHandlerKey];  
  
    if (!handler) throw new Error("未找到工具");  
  
    type HandlerParams = Parameters<typeof handler>;  
    return handler(...[params] as HandlerParams);  
  });  
};

最后,更新服务器初始化:

// src/index.ts  
import { Server } from "@modelcontextprotocol/sdk/server/index.js";  
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";  
import { setupHandlers } from "./handlers.js";  
  
const server = new Server(  
  {  
    name: "hello-mcp",  
    version: "1.0.0",  
  },  
  {  
    capabilities: {  
      resources: {},  
      prompts: {},  
      tools: {}, // <-- 添加工具能力  
    },  
  },  
);  
  
setupHandlers(server);  
  
// 使用stdio传输启动服务器  
const transport = new StdioServerTransport();  
await server.connect(transport);  
  
console.info(  
  '{"jsonrpc": "2.0", "method": "log", "params": { "message": "服务器运行中..." }}',  
);

工具结构:

  • 工具通过inputSchema定义其接口
  • 处理器实现实际功能
  • 返回格式符合MCP规范

错误处理:

  • 必需参数验证
  • 特定错误消息
  • 类型安全的处理器访问

4、使用Inspector测试

记得先构建输出:

npm run build

然后启动Inspector:

npx @modelcontextprotocol/inspector node build/index.js

测试工具:

  • 点击“工具”标签
  • 查找“create-message”
  • 尝试不同的组合:
{  
  "messageType": "thank-you",  
  "recipient": "Alice",  
  "tone": "playful"  
}

5、使用Claude桌面测试

尝试以下示例

注意:您可能会收到授权工具使用的请求:

基本消息:

用户:

为Bob创建一条问候消息

Claude:

我将使用消息工具……‘嗨Bob!你好吗?’

样式化消息:

用户:

给Alice发送一条有趣的感谢

Claude:

使用消息工具……‘你是最棒的,Alice!🌟 谢谢你一百万次!’

不同消息类型:

用户:

你能创建哪些类型的消息?

Claude:

我可以帮助你使用create-message函数创建不同类型的消息。你可以生成:

1. 问候
2. 再见
3. 感谢消息

对于每种类型,你可以指定接收者,并且可以设置语气为正式、随意或有趣。你想让我演示创建特定类型的消息吗?

您的结果可能与此不同,特别是如果您使用的是不同于Claude的不同工具或不同于Sonnet 3.5的不同模型

6、结束语

恭喜!你现在构建了一个完整的MCP服务器,包括:

  • 静态和基于模板的资源
  • 可定制的提示
  • 动态工具
  • 结构良好、类型安全的代码

你已经学会了如何:

  • 构建MCP服务器
  • 实现不同的MCP功能
  • 有效组织代码
  • 优雅地处理错误
  • 使用Inspector进行测试
  • 与Claude桌面集成

从这里开始,你可以:

  • 添加更复杂的工具
  • 集成外部API
  • 实现文件操作
  • 添加数据库连接
  • 创建自己的自定义功能

请记住,MCP是一个不断发展的协议,因此请密切关注官方文档中的新功能和最佳实践。


原文链接:Building MCP Servers: Part 4 — Creating Tools

汇智网翻译整理,转载请标明出处