Skip to content

Commit 21668b8

Browse files
authored
Merge pull request #180 from githubnext/copilot/fix-gateway-http-server-handling
Add HTTP transport fallback for non-standard MCP servers
2 parents f9d62fa + 0904e6d commit 21668b8

1 file changed

Lines changed: 225 additions & 46 deletions

File tree

internal/mcp/connection.go

Lines changed: 225 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,31 @@ const SessionIDContextKey ContextKey = "awmg-session-id"
3030
// requestIDCounter is used to generate unique request IDs for HTTP requests
3131
var requestIDCounter uint64
3232

33+
// HTTPTransportType represents the type of HTTP transport being used
34+
type HTTPTransportType string
35+
36+
const (
37+
// HTTPTransportStreamable uses the streamable HTTP transport (2025-03-26 spec)
38+
HTTPTransportStreamable HTTPTransportType = "streamable"
39+
// HTTPTransportSSE uses the SSE transport (2024-11-05 spec)
40+
HTTPTransportSSE HTTPTransportType = "sse"
41+
// HTTPTransportPlainJSON uses plain JSON-RPC 2.0 over HTTP POST (non-standard)
42+
HTTPTransportPlainJSON HTTPTransportType = "plain-json"
43+
)
44+
3345
// Connection represents a connection to an MCP server using the official SDK
3446
type Connection struct {
3547
client *sdk.Client
3648
session *sdk.ClientSession
3749
ctx context.Context
3850
cancel context.CancelFunc
3951
// HTTP-specific fields
40-
isHTTP bool
41-
httpURL string
42-
headers map[string]string
43-
httpClient *http.Client
44-
httpSessionID string // Session ID returned by the HTTP backend
52+
isHTTP bool
53+
httpURL string
54+
headers map[string]string
55+
httpClient *http.Client
56+
httpSessionID string // Session ID returned by the HTTP backend
57+
httpTransportType HTTPTransportType // Type of HTTP transport in use
4558
}
4659

4760
// NewConnection creates a new MCP connection using the official SDK
@@ -126,20 +139,20 @@ func NewConnection(ctx context.Context, command string, args []string, env map[s
126139
return conn, nil
127140
}
128141

129-
// NewHTTPConnection creates a new HTTP-based MCP connection
142+
// NewHTTPConnection creates a new HTTP-based MCP connection with transport fallback
130143
// For HTTP servers that are already running, we connect and initialize a session
131144
//
132-
// NOTE: This currently implements the MCP HTTP protocol manually instead of using
133-
// the SDK's SSEClientTransport. This is because:
134-
// 1. SSEClientTransport requires Server-Sent Events (SSE) format
135-
// 2. Some HTTP MCP servers (like safeinputs) use plain JSON-RPC over HTTP POST
136-
// 3. The MCP spec allows both SSE and plain HTTP transports
145+
// This function implements a fallback strategy for HTTP transports:
146+
// 1. If custom headers are provided, skip SDK transports (they don't support custom headers)
147+
// and use plain JSON-RPC 2.0 over HTTP POST (for safeinputs compatibility)
148+
// 2. Otherwise, try standard transports:
149+
// a. Streamable HTTP (2025-03-26 spec) using SDK's StreamableClientTransport
150+
// b. SSE (2024-11-05 spec) using SDK's SSEClientTransport
151+
// c. Plain JSON-RPC 2.0 over HTTP POST as final fallback
137152
//
138-
// TODO: Migrate to sdk.SSEClientTransport once we confirm all target HTTP backends
139-
// support the SSE format, or extend the SDK to support both transport formats.
140-
// This would eliminate manual JSON-RPC handling and improve maintainability.
153+
// This ensures compatibility with all types of HTTP MCP servers.
141154
func NewHTTPConnection(ctx context.Context, url string, headers map[string]string) (*Connection, error) {
142-
logger.LogInfo("backend", "Creating HTTP MCP connection, url=%s", url)
155+
logger.LogInfo("backend", "Creating HTTP MCP connection with transport fallback, url=%s", url)
143156
logConn.Printf("Creating HTTP MCP connection: url=%s", url)
144157
ctx, cancel := context.WithCancel(ctx)
145158

@@ -153,29 +166,158 @@ func NewHTTPConnection(ctx context.Context, url string, headers map[string]strin
153166
},
154167
}
155168

169+
// If custom headers are provided, skip SDK transports as they don't support headers
170+
// This is typical for backends like safeinputs that require authentication
171+
if len(headers) > 0 {
172+
logConn.Printf("Custom headers detected, using plain JSON-RPC transport for %s", url)
173+
conn, err := tryPlainJSONTransport(ctx, cancel, url, headers, httpClient)
174+
if err == nil {
175+
logger.LogInfo("backend", "Successfully connected using plain JSON-RPC transport, url=%s", url)
176+
log.Printf("Configured HTTP MCP server with plain JSON-RPC transport: %s", url)
177+
return conn, nil
178+
}
179+
cancel()
180+
logger.LogError("backend", "Plain JSON-RPC transport failed for url=%s, error=%v", url, err)
181+
return nil, fmt.Errorf("failed to connect with plain JSON-RPC transport: %w", err)
182+
}
183+
184+
// Try standard transports in order: streamable HTTP → SSE → plain JSON-RPC
185+
186+
// Try 1: Streamable HTTP (2025-03-26 spec)
187+
logConn.Printf("Attempting streamable HTTP transport for %s", url)
188+
conn, err := tryStreamableHTTPTransport(ctx, cancel, url, headers, httpClient)
189+
if err == nil {
190+
logger.LogInfo("backend", "Successfully connected using streamable HTTP transport, url=%s", url)
191+
log.Printf("Configured HTTP MCP server with streamable transport: %s", url)
192+
return conn, nil
193+
}
194+
logConn.Printf("Streamable HTTP failed: %v", err)
195+
196+
// Try 2: SSE (2024-11-05 spec)
197+
logConn.Printf("Attempting SSE transport for %s", url)
198+
conn, err = trySSETransport(ctx, cancel, url, headers, httpClient)
199+
if err == nil {
200+
logger.LogInfo("backend", "Successfully connected using SSE transport, url=%s", url)
201+
log.Printf("Configured HTTP MCP server with SSE transport: %s", url)
202+
return conn, nil
203+
}
204+
logConn.Printf("SSE transport failed: %v", err)
205+
206+
// Try 3: Plain JSON-RPC over HTTP (non-standard, for fallback)
207+
logConn.Printf("Attempting plain JSON-RPC transport for %s", url)
208+
conn, err = tryPlainJSONTransport(ctx, cancel, url, headers, httpClient)
209+
if err == nil {
210+
logger.LogInfo("backend", "Successfully connected using plain JSON-RPC transport, url=%s", url)
211+
log.Printf("Configured HTTP MCP server with plain JSON-RPC transport: %s", url)
212+
return conn, nil
213+
}
214+
logConn.Printf("Plain JSON-RPC transport failed: %v", err)
215+
216+
// All transports failed
217+
cancel()
218+
logger.LogError("backend", "All HTTP transports failed for url=%s", url)
219+
return nil, fmt.Errorf("failed to connect using any HTTP transport (tried streamable, SSE, and plain JSON-RPC): last error: %w", err)
220+
}
221+
222+
// tryStreamableHTTPTransport attempts to connect using the streamable HTTP transport (2025-03-26 spec)
223+
func tryStreamableHTTPTransport(ctx context.Context, cancel context.CancelFunc, url string, headers map[string]string, httpClient *http.Client) (*Connection, error) {
224+
// Create MCP client
225+
client := sdk.NewClient(&sdk.Implementation{
226+
Name: "awmg",
227+
Version: "1.0.0",
228+
}, nil)
229+
230+
// Create streamable HTTP transport
231+
transport := &sdk.StreamableClientTransport{
232+
Endpoint: url,
233+
HTTPClient: httpClient,
234+
MaxRetries: 0, // Don't retry on failure - we'll try other transports
235+
}
236+
237+
// Try to connect - this will fail if the server doesn't support streamable HTTP
238+
session, err := client.Connect(ctx, transport, nil)
239+
if err != nil {
240+
return nil, fmt.Errorf("streamable HTTP transport connect failed: %w", err)
241+
}
242+
243+
conn := &Connection{
244+
client: client,
245+
session: session,
246+
ctx: ctx,
247+
cancel: cancel,
248+
isHTTP: true,
249+
httpURL: url,
250+
headers: headers,
251+
httpClient: httpClient,
252+
httpTransportType: HTTPTransportStreamable,
253+
}
254+
255+
logger.LogInfo("backend", "Streamable HTTP transport connected successfully")
256+
logConn.Printf("Connected with streamable HTTP transport")
257+
return conn, nil
258+
}
259+
260+
// trySSETransport attempts to connect using the SSE transport (2024-11-05 spec)
261+
func trySSETransport(ctx context.Context, cancel context.CancelFunc, url string, headers map[string]string, httpClient *http.Client) (*Connection, error) {
262+
// Create MCP client
263+
client := sdk.NewClient(&sdk.Implementation{
264+
Name: "awmg",
265+
Version: "1.0.0",
266+
}, nil)
267+
268+
// Create SSE transport
269+
transport := &sdk.SSEClientTransport{
270+
Endpoint: url,
271+
HTTPClient: httpClient,
272+
}
273+
274+
// Try to connect - this will fail if the server doesn't support SSE
275+
session, err := client.Connect(ctx, transport, nil)
276+
if err != nil {
277+
return nil, fmt.Errorf("SSE transport connect failed: %w", err)
278+
}
279+
156280
conn := &Connection{
157-
ctx: ctx,
158-
cancel: cancel,
159-
isHTTP: true,
160-
httpURL: url,
161-
headers: headers,
162-
httpClient: httpClient,
281+
client: client,
282+
session: session,
283+
ctx: ctx,
284+
cancel: cancel,
285+
isHTTP: true,
286+
httpURL: url,
287+
headers: headers,
288+
httpClient: httpClient,
289+
httpTransportType: HTTPTransportSSE,
290+
}
291+
292+
logger.LogInfo("backend", "SSE transport connected successfully")
293+
logConn.Printf("Connected with SSE transport")
294+
return conn, nil
295+
}
296+
297+
// tryPlainJSONTransport attempts to connect using plain JSON-RPC 2.0 over HTTP POST (non-standard)
298+
// This is used for compatibility with servers like safeinputs that don't implement standard MCP HTTP transports
299+
func tryPlainJSONTransport(ctx context.Context, cancel context.CancelFunc, url string, headers map[string]string, httpClient *http.Client) (*Connection, error) {
300+
conn := &Connection{
301+
ctx: ctx,
302+
cancel: cancel,
303+
isHTTP: true,
304+
httpURL: url,
305+
headers: headers,
306+
httpClient: httpClient,
307+
httpTransportType: HTTPTransportPlainJSON,
163308
}
164309

165310
// Send initialize request to establish a session with the HTTP backend
166311
// This is critical for backends that require session management
167-
logConn.Printf("Sending initialize request to HTTP backend: url=%s", url)
312+
logConn.Printf("Sending initialize request via plain JSON-RPC to: %s", url)
168313
sessionID, err := conn.initializeHTTPSession()
169314
if err != nil {
170-
cancel()
171-
logger.LogError("backend", "Failed to initialize HTTP session, url=%s, error=%v", url, err)
172-
return nil, fmt.Errorf("failed to initialize HTTP session: %w", err)
315+
return nil, fmt.Errorf("plain JSON-RPC initialize failed: %w", err)
173316
}
174317

175318
conn.httpSessionID = sessionID
176-
logger.LogInfo("backend", "Successfully created HTTP MCP connection with session, url=%s, session=%s", url, sessionID)
177-
logConn.Printf("HTTP connection created with session: url=%s, session=%s", url, sessionID)
178-
log.Printf("Configured HTTP MCP server with session: %s (session: %s)", url, sessionID)
319+
logger.LogInfo("backend", "Plain JSON-RPC transport connected successfully with session=%s", sessionID)
320+
logConn.Printf("Connected with plain JSON-RPC transport, session=%s", sessionID)
179321
return conn, nil
180322
}
181323

@@ -214,9 +356,22 @@ func (c *Connection) SendRequestWithServerID(ctx context.Context, method string,
214356
var result *Response
215357
var err error
216358

217-
// Handle HTTP connections by proxying the request
359+
// Handle HTTP connections
218360
if c.isHTTP {
219-
result, err = c.sendHTTPRequest(ctx, method, params)
361+
// For plain JSON-RPC transport, use manual HTTP requests
362+
if c.httpTransportType == HTTPTransportPlainJSON {
363+
result, err = c.sendHTTPRequest(ctx, method, params)
364+
// Log the response from backend server
365+
var responsePayload []byte
366+
if result != nil {
367+
responsePayload, _ = json.Marshal(result)
368+
}
369+
logger.LogRPCResponse(logger.RPCDirectionInbound, serverID, responsePayload, err)
370+
return result, err
371+
}
372+
373+
// For streamable and SSE transports, use SDK session methods
374+
result, err = c.callSDKMethod(method, params)
220375
// Log the response from backend server
221376
var responsePayload []byte
222377
if result != nil {
@@ -227,22 +382,7 @@ func (c *Connection) SendRequestWithServerID(ctx context.Context, method string,
227382
}
228383

229384
// Handle stdio connections using SDK client
230-
switch method {
231-
case "tools/list":
232-
result, err = c.listTools()
233-
case "tools/call":
234-
result, err = c.callTool(params)
235-
case "resources/list":
236-
result, err = c.listResources()
237-
case "resources/read":
238-
result, err = c.readResource(params)
239-
case "prompts/list":
240-
result, err = c.listPrompts()
241-
case "prompts/get":
242-
result, err = c.getPrompt(params)
243-
default:
244-
err = fmt.Errorf("unsupported method: %s", method)
245-
}
385+
result, err = c.callSDKMethod(method, params)
246386

247387
// Log the response from backend server
248388
var responsePayload []byte
@@ -254,6 +394,27 @@ func (c *Connection) SendRequestWithServerID(ctx context.Context, method string,
254394
return result, err
255395
}
256396

397+
// callSDKMethod calls the appropriate SDK method based on the method name
398+
// This centralizes the method dispatch logic used by both HTTP SDK transports and stdio
399+
func (c *Connection) callSDKMethod(method string, params interface{}) (*Response, error) {
400+
switch method {
401+
case "tools/list":
402+
return c.listTools()
403+
case "tools/call":
404+
return c.callTool(params)
405+
case "resources/list":
406+
return c.listResources()
407+
case "resources/read":
408+
return c.readResource(params)
409+
case "prompts/list":
410+
return c.listPrompts()
411+
case "prompts/get":
412+
return c.getPrompt(params)
413+
default:
414+
return nil, fmt.Errorf("unsupported method: %s", method)
415+
}
416+
}
417+
257418
// initializeHTTPSession sends an initialize request to the HTTP backend and captures the session ID
258419
func (c *Connection) initializeHTTPSession() (string, error) {
259420
// Generate unique request ID
@@ -432,6 +593,9 @@ func (c *Connection) sendHTTPRequest(ctx context.Context, method string, params
432593
}
433594

434595
func (c *Connection) listTools() (*Response, error) {
596+
if c.session == nil {
597+
return nil, fmt.Errorf("SDK session not available for plain JSON-RPC transport")
598+
}
435599
result, err := c.session.ListTools(c.ctx, &sdk.ListToolsParams{})
436600
if err != nil {
437601
return nil, err
@@ -450,6 +614,9 @@ func (c *Connection) listTools() (*Response, error) {
450614
}
451615

452616
func (c *Connection) callTool(params interface{}) (*Response, error) {
617+
if c.session == nil {
618+
return nil, fmt.Errorf("SDK session not available for plain JSON-RPC transport")
619+
}
453620
var callParams CallToolParams
454621
paramsJSON, _ := json.Marshal(params)
455622
if err := json.Unmarshal(paramsJSON, &callParams); err != nil {
@@ -477,6 +644,9 @@ func (c *Connection) callTool(params interface{}) (*Response, error) {
477644
}
478645

479646
func (c *Connection) listResources() (*Response, error) {
647+
if c.session == nil {
648+
return nil, fmt.Errorf("SDK session not available for plain JSON-RPC transport")
649+
}
480650
result, err := c.session.ListResources(c.ctx, &sdk.ListResourcesParams{})
481651
if err != nil {
482652
return nil, err
@@ -495,6 +665,9 @@ func (c *Connection) listResources() (*Response, error) {
495665
}
496666

497667
func (c *Connection) readResource(params interface{}) (*Response, error) {
668+
if c.session == nil {
669+
return nil, fmt.Errorf("SDK session not available for plain JSON-RPC transport")
670+
}
498671
var readParams struct {
499672
URI string `json:"uri"`
500673
}
@@ -523,6 +696,9 @@ func (c *Connection) readResource(params interface{}) (*Response, error) {
523696
}
524697

525698
func (c *Connection) listPrompts() (*Response, error) {
699+
if c.session == nil {
700+
return nil, fmt.Errorf("SDK session not available for plain JSON-RPC transport")
701+
}
526702
result, err := c.session.ListPrompts(c.ctx, &sdk.ListPromptsParams{})
527703
if err != nil {
528704
return nil, err
@@ -541,6 +717,9 @@ func (c *Connection) listPrompts() (*Response, error) {
541717
}
542718

543719
func (c *Connection) getPrompt(params interface{}) (*Response, error) {
720+
if c.session == nil {
721+
return nil, fmt.Errorf("SDK session not available for plain JSON-RPC transport")
722+
}
544723
var getParams struct {
545724
Name string `json:"name"`
546725
Arguments map[string]string `json:"arguments"`

0 commit comments

Comments
 (0)