Skip to content
PricingBlog
✨ Markdown

McpRegistry

The MCP registry owns the live set of Model Context Protocol servers a runtime is connected to. It manages stdio subprocesses and HTTP clients, drives the OAuth state machine, and emits push-based snapshots so embedders can render UI without polling.

Source: @electric-ax/agents-mcp (re-exported as McpRegistry from @electric-ax/agents)

BuiltinAgentsServer.mcpRegistry exposes the instance that the embedded runtime owns; the usage guide walks through registering servers from agent-host code.

ts
interface Registry {
  addServer(cfg: McpServerConfig): Promise<AddServerResult>
  applyConfig(cfg: McpConfig): Promise<AddServerResult[]>
  removeServer(name: string): Promise<void>
  list(): ReadonlyArray<ListedEntry>
  get(name: string): Entry | undefined
  finishAuth(server: string, code: string, state?: string): Promise<AddServerResult>
  reauthorize(name: string): Promise<void>
  disable(name: string): Promise<void>
  enable(name: string): Promise<AddServerResult>
  subscribe(handler: RegistrySubscriber): () => void
}

Methods

MethodDescription
addServer(cfg)Register or reconfigure a single server. Idempotent on unchanged (name, url, transport, authMode, scopes, command, args).
applyConfig(cfg)Replace the full set of registered servers. Adds new ones, reconfigures changed ones, and removes anything not in cfg.
removeServer(name)Tear down a single server: close the transport, drop tokens from the in-memory cache, remove the entry.
list()Returns the current snapshot as a plain array — same shape as the servers field of RegistrySnapshot.
get(name)Internal lookup of a single entry, with the live transport handle and the resolved provider. Used by IPC handlers.
finishAuth(name, code, state?)Complete the OAuth authorization-code flow for an authenticating server. Called by the embedder after intercepting the redirect URI.
reauthorize(name)Force a fresh OAuth flow without removing the entry. Closes the transport, drops cached tokens (hooks remain registered), and rebuilds in place.
disable(name)Pause a server. Closes the transport but keeps the entry; tokens stay in the cache.
enable(name)Re-add a previously-disabled server using its last-known config.
subscribe(handler)Push-based view of registry state. The handler fires synchronously with a sentinel snapshot, then on every mutation. Returns an unsubscribe function.

addServer vs applyConfig

Both feed the same internal pipeline; pick by what you have:

  • addServer(cfg) — register one server. Use when you're adding an entry in response to a user action, a per-session tool, or a one-off integration.
  • applyConfig({ servers }) — replace the full set. Anything in the registry that isn't in cfg.servers is removed; existing entries with unchanged config are left alone (no transport churn). This is what the file-based loaders for mcp.json and the desktop settings.json compile down to.

Idempotency is the load-bearing property: editors save spuriously, file watchers fire double events on macOS, and most apps re-apply the same baseline on every restart. Calling applyConfig with the same shape twice does nothing the second time, so it's safe to wire to noisy upstreams.

AddServerResult

addServer and finishAuth return a discriminated union so the caller can react without inspecting the registry afterwards:

ts
type AddServerResult =
  | { state: "ready"; id: string; toolCount: number }
  | { state: "authenticating"; id: string; authUrl: string }
  | { state: "error"; id: string; error: McpToolError }
StateMeaning
readyConnected and tools listed; calls available at the next agent wake.
authenticatingOAuth required. authUrl is the URL to send the user to. The desktop's openAuthorizeUrl hook opens it in a sandboxed BrowserWindow automatically.
errorConnect, transport, or auth-config failure. error.kind and error.message describe what went wrong.

applyConfig returns one AddServerResult per server in the supplied config (in the same order).

Lifecycle of an entry

Every entry transitions through one of five statuses, surfaced on the snapshot. The states that matter to UI:

StatusMeaning
connectingTransport is being built (DCR, HTTPS discovery) or reconnecting.
authenticatingOAuth flow needed; authUrl is set, browser window is open.
readyTransport connected; tools listed and callable.
errorTransport or auth-config failure. The entry stays in list() so the UI can surface the failure.
disabledOperator paused the server via disable(name). Recoverable through enable(name).

Transitions are atomic with respect to subscribers: every state change fires a single snapshot in which the entry shows its new status. reauthorize mutates entries in place — the row never disappears from list(), even mid-rebuild, so renderers don't see a flicker.

subscribe(handler) and RegistrySnapshot

ts
type RegistrySubscriber = (snapshot: RegistrySnapshot) => void

interface RegistrySnapshot {
  seq: number
  servers: ReadonlyArray<ListedEntry>
}

Subscribing is the primary way to drive a UI off the registry. The first invocation is synchronous and carries seq: 0 as a sentinel — embedders treat it as the bootstrap snapshot, not part of the event stream. After that, every mutation increments seq (1, 2, 3, …) and broadcasts the full snapshot. A late subscriber still sees seq: 0 on its first delivery; emitted events continue from the registry's current counter.

Handlers must not throw. The registry catches exceptions per subscriber so a misbehaving consumer can't break the others, but the catch is a safety net, not a feature — log and swallow inside your handler.

ts
const off = registry.subscribe((snap) => {
  if (snap.seq === 0) {
    // bootstrap — render the initial list
  } else {
    // diff against the previous snapshot, or just re-render
  }
})
// ...
off()

ListedEntry

The shape of each servers[] entry inside a snapshot:

ts
interface ListedEntry {
  name: string
  status: McpServerStatus
  toolCount: number
  transport?: "http" | "stdio"
  authMode?: "none" | "apiKey" | "clientCredentials" | "authorizationCode"
  authUrl?: string
  error?: McpToolError
  tools: Array<{ name: string; description?: string; inputSchema: unknown }>
  capabilities?: unknown
}
FieldTypeDescription
namestringThe server's stable identifier.
statusMcpServerStatusCurrent lifecycle state — see the table above.
toolCountnumberNumber of tools the server advertises. 0 until status === "ready".
transport"http" | "stdio"The transport variant in use.
authModestringnone / apiKey / clientCredentials / authorizationCode. UI badges + "show Authorize" check use this.
authUrlstringSet while status === "authenticating". The URL to open for OAuth consent.
errorMcpToolErrorSet while status === "error".
toolsArray<{ name; description?; inputSchema }>Tool metadata as advertised by the server. Each becomes mcp__<server>__<tool> for the LLM.
capabilitiesunknownServer-declared MCP capabilities object (resources, prompts, etc.).

McpToolError

ts
interface McpToolError {
  kind: McpToolErrorKind
  message: string
  details?: unknown
}

type McpToolErrorKind =
  | "auth_unavailable"
  | "transport_error"
  | "timeout"
  | "server_error"
  | "tool_not_found"

The same shape surfaces on entry-level error (when addServer fails to connect) and on individual tool calls. See the usage guide's failure modes table.

RegistryOpts

BuiltinAgentsServer constructs the registry on your behalf. You only see this shape if you instantiate agents-mcp directly (e.g. from a custom embedder):

ts
interface RegistryOpts {
  publicUrl?: string
  openAuthorizeUrl?: (url: string, server: string) => void
}

function createRegistry(opts: RegistryOpts): Registry
FieldDescription
publicUrlBase URL used to construct the OAuth redirect_uri (full URI is <publicUrl>/oauth/callback/<server-name>). MUST be stable across restarts — DCR registers it with the auth server and persists it in the keychain, so a value that drifts forces re-authorization on every launch. Embedders that listen on an ephemeral port should pass a fixed loopback literal (the desktop uses http://127.0.0.1:53117); nothing actually listens at the URL — the embedder's BrowserWindow intercepts the redirect by prefix.
openAuthorizeUrlHook invoked when an authorizationCode server first needs consent. Receives the SDK-generated authorize URL. The desktop opens it in a sandboxed BrowserWindow; headless embedders can read the URL from the authenticating envelope of addServer and surface it themselves.

See also

  • MCP servers usage guide — the practical walkthrough of registering servers, OAuth, persistence, and the per-agent allowlist.
  • McpServerConfig — schema for the cfg argument to addServer / applyConfig.
  • BuiltinAgentsServer — host options that affect MCP, including extraMcpServers and openAuthorizeUrl.