Skip to content

feat: republish downstream MCP servers through Exeora - #54

Draft
usehoplite[bot] wants to merge 3 commits into
mainfrom
hoplite/naxos-24cf7039
Draft

usehoplite[bot] wants to merge 3 commits into
mainfrom
hoplite/naxos-24cf7039

Conversation

@usehoplite

@usehoplite usehoplite Bot commented Sep 3, 2026

Copy link
Copy Markdown

What this changes

Exeora can now be a client of other MCP servers and republish their tools through its own MCP endpoint. A machine that runs exeora connect launches the servers configured in <project root>/.exeora/mcp.json (project level) and mcp.json beside the CLI's own config (user level, same mcpServers shape the ecosystem already uses, stdio only), handshakes each one, and announces the tools it finds to the relay. The gateway stores one announcement per project in the relay Durable Object and registers the tools on the existing MCP endpoints under a prefixed name — mcp__<server>__<tool> — so one client connection reaches the project's files, its commands and its other MCP servers at once.

Everything downstream tools enjoy is everything the canonical tools already had: the same OAuth token and scopes, the same per-project policy (read_only honours the server's readOnlyHint, with a missing claim read conservatively as "changes something"), the same confirmation flow (input_required with a signed, argument-hash-bound state naming the republished tool), and the same audit log under the prefixed name. The account endpoint (/mcp) offers downstream tools only when the connection reaches exactly one project, because with several, one prefixed name could mean a different server on a different machine.

Surface, by package

  • packages/protocolmcp.ts (descriptors, McpServerTools, mcpToolName/parseMcpToolName, the mcp__ prefix), four shared limits (16 servers/project, 64 tools/server, 16 KB per input schema, 100 KB per announcement), new mcp.tools (executor→relay) and mcp.call (relay→executor) frames, and mcpPolicyAllows/needsMcpApproval. ApprovalRequestMessage.tool widened from the tool enum to a string so a state can name a downstream tool.
  • crates/exeora-cli — new src/mcp/ module: config load and merge (project wins on a name clash, broken entries warn instead of poisoning the file), a hand-rolled JSON-RPC 2.0 stdio client (initialize handshake offering 2025-06-18 and adopting whatever the server answers, paginated tools/list, tools/call, timeouts, cancellation), and a registry that owns the server processes for the life of connect and assembles the announcement under the byte budget. Contract types regenerated from the canonical schemas.
  • apps/gateway — announcement storage in the relay DO (relay-do-mcp.ts), registration of downstream tools (mcp-servers.ts), dispatch through the same resolve → policy → approval → relay → audit road as canonical calls (dispatch-mcp.ts), MCP confirmation (askToConfirmMcp), and wiring on both endpoints. Several modules were extracted to stay inside the 500-line budget.
  • apps/web/landing — a new docs/mcp page (configuration, how tools appear, policy, limits, security) plus navigation and readme entries.
  • .hoplite/settings.json — the sandbox setup installs the pinned Rust toolchain via mise and, best-effort, git-lfs: the trusted publication path needs the git-lfs binary, and a sandbox without it cannot push at all.

Compatibility

The change is additive and negotiated by presence, with no PROTOCOL_VERSION bump: an old gateway never learns mcp-v1 and offers zero downstream tools; an old CLI ignores mcp.tools/mcp.call frames it does not know. A server that fails to start announces an error and offers none of its tools rather than tools that fail; a tool whose schema does not fit the byte budget is reported, never silently truncated.

Also repairs a NUL byte that had been sitting in approval.ts on the base branch: git treated the file as binary, so no diff of it could be read or reviewed. The byte is now the : separator it was always meant to be.

Verification

  • cargo test --workspace95 passed, 0 failed (includes the 7 new Rust tests for the mcp client, config merge, and warning behaviour).
  • bun run test (vitest, node + workerd) — 540 passed, 0 failed across 49 files, including the new relay-do-mcp, mcp-servers, and protocol mcp suites.
  • bun run typecheck, biome check . (repo-pinned 2.5.10), and bun run check:file-length (227 files, max 500) all clean.
  • bun run --cwd apps/web build succeeds with the new docs page.
  • The gateway tests require the placeholder apps/gateway/.dev.vars that CI's "Prepare test secrets" step writes; this sandbox now has the same file.
  • One pre-existing failure was investigated to closure: oauth/cimd.workers.test.ts fails without that .dev.vars file (no identity provider configured → 500 on the authorize page), on the clean base branch as well; with the CI-equivalent file in place it passes.
  • The new docs page is verified by the successful production build; no pixel-level or live-browser verification was performed in this session.

Open in Hoplite

usehoplite Bot and others added 3 commits September 3, 2026 06:08
Exeora becomes an MCP client of the servers a project or a user configures
(.exeora/mcp.json at the project root, mcp.json beside the CLI's config):
connect launches each stdio server, handshakes it and announces its tools to
the relay, which keeps them per project and offers them on the same MCP
endpoints under mcp__<server>__<tool> — same OAuth token, same policy, same
approval flow, same audit log. The wire surface is additive (mcp.tools and
mcp.call frames, the mcp-v1 feature flag), so an old gateway offers none of
the downstream tools and an old CLI ignores the new frames, with no protocol
version bump.

Also repairs a NUL byte that had been sitting in approval.ts since before
this branch: git treated the file as binary, so no diff of it could be read.

Co-authored-by: Leynier Gutiérrez González <leynier41@gmail.com>
The trusted publication path runs git lfs ls-files before pushing, and a
sandbox without the git-lfs binary cannot publish at all. The install is
best-effort so a mirror without the package never takes setup down with it.

Co-authored-by: Leynier Gutiérrez González <leynier41@gmail.com>
Run rustfmt over the new mcp files and replace the manual max/min
bounds in connection.rs with clamp(), as clippy's manual_clamp lint
requires.

Co-authored-by: Leynier Gutiérrez González <leynier41@gmail.com>
@usehoplite
usehoplite Bot force-pushed the hoplite/naxos-24cf7039 branch from 94c4244 to f7ed1a0 Compare September 3, 2026 06:08
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.

0 participants