diff --git a/examples/http/README.md b/examples/http/README.md new file mode 100644 index 00000000..16a4801d --- /dev/null +++ b/examples/http/README.md @@ -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) diff --git a/examples/http/logging_middleware.go b/examples/http/logging_middleware.go new file mode 100644 index 00000000..4266012c --- /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 ( + "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) + }) +} diff --git a/examples/http/main.go b/examples/http/main.go new file mode 100644 index 00000000..682dc8d8 --- /dev/null +++ b/examples/http/main.go @@ -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 [-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(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") +}