Skip to content

Commit a643f22

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 a643f22

File tree

3 files changed

+394
-19
lines changed

3 files changed

+394
-19
lines changed

examples/dynamic_path/main.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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(fmt.Sprintf("http://localhost%s", addr)),
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+
}
46+

server/sse.go

Lines changed: 111 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,24 @@ 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+
if fn != nil {
127+
s.dynamicBasePathFunc = func(r *http.Request, sid string) string {
128+
bp := fn(r, sid)
129+
if !strings.HasPrefix(bp, "/") {
130+
bp = "/" + bp
131+
}
132+
return strings.TrimSuffix(bp, "/")
133+
}
134+
}
135+
}
136+
}
137+
110138
// WithMessageEndpoint sets the message endpoint path
111139
func WithMessageEndpoint(endpoint string) SSEOption {
112140
return func(s *SSEServer) {
@@ -308,7 +336,8 @@ func (s *SSEServer) handleSSE(w http.ResponseWriter, r *http.Request) {
308336
}
309337

310338
// Send the initial endpoint event
311-
fmt.Fprintf(w, "event: endpoint\ndata: %s\r\n\r\n", s.GetMessageEndpointForClient(sessionID))
339+
endpoint := s.GetMessageEndpointForClient(r, sessionID)
340+
fmt.Fprintf(w, "event: endpoint\ndata: %s\r\n\r\n", endpoint)
312341
flusher.Flush()
313342

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

330359
// 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()
360+
// for the given request. This is the canonical way to compute the message endpoint for a client.
361+
// It handles both dynamic and static path modes, and honors the WithUseFullURLForMessageEndpoint flag.
362+
func (s *SSEServer) GetMessageEndpointForClient(r *http.Request, sessionID string) string {
363+
basePath := s.basePath
364+
if s.dynamicBasePathFunc != nil {
365+
basePath = s.dynamicBasePathFunc(r, sessionID)
336366
}
337-
return fmt.Sprintf("%s?sessionId=%s", messageEndpoint, sessionID)
367+
368+
endpointPath := basePath + s.messageEndpoint
369+
if s.useFullURLForMessageEndpoint && s.baseURL != "" {
370+
endpointPath = s.baseURL + endpointPath
371+
}
372+
373+
return fmt.Sprintf("%s?sessionId=%s", endpointPath, sessionID)
338374
}
339375

340376
// handleMessage processes incoming JSON-RPC messages from clients and sends responses
@@ -447,6 +483,9 @@ func (s *SSEServer) GetUrlPath(input string) (string, error) {
447483
}
448484

449485
func (s *SSEServer) CompleteSseEndpoint() string {
486+
if s.dynamicBasePathFunc != nil {
487+
panic("CompleteSseEndpoint cannot be used with WithDynamicBasePath. Use dynamic path logic in your router.")
488+
}
450489
return s.baseURL + s.basePath + s.sseEndpoint
451490
}
452491

@@ -459,6 +498,9 @@ func (s *SSEServer) CompleteSsePath() string {
459498
}
460499

461500
func (s *SSEServer) CompleteMessageEndpoint() string {
501+
if s.dynamicBasePathFunc != nil {
502+
panic("CompleteMessageEndpoint cannot be used with WithDynamicBasePath. Use dynamic path logic in your router.")
503+
}
462504
return s.baseURL + s.basePath + s.messageEndpoint
463505
}
464506

@@ -470,8 +512,69 @@ func (s *SSEServer) CompleteMessagePath() string {
470512
return path
471513
}
472514

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

0 commit comments

Comments
 (0)