@@ -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,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
111139func 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\n data: %s\r \n \r \n " , s .GetMessageEndpointForClient (sessionID ))
339+ endpoint := s .GetMessageEndpointForClient (r , sessionID )
340+ fmt .Fprintf (w , "event: endpoint\n data: %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 )
366+ }
367+
368+ endpointPath := basePath + s .messageEndpoint
369+ if s .useFullURLForMessageEndpoint && s .baseURL != "" {
370+ endpointPath = s .baseURL + endpointPath
336371 }
337- return fmt .Sprintf ("%s?sessionId=%s" , messageEndpoint , sessionID )
372+
373+ return fmt .Sprintf ("%s?sessionId=%s" , endpointPath , sessionID )
338374}
339375
340376// handleMessage processes incoming JSON-RPC messages from clients and sends responses
@@ -446,32 +482,108 @@ func (s *SSEServer) GetUrlPath(input string) (string, error) {
446482 return parse .Path , nil
447483}
448484
449- func (s * SSEServer ) CompleteSseEndpoint () string {
450- return s .baseURL + s .basePath + s .sseEndpoint
485+ func (s * SSEServer ) CompleteSseEndpoint () (string , error ) {
486+ if s .dynamicBasePathFunc != nil {
487+ return "" , & ErrDynamicPathConfig {Method : "CompleteSseEndpoint" }
488+ }
489+ return s .baseURL + s .basePath + s .sseEndpoint , nil
451490}
452491
453492func (s * SSEServer ) CompleteSsePath () string {
454- path , err := s .GetUrlPath ( s . CompleteSseEndpoint () )
493+ path , err := s .CompleteSseEndpoint ()
455494 if err != nil {
456495 return s .basePath + s .sseEndpoint
457496 }
458- return path
497+ urlPath , err := s .GetUrlPath (path )
498+ if err != nil {
499+ return s .basePath + s .sseEndpoint
500+ }
501+ return urlPath
459502}
460503
461- func (s * SSEServer ) CompleteMessageEndpoint () string {
462- return s .baseURL + s .basePath + s .messageEndpoint
504+ func (s * SSEServer ) CompleteMessageEndpoint () (string , error ) {
505+ if s .dynamicBasePathFunc != nil {
506+ return "" , & ErrDynamicPathConfig {Method : "CompleteMessageEndpoint" }
507+ }
508+ return s .baseURL + s .basePath + s .messageEndpoint , nil
463509}
464510
465511func (s * SSEServer ) CompleteMessagePath () string {
466- path , err := s .GetUrlPath (s .CompleteMessageEndpoint ())
512+ path , err := s .CompleteMessageEndpoint ()
513+ if err != nil {
514+ return s .basePath + s .messageEndpoint
515+ }
516+ urlPath , err := s .GetUrlPath (path )
467517 if err != nil {
468518 return s .basePath + s .messageEndpoint
469519 }
470- return path
520+ return urlPath
521+ }
522+
523+ // SSEHandler returns an http.Handler for the SSE endpoint.
524+ //
525+ // This method allows you to mount the SSE handler at any arbitrary path
526+ // using your own router (e.g. net/http, gorilla/mux, chi, etc.). It is
527+ // intended for advanced scenarios where you want to control the routing or
528+ // support dynamic segments.
529+ //
530+ // IMPORTANT: When using this handler in advanced/dynamic mounting scenarios,
531+ // you must use the WithDynamicBasePath option to ensure the correct base path
532+ // is communicated to clients.
533+ //
534+ // Example usage:
535+ //
536+ // // Advanced/dynamic:
537+ // sseServer := NewSSEServer(mcpServer,
538+ // WithDynamicBasePath(func(r *http.Request, sessionID string) string {
539+ // tenant := r.PathValue("tenant")
540+ // return "/mcp/" + tenant
541+ // }),
542+ // WithBaseURL("http://localhost:8080")
543+ // )
544+ // mux.Handle("/mcp/{tenant}/sse", sseServer.SSEHandler())
545+ // mux.Handle("/mcp/{tenant}/message", sseServer.MessageHandler())
546+ //
547+ // For non-dynamic cases, use ServeHTTP method instead.
548+ func (s * SSEServer ) SSEHandler () http.Handler {
549+ return http .HandlerFunc (s .handleSSE )
550+ }
551+
552+ // MessageHandler returns an http.Handler for the message endpoint.
553+ //
554+ // This method allows you to mount the message handler at any arbitrary path
555+ // using your own router (e.g. net/http, gorilla/mux, chi, etc.). It is
556+ // intended for advanced scenarios where you want to control the routing or
557+ // support dynamic segments.
558+ //
559+ // IMPORTANT: When using this handler in advanced/dynamic mounting scenarios,
560+ // you must use the WithDynamicBasePath option to ensure the correct base path
561+ // is communicated to clients.
562+ //
563+ // Example usage:
564+ //
565+ // // Advanced/dynamic:
566+ // sseServer := NewSSEServer(mcpServer,
567+ // WithDynamicBasePath(func(r *http.Request, sessionID string) string {
568+ // tenant := r.PathValue("tenant")
569+ // return "/mcp/" + tenant
570+ // }),
571+ // WithBaseURL("http://localhost:8080")
572+ // )
573+ // mux.Handle("/mcp/{tenant}/sse", sseServer.SSEHandler())
574+ // mux.Handle("/mcp/{tenant}/message", sseServer.MessageHandler())
575+ //
576+ // For non-dynamic cases, use ServeHTTP method instead.
577+ func (s * SSEServer ) MessageHandler () http.Handler {
578+ return http .HandlerFunc (s .handleMessage )
471579}
472580
473581// ServeHTTP implements the http.Handler interface.
474582func (s * SSEServer ) ServeHTTP (w http.ResponseWriter , r * http.Request ) {
583+ if s .dynamicBasePathFunc != nil {
584+ http .Error (w , (& ErrDynamicPathConfig {Method : "ServeHTTP" }).Error (), http .StatusInternalServerError )
585+ return
586+ }
475587 path := r .URL .Path
476588 // Use exact path matching rather than Contains
477589 ssePath := s .CompleteSsePath ()
0 commit comments