feat(mcp): expose Extra as an MCP server over stdio - #137
Conversation
|
Thanks — this is a significant improvement over #126. The Principal API is now used correctly, the example is valid, cleanup is better, the stdio E2E coverage is useful, and CI is green. I still see three blockers before approving.
But the normal CLI runtime also injects: and passes: into So MCP still has a separate composition path where part of the runtime state may silently fall back to process-local implementations. I would strongly prefer reusing the same application/runtime composition used by
before starting.
The integration test actually performs Please make the command responsible for the same database initialization contract as the existing runtime and remove the test-side workaround.
But So a suspended run can look like an empty successful response to the MCP caller, and there is no way to understand or resume that pending approval. For the initial version, I think we need an explicit contract here. Either:
or:
What I don't want is a pending approval being silently represented as an empty answer. One additional design/security concern:
But Here the value is caller-controlled MCP input, so we should not describe or treat it as host-verified unless stdio mode explicitly defines the whole caller process as trusted. Please make that trust model explicit and avoid crossing the Principal boundary with an arbitrary unverified identifier. Once the runtime composition, migrations, HITL contract, and caller identity boundary are aligned with the rest of Extra, I think this will be much closer to approval. |
Thanks for the detailed review — I really appreciate the thorough feedback, especially the callouts about the trust boundary for caller identity and the silent fallback on the HITL path. Those were both gaps that would have bitten us in production, and I'm glad they surfaced now rather than later. I've pushed a follow-up commit that closes those gaps: the MCP path now wires up the same runtime pieces as the normal CLI flow, runs database migrations automatically before the server starts, and exposes the approval state explicitly over the MCP boundary with a supported resume flow. I also made sure the caller identity is treated as an anonymous local label rather than something host-verified, since stdio callers are external processes. The test suite is green and the widget TypeScript issue is fixed too — ready for another look when you have time. |
|
Thanks for the follow-up — the previous runtime composition, migration, identity-boundary, and initial HITL gaps are much better now. I still see a few blockers before approving.
but So this flow will fail: Please keep the caller identity consistent across the whole conversation / approval lifecycle. One option is to include Another, and probably cleaner for stdio v1, would be to define one stable identity for the MCP server connection/session instead of allowing identity to vary independently on each tool call.
when the result is suspended. But So this case is still lossy: The result serialization should be the same regardless of whether the I would strongly suggest extracting one shared
The current code does: That means a typo such as: is silently converted into a denial. At an external MCP boundary I would prefer invalid input to fail explicitly rather than turning malformed input into a real user decision. Please use the strict parsing behavior here and return a proper MCP/tool input error for unsupported decision values.
This is now a scope blocker. #137 currently includes commits and files related to the thumbs up/down feedback feature, including feedback persistence, widget changes, streamed assistant message IDs, and the widget bundle. Those changes belong to #138 and should not be part of the MCP server PR. Please clean the branch so #137 contains only the MCP server work and its directly required changes. The branch is also behind current The MCP implementation itself is much closer now. Once the identity lifecycle is consistent, chained approvals preserve the full pending state, invalid decisions are rejected explicitly, and the PR is cleaned back to its intended scope, I think this will be ready for a final review. |
f11fb7d to
f8a165e
Compare
Pushed fixes for all four blockers from the review: Identity consistency — decide_approval now takes user_id and builds the same Principal as extra_chat, so identity stays consistent across the approval lifecycle within a conversation. Ready for another look whenever you get a chance. |
|
Thanks — this is much closer now. The previous blockers around identity consistency, chained approvals, branch scope, and shared result serialization are addressed. I still see two things to fix before I can approve.
The MCP path now does its own mapping: But That module explicitly documents that free-text approval values from UI/API/CLI should be normalized in one place. For MCP, the strict behavior we want is already available by calling: without a default. Invalid input will then raise Please reuse that existing contract rather than introducing a second parser/mapping in
The implementation now addresses the bugs, but I don't see dedicated tests proving those regressions stay fixed. Please add coverage for at least:
These were real correctness issues, so I would like them captured directly in the test suite rather than relying only on the happy path. One final repo-state check: GitHub still shows this branch behind current main, so please update it against the latest main and rerun CI before the final review. After that, I think this should be ready for approval. |
|
Thanks for the follow-up review — both points are addressed: Reuse the existing approval parsing boundary — replaced the ad-hoc mapping in mcp_serve.py with parse_decision(decision) (no default), so invalid input raises InvalidDecision and is surfaced as an explicit tool error. The MCP path now uses the single parsing contract from agent_engine.approvals.decision. |
f8a165e to
a21c188
Compare
Adds
agentctl mcp serve --config ./agents.yml, which starts an MCP server (stdio transport) that exposes the full Extra agent graph as a singleextra_chattool.What changed
src/agentctl/mcp_serve.py— newExtraMCPServerclass that:LangGraphEngineonce at startup and reuses it for all requests,extra_chatFastMCP tool that capturesselfdirectly (no module-level mutable global),build()so a build failure still triggersclose(),Principalfor the caller, creates a session whensession_idis empty, and delegates to the existingConversationService.send,RunResult(answer,visited,used_tools,session_id) as an MCP tool response.src/agentctl/main.py— adds themcpClick group andmcp servesubcommand (wired beforeif __name__ == "__main__").tests/cli/test_mcp_serve_command.py— 9 tests covering:extra_chatcreates a new session whensession_idis empty,extra_chatreuses the suppliedsession_id,user_idfalls back to the stableext:<digest>principal,used_toolsare propagated from theRunResult,build()fails,examples/mcp-server/— minimal single-agent spec with a tinyechotool and aclient.pythat drives the server (also serves as living documentation of the wire format).Done when (all checked)
agentctl mcp serve --config ./agents.ymlstarts an MCP serverextra_chat