交互
Say Hi
服务端提供一个 SayHi 的 Tool,然后客户端请求该 Tool,得到 Tool 结果
go
package main
import (
"context"
"fmt"
"log"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
type SayHiParams struct {
Name string `json:"name"`
}
func SayHi(ctx context.Context, req *mcp.CallToolRequest, args SayHiParams) (*mcp.CallToolResult, any, error) {
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: "Hi " + args.Name},
},
}, nil, nil
}
func main() {
ctx := context.Background()
clientTransport, serverTransport := mcp.NewInMemoryTransports()
server := mcp.NewServer(&mcp.Implementation{Name: "greeter", Version: "v0.0.1"}, nil)
mcp.AddTool(server, &mcp.Tool{Name: "greet", Description: "say hi"}, SayHi)
serverSession, err := server.Connect(ctx, serverTransport, nil)
if err != nil {
log.Fatal(err)
}
client := mcp.NewClient(&mcp.Implementation{Name: "client"}, nil)
clientSession, err := client.Connect(ctx, clientTransport, nil)
if err != nil {
log.Fatal(err)
}
res, err := clientSession.CallTool(ctx, &mcp.CallToolParams{
Name: "greet",
Arguments: map[string]any{"name": "user"},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(res.Content[0].(*mcp.TextContent).Text)
clientSession.Close()
serverSession.Wait()
// Output: Hi user
}
sdk中用了InMemoryTransports,它实际是一个 Pipe
Pipe 创建一个同步的(synchronous)、内存中的全双工(full duplex)网络连接;连接的两端均实现了 [Conn] 接口
一端的读取操作会与另一端的写入操作相匹配,数据直接在两端之间复制;不存在内部缓冲
go
// NewInMemoryTransports returns two [InMemoryTransports] that connect to each
// other.
func NewInMemoryTransports() (*InMemoryTransport, *InMemoryTransport) {
c1, c2 := net.Pipe()
return &InMemoryTransport{c1}, &InMemoryTransport{c2}
}
客户端 list 服务端提供的 tool
客户端代码
sh
go build -o listfeatures main.go
./listfeatures go run github.com/modelcontextprotocol/go-sdk/examples/server/hello
go
package main
import (
"context"
"flag"
"fmt"
"iter"
"log"
"os"
"os/exec"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
func main() {
flag.Parse()
args := flag.Args()
if len(args) == 0 {
fmt.Fprintf(os.Stderr, "Usage: listfeatures <command> [<args>]")
fmt.Fprintf(os.Stderr, "List all features for a stdio MCP server")
fmt.Fprintln(os.Stderr)
fmt.Fprintf(os.Stderr, "Example: listfeatures npx @modelcontextprotocol/server-everything")
os.Exit(2)
}
ctx := context.Background()
cmd := exec.Command(args[0], args[1:]...)
client := mcp.NewClient(&mcp.Implementation{Name: "mcp-client", Version: "v1.0.0"}, nil)
cs, err := client.Connect(ctx, &mcp.CommandTransport{Command: cmd}, nil)
if err != nil {
log.Fatal(err)
}
defer cs.Close()
printSection("tools", cs.Tools(ctx, nil), func(t *mcp.Tool) string { return t.Name })
printSection("resources", cs.Resources(ctx, nil), func(r *mcp.Resource) string { return r.Name })
printSection("resource templates", cs.ResourceTemplates(ctx, nil), func(r *mcp.ResourceTemplate) string { return r.Name })
printSection("prompts", cs.Prompts(ctx, nil), func(p *mcp.Prompt) string { return p.Name })
}
func printSection[T any](name string, features iter.Seq2[T, error], featName func(T) string) {
fmt.Printf("%s:\n", name)
for feat, err := range features {
if err != nil {
log.Fatal(err)
}
fmt.Printf("\t%s\n", featName(feat))
}
fmt.Println()
}
对应的服务端代码
go
// The hello server contains a single tool that says hi to the user.
//
// It runs over the stdio transport.
package main
import (
"context"
"log"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
func main() {
// Create a server with a single tool that says "Hi".
server := mcp.NewServer(&mcp.Implementation{Name: "greeter"}, nil)
// Using the generic AddTool automatically populates the input and output
// schema of the tool.
//
// The schema considers 'json' and 'jsonschema' struct tags to get argument
// names and descriptions.
// schema 考虑 json 和 jsonschema tag,来获取参数 name 和其 description
type args struct {
Name string `json:"name" jsonschema:"the person to greet"`
}
mcp.AddTool(server, &mcp.Tool{
Name: "greet",
Description: "say hi",
}, func(ctx context.Context, req *mcp.CallToolRequest, args args) (*mcp.CallToolResult, any, error) {
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: "Hi " + args.Name},
},
}, nil, nil
})
// server.Run runs the server on the given transport.
//
// In this case, the server communicates over stdin/stdout.
if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
log.Printf("Server failed: %v", err)
}
}
注意,这里用了范型 AddTool,自动推导 In、Out
go
// AddTool adds a tool and typed tool handler to the server.
//
// If the tool's input schema is nil, it is set to the schema inferred from the
// In type parameter, using [jsonschema.For]. The In type parameter must be a
// map or a struct, so that its inferred JSON Schema has type "object".
//
// For tools that don't return structured output, Out should be 'any'.
// Otherwise, if the tool's output schema is nil the output schema is set to
// the schema inferred from Out, which must be a map or a struct.
//
// It is a convenience for s.AddTool(ToolFor(t, h)).
func AddTool[In, Out any](s *Server, t *Tool, h ToolHandlerFor[In, Out]) {
s.AddTool(ToolFor(t, h))
}
// A ToolHandlerFor handles a call to tools/call with typed arguments and results.
type ToolHandlerFor[In, Out any] func(context.Context, *CallToolRequest, In) (*CallToolResult, Out, error)
引导
服务端主动向客户端请求补充信息,并通过标准化的交互完成数据收集
go
import (
"context"
"fmt"
"log"
"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
func main() {
ctx := context.Background()
clientTransport, serverTransport := mcp.NewInMemoryTransports()
// Create server
server := mcp.NewServer(&mcp.Implementation{Name: "config-server", Version: "v1.0.0"}, nil)
serverSession, err := server.Connect(ctx, serverTransport, nil)
if err != nil {
log.Fatal(err)
}
// Create client with elicitation handler
// Note: Never use elicitation for sensitive data like API keys or passwords
client := mcp.NewClient(&mcp.Implementation{Name: "config-client", Version: "v1.0.0"}, &mcp.ClientOptions{
// 这是客户端接收并响应服务端信息请求的核心回调函数
ElicitationHandler: func(ctx context.Context, request *mcp.ElicitRequest) (*mcp.ElicitResult, error) {
fmt.Printf("Server requests: %s\n", request.Params.Message)
// In a real application, this would prompt the user for input
// Here we simulate user providing configuration data
return &mcp.ElicitResult{
Action: "accept",
Content: map[string]any{
"serverEndpoint": "https://api.example.com",
"maxRetries": float64(3),
"enableLogs": true,
},
}, nil
},
})
_, err = client.Connect(ctx, clientTransport, nil)
if err != nil {
log.Fatal(err)
}
// 服务端首先定义需要客户端提供的配置数据结构(通过 JSON Schema 描述)
// 这个 Schema 用于告诉客户端:"需要提供哪些字段,格式是什么,是否必填"
// Server requests user configuration via elicitation
configSchema := &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"serverEndpoint": {Type: "string", Description: "Server endpoint URL"},
"maxRetries": {Type: "number", Minimum: ptr(1.0), Maximum: ptr(10.0)},
"enableLogs": {Type: "boolean", Description: "Enable debug logging"},
},
Required: []string{"serverEndpoint"},
}
// 服务端通过 serverSession.Elicit() 方法向客户端发送信息请求
// MCP 协议会将该请求通过内存传输层发送给客户端
result, err := serverSession.Elicit(ctx, &mcp.ElicitParams{
Message: "Please provide your configuration settings", // 人类可读的请求说明
RequestedSchema: configSchema, // 上述定义的Schema,约束客户端返回的数据格式
})
if err != nil {
log.Fatal(err)
}
// 客户端的 ElicitationHandler 回调函数接收到服务端的请求,参数 *mcp.ElicitRequest 包含服务端的 Message 和 RequestedSchema
// 打印引导信息,并鼓励用户交互输入(本示例中,ElicitationHandler 中直接写死了交互结果,并向服务端返回消息)
// 从消息中取出正确的参数
if result.Action == "accept" {
fmt.Printf("Configuration received: Endpoint: %v, Max Retries: %.0f, Logs: %v\n",
result.Content["serverEndpoint"],
result.Content["maxRetries"],
result.Content["enableLogs"])
}
// Output:
// Server requests: Please provide your configuration settings
// Configuration received: Endpoint: https://api.example.com, Max Retries: 3, Logs: true
}
// ptr is a helper function to create pointers for schema constraints
func ptr[T any](v T) *T {
return &v
}
仅服务端 - CompletionHandler的配置
go
package main
import (
"context"
"fmt"
"log"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
// This example demonstrates the minimal code to declare and assign
// a CompletionHandler to an MCP Server's options.
func main() {
// Define your custom CompletionHandler logic.
myCompletionHandler := func(_ context.Context, req *mcp.CompleteRequest) (*mcp.CompleteResult, error) {
// In a real application, you'd implement actual completion logic here.
// For this example, we return a fixed set of suggestions.
var suggestions []string
switch req.Params.Ref.Type {
case "ref/prompt":
suggestions = []string{"suggestion1", "suggestion2", "suggestion3"}
case "ref/resource":
suggestions = []string{"suggestion4", "suggestion5", "suggestion6"}
default:
return nil, fmt.Errorf("unrecognized content type %s", req.Params.Ref.Type)
}
return &mcp.CompleteResult{
Completion: mcp.CompletionResultDetails{
HasMore: false,
Total: len(suggestions),
Values: suggestions,
},
}, nil
}
// Create the MCP Server instance and assign the handler.
// No server running, just showing the configuration.
_ = mcp.NewServer(&mcp.Implementation{Name: "server"}, &mcp.ServerOptions{
CompletionHandler: myCompletionHandler,
})
log.Println("MCP Server instance created with a CompletionHandler assigned (but not running).")
log.Println("This example demonstrates configuration, not live interaction.")
}
附录
MCP 提供的 ClientOptions
go
type ClientOptions struct {
// Handler for sampling.
// Called when a server calls CreateMessage.
CreateMessageHandler func(context.Context, *CreateMessageRequest) (*CreateMessageResult, error)
// Handler for elicitation.
// Called when a server requests user input via Elicit.
ElicitationHandler func(context.Context, *ElicitRequest) (*ElicitResult, error)
// Handlers for notifications from the server.
ToolListChangedHandler func(context.Context, *ToolListChangedRequest)
PromptListChangedHandler func(context.Context, *PromptListChangedRequest)
ResourceListChangedHandler func(context.Context, *ResourceListChangedRequest)
ResourceUpdatedHandler func(context.Context, *ResourceUpdatedNotificationRequest)
LoggingMessageHandler func(context.Context, *LoggingMessageRequest)
ProgressNotificationHandler func(context.Context, *ProgressNotificationClientRequest)
// If non-zero, defines an interval for regular "ping" requests.
// If the peer fails to respond to pings originating from the keepalive check,
// the session is automatically closed.
KeepAlive time.Duration
}
MCP 之 ElicitationHandler 信息诱导
ElicitationHandler(通常翻译为 "启发 / 诱导处理器")是用于处理 "客户端需要补充信息" 场景的核心组件
主要解决服务端在处理请求时发现信息不完整的问题。
当服务端在处理客户端请求(如工具调用、内容补全)时,发现必要信息缺失(例如调用 "天气查询" 工具但缺少 "城市名称" 参数),ElicitationHandler 会触发一个 "信息诱导流程"
信息诱导流程
- 服务端通过该处理器生成 "补充信息的请求"(如 "请提供需要查询天气的城市名称")
- 客户端接收后,收集所需信息并重新发起请求
- 服务端利用补充的信息完成后续处理
典型使用场景
工具调用参数不全
客户端调用需要多参数的工具(如 "转账" 工具需要 "金额""收款人" 参数),但只提供了部分参数。此时 ElicitationHandler 会自动生成提示,要求客户端补充缺失的参数。
补全请求上下文不足
客户端请求生成 "邮件回复",但未提供原始邮件内容。服务端通过 ElicitationHandler 询问:"请提供需要回复的原始邮件内容,以便生成合适的回复。"
权限或格式校验不通过
若客户端提供的参数格式错误(如 "日期" 参数应为 YYYY-MM-DD 却传入 2024/8/31),处理器会返回格式要求,引导客户端修正。
与其他处理器的区别
和 CompletionHandler 不同:CompletionHandler 专注于生成补全内容,而 ElicitationHandler 专注于 "发现信息缺失并引导补充"
和 CallToolHandler 不同:工具处理器执行具体功能,而 ElicitationHandler 是在功能执行前的 "信息校验与补充流程"
工作流程示例
- 客户端调用 greet 工具,但未提供 name 参数
- 服务端检测到参数缺失,触发 ElicitationHandler
- 处理器生成诱导信息:{"elicit": {"field": "name", "message": "请提供您的姓名以便问候"}}
- 客户端接收后,补充 name: "Bob" 并重新调用
- 服务端完成处理,返回 Hi Bob
总结
ElicitationHandler 是 MCP 中实现 "交互式信息补充" 的关键,让服务端能动态引导客户端完善请求,避免因信息不全导致的请求失败,提升交互的流畅性