LuoYushi7 opened a new issue, #4758:
URL: https://github.com/apache/rocketmq-dashboard/issues/4758
### Summary
`rmqctl mcp-stdio` bridges a newline-delimited stdio MCP client to a Studio
MCP session over Streamable HTTP. It can only forward client-to-server
requests and server-to-client notifications today. Two frame shapes have no
path through the proxy, so a stdio MCP client and the Studio server cannot
complete any exchange that is not a plain client→server request.
Raised from #4638. Posting the design first, per the contribution guide's
"discuss before coding" rule for this track.
### Current state
* `stdioProxy.handleEvent` (`rmqctl/cmd/mcp_stdio.go`) sends **every** stdin
line to `session.SendMessage` and prints whatever comes back on stdout.
* `studio.decodeMCPMessage` (`rmqctl/internal/studio/mcp_message.go`) accepts
only frames that carry a `method`. A JSON-RPC **response** carries `id`
plus
`result`/`error` and no `method`, so it fails with
`invalid MCP JSON-RPC message: missing method`, which the proxy surfaces to
the caller as `-32603 Internal error`.
* The session exposes `SetNotificationHandler` (`mcp_http.go`) but no handler
for a **server-initiated request**, and no way to post a client response
back to the server.
Net effect: a server request (`sampling/createMessage`, `roots/list`) is
answered by the transport with `-32601 method not found` instead of reaching
the stdio client, and a client response never reaches the server, so the
server-side call waits for its timeout.
### Proposed design
**1. Classify stdin frames before forwarding.**
Extend the existing decode step to a three-way classification instead of
accepting only frames with a `method`:
| Frame | Shape | Action |
|---|---|---|
| request | `method` + `id` | `SendMessage` → stdout (today's behaviour) |
| notification | `method`, no `id` | `SendMessage` → no stdout (today's
behaviour) |
| response | `id` + `result`/`error`, no `method` | `SendResponse` → no
stdout |
Only the response case is new. Classifying in one place keeps stdin order and
the existing concurrency limits intact.
**2. Add a response path to the session.**
Add `SendResponse(ctx, payload) error` to `MCPClientSession`, and to the
`mcpSession` interface the proxy depends on (with the test fake extended to
match). It posts the frame to the same Streamable HTTP endpoint as a request,
but expects no JSON-RPC reply — the same shape as `SendNotification`, so it
can
reuse `sendWithReconnect` for session-terminated recovery.
**3. Add a server-request handler that forwards to stdout.**
Register a handler on the transport alongside the existing notification
handler. On a server request the proxy writes the frame to stdout so the
stdio
client can answer it. The answer comes back on stdin and is routed by (1) and
(2). Requests need to be tracked by id so a response can be matched, and an
in-flight server request must not be counted against `maxConcurrentCalls`,
which bounds client-originated calls only.
### Open questions
1. **Should a server request be forwarded while the initialize handshake is
still in flight?** The proxy deliberately preserves stdin order until
`initialized` is sent. Server requests can only arrive after that point in
practice, but I would rather state the ordering rule than assume it.
2. **What should happen to a response whose id matches no tracked request?**
Dropping it silently is cheap but hides a client bug; reporting it on
stderr
is observable but noisy. I lean towards a bounded stderr diagnostic.
3. **Is adding `SendResponse` to the session the right layer**, or should the
transport expose it? The session already owns reconnect and timeout
behaviour, so putting it there keeps that policy in one place.
4. **Do we need a bound on tracked server requests?** A server that opens
requests and never reads the answers would otherwise grow the map.
### Tests I would add
* A response frame from stdin reaches the session response path instead of
being sent as a request, and does not print to stdout.
* A server-initiated request is forwarded to stdout verbatim.
* A client answer to a forwarded request reaches the server response path
matched by id.
* Existing behaviour is unchanged: request/notification forwarding, the
initialize-order gate, and `maxConcurrentCalls`.
Happy to adjust the shape before I write any code — in particular on
questions 2 and 4, which change observable behaviour.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]