feat(execd): add embedded MCP proxy endpoint#1111
Conversation
Add /mcpproxy endpoint to execd that acts as an MCP gateway, proxying and aggregating multiple upstream MCP servers (stdio subprocess or HTTP) through a single streamable-http endpoint. Upstream servers are managed dynamically via REST API at /mcpproxy/upstreams. New package execd/pkg/mcpproxy with stdio/HTTP upstream transports, tool aggregation with conflict detection, session management, and JSON-RPC 2.0 routing. Includes unit tests, controller tests, and a smoke test using an inline mock MCP server. Windows compatible via platform-specific process management files.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e1e3eceff7
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
- Remove unused json import in smoke test - Add stderr warning to empty except clause - Fix data race on httpUpstream.sessionID with RWMutex - Move upstream Initialize/Tools outside manager lock to avoid blocking all proxy traffic during slow upstream registration - Handle all JSON-RPC notifications generically via IsNotification() instead of only notifications/initialized - Support SSE (text/event-stream) responses from HTTP upstreams - Set listChanged capability to false (no notifications sent yet) - Preserve tool annotations field in aggregated tools/list - Follow tools/list pagination (nextCursor) in both transports
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1361f7b09b
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
- Release manager lock before closing upstreams in RemoveUpstream to avoid blocking proxy traffic during slow upstream shutdown - Validate upstream names: alphanumeric, dot, hyphen, underscore, 1-64 chars, must start with alphanumeric - Cache empty tool lists using toolsFetched bool flag instead of nil check to avoid re-fetching on every rebuildToolList
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 98bfa04cbc
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| if resp.ID != nil { | ||
| return &resp, nil |
There was a problem hiding this comment.
Keep reading until the matching SSE response
For an HTTP upstream that sends a valid Streamable HTTP SSE response containing a server JSON-RPC request before the final response, this returns the first message with any id instead of the response for requestID. The current fresh evidence is that if resp.ID != nil still ignores requestID, so Initialize/Tools/CallTool can treat an upstream request as the result and either fail parsing or forward the wrong payload; compare the response id to the request id and continue past non-response messages.
Useful? React with 👍 / 👎.
| // When ID is nil the message is a notification (no response expected). | ||
| type Request struct { | ||
| JSONRPC string `json:"jsonrpc"` | ||
| ID any `json:"id,omitempty"` |
There was a problem hiding this comment.
Preserve JSON-RPC ids without float rounding
When a JSON-RPC client sends an integer id outside float64's exact range, encoding/json decodes this any field as a float64, so the proxy echoes a rounded id (for example, 9007199254740993 becomes 9007199254740992). Clients match responses by exact id and will ignore the rounded response; keep ids as json.RawMessage/json.Number or decode with UseNumber so they round-trip unchanged.
Useful? React with 👍 / 👎.
| key := normalizeID(resp.ID) | ||
| if ch, ok := s.pending.LoadAndDelete(key); ok { | ||
| ch.(chan *Response) <- &resp |
There was a problem hiding this comment.
Distinguish stdio requests from responses
For a stdio upstream that sends a server JSON-RPC request with an id while the proxy is waiting for initialize, tools/list, or tools/call, this loop unmarshals it as a Response and routes solely by id. If the upstream request id collides with a pending proxy id, the pending call is satisfied with a non-response object and can fail parsing or return the wrong result; parse the envelope and only deliver actual responses to pending.
Useful? React with 👍 / 👎.
| } | ||
|
|
||
| controller.InitCodeRunner() | ||
| controller.InitMCPProxy() |
There was a problem hiding this comment.
Close MCP upstreams when execd exits
When a stdio upstream is registered, it is started in its own process group so it will not receive terminal or SIGTERM signals sent to execd's process group, but this initialization has no matching shutdown path that calls Manager.Close(). If execd is stopped or restarted before each upstream is explicitly deleted, those MCP server subprocesses can keep running as orphans; wire the manager close path into main's shutdown handling.
Useful? React with 👍 / 👎.
Summary
/mcpproxyendpoint to execd that acts as an MCP gateway, proxying and aggregating multiple upstream MCP servers (stdio subprocess or HTTP) through a single MCP streamable-http endpoint/mcpproxy/upstreams(register, list, get, remove)execd/pkg/mcpproxy/with stdio/HTTP upstream transports, JSON-RPC 2.0 routing, and session managementprocess.go/process_windows.go)API Endpoints
/mcpproxy/mcpproxy/mcpproxy/mcpproxy/upstreams/mcpproxy/upstreams/mcpproxy/upstreams/:name/mcpproxy/upstreams/:nameTest plan
tests/smoke_mcpproxy.py) with inline mock stdio MCP server — full end-to-end flowGOOS=windows go buildexecd-test.ymlsmoke job (ubuntu + windows)