diff --git a/examples/server/everything/main.go b/examples/server/everything/main.go new file mode 100644 index 00000000..d2b7b337 --- /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 72d98b21..796feff8 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 febf2e6e..eef6a5ad 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))) }