golang mcp 服务端指南

02-工具的参数

读完这篇你能:把工具的参数写规矩——控制必填/可选、给出候选值、知道哪些校验不用自己做。

前置:第一个 MCP 能跑通。约 15 分钟。

换一个真实的案例

hello_world 到此为止。后面全专栏都用这个场景:给公司的日志聚合系统做一个 MCP,让 AI 能自己查日志、排查问题。

服务端提供两个工具:

list_services 和 query_logs

工具 作用
list_services 列出所有服务名
query_logs 按服务、日志级别、时间范围查询日志

query_logs 的三个参数正好覆盖三种情况:service_name 必填、time_range 必填、log_levels 可选且值必须从固定集合里挑。这就是这篇要解决的全部问题。

第一版:直接把参数写出来

type queryLogsInput struct {
	ServiceName string   `json:"service_name" jsonschema:"服务名称"`
	LogLevels   []string `json:"log_levels" jsonschema:"日志级别"`
	TimeRange   string   `json:"time_range" jsonschema:"时间范围"`
}

type queryLogsOutput struct {
	Logs []string `json:"logs" jsonschema:"日志"`
}

func queryLogsTool(ctx context.Context, req *mcp.CallToolRequest, in queryLogsInput) (*mcp.CallToolResult, queryLogsOutput, error) {
	return nil, queryLogsOutput{
		Logs: []string{"log1", "log2", "log3"},
	}, nil
}

注册时不需要写 schema:

mcp.AddTool(s, &mcp.Tool{
	Name:        "query_logs",
	Description: "按服务名和时间范围查询日志,可筛选日志级别",
}, queryLogsTool)

结构体就是参数声明。 SDK 用 jsonschema.For[queryLogsInput] 从类型推断出 inputSchema,再把客户端传来的 arguments 反序列化填进来。你不用写 json.Unmarshal。

结构的规则也很朴素:

  • 字段名 → JSON 属性名,由 json 标签决定
  • 字段类型 → JSON Schema 类型(string / array / integer …)
  • jsonschema 标签 → 属性的 description,模型能读到,所以要当文档写

可选参数:加 omitempty

第一版有个副作用:三个字段默认都是必填。SDK 的规则是——没有 omitempty 的字段会进 schema 的 required 列表,客户端不传就校验失败。

log_levels 显然应该可选,加上:

LogLevels []string `json:"log_levels,omitempty" jsonschema:"日志级别(可选)"`

再连上去,会发现参数列表里级别已经不再是必填项:

非必填效果

omitempty 在这里的作用是「不参与必填校验」。指针类型(*string)也是可选的,但多数场景 omitempty 更顺手。

候选值:得自己写 schema

现在 log_levels 是个自由文本,用户和模型都不知道能填什么——这正是 IDE 里最让人烦躁的那种参数。

SDK 从 Go 类型推断不出候选值,所以要显式构造一份 schema 去覆盖默认推断。建议单独放一个 schema.go:

package main

import (
	"log"

	"github.com/google/jsonschema-go/jsonschema"
)

// 服务端定义的合法日志级别,写进 query_logs 的 inputSchema。
var queryLogsAllowedLogLevels = []any{"debug", "info", "warn", "error"}

var queryLogsInputSchema *jsonschema.Schema

func init() {
	s, err := jsonschema.For[queryLogsInput](nil)
	if err != nil {
		log.Fatalf("query-logs 输入 schema: %v", err)
	}
	// 多选:给数组的 items 加枚举,并禁止重复
	if p := s.Properties["log_levels"]; p != nil {
		p.UniqueItems = true
		if p.Items == nil {
			p.Items = &jsonschema.Schema{Type: "string"}
		}
		p.Items.Enum = append([]any(nil), queryLogsAllowedLogLevels...)
	}
	queryLogsInputSchema = s
}

注册时把它挂上去:

mcp.AddTool(s, &mcp.Tool{
	Name:        "query_logs",
	Description: "按服务名和时间范围查询日志,可筛选日志级别",
	InputSchema: queryLogsInputSchema, // 由不设置改为设置
}, queryLogsTool)

再连上去,级别就变成带下拉的多选框了:

多选候选值

如果只想要单选,把字段类型从 []string 改成 string,然后枚举直接加在属性上:

if p := s.Properties["log_level"]; p != nil {
	p.Enum = append([]any(nil), queryLogsAllowedLogLevels...)
}

单选候选值

写法就是「先 jsonschema.For 拿到推断结果,再按属性名改」。这个包里还能改 Default、Minimum、Pattern 等等,本质是把 JSON Schema 的关键字挂到对应属性上。想写什么查 json-schema.org 即可。

哪些校验不用你做

走到这里,参数层面已经覆盖了三类约束:

约束 在哪表达 谁执行
类型对不对 Go 字段类型 SDK 自动
必填还是可选 有没有 omitempty SDK 自动
值在不在候选集里 Enum / Items.Enum SDK 自动

这三类校验都是 SDK 在进 handler 之前做的,handler 根本不会被执行,客户端直接收到 InvalidParams。所以别在 handler 里再写一遍 if in.ServiceName == ""。

schema 表达不了的东西只有一类——字段之间的关系,比如「start_time 不能晚于 end_time」。这类只能自己拦,返回什么错误见错误返回与重试。


现在你能做到:写清楚工具的参数——哪些必填、允许哪些值,并且知道校验不需要自己写。


下一篇:跑成 HTTP 服务——工具跑通了,让远程也能用。