Skip to content

Commit c6a3162

Browse files
committed
refactor(server): extract common HTTP transport configuration options
Create a reusable pattern for HTTP transport configuration by: - Adding httpTransportConfigurable interface - Implementing HTTPTransportOption pattern - Moving HTTP transport related options to use the new pattern - Refactor NewSSEServer and NewTestServer to accept a variadic list of options (interface{}), supporting both SSEOption and HTTPTransportOption - Supporting both old and new option formats for backward compatibility This non-breaking refactor enables code sharing for HTTP transport configuration between SSE and future streamable protocols.
1 parent 9d6b793 commit c6a3162

File tree

3 files changed

+158
-82
lines changed

3 files changed

+158
-82
lines changed

server/http_transport_options.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
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+
// WithStaticBasePath adds a new option for setting a static base path.
29+
// This is useful for mounting the server at a known, fixed path.
30+
func WithStaticBasePath(basePath string) HTTPTransportOption {
31+
return func(c httpTransportConfigurable) {
32+
c.setBasePath(basePath)
33+
}
34+
}
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+
43+
// WithDynamicBasePath accepts a function for generating the base path.
44+
// This is useful for cases where the base path is not known at the time of server creation,
45+
// such as when using a reverse proxy or when the server is mounted at a dynamic path.
46+
func WithDynamicBasePath(fn DynamicBasePathFunc) HTTPTransportOption {
47+
return func(c httpTransportConfigurable) {
48+
c.setDynamicBasePath(fn)
49+
}
50+
}
51+
52+
// WithKeepAliveInterval sets the keep-alive interval for the transport.
53+
// When enabled, the server will periodically send ping events to keep the connection alive.
54+
func WithKeepAliveInterval(interval time.Duration) HTTPTransportOption {
55+
return func(c httpTransportConfigurable) {
56+
c.setKeepAliveInterval(interval)
57+
}
58+
}
59+
60+
// WithKeepAlive enables or disables keep-alive for the transport.
61+
// When enabled, the server will send periodic keep-alive events to clients.
62+
func WithKeepAlive(keepAlive bool) HTTPTransportOption {
63+
return func(c httpTransportConfigurable) {
64+
c.setKeepAlive(keepAlive)
65+
}
66+
}
67+
68+
// WithHTTPContextFunc sets a function that will be called to customize the context
69+
// for the server using the incoming request. This is useful for injecting
70+
// context values from headers or other request properties.
71+
func WithHTTPContextFunc(fn HTTPContextFunc) HTTPTransportOption {
72+
return func(c httpTransportConfigurable) {
73+
c.setContextFunc(fn)
74+
}
75+
}
76+
77+
// WithBaseURL sets the base URL for the HTTP transport server.
78+
// This is useful for configuring the externally visible base URL for clients.
79+
func WithBaseURL(baseURL string) HTTPTransportOption {
80+
return func(c httpTransportConfigurable) {
81+
if s, ok := c.(*SSEServer); ok {
82+
if baseURL != "" {
83+
u, err := url.Parse(baseURL)
84+
if err != nil {
85+
return
86+
}
87+
if u.Scheme != "http" && u.Scheme != "https" {
88+
return
89+
}
90+
if u.Host == "" || strings.HasPrefix(u.Host, ":") {
91+
return
92+
}
93+
if len(u.Query()) > 0 {
94+
return
95+
}
96+
}
97+
s.baseURL = strings.TrimSuffix(baseURL, "/")
98+
}
99+
// For future protocols, add similar logic here
100+
}
101+
}
102+
103+
// WithHTTPServer sets the HTTP server instance for the transport.
104+
// This is useful for advanced scenarios where you want to provide your own http.Server.
105+
func WithHTTPServer(srv *http.Server) HTTPTransportOption {
106+
return func(c httpTransportConfigurable) {
107+
if s, ok := c.(*SSEServer); ok {
108+
s.srv = srv
109+
}
110+
// For future protocols, add similar logic here
111+
}
112+
}
113+

server/sse.go

Lines changed: 43 additions & 80 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,63 +102,43 @@ 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
143127
}
144128

145-
// WithBasePath adds a new option for setting a static base path.
146-
//
147-
// Deprecated: Use WithStaticBasePath instead. This will be removed in a future version.
148-
//
149-
//go:deprecated
150-
func WithBasePath(basePath string) SSEOption {
151-
return WithStaticBasePath(basePath)
129+
// setKeepAlive enables or disables keep-alive (internal use only)
130+
func (s *SSEServer) setKeepAlive(keepAlive bool) {
131+
s.keepAlive = keepAlive
152132
}
153133

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-
}
134+
// setContextFunc sets the context customization function (internal use only)
135+
func (s *SSEServer) setContextFunc(fn HTTPContextFunc) {
136+
s.contextFunc = fn
167137
}
168138

139+
// SSEOption defines a function type for configuring SSEServer
140+
type SSEOption func(*SSEServer)
141+
169142
// WithMessageEndpoint sets the message endpoint path
170143
func WithMessageEndpoint(endpoint string) SSEOption {
171144
return func(s *SSEServer) {
@@ -200,36 +173,19 @@ func WithSSEEndpoint(endpoint string) SSEOption {
200173
}
201174
}
202175

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-
}
221-
}
222-
223176
// WithSSEContextFunc sets a function that will be called to customise the context
224177
// to the server using the incoming request.
178+
//
179+
// Deprecated: Use WithContextFunc instead. This will be removed in a future version.
180+
//go:deprecated
225181
func WithSSEContextFunc(fn SSEContextFunc) SSEOption {
226182
return func(s *SSEServer) {
227-
s.contextFunc = fn
183+
WithHTTPContextFunc(HTTPContextFunc(fn))(s)
228184
}
229185
}
230186

231187
// NewSSEServer creates a new SSE server instance with the given MCP server and options.
232-
func NewSSEServer(server *MCPServer, opts ...SSEOption) *SSEServer {
188+
func NewSSEServer(server *MCPServer, opts ...interface{}) *SSEServer {
233189
s := &SSEServer{
234190
server: server,
235191
sseEndpoint: "/sse",
@@ -241,16 +197,23 @@ func NewSSEServer(server *MCPServer, opts ...SSEOption) *SSEServer {
241197

242198
// Apply all options
243199
for _, opt := range opts {
244-
opt(s)
200+
switch o := opt.(type) {
201+
case SSEOption:
202+
o(s)
203+
case HTTPTransportOption:
204+
o(s)
205+
default:
206+
// Optionally: log or panic for unknown option type
207+
}
245208
}
246209

247210
return s
248211
}
249212

250-
// NewTestServer creates a test server for testing purposes
251-
func NewTestServer(server *MCPServer, opts ...SSEOption) *httptest.Server {
213+
// NewTestServer creates a test server for testing purposes.
214+
// It takes the same options as NewSSEServer (variadic ...interface{}).
215+
func NewTestServer(server *MCPServer, opts ...interface{}) *httptest.Server {
252216
sseServer := NewSSEServer(server, opts...)
253-
254217
testServer := httptest.NewServer(sseServer)
255218
sseServer.baseURL = testServer.URL
256219
return testServer

server/sse_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -715,7 +715,7 @@ func TestSSEServer(t *testing.T) {
715715
sseEndpoint := "/sse-test"
716716
useFullURLForMessageEndpoint := false
717717
srv := &http.Server{}
718-
rands := []SSEOption{
718+
rands := []interface {}{
719719
WithStaticBasePath(basePath),
720720
WithBaseURL(baseURL),
721721
WithMessageEndpoint(messageEndpoint),
@@ -725,7 +725,7 @@ func TestSSEServer(t *testing.T) {
725725
}
726726
for i := 0; i < 100; i++ {
727727

728-
var options []SSEOption
728+
var options []interface {}
729729
for i2 := 0; i2 < 100; i2++ {
730730
index := rand.Intn(len(rands))
731731
options = append(options, rands[index])

0 commit comments

Comments
 (0)