Skip to content

Commit a617dce

Browse files
authored
mcp/examples: HTTP server example with a simple client built-in (#168)
I'm adding a practical example of an MCP with HTTP streaming: both client and server. Those are useful for testing real-world applications.
1 parent 7bfde44 commit a617dce

File tree

3 files changed

+320
-0
lines changed

3 files changed

+320
-0
lines changed

examples/http/README.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# MCP HTTP Example
2+
3+
This example demonstrates how to use the Model Context Protocol (MCP) over HTTP using the streamable transport. It includes both a server and client implementation.
4+
5+
## Overview
6+
7+
The example implements:
8+
- A server that provides a `cityTime` tool
9+
- A client that connects to the server, lists available tools, and calls the `cityTime` tool
10+
11+
## Usage
12+
13+
Start the Server
14+
15+
```bash
16+
go run main.go server
17+
```
18+
This starts an MCP server on `http://localhost:8080` (default) that provides a `cityTime` tool.
19+
20+
To run a client in another terminal:
21+
22+
```bash
23+
go run main.go client
24+
```
25+
26+
The client will:
27+
1. Connect to the server
28+
2. List available tools
29+
3. Call the `cityTime` tool for NYC, San Francisco, and Boston
30+
4. Display the results
31+
32+
At any given time you can pass a custom URL to the program to run it on a custom host/port:
33+
34+
```
35+
go run main.go -host 0.0.0.0 -port 9000 server
36+
```
37+
38+
## Testing with real-world MCP Clients
39+
40+
Once the server is started, assuming it's the default
41+
localhost:8080, you can try to add it to a popular MCP client:
42+
43+
claude mcp add -t http timezone http://localhost:8080
44+
45+
Once added, Claude Code will be able to discover and use the `cityTime` tool provided by this server.
46+
47+
In Claude Code:
48+
49+
> what's the timezone
50+
51+
⏺ I'll get the current time in a major US city for you.
52+
53+
⏺ timezone - cityTime (MCP)(city: "nyc")
54+
⎿ The current time in New York City is 7:30:16 PM EDT on Wedn
55+
esday, July 23, 2025
56+
57+
58+
⏺ The current timezone is EDT (Eastern Daylight Time), and it's
59+
7:30 PM on Wednesday, July 23, 2025.
60+
61+
> what timezones do you support?
62+
63+
⏺ The timezone tool supports three US cities:
64+
- NYC (Eastern Time)
65+
- SF (Pacific Time)
66+
- Boston (Eastern Time)
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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+
package main
6+
7+
import (
8+
"log"
9+
"net/http"
10+
"time"
11+
)
12+
13+
// responseWriter wraps http.ResponseWriter to capture the status code.
14+
type responseWriter struct {
15+
http.ResponseWriter
16+
statusCode int
17+
}
18+
19+
func (rw *responseWriter) WriteHeader(code int) {
20+
rw.statusCode = code
21+
rw.ResponseWriter.WriteHeader(code)
22+
}
23+
24+
func loggingHandler(handler http.Handler) http.Handler {
25+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
26+
start := time.Now()
27+
28+
// Create a response writer wrapper to capture status code.
29+
wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
30+
31+
// Log request details.
32+
log.Printf("[REQUEST] %s | %s | %s %s",
33+
start.Format(time.RFC3339),
34+
r.RemoteAddr,
35+
r.Method,
36+
r.URL.Path)
37+
38+
// Call the actual handler.
39+
handler.ServeHTTP(wrapped, r)
40+
41+
// Log response details.
42+
duration := time.Since(start)
43+
log.Printf("[RESPONSE] %s | %s | %s %s | Status: %d | Duration: %v",
44+
time.Now().Format(time.RFC3339),
45+
r.RemoteAddr,
46+
r.Method,
47+
r.URL.Path,
48+
wrapped.statusCode,
49+
duration)
50+
})
51+
}

examples/http/main.go

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
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+
package main
6+
7+
import (
8+
"context"
9+
"flag"
10+
"fmt"
11+
"log"
12+
"net/http"
13+
"os"
14+
"time"
15+
16+
"github.com/modelcontextprotocol/go-sdk/mcp"
17+
)
18+
19+
var (
20+
host = flag.String("host", "localhost", "host to connect to/listen on")
21+
port = flag.Int("port", 8000, "port number to connect to/listen on")
22+
proto = flag.String("proto", "http", "if set, use as proto:// part of URL (ignored for server)")
23+
)
24+
25+
func main() {
26+
out := flag.CommandLine.Output()
27+
flag.Usage = func() {
28+
fmt.Fprintf(out, "Usage: %s <client|server> [-proto <http|https>] [-port <port] [-host <host>]\n\n", os.Args[0])
29+
fmt.Fprintf(out, "This program demonstrates MCP over HTTP using the streamable transport.\n")
30+
fmt.Fprintf(out, "It can run as either a server or client.\n\n")
31+
fmt.Fprintf(out, "Options:\n")
32+
flag.PrintDefaults()
33+
fmt.Fprintf(out, "\nExamples:\n")
34+
fmt.Fprintf(out, " Run as server: %s server\n", os.Args[0])
35+
fmt.Fprintf(out, " Run as client: %s client\n", os.Args[0])
36+
fmt.Fprintf(out, " Custom host/port: %s -port 9000 -host 0.0.0.0 server\n", os.Args[0])
37+
os.Exit(1)
38+
}
39+
flag.Parse()
40+
41+
if flag.NArg() != 1 {
42+
fmt.Fprintf(out, "Error: Must specify 'client' or 'server' as first argument\n")
43+
flag.Usage()
44+
}
45+
mode := flag.Arg(0)
46+
47+
switch mode {
48+
case "server":
49+
if *proto != "http" {
50+
log.Fatalf("Server only works with 'http' (you passed proto=%s)", *proto)
51+
}
52+
runServer(fmt.Sprintf("%s:%d", *host, *port))
53+
case "client":
54+
runClient(fmt.Sprintf("%s://%s:%d", *proto, *host, *port))
55+
default:
56+
fmt.Fprintf(os.Stderr, "Error: Invalid mode '%s'. Must be 'client' or 'server'\n\n", mode)
57+
flag.Usage()
58+
}
59+
}
60+
61+
// GetTimeParams defines the parameters for the cityTime tool.
62+
type GetTimeParams struct {
63+
City string `json:"city" jsonschema:"City to get time for (nyc, sf, or boston)"`
64+
}
65+
66+
// getTime implements the tool that returns the current time for a given city.
67+
func getTime(ctx context.Context, ss *mcp.ServerSession, params *mcp.CallToolParamsFor[GetTimeParams]) (*mcp.CallToolResultFor[any], error) {
68+
// Define time zones for each city
69+
locations := map[string]string{
70+
"nyc": "America/New_York",
71+
"sf": "America/Los_Angeles",
72+
"boston": "America/New_York",
73+
}
74+
75+
city := params.Arguments.City
76+
if city == "" {
77+
city = "nyc" // Default to NYC
78+
}
79+
80+
// Get the timezone.
81+
tzName, ok := locations[city]
82+
if !ok {
83+
return nil, fmt.Errorf("unknown city: %s", city)
84+
}
85+
86+
// Load the location.
87+
loc, err := time.LoadLocation(tzName)
88+
if err != nil {
89+
return nil, fmt.Errorf("failed to load timezone: %w", err)
90+
}
91+
92+
// Get current time in that location.
93+
now := time.Now().In(loc)
94+
95+
// Format the response.
96+
cityNames := map[string]string{
97+
"nyc": "New York City",
98+
"sf": "San Francisco",
99+
"boston": "Boston",
100+
}
101+
102+
response := fmt.Sprintf("The current time in %s is %s",
103+
cityNames[city],
104+
now.Format(time.RFC3339))
105+
106+
return &mcp.CallToolResultFor[any]{
107+
Content: []mcp.Content{
108+
&mcp.TextContent{Text: response},
109+
},
110+
}, nil
111+
}
112+
113+
func runServer(url string) {
114+
// Create an MCP server.
115+
server := mcp.NewServer(&mcp.Implementation{
116+
Name: "time-server",
117+
Version: "1.0.0",
118+
}, nil)
119+
120+
// Add the cityTime tool.
121+
mcp.AddTool(server, &mcp.Tool{
122+
Name: "cityTime",
123+
Description: "Get the current time in NYC, San Francisco, or Boston",
124+
}, getTime)
125+
126+
// Create the streamable HTTP handler.
127+
handler := mcp.NewStreamableHTTPHandler(func(req *http.Request) *mcp.Server {
128+
return server
129+
}, nil)
130+
131+
handlerWithLogging := loggingHandler(handler)
132+
133+
log.Printf("MCP server listening on %s", url)
134+
log.Printf("Available tool: cityTime (cities: nyc, sf, boston)")
135+
136+
// Start the HTTP server with logging handler.
137+
if err := http.ListenAndServe(url, handlerWithLogging); err != nil {
138+
log.Fatalf("Server failed: %v", err)
139+
}
140+
}
141+
142+
func runClient(url string) {
143+
ctx := context.Background()
144+
145+
// Create the URL for the server.
146+
log.Printf("Connecting to MCP server at %s", url)
147+
148+
// Create a streamable client transport.
149+
transport := mcp.NewStreamableClientTransport(url, nil)
150+
151+
// Create an MCP client.
152+
client := mcp.NewClient(&mcp.Implementation{
153+
Name: "time-client",
154+
Version: "1.0.0",
155+
}, nil)
156+
157+
// Connect to the server.
158+
session, err := client.Connect(ctx, transport)
159+
if err != nil {
160+
log.Fatalf("Failed to connect: %v", err)
161+
}
162+
defer session.Close()
163+
164+
log.Printf("Connected to server (session ID: %s)", session.ID())
165+
166+
// First, list available tools.
167+
log.Println("Listing available tools...")
168+
toolsResult, err := session.ListTools(ctx, nil)
169+
if err != nil {
170+
log.Fatalf("Failed to list tools: %v", err)
171+
}
172+
173+
for _, tool := range toolsResult.Tools {
174+
log.Printf(" - %s: %s\n", tool.Name, tool.Description)
175+
}
176+
177+
// Call the cityTime tool for each city.
178+
cities := []string{"nyc", "sf", "boston"}
179+
180+
log.Println("Getting time for each city...")
181+
for _, city := range cities {
182+
// Call the tool.
183+
result, err := session.CallTool(ctx, &mcp.CallToolParams{
184+
Name: "cityTime",
185+
Arguments: map[string]any{
186+
"city": city,
187+
},
188+
})
189+
if err != nil {
190+
log.Printf("Failed to get time for %s: %v\n", city, err)
191+
continue
192+
}
193+
194+
// Print the result.
195+
for _, content := range result.Content {
196+
if textContent, ok := content.(*mcp.TextContent); ok {
197+
log.Printf(" %s", textContent.Text)
198+
}
199+
}
200+
}
201+
202+
log.Println("Client completed successfully")
203+
}

0 commit comments

Comments
 (0)