go-sdk/examples/server/distributed/main.go
2026-06-23 11:11:08 +02:00

168 lines
4.8 KiB
Go

// 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.
// The distributed command is an example of a distributed MCP server.
//
// It forks multiple child processes (according to the -child_ports flag), each
// of which is a streamable HTTP MCP server with the 'inc' tool, and proxies
// incoming http requests to them.
//
// Distributed MCP servers must be stateless, because there's no guarantee that
// subsequent requests for a session land on the same backend. However, they
// may still have logical session IDs, as can be seen with verbose logging
// (-v).
//
// Example:
//
// ./distributed -http=localhost:8080 -child_ports=8081,8082
//
//lint:file-ignore SA1019 example server exercises deprecated SEP-2577 logging API for demonstration.
package main
import (
"context"
"flag"
"fmt"
"log"
"net/http"
"net/http/httputil"
"net/url"
"os"
"os/exec"
"os/signal"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/modelcontextprotocol/go-sdk/mcp"
)
const childPortVar = "MCP_CHILD_PORT"
var (
httpAddr = flag.String("http", "", "if set, use streamable HTTP at this address, instead of stdin/stdout")
childPorts = flag.String("child_ports", "", "comma-separated child ports to distribute to")
verbose = flag.Bool("v", false, "if set, enable verbose logging")
)
func main() {
// This command runs as either a parent or a child, depending on whether
// childPortVar is set (a.k.a. the fork-and-exec trick).
//
// Each child is a streamable HTTP server, and the parent is a reverse proxy.
flag.Parse()
if v := os.Getenv(childPortVar); v != "" {
child(v)
} else {
parent()
}
}
func parent() {
exe, err := os.Executable()
if err != nil {
log.Fatal(err)
}
if *httpAddr == "" {
log.Fatal("must provide -http")
}
if *childPorts == "" {
log.Fatal("must provide -child_ports")
}
// Ensure that children are cleaned up on CTRL-C
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
// Start the child processes.
ports := strings.Split(*childPorts, ",")
var wg sync.WaitGroup
childURLs := make([]*url.URL, len(ports))
for i, port := range ports {
childURL := fmt.Sprintf("http://localhost:%s", port)
childURLs[i], err = url.Parse(childURL)
if err != nil {
log.Fatal(err)
}
cmd := exec.CommandContext(ctx, exe, os.Args[1:]...)
cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%s", childPortVar, port))
cmd.Stderr = os.Stderr
wg.Go(func() {
log.Printf("starting child %d at %s", i, childURL)
if err := cmd.Run(); err != nil && ctx.Err() == nil {
log.Printf("child %d failed: %v", i, err)
} else {
log.Printf("child %d exited gracefully", i)
}
})
}
// Start a reverse proxy that round-robin's requests to each backend.
var nextBackend atomic.Int64
server := http.Server{
Addr: *httpAddr,
Handler: &httputil.ReverseProxy{
Rewrite: func(r *httputil.ProxyRequest) {
child := int(nextBackend.Add(1)) % len(childURLs)
if *verbose {
log.Printf("dispatching to localhost:%s", ports[child])
}
r.SetURL(childURLs[child])
},
},
}
wg.Go(func() {
if err := server.ListenAndServe(); err != nil && ctx.Err() == nil {
log.Printf("Server failed: %v", err)
}
})
log.Printf("Serving at %s (CTRL-C to cancel)", *httpAddr)
<-ctx.Done()
stop() // restore the interrupt signal
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Attempt the graceful shutdown.
if err := server.Shutdown(shutdownCtx); err != nil {
log.Fatalf("Server shutdown failed: %v", err)
}
// Wait for the subprocesses and http server to stop.
wg.Wait()
log.Println("Server shutdown gracefully.")
}
func child(port string) {
// Create a server with a single tool that increments a counter and sends a notification.
server := mcp.NewServer(&mcp.Implementation{Name: "counter"}, nil)
var count atomic.Int64
inc := func(ctx context.Context, req *mcp.CallToolRequest, args struct{}) (*mcp.CallToolResult, struct{ Count int64 }, error) {
n := count.Add(1)
if *verbose {
log.Printf("request %d (session %s)", n, req.Session.ID())
}
// Send a notification in the context of the request
// Hint: in stateless mode, at least log level 'info' is required to send notifications
req.Session.Log(ctx, &mcp.LoggingMessageParams{Data: fmt.Sprintf("request %d (session %s)", n, req.Session.ID()), Level: "info"})
return nil, struct{ Count int64 }{n}, nil
}
mcp.AddTool(server, &mcp.Tool{Name: "inc"}, inc)
handler := mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server {
return server
}, &mcp.StreamableHTTPOptions{
Stateless: true,
})
log.Printf("child listening on localhost:%s", port)
log.Fatal(http.ListenAndServe(fmt.Sprintf("localhost:%s", port), handler))
}