实现工具
工具提供 LLM 可以调用来执行操作或进行计算的功能。可以将它们视为扩展 LLM 能力的函数调用。
工具基础
工具是 LLM 与你的服务端交互以执行操作的主要方式。它们具有定义参数、类型和约束的结构化模式,确保类型安全的交互。
基本工具结构
// 创建一个简单的工具
tool := mcp.NewTool("calculate",
mcp.WithDescription("Perform arithmetic operations"),
mcp.WithString("operation",
mcp.Required(),
mcp.Enum("add", "subtract", "multiply", "divide"),
mcp.Description("The arithmetic operation to perform"),
),
mcp.WithNumber("x", mcp.Required(), mcp.Description("First number")),
mcp.WithNumber("y", mcp.Required(), mcp.Description("Second number")),
)工具定义
显示标题
工具可以携带与程序化 name 分开的人类可读显示标题。客户端在工具选择器和 UI 表面显示标题;name 保留用于面向模型的标识符。
tool := mcp.NewTool("calculate_arithmetic",
mcp.WithToolTitle("Calculator"), // 在 UI 中显示
mcp.WithDescription("Perform arithmetic operations"),
// ... 参数
)根据 MCP 规范,客户端应按此顺序解析显示标签:
Title(由WithToolTitle设置的顶层字段)Annotations.Title(由WithTitleAnnotation设置的传统提示,参见工具注释)Name
两个字段都是可选的;省略它们会回退到下一个候选。
工具图标
工具可以使用图标包含视觉标识符。图标必须使用 HTTPS 或数据 URI:
tool := mcp.NewTool("calculate",
mcp.WithDescription("Perform arithmetic operations"),
mcp.WithToolIcons(
mcp.Icon{
Src: "https://example.com/icons/calculator.png",
MIMEType: "image/png",
Sizes: []string{"32x32", "64x64"},
},
),
// ... 参数
)参数类型
MCP-Go 支持各种带验证的参数类型:
// 字符串参数
mcp.WithString("name",
mcp.Required(),
mcp.Description("User's name"),
mcp.MinLength(1),
mcp.MaxLength(100),
)
// 数字参数
mcp.WithNumber("age",
mcp.Required(),
mcp.Description("User's age"),
mcp.Min(0),
mcp.Max(150),
)
// 整数参数
mcp.WithInteger("count",
mcp.DefaultNumber(10),
mcp.Description("Number of items"),
mcp.Min(1),
mcp.Max(1000),
)
// 布尔参数
mcp.WithBoolean("enabled",
mcp.DefaultBool(true),
mcp.Description("Whether feature is enabled"),
)
// 数组参数
mcp.WithArray("tags",
mcp.Description("List of tags"),
mcp.Items(map[string]any{"type": "string"}),
)
// 对象参数
mcp.WithObject("config",
mcp.Description("Configuration object"),
mcp.Properties(map[string]any{
"timeout": map[string]any{"type": "number"},
"retries": map[string]any{"type": "integer"},
}),
)枚举和约束
// 枚举值
mcp.WithString("priority",
mcp.Required(),
mcp.Enum("low", "medium", "high", "critical"),
mcp.Description("Task priority level"),
)
// 字符串约束
mcp.WithString("email",
mcp.Required(),
mcp.Pattern(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`),
mcp.Description("Valid email address"),
)
// 数字约束
mcp.WithNumber("price",
mcp.Required(),
mcp.Min(0),
mcp.Max(10000),
mcp.Description("Product price in USD"),
)基于结构体的模式定义
MCP-Go 支持使用 Go 结构体定义输入和输出模式,自动生成 JSON 模式。这为手动参数定义提供了一种类型安全的替代方案,对于具有结构化输入和输出的复杂工具特别有用。
使用 Go 结构体的输入模式
将输入参数定义为 Go 结构体并使用 WithInputSchema:
// 使用 JSON 模式标签定义输入结构体
type SearchRequest struct {
Query string `json:"query" jsonschema:"Search query"`
Limit int `json:"limit,omitempty" jsonschema:"Maximum results"`
Categories []string `json:"categories,omitempty" jsonschema:"Filter by categories"`
SortBy string `json:"sortBy,omitempty" jsonschema:"Sort field"`
}
// 使用基于结构体的输入模式创建工具
searchTool := mcp.NewTool("search_products",
mcp.WithDescription("Search product catalog"),
mcp.WithInputSchema[SearchRequest](),
)使用 Go 结构体的输出模式
为可预测的工具响应定义结构化输出:
// 定义输出结构体
type SearchResponse struct {
Query string `json:"query" jsonschema:"Original search query"`
TotalCount int `json:"totalCount" jsonschema:"Total matching products"`
Products []Product `json:"products" jsonschema:"Search results"`
ProcessedAt time.Time `json:"processedAt" jsonschema:"When search was performed"`
}
type Product struct {
ID string `json:"id" jsonschema:"Product ID"`
Name string `json:"name" jsonschema:"Product name"`
Price float64 `json:"price" jsonschema:"Price in USD"`
InStock bool `json:"inStock" jsonschema:"Availability"`
}
// 创建同时具有输入和输出模式的工具
searchTool := mcp.NewTool("search_products",
mcp.WithDescription("Search product catalog with structured output"),
mcp.WithInputSchema[SearchRequest](),
mcp.WithOutputSchema[SearchResponse](),
)结构化工具处理器
使用 NewStructuredToolHandler 进行类型安全的处理器实现:
func main() {
s := server.NewMCPServer("Product Search", "1.0.0",
server.WithToolCapabilities(false),
)
// 定义具有输入和输出模式的工具
searchTool := mcp.NewTool("search_products",
mcp.WithDescription("Search product catalog"),
mcp.WithInputSchema[SearchRequest](),
mcp.WithOutputSchema[SearchResponse](),
)
// 使用结构化处理器添加工具
s.AddTool(searchTool, mcp.NewStructuredToolHandler(searchProductsHandler))
server.ServeStdio(s)
}
// 处理器接收类型化输入并返回类型化输出
func searchProductsHandler(ctx context.Context, req mcp.CallToolRequest, args SearchRequest) (SearchResponse, error) {
// 输入已经过验证并绑定到 SearchRequest 结构体
limit := args.Limit
if limit <= 0 {
limit = 10
}
// 执行搜索逻辑
products := searchDatabase(args.Query, args.Categories, limit)
// 返回结构化响应
return SearchResponse{
Query: args.Query,
TotalCount: len(products),
Products: products,
ProcessedAt: time.Now(),
}, nil
}根据模式验证输出
声明输出模式是一个契约:客户端可以依赖 structuredContent 与你通告的模式匹配。为了在运行时强制执行该契约,在构造服务端时选择加入输出模式验证:
s := server.NewMCPServer("Product Search", "1.0.0",
server.WithToolCapabilities(false),
server.WithOutputSchemaValidation(), // 拒绝与 outputSchema 不匹配的结果
)启用后,每个其 StructuredContent 不符合声明的 outputSchema 的工具结果都会被替换为工具执行错误(IsError: true)。这会在服务端捕获处理器错误,而不是让它们作为静默协议违规到达客户端。没有输出模式的工具、没有 StructuredContent 的结果和错误结果都会不变地传递。
数组输出模式
工具可以返回结构化数据数组:
// 定义资产结构体
type Asset struct {
ID string `json:"id" jsonschema:"Asset identifier"`
Name string `json:"name" jsonschema:"Asset name"`
Value float64 `json:"value" jsonschema:"Current value"`
Currency string `json:"currency" jsonschema:"Currency code"`
}
// 返回资产数组的工具
assetsTool := mcp.NewTool("list_assets",
mcp.WithDescription("List portfolio assets"),
mcp.WithInputSchema[struct {
Portfolio string `json:"portfolio" jsonschema:"Portfolio ID"`
}](),
mcp.WithOutputSchema[[]Asset](), // 数组输出模式
)
func listAssetsHandler(ctx context.Context, req mcp.CallToolRequest, args struct{ Portfolio string }) ([]Asset, error) {
// 返回资产数组
return []Asset{
{ID: "btc", Name: "Bitcoin", Value: 45000.50, Currency: "USD"},
{ID: "eth", Name: "Ethereum", Value: 3200.75, Currency: "USD"},
}, nil
}无状态/无服务器部署的缓存模式
使用 WithInputSchema[T]() 和 WithOutputSchema[T]() 生成 JSON 模式在服务端启动时使用反射。在长期运行的服务端中这是一次性成本,但在无服务器环境(AWS Lambda、云函数、冷启动时重建的 Cloud Run)中,每次冷启动都会重复支付反射成本。
使用 mcp.SchemaCache 在构建时预计算模式,将其持久化到磁盘,并在启动时重新加载:
// build/warm-cache.go — 作为构建管道的一部分运行
func main() {
cache := mcp.NewSchemaCache()
if err := mcp.WarmFor[WeatherRequest](cache); err != nil {
log.Fatal(err)
}
if err := mcp.WarmFor[WeatherResponse](cache); err != nil {
log.Fatal(err)
}
if err := cache.Save("schemas.json"); err != nil {
log.Fatal(err)
}
}在运行时,加载缓存一次并将其传递给缓存感知的工具选项。缓存命中时,模式直接从内存提供;缓存未命中时,缓存回退到反射并存储结果以供下次使用,因此相同的代码在本地工作时无需预构建缓存文件:
var cache = func() *mcp.SchemaCache {
c, err := mcp.LoadSchemaCache("schemas.json")
if err != nil {
return mcp.NewSchemaCache() // 回退到实时反射
}
return c
}()
weatherTool := mcp.NewTool("get_weather",
mcp.WithDescription("Get current weather"),
mcp.WithCachedInputSchema[WeatherRequest](cache),
mcp.WithCachedOutputSchema[WeatherResponse](cache),
)缓存键默认为包限定的 Go 类型名(例如 main.WeatherRequest)。如果你需要一个能抵抗重命名或包移动的稳定键,请使用显式键变体:
if err := mcp.WarmFor[WeatherRequest](cache, "weather.v1.input"); err != nil {
log.Fatal(err)
}
mcp.WithCachedInputSchemaKey[WeatherRequest](cache, "weather.v1.input")SchemaCache 可安全用于并发使用,编组为确定性 JSON(排序键),并通过 Save 中的原子临时文件和重命名写入。
模式标签参考
MCP-Go 使用 google/jsonschema-go 库和 jsonschema 结构体标签进行模式生成:
type ExampleStruct struct {
// 必需字段(json 标签中没有 omitempty 表示必需)
Name string `json:"name" jsonschema:"User's full name"`
// 带描述的字段
Age int `json:"age" jsonschema:"User age in years"`
// 可选字段(omitempty 使其成为可选)
PageSize int `json:"pageSize,omitempty" jsonschema:"Number of items per page"`
// 带描述的可选字段
Status string `json:"status,omitempty" jsonschema:"Current status"`
}迁移说明
MCP-Go 从 invopop/jsonschema 迁移到 google/jsonschema-go。如果你从旧版本升级,请注意这些破坏性变更:
- 描述标签:从
jsonschema_description:"text"改为jsonschema:"text" - 必需字段:
jsonschema:"required"标签不再支持。没有omitempty的 json 标签的字段自动成为必需。 - 验证约束:标签如
jsonschema:"enum=..."、jsonschema:"minimum=..."不再支持在结构体标签中使用。对于这些约束,使用mcp.WithRawInputSchema()或使用构建器 API(mcp.WithString、mcp.WithNumber等)定义。 - 额外属性:
additionalProperties: false现在自动包含在生成模式中。
手动结构化结果
为了更好地控制响应,使用 NewTypedToolHandler 和手动结果创建:
manualTool := mcp.NewTool("process_data",
mcp.WithDescription("Process data with custom result"),
mcp.WithInputSchema[ProcessRequest](),
mcp.WithOutputSchema[ProcessResponse](),
)
s.AddTool(manualTool, mcp.NewTypedToolHandler(manualProcessHandler))
func manualProcessHandler(ctx context.Context, req mcp.CallToolRequest, args ProcessRequest) (*mcp.CallToolResult, error) {
// 处理数据
response := ProcessResponse{
Status: "completed",
ProcessedAt: time.Now(),
ItemCount: 42,
// ...
}
return mcp.NewToolResult(response), nil
}添加工具到服务端
基本工具处理
package main
import (
"context"
"fmt"
"log"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)
func main() {
s := server.NewMCPServer("My Server", "1.0.0",
server.WithToolCapabilities(true),
)
// 定义工具
calculatorTool := mcp.NewTool("calculate",
mcp.WithDescription("Perform arithmetic operations"),
mcp.WithString("operation",
mcp.Required(),
mcp.Enum("add", "subtract", "multiply", "divide"),
),
mcp.WithNumber("x", mcp.Required()),
mcp.WithNumber("y", mcp.Required()),
)
// 添加工具及其处理器
s.AddTool(calculatorTool, handleCalculate)
server.ServeStdio(s)
}
func handleCalculate(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
operation := req.GetString("operation", "")
x := req.GetNumber("x", 0)
y := req.GetNumber("y", 0)
var result float64
switch operation {
case "add":
result = x + y
case "subtract":
result = x - y
case "multiply":
result = x * y
case "divide":
if y == 0 {
return mcp.NewToolResultError("Division by zero"), nil
}
result = x / y
default:
return mcp.NewToolResultError("Unknown operation"), nil
}
return mcp.NewToolResultText(fmt.Sprintf("Result: %.2f", result)), nil
}异步工具处理
对于长时间运行的工具,使用 goroutine 实现异步处理:
func handleLongRunningTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolRequest, error) {
taskID := req.GetString("task_id", "")
// 在 goroutine 中启动处理
go func() {
// 模拟长时间运行的任务
for i := 0; i < 10; i++ {
time.Sleep(time.Second)
log.Printf("Processing task %s: %d/10", taskID, i+1)
}
log.Printf("Task %s completed", taskID)
}()
// 立即返回
return mcp.NewToolResultText("Task started. Check progress with get_task_status."), nil
}错误处理
func handleToolWithErrors(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// 验证输入
path := req.GetString("path", "")
if path == "" {
return mcp.NewToolResultError("path is required"), nil
}
// 检查上下文取消
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
// 执行操作
result, err := performOperation(path)
if err != nil {
// 返回用户友好的错误
return mcp.NewToolResultError(err.Error()), nil
}
return mcp.NewToolResultText(result), nil
}工具注释
工具注释提供额外的元数据和行为提示:
tool := mcp.NewTool("dangerous_operation",
mcp.WithDescription("Perform a potentially dangerous operation"),
mcp.WithTitleAnnotation("⚠️ Dangerous Operation"), // 传统标题提示
mcp.WithReadOnlyAnnotation(false), // 工具可能会修改数据
mpc.WithDestructiveAnnotation(true), // 标记为破坏性
mcp.WithIdempotentAnnotation(false), // 不保证幂等
mcp.WithCacheControlAnnotation(mcp.CacheControlEphemeral), // 不缓存结果
)工具优先级
为 LLM 优先级排序设置工具注释:
tool := mcp.NewTool("critical_operation",
mcp.WithDescription("Critical system operation"),
mcp.WithPriorityAnnotation(1000), // 高优先级
)
lowPriorityTool := mcp.NewTool("background_task",
mcp.WithDescription("Background maintenance task"),
mcp.WithPriorityAnnotation(100), // 低优先级
)相关工具
通告相关工具以帮助 LLM 发现互补功能:
mainTool := mcp.NewTool("create_file",
mcp.WithDescription("Create a new file"),
)
mainTool.Annotate().AddRelatedTool("read_file", mcp.RelationRelated)
mainTool.Annotate().AddRelatedTool("delete_file", mcp.RelationRelated)
mainTool.Annotate().AddRelatedTool("list_files", mcp.RelationSuggested)迁移指南
从旧版本迁移
如果你从旧版本的 MCP-Go 迁移,注意以下变更:
工具处理器签名:
go// 旧版本 func handleTool(req mcp.CallToolRequest) (*mcp.CallToolResult, error) // 新版本 func handleTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error)工具结果创建:
go// 旧版本 return &mcp.CallToolResult{...}, nil // 新版本 return mcp.NewToolResultText("text"), nil参数访问:
go// 旧版本 name := req.Params.Arguments["name"].(string) // 新版本 name := req.GetString("name", "default")

