From 60422edb98db6f1b50a11dc003ba3ce1c687cc13 Mon Sep 17 00:00:00 2001 From: Adam Koszek Date: Wed, 23 Jul 2025 16:22:00 -0700 Subject: [PATCH 01/14] HTTP example. --- examples/http/README.md | 60 ++++++++++++ examples/http/main.go | 196 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 256 insertions(+) create mode 100644 examples/http/README.md create mode 100644 examples/http/main.go diff --git a/examples/http/README.md b/examples/http/README.md new file mode 100644 index 00000000..724fbff5 --- /dev/null +++ b/examples/http/README.md @@ -0,0 +1,60 @@ +# 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 `get_time` tool +- A client that connects to the server, lists available tools, and calls the `get_time` tool + +## Running the Example + +### Start the Server + +```bash +go run main.go -server +``` + +This starts an MCP server on `localhost:8080` (default) that provides a `get_time` tool. + +### Run the 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 `get_time` tool for NYC, San Francisco, and Boston +4. Display the results + +### Custom Host and Port + +You can specify custom host and port: + +```bash +# Server on all interfaces, port 9090 +go run main.go -server -host 0.0.0.0 -port 9090 + +# Client connecting to custom address +go run main.go -client -host 192.168.1.100 -port 9090 +``` + +## Testing with Real-World MCP Clients + +You can test this server with Claude Code or other MCP clients: + +```bash +# Start the server +go run main.go -server -host localhost -port 8080 + +# In another terminal, add the server to Claude Code +claude mcp add http://localhost:8080 +``` + +Once added, Claude Code will be able to discover and use the `get_time` tool provided by this server. + diff --git a/examples/http/main.go b/examples/http/main.go new file mode 100644 index 00000000..5f5e8931 --- /dev/null +++ b/examples/http/main.go @@ -0,0 +1,196 @@ +// 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" +) + +func main() { + var ( + serverMode = flag.Bool("server", false, "Run as server") + clientMode = flag.Bool("client", false, "Run as client") + host = flag.String("host", "localhost", "Host to connect to or listen on") + port = flag.String("port", "8080", "Port to connect to or listen on") + ) + + flag.Usage = func() { + fmt.Fprintf(os.Stderr, "Usage: %s [options]\n\n", os.Args[0]) + fmt.Fprintf(os.Stderr, "This program demonstrates MCP over HTTP using the streamable transport.\n") + fmt.Fprintf(os.Stderr, "It can run as either a server or client.\n\n") + fmt.Fprintf(os.Stderr, "Options:\n") + flag.PrintDefaults() + fmt.Fprintf(os.Stderr, "\nExamples:\n") + fmt.Fprintf(os.Stderr, " Run as server: %s -server\n", os.Args[0]) + fmt.Fprintf(os.Stderr, " Run as client: %s -client\n", os.Args[0]) + fmt.Fprintf(os.Stderr, " Custom host/port: %s -server -host 0.0.0.0 -port 9090\n", os.Args[0]) + os.Exit(1) + } + + flag.Parse() + + if (*serverMode && *clientMode) || (!*serverMode && !*clientMode) { + fmt.Fprintf(os.Stderr, "Error: Must specify exactly one of -server or -client\n\n") + flag.Usage() + } + + if *serverMode { + runServer(*host, *port) + } else { + runClient(*host, *port) + } +} + +// GetTimeParams defines the parameters for the get_time 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("3:04:05 PM MST on Monday, January 2, 2006")) + + return &mcp.CallToolResultFor[any]{ + Content: []mcp.Content{ + &mcp.TextContent{Text: response}, + }, + }, nil +} + +func runServer(host, port string) { + // Create an MCP server + server := mcp.NewServer(&mcp.Implementation{ + Name: "time-server", + Version: "1.0.0", + }, nil) + + // Add the get_time tool + mcp.AddTool(server, &mcp.Tool{ + Name: "get_time", + 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) + + addr := fmt.Sprintf("%s:%s", host, port) + log.Printf("MCP server listening on http://%s", addr) + log.Printf("Available tool: get_time (cities: nyc, sf, boston)") + + // Start the HTTP server + if err := http.ListenAndServe(addr, handler); err != nil { + log.Fatalf("Server failed: %v", err) + } +} + +func runClient(host, port string) { + ctx := context.Background() + + // Create the URL for the server + url := fmt.Sprintf("http://%s:%s", host, port) + 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("\nListing available tools...") + toolsResult, err := session.ListTools(ctx, &mcp.ListToolsParams{}) + if err != nil { + log.Fatalf("Failed to list tools: %v", err) + } + + for _, tool := range toolsResult.Tools { + log.Printf(" - %s: %s", tool.Name, tool.Description) + } + + // Call the get_time tool for each city + cities := []string{"nyc", "sf", "boston"} + + log.Println("\nGetting time for each city...") + for _, city := range cities { + // Call the tool + result, err := session.CallTool(ctx, &mcp.CallToolParams{ + Name: "get_time", + Arguments: map[string]interface{}{ + "city": city, + }, + }) + if err != nil { + log.Printf("Failed to get time for %s: %v", 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("\nClient completed successfully") +} \ No newline at end of file From 2b21dbbc581c4708336151b606ec189a1ff076fc Mon Sep 17 00:00:00 2001 From: Adam Koszek Date: Wed, 23 Jul 2025 16:31:46 -0700 Subject: [PATCH 02/14] Add more details to README. --- examples/http/README.md | 42 ++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/examples/http/README.md b/examples/http/README.md index 724fbff5..51686950 100644 --- a/examples/http/README.md +++ b/examples/http/README.md @@ -10,6 +10,10 @@ The example implements: ## Running the Example +When run with -server, the program becomes an MCP server. +When run with -client, it becomes an MCP client. +Server and client support passing custom -port and -host. + ### Start the Server ```bash @@ -32,29 +36,33 @@ The client will: 3. Call the `get_time` tool for NYC, San Francisco, and Boston 4. Display the results -### Custom Host and Port -You can specify custom host and port: +## Testing with real-world MCP Clients -```bash -# Server on all interfaces, port 9090 -go run main.go -server -host 0.0.0.0 -port 9090 +Once the server is started, assuming it's the default +localhost:8080, you can try to add it to a popular MCP client: -# Client connecting to custom address -go run main.go -client -host 192.168.1.100 -port 9090 -``` + claude mcp add -t http timezone http://localhost:8080 -## Testing with Real-World MCP Clients +Once added, Claude Code will be able to discover and use the `get_time` tool provided by this server. -You can test this server with Claude Code or other MCP clients: +In Claude Code: -```bash -# Start the server -go run main.go -server -host localhost -port 8080 + > what's the timezone -# In another terminal, add the server to Claude Code -claude mcp add http://localhost:8080 -``` + ⏺ I'll get the current time in a major US city for you. -Once added, Claude Code will be able to discover and use the `get_time` tool provided by this server. + ⏺ timezone - get_time (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) From 8d8a0b69a0b9500d92f22c9363bc5b73b865df0f Mon Sep 17 00:00:00 2001 From: Adam Koszek Date: Wed, 23 Jul 2025 17:00:19 -0700 Subject: [PATCH 03/14] Add logging to make sure we know what's going on --- examples/http/main.go | 43 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/examples/http/main.go b/examples/http/main.go index 5f5e8931..274e0502 100644 --- a/examples/http/main.go +++ b/examples/http/main.go @@ -103,6 +103,17 @@ func getTime(ctx context.Context, ss *mcp.ServerSession, params *mcp.CallToolPar }, nil } +// 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 runServer(host, port string) { // Create an MCP server server := mcp.NewServer(&mcp.Implementation{ @@ -121,12 +132,40 @@ func runServer(host, port string) { return server }, nil) + // Wrap the handler with logging middleware + loggingHandler := 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("2006-01-02 15:04:05.000"), + 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("2006-01-02 15:04:05.000"), + r.RemoteAddr, + r.Method, + r.URL.Path, + wrapped.statusCode, + duration) + }) + addr := fmt.Sprintf("%s:%s", host, port) log.Printf("MCP server listening on http://%s", addr) log.Printf("Available tool: get_time (cities: nyc, sf, boston)") - // Start the HTTP server - if err := http.ListenAndServe(addr, handler); err != nil { + // Start the HTTP server with logging handler + if err := http.ListenAndServe(addr, loggingHandler); err != nil { log.Fatalf("Server failed: %v", err) } } From 45bff55e0094f0434834f9096138172d5fba792e Mon Sep 17 00:00:00 2001 From: Adam Koszek Date: Fri, 25 Jul 2025 21:48:20 -0700 Subject: [PATCH 04/14] Fix arguments and log formatting. --- examples/http/main.go | 55 +++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/examples/http/main.go b/examples/http/main.go index 274e0502..6c416ba9 100644 --- a/examples/http/main.go +++ b/examples/http/main.go @@ -18,36 +18,41 @@ import ( func main() { var ( - serverMode = flag.Bool("server", false, "Run as server") - clientMode = flag.Bool("client", false, "Run as client") - host = flag.String("host", "localhost", "Host to connect to or listen on") - port = flag.String("port", "8080", "Port to connect to or listen on") + host = flag.String("host", "localhost", "Host to connect to or listen on") + port = flag.String("port", "8080", "Port to connect to or listen on") ) flag.Usage = func() { - fmt.Fprintf(os.Stderr, "Usage: %s [options]\n\n", os.Args[0]) + fmt.Fprintf(os.Stderr, "Usage: %s [options]\n\n", os.Args[0]) fmt.Fprintf(os.Stderr, "This program demonstrates MCP over HTTP using the streamable transport.\n") fmt.Fprintf(os.Stderr, "It can run as either a server or client.\n\n") fmt.Fprintf(os.Stderr, "Options:\n") flag.PrintDefaults() fmt.Fprintf(os.Stderr, "\nExamples:\n") - fmt.Fprintf(os.Stderr, " Run as server: %s -server\n", os.Args[0]) - fmt.Fprintf(os.Stderr, " Run as client: %s -client\n", os.Args[0]) - fmt.Fprintf(os.Stderr, " Custom host/port: %s -server -host 0.0.0.0 -port 9090\n", os.Args[0]) + fmt.Fprintf(os.Stderr, " Run as server: %s server\n", os.Args[0]) + fmt.Fprintf(os.Stderr, " Run as client: %s client\n", os.Args[0]) + fmt.Fprintf(os.Stderr, " Custom host/port: %s server -host 0.0.0.0 -port 9090\n", os.Args[0]) os.Exit(1) } - flag.Parse() - - if (*serverMode && *clientMode) || (!*serverMode && !*clientMode) { - fmt.Fprintf(os.Stderr, "Error: Must specify exactly one of -server or -client\n\n") + // Check if we have at least one argument + if len(os.Args) < 2 { + fmt.Fprintf(os.Stderr, "Error: Must specify 'client' or 'server' as first argument\n\n") flag.Usage() } + mode := os.Args[1] - if *serverMode { + os.Args = append(os.Args[:1], os.Args[2:]...) + flag.Parse() + + switch mode { + case "server": runServer(*host, *port) - } else { + case "client": runClient(*host, *port) + default: + fmt.Fprintf(os.Stderr, "Error: Invalid mode '%s'. Must be 'client' or 'server'\n\n", mode) + flag.Usage() } } @@ -94,7 +99,7 @@ func getTime(ctx context.Context, ss *mcp.ServerSession, params *mcp.CallToolPar response := fmt.Sprintf("The current time in %s is %s", cityNames[city], - now.Format("3:04:05 PM MST on Monday, January 2, 2006")) + now.Format(time.RFC3339)) return &mcp.CallToolResultFor[any]{ Content: []mcp.Content{ @@ -141,7 +146,7 @@ func runServer(host, port string) { // Log request details log.Printf("[REQUEST] %s | %s | %s %s", - start.Format("2006-01-02 15:04:05.000"), + start.Format(time.RFC3339), r.RemoteAddr, r.Method, r.URL.Path) @@ -152,7 +157,7 @@ func runServer(host, port string) { // Log response details duration := time.Since(start) log.Printf("[RESPONSE] %s | %s | %s %s | Status: %d | Duration: %v", - time.Now().Format("2006-01-02 15:04:05.000"), + time.Now().Format(time.RFC3339), r.RemoteAddr, r.Method, r.URL.Path, @@ -196,30 +201,30 @@ func runClient(host, port string) { log.Printf("Connected to server (session ID: %s)", session.ID()) // First, list available tools - log.Println("\nListing available tools...") - toolsResult, err := session.ListTools(ctx, &mcp.ListToolsParams{}) + 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", tool.Name, tool.Description) + log.Printf(" - %s: %s\n", tool.Name, tool.Description) } // Call the get_time tool for each city cities := []string{"nyc", "sf", "boston"} - log.Println("\nGetting time for each city...") + log.Println("Getting time for each city...") for _, city := range cities { // Call the tool result, err := session.CallTool(ctx, &mcp.CallToolParams{ Name: "get_time", - Arguments: map[string]interface{}{ + Arguments: map[string]any{ "city": city, }, }) if err != nil { - log.Printf("Failed to get time for %s: %v", city, err) + log.Printf("Failed to get time for %s: %v\n", city, err) continue } @@ -231,5 +236,5 @@ func runClient(host, port string) { } } - log.Println("\nClient completed successfully") -} \ No newline at end of file + log.Println("Client completed successfully") +} From f8327306f5655bddf0493d54930942165410146e Mon Sep 17 00:00:00 2001 From: Adam Koszek Date: Fri, 25 Jul 2025 22:21:12 -0700 Subject: [PATCH 05/14] Split up logging middleware. --- examples/http/logging_middleware.go | 51 +++++++++++++++++++++++++++++ examples/http/main.go | 41 ++--------------------- 2 files changed, 53 insertions(+), 39 deletions(-) create mode 100644 examples/http/logging_middleware.go diff --git a/examples/http/logging_middleware.go b/examples/http/logging_middleware.go new file mode 100644 index 00000000..c17153c8 --- /dev/null +++ b/examples/http/logging_middleware.go @@ -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 ( + "net/http" + "time" + "log" +) + +// 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) + }) +} diff --git a/examples/http/main.go b/examples/http/main.go index 6c416ba9..872d2172 100644 --- a/examples/http/main.go +++ b/examples/http/main.go @@ -108,17 +108,6 @@ func getTime(ctx context.Context, ss *mcp.ServerSession, params *mcp.CallToolPar }, nil } -// 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 runServer(host, port string) { // Create an MCP server server := mcp.NewServer(&mcp.Implementation{ @@ -137,40 +126,14 @@ func runServer(host, port string) { return server }, nil) - // Wrap the handler with logging middleware - loggingHandler := 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) - }) + handlerWithLogging := loggingHandler(handler) addr := fmt.Sprintf("%s:%s", host, port) log.Printf("MCP server listening on http://%s", addr) log.Printf("Available tool: get_time (cities: nyc, sf, boston)") // Start the HTTP server with logging handler - if err := http.ListenAndServe(addr, loggingHandler); err != nil { + if err := http.ListenAndServe(addr, handlerWithLogging); err != nil { log.Fatalf("Server failed: %v", err) } } From f3d0ada0e2d593dc8e04e2e16d87d1b10ab3a2df Mon Sep 17 00:00:00 2001 From: Adam Koszek Date: Fri, 25 Jul 2025 22:25:15 -0700 Subject: [PATCH 06/14] get_time -> cityTime --- examples/http/main.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/http/main.go b/examples/http/main.go index 872d2172..d82029ba 100644 --- a/examples/http/main.go +++ b/examples/http/main.go @@ -56,7 +56,7 @@ func main() { } } -// GetTimeParams defines the parameters for the get_time tool +// 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)"` } @@ -115,9 +115,9 @@ func runServer(host, port string) { Version: "1.0.0", }, nil) - // Add the get_time tool + // Add the cityTime tool mcp.AddTool(server, &mcp.Tool{ - Name: "get_time", + Name: "cityTime", Description: "Get the current time in NYC, San Francisco, or Boston", }, getTime) @@ -130,7 +130,7 @@ func runServer(host, port string) { addr := fmt.Sprintf("%s:%s", host, port) log.Printf("MCP server listening on http://%s", addr) - log.Printf("Available tool: get_time (cities: nyc, sf, boston)") + log.Printf("Available tool: cityTime (cities: nyc, sf, boston)") // Start the HTTP server with logging handler if err := http.ListenAndServe(addr, handlerWithLogging); err != nil { @@ -174,14 +174,14 @@ func runClient(host, port string) { log.Printf(" - %s: %s\n", tool.Name, tool.Description) } - // Call the get_time tool for each city + // 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: "get_time", + Name: "cityTime", Arguments: map[string]any{ "city": city, }, From d5c9351d705d91290ddcaf6d261a4fdb7c523091 Mon Sep 17 00:00:00 2001 From: Adam Koszek Date: Fri, 25 Jul 2025 22:27:25 -0700 Subject: [PATCH 07/14] Format. --- examples/http/logging_middleware.go | 12 ++++++------ examples/http/main.go | 10 +++++----- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/examples/http/logging_middleware.go b/examples/http/logging_middleware.go index c17153c8..9dc2c952 100644 --- a/examples/http/logging_middleware.go +++ b/examples/http/logging_middleware.go @@ -5,9 +5,9 @@ package main import ( + "log" "net/http" "time" - "log" ) // responseWriter wraps http.ResponseWriter to capture the status code @@ -24,20 +24,20 @@ func (rw *responseWriter) WriteHeader(code int) { 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", + 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", diff --git a/examples/http/main.go b/examples/http/main.go index d82029ba..6e4bd905 100644 --- a/examples/http/main.go +++ b/examples/http/main.go @@ -44,7 +44,7 @@ func main() { os.Args = append(os.Args[:1], os.Args[2:]...) flag.Parse() - + switch mode { case "server": runServer(*host, *port) @@ -97,8 +97,8 @@ func getTime(ctx context.Context, ss *mcp.ServerSession, params *mcp.CallToolPar "boston": "Boston", } - response := fmt.Sprintf("The current time in %s is %s", - cityNames[city], + response := fmt.Sprintf("The current time in %s is %s", + cityNames[city], now.Format(time.RFC3339)) return &mcp.CallToolResultFor[any]{ @@ -140,7 +140,7 @@ func runServer(host, port string) { func runClient(host, port string) { ctx := context.Background() - + // Create the URL for the server url := fmt.Sprintf("http://%s:%s", host, port) log.Printf("Connecting to MCP server at %s", url) @@ -176,7 +176,7 @@ func runClient(host, port string) { // 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 From e312ab53c9c66f2a12b8083ceb58fd9205bec811 Mon Sep 17 00:00:00 2001 From: Adam Koszek Date: Fri, 25 Jul 2025 22:36:18 -0700 Subject: [PATCH 08/14] README updates. --- examples/http/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/http/README.md b/examples/http/README.md index 51686950..98caf81d 100644 --- a/examples/http/README.md +++ b/examples/http/README.md @@ -5,8 +5,8 @@ This example demonstrates how to use the Model Context Protocol (MCP) over HTTP ## Overview The example implements: -- A server that provides a `get_time` tool -- A client that connects to the server, lists available tools, and calls the `get_time` tool +- A server that provides a `cityTime` tool +- A client that connects to the server, lists available tools, and calls the `cityTime` tool ## Running the Example @@ -20,7 +20,7 @@ Server and client support passing custom -port and -host. go run main.go -server ``` -This starts an MCP server on `localhost:8080` (default) that provides a `get_time` tool. +This starts an MCP server on `localhost:8080` (default) that provides a `cityTime` tool. ### Run the Client @@ -33,7 +33,7 @@ go run main.go -client The client will: 1. Connect to the server 2. List available tools -3. Call the `get_time` tool for NYC, San Francisco, and Boston +3. Call the `cityTime` tool for NYC, San Francisco, and Boston 4. Display the results @@ -44,7 +44,7 @@ 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 `get_time` tool provided by this server. +Once added, Claude Code will be able to discover and use the `cityTime` tool provided by this server. In Claude Code: @@ -52,7 +52,7 @@ In Claude Code: ⏺ I'll get the current time in a major US city for you. - ⏺ timezone - get_time (MCP)(city: "nyc") + ⏺ timezone - cityTime (MCP)(city: "nyc") ⎿ The current time in New York City is 7:30:16 PM EDT on Wedn esday, July 23, 2025 From 1e2c65c88a09d75a19e569103581f006cf5f83e4 Mon Sep 17 00:00:00 2001 From: Adam Koszek Date: Sat, 26 Jul 2025 08:50:11 -0700 Subject: [PATCH 09/14] Dots --- examples/http/logging_middleware.go | 10 ++++---- examples/http/main.go | 38 ++++++++++++++--------------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/examples/http/logging_middleware.go b/examples/http/logging_middleware.go index 9dc2c952..4266012c 100644 --- a/examples/http/logging_middleware.go +++ b/examples/http/logging_middleware.go @@ -10,7 +10,7 @@ import ( "time" ) -// responseWriter wraps http.ResponseWriter to capture the status code +// responseWriter wraps http.ResponseWriter to capture the status code. type responseWriter struct { http.ResponseWriter statusCode int @@ -25,20 +25,20 @@ 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 + // Create a response writer wrapper to capture status code. wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK} - // Log request details + // 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 + // Call the actual handler. handler.ServeHTTP(wrapped, r) - // Log response details + // Log response details. duration := time.Since(start) log.Printf("[RESPONSE] %s | %s | %s %s | Status: %d | Duration: %v", time.Now().Format(time.RFC3339), diff --git a/examples/http/main.go b/examples/http/main.go index 6e4bd905..1925ecdf 100644 --- a/examples/http/main.go +++ b/examples/http/main.go @@ -35,7 +35,7 @@ func main() { os.Exit(1) } - // Check if we have at least one argument + // Check if we have at least one argument. if len(os.Args) < 2 { fmt.Fprintf(os.Stderr, "Error: Must specify 'client' or 'server' as first argument\n\n") flag.Usage() @@ -56,12 +56,12 @@ func main() { } } -// GetTimeParams defines the parameters for the cityTime tool +// 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 +// 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{ @@ -75,22 +75,22 @@ func getTime(ctx context.Context, ss *mcp.ServerSession, params *mcp.CallToolPar city = "nyc" // Default to NYC } - // Get the timezone + // Get the timezone. tzName, ok := locations[city] if !ok { return nil, fmt.Errorf("unknown city: %s", city) } - // Load the location + // 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 + // Get current time in that location. now := time.Now().In(loc) - // Format the response + // Format the response. cityNames := map[string]string{ "nyc": "New York City", "sf": "San Francisco", @@ -109,19 +109,19 @@ func getTime(ctx context.Context, ss *mcp.ServerSession, params *mcp.CallToolPar } func runServer(host, port string) { - // Create an MCP server + // Create an MCP server. server := mcp.NewServer(&mcp.Implementation{ Name: "time-server", Version: "1.0.0", }, nil) - // Add the cityTime tool + // 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 + // Create the streamable HTTP handler. handler := mcp.NewStreamableHTTPHandler(func(req *http.Request) *mcp.Server { return server }, nil) @@ -132,7 +132,7 @@ func runServer(host, port string) { log.Printf("MCP server listening on http://%s", addr) log.Printf("Available tool: cityTime (cities: nyc, sf, boston)") - // Start the HTTP server with logging handler + // Start the HTTP server with logging handler. if err := http.ListenAndServe(addr, handlerWithLogging); err != nil { log.Fatalf("Server failed: %v", err) } @@ -141,20 +141,20 @@ func runServer(host, port string) { func runClient(host, port string) { ctx := context.Background() - // Create the URL for the server + // Create the URL for the server. url := fmt.Sprintf("http://%s:%s", host, port) log.Printf("Connecting to MCP server at %s", url) - // Create a streamable client transport + // Create a streamable client transport. transport := mcp.NewStreamableClientTransport(url, nil) - // Create an MCP client + // Create an MCP client. client := mcp.NewClient(&mcp.Implementation{ Name: "time-client", Version: "1.0.0", }, nil) - // Connect to the server + // Connect to the server. session, err := client.Connect(ctx, transport) if err != nil { log.Fatalf("Failed to connect: %v", err) @@ -163,7 +163,7 @@ func runClient(host, port string) { log.Printf("Connected to server (session ID: %s)", session.ID()) - // First, list available tools + // First, list available tools. log.Println("Listing available tools...") toolsResult, err := session.ListTools(ctx, nil) if err != nil { @@ -174,12 +174,12 @@ func runClient(host, port string) { log.Printf(" - %s: %s\n", tool.Name, tool.Description) } - // Call the cityTime tool for each city + // 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 + // Call the tool. result, err := session.CallTool(ctx, &mcp.CallToolParams{ Name: "cityTime", Arguments: map[string]any{ @@ -191,7 +191,7 @@ func runClient(host, port string) { continue } - // Print the result + // Print the result. for _, content := range result.Content { if textContent, ok := content.(*mcp.TextContent); ok { log.Printf(" %s", textContent.Text) From cdb64cee7127bf9b6566c9722edad026a66b86a3 Mon Sep 17 00:00:00 2001 From: Adam Koszek Date: Mon, 28 Jul 2025 17:04:40 -0700 Subject: [PATCH 10/14] Accept URL --- examples/http/main.go | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/examples/http/main.go b/examples/http/main.go index 1925ecdf..f791c706 100644 --- a/examples/http/main.go +++ b/examples/http/main.go @@ -10,6 +10,7 @@ import ( "fmt" "log" "net/http" + "net/url" "os" "time" @@ -17,13 +18,8 @@ import ( ) func main() { - var ( - host = flag.String("host", "localhost", "Host to connect to or listen on") - port = flag.String("port", "8080", "Port to connect to or listen on") - ) - flag.Usage = func() { - fmt.Fprintf(os.Stderr, "Usage: %s [options]\n\n", os.Args[0]) + fmt.Fprintf(os.Stderr, "Usage: %s [proto://:]\n\n", os.Args[0]) fmt.Fprintf(os.Stderr, "This program demonstrates MCP over HTTP using the streamable transport.\n") fmt.Fprintf(os.Stderr, "It can run as either a server or client.\n\n") fmt.Fprintf(os.Stderr, "Options:\n") @@ -31,25 +27,30 @@ func main() { fmt.Fprintf(os.Stderr, "\nExamples:\n") fmt.Fprintf(os.Stderr, " Run as server: %s server\n", os.Args[0]) fmt.Fprintf(os.Stderr, " Run as client: %s client\n", os.Args[0]) - fmt.Fprintf(os.Stderr, " Custom host/port: %s server -host 0.0.0.0 -port 9090\n", os.Args[0]) + fmt.Fprintf(os.Stderr, " Custom host/port: %s server https://0.0.0.0:8000\n", os.Args[0]) os.Exit(1) } - // Check if we have at least one argument. if len(os.Args) < 2 { fmt.Fprintf(os.Stderr, "Error: Must specify 'client' or 'server' as first argument\n\n") flag.Usage() } mode := os.Args[1] - os.Args = append(os.Args[:1], os.Args[2:]...) - flag.Parse() + rawurl := "http://localhost:8000" + if len(os.Args) >= 3 { + rawurl = os.Args[2] + } + url, err := url.Parse(rawurl) + if err != nil { + log.Fatalf("Server failed: %v", err) + } switch mode { case "server": - runServer(*host, *port) + runServer(url) case "client": - runClient(*host, *port) + runClient(url) default: fmt.Fprintf(os.Stderr, "Error: Invalid mode '%s'. Must be 'client' or 'server'\n\n", mode) flag.Usage() @@ -108,7 +109,7 @@ func getTime(ctx context.Context, ss *mcp.ServerSession, params *mcp.CallToolPar }, nil } -func runServer(host, port string) { +func runServer(url *url.URL) { // Create an MCP server. server := mcp.NewServer(&mcp.Implementation{ Name: "time-server", @@ -128,25 +129,24 @@ func runServer(host, port string) { handlerWithLogging := loggingHandler(handler) - addr := fmt.Sprintf("%s:%s", host, port) - log.Printf("MCP server listening on http://%s", addr) + laddr := fmt.Sprintf("%s:%s", url.Hostname(), url.Port()) + log.Printf("MCP server listening on %s", laddr) log.Printf("Available tool: cityTime (cities: nyc, sf, boston)") // Start the HTTP server with logging handler. - if err := http.ListenAndServe(addr, handlerWithLogging); err != nil { + if err := http.ListenAndServe("localhost:8000", handlerWithLogging); err != nil { log.Fatalf("Server failed: %v", err) } } -func runClient(host, port string) { +func runClient(url *url.URL) { ctx := context.Background() // Create the URL for the server. - url := fmt.Sprintf("http://%s:%s", host, port) - log.Printf("Connecting to MCP server at %s", url) + log.Printf("Connecting to MCP server at %s", url.String()) // Create a streamable client transport. - transport := mcp.NewStreamableClientTransport(url, nil) + transport := mcp.NewStreamableClientTransport(url.String(), nil) // Create an MCP client. client := mcp.NewClient(&mcp.Implementation{ From 73c10f2d39aef2f653e002d0106574a6b080d247 Mon Sep 17 00:00:00 2001 From: Adam Koszek Date: Mon, 28 Jul 2025 17:29:03 -0700 Subject: [PATCH 11/14] Bring better docs. --- examples/http/README.md | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/examples/http/README.md b/examples/http/README.md index 98caf81d..7e17990a 100644 --- a/examples/http/README.md +++ b/examples/http/README.md @@ -8,26 +8,19 @@ 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 -## Running the Example +## Usage -When run with -server, the program becomes an MCP server. -When run with -client, it becomes an MCP client. -Server and client support passing custom -port and -host. - -### Start the Server +Start the Server ```bash -go run main.go -server +go run main.go server ``` +This starts an MCP server on `http://localhost:8080` (default) that provides a `cityTime` tool. -This starts an MCP server on `localhost:8080` (default) that provides a `cityTime` tool. - -### Run the Client - -In another terminal: +To run a client in another terminal: ```bash -go run main.go -client +go run main.go client ``` The client will: @@ -36,6 +29,11 @@ The client will: 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 server http://0.0.0.0:9000 +``` ## Testing with real-world MCP Clients From 797dce462ec8b681d768d67a51f77d41f871654d Mon Sep 17 00:00:00 2001 From: Adam Koszek Date: Tue, 29 Jul 2025 13:39:34 -0700 Subject: [PATCH 12/14] Back to flags. --- examples/http/main.go | 61 ++++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/examples/http/main.go b/examples/http/main.go index f791c706..780dd8c3 100644 --- a/examples/http/main.go +++ b/examples/http/main.go @@ -10,53 +10,55 @@ import ( "fmt" "log" "net/http" - "net/url" "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(os.Stderr, "Usage: %s [proto://:]\n\n", os.Args[0]) - fmt.Fprintf(os.Stderr, "This program demonstrates MCP over HTTP using the streamable transport.\n") - fmt.Fprintf(os.Stderr, "It can run as either a server or client.\n\n") - fmt.Fprintf(os.Stderr, "Options:\n") + fmt.Fprintf(out, "Usage: %s [-proto ] [-port ]\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(os.Stderr, "\nExamples:\n") - fmt.Fprintf(os.Stderr, " Run as server: %s server\n", os.Args[0]) - fmt.Fprintf(os.Stderr, " Run as client: %s client\n", os.Args[0]) - fmt.Fprintf(os.Stderr, " Custom host/port: %s server https://0.0.0.0:8000\n", os.Args[0]) + 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 len(os.Args) < 2 { - fmt.Fprintf(os.Stderr, "Error: Must specify 'client' or 'server' as first argument\n\n") + if flag.NArg() != 1 { + fmt.Fprintf(out, "Error: Must specify 'client' or 'server' as first argument\n") flag.Usage() } - mode := os.Args[1] - - rawurl := "http://localhost:8000" - if len(os.Args) >= 3 { - rawurl = os.Args[2] - } - url, err := url.Parse(rawurl) - if err != nil { - log.Fatalf("Server failed: %v", err) - } + mode := flag.Arg(0) switch mode { case "server": - runServer(url) + 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(url) + 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)"` @@ -109,7 +111,7 @@ func getTime(ctx context.Context, ss *mcp.ServerSession, params *mcp.CallToolPar }, nil } -func runServer(url *url.URL) { +func runServer(url string) { // Create an MCP server. server := mcp.NewServer(&mcp.Implementation{ Name: "time-server", @@ -129,24 +131,23 @@ func runServer(url *url.URL) { handlerWithLogging := loggingHandler(handler) - laddr := fmt.Sprintf("%s:%s", url.Hostname(), url.Port()) - log.Printf("MCP server listening on %s", laddr) + 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("localhost:8000", handlerWithLogging); err != nil { + if err := http.ListenAndServe(url, handlerWithLogging); err != nil { log.Fatalf("Server failed: %v", err) } } -func runClient(url *url.URL) { +func runClient(url string) { ctx := context.Background() // Create the URL for the server. - log.Printf("Connecting to MCP server at %s", url.String()) + log.Printf("Connecting to MCP server at %s", url) // Create a streamable client transport. - transport := mcp.NewStreamableClientTransport(url.String(), nil) + transport := mcp.NewStreamableClientTransport(url, nil) // Create an MCP client. client := mcp.NewClient(&mcp.Implementation{ From ff8dcdd60f640183d5a49d8ba2d9008330e0acf4 Mon Sep 17 00:00:00 2001 From: Adam Koszek Date: Tue, 29 Jul 2025 13:41:17 -0700 Subject: [PATCH 13/14] Fix the docs. --- examples/http/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/http/README.md b/examples/http/README.md index 7e17990a..16a4801d 100644 --- a/examples/http/README.md +++ b/examples/http/README.md @@ -32,7 +32,7 @@ The client will: 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 server http://0.0.0.0:9000 +go run main.go -host 0.0.0.0 -port 9000 server ``` ## Testing with real-world MCP Clients From 391a32e8b3ce6937607cb09a6200d15fc15c71f8 Mon Sep 17 00:00:00 2001 From: Adam Koszek Date: Fri, 8 Aug 2025 09:25:42 -0700 Subject: [PATCH 14/14] Format. --- examples/http/main.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/examples/http/main.go b/examples/http/main.go index 780dd8c3..682dc8d8 100644 --- a/examples/http/main.go +++ b/examples/http/main.go @@ -17,8 +17,8 @@ import ( ) var ( - host = flag.String("host", "localhost", "host to connect to/listen on") - port = flag.Int("port", 8000, "port number to connect to/listen on") + 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)") ) @@ -46,7 +46,7 @@ func main() { switch mode { case "server": - if (*proto != "http") { + if *proto != "http" { log.Fatalf("Server only works with 'http' (you passed proto=%s)", *proto) } runServer(fmt.Sprintf("%s:%d", *host, *port)) @@ -58,7 +58,6 @@ func main() { } } - // 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)"`