公告

任何建议或需求可联系我!


Skip to content

实现工具

工具提供 LLM 可以调用来执行操作或进行计算的功能。可以将它们视为扩展 LLM 能力的函数调用。

工具基础

工具是 LLM 与你的服务端交互以执行操作的主要方式。它们具有定义参数、类型和约束的结构化模式,确保类型安全的交互。

基本工具结构

go
// 创建一个简单的工具
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 保留用于面向模型的标识符。

go
tool := mcp.NewTool("calculate_arithmetic",
    mcp.WithToolTitle("Calculator"),               // 在 UI 中显示
    mcp.WithDescription("Perform arithmetic operations"),
    // ... 参数
)

根据 MCP 规范,客户端应按此顺序解析显示标签:

  1. Title(由 WithToolTitle 设置的顶层字段)
  2. Annotations.Title(由 WithTitleAnnotation 设置的传统提示,参见工具注释
  3. Name

两个字段都是可选的;省略它们会回退到下一个候选。

工具图标

工具可以使用图标包含视觉标识符。图标必须使用 HTTPS 或数据 URI:

go
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 支持各种带验证的参数类型:

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"},
    }),
)

枚举和约束

go
// 枚举值
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

go
// 使用 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 结构体的输出模式

为可预测的工具响应定义结构化输出:

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 进行类型安全的处理器实现:

go
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 与你通告的模式匹配。为了在运行时强制执行该契约,在构造服务端时选择加入输出模式验证:

go
s := server.NewMCPServer("Product Search", "1.0.0",
    server.WithToolCapabilities(false),
    server.WithOutputSchemaValidation(), // 拒绝与 outputSchema 不匹配的结果
)

启用后,每个其 StructuredContent 不符合声明的 outputSchema 的工具结果都会被替换为工具执行错误(IsError: true)。这会在服务端捕获处理器错误,而不是让它们作为静默协议违规到达客户端。没有输出模式的工具、没有 StructuredContent 的结果和错误结果都会不变地传递。

数组输出模式

工具可以返回结构化数据数组:

go
// 定义资产结构体
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 在构建时预计算模式,将其持久化到磁盘,并在启动时重新加载:

go
// 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)
    }
}

在运行时,加载缓存一次并将其传递给缓存感知的工具选项。缓存命中时,模式直接从内存提供;缓存未命中时,缓存回退到反射并存储结果以供下次使用,因此相同的代码在本地工作时无需预构建缓存文件:

go
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)。如果你需要一个能抵抗重命名或包移动的稳定键,请使用显式键变体:

go
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 结构体标签进行模式生成:

go
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.WithStringmcp.WithNumber 等)定义。
  • 额外属性additionalProperties: false 现在自动包含在生成模式中。

手动结构化结果

为了更好地控制响应,使用 NewTypedToolHandler 和手动结果创建:

go
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
}

添加工具到服务端

基本工具处理

go
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 实现异步处理:

go
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
}

错误处理

go
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
}

工具注释

工具注释提供额外的元数据和行为提示:

go
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 优先级排序设置工具注释:

go
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 发现互补功能:

go
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 迁移,注意以下变更:

  1. 工具处理器签名

    go
    // 旧版本
    func handleTool(req mcp.CallToolRequest) (*mcp.CallToolResult, error)
    
    // 新版本
    func handleTool(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error)
  2. 工具结果创建

    go
    // 旧版本
    return &mcp.CallToolResult{...}, nil
    
    // 新版本
    return mcp.NewToolResultText("text"), nil
  3. 参数访问

    go
    // 旧版本
    name := req.Params.Arguments["name"].(string)
    
    // 新版本
    name := req.GetString("name", "default")

下一步