Skip to content

Commit 887b314

Browse files
committed
feat(server/sse): Add support for dynamic base paths
This change introduces the ability to mount SSE endpoints at dynamic paths with variable segments (e.g., `/api/{tenant}/sse`) by adding a new `WithDynamicBasePath` option and related functionality. This enables advanced use cases such as multi-tenant architectures or integration with routers that support path parameters. Key Features: * DynamicBasePathFunc: New function type and option (WithDynamicBasePath) to generate the SSE server's base path dynamically per request/session. * Flexible Routing: New SSEHandler() and MessageHandler() methods allow mounting handlers at arbitrary or dynamic paths using any router (e.g., net/http, chi, gorilla/mux). * Endpoint Generation: GetMessageEndpointForClient now supports both static and dynamic path modes, and correctly generates full URLs when configured. * Example: Added examples/dynamic_path/main.go demonstrating dynamic path mounting and usage. ```go mcpServer := mcp.NewMCPServer("dynamic-path-example", "1.0.0") sseServer := mcp.NewSSEServer( mcpServer, mcp.WithDynamicBasePath(func(r *http.Request, sessionID string) string { tenant := r.PathValue("tenant") return "/api/" + tenant }), mcp.WithBaseURL("http://localhost:8080"), ) mux := http.NewServeMux() mux.Handle("/api/{tenant}/sse", sseServer.SSEHandler()) mux.Handle("/api/{tenant}/message", sseServer.MessageHandler()) ```
1 parent 33c98f1 commit 887b314

File tree

3 files changed

+349
-8
lines changed

3 files changed

+349
-8
lines changed

examples/dynamic_path/main.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"flag"
6+
"fmt"
7+
"log"
8+
"net/http"
9+
10+
"github.com/mark3labs/mcp-go/mcp"
11+
"github.com/mark3labs/mcp-go/server"
12+
)
13+
14+
func main() {
15+
var addr string
16+
flag.StringVar(&addr, "addr", ":8080", "address to listen on")
17+
flag.Parse()
18+
19+
mcpServer := server.NewMCPServer("dynamic-path-example", "1.0.0")
20+
21+
// Add a trivial tool for demonstration
22+
mcpServer.AddTool(mcp.NewTool("echo"), func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
23+
return mcp.NewToolResultText(fmt.Sprintf("Echo: %v", req.Params.Arguments["message"])), nil
24+
})
25+
26+
// Use a dynamic base path based on a path parameter (Go 1.22+)
27+
sseServer := server.NewSSEServer(
28+
mcpServer,
29+
server.WithDynamicBasePath(func(r *http.Request, sessionID string) string {
30+
tenant := r.PathValue("tenant")
31+
return "/api/" + tenant
32+
}),
33+
server.WithBaseURL("http://localhost:8080"),
34+
server.WithUseFullURLForMessageEndpoint(true),
35+
)
36+
37+
mux := http.NewServeMux()
38+
mux.Handle("/api/{tenant}/sse", sseServer.SSEHandler())
39+
mux.Handle("/api/{tenant}/message", sseServer.MessageHandler())
40+
41+
log.Printf("Dynamic SSE server listening on %s", addr)
42+
if err := http.ListenAndServe(addr, mux); err != nil {
43+
log.Fatalf("Server error: %v", err)
44+
}
45+
}

server/sse.go

Lines changed: 103 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ type sseSession struct {
3333
// content. This can be used to inject context values from headers, for example.
3434
type SSEContextFunc func(ctx context.Context, r *http.Request) context.Context
3535

36+
// DynamicBasePathFunc allows the user to provide a function to generate the
37+
// base path for a given request and sessionID. This is useful for cases where
38+
// the base path is not known at the time of SSE server creation, such as when
39+
// using a reverse proxy or when the base path is dynamically generated. The
40+
// function should return the base path (e.g., "/mcp/tenant123").
41+
type DynamicBasePathFunc func(r *http.Request, sessionID string) string
42+
3643
func (s *sseSession) SessionID() string {
3744
return s.sessionID
3845
}
@@ -68,6 +75,9 @@ type SSEServer struct {
6875
keepAliveInterval time.Duration
6976

7077
mu sync.RWMutex
78+
79+
// user-provided function for determining the dynamic base path
80+
dynamicBasePathFunc DynamicBasePathFunc
7181
}
7282

7383
// SSEOption defines a function type for configuring SSEServer
@@ -96,7 +106,7 @@ func WithBaseURL(baseURL string) SSEOption {
96106
}
97107
}
98108

99-
// Add a new option for setting base path
109+
// Add a new option for setting a static base path
100110
func WithBasePath(basePath string) SSEOption {
101111
return func(s *SSEServer) {
102112
// Ensure the path starts with / and doesn't end with /
@@ -107,6 +117,16 @@ func WithBasePath(basePath string) SSEOption {
107117
}
108118
}
109119

120+
// WithDynamicBasePath accepts a function for generating the base path. This is
121+
// useful for cases where the base path is not known at the time of SSE server
122+
// creation, such as when using a reverse proxy or when the server is mounted
123+
// at a dynamic path.
124+
func WithDynamicBasePath(fn DynamicBasePathFunc) SSEOption {
125+
return func(s *SSEServer) {
126+
s.dynamicBasePathFunc = fn
127+
}
128+
}
129+
110130
// WithMessageEndpoint sets the message endpoint path
111131
func WithMessageEndpoint(endpoint string) SSEOption {
112132
return func(s *SSEServer) {
@@ -308,7 +328,8 @@ func (s *SSEServer) handleSSE(w http.ResponseWriter, r *http.Request) {
308328
}
309329

310330
// Send the initial endpoint event
311-
fmt.Fprintf(w, "event: endpoint\ndata: %s\r\n\r\n", s.GetMessageEndpointForClient(sessionID))
331+
endpoint := s.GetMessageEndpointForClient(r, sessionID)
332+
fmt.Fprintf(w, "event: endpoint\ndata: %s\r\n\r\n", endpoint)
312333
flusher.Flush()
313334

314335
// Main event loop - this runs in the HTTP handler goroutine
@@ -328,13 +349,20 @@ func (s *SSEServer) handleSSE(w http.ResponseWriter, r *http.Request) {
328349
}
329350

330351
// GetMessageEndpointForClient returns the appropriate message endpoint URL with session ID
331-
// based on the useFullURLForMessageEndpoint configuration.
332-
func (s *SSEServer) GetMessageEndpointForClient(sessionID string) string {
333-
messageEndpoint := s.messageEndpoint
334-
if s.useFullURLForMessageEndpoint {
335-
messageEndpoint = s.CompleteMessageEndpoint()
352+
// for the given request. This is the canonical way to compute the message endpoint for a client.
353+
// It handles both dynamic and static path modes, and honors the WithUseFullURLForMessageEndpoint flag.
354+
func (s *SSEServer) GetMessageEndpointForClient(r *http.Request, sessionID string) string {
355+
basePath := s.basePath
356+
if s.dynamicBasePathFunc != nil {
357+
basePath = s.dynamicBasePathFunc(r, sessionID)
358+
}
359+
360+
endpointPath := basePath + s.messageEndpoint
361+
if s.useFullURLForMessageEndpoint && s.baseURL != "" {
362+
endpointPath = s.baseURL + endpointPath
336363
}
337-
return fmt.Sprintf("%s?sessionId=%s", messageEndpoint, sessionID)
364+
365+
return fmt.Sprintf("%s?sessionId=%s", endpointPath, sessionID)
338366
}
339367

340368
// handleMessage processes incoming JSON-RPC messages from clients and sends responses
@@ -447,6 +475,9 @@ func (s *SSEServer) GetUrlPath(input string) (string, error) {
447475
}
448476

449477
func (s *SSEServer) CompleteSseEndpoint() string {
478+
if s.dynamicBasePathFunc != nil {
479+
panic("CompleteSseEndpoint cannot be used with WithDynamicBasePath. Use dynamic path logic in your router.")
480+
}
450481
return s.baseURL + s.basePath + s.sseEndpoint
451482
}
452483

@@ -459,6 +490,9 @@ func (s *SSEServer) CompleteSsePath() string {
459490
}
460491

461492
func (s *SSEServer) CompleteMessageEndpoint() string {
493+
if s.dynamicBasePathFunc != nil {
494+
panic("CompleteMessageEndpoint cannot be used with WithDynamicBasePath. Use dynamic path logic in your router.")
495+
}
462496
return s.baseURL + s.basePath + s.messageEndpoint
463497
}
464498

@@ -470,8 +504,69 @@ func (s *SSEServer) CompleteMessagePath() string {
470504
return path
471505
}
472506

507+
// SSEHandler returns an http.Handler for the SSE endpoint.
508+
//
509+
// This method allows you to mount the SSE handler at any arbitrary path
510+
// using your own router (e.g. net/http, gorilla/mux, chi, etc.). It is
511+
// intended for advanced scenarios where you want to control the routing or
512+
// support dynamic segments.
513+
//
514+
// IMPORTANT: When using this handler in advanced/dynamic mounting scenarios,
515+
// you must use the WithDynamicBasePath option to ensure the correct base path
516+
// is communicated to clients.
517+
//
518+
// Example usage:
519+
//
520+
// // Advanced/dynamic:
521+
// sseServer := NewSSEServer(mcpServer,
522+
// WithDynamicBasePath(func(r *http.Request, sessionID string) string {
523+
// tenant := r.PathValue("tenant")
524+
// return "/mcp/" + tenant
525+
// }),
526+
// WithBaseURL("http://localhost:8080")
527+
// )
528+
// mux.Handle("/mcp/{tenant}/sse", sseServer.SSEHandler())
529+
// mux.Handle("/mcp/{tenant}/message", sseServer.MessageHandler())
530+
//
531+
// For non-dynamic cases, use ServeHTTP method instead.
532+
func (s *SSEServer) SSEHandler() http.Handler {
533+
return http.HandlerFunc(s.handleSSE)
534+
}
535+
536+
// MessageHandler returns an http.Handler for the message endpoint.
537+
//
538+
// This method allows you to mount the message handler at any arbitrary path
539+
// using your own router (e.g. net/http, gorilla/mux, chi, etc.). It is
540+
// intended for advanced scenarios where you want to control the routing or
541+
// support dynamic segments.
542+
//
543+
// IMPORTANT: When using this handler in advanced/dynamic mounting scenarios,
544+
// you must use the WithDynamicBasePath option to ensure the correct base path
545+
// is communicated to clients.
546+
//
547+
// Example usage:
548+
//
549+
// // Advanced/dynamic:
550+
// sseServer := NewSSEServer(mcpServer,
551+
// WithDynamicBasePath(func(r *http.Request, sessionID string) string {
552+
// tenant := r.PathValue("tenant")
553+
// return "/mcp/" + tenant
554+
// }),
555+
// WithBaseURL("http://localhost:8080")
556+
// )
557+
// mux.Handle("/mcp/{tenant}/sse", sseServer.SSEHandler())
558+
// mux.Handle("/mcp/{tenant}/message", sseServer.MessageHandler())
559+
//
560+
// For non-dynamic cases, use ServeHTTP method instead.
561+
func (s *SSEServer) MessageHandler() http.Handler {
562+
return http.HandlerFunc(s.handleMessage)
563+
}
564+
473565
// ServeHTTP implements the http.Handler interface.
474566
func (s *SSEServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
567+
if s.dynamicBasePathFunc != nil {
568+
panic("ServeHTTP cannot be used with WithDynamicBasePath. Use SSEHandler/MessageHandler and mount them with your router.")
569+
}
475570
path := r.URL.Path
476571
// Use exact path matching rather than Contains
477572
ssePath := s.CompleteSsePath()

0 commit comments

Comments
 (0)