@@ -30,18 +30,31 @@ const SessionIDContextKey ContextKey = "awmg-session-id"
3030// requestIDCounter is used to generate unique request IDs for HTTP requests
3131var 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
3446type 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.
141154func 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
258419func (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
434595func (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
452616func (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
479646func (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
497667func (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
525698func (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
543719func (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