Repository navigation
prd phase 2.2 registry
Status: implemented. Technical design:
design-phase-2.2-registry.md.
Phase 2.1 lets users describe upstream MCP connections, but nothing actually dials them. Each coding tool (Claude Code, ZCode, Hermes) still has to manage its own MCP server list and repeat the same connection config across machines. There is no way for an agent tool to connect to Harness Nexus and get an aggregated set of tools from multiple upstreams in a single connection. Users also need a way to organize which upstreams a given connection should expose — not every tool should see every server.
A live McpRegistry that pools connections to every proxied upstream, resolves
their ${cred:NAME} placeholders into real values, and aggregates their tools
under namespaced keys. A proxy endpoint (/mcp Streamable HTTP + /mcp/sse SSE)
re-exposes the aggregated surface as a single MCP server that agent tools
authenticate to with a PAT. Profiles let users bundle a chosen set of MCP
servers; an agent tool connects with ?profile=<id> and sees only that profile's
aggregated tools, giving clean isolation.
- As an agent tool, I want to connect to one MCP endpoint and receive tools from multiple upstreams, so that I don't configure each server separately.
- As an agent tool, I want to authenticate with a PAT, so that I can connect non-interactively.
- As an agent tool, I want to choose a profile via a query parameter, so that I get only the tools I need.
- As a user, I want to create a profile that bundles specific MCP servers, so that an agent tool gets exactly that set.
- As a user, I want to create a personal profile, so that my bundles are private.
- As an admin, I want to create a global profile, so that everyone can connect through a shared bundle.
- As a non-admin, I want to be blocked from creating global profiles, so that shared bundles stay admin-controlled.
- As a user, I want a profile rejected if it references an MCP server I can't see, so that I don't build a broken bundle.
- As a user, I want to list profiles available to me, so that I know which ones my agent tool can use.
- As a user, I want to see the live connection status of each upstream, so that I know whether a bundle will actually work.
- As an operator, I want an unreachable upstream marked as errored and skipped, so that one bad server doesn't break the whole aggregate.
- As an operator, I want the registry to reconnect when I change a connection's config, so that edits take effect without a restart.
- As an operator, I want the registry to disconnect removed servers, so that stale connections don't linger.
- As a developer, I want tool names namespaced by origin, so that same-named tools from different upstreams don't collide.
- As an agent tool, I want Streamable HTTP supported, so that I can use the modern transport.
- As an agent tool, I want SSE supported, so that older clients still work.
- As a user, I want a connection without a profile rejected, so that I understand the profile is required.
- As a user, I want a connection to an inaccessible profile rejected, so that I can't see another user's bundle.
- As a user, I want the dashboard mesh to show real connection states, so that it's honest rather than decorative.
- As a developer, I want the registry decoupled from the HTTP routes, so that the same registry can serve multiple transports.
-
McpRegistry(server/src/mcp/registry.ts) owns a pool of SDKClientconnections (one perproxied: trueupstream). Lazy connect;reload()reconciles the pool on config change;shutdown()on app close. Decorated asapp.mcpRegistry. -
Aggregation is namespaced: tools surface as
<server-name>__<tool-name>;callToolsplits the namespace and routes to the owning client. -
${cred:NAME}placeholders resolved at connect time: the registry decrypts each referenced credential (looked up by name) and substitutes the plaintext into the transport's header values and URL. -
Best-effort: an errored upstream is skipped, never fatal.
getStatuses()returnsconnecting | connected | error | disconnected. -
Proxy mounts (
server/src/mcp/proxy.ts):/mcp(Streamable HTTP, with session id) and/mcp/sse+/mcp/sse/messages(legacy SSE). Both PAT-gated via apreHandler(the rootonRequesthook setsreq.user) requiring a?profile=<id>visible to the caller. Raw Node streams handed to the SDK transport viareply.hijack(). Per-sessionSdkMcpServerforwardstools/call. -
Explicit profile routing:
/mcp?profile=<id>exposes only the MCP servers in that profile's entries (kind === 'mcp';resourceId=McpServer.idin 2.2). Inaccessible profile entry →403 PROFILE_ENTRY_NOT_ACCESSIBLE. -
Profile CRUD (
server/src/modules/profiles.ts): scope rules identical to credentials/mcp-servers. Entry validation rejects inaccessible MCP servers (409 ENTRY_TARGET_NOT_ACCESSIBLE). -
Auto-reload: the mcp-servers REST handlers call
registry.reload()fire-and-forget after mutations.GET /api/mcp-servers/statusreturns live states (drives the dashboard mesh). -
Name clash: the SDK's
McpServerclass is imported asSdkMcpServer. -
Migration: SQLite v3 adds the
profilestable.
- HTTP smoke tests cover: profile CRUD, entry validation (bad reference → 409),
scope permission denials, profile detail, the status endpoint,
/mcpwithout a profile (400),/mcpwithout auth (401), profile delete. - Tests assert status codes and shapes (external behavior) against a memory-driver server.
- callable-function scripts (Phase 2.3).
- The stdio bridge entry for local tools.
-
kind:keyresource indirection in profile entries (2.2 usesMcpServer.id). - Per-PAT profile binding (2.2 uses query-param selection).
- Tool-level authorization (2.2 exposes all tools in the selected profile).
- Profile import/export (ECC/Superpower
importsfield).
- Agent tools connect via
GET /api/profiles(PAT-authenticated) to discover profiles, then/mcp?profile=<id>to use one. - Full technical design (registry internals, proxy wiring, profile routing,
API table) lives in
design-phase-2.2-registry.md.