Skip to content

Commit a0b2158

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 a0b2158

File tree

2 files changed

+224
-83
lines changed

2 files changed

+224
-83
lines changed

server/http_transport_options.go

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

server/sse.go

Lines changed: 44 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,38 @@ 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+
// setBasePath sets the static base path (internal use only)
109+
func (s *SSEServer) setBasePath(basePath string) {
110+
s.basePath = normalizeURLPath(basePath)
111+
}
112+
113+
// setDynamicBasePath sets the dynamic base path function (internal use only)
114+
func (s *SSEServer) setDynamicBasePath(fn DynamicBasePathFunc) {
115+
if fn != nil {
116+
s.dynamicBasePathFunc = func(r *http.Request, sid string) string {
117+
bp := fn(r, sid)
118+
return normalizeURLPath(bp)
133119
}
134-
s.baseURL = strings.TrimSuffix(baseURL, "/")
135120
}
136121
}
137122

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-
}
123+
// setKeepAliveInterval sets the keep-alive interval (internal use only)
124+
func (s *SSEServer) setKeepAliveInterval(interval time.Duration) {
125+
s.keepAlive = true
126+
s.keepAliveInterval = interval
127+
}
128+
129+
// setKeepAlive enables or disables keep-alive (internal use only)
130+
func (s *SSEServer) setKeepAlive(keepAlive bool) {
131+
s.keepAlive = keepAlive
132+
}
133+
134+
// setContextFunc sets the context customization function (internal use only)
135+
func (s *SSEServer) setContextFunc(fn HTTPContextFunc) {
136+
s.contextFunc = fn
143137
}
144138

145139
// WithBasePath adds a new option for setting a static base path.
@@ -151,26 +145,11 @@ func WithBasePath(basePath string) SSEOption {
151145
return WithStaticBasePath(basePath)
152146
}
153147

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-
169148
// WithMessageEndpoint sets the message endpoint path
170149
func WithMessageEndpoint(endpoint string) SSEOption {
171-
return func(s *SSEServer) {
150+
return sseOption(func(s *SSEServer) {
172151
s.messageEndpoint = endpoint
173-
}
152+
})
174153
}
175154

176155
// WithAppendQueryToMessageEndpoint configures the SSE server to append the original request's
@@ -179,53 +158,36 @@ func WithMessageEndpoint(endpoint string) SSEOption {
179158
// SSE connection request and carry them over to subsequent message requests, maintaining
180159
// context or authentication details across the communication channel.
181160
func WithAppendQueryToMessageEndpoint() SSEOption {
182-
return func(s *SSEServer) {
161+
return sseOption(func(s *SSEServer) {
183162
s.appendQueryToMessageEndpoint = true
184-
}
163+
})
185164
}
186165

187166
// WithUseFullURLForMessageEndpoint controls whether the SSE server returns a complete URL (including baseURL)
188167
// or just the path portion for the message endpoint. Set to false when clients will concatenate
189168
// the baseURL themselves to avoid malformed URLs like "http://localhost/mcphttp://localhost/mcp/message".
190169
func WithUseFullURLForMessageEndpoint(useFullURLForMessageEndpoint bool) SSEOption {
191-
return func(s *SSEServer) {
170+
return sseOption(func(s *SSEServer) {
192171
s.useFullURLForMessageEndpoint = useFullURLForMessageEndpoint
193-
}
172+
})
194173
}
195174

196175
// WithSSEEndpoint sets the SSE endpoint path
197176
func WithSSEEndpoint(endpoint string) SSEOption {
198-
return func(s *SSEServer) {
177+
return sseOption(func(s *SSEServer) {
199178
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-
}
179+
})
221180
}
222181

223182
// WithSSEContextFunc sets a function that will be called to customise the context
224183
// to the server using the incoming request.
184+
//
185+
// Deprecated: Use WithContextFunc instead. This will be removed in a future version.
186+
//go:deprecated
225187
func WithSSEContextFunc(fn SSEContextFunc) SSEOption {
226-
return func(s *SSEServer) {
227-
s.contextFunc = fn
228-
}
188+
return sseOption(func(s *SSEServer) {
189+
WithHTTPContextFunc(HTTPContextFunc(fn)).applyToSSE(s)
190+
})
229191
}
230192

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

242204
// Apply all options
243205
for _, opt := range opts {
244-
opt(s)
206+
opt.applyToSSE(s)
245207
}
246208

247209
return s
248210
}
249211

250-
// NewTestServer creates a test server for testing purposes
212+
// NewTestServer creates a test server for testing purposes.
251213
func NewTestServer(server *MCPServer, opts ...SSEOption) *httptest.Server {
252214
sseServer := NewSSEServer(server, opts...)
253-
254215
testServer := httptest.NewServer(sseServer)
255216
sseServer.baseURL = testServer.URL
256217
return testServer

0 commit comments

Comments
 (0)