@@ -33,6 +33,13 @@ type sseSession struct {
3333// content. This can be used to inject context values from headers, for example.
3434type 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+
3643func (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
100110func 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
111131func 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\n data: %s\r \n \r \n " , s .GetMessageEndpointForClient (sessionID ))
331+ endpoint := s .GetMessageEndpointForClient (r , sessionID )
332+ fmt .Fprintf (w , "event: endpoint\n data: %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
449477func (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
461492func (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.
474566func (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