@@ -34,6 +34,13 @@ type sseSession struct {
34
34
// content. This can be used to inject context values from headers, for example.
35
35
type SSEContextFunc func (ctx context.Context , r * http.Request ) context.Context
36
36
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
+
37
44
func (s * sseSession ) SessionID () string {
38
45
return s .sessionID
39
46
}
@@ -58,19 +65,19 @@ type SSEServer struct {
58
65
server * MCPServer
59
66
baseURL string
60
67
basePath string
68
+ appendQueryToMessageEndpoint bool
61
69
useFullURLForMessageEndpoint bool
62
70
messageEndpoint string
63
71
sseEndpoint string
64
72
sessions sync.Map
65
73
srv * http.Server
66
74
contextFunc SSEContextFunc
75
+ dynamicBasePathFunc DynamicBasePathFunc
67
76
68
77
keepAlive bool
69
78
keepAliveInterval time.Duration
70
79
71
80
mu sync.RWMutex
72
-
73
- appendQueryToMessageEndpoint bool
74
81
}
75
82
76
83
// SSEOption defines a function type for configuring SSEServer
@@ -99,7 +106,7 @@ func WithBaseURL(baseURL string) SSEOption {
99
106
}
100
107
}
101
108
102
- // WithBasePath adds a new option for setting base path
109
+ // WithBasePath adds a new option for setting a static base path
103
110
func WithBasePath (basePath string ) SSEOption {
104
111
return func (s * SSEServer ) {
105
112
// Ensure the path starts with / and doesn't end with /
@@ -110,6 +117,24 @@ func WithBasePath(basePath string) SSEOption {
110
117
}
111
118
}
112
119
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
+
113
138
// WithMessageEndpoint sets the message endpoint path
114
139
func WithMessageEndpoint (endpoint string ) SSEOption {
115
140
return func (s * SSEServer ) {
@@ -208,8 +233,8 @@ func (s *SSEServer) Start(addr string) error {
208
233
209
234
if s .srv == nil {
210
235
s .srv = & http.Server {
211
- Addr : addr ,
212
- Handler : s ,
236
+ Addr : addr ,
237
+ Handler : s ,
213
238
}
214
239
} else {
215
240
if s .srv .Addr == "" {
@@ -218,7 +243,7 @@ func (s *SSEServer) Start(addr string) error {
218
243
return fmt .Errorf ("conflicting listen address: WithHTTPServer(%q) vs Start(%q)" , s .srv .Addr , addr )
219
244
}
220
245
}
221
-
246
+
222
247
return s .srv .ListenAndServe ()
223
248
}
224
249
@@ -331,7 +356,7 @@ func (s *SSEServer) handleSSE(w http.ResponseWriter, r *http.Request) {
331
356
}
332
357
333
358
// Send the initial endpoint event
334
- endpoint := s .GetMessageEndpointForClient (sessionID )
359
+ endpoint := s .GetMessageEndpointForClient (r , sessionID )
335
360
if s .appendQueryToMessageEndpoint && len (r .URL .RawQuery ) > 0 {
336
361
endpoint += "&" + r .URL .RawQuery
337
362
}
@@ -355,13 +380,20 @@ func (s *SSEServer) handleSSE(w http.ResponseWriter, r *http.Request) {
355
380
}
356
381
357
382
// 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 )
363
389
}
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 )
365
397
}
366
398
367
399
// handleMessage processes incoming JSON-RPC messages from clients and sends responses
@@ -479,32 +511,108 @@ func (s *SSEServer) GetUrlPath(input string) (string, error) {
479
511
return parse .Path , nil
480
512
}
481
513
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
484
519
}
485
520
486
521
func (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 )
488
527
if err != nil {
489
528
return s .basePath + s .sseEndpoint
490
529
}
491
- return path
530
+ return urlPath
492
531
}
493
532
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
496
538
}
497
539
498
540
func (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 )
500
546
if err != nil {
501
547
return s .basePath + s .messageEndpoint
502
548
}
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 )
504
608
}
505
609
506
610
// ServeHTTP implements the http.Handler interface.
507
611
func (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
+ }
508
616
path := r .URL .Path
509
617
// Use exact path matching rather than Contains
510
618
ssePath := s .CompleteSsePath ()
0 commit comments