Skip to content

feat: bring your own MCP server to OpenMuse (replaces #88) - #221

Open
singhguri wants to merge 5 commits into
CopilotKit:mainfrom
singhguri:feat/mcp-connections-v2
Open

singhguri wants to merge 5 commits into
CopilotKit:mainfrom
singhguri:feat/mcp-connections-v2

Conversation

@singhguri

Copy link
Copy Markdown

Replaces #88, as suggested in #88 (comment). Same feature, rebuilt on current main, with the tool-authorization finding from the October 6 review fixed.

What changed

Owners can add their own MCP servers from Apps → MCP connections: Streamable HTTP, legacy SSE, or a local stdio command. OpenMuse discovers the tools, and the owner enables the ones the server marks read-only. Built-in chat and delegated tasks can then list connections and call enabled tools. Connection settings and OAuth material are encrypted server-side.

Tool authorization is checked against the server's current list, not the saved one. Before, call() checked the grant against the tool list saved at the last connect and then dispatched on a fresh connection. A tool the server had since re-marked as a write tool kept running. Now call() lists tools on its own connection before callTool. That list is saved, grants for tools that are missing or no longer read-only are dropped, and the call is refused. The check fails closed: if listing fails, nothing runs, and a dropped grant stays dropped until the owner reconnects and enables the tool again. (apps/server/src/mcp.ts, call())

Credential scoping from the first review is kept. Configured headers go only to the configured MCP origin, never to a /.well-known/ document or a separate OAuth origin, and a request that carries them refuses any redirect.

Integrated with current main:

  • @modelcontextprotocol/sdk uses main's ^1.30.0 (added for Parallel search) instead of the old 1.26.0 pin. The lockfile is main's, unchanged.
  • The chat and task prompts keep main's text; the MCP sentences are added to it.
  • read_calendar and the session tests from main sit beside the MCP tools and tests.
  • The README keeps the Public web search section, with MCP connections next to it.

Verification

  • New regression test, a call re-checks the server's current tool list and fails closed on a changed grant, over both Streamable HTTP and SSE: enable echo while read-only, flip it to write on the server with no reconnect, and call. The call is refused, the server records no dispatch, and the grant is removed. A tool the server stops listing is refused the same way. When both come back read-only they stay off until the owner reconnects and enables them. The test fails on the previous head with Missing expected rejection and passes with the fix.
  • tests/mcp.test.ts: 5/5 pass, including the separate-OAuth-origin and redirect tests, now on SDK 1.30.
  • pnpm typecheck passes. pnpm test: 523 passed, 3 skipped, 0 failed.
  • pnpm build:server and pnpm build:web pass.
  • biome check passes on every changed file. Full pnpm lint on my Windows checkout only reports CRLF in files this branch does not touch.

Integration limits

  • Read-only labels are the remote server's own annotations and are advisory. docs/MCP.md says so, and says that enabling a local stdio server grants it local command execution as the server user.
  • Write tools are listed but never run automatically. A reviewed write flow would be a separate change.
  • OAuth was exercised end to end with ElevenLabs only.

MCP connection with ElevenLabs tools expanded

Tools collapsed and the Add an MCP server form

singhguri and others added 5 commits November 4, 2026 16:41
Serve the same three tools in-process over Streamable HTTP (stateless)
and legacy SSE behind a required bearer header, then drive both through
McpService: custom headers reach the server, settings stay encrypted,
write-capable tools cannot be enabled, read-only calls succeed, and a
tool that loses its read-only annotation drops out of enabledTools on
the next connect.
The custom fetch handed to the SDK transports is reused for OAuth
discovery, registration and token requests, and global fetch follows
redirects, so configured headers could leave the configured server.

Decide per request URL: only the configured origin gets the headers,
never a /.well-known/ discovery document, and a request carrying them
is sent with redirect: "manual" and refused on any 3xx. OAuth traffic
to a separate authorization server goes through plain fetch.

Tests cover a separate OAuth origin (headers reach /mcp only, never
discovery or the authorization server) and a redirected request over
both Streamable HTTP and SSE (connect fails, the redirect target
receives nothing). Docs state that read-only annotations are advisory
and that a local command server grants local command execution.
The grant check used the tool list saved at the last connect, so a tool
the server had since marked as a write tool, or stopped listing, still ran.
call() now lists tools on its own connection first. discover() saves that
list and drops grants that no longer apply, and the call is refused before
callTool when the grant is gone. A failed listing fails the call.

The regression test covers both transports: a tool that turns write-
classified and a tool that disappears are refused with nothing dispatched,
their grants are dropped, and they stay off when the server lists them as
read-only again until the owner enables them.
@singhguri

Copy link
Copy Markdown
Author

@jerelvelarde This is the focused replacement for #88 that you suggested. call() now lists tools on its own connection before dispatch, drops grants for tools that are missing or no longer read-only, and refuses the call. If the listing fails, nothing runs. The new regression test reproduces your stale-annotation case on Streamable HTTP and SSE and shows nothing reaches the server. The credential scoping is unchanged, and the branch is rebuilt on current main using its SDK 1.30. Could you take another look?

This branch has not been deployed

No deployments
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