Skip to content

feat: per-Dot MCP connections with owner approval for non-read-only tools - #52

Open
jibraaan wants to merge 1 commit into
CopilotKit:mainfrom
jibraaan:pr/mcp-connections
Open

jibraaan wants to merge 1 commit into
CopilotKit:mainfrom
jibraaan:pr/mcp-connections

Conversation

@jibraaan

@jibraaan jibraaan commented Oct 3, 2026

Copy link
Copy Markdown

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.

  1. In a Dot's settings (Edit specialist), a new Connections section lets the owner add an MCP server: name, Streamable HTTP URL and an optional bearer token. OpenDots lists its tools.
  2. Each tool has an enabled switch and an Ask first switch. Tools the server marks readOnlyHint start with Ask first off. Every other tool starts with it on.
  3. In chat, the Dot calls read-only tools directly. For an Ask first tool, the tool returns approval_required. The Dot then raises a request_connection_action human-in-the-loop card showing the summary and the exact arguments, and Approve & run executes the call.

Design notes for review

  • The model never executes a gated tool. The only path that runs one is the owner route 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 existing review_space_page flow.
  • Slack and headless runs (scheduled tasks, voice compute) cannot show the card. There, gated tools return unavailable and the Dot is told to continue in the web app.
  • Changing a Dot's connections or tool settings aborts its active turn. This works the same way as the existing permission watcher.
  • Tokens are stored server-side in SQLite and never returned to the browser (hasToken only). Tool results are capped at 20k characters, and the system prompt marks them as untrusted.
  • Model-facing tool names are derived deterministically (<connection>__<tool>), sanitized to ^[a-zA-Z0-9_-]{1,64}$ and de-duplicated.
  • Endpoints must be 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.
  • Uses the existing @modelcontextprotocol/sdk dependency. No new dependencies.
  • Not included: OAuth-only MCP servers (documented), and stdio servers.

Verification

  • npm run check-format, lint, typecheck, test (175 passing, 11 new) and build all pass.
  • tests/connections.test.ts runs a real McpServer over the SDK's in-memory transport. It covers:
    • discovery and default gating
    • that the token is never returned
    • URL validation
    • name sanitizing and collisions
    • read-only tools running while gated tools never reach the server handler
    • headless runs reporting unavailable
    • owner changes applying mid-turn
    • an approved action running exactly once and its result being restored
    • refusing tools from another Dot or a thread this owner doesn't own
    • refresh keeping owner choices, and reporting unreachable servers
    • result normalization
  • tests/tanstack-agent.test.ts checks that the model request includes connection tools, and includes the approval tool only when the web client offers it.
  • Live, against a local Streamable HTTP MCP server with bearer auth: connecting, tool discovery, settings surviving refresh, and clear errors for a bad token and an unreachable host.
  • UI checked at 375px width (no horizontal overflow) and with the keyboard (Enter in the add-connection fields doesn't submit the surrounding Dot form; tab order follows the visual order).
  • Not yet verified: a live model turn that raises and approves the in-chat card, because no Intelligence or model keys were available. The card's initial render is covered by tests/connection-card.test.tsx.

Docs: docs/CONNECTIONS.md, plus a README section and a row in the Features table.

🤖 Generated with Claude Code

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant