Skip to content

Commit 9754a2a

Browse files
committed
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
1 parent c631641 commit 9754a2a

File tree

3 files changed

+240
-71
lines changed

3 files changed

+240
-71
lines changed

examples/server/everything/main.go

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
// Copyright 2025 The Go MCP SDK Authors. All rights reserved.
2+
// Use of this source code is governed by an MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
// The everything server implements all supported features of an MCP server.
6+
package main
7+
8+
import (
9+
"context"
10+
"flag"
11+
"fmt"
12+
"log"
13+
"net/http"
14+
"net/url"
15+
"os"
16+
"strings"
17+
18+
"github.com/google/jsonschema-go/jsonschema"
19+
"github.com/modelcontextprotocol/go-sdk/mcp"
20+
)
21+
22+
var httpAddr = flag.String("http", "", "if set, use streamable HTTP at this address, instead of stdin/stdout")
23+
24+
func main() {
25+
flag.Parse()
26+
27+
opts := &mcp.ServerOptions{
28+
Instructions: "Use this server!",
29+
CompletionHandler: complete, // support completions by setting this handler
30+
}
31+
32+
server := mcp.NewServer(&mcp.Implementation{Name: "everything"}, opts)
33+
34+
// Add tools that exercise different features of the protocol.
35+
mcp.AddTool(server, &mcp.Tool{Name: "greet", Description: "say hi"}, contentTool)
36+
mcp.AddTool(server, &mcp.Tool{Name: "greet (structured)"}, structuredTool) // returns structured output
37+
mcp.AddTool(server, &mcp.Tool{Name: "ping"}, pingingTool) // performs a ping
38+
mcp.AddTool(server, &mcp.Tool{Name: "log"}, loggingTool) // performs a log
39+
mcp.AddTool(server, &mcp.Tool{Name: "sample"}, samplingTool) // performs sampling
40+
mcp.AddTool(server, &mcp.Tool{Name: "elicit"}, elicitingTool) // performs elicitation
41+
mcp.AddTool(server, &mcp.Tool{Name: "roots"}, rootsTool) // lists roots
42+
43+
// Add a basic prompt.
44+
server.AddPrompt(&mcp.Prompt{Name: "greet"}, prompt)
45+
46+
// Add an embedded resource.
47+
server.AddResource(&mcp.Resource{
48+
Name: "info",
49+
MIMEType: "text/plain",
50+
URI: "embedded:info",
51+
}, embeddedResource)
52+
53+
// Serve over stdio, or streamable HTTP if -http is set.
54+
if *httpAddr != "" {
55+
handler := mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server {
56+
return server
57+
}, nil)
58+
log.Printf("MCP handler listening at %s", *httpAddr)
59+
http.ListenAndServe(*httpAddr, handler)
60+
} else {
61+
t := &mcp.LoggingTransport{Transport: &mcp.StdioTransport{}, Writer: os.Stderr}
62+
if err := server.Run(context.Background(), t); err != nil {
63+
log.Printf("Server failed: %v", err)
64+
}
65+
}
66+
}
67+
68+
func prompt(ctx context.Context, req *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
69+
return &mcp.GetPromptResult{
70+
Description: "Hi prompt",
71+
Messages: []*mcp.PromptMessage{
72+
{
73+
Role: "user",
74+
Content: &mcp.TextContent{Text: "Say hi to " + req.Params.Arguments["name"]},
75+
},
76+
},
77+
}, nil
78+
}
79+
80+
var embeddedResources = map[string]string{
81+
"info": "This is the hello example server.",
82+
}
83+
84+
func embeddedResource(_ context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
85+
u, err := url.Parse(req.Params.URI)
86+
if err != nil {
87+
return nil, err
88+
}
89+
if u.Scheme != "embedded" {
90+
return nil, fmt.Errorf("wrong scheme: %q", u.Scheme)
91+
}
92+
key := u.Opaque
93+
text, ok := embeddedResources[key]
94+
if !ok {
95+
return nil, fmt.Errorf("no embedded resource named %q", key)
96+
}
97+
return &mcp.ReadResourceResult{
98+
Contents: []*mcp.ResourceContents{
99+
{URI: req.Params.URI, MIMEType: "text/plain", Text: text},
100+
},
101+
}, nil
102+
}
103+
104+
type args struct {
105+
Name string `json:"name" jsonschema:"the name to say hi to"`
106+
}
107+
108+
// contentTool is a tool that returns unstructured content.
109+
//
110+
// Since its output type is 'any', no output schema is created.
111+
func contentTool(ctx context.Context, req *mcp.CallToolRequest, args args) (*mcp.CallToolResult, any, error) {
112+
return &mcp.CallToolResult{
113+
Content: []mcp.Content{
114+
&mcp.TextContent{Text: "Hi " + args.Name},
115+
},
116+
}, nil, nil
117+
}
118+
119+
type result struct {
120+
Message string `json:"message" jsonschema:"the message to convey"`
121+
}
122+
123+
// structuredTool returns a structured result.
124+
func structuredTool(ctx context.Context, req *mcp.CallToolRequest, args *args) (*mcp.CallToolResult, *result, error) {
125+
return nil, &result{Message: "Hi " + args.Name}, nil
126+
}
127+
128+
func pingingTool(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
129+
if err := req.Session.Ping(ctx, nil); err != nil {
130+
return nil, nil, fmt.Errorf("ping failed")
131+
}
132+
return nil, nil, nil
133+
}
134+
135+
func loggingTool(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
136+
if err := req.Session.Log(ctx, &mcp.LoggingMessageParams{
137+
Data: "something happened!",
138+
Level: "error",
139+
}); err != nil {
140+
return nil, nil, fmt.Errorf("log failed")
141+
}
142+
return nil, nil, nil
143+
}
144+
145+
func rootsTool(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
146+
res, err := req.Session.ListRoots(ctx, nil)
147+
if err != nil {
148+
return nil, nil, fmt.Errorf("listing roots failed: %v", err)
149+
}
150+
var allroots []string
151+
for _, r := range res.Roots {
152+
allroots = append(allroots, fmt.Sprintf("%s:%s", r.Name, r.URI))
153+
}
154+
return &mcp.CallToolResult{
155+
Content: []mcp.Content{
156+
&mcp.TextContent{Text: strings.Join(allroots, ",")},
157+
},
158+
}, nil, nil
159+
}
160+
161+
func samplingTool(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
162+
res, err := req.Session.CreateMessage(ctx, new(mcp.CreateMessageParams))
163+
if err != nil {
164+
return nil, nil, fmt.Errorf("sampling failed: %v", err)
165+
}
166+
return &mcp.CallToolResult{
167+
Content: []mcp.Content{
168+
res.Content,
169+
},
170+
}, nil, nil
171+
}
172+
173+
func elicitingTool(ctx context.Context, req *mcp.CallToolRequest, _ any) (*mcp.CallToolResult, any, error) {
174+
res, err := req.Session.Elicit(ctx, &mcp.ElicitParams{
175+
Message: "provide a random string",
176+
RequestedSchema: &jsonschema.Schema{
177+
Type: "object",
178+
Properties: map[string]*jsonschema.Schema{
179+
"random": {Type: "string"},
180+
},
181+
},
182+
})
183+
if err != nil {
184+
return nil, nil, fmt.Errorf("eliciting failed: %v", err)
185+
}
186+
return &mcp.CallToolResult{
187+
Content: []mcp.Content{
188+
&mcp.TextContent{Text: res.Content["random"].(string)},
189+
},
190+
}, nil, nil
191+
}
192+
193+
func complete(ctx context.Context, req *mcp.CompleteRequest) (*mcp.CompleteResult, error) {
194+
return &mcp.CompleteResult{
195+
Completion: mcp.CompletionResultDetails{
196+
Total: 1,
197+
Values: []string{req.Params.Argument.Value + "x"},
198+
},
199+
}, nil
200+
}

examples/server/hello/main.go

Lines changed: 27 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -2,89 +2,45 @@
22
// Use of this source code is governed by an MIT-style
33
// license that can be found in the LICENSE file.
44

5+
// The hello server contains a single tool that says hi to the user.
6+
//
7+
// It runs over the stdio transport.
58
package main
69

710
import (
811
"context"
9-
"flag"
10-
"fmt"
1112
"log"
12-
"net/http"
13-
"net/url"
14-
"os"
1513

1614
"github.com/modelcontextprotocol/go-sdk/mcp"
1715
)
1816

19-
var httpAddr = flag.String("http", "", "if set, use streamable HTTP at this address, instead of stdin/stdout")
20-
21-
type HiArgs struct {
22-
Name string `json:"name" jsonschema:"the name to say hi to"`
23-
}
24-
25-
func SayHi(ctx context.Context, req *mcp.CallToolRequest, args HiArgs) (*mcp.CallToolResult, any, error) {
26-
return &mcp.CallToolResult{
27-
Content: []mcp.Content{
28-
&mcp.TextContent{Text: "Hi " + args.Name},
29-
},
30-
}, nil, nil
31-
}
32-
33-
func PromptHi(ctx context.Context, req *mcp.GetPromptRequest) (*mcp.GetPromptResult, error) {
34-
return &mcp.GetPromptResult{
35-
Description: "Code review prompt",
36-
Messages: []*mcp.PromptMessage{
37-
{Role: "user", Content: &mcp.TextContent{Text: "Say hi to " + req.Params.Arguments["name"]}},
38-
},
39-
}, nil
40-
}
41-
4217
func main() {
43-
flag.Parse()
44-
18+
// Create a server with a single tool that says "Hi".
4519
server := mcp.NewServer(&mcp.Implementation{Name: "greeter"}, nil)
46-
mcp.AddTool(server, &mcp.Tool{Name: "greet", Description: "say hi"}, SayHi)
47-
server.AddPrompt(&mcp.Prompt{Name: "greet"}, PromptHi)
48-
server.AddResource(&mcp.Resource{
49-
Name: "info",
50-
MIMEType: "text/plain",
51-
URI: "embedded:info",
52-
}, handleEmbeddedResource)
53-
54-
if *httpAddr != "" {
55-
handler := mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server {
56-
return server
57-
}, nil)
58-
log.Printf("MCP handler listening at %s", *httpAddr)
59-
http.ListenAndServe(*httpAddr, handler)
60-
} else {
61-
t := &mcp.LoggingTransport{Transport: &mcp.StdioTransport{}, Writer: os.Stderr}
62-
if err := server.Run(context.Background(), t); err != nil {
63-
log.Printf("Server failed: %v", err)
64-
}
65-
}
66-
}
6720

68-
var embeddedResources = map[string]string{
69-
"info": "This is the hello example server.",
70-
}
71-
72-
func handleEmbeddedResource(_ context.Context, req *mcp.ReadResourceRequest) (*mcp.ReadResourceResult, error) {
73-
u, err := url.Parse(req.Params.URI)
74-
if err != nil {
75-
return nil, err
76-
}
77-
if u.Scheme != "embedded" {
78-
return nil, fmt.Errorf("wrong scheme: %q", u.Scheme)
21+
// Using the generic AddTool automatically populates the the input and output
22+
// schema of the tool.
23+
//
24+
// The schema considers 'json' and 'jsonschema' struct tags to get argument
25+
// names and descriptions.
26+
type args struct {
27+
Name string `json:"name" jsonschema:"the person to greet"`
7928
}
80-
key := u.Opaque
81-
text, ok := embeddedResources[key]
82-
if !ok {
83-
return nil, fmt.Errorf("no embedded resource named %q", key)
29+
mcp.AddTool(server, &mcp.Tool{
30+
Name: "greet",
31+
Description: "say hi",
32+
}, func(ctx context.Context, req *mcp.CallToolRequest, args args) (*mcp.CallToolResult, any, error) {
33+
return &mcp.CallToolResult{
34+
Content: []mcp.Content{
35+
&mcp.TextContent{Text: "Hi " + args.Name},
36+
},
37+
}, nil, nil
38+
})
39+
40+
// server.Run runs the server on the given transport.
41+
//
42+
// In this case, the server communicates over stdin/stdout.
43+
if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
44+
log.Printf("Server failed: %v", err)
8445
}
85-
return &mcp.ReadResourceResult{
86-
Contents: []*mcp.ResourceContents{
87-
{URI: req.Params.URI, MIMEType: "text/plain", Text: text},
88-
},
89-
}, nil
9046
}

mcp/server.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ type ServerOptions struct {
5757
InitializedHandler func(context.Context, *InitializedRequest)
5858
// PageSize is the maximum number of items to return in a single page for
5959
// list methods (e.g. ListTools).
60+
//
61+
// If zero, defaults to [DefaultPageSize].
6062
PageSize int
6163
// If non-nil, called when "notifications/roots/list_changed" is received.
6264
RootsListChangedHandler func(context.Context, *RootsListChangedRequest)
@@ -266,6 +268,9 @@ func toolForErr[In, Out any](t *Tool, h ToolHandlerFor[In, Out]) (*Tool, ToolHan
266268
if res == nil {
267269
res = &CallToolResult{}
268270
}
271+
if res.Content == nil {
272+
res.Content = []Content{} // avoid returning 'null'
273+
}
269274
res.StructuredContent = out
270275
if elemZero != nil {
271276
// Avoid typed nil, which will serialize as JSON null.
@@ -843,6 +848,14 @@ func (ss *ServerSession) ListRoots(ctx context.Context, params *ListRootsParams)
843848

844849
// CreateMessage sends a sampling request to the client.
845850
func (ss *ServerSession) CreateMessage(ctx context.Context, params *CreateMessageParams) (*CreateMessageResult, error) {
851+
if params == nil {
852+
params = &CreateMessageParams{Messages: []*SamplingMessage{}}
853+
}
854+
if params.Messages == nil {
855+
p2 := *params
856+
p2.Messages = []*SamplingMessage{} // avoid JSON "null"
857+
params = &p2
858+
}
846859
return handleSend[*CreateMessageResult](ctx, methodCreateMessage, newServerRequest(ss, orZero[Params](params)))
847860
}
848861

0 commit comments

Comments
 (0)