@@ -33,6 +33,13 @@ type sseSession struct {
33
33
// content. This can be used to inject context values from headers, for example.
34
34
type SSEContextFunc func (ctx context.Context , r * http.Request ) context.Context
35
35
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
+
36
43
func (s * sseSession ) SessionID () string {
37
44
return s .sessionID
38
45
}
@@ -68,6 +75,9 @@ type SSEServer struct {
68
75
keepAliveInterval time.Duration
69
76
70
77
mu sync.RWMutex
78
+
79
+ // user-provided function for determining the dynamic base path
80
+ dynamicBasePathFunc DynamicBasePathFunc
71
81
}
72
82
73
83
// SSEOption defines a function type for configuring SSEServer
@@ -96,7 +106,7 @@ func WithBaseURL(baseURL string) SSEOption {
96
106
}
97
107
}
98
108
99
- // WithBasePath adds a new option for setting base path
109
+ // WithBasePath adds a new option for setting a static base path
100
110
func WithBasePath (basePath string ) SSEOption {
101
111
return func (s * SSEServer ) {
102
112
// Ensure the path starts with / and doesn't end with /
@@ -107,6 +117,24 @@ func WithBasePath(basePath string) SSEOption {
107
117
}
108
118
}
109
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
+
110
138
// WithMessageEndpoint sets the message endpoint path
111
139
func WithMessageEndpoint (endpoint string ) SSEOption {
112
140
return func (s * SSEServer ) {
@@ -308,7 +336,8 @@ func (s *SSEServer) handleSSE(w http.ResponseWriter, r *http.Request) {
308
336
}
309
337
310
338
// 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 )
312
341
flusher .Flush ()
313
342
314
343
// Main event loop - this runs in the HTTP handler goroutine
@@ -328,13 +357,20 @@ func (s *SSEServer) handleSSE(w http.ResponseWriter, r *http.Request) {
328
357
}
329
358
330
359
// 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
336
371
}
337
- return fmt .Sprintf ("%s?sessionId=%s" , messageEndpoint , sessionID )
372
+
373
+ return fmt .Sprintf ("%s?sessionId=%s" , endpointPath , sessionID )
338
374
}
339
375
340
376
// handleMessage processes incoming JSON-RPC messages from clients and sends responses
@@ -446,32 +482,108 @@ func (s *SSEServer) GetUrlPath(input string) (string, error) {
446
482
return parse .Path , nil
447
483
}
448
484
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
451
490
}
452
491
453
492
func (s * SSEServer ) CompleteSsePath () string {
454
- path , err := s .GetUrlPath ( s . CompleteSseEndpoint () )
493
+ path , err := s .CompleteSseEndpoint ()
455
494
if err != nil {
456
495
return s .basePath + s .sseEndpoint
457
496
}
458
- return path
497
+ urlPath , err := s .GetUrlPath (path )
498
+ if err != nil {
499
+ return s .basePath + s .sseEndpoint
500
+ }
501
+ return urlPath
459
502
}
460
503
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
463
509
}
464
510
465
511
func (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 )
467
517
if err != nil {
468
518
return s .basePath + s .messageEndpoint
469
519
}
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 )
471
579
}
472
580
473
581
// ServeHTTP implements the http.Handler interface.
474
582
func (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
+ }
475
587
path := r .URL .Path
476
588
// Use exact path matching rather than Contains
477
589
ssePath := s .CompleteSsePath ()
0 commit comments