@@ -34,6 +34,13 @@ type sseSession struct {
3434// content. This can be used to inject context values from headers, for example.
3535type SSEContextFunc func (ctx context.Context , r * http.Request ) context.Context
3636
37+ // DynamicBasePathFunc allows the user to provide a function to generate the
38+ // base path for a given request and sessionID. This is useful for cases where
39+ // the base path is not known at the time of SSE server creation, such as when
40+ // using a reverse proxy or when the base path is dynamically generated. The
41+ // function should return the base path (e.g., "/mcp/tenant123").
42+ type DynamicBasePathFunc func (r * http.Request , sessionID string ) string
43+
3744func (s * sseSession ) SessionID () string {
3845 return s .sessionID
3946}
@@ -58,19 +65,19 @@ type SSEServer struct {
5865 server * MCPServer
5966 baseURL string
6067 basePath string
68+ appendQueryToMessageEndpoint bool
6169 useFullURLForMessageEndpoint bool
6270 messageEndpoint string
6371 sseEndpoint string
6472 sessions sync.Map
6573 srv * http.Server
6674 contextFunc SSEContextFunc
75+ dynamicBasePathFunc DynamicBasePathFunc
6776
6877 keepAlive bool
6978 keepAliveInterval time.Duration
7079
7180 mu sync.RWMutex
72-
73- appendQueryToMessageEndpoint bool
7481}
7582
7683// SSEOption defines a function type for configuring SSEServer
@@ -99,7 +106,7 @@ func WithBaseURL(baseURL string) SSEOption {
99106 }
100107}
101108
102- // WithBasePath adds a new option for setting base path
109+ // WithBasePath adds a new option for setting a static base path
103110func WithBasePath (basePath string ) SSEOption {
104111 return func (s * SSEServer ) {
105112 // Ensure the path starts with / and doesn't end with /
@@ -110,6 +117,24 @@ func WithBasePath(basePath string) SSEOption {
110117 }
111118}
112119
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+
113138// WithMessageEndpoint sets the message endpoint path
114139func WithMessageEndpoint (endpoint string ) SSEOption {
115140 return func (s * SSEServer ) {
@@ -208,8 +233,8 @@ func (s *SSEServer) Start(addr string) error {
208233
209234 if s .srv == nil {
210235 s .srv = & http.Server {
211- Addr : addr ,
212- Handler : s ,
236+ Addr : addr ,
237+ Handler : s ,
213238 }
214239 } else {
215240 if s .srv .Addr == "" {
@@ -218,7 +243,7 @@ func (s *SSEServer) Start(addr string) error {
218243 return fmt .Errorf ("conflicting listen address: WithHTTPServer(%q) vs Start(%q)" , s .srv .Addr , addr )
219244 }
220245 }
221-
246+
222247 return s .srv .ListenAndServe ()
223248}
224249
@@ -331,7 +356,7 @@ func (s *SSEServer) handleSSE(w http.ResponseWriter, r *http.Request) {
331356 }
332357
333358 // Send the initial endpoint event
334- endpoint := s .GetMessageEndpointForClient (sessionID )
359+ endpoint := s .GetMessageEndpointForClient (r , sessionID )
335360 if s .appendQueryToMessageEndpoint && len (r .URL .RawQuery ) > 0 {
336361 endpoint += "&" + r .URL .RawQuery
337362 }
@@ -355,13 +380,20 @@ func (s *SSEServer) handleSSE(w http.ResponseWriter, r *http.Request) {
355380}
356381
357382// GetMessageEndpointForClient returns the appropriate message endpoint URL with session ID
358- // based on the useFullURLForMessageEndpoint configuration.
359- func (s * SSEServer ) GetMessageEndpointForClient (sessionID string ) string {
360- messageEndpoint := s .messageEndpoint
361- if s .useFullURLForMessageEndpoint {
362- messageEndpoint = s .CompleteMessageEndpoint ()
383+ // for the given request. This is the canonical way to compute the message endpoint for a client.
384+ // It handles both dynamic and static path modes, and honors the WithUseFullURLForMessageEndpoint flag.
385+ func (s * SSEServer ) GetMessageEndpointForClient (r * http.Request , sessionID string ) string {
386+ basePath := s .basePath
387+ if s .dynamicBasePathFunc != nil {
388+ basePath = s .dynamicBasePathFunc (r , sessionID )
363389 }
364- return fmt .Sprintf ("%s?sessionId=%s" , messageEndpoint , sessionID )
390+
391+ endpointPath := basePath + s .messageEndpoint
392+ if s .useFullURLForMessageEndpoint && s .baseURL != "" {
393+ endpointPath = s .baseURL + endpointPath
394+ }
395+
396+ return fmt .Sprintf ("%s?sessionId=%s" , endpointPath , sessionID )
365397}
366398
367399// handleMessage processes incoming JSON-RPC messages from clients and sends responses
@@ -479,32 +511,108 @@ func (s *SSEServer) GetUrlPath(input string) (string, error) {
479511 return parse .Path , nil
480512}
481513
482- func (s * SSEServer ) CompleteSseEndpoint () string {
483- return s .baseURL + s .basePath + s .sseEndpoint
514+ func (s * SSEServer ) CompleteSseEndpoint () (string , error ) {
515+ if s .dynamicBasePathFunc != nil {
516+ return "" , & ErrDynamicPathConfig {Method : "CompleteSseEndpoint" }
517+ }
518+ return s .baseURL + s .basePath + s .sseEndpoint , nil
484519}
485520
486521func (s * SSEServer ) CompleteSsePath () string {
487- path , err := s .GetUrlPath (s .CompleteSseEndpoint ())
522+ path , err := s .CompleteSseEndpoint ()
523+ if err != nil {
524+ return s .basePath + s .sseEndpoint
525+ }
526+ urlPath , err := s .GetUrlPath (path )
488527 if err != nil {
489528 return s .basePath + s .sseEndpoint
490529 }
491- return path
530+ return urlPath
492531}
493532
494- func (s * SSEServer ) CompleteMessageEndpoint () string {
495- return s .baseURL + s .basePath + s .messageEndpoint
533+ func (s * SSEServer ) CompleteMessageEndpoint () (string , error ) {
534+ if s .dynamicBasePathFunc != nil {
535+ return "" , & ErrDynamicPathConfig {Method : "CompleteMessageEndpoint" }
536+ }
537+ return s .baseURL + s .basePath + s .messageEndpoint , nil
496538}
497539
498540func (s * SSEServer ) CompleteMessagePath () string {
499- path , err := s .GetUrlPath (s .CompleteMessageEndpoint ())
541+ path , err := s .CompleteMessageEndpoint ()
542+ if err != nil {
543+ return s .basePath + s .messageEndpoint
544+ }
545+ urlPath , err := s .GetUrlPath (path )
500546 if err != nil {
501547 return s .basePath + s .messageEndpoint
502548 }
503- return path
549+ return urlPath
550+ }
551+
552+ // SSEHandler returns an http.Handler for the SSE endpoint.
553+ //
554+ // This method allows you to mount the SSE 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 ) SSEHandler () http.Handler {
578+ return http .HandlerFunc (s .handleSSE )
579+ }
580+
581+ // MessageHandler returns an http.Handler for the message endpoint.
582+ //
583+ // This method allows you to mount the message handler at any arbitrary path
584+ // using your own router (e.g. net/http, gorilla/mux, chi, etc.). It is
585+ // intended for advanced scenarios where you want to control the routing or
586+ // support dynamic segments.
587+ //
588+ // IMPORTANT: When using this handler in advanced/dynamic mounting scenarios,
589+ // you must use the WithDynamicBasePath option to ensure the correct base path
590+ // is communicated to clients.
591+ //
592+ // Example usage:
593+ //
594+ // // Advanced/dynamic:
595+ // sseServer := NewSSEServer(mcpServer,
596+ // WithDynamicBasePath(func(r *http.Request, sessionID string) string {
597+ // tenant := r.PathValue("tenant")
598+ // return "/mcp/" + tenant
599+ // }),
600+ // WithBaseURL("http://localhost:8080")
601+ // )
602+ // mux.Handle("/mcp/{tenant}/sse", sseServer.SSEHandler())
603+ // mux.Handle("/mcp/{tenant}/message", sseServer.MessageHandler())
604+ //
605+ // For non-dynamic cases, use ServeHTTP method instead.
606+ func (s * SSEServer ) MessageHandler () http.Handler {
607+ return http .HandlerFunc (s .handleMessage )
504608}
505609
506610// ServeHTTP implements the http.Handler interface.
507611func (s * SSEServer ) ServeHTTP (w http.ResponseWriter , r * http.Request ) {
612+ if s .dynamicBasePathFunc != nil {
613+ http .Error (w , (& ErrDynamicPathConfig {Method : "ServeHTTP" }).Error (), http .StatusInternalServerError )
614+ return
615+ }
508616 path := r .URL .Path
509617 // Use exact path matching rather than Contains
510618 ssePath := s .CompleteSsePath ()
0 commit comments