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.
This commit is contained in:
parent
7bfde44981
commit
a617dcea31
3 changed files with 320 additions and 0 deletions
66
examples/http/README.md
Normal file
66
examples/http/README.md
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# MCP HTTP Example
|
||||
|
||||
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.
|
||||
|
||||
## Overview
|
||||
|
||||
The example implements:
|
||||
- A server that provides a `cityTime` tool
|
||||
- A client that connects to the server, lists available tools, and calls the `cityTime` tool
|
||||
|
||||
## Usage
|
||||
|
||||
Start the Server
|
||||
|
||||
```bash
|
||||
go run main.go server
|
||||
```
|
||||
This starts an MCP server on `http://localhost:8080` (default) that provides a `cityTime` tool.
|
||||
|
||||
To run a client in another terminal:
|
||||
|
||||
```bash
|
||||
go run main.go client
|
||||
```
|
||||
|
||||
The client will:
|
||||
1. Connect to the server
|
||||
2. List available tools
|
||||
3. Call the `cityTime` tool for NYC, San Francisco, and Boston
|
||||
4. Display the results
|
||||
|
||||
At any given time you can pass a custom URL to the program to run it on a custom host/port:
|
||||
|
||||
```
|
||||
go run main.go -host 0.0.0.0 -port 9000 server
|
||||
```
|
||||
|
||||
## Testing with real-world MCP Clients
|
||||
|
||||
Once the server is started, assuming it's the default
|
||||
localhost:8080, you can try to add it to a popular MCP client:
|
||||
|
||||
claude mcp add -t http timezone http://localhost:8080
|
||||
|
||||
Once added, Claude Code will be able to discover and use the `cityTime` tool provided by this server.
|
||||
|
||||
In Claude Code:
|
||||
|
||||
> what's the timezone
|
||||
|
||||
⏺ I'll get the current time in a major US city for you.
|
||||
|
||||
⏺ timezone - cityTime (MCP)(city: "nyc")
|
||||
⎿ The current time in New York City is 7:30:16 PM EDT on Wedn
|
||||
esday, July 23, 2025
|
||||
|
||||
|
||||
⏺ The current timezone is EDT (Eastern Daylight Time), and it's
|
||||
7:30 PM on Wednesday, July 23, 2025.
|
||||
|
||||
> what timezones do you support?
|
||||
|
||||
⏺ The timezone tool supports three US cities:
|
||||
- NYC (Eastern Time)
|
||||
- SF (Pacific Time)
|
||||
- Boston (Eastern Time)
|
||||
51
examples/http/logging_middleware.go
Normal file
51
examples/http/logging_middleware.go
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
// 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.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// responseWriter wraps http.ResponseWriter to capture the status code.
|
||||
type responseWriter struct {
|
||||
http.ResponseWriter
|
||||
statusCode int
|
||||
}
|
||||
|
||||
func (rw *responseWriter) WriteHeader(code int) {
|
||||
rw.statusCode = code
|
||||
rw.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
func loggingHandler(handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
|
||||
// Create a response writer wrapper to capture status code.
|
||||
wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
|
||||
|
||||
// Log request details.
|
||||
log.Printf("[REQUEST] %s | %s | %s %s",
|
||||
start.Format(time.RFC3339),
|
||||
r.RemoteAddr,
|
||||
r.Method,
|
||||
r.URL.Path)
|
||||
|
||||
// Call the actual handler.
|
||||
handler.ServeHTTP(wrapped, r)
|
||||
|
||||
// Log response details.
|
||||
duration := time.Since(start)
|
||||
log.Printf("[RESPONSE] %s | %s | %s %s | Status: %d | Duration: %v",
|
||||
time.Now().Format(time.RFC3339),
|
||||
r.RemoteAddr,
|
||||
r.Method,
|
||||
r.URL.Path,
|
||||
wrapped.statusCode,
|
||||
duration)
|
||||
})
|
||||
}
|
||||
203
examples/http/main.go
Normal file
203
examples/http/main.go
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
// 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.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
)
|
||||
|
||||
var (
|
||||
host = flag.String("host", "localhost", "host to connect to/listen on")
|
||||
port = flag.Int("port", 8000, "port number to connect to/listen on")
|
||||
proto = flag.String("proto", "http", "if set, use as proto:// part of URL (ignored for server)")
|
||||
)
|
||||
|
||||
func main() {
|
||||
out := flag.CommandLine.Output()
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintf(out, "Usage: %s <client|server> [-proto <http|https>] [-port <port] [-host <host>]\n\n", os.Args[0])
|
||||
fmt.Fprintf(out, "This program demonstrates MCP over HTTP using the streamable transport.\n")
|
||||
fmt.Fprintf(out, "It can run as either a server or client.\n\n")
|
||||
fmt.Fprintf(out, "Options:\n")
|
||||
flag.PrintDefaults()
|
||||
fmt.Fprintf(out, "\nExamples:\n")
|
||||
fmt.Fprintf(out, " Run as server: %s server\n", os.Args[0])
|
||||
fmt.Fprintf(out, " Run as client: %s client\n", os.Args[0])
|
||||
fmt.Fprintf(out, " Custom host/port: %s -port 9000 -host 0.0.0.0 server\n", os.Args[0])
|
||||
os.Exit(1)
|
||||
}
|
||||
flag.Parse()
|
||||
|
||||
if flag.NArg() != 1 {
|
||||
fmt.Fprintf(out, "Error: Must specify 'client' or 'server' as first argument\n")
|
||||
flag.Usage()
|
||||
}
|
||||
mode := flag.Arg(0)
|
||||
|
||||
switch mode {
|
||||
case "server":
|
||||
if *proto != "http" {
|
||||
log.Fatalf("Server only works with 'http' (you passed proto=%s)", *proto)
|
||||
}
|
||||
runServer(fmt.Sprintf("%s:%d", *host, *port))
|
||||
case "client":
|
||||
runClient(fmt.Sprintf("%s://%s:%d", *proto, *host, *port))
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "Error: Invalid mode '%s'. Must be 'client' or 'server'\n\n", mode)
|
||||
flag.Usage()
|
||||
}
|
||||
}
|
||||
|
||||
// GetTimeParams defines the parameters for the cityTime tool.
|
||||
type GetTimeParams struct {
|
||||
City string `json:"city" jsonschema:"City to get time for (nyc, sf, or boston)"`
|
||||
}
|
||||
|
||||
// getTime implements the tool that returns the current time for a given city.
|
||||
func getTime(ctx context.Context, ss *mcp.ServerSession, params *mcp.CallToolParamsFor[GetTimeParams]) (*mcp.CallToolResultFor[any], error) {
|
||||
// Define time zones for each city
|
||||
locations := map[string]string{
|
||||
"nyc": "America/New_York",
|
||||
"sf": "America/Los_Angeles",
|
||||
"boston": "America/New_York",
|
||||
}
|
||||
|
||||
city := params.Arguments.City
|
||||
if city == "" {
|
||||
city = "nyc" // Default to NYC
|
||||
}
|
||||
|
||||
// Get the timezone.
|
||||
tzName, ok := locations[city]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown city: %s", city)
|
||||
}
|
||||
|
||||
// Load the location.
|
||||
loc, err := time.LoadLocation(tzName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load timezone: %w", err)
|
||||
}
|
||||
|
||||
// Get current time in that location.
|
||||
now := time.Now().In(loc)
|
||||
|
||||
// Format the response.
|
||||
cityNames := map[string]string{
|
||||
"nyc": "New York City",
|
||||
"sf": "San Francisco",
|
||||
"boston": "Boston",
|
||||
}
|
||||
|
||||
response := fmt.Sprintf("The current time in %s is %s",
|
||||
cityNames[city],
|
||||
now.Format(time.RFC3339))
|
||||
|
||||
return &mcp.CallToolResultFor[any]{
|
||||
Content: []mcp.Content{
|
||||
&mcp.TextContent{Text: response},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func runServer(url string) {
|
||||
// Create an MCP server.
|
||||
server := mcp.NewServer(&mcp.Implementation{
|
||||
Name: "time-server",
|
||||
Version: "1.0.0",
|
||||
}, nil)
|
||||
|
||||
// Add the cityTime tool.
|
||||
mcp.AddTool(server, &mcp.Tool{
|
||||
Name: "cityTime",
|
||||
Description: "Get the current time in NYC, San Francisco, or Boston",
|
||||
}, getTime)
|
||||
|
||||
// Create the streamable HTTP handler.
|
||||
handler := mcp.NewStreamableHTTPHandler(func(req *http.Request) *mcp.Server {
|
||||
return server
|
||||
}, nil)
|
||||
|
||||
handlerWithLogging := loggingHandler(handler)
|
||||
|
||||
log.Printf("MCP server listening on %s", url)
|
||||
log.Printf("Available tool: cityTime (cities: nyc, sf, boston)")
|
||||
|
||||
// Start the HTTP server with logging handler.
|
||||
if err := http.ListenAndServe(url, handlerWithLogging); err != nil {
|
||||
log.Fatalf("Server failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func runClient(url string) {
|
||||
ctx := context.Background()
|
||||
|
||||
// Create the URL for the server.
|
||||
log.Printf("Connecting to MCP server at %s", url)
|
||||
|
||||
// Create a streamable client transport.
|
||||
transport := mcp.NewStreamableClientTransport(url, nil)
|
||||
|
||||
// Create an MCP client.
|
||||
client := mcp.NewClient(&mcp.Implementation{
|
||||
Name: "time-client",
|
||||
Version: "1.0.0",
|
||||
}, nil)
|
||||
|
||||
// Connect to the server.
|
||||
session, err := client.Connect(ctx, transport)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect: %v", err)
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
log.Printf("Connected to server (session ID: %s)", session.ID())
|
||||
|
||||
// First, list available tools.
|
||||
log.Println("Listing available tools...")
|
||||
toolsResult, err := session.ListTools(ctx, nil)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to list tools: %v", err)
|
||||
}
|
||||
|
||||
for _, tool := range toolsResult.Tools {
|
||||
log.Printf(" - %s: %s\n", tool.Name, tool.Description)
|
||||
}
|
||||
|
||||
// Call the cityTime tool for each city.
|
||||
cities := []string{"nyc", "sf", "boston"}
|
||||
|
||||
log.Println("Getting time for each city...")
|
||||
for _, city := range cities {
|
||||
// Call the tool.
|
||||
result, err := session.CallTool(ctx, &mcp.CallToolParams{
|
||||
Name: "cityTime",
|
||||
Arguments: map[string]any{
|
||||
"city": city,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("Failed to get time for %s: %v\n", city, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Print the result.
|
||||
for _, content := range result.Content {
|
||||
if textContent, ok := content.(*mcp.TextContent); ok {
|
||||
log.Printf(" %s", textContent.Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("Client completed successfully")
|
||||
}
|
||||
Loading…
Reference in a new issue