-
Notifications
You must be signed in to change notification settings - Fork 15
fix: use server-issued session ID for HTTP backend tools/list #1559
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -15,89 +15,87 @@ import ( | |||||||||||||||||||||||||
| "github.com/github/gh-aw-mcpg/internal/mcp" | ||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // TestHTTPBackendInitialization tests that HTTP backends receive session ID during initialization | ||||||||||||||||||||||||||
| // TestHTTPBackendInitialization tests that HTTP backends use the session ID issued by the | ||||||||||||||||||||||||||
| // server during initialize (not a locally-fabricated one) when calling tools/list. | ||||||||||||||||||||||||||
| // This is a regression test for https://github.com/github/gh-aw/issues/18712 where | ||||||||||||||||||||||||||
| // gateway-issued fake session IDs overrode the real server-issued session ID, causing | ||||||||||||||||||||||||||
| // HTTP 400 on tools/list from strict backends like Datadog. | ||||||||||||||||||||||||||
| func TestHTTPBackendInitialization(t *testing.T) { | ||||||||||||||||||||||||||
| // Track whether the session ID header was received | ||||||||||||||||||||||||||
| var receivedSessionID string | ||||||||||||||||||||||||||
| var requestMethod string | ||||||||||||||||||||||||||
| const serverSessionID = "server-issued-session-42" | ||||||||||||||||||||||||||
| var toolsListSessionID string | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Create a mock HTTP MCP server that requires Mcp-Session-Id header | ||||||||||||||||||||||||||
| // Create a mock HTTP MCP server that: | ||||||||||||||||||||||||||
| // 1. Issues a specific session ID during initialize | ||||||||||||||||||||||||||
| // 2. Requires that exact session ID for subsequent requests | ||||||||||||||||||||||||||
| mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||||||||||||||||||||||||||
| receivedSessionID = r.Header.Get("Mcp-Session-Id") | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Parse the JSON-RPC request to get the method | ||||||||||||||||||||||||||
| var req struct { | ||||||||||||||||||||||||||
| Method string `json:"method"` | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| json.NewDecoder(r.Body).Decode(&req) | ||||||||||||||||||||||||||
| requestMethod = req.Method | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Simulate a backend that requires Mcp-Session-Id header | ||||||||||||||||||||||||||
| if receivedSessionID == "" { | ||||||||||||||||||||||||||
| w.WriteHeader(http.StatusBadRequest) | ||||||||||||||||||||||||||
| response := map[string]interface{}{ | ||||||||||||||||||||||||||
| switch req.Method { | ||||||||||||||||||||||||||
| case "initialize": | ||||||||||||||||||||||||||
| // Issue a specific session ID (as Datadog and other Streamable HTTP servers do) | ||||||||||||||||||||||||||
| w.Header().Set("Mcp-Session-Id", serverSessionID) | ||||||||||||||||||||||||||
| w.Header().Set("Content-Type", "application/json") | ||||||||||||||||||||||||||
| json.NewEncoder(w).Encode(map[string]interface{}{ | ||||||||||||||||||||||||||
| "jsonrpc": "2.0", | ||||||||||||||||||||||||||
| "error": map[string]interface{}{ | ||||||||||||||||||||||||||
| "code": -32600, | ||||||||||||||||||||||||||
| "message": "Invalid Request: Missing Mcp-Session-Id header", | ||||||||||||||||||||||||||
| "id": 1, | ||||||||||||||||||||||||||
| "result": map[string]interface{}{ | ||||||||||||||||||||||||||
| "protocolVersion": "2024-11-05", | ||||||||||||||||||||||||||
| "capabilities": map[string]interface{}{}, | ||||||||||||||||||||||||||
| "serverInfo": map[string]interface{}{"name": "test-server", "version": "1.0.0"}, | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| "id": 1, | ||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
| case "tools/list": | ||||||||||||||||||||||||||
| toolsListSessionID = r.Header.Get("Mcp-Session-Id") | ||||||||||||||||||||||||||
| // Reject requests with wrong or missing session ID (as strict backends do) | ||||||||||||||||||||||||||
| if toolsListSessionID != serverSessionID { | ||||||||||||||||||||||||||
| w.WriteHeader(http.StatusBadRequest) | ||||||||||||||||||||||||||
| json.NewEncoder(w).Encode(map[string]interface{}{ | ||||||||||||||||||||||||||
| "jsonrpc": "2.0", | ||||||||||||||||||||||||||
| "error": map[string]interface{}{"code": -32603, "message": "Invalid session ID"}, | ||||||||||||||||||||||||||
| "id": 1, | ||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| json.NewEncoder(w).Encode(response) | ||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Return a successful tools/list response | ||||||||||||||||||||||||||
| response := map[string]interface{}{ | ||||||||||||||||||||||||||
| "jsonrpc": "2.0", | ||||||||||||||||||||||||||
| "id": 1, | ||||||||||||||||||||||||||
| "result": map[string]interface{}{ | ||||||||||||||||||||||||||
| "tools": []map[string]interface{}{ | ||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||
| "name": "test_tool", | ||||||||||||||||||||||||||
| "description": "A test tool", | ||||||||||||||||||||||||||
| "inputSchema": map[string]interface{}{ | ||||||||||||||||||||||||||
| "type": "object", | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| w.Header().Set("Content-Type", "application/json") | ||||||||||||||||||||||||||
| json.NewEncoder(w).Encode(map[string]interface{}{ | ||||||||||||||||||||||||||
| "jsonrpc": "2.0", | ||||||||||||||||||||||||||
| "id": 1, | ||||||||||||||||||||||||||
| "result": map[string]interface{}{ | ||||||||||||||||||||||||||
| "tools": []map[string]interface{}{ | ||||||||||||||||||||||||||
| {"name": "test_tool", "description": "A test tool", "inputSchema": map[string]interface{}{"type": "object"}}, | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
| }) | |
| }) | |
| default: | |
| // Fail fast on unexpected JSON-RPC methods so tests are easier to diagnose. | |
| t.Errorf("unexpected JSON-RPC method %q", req.Method) | |
| w.WriteHeader(http.StatusBadRequest) | |
| w.Header().Set("Content-Type", "application/json") | |
| _ = json.NewEncoder(w).Encode(map[string]interface{}{ | |
| "jsonrpc": "2.0", | |
| "error": map[string]interface{}{"code": -32601, "message": "Unexpected method"}, | |
| "id": 1, | |
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
toolsListSessionIDis written from the httptest server handler goroutine and read from the main test goroutine without synchronization. This will trigger the Go race detector and can be flaky. Consider capturing the session ID via a buffered channel,sync.Mutex, oratomic.Valueand reading it afterNewUnifiedcompletes.