Skip to content
66 changes: 66 additions & 0 deletions examples/http/README.md
Original file line number Diff line number Diff line change
@@ -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 server http://0.0.0.0:9000
```

## 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 changes: 51 additions & 0 deletions examples/http/logging_middleware.go
Original file line number Diff line number Diff line change
@@ -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 changes: 203 additions & 0 deletions examples/http/main.go
Original file line number Diff line number Diff line change
@@ -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"
"net/url"
"os"
"time"

"github.com/modelcontextprotocol/go-sdk/mcp"
)

func main() {
flag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: %s <client|server> [proto://<host>:<port>]\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 https://0.0.0.0:8000\n", os.Args[0])
os.Exit(1)
}

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]

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(url)
case "client":
runClient(url)
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 *url.URL) {
// 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)

laddr := fmt.Sprintf("%s:%s", url.Hostname(), url.Port())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allowing proto is deceptive: I can't create an HTTPS server if I write https://example.com:8080.
I think you should go back to host and port flags. The host will essentially always be localhost, so that should definitely be a flag. If you want to have port be an arg, I'm OK with that, but I'm also fine with a flag and a default value.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jba take a look -- I fixed the rest of stuff, and I want to keep the proto, because without a way to set it to https:// for the client, I can't really test the remote side, if it's behind the load balancer.

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("localhost:8000", handlerWithLogging); err != nil {
log.Fatalf("Server failed: %v", err)
}
}

func runClient(url *url.URL) {
ctx := context.Background()

// Create the URL for the server.
log.Printf("Connecting to MCP server at %s", url.String())

// Create a streamable client transport.
transport := mcp.NewStreamableClientTransport(url.String(), 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