From bca9c4d6d8bb29bc5188296ce7f3104f575d65bc Mon Sep 17 00:00:00 2001 From: Rob Findley Date: Fri, 22 Aug 2025 17:46:25 +0000 Subject: [PATCH] examples: add an everything example, and simplify hello Our 'hello' example should be as simple as possible. On the other hand, we should have an 'everything' example that exercises (almost) everything we offer. Using the everything example, I did some final testing using the inspector. This turned up a couple rough edges related to JSON null that I addressed by preferring empty slices. For #33 --- examples/server/everything/main.go | 200 +++++++++++++++++++++++++++++ examples/server/hello/main.go | 98 ++++---------- mcp/server.go | 13 ++ 3 files changed, 240 insertions(+), 71 deletions(-) create mode 100644 examples/server/everything/main.go diff --git a/examples/server/everything/main.go b/examples/server/everything/main.go new file mode 100644 index 0000000..d2b7b33 --- /dev/null +++ b/examples/server/everything/main.go @@ -0,0 +1,200 @@ +// Copyright 2025 The Go MCP SDK Authors. All rights reserved. +// Use of this source code is governed by an MIT-style +// license that can be found in the LICENSE file. + +// The everything server implements all supported features of an MCP server. +package main + +import ( + "context" + "flag" + "fmt" + "log" + "net/http" + "net/url" + "os" + "strings" + + "github.com/google/jsonschema-go/jsonschema" + "github.com/modelcontextprotocol/go-sdk/mcp" +) + +var httpAddr = flag.String("http", "", "if set, use streamable HTTP at this address, instead of stdin/stdout") + +func main() { + flag.Parse() + + opts := &mcp.ServerOptions{ + Instructions: "Use this server!", + CompletionHandler: complete, // support completions by setting this handler + } + + server := mcp.NewServer(&mcp.Implementation{Name: "everything"}, opts) + + // Add tools that exercise different features of the protocol. + mcp.AddTool(server, &mcp.Tool{Name: "greet", Description: "say hi"}, contentTool) + mcp.AddTool(server, &mcp.Tool{Name: "greet (structured)"}, structuredTool) // returns structured output + mcp.AddTool(server, &mcp.Tool{Name: "ping"}, pingingTool) // performs a ping + mcp.AddTool(server, &mcp.Tool{Name: "log"}, loggingTool) // performs a log + mcp.AddTool(server, &mcp.Tool{Name: "sample"}, samplingTool) // performs sampling + mcp.AddTool(server, &mcp.Tool{Name: "elicit"}, elicitingTool) // performs elicitation + mcp.AddTool(server, &mcp.Tool{Name: "roots"}, rootsTool) // lists roots + + // Add a basic prompt. + server.AddPrompt(&mcp.Prompt{Name: "greet"}, prompt) + + // Add an embedded resource. + server.AddResource(&mcp.Resource{ + Name: "info", + MIMEType: "text/plain", + URI: "embedded:info", + }, embeddedResource) + + // Serve over stdio, or streamable HTTP if -http is set. + if *httpAddr != "" { + handler := mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server { + return server + }, nil) + log.Printf("MCP handler listening at %s", *httpAddr) + http.ListenAndServe(*httpAddr, handler) + } else { + t := &mcp.LoggingTransport{Transport: &mcp.StdioTransport{}, Writer: os.Stderr} + if err := server.Run(context.Background(), t); err != nil { + log.Printf("Server failed: %v", err) + } + } +} + +func prompt(ctx context.Context, req *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { + return &mcp.GetPromptResult{ + Description: "Hi prompt", + Messages: []*mcp.PromptMessage{ + { + Role: "user", + Content: &mcp.TextContent{Text: "Say hi to " + req.Params.Arguments["name"]}, + }, + }, + }, nil +} + +var embeddedResources = map[string]string{ + "info": "This is the hello example server.", +} + +func embeddedResource(_ context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { + u, err := url.Parse(req.Params.URI) + if err != nil { + return nil, err + } + if u.Scheme != "embedded" { + return nil, fmt.Errorf("wrong scheme: %q", u.Scheme) + } + key := u.Opaque + text, ok := embeddedResources[key] + if !ok { + return nil, fmt.Errorf("no embedded resource named %q", key) + } + return &mcp.ReadResourceResult{ + Contents: []*mcp.ResourceContents{ + {URI: req.Params.URI, MIMEType: "text/plain", Text: text}, + }, + }, nil +} + +type args struct { + Name string `json:"name" jsonschema:"the name to say hi to"` +} + +// contentTool is a tool that returns unstructured content. +// +// Since its output type is 'any', no output schema is created. +func contentTool(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 +} + +type result struct { + Message string `json:"message" jsonschema:"the message to convey"` +} + +// structuredTool returns a structured result. +func structuredTool(ctx context.Context, req *mcp.CallToolRequest, args *args) (*mcp.CallToolResult, *result, error) { + return nil, &result{Message: "Hi " + args.Name}, nil +} + +func pingingTool(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) { + if err := req.Session.Ping(ctx, nil); err != nil { + return nil, nil, fmt.Errorf("ping failed") + } + return nil, nil, nil +} + +func loggingTool(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) { + if err := req.Session.Log(ctx, &mcp.LoggingMessageParams{ + Data: "something happened!", + Level: "error", + }); err != nil { + return nil, nil, fmt.Errorf("log failed") + } + return nil, nil, nil +} + +func rootsTool(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) { + res, err := req.Session.ListRoots(ctx, nil) + if err != nil { + return nil, nil, fmt.Errorf("listing roots failed: %v", err) + } + var allroots []string + for _, r := range res.Roots { + allroots = append(allroots, fmt.Sprintf("%s:%s", r.Name, r.URI)) + } + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: strings.Join(allroots, ",")}, + }, + }, nil, nil +} + +func samplingTool(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) { + res, err := req.Session.CreateMessage(ctx, new(mcp.CreateMessageParams)) + if err != nil { + return nil, nil, fmt.Errorf("sampling failed: %v", err) + } + return &mcp.CallToolResult{ + Content: []mcp.Content{ + res.Content, + }, + }, nil, nil +} + +func elicitingTool(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) { + res, err := req.Session.Elicit(ctx, &mcp.ElicitParams{ + Message: "provide a random string", + RequestedSchema: &jsonschema.Schema{ + Type: "object", + Properties: map[string]*jsonschema.Schema{ + "random": {Type: "string"}, + }, + }, + }) + if err != nil { + return nil, nil, fmt.Errorf("eliciting failed: %v", err) + } + return &mcp.CallToolResult{ + Content: []mcp.Content{ + &mcp.TextContent{Text: res.Content["random"].(string)}, + }, + }, nil, nil +} + +func complete(ctx context.Context, req *mcp.CompleteRequest) (*mcp.CompleteResult, error) { + return &mcp.CompleteResult{ + Completion: mcp.CompletionResultDetails{ + Total: 1, + Values: []string{req.Params.Argument.Value + "x"}, + }, + }, nil +} diff --git a/examples/server/hello/main.go b/examples/server/hello/main.go index 72d98b2..796feff 100644 --- a/examples/server/hello/main.go +++ b/examples/server/hello/main.go @@ -2,89 +2,45 @@ // Use of this source code is governed by an MIT-style // license that can be found in the LICENSE file. +// The hello server contains a single tool that says hi to the user. +// +// It runs over the stdio transport. package main import ( "context" - "flag" - "fmt" "log" - "net/http" - "net/url" - "os" "github.com/modelcontextprotocol/go-sdk/mcp" ) -var httpAddr = flag.String("http", "", "if set, use streamable HTTP at this address, instead of stdin/stdout") - -type HiArgs struct { - Name string `json:"name" jsonschema:"the name to say hi to"` -} - -func SayHi(ctx context.Context, req *mcp.CallToolRequest, args HiArgs) (*mcp.CallToolResult, any, error) { - return &mcp.CallToolResult{ - Content: []mcp.Content{ - &mcp.TextContent{Text: "Hi " + args.Name}, - }, - }, nil, nil -} - -func PromptHi(ctx context.Context, req *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) { - return &mcp.GetPromptResult{ - Description: "Code review prompt", - Messages: []*mcp.PromptMessage{ - {Role: "user", Content: &mcp.TextContent{Text: "Say hi to " + req.Params.Arguments["name"]}}, - }, - }, nil -} - func main() { - flag.Parse() - + // Create a server with a single tool that says "Hi". server := mcp.NewServer(&mcp.Implementation{Name: "greeter"}, nil) - mcp.AddTool(server, &mcp.Tool{Name: "greet", Description: "say hi"}, SayHi) - server.AddPrompt(&mcp.Prompt{Name: "greet"}, PromptHi) - server.AddResource(&mcp.Resource{ - Name: "info", - MIMEType: "text/plain", - URI: "embedded:info", - }, handleEmbeddedResource) - - if *httpAddr != "" { - handler := mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server { - return server - }, nil) - log.Printf("MCP handler listening at %s", *httpAddr) - http.ListenAndServe(*httpAddr, handler) - } else { - t := &mcp.LoggingTransport{Transport: &mcp.StdioTransport{}, Writer: os.Stderr} - if err := server.Run(context.Background(), t); err != nil { - log.Printf("Server failed: %v", err) - } - } -} -var embeddedResources = map[string]string{ - "info": "This is the hello example server.", -} - -func handleEmbeddedResource(_ context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) { - u, err := url.Parse(req.Params.URI) - if err != nil { - return nil, err - } - if u.Scheme != "embedded" { - return nil, fmt.Errorf("wrong scheme: %q", u.Scheme) + // Using the generic AddTool automatically populates the the input and output + // schema of the tool. + // + // The schema considers 'json' and 'jsonschema' struct tags to get argument + // names and descriptions. + type args struct { + Name string `json:"name" jsonschema:"the person to greet"` } - key := u.Opaque - text, ok := embeddedResources[key] - if !ok { - return nil, fmt.Errorf("no embedded resource named %q", key) + 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) } - return &mcp.ReadResourceResult{ - Contents: []*mcp.ResourceContents{ - {URI: req.Params.URI, MIMEType: "text/plain", Text: text}, - }, - }, nil } diff --git a/mcp/server.go b/mcp/server.go index febf2e6..eef6a5a 100644 --- a/mcp/server.go +++ b/mcp/server.go @@ -57,6 +57,8 @@ type ServerOptions struct { InitializedHandler func(context.Context, *InitializedRequest) // PageSize is the maximum number of items to return in a single page for // list methods (e.g. ListTools). + // + // If zero, defaults to [DefaultPageSize]. PageSize int // If non-nil, called when "notifications/roots/list_changed" is received. RootsListChangedHandler func(context.Context, *RootsListChangedRequest) @@ -266,6 +268,9 @@ func toolForErr[In, Out any](t *Tool, h ToolHandlerFor[In, Out]) (*Tool, ToolHan if res == nil { res = &CallToolResult{} } + if res.Content == nil { + res.Content = []Content{} // avoid returning 'null' + } res.StructuredContent = out if elemZero != nil { // Avoid typed nil, which will serialize as JSON null. @@ -843,6 +848,14 @@ func (ss *ServerSession) ListRoots(ctx context.Context, params *ListRootsParams) // CreateMessage sends a sampling request to the client. func (ss *ServerSession) CreateMessage(ctx context.Context, params *CreateMessageParams) (*CreateMessageResult, error) { + if params == nil { + params = &CreateMessageParams{Messages: []*SamplingMessage{}} + } + if params.Messages == nil { + p2 := *params + p2.Messages = []*SamplingMessage{} // avoid JSON "null" + params = &p2 + } return handleSend[*CreateMessageResult](ctx, methodCreateMessage, newServerRequest(ss, orZero[Params](params))) }