Skip to content

Commit 29f87db

Browse files
committed
refactor(server): extract shared HTTP transport configuration options
Create a common interface and pattern for HTTP transport configuration to enable code sharing between SSEServer and the upcoming StreamableHTTPServer. - Add new httpTransportConfigurable interface for shared configuration - Refactor SSEServer to implement the shared interface - Convert With* option functions to work with both server types - Add stub for StreamableHTTPServer to demonstrate implementation pattern - Deprecate WithSSEContextFunc in favor of WithHTTPContextFunc This change preserves backward compatibility while allowing the reuse of configuration code across different HTTP server implementations.
1 parent 9d6b793 commit 29f87db

File tree

2 files changed

+237
-83
lines changed

2 files changed

+237
-83
lines changed

server/http_transport_options.go

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
package server
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/url"
7+
"strings"
8+
"time"
9+
)
10+
11+
// HTTPContextFunc is a function that takes an existing context and the current
12+
// request and returns a potentially modified context based on the request
13+
// content. This can be used to inject context values from headers, for example.
14+
type HTTPContextFunc func(ctx context.Context, r *http.Request) context.Context
15+
16+
// httpTransportConfigurable is an internal interface for shared HTTP transport configuration.
17+
type httpTransportConfigurable interface {
18+
setBasePath(string)
19+
setDynamicBasePath(DynamicBasePathFunc)
20+
setKeepAliveInterval(time.Duration)
21+
setKeepAlive(bool)
22+
setContextFunc(HTTPContextFunc)
23+
setHTTPServer(*http.Server)
24+
setBaseURL(string)
25+
}
26+
27+
// HTTPTransportOption is a function that configures an httpTransportConfigurable.
28+
type HTTPTransportOption func(httpTransportConfigurable)
29+
30+
// Option interfaces and wrappers for server configuration
31+
// Base option interface
32+
type HTTPServerOption interface {
33+
isHTTPServerOption()
34+
}
35+
36+
// SSE-specific option interface
37+
type SSEOption interface {
38+
HTTPServerOption
39+
applyToSSE(*SSEServer)
40+
}
41+
42+
// StreamableHTTP-specific option interface
43+
type StreamableHTTPOption interface {
44+
HTTPServerOption
45+
applyToStreamableHTTP(*StreamableHTTPServer)
46+
}
47+
48+
// Common options that work with both server types
49+
type CommonHTTPServerOption interface {
50+
SSEOption
51+
StreamableHTTPOption
52+
}
53+
54+
// Wrapper for SSE-specific functional options
55+
type sseOption func(*SSEServer)
56+
57+
func (o sseOption) isHTTPServerOption() {}
58+
func (o sseOption) applyToSSE(s *SSEServer) { o(s) }
59+
60+
// Wrapper for StreamableHTTP-specific functional options
61+
type streamableHTTPOption func(*StreamableHTTPServer)
62+
63+
func (o streamableHTTPOption) isHTTPServerOption() {}
64+
func (o streamableHTTPOption) applyToStreamableHTTP(s *StreamableHTTPServer) { o(s) }
65+
66+
// Refactor commonOption to use a single apply func(httpTransportConfigurable)
67+
type commonOption struct {
68+
apply func(httpTransportConfigurable)
69+
}
70+
71+
func (o commonOption) isHTTPServerOption() {}
72+
func (o commonOption) applyToSSE(s *SSEServer) { o.apply(s) }
73+
func (o commonOption) applyToStreamableHTTP(s *StreamableHTTPServer) { o.apply(s) }
74+
75+
// TODO: This is a stub implementation of StreamableHTTPServer just to show how
76+
// to use it with the new options interfaces.
77+
type StreamableHTTPServer struct{}
78+
79+
// Add stub methods to satisfy httpTransportConfigurable
80+
81+
func (s *StreamableHTTPServer) setBasePath(string) {}
82+
func (s *StreamableHTTPServer) setDynamicBasePath(DynamicBasePathFunc) {}
83+
func (s *StreamableHTTPServer) setKeepAliveInterval(time.Duration) {}
84+
func (s *StreamableHTTPServer) setKeepAlive(bool) {}
85+
func (s *StreamableHTTPServer) setContextFunc(HTTPContextFunc) {}
86+
func (s *StreamableHTTPServer) setHTTPServer(srv *http.Server) {}
87+
func (s *StreamableHTTPServer) setBaseURL(baseURL string) {}
88+
89+
// Ensure the option types implement the correct interfaces
90+
var (
91+
_ httpTransportConfigurable = (*StreamableHTTPServer)(nil)
92+
_ SSEOption = sseOption(nil)
93+
_ StreamableHTTPOption = streamableHTTPOption(nil)
94+
_ CommonHTTPServerOption = commonOption{}
95+
)
96+
97+
// WithStaticBasePath adds a new option for setting a static base path.
98+
// This is useful for mounting the server at a known, fixed path.
99+
func WithStaticBasePath(basePath string) CommonHTTPServerOption {
100+
return commonOption{
101+
apply: func(c httpTransportConfigurable) {
102+
c.setBasePath(basePath)
103+
},
104+
}
105+
}
106+
107+
// DynamicBasePathFunc allows the user to provide a function to generate the
108+
// base path for a given request and sessionID. This is useful for cases where
109+
// the base path is not known at the time of SSE server creation, such as when
110+
// using a reverse proxy or when the base path is dynamically generated. The
111+
// function should return the base path (e.g., "/mcp/tenant123").
112+
type DynamicBasePathFunc func(r *http.Request, sessionID string) string
113+
114+
// WithDynamicBasePath accepts a function for generating the base path.
115+
// This is useful for cases where the base path is not known at the time of server creation,
116+
// such as when using a reverse proxy or when the server is mounted at a dynamic path.
117+
func WithDynamicBasePath(fn DynamicBasePathFunc) CommonHTTPServerOption {
118+
return commonOption{
119+
apply: func(c httpTransportConfigurable) {
120+
c.setDynamicBasePath(fn)
121+
},
122+
}
123+
}
124+
125+
// WithKeepAliveInterval sets the keep-alive interval for the transport.
126+
// When enabled, the server will periodically send ping events to keep the connection alive.
127+
func WithKeepAliveInterval(interval time.Duration) CommonHTTPServerOption {
128+
return commonOption{
129+
apply: func(c httpTransportConfigurable) {
130+
c.setKeepAliveInterval(interval)
131+
},
132+
}
133+
}
134+
135+
// WithKeepAlive enables or disables keep-alive for the transport.
136+
// When enabled, the server will send periodic keep-alive events to clients.
137+
func WithKeepAlive(keepAlive bool) CommonHTTPServerOption {
138+
return commonOption{
139+
apply: func(c httpTransportConfigurable) {
140+
c.setKeepAlive(keepAlive)
141+
},
142+
}
143+
}
144+
145+
// WithHTTPContextFunc sets a function that will be called to customize the context
146+
// for the server using the incoming request. This is useful for injecting
147+
// context values from headers or other request properties.
148+
func WithHTTPContextFunc(fn HTTPContextFunc) CommonHTTPServerOption {
149+
return commonOption{
150+
apply: func(c httpTransportConfigurable) {
151+
c.setContextFunc(fn)
152+
},
153+
}
154+
}
155+
156+
// WithBaseURL sets the base URL for the HTTP transport server.
157+
// This is useful for configuring the externally visible base URL for clients.
158+
func WithBaseURL(baseURL string) CommonHTTPServerOption {
159+
return commonOption{
160+
apply: func(c httpTransportConfigurable) {
161+
if baseURL != "" {
162+
u, err := url.Parse(baseURL)
163+
if err != nil {
164+
return
165+
}
166+
if u.Scheme != "http" && u.Scheme != "https" {
167+
return
168+
}
169+
if u.Host == "" || strings.HasPrefix(u.Host, ":") {
170+
return
171+
}
172+
if len(u.Query()) > 0 {
173+
return
174+
}
175+
}
176+
c.setBaseURL(strings.TrimSuffix(baseURL, "/"))
177+
},
178+
}
179+
}
180+
181+
// WithHTTPServer sets the HTTP server instance for the transport.
182+
// This is useful for advanced scenarios where you want to provide your own http.Server.
183+
func WithHTTPServer(srv *http.Server) CommonHTTPServerOption {
184+
return commonOption{
185+
apply: func(c httpTransportConfigurable) {
186+
c.setHTTPServer(srv)
187+
},
188+
}
189+
}

server/sse.go

Lines changed: 48 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,6 @@ type sseSession struct {
3636
// content. This can be used to inject context values from headers, for example.
3737
type SSEContextFunc func(ctx context.Context, r *http.Request) context.Context
3838

39-
// DynamicBasePathFunc allows the user to provide a function to generate the
40-
// base path for a given request and sessionID. This is useful for cases where
41-
// the base path is not known at the time of SSE server creation, such as when
42-
// using a reverse proxy or when the base path is dynamically generated. The
43-
// function should return the base path (e.g., "/mcp/tenant123").
44-
type DynamicBasePathFunc func(r *http.Request, sessionID string) string
45-
4639
func (s *sseSession) SessionID() string {
4740
return s.sessionID
4841
}
@@ -100,7 +93,7 @@ type SSEServer struct {
10093
sseEndpoint string
10194
sessions sync.Map
10295
srv *http.Server
103-
contextFunc SSEContextFunc
96+
contextFunc HTTPContextFunc
10497
dynamicBasePathFunc DynamicBasePathFunc
10598

10699
keepAlive bool
@@ -109,37 +102,41 @@ type SSEServer struct {
109102
mu sync.RWMutex
110103
}
111104

112-
// SSEOption defines a function type for configuring SSEServer
113-
type SSEOption func(*SSEServer)
105+
// Ensure SSEServer implements httpTransportConfigurable
106+
var _ httpTransportConfigurable = (*SSEServer)(nil)
114107

115-
// WithBaseURL sets the base URL for the SSE server
116-
func WithBaseURL(baseURL string) SSEOption {
117-
return func(s *SSEServer) {
118-
if baseURL != "" {
119-
u, err := url.Parse(baseURL)
120-
if err != nil {
121-
return
122-
}
123-
if u.Scheme != "http" && u.Scheme != "https" {
124-
return
125-
}
126-
// Check if the host is empty or only contains a port
127-
if u.Host == "" || strings.HasPrefix(u.Host, ":") {
128-
return
129-
}
130-
if len(u.Query()) > 0 {
131-
return
132-
}
108+
func (s *SSEServer) setBasePath(basePath string) {
109+
s.basePath = normalizeURLPath(basePath)
110+
}
111+
112+
func (s *SSEServer) setDynamicBasePath(fn DynamicBasePathFunc) {
113+
if fn != nil {
114+
s.dynamicBasePathFunc = func(r *http.Request, sid string) string {
115+
bp := fn(r, sid)
116+
return normalizeURLPath(bp)
133117
}
134-
s.baseURL = strings.TrimSuffix(baseURL, "/")
135118
}
136119
}
137120

138-
// WithStaticBasePath adds a new option for setting a static base path
139-
func WithStaticBasePath(basePath string) SSEOption {
140-
return func(s *SSEServer) {
141-
s.basePath = normalizeURLPath(basePath)
142-
}
121+
func (s *SSEServer) setKeepAliveInterval(interval time.Duration) {
122+
s.keepAlive = true
123+
s.keepAliveInterval = interval
124+
}
125+
126+
func (s *SSEServer) setKeepAlive(keepAlive bool) {
127+
s.keepAlive = keepAlive
128+
}
129+
130+
func (s *SSEServer) setContextFunc(fn HTTPContextFunc) {
131+
s.contextFunc = fn
132+
}
133+
134+
func (s *SSEServer) setHTTPServer(srv *http.Server) {
135+
s.srv = srv
136+
}
137+
138+
func (s *SSEServer) setBaseURL(baseURL string) {
139+
s.baseURL = baseURL
143140
}
144141

145142
// WithBasePath adds a new option for setting a static base path.
@@ -151,26 +148,11 @@ func WithBasePath(basePath string) SSEOption {
151148
return WithStaticBasePath(basePath)
152149
}
153150

154-
// WithDynamicBasePath accepts a function for generating the base path. This is
155-
// useful for cases where the base path is not known at the time of SSE server
156-
// creation, such as when using a reverse proxy or when the server is mounted
157-
// at a dynamic path.
158-
func WithDynamicBasePath(fn DynamicBasePathFunc) SSEOption {
159-
return func(s *SSEServer) {
160-
if fn != nil {
161-
s.dynamicBasePathFunc = func(r *http.Request, sid string) string {
162-
bp := fn(r, sid)
163-
return normalizeURLPath(bp)
164-
}
165-
}
166-
}
167-
}
168-
169151
// WithMessageEndpoint sets the message endpoint path
170152
func WithMessageEndpoint(endpoint string) SSEOption {
171-
return func(s *SSEServer) {
153+
return sseOption(func(s *SSEServer) {
172154
s.messageEndpoint = endpoint
173-
}
155+
})
174156
}
175157

176158
// WithAppendQueryToMessageEndpoint configures the SSE server to append the original request's
@@ -179,53 +161,37 @@ func WithMessageEndpoint(endpoint string) SSEOption {
179161
// SSE connection request and carry them over to subsequent message requests, maintaining
180162
// context or authentication details across the communication channel.
181163
func WithAppendQueryToMessageEndpoint() SSEOption {
182-
return func(s *SSEServer) {
164+
return sseOption(func(s *SSEServer) {
183165
s.appendQueryToMessageEndpoint = true
184-
}
166+
})
185167
}
186168

187169
// WithUseFullURLForMessageEndpoint controls whether the SSE server returns a complete URL (including baseURL)
188170
// or just the path portion for the message endpoint. Set to false when clients will concatenate
189171
// the baseURL themselves to avoid malformed URLs like "http://localhost/mcphttp://localhost/mcp/message".
190172
func WithUseFullURLForMessageEndpoint(useFullURLForMessageEndpoint bool) SSEOption {
191-
return func(s *SSEServer) {
173+
return sseOption(func(s *SSEServer) {
192174
s.useFullURLForMessageEndpoint = useFullURLForMessageEndpoint
193-
}
175+
})
194176
}
195177

196178
// WithSSEEndpoint sets the SSE endpoint path
197179
func WithSSEEndpoint(endpoint string) SSEOption {
198-
return func(s *SSEServer) {
180+
return sseOption(func(s *SSEServer) {
199181
s.sseEndpoint = endpoint
200-
}
201-
}
202-
203-
// WithHTTPServer sets the HTTP server instance
204-
func WithHTTPServer(srv *http.Server) SSEOption {
205-
return func(s *SSEServer) {
206-
s.srv = srv
207-
}
208-
}
209-
210-
func WithKeepAliveInterval(keepAliveInterval time.Duration) SSEOption {
211-
return func(s *SSEServer) {
212-
s.keepAlive = true
213-
s.keepAliveInterval = keepAliveInterval
214-
}
215-
}
216-
217-
func WithKeepAlive(keepAlive bool) SSEOption {
218-
return func(s *SSEServer) {
219-
s.keepAlive = keepAlive
220-
}
182+
})
221183
}
222184

223185
// WithSSEContextFunc sets a function that will be called to customise the context
224186
// to the server using the incoming request.
187+
//
188+
// Deprecated: Use WithContextFunc instead. This will be removed in a future version.
189+
//
190+
//go:deprecated
225191
func WithSSEContextFunc(fn SSEContextFunc) SSEOption {
226-
return func(s *SSEServer) {
227-
s.contextFunc = fn
228-
}
192+
return sseOption(func(s *SSEServer) {
193+
WithHTTPContextFunc(HTTPContextFunc(fn)).applyToSSE(s)
194+
})
229195
}
230196

231197
// NewSSEServer creates a new SSE server instance with the given MCP server and options.
@@ -241,16 +207,15 @@ func NewSSEServer(server *MCPServer, opts ...SSEOption) *SSEServer {
241207

242208
// Apply all options
243209
for _, opt := range opts {
244-
opt(s)
210+
opt.applyToSSE(s)
245211
}
246212

247213
return s
248214
}
249215

250-
// NewTestServer creates a test server for testing purposes
216+
// NewTestServer creates a test server for testing purposes.
251217
func NewTestServer(server *MCPServer, opts ...SSEOption) *httptest.Server {
252218
sseServer := NewSSEServer(server, opts...)
253-
254219
testServer := httptest.NewServer(sseServer)
255220
sseServer.baseURL = testServer.URL
256221
return testServer

0 commit comments

Comments
 (0)