工具是 Model Context Protocol (MCP) 中的一个强大原语,它使服务器能够向客户端暴露可执行的功能。通过工具,LLM 可以与外部系统交互、执行计算并在现实世界中采取行动。
工具被设计为模型可控的,这意味着工具从服务器暴露给客户端时,目的是让 AI 模型能够自动调用它们(在人工监督下获得批准)。
MCP 中的工具允许服务器暴露可执行的函数,这些函数可以被客户端调用并被 LLM 用来执行操作。工具的关键方面包括:
- 发现:客户端可以通过
tools/list
端点列出可用工具
- 调用:工具通过
tools/call
端点调用,服务器执行请求的操作并返回结果
- 灵活性:工具可以从简单的计算到复杂的 API 交互
与资源一样,工具由唯一的名称标识,并可以包含描述来指导其使用。但是,与资源不同,工具代表可以修改状态或与外部系统交互的动态操作。
工具定义结构
每个工具都使用以下结构定义:
{
name: string; // 工具的唯一标识符
description?: string; // 人类可读的描述
inputSchema: { // 工具参数的 JSON Schema
type: "object",
properties: { ... } // 工具特定的参数
},
annotations?: { // 可选的工具行为提示
title?: string; // 工具的人类可读标题
readOnlyHint?: boolean; // 如果为 true,工具不会修改其环境
destructiveHint?: boolean; // 如果为 true,工具可能执行破坏性更新
idempotentHint?: boolean; // 如果为 true,使用相同参数重复调用没有额外效果
openWorldHint?: boolean; // 如果为 true,工具与外部实体交互
}
}
实现工具
以下是在 MCP 服务器中实现基本工具的示例:
const server = new Server({
name: "example-server",
version: "1.0.0"
}, {
capabilities: {
tools: {}
}
});
// 定义可用工具
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [{
name: "calculate_sum",
description: "将两个数字相加",
inputSchema: {
type: "object",
properties: {
a: { type: "number" },
b: { type: "number" }
},
required: ["a", "b"]
}
}]
};
});
// 处理工具执行
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "calculate_sum") {
const { a, b } = request.params.arguments;
return {
content: [
{
type: "text",
text: String(a + b)
}
]
};
}
throw new Error("未找到工具");
});
const server = new Server({
name: "example-server",
version: "1.0.0"
}, {
capabilities: {
tools: {}
}
});
// 定义可用工具
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [{
name: "calculate_sum",
description: "将两个数字相加",
inputSchema: {
type: "object",
properties: {
a: { type: "number" },
b: { type: "number" }
},
required: ["a", "b"]
}
}]
};
});
// 处理工具执行
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === "calculate_sum") {
const { a, b } = request.params.arguments;
return {
content: [
{
type: "text",
text: String(a + b)
}
]
};
}
throw new Error("未找到工具");
});
app = Server("example-server")
@app.list_tools()
async def list_tools() -> list[types.Tool]:
return [
types.Tool(
name="calculate_sum",
description="将两个数字相加",
inputSchema={
"type": "object",
"properties": {
"a": {"type": "number"},
"b": {"type": "number"}
},
"required": ["a", "b"]
}
)
]
@app.call_tool()
async def call_tool(
name: str,
arguments: dict
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
if name == "calculate_sum":
a = arguments["a"]
b = arguments["b"]
result = a + b
return [types.TextContent(type="text", text=str(result))]
raise ValueError(f"未找到工具:{name}")
工具模式示例
以下是服务器可以提供的一些工具类型示例:
系统操作
与本地系统交互的工具:
{
name: "execute_command",
description: "运行 shell 命令",
inputSchema: {
type: "object",
properties: {
command: { type: "string" },
args: { type: "array", items: { type: "string" } }
}
}
}
API 集成
封装外部 API 的工具:
{
name: "github_create_issue",
description: "创建 GitHub issue",
inputSchema: {
type: "object",
properties: {
title: { type: "string" },
body: { type: "string" },
labels: { type: "array", items: { type: "string" } }
}
}
}
数据处理
转换或分析数据的工具:
{
name: "analyze_csv",
description: "分析 CSV 文件",
inputSchema: {
type: "object",
properties: {
filepath: { type: "string" },
operations: {
type: "array",
items: {
enum: ["sum", "average", "count"]
}
}
}
}
}
最佳实践
在实现工具时:
- 提供清晰、描述性的名称和描述
- 使用详细的 JSON Schema 定义参数
- 在工具描述中包含示例来演示模型应如何使用它们
- 实现适当的错误处理和验证
- 对长时间操作使用进度报告
- 保持工具操作的重点和原子性
- 记录预期的返回值结构
- 实现适当的超时机制
- 考虑资源密集型操作的速率限制
- 记录工具使用情况以进行调试和监控
安全考虑
在暴露工具时:
输入验证
- 根据 schema 验证所有参数
- 净化文件路径和系统命令
- 验证 URL 和外部标识符
- 检查参数大小和范围
- 防止命令注入
访问控制
- 在需要时实现身份验证
- 使用适当的授权检查
- 审计工具使用情况
- 限制请求速率
- 监控滥用情况
错误处理
- 不要向客户端暴露内部错误
- 记录安全相关错误
- 适当处理超时
- 在错误后清理资源
- 验证返回值
工具发现和更新
MCP 支持动态工具发现:
- 客户端可以随时列出可用工具
- 服务器可以使用
notifications/tools/list_changed
通知客户端工具变化
- 工具可以在运行时添加或删除
- 工具定义可以更新(但应谨慎进行)
错误处理
工具错误应该在结果对象中报告,而不是作为 MCP 协议级别的错误。这允许 LLM 看到并可能处理错误。当工具遇到错误时:
- 在结果中将
isError
设置为 true
- 在
content
数组中包含错误详情
以下是工具正确错误处理的示例:
try {
// 工具操作
const result = performOperation();
return {
content: [
{
type: "text",
text: `操作成功:${result}`
}
]
};
} catch (error) {
return {
isError: true,
content: [
{
type: "text",
text: `错误:${error.message}`
}
]
};
}
try {
// 工具操作
const result = performOperation();
return {
content: [
{
type: "text",
text: `操作成功:${result}`
}
]
};
} catch (error) {
return {
isError: true,
content: [
{
type: "text",
text: `错误:${error.message}`
}
]
};
}
try:
# 工具操作
result = perform_operation()
return types.CallToolResult(
content=[
types.TextContent(
type="text",
text=f"操作成功:{result}"
)
]
)
except Exception as error:
return types.CallToolResult(
isError=True,
content=[
types.TextContent(
type="text",
text=f"错误:{str(error)}"
)
]
)
这种方法允许 LLM 看到发生了错误,并可能采取纠正措施或请求人工干预。
工具注解
工具注解提供了关于工具行为的额外元数据,帮助客户端理解如何展示和管理工具。这些注解是描述工具性质和影响的提示,但不应用于安全决策。
工具注解的目的
工具注解有几个关键目的:
- 提供特定于 UX 的信息,而不影响模型上下文
- 帮助客户端适当地分类和展示工具
- 传达有关工具潜在副作用的信息
- 协助开发直观的工具批准界面
可用的工具注解
MCP 规范为工具定义了以下注解:
注解 | 类型 | 默认值 | 描述 |
---|
title | string | - | 工具的人类可读标题,适用于 UI 显示 |
readOnlyHint | boolean | false | 如果为 true,表示工具不会修改其环境 |
destructiveHint | boolean | true | 如果为 true,工具可能执行破坏性更新(仅当 readOnlyHint 为 false 时有意义) |
idempotentHint | boolean | false | 如果为 true,使用相同参数重复调用工具没有额外效果(仅当 readOnlyHint 为 false 时有意义) |
openWorldHint | boolean | true | 如果为 true,工具可能与外部实体的”开放世界”交互 |
使用示例
以下是如何为不同场景定义带有注解的工具:
// 只读搜索工具
{
name: "web_search",
description: "在网上搜索信息",
inputSchema: {
type: "object",
properties: {
query: { type: "string" }
},
required: ["query"]
},
annotations: {
title: "网络搜索",
readOnlyHint: true,
openWorldHint: true
}
}
// 破坏性文件删除工具
{
name: "delete_file",
description: "从文件系统删除文件",
inputSchema: {
type: "object",
properties: {
path: { type: "string" }
},
required: ["path"]
},
annotations: {
title: "删除文件",
readOnlyHint: false,
destructiveHint: true,
idempotentHint: true,
openWorldHint: false
}
}
// 非破坏性数据库记录创建工具
{
name: "create_record",
description: "在数据库中创建新记录",
inputSchema: {
type: "object",
properties: {
table: { type: "string" },
data: { type: "object" }
},
required: ["table", "data"]
},
annotations: {
title: "创建数据库记录",
readOnlyHint: false,
destructiveHint: false,
idempotentHint: false,
openWorldHint: false
}
}
在服务器实现中集成注解
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [{
name: "calculate_sum",
description: "将两个数字相加",
inputSchema: {
type: "object",
properties: {
a: { type: "number" },
b: { type: "number" }
},
required: ["a", "b"]
},
annotations: {
title: "计算总和",
readOnlyHint: true,
openWorldHint: false
}
}]
};
});
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [{
name: "calculate_sum",
description: "将两个数字相加",
inputSchema: {
type: "object",
properties: {
a: { type: "number" },
b: { type: "number" }
},
required: ["a", "b"]
},
annotations: {
title: "计算总和",
readOnlyHint: true,
openWorldHint: false
}
}]
};
});
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("example-server")
@mcp.tool(
annotations={
"title": "计算总和",
"readOnlyHint": True,
"openWorldHint": False
}
)
async def calculate_sum(a: float, b: float) -> str:
"""将两个数字相加。
参数:
a: 要相加的第一个数字
b: 要相加的第二个数字
"""
result = a + b
return str(result)
工具注解的最佳实践
-
准确描述副作用:清楚地指出工具是否修改其环境以及这些修改是否具有破坏性。
-
使用描述性标题:提供清楚描述工具用途的人性化标题。
-
正确指示幂等性:只有当使用相同参数重复调用确实没有额外效果时,才将工具标记为幂等。
-
设置适当的开放/封闭世界提示:指出工具是与封闭系统(如数据库)还是开放系统(如网络)交互。
-
记住注解是提示:ToolAnnotations 中的所有属性都是提示,不保证能忠实描述工具行为。客户端不应仅基于注解做出安全关键决策。
测试工具
MCP 工具的全面测试策略应该涵盖:
- 功能测试:验证工具使用有效输入时正确执行,并适当处理无效输入
- 集成测试:使用真实和模拟的依赖项测试工具与外部系统的交互
- 安全测试:验证身份验证、授权、输入净化和速率限制
- 性能测试:检查负载下的行为、超时处理和资源清理
- 错误处理:确保工具通过 MCP 协议正确报告错误并清理资源