Repository navigation
feat: per-Dot MCP connections with owner approval for non-read-only tools - #52
Conversation
Dots can now use tools from remote MCP servers (Streamable HTTP, optional bearer token). Each Dot's settings list its connections and tools; each tool can be enabled or disabled and set to "Ask first". - Read-only tools (readOnlyHint) run directly. Every other tool starts as "Ask first": the model gets approval_required and must raise an in-chat approval card. Only the owner's approval route executes the call, after re-checking the conversation's Dot still has the tool, and each approval runs at most once with a restorable receipt. - Slack and headless runs cannot approve, so gated tools tell the Dot to continue in the web app. - Changing a Dot's connections aborts its active turn. - Tokens stay server-side; results are bounded and treated as untrusted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The card imports api.ts, which reads sessionStorage at load. The test only passed when another file had set one up first, so it failed when run alone or in a different worker order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jerelvelarde
left a comment
There was a problem hiding this comment.
MCP connections are a valuable template extension, but approval binding and permission races need fixing before merge. Existing owner/thread checks, token omission, headless gating, and intentional local-server support were reviewed. Validation: 175 tests, typecheck, lint, formatting, and the rerun production build passed. Both server findings were reproduced using SQLite and in-memory MCP transport.
| return () => { | ||
| active = false; | ||
| }; | ||
| }, [threadId, toolCallId, attempt]); |
There was a problem hiding this comment.
[P2] Allow running receipts to recover without another reload. Reopening a conversation while an approved action is running fetches the receipt once. After the server finishes, this card stays in the running state because there is no polling or refresh action for a successful running response. Poll running receipts or expose a refresh control.
There was a problem hiding this comment.
Fixed in 68b527a. While a receipt is running, the card polls it every 2 seconds and stops once it completes. The approve controls stay hidden during that time, and the card says the action is still running on the server.
I haven't added a unit test for the polling, because these tests render the card statically and have no DOM environment.
# Conflicts: # src/server/dot-agent.ts # tests/tanstack-agent.test.ts
…resh Review fixes for the MCP connections PR. - [P1] Approvals bind to a server-side record. A gated call stores its connection, real tool name, and arguments and returns an approvalId; request_connection_action carries only that id and a summary. The card shows the stored arguments, and the approval route runs exactly that record (never arguments sent with the approval), only from the same conversation, within an hour, and only if that exact tool is still enabled. Model-facing names are also assigned over all tools, so disabling one no longer moves its name to a look-alike. - [P2] Refresh merges discovered metadata with the owner's settings as they are when discovery finishes, so a revocation or Ask first change made mid-refresh is kept. - [P2] The approval card polls a running receipt until it completes. Regression tests cover the look-alike collision (send mail! / send mail?), argument injection, cross-thread and expired approvals, and the refresh race (fails on the previous logic, passes now). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@jerelvelarde Ready for another look whenever you have time. All three findings are addressed in 68b527a, with replies on each thread:
The branch is merged with current |
jerelvelarde
left a comment
There was a problem hiding this comment.
The per-Dot MCP connections and owner approval flow fit the template well, and the previous tool-alias and refresh races are fixed. The 37 focused tests pass. One receipt-binding issue remains: a completed action can be presented as approval of a different stored request. Full formatting, lint, typecheck, 267 tests, build, and GitHub CI pass.
| .parse(await c.req.json()); | ||
| const thread = workspace.requireThread(c.req.param('id')); | ||
| const previous = connections.store.action(thread.id, body.toolCallId); | ||
| if (previous?.result) return c.json(previous.result); |
There was a problem hiding this comment.
[P2] Bind recovered action results to the stored approval
This returns the previous result using only threadId/toolCallId, without checking that it belongs to body.approvalId. After approving a request for Alice, posting the same toolCallId with a different approval for Bob returns Alice's success. The GET receipt also lacks approvalId, so the actual card displays Bob's arguments as Approved and Continue conversation responds approved:true with Alice's result. Store the approval identity in the action receipt and reject mismatched recovery in both the route and card; retries of the original approval should still recover its result.
There was a problem hiding this comment.
Fixed in be38e2b.
- Receipts: each saved result now records the approval that produced it (
mcp_actions.approvalId, added with a migration). - Route:
POST /connection-actionsreturns a saved result only whenapprovalIdmatches. A different approval on the sametoolCallIdgets a 409 ("This action belongs to a different approval request.") and nothing runs. Retrying the original approval still recovers its result. - Card: the GET receipt now includes
approvalId. If it doesn't match the card's own approval, the card shows that the saved result belongs to a different request and offers no Approve or Continue.
Regression test: returns a saved result only for the approval that produced it. It reproduces your Alice/Bob case, and I checked that it fails without the new check. I also merged current main (291 tests, typecheck, lint, format and build pass). The card's mismatch path has no unit test, since these tests render it statically without a DOM environment.
# Conflicts: # src/client/style.css
Review fix: a saved action result was recovered by thread and tool call only, so posting the same toolCallId with a different approval returned the first approval's result, and the card showed it as approving the other request. - Receipts store their approvalId (migrated column). - The approval route returns a saved result only for the same approval; a different approval gets 409 and nothing runs. Retrying the original approval still recovers its result. - The GET receipt includes approvalId; the card ignores a receipt from a different approval, says so, and offers no approve/continue. Regression test reproduces the Alice/Bob case (fails without the check). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jerelvelarde
left a comment
There was a problem hiding this comment.
Per-Dot MCP connections fit the template, and this recheck confirms the remaining approval receipt identity issue is fixed. Server receipts now carry their approval identity; Alice/Bob replay is rejected with409, retries of the original request recover the saved result without rerunning, and mounted pending cards suppress mismatched results and Continue. Legacy receipts without identity fail closed. Earlier stable-tool-alias and refresh race regressions also pass. Verification at be38e2b: 13 focused tests and all291 repository tests pass; formatter, lint, typecheck and client/server production build pass. The pre-fix server failure was reproduced before verifying the fix, and mounted card fixtures cover matched, mismatched and legacy receipts. No new security blocker found; intentional owner-configured local MCP URLs remain supported. GitHub CI is green and this head includes current main1b2425d without conflicts. Live model/MCP interaction and browser narrow-screen/keyboard behavior were not rerun in this recheck; testing used credential-free in-memory MCP and React fixtures. Merge remains subject to fresh exact-head/current-main checks.
Workflow
A Dot can use tools from remote MCP servers, such as email, calendars, issue trackers, notes or the owner's own services. Read-only tools run on their own. Any tool that changes something waits for the owner to approve it in chat.
readOnlyHintstart with Ask first off. Every other tool starts with it on.approval_required. The Dot then raises arequest_connection_actionhuman-in-the-loop card showing the summary and the exact arguments, and Approve & run executes the call.Design notes for review
POST /api/conversations/:id/connection-actions. It checks that the conversation's Dot currently has that tool enabled, claims(threadId, toolCallId)once, and stores the result. Retries and double clicks return the stored result, and reopening the conversation restores the card from it. This mirrors the existingreview_space_pageflow.unavailableand the Dot is told to continue in the web app.hasTokenonly). Tool results are capped at 20k characters, and the system prompt marks them as untrusted.<connection>__<tool>), sanitized to^[a-zA-Z0-9_-]{1,64}$and de-duplicated.http(s)without embedded credentials. Local addresses are allowed on purpose, so the owner can run MCP servers on the same machine. This is noted in the docs.@modelcontextprotocol/sdkdependency. No new dependencies.Verification
npm run check-format,lint,typecheck,test(175 passing, 11 new) andbuildall pass.tests/connections.test.tsruns a realMcpServerover the SDK's in-memory transport. It covers:unavailabletests/tanstack-agent.test.tschecks that the model request includes connection tools, and includes the approval tool only when the web client offers it.tests/connection-card.test.tsx.Docs:
docs/CONNECTIONS.md, plus a README section and a row in the Features table.🤖 Generated with Claude Code