Skip to content

Commit cce3e24

Browse files
authored
Refactor HTTP request initialization to eliminate duplicate code pattern (#937)
The `internal/mcp/connection.go` file contained duplicate HTTP request initialization workflow across `initializeHTTPSession` and `sendHTTPRequest` methods, differing only in header customization logic. This made the codebase harder to maintain and inconsistent for future HTTP-related changes. ## Changes - **Extracted `executeHTTPRequest` helper function** that consolidates the common pattern: create JSON-RPC request → marshal → setup HTTP → execute → read response - Returns `httpRequestResult` struct containing status code, response body, and headers - Accepts optional `headerModifier` callback for caller-specific header customization - Provides consistent connection error handling with method-specific error messages - **Refactored `initializeHTTPSession`** to use the helper with session ID header management - **Refactored `sendHTTPRequest`** to use the helper with context-based session ID priority handling ## Example Before (duplicated in both methods): ```go request := createJSONRPCRequest(requestID, method, params) requestBody, err := json.Marshal(request) // ... marshal error handling httpReq, err := setupHTTPRequest(ctx, c.httpURL, requestBody, c.headers) // ... setup error handling httpResp, err := c.httpClient.Do(httpReq) // ... connection error handling defer httpResp.Body.Close() responseBody, err := io.ReadAll(httpResp.Body) // ... read error handling ``` After (single location): ```go result, err := c.executeHTTPRequest(ctx, method, params, requestID, func(httpReq *http.Request) { // Caller-specific header customization httpReq.Header.Set("Mcp-Session-Id", sessionID) }) ``` ## Impact - Future HTTP changes (timeouts, retries, logging) require updates in one location - Consistent error messages across all HTTP operations - Net change: +105 lines, -99 lines (new struct and documentation) > [!WARNING] > > <details> > <summary>Firewall rules blocked me from connecting to one or more addresses (expand for details)</summary> > > #### I tried to connect to the following addresses, but was blocked by firewall rules: > > - `example.com` > - Triggering command: `/tmp/go-build3466960917/b275/launcher.test /tmp/go-build3466960917/b275/launcher.test -test.testlogfile=/tmp/go-build3466960917/b275/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true 64/src/net /bin/sh` (dns block) > - Triggering command: `/tmp/go-build3643262344/b275/launcher.test /tmp/go-build3643262344/b275/launcher.test -test.testlogfile=/tmp/go-build3643262344/b275/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true /opt/hostedtoolcache/go/1.25.7/x64/src/net ache/go/1.25.7/x64/src/crypto/internal/fips140hash/hash.go /usr/bin/gcc --gdwarf-5 --64 -o /usr/bin/gcc -I /opt/hostedtoolcache/go/1.25.7/x64/src/net -fPIC 64/pkg/tool/linux_amd64/vet -pthread -Wl,--no-gc-sect-d -fmessage-length=0 64/pkg/tool/linux_amd64/vet` (dns block) > - Triggering command: `/tmp/go-build3690481232/b279/launcher.test /tmp/go-build3690481232/b279/launcher.test -test.testlogfile=/tmp/go-build3690481232/b279/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true /home/REDACTED/work/gh-aw-mcpg/gh-aw-mcpg/internal/envutil/envutil.go /home/REDACTED/work/gh-aw-mcpg/gh-aw-mcpg/internal/envutil/envutil_test.go ache/uv/0.10.2/x86_64/git elect.go nix.go 64/pkg/tool/linu/tmp/go-build4145022949/b166/vet.cfg bash /usr�� --version T.md d ache/go/1.25.7/x/opt/hostedtoolcache/go/1.25.7/x64/pkg/tool/linux_amd64/vet -trimpath /x86_64-linux-gn/tmp/go-build4145022949/b183/vet.cfg /usr/bin/runc.original` (dns block) > - `invalid-host-that-does-not-exist-12345.com` > - Triggering command: `/tmp/go-build3466960917/b260/config.test /tmp/go-build3466960917/b260/config.test -test.testlogfile=/tmp/go-build3466960917/b260/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true 64/src/net node cal/bin/as --output-format stream-json cal/bin/which 01.o -p 64/src/net client.go 64/pkg/tool/linux_amd64/compile -I /tmp/go-build271-unsafeptr=false -I 64/pkg/tool/linu/tmp/go-build3466960917/b144/vet.cfg` (dns block) > - `nonexistent.local` > - Triggering command: `/tmp/go-build3466960917/b275/launcher.test /tmp/go-build3466960917/b275/launcher.test -test.testlogfile=/tmp/go-build3466960917/b275/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true 64/src/net /bin/sh` (dns block) > - Triggering command: `/tmp/go-build3643262344/b275/launcher.test /tmp/go-build3643262344/b275/launcher.test -test.testlogfile=/tmp/go-build3643262344/b275/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true /opt/hostedtoolcache/go/1.25.7/x64/src/net ache/go/1.25.7/x64/src/crypto/internal/fips140hash/hash.go /usr/bin/gcc --gdwarf-5 --64 -o /usr/bin/gcc -I /opt/hostedtoolcache/go/1.25.7/x64/src/net -fPIC 64/pkg/tool/linux_amd64/vet -pthread -Wl,--no-gc-sect-d -fmessage-length=0 64/pkg/tool/linux_amd64/vet` (dns block) > - Triggering command: `/tmp/go-build3690481232/b279/launcher.test /tmp/go-build3690481232/b279/launcher.test -test.testlogfile=/tmp/go-build3690481232/b279/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true /home/REDACTED/work/gh-aw-mcpg/gh-aw-mcpg/internal/envutil/envutil.go /home/REDACTED/work/gh-aw-mcpg/gh-aw-mcpg/internal/envutil/envutil_test.go ache/uv/0.10.2/x86_64/git elect.go nix.go 64/pkg/tool/linu/tmp/go-build4145022949/b166/vet.cfg bash /usr�� --version T.md d ache/go/1.25.7/x/opt/hostedtoolcache/go/1.25.7/x64/pkg/tool/linux_amd64/vet -trimpath /x86_64-linux-gn/tmp/go-build4145022949/b183/vet.cfg /usr/bin/runc.original` (dns block) > - `slow.example.com` > - Triggering command: `/tmp/go-build3466960917/b275/launcher.test /tmp/go-build3466960917/b275/launcher.test -test.testlogfile=/tmp/go-build3466960917/b275/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true 64/src/net /bin/sh` (dns block) > - Triggering command: `/tmp/go-build3643262344/b275/launcher.test /tmp/go-build3643262344/b275/launcher.test -test.testlogfile=/tmp/go-build3643262344/b275/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true /opt/hostedtoolcache/go/1.25.7/x64/src/net ache/go/1.25.7/x64/src/crypto/internal/fips140hash/hash.go /usr/bin/gcc --gdwarf-5 --64 -o /usr/bin/gcc -I /opt/hostedtoolcache/go/1.25.7/x64/src/net -fPIC 64/pkg/tool/linux_amd64/vet -pthread -Wl,--no-gc-sect-d -fmessage-length=0 64/pkg/tool/linux_amd64/vet` (dns block) > - Triggering command: `/tmp/go-build3690481232/b279/launcher.test /tmp/go-build3690481232/b279/launcher.test -test.testlogfile=/tmp/go-build3690481232/b279/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true /home/REDACTED/work/gh-aw-mcpg/gh-aw-mcpg/internal/envutil/envutil.go /home/REDACTED/work/gh-aw-mcpg/gh-aw-mcpg/internal/envutil/envutil_test.go ache/uv/0.10.2/x86_64/git elect.go nix.go 64/pkg/tool/linu/tmp/go-build4145022949/b166/vet.cfg bash /usr�� --version T.md d ache/go/1.25.7/x/opt/hostedtoolcache/go/1.25.7/x64/pkg/tool/linux_amd64/vet -trimpath /x86_64-linux-gn/tmp/go-build4145022949/b183/vet.cfg /usr/bin/runc.original` (dns block) > - `this-host-does-not-exist-12345.com` > - Triggering command: `/tmp/go-build3466960917/b284/mcp.test /tmp/go-build3466960917/b284/mcp.test -test.testlogfile=/tmp/go-build3466960917/b284/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true g_.a -trimpath ache/go/1.25.7/x64/pkg/tool/linux_amd64/asm 2690816/b098/ strings ctor ache/go/1.25.7/x/tmp/go-build3466960917/b205/vet.cfg -I fips140/sha3 gkWDTCvzb 2690816/b130=&gt; --gdwarf-5 2690816/b130/ -o ache/go/1.25.7/x64/pkg/include` (dns block) > - Triggering command: `/tmp/go-build330381427/b001/mcp.test /tmp/go-build330381427/b001/mcp.test -test.testlogfile=/tmp/go-build330381427/b001/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true -test.run=TestNewHTTPConnection|TestSendHTTPRequest|Test.*HTTP .cfg 64/pkg/tool/linux_amd64/vet -D GOAMD64_v1 -gensymabis 64/pkg/tool/linux_amd64/vet s#\.�� 64/src/runtime/trace/annotation.-s .cfg ache/go/1.25.7/x64/pkg/tool/linu-buildmode=exe rsions$# d; base64 HEAD 64/pkg/tool/linu-o ache/go/1.25.7/x64/pkg/tool/linuHEAD` (dns block) > - Triggering command: `/tmp/go-build2307564073/b001/mcp.test /tmp/go-build2307564073/b001/mcp.test -test.testlogfile=/tmp/go-build2307564073/b001/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true -test.run=TestNewHTTPConnection.* 64/pkg/tool/linu-plugin-opt=/usr/libexec/gcc/x86_64-linux-gnu/13/lto-wrapper ache/go/1.25.7/x64/pkg/tool/linux_amd64/vet go syscall/unix/at.-d x_amd64/compile ache/go/1.25.7/x64/pkg/tool/linux_amd64/vet` (dns block) > > If you need me to access, download, or install something from one of these locations, you can either: > > - Configure [Actions setup steps](https://gh.io/copilot/actions-setup-steps) to set up my environment, which run before the firewall is enabled > - Add the appropriate URLs or hosts to the custom allowlist in this repository's [Copilot coding agent settings](https://github.com/github/gh-aw-mcpg/settings/copilot/coding_agent) (admins only) > > </details> <!-- START COPILOT ORIGINAL PROMPT --> <details> <summary>Original prompt</summary> ---- *This section details on the original issue you should resolve* <issue_title>[duplicate-code] Duplicate Code Pattern: HTTP Request Initialization in MCP Connection</issue_title> <issue_description># 🔍 Duplicate Code Pattern: HTTP Request Initialization *Part of duplicate code analysis: #892* ## Summary The `internal/mcp/connection.go` file contains **2 nearly identical instances** of HTTP request initialization workflow, differing only in the RPC method name. This pattern appears in both `initializeHTTPSession` and `sendHTTPRequest` methods. ## Duplication Details ### Pattern: HTTP Request Creation Workflow - **Severity**: Medium - **Occurrences**: 2 instances in connection.go - **Locations**: - `internal/mcp/connection.go` (lines 576-625) - initializeHTTPSession method - `internal/mcp/connection.go` (lines 678-726) - sendHTTPRequest method - **Code Sample**: ``````go // Duplicated pattern (appears 2 times with minor variations): // 1. Create JSON-RPC request request := createJSONRPCRequest(requestID, method, params) // 2. Marshal request body requestBody, err := json.Marshal(request) if err != nil { return ..., fmt.Errorf("failed to marshal request: %w", err) } // 3. Create HTTP request httpReq, err := setupHTTPRequest(ctx, c.httpURL, requestBody, c.headers) if err != nil { return ..., err } // 4. Execute HTTP request httpResp, err := c.httpClient.Do(httpReq) if err != nil { if isHTTPConnectionError(err) { return ..., fmt.Errorf("cannot connect to HTTP backend at %s: %w", c.httpURL, err) } return ..., fmt.Errorf("HTTP request failed: %w", err) } defer httpResp.Body.Close() // 5. Read response body responseBody, err := io.ReadAll(httpResp.Body) if err != nil { return ..., fmt.Errorf("failed to read response: %w", err) } // 6. Parse response (method-specific) ... `````` ## Impact Analysis - **Maintainability**: Any change to HTTP request handling (e.g., timeout logic, retry mechanism, logging) requires updating 2 locations - **Bug Risk**: Medium - If HTTP error handling is updated in one method but not the other, behavior becomes inconsistent - **Code Bloat**: ~35 lines of duplicated code (steps 1-5 above) - **Extensibility**: Adding new HTTP-based MCP methods would require copying this entire pattern again ## Refactoring Recommendations ### 1. **Extract Helper Function: executeHTTPRequest** - Create a shared helper function in `internal/mcp/connection.go`: ``````go // executeHTTPRequest executes an HTTP JSON-RPC request and returns the raw response body func (c *Connection) executeHTTPRequest(ctx context.Context, method string, params interface{}, requestID int) ([]byte, error) { // Create JSON-RPC request request := createJSONRPCRequest(requestID, method, params) // Marshal request body requestBody, err := json.Marshal(request) if err != nil { return nil, fmt.Errorf("failed to marshal %s request: %w", method, err) } // Create HTTP request httpReq, err := setupHTTPRequest(ctx, c.httpURL, requestBody, c.headers) if err != nil { return nil, err } // Execute HTTP request httpResp, err := c.httpClient.Do(httpReq) if err != nil { if isHTTPConnectionError(err) { return nil, fmt.Errorf("cannot connect to HTTP backend at %s: %w", c.httpURL, err) } return nil, fmt.Errorf("%s HTTP request failed: %w", method, err) } defer httpResp.Body.Close() // Read response body responseBody, err := io.ReadAll(httpResp.Body) if err != nil { return nil, fmt.Errorf("failed to read %s response: %w", method, err) } return responseBody, nil } `````` - Update both methods: ``````go // In initializeHTTPSession: responseBody, err := c.executeHTTPRequest(context.Background(), "initialize", initParams, requestID) if err != nil { return "", err } // ... parse response ... // In sendHTTPRequest: responseBody, err := c.executeHTTPRequest(ctx, method, params, requestID) if err != nil { return nil, err } // ... parse response ... `````` - Estimated effort: 2-3 hours - Benefits: - Single location for HTTP request execution logic - Easier to add retry logic, request/response logging, or timeouts - Consistent error messages across all HTTP operations - Reduces file from 1,011 lines to ~976 lines ### 2. **Future Enhancement: Add Request/Response Logging** - Once extracted, the helper function becomes an ideal place to add: - Debug logging for request/response payloads - Request timing metrics - Retry logic with exponential backoff - These enhancements would automatically apply to all HTTP operations ## Implementation Checklist - [ ] Review duplication findings with team - [ ] Create h... </details> <!-- START COPILOT CODING AGENT SUFFIX --> - Fixes #893
2 parents 891c160 + 69c2781 commit cce3e24

2 files changed

Lines changed: 106 additions & 100 deletions

File tree

internal/mcp/connection.go

Lines changed: 105 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,62 @@ func setupHTTPRequest(ctx context.Context, url string, requestBody []byte, heade
170170
return httpReq, nil
171171
}
172172

173+
// httpRequestResult contains the result of an HTTP request execution
174+
type httpRequestResult struct {
175+
StatusCode int
176+
ResponseBody []byte
177+
Header http.Header
178+
}
179+
180+
// executeHTTPRequest executes an HTTP JSON-RPC request and returns the response details.
181+
// This helper consolidates the common pattern of: create request → marshal → setup HTTP → execute → read response.
182+
// It handles connection errors consistently and provides method-specific error messages.
183+
// The headerModifier function allows callers to modify headers before the request is sent.
184+
func (c *Connection) executeHTTPRequest(ctx context.Context, method string, params interface{}, requestID uint64, headerModifier func(*http.Request)) (*httpRequestResult, error) {
185+
// Create JSON-RPC request
186+
request := createJSONRPCRequest(requestID, method, params)
187+
188+
// Marshal request body
189+
requestBody, err := json.Marshal(request)
190+
if err != nil {
191+
return nil, fmt.Errorf("failed to marshal %s request: %w", method, err)
192+
}
193+
194+
// Create HTTP request with standard headers
195+
httpReq, err := setupHTTPRequest(ctx, c.httpURL, requestBody, c.headers)
196+
if err != nil {
197+
return nil, err
198+
}
199+
200+
// Allow caller to modify headers (e.g., add session ID)
201+
if headerModifier != nil {
202+
headerModifier(httpReq)
203+
}
204+
205+
// Execute HTTP request
206+
httpResp, err := c.httpClient.Do(httpReq)
207+
if err != nil {
208+
// Check if it's a connection error (cannot connect at all)
209+
if isHTTPConnectionError(err) {
210+
return nil, fmt.Errorf("cannot connect to HTTP backend at %s: %w", c.httpURL, err)
211+
}
212+
return nil, fmt.Errorf("%s HTTP request failed: %w", method, err)
213+
}
214+
defer httpResp.Body.Close()
215+
216+
// Read response body
217+
responseBody, err := io.ReadAll(httpResp.Body)
218+
if err != nil {
219+
return nil, fmt.Errorf("failed to read %s response: %w", method, err)
220+
}
221+
222+
return &httpRequestResult{
223+
StatusCode: httpResp.StatusCode,
224+
ResponseBody: responseBody,
225+
Header: httpResp.Header,
226+
}, nil
227+
}
228+
173229
// NewConnection creates a new MCP connection using the official SDK
174230
func NewConnection(ctx context.Context, serverID, command string, args []string, env map[string]string) (*Connection, error) {
175231
logger.LogInfo("backend", "Creating new MCP backend connection, command=%s, args=%v", command, sanitize.SanitizeArgs(args))
@@ -573,42 +629,23 @@ func (c *Connection) initializeHTTPSession() (string, error) {
573629
},
574630
}
575631

576-
request := createJSONRPCRequest(requestID, "initialize", initParams)
577-
578-
requestBody, err := json.Marshal(request)
579-
if err != nil {
580-
return "", fmt.Errorf("failed to marshal initialize request: %w", err)
581-
}
582-
583-
logConn.Printf("Sending initialize request: %s", string(requestBody))
584-
585-
// Create HTTP request with standard headers
586-
httpReq, err := setupHTTPRequest(context.Background(), c.httpURL, requestBody, c.headers)
587-
if err != nil {
588-
return "", err
589-
}
632+
logConn.Printf("Sending initialize request")
590633

591634
// Generate a temporary session ID for the initialize request
592635
// Some backends may require this header even during initialization
593636
tempSessionID := fmt.Sprintf("awmg-init-%d", requestID)
594-
httpReq.Header.Set("Mcp-Session-Id", tempSessionID)
595-
logConn.Printf("Sending initialize with temporary session ID: %s", tempSessionID)
596637

597-
logConn.Printf("Sending initialize to %s", c.httpURL)
598-
599-
// Send request
600-
httpResp, err := c.httpClient.Do(httpReq)
638+
// Execute HTTP request with custom header modification
639+
result, err := c.executeHTTPRequest(context.Background(), "initialize", initParams, requestID, func(httpReq *http.Request) {
640+
httpReq.Header.Set("Mcp-Session-Id", tempSessionID)
641+
logConn.Printf("Sending initialize with temporary session ID: %s", tempSessionID)
642+
})
601643
if err != nil {
602-
// Check if it's a connection error (cannot connect at all)
603-
if isHTTPConnectionError(err) {
604-
return "", fmt.Errorf("cannot connect to HTTP backend at %s: %w", c.httpURL, err)
605-
}
606-
return "", fmt.Errorf("failed to send initialize request to %s: %w", c.httpURL, err)
644+
return "", err
607645
}
608-
defer httpResp.Body.Close()
609646

610647
// Capture the Mcp-Session-Id from response headers
611-
sessionID := httpResp.Header.Get("Mcp-Session-Id")
648+
sessionID := result.Header.Get("Mcp-Session-Id")
612649
if sessionID != "" {
613650
logConn.Printf("Captured Mcp-Session-Id from response: %s", sessionID)
614651
} else {
@@ -618,31 +655,25 @@ func (c *Connection) initializeHTTPSession() (string, error) {
618655
logConn.Printf("No Mcp-Session-Id in response, using temporary session ID: %s", sessionID)
619656
}
620657

621-
// Read response body
622-
responseBody, err := io.ReadAll(httpResp.Body)
623-
if err != nil {
624-
return "", fmt.Errorf("failed to read initialize response: %w", err)
625-
}
626-
627-
logConn.Printf("Initialize response: status=%d, body_len=%d, session=%s", httpResp.StatusCode, len(responseBody), sessionID)
658+
logConn.Printf("Initialize response: status=%d, body_len=%d, session=%s", result.StatusCode, len(result.ResponseBody), sessionID)
628659

629660
// Check for HTTP errors
630-
if httpResp.StatusCode != http.StatusOK {
631-
return "", fmt.Errorf("initialize failed: status=%d, body=%s", httpResp.StatusCode, string(responseBody))
661+
if result.StatusCode != http.StatusOK {
662+
return "", fmt.Errorf("initialize failed: status=%d, body=%s", result.StatusCode, string(result.ResponseBody))
632663
}
633664

634665
// Parse JSON-RPC response to check for errors
635666
// The response might be in SSE format (event: message\ndata: {...})
636667
// Try to parse as JSON first, if that fails, try SSE format
637668
var rpcResponse Response
638669

639-
if err := json.Unmarshal(responseBody, &rpcResponse); err != nil {
670+
if err := json.Unmarshal(result.ResponseBody, &rpcResponse); err != nil {
640671
// Try parsing as SSE format
641672
logConn.Printf("Initial JSON parse failed, attempting SSE format parsing")
642-
sseData, sseErr := parseSSEResponse(responseBody)
673+
sseData, sseErr := parseSSEResponse(result.ResponseBody)
643674
if sseErr != nil {
644675
// Include the response body to help debug what the server actually returned
645-
bodyPreview := string(responseBody)
676+
bodyPreview := string(result.ResponseBody)
646677
if len(bodyPreview) > 500 {
647678
bodyPreview = bodyPreview[:500] + "... (truncated)"
648679
}
@@ -674,84 +705,59 @@ func (c *Connection) sendHTTPRequest(ctx context.Context, method string, params
674705
params = ensureToolCallArguments(params)
675706
}
676707

677-
// Create JSON-RPC request
678-
request := createJSONRPCRequest(requestID, method, params)
679-
680-
requestBody, err := json.Marshal(request)
681-
if err != nil {
682-
return nil, fmt.Errorf("failed to marshal request: %w", err)
683-
}
684-
685-
// Create HTTP request with standard headers
686-
httpReq, err := setupHTTPRequest(ctx, c.httpURL, requestBody, c.headers)
687-
if err != nil {
688-
return nil, err
689-
}
690-
691-
// Add Mcp-Session-Id header with priority:
692-
// 1) Context session ID (if explicitly provided for this request)
693-
// 2) Stored httpSessionID from initialization
694-
var sessionID string
695-
if ctxSessionID, ok := ctx.Value(SessionIDContextKey).(string); ok && ctxSessionID != "" {
696-
sessionID = ctxSessionID
697-
logConn.Printf("Using session ID from context: %s", sessionID)
698-
} else if c.httpSessionID != "" {
699-
sessionID = c.httpSessionID
700-
logConn.Printf("Using stored session ID from initialization: %s", sessionID)
701-
}
702-
703-
if sessionID != "" {
704-
httpReq.Header.Set("Mcp-Session-Id", sessionID)
705-
} else {
706-
logConn.Printf("No session ID available (backend may not require session management)")
707-
}
708-
709708
logConn.Printf("Sending HTTP request to %s: method=%s, id=%d", c.httpURL, method, requestID)
710709

711-
// Send request using the reusable HTTP client
712-
httpResp, err := c.httpClient.Do(httpReq)
713-
if err != nil {
714-
// Check if it's a connection error (cannot connect at all)
715-
if isHTTPConnectionError(err) {
716-
return nil, fmt.Errorf("cannot connect to HTTP backend at %s: %w", c.httpURL, err)
710+
// Execute HTTP request with custom header modification for session ID
711+
result, err := c.executeHTTPRequest(ctx, method, params, requestID, func(httpReq *http.Request) {
712+
// Add Mcp-Session-Id header with priority:
713+
// 1) Context session ID (if explicitly provided for this request)
714+
// 2) Stored httpSessionID from initialization
715+
var sessionID string
716+
if ctxSessionID, ok := ctx.Value(SessionIDContextKey).(string); ok && ctxSessionID != "" {
717+
sessionID = ctxSessionID
718+
logConn.Printf("Using session ID from context: %s", sessionID)
719+
} else if c.httpSessionID != "" {
720+
sessionID = c.httpSessionID
721+
logConn.Printf("Using stored session ID from initialization: %s", sessionID)
717722
}
718-
return nil, fmt.Errorf("failed to send HTTP request to %s: %w", c.httpURL, err)
719-
}
720-
defer httpResp.Body.Close()
721723

722-
// Read response
723-
responseBody, err := io.ReadAll(httpResp.Body)
724+
if sessionID != "" {
725+
httpReq.Header.Set("Mcp-Session-Id", sessionID)
726+
} else {
727+
logConn.Printf("No session ID available (backend may not require session management)")
728+
}
729+
})
724730
if err != nil {
725-
return nil, fmt.Errorf("failed to read HTTP response: %w", err)
731+
return nil, err
726732
}
727733

728-
logConn.Printf("Received HTTP response: status=%d, body_len=%d", httpResp.StatusCode, len(responseBody))
734+
logConn.Printf("Received HTTP response: status=%d, body_len=%d", result.StatusCode, len(result.ResponseBody))
729735

730736
// Parse JSON-RPC response
731737
// The response might be in SSE format (event: message\ndata: {...})
732738
// Try to parse as JSON first, if that fails, try SSE format
733739
var rpcResponse Response
734740

735-
if err := json.Unmarshal(responseBody, &rpcResponse); err != nil {
741+
if err := json.Unmarshal(result.ResponseBody, &rpcResponse); err != nil {
736742
// Try parsing as SSE format
737743
logConn.Printf("Initial JSON parse failed, attempting SSE format parsing")
738-
sseData, sseErr := parseSSEResponse(responseBody)
744+
sseData, sseErr := parseSSEResponse(result.ResponseBody)
739745
if sseErr != nil {
740746
// If we have a non-OK HTTP status and can't parse the response,
741747
// construct a JSON-RPC error response with HTTP error details
742-
if httpResp.StatusCode != http.StatusOK {
743-
logConn.Printf("HTTP error status=%d, body cannot be parsed as JSON-RPC", httpResp.StatusCode)
748+
if result.StatusCode != http.StatusOK {
749+
logConn.Printf("HTTP error status=%d, body cannot be parsed as JSON-RPC", result.StatusCode)
744750
return &Response{
745751
JSONRPC: "2.0",
746752
Error: &ResponseError{
747753
Code: -32603, // Internal error
748-
Message: fmt.Sprintf("HTTP %d: %s", httpResp.StatusCode, http.StatusText(httpResp.StatusCode)),
749-
Data: json.RawMessage(responseBody),
754+
Message: fmt.Sprintf("HTTP %d: %s", result.StatusCode, http.StatusText(result.StatusCode)),
755+
Data: json.RawMessage(result.ResponseBody),
750756
},
751757
}, nil
752758
}
753759
// Include the response body to help debug what the server actually returned
754-
bodyPreview := string(responseBody)
760+
bodyPreview := string(result.ResponseBody)
755761
if len(bodyPreview) > 500 {
756762
bodyPreview = bodyPreview[:500] + "... (truncated)"
757763
}
@@ -762,14 +768,14 @@ func (c *Connection) sendHTTPRequest(ctx context.Context, method string, params
762768
if err := json.Unmarshal(sseData, &rpcResponse); err != nil {
763769
// If we have a non-OK HTTP status and can't parse the SSE data,
764770
// construct a JSON-RPC error response with HTTP error details
765-
if httpResp.StatusCode != http.StatusOK {
766-
logConn.Printf("HTTP error status=%d, SSE data cannot be parsed as JSON-RPC", httpResp.StatusCode)
771+
if result.StatusCode != http.StatusOK {
772+
logConn.Printf("HTTP error status=%d, SSE data cannot be parsed as JSON-RPC", result.StatusCode)
767773
return &Response{
768774
JSONRPC: "2.0",
769775
Error: &ResponseError{
770776
Code: -32603, // Internal error
771-
Message: fmt.Sprintf("HTTP %d: %s", httpResp.StatusCode, http.StatusText(httpResp.StatusCode)),
772-
Data: json.RawMessage(responseBody),
777+
Message: fmt.Sprintf("HTTP %d: %s", result.StatusCode, http.StatusText(result.StatusCode)),
778+
Data: json.RawMessage(result.ResponseBody),
773779
},
774780
}, nil
775781
}
@@ -781,14 +787,14 @@ func (c *Connection) sendHTTPRequest(ctx context.Context, method string, params
781787
// Check for HTTP errors after parsing
782788
// If we have a non-OK status but successfully parsed a JSON-RPC response,
783789
// pass it through (it may already contain an error field)
784-
if httpResp.StatusCode != http.StatusOK {
785-
logConn.Printf("HTTP error status=%d with valid JSON-RPC response, passing through", httpResp.StatusCode)
790+
if result.StatusCode != http.StatusOK {
791+
logConn.Printf("HTTP error status=%d with valid JSON-RPC response, passing through", result.StatusCode)
786792
// If the response doesn't already have an error, construct one
787793
if rpcResponse.Error == nil {
788794
rpcResponse.Error = &ResponseError{
789795
Code: -32603, // Internal error
790-
Message: fmt.Sprintf("HTTP %d: %s", httpResp.StatusCode, http.StatusText(httpResp.StatusCode)),
791-
Data: responseBody,
796+
Message: fmt.Sprintf("HTTP %d: %s", result.StatusCode, http.StatusText(result.StatusCode)),
797+
Data: result.ResponseBody,
792798
}
793799
}
794800
}

main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ var log = logger.New("main:main")
1313

1414
func main() {
1515
log.Print("Starting MCP Gateway application")
16-
16+
1717
// Build version string with metadata
1818
versionStr := buildVersionString()
1919
log.Printf("Built version string: %s", versionStr)

0 commit comments

Comments
 (0)