go-sdk/examples/server/everything/main.go
Robert Findley 411d5a049a
mcp: switch icon theme to named string type (#733)
To be consistent with logging level, use a named string type for icon
themes. This improves readability / discoverability, and is more
consistent.

Updates #552
For #725
2025-12-19 16:44:10 -05:00

296 lines
9.6 KiB
Go

// 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"
_ "embed"
"encoding/base64"
"flag"
"fmt"
"log"
"net/http"
_ "net/http/pprof"
"net/url"
"os"
"runtime"
"strings"
"sync/atomic"
"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")
pprofAddr = flag.String("pprof", "", "if set, host the pprof debugging server at this address")
)
func main() {
flag.Parse()
if *pprofAddr != "" {
// For debugging memory leaks, add an endpoint to trigger a few garbage
// collection cycles and ensure the /debug/pprof/heap endpoint only reports
// reachable memory.
http.DefaultServeMux.Handle("/gc", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
for range 3 {
runtime.GC()
}
fmt.Fprintln(w, "GC'ed")
}))
go func() {
// DefaultServeMux was mutated by the /debug/pprof import.
http.ListenAndServe(*pprofAddr, http.DefaultServeMux)
}()
}
opts := &mcp.ServerOptions{
Instructions: "Use this server!",
CompletionHandler: complete, // support completions by setting this handler
}
// Add an icon to the server implementation.
icons := mcpIcons()
server := mcp.NewServer(&mcp.Implementation{Name: "everything", WebsiteURL: "https://example.com", Icons: icons}, 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: "greet (with Icons)", Icons: icons}, structuredTool) // tool with icons
mcp.AddTool(server, &mcp.Tool{Name: "greet (content with ResourceLink)"}, resourceLinkContentTool(icons)) // tool that returns content with a resource link
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 (form)"}, elicitFormTool) // performs form elicitation
mcp.AddTool(server, &mcp.Tool{Name: "elicit (url)"}, elicitURLTool) // performs url elicitation
mcp.AddTool(server, &mcp.Tool{Name: "roots"}, rootsTool) // lists roots
// Add a basic prompt.
server.AddPrompt(&mcp.Prompt{Name: "greet"}, prompt)
server.AddPrompt(&mcp.Prompt{Name: "greet (with Icons)", Icons: icons}, prompt) // greet prompt with icons
// Add an embedded resource.
server.AddResource(&mcp.Resource{
Name: "info",
MIMEType: "text/plain",
URI: "embedded:info",
}, embeddedResource)
server.AddResource(&mcp.Resource{ // text resource with icons
Name: "info (with Icons)",
MIMEType: "text/plain",
URI: "embedded:info",
Icons: icons,
}, embeddedResource)
// Add a resource template.
server.AddResourceTemplate(&mcp.ResourceTemplate{
Name: "Resource template (with Icon)",
MIMEType: "text/plain",
URITemplate: "http://example.com/~{resource_name}/",
Icons: icons,
}, 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)
if *pprofAddr != "" {
log.Printf("pprof listening at http://%s/debug/pprof", *pprofAddr)
}
log.Fatal(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
}
// resourceLinkContentTool returns a ResourceLink content with icons.
func resourceLinkContentTool(icons []mcp.Icon) func(ctx context.Context, req *mcp.CallToolRequest, args args) (*mcp.CallToolResult, any, error) {
return func(ctx context.Context, req *mcp.CallToolRequest, args args) (*mcp.CallToolResult, any, error) {
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.ResourceLink{
Name: "greeting",
Title: "A friendly greeting",
MIMEType: "text/plain",
URI: "data:text/plain,Hi%20" + url.PathEscape(args.Name),
Icons: icons,
},
},
}, 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 elicitFormTool(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
}
var elicitations atomic.Int32
func elicitURLTool(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
elicitID := fmt.Sprintf("%d", elicitations.Add(1))
_, err := req.Session.Elicit(ctx, &mcp.ElicitParams{
Message: "submit a string",
URL: fmt.Sprintf("http://localhost:6062?id=%s", elicitID),
ElicitationID: elicitID,
})
if err != nil {
return nil, nil, fmt.Errorf("eliciting failed: %v", err)
}
// TODO: actually wait for the elicitation form to be submitted.
return &mcp.CallToolResult{
Content: []mcp.Content{
&mcp.TextContent{Text: "(elicitation pending)"},
},
}, 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
}
//go:embed mcp.png
var mcpIconData []byte
func mcpIcons() []mcp.Icon {
return []mcp.Icon{{
Source: "data:image/png;base64," + base64.StdEncoding.EncodeToString(mcpIconData),
MIMEType: "image/png",
Sizes: []string{"48x48"},
Theme: mcp.IconThemeLight,
}}
}