@electric-ax/agents 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,18 +2,18 @@
2
2
  title: Embedded built-ins
3
3
  titleTemplate: "... - Electric Agents"
4
4
  description: >-
5
- Embed the built-in Horton, worker, and coder runtime in your own process using
5
+ Embed the built-in Horton and worker runtime in your own process using
6
6
  @electric-ax/agents, BuiltinAgentsServer, or the entrypoint helpers.
7
7
  outline: [2, 3]
8
8
  ---
9
9
 
10
10
  # Embedded built-ins
11
11
 
12
- The CLI commands `electric agents start-builtin` and `electric agents quickstart` run the built-in Horton, worker, and coder runtime for you. If you need to host those built-ins inside your own process, use the exported APIs from `@electric-ax/agents`.
12
+ The CLI commands `electric agents start-builtin` and `electric agents quickstart` run the built-in Horton and worker runtime for you. If you need to host those built-ins inside your own process, use the exported APIs from `@electric-ax/agents`.
13
13
 
14
14
  ## BuiltinAgentsServer
15
15
 
16
- `BuiltinAgentsServer` starts an HTTP webhook server, registers `horton`, `worker`, and `coder`, and forwards Electric Agents webhook wakes to the built-in handler.
16
+ `BuiltinAgentsServer` starts an HTTP webhook server, registers `horton` and `worker`, and forwards Electric Agents webhook wakes to the built-in handler.
17
17
 
18
18
  ```ts
19
19
  import { BuiltinAgentsServer } from "@electric-ax/agents"
@@ -49,19 +49,30 @@ interface BuiltinAgentsServerOptions {
49
49
  mockStreamFn?: StreamFn
50
50
  webhookPath?: string
51
51
  createElectricTools?: CreateElectricTools
52
+ // MCP integration
53
+ extraMcpServers?: ReadonlyArray<McpServerConfig>
54
+ loadProjectMcpConfig?: boolean
55
+ mcpOAuthRedirectBase?: string
56
+ openAuthorizeUrl?: (url: string, server: string) => void
57
+ onConfigError?: (error: unknown) => void
52
58
  }
53
59
  ```
54
60
 
55
- | Field | Description |
56
- | --------------------- | --------------------------------------------------------------------------- |
57
- | `agentServerUrl` | Electric Agents coordinator server URL. |
58
- | `baseUrl` | Public base URL used when registering the webhook. Defaults to local URL. |
59
- | `port` | Local webhook server port. |
60
- | `host` | Bind host. Defaults to `127.0.0.1`. |
61
- | `workingDirectory` | Directory used by Horton, worker file tools, and the default coder cwd. Defaults to `process.cwd()`. |
62
- | `mockStreamFn` | Optional test stream function. Lets you run without `ANTHROPIC_API_KEY`. |
63
- | `webhookPath` | Webhook path. Defaults to `/_electric/builtin-agent-handler`. |
64
- | `createElectricTools` | Optional factory for extra tools injected into built-in agent handlers. |
61
+ | Field | Description |
62
+ | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
63
+ | `agentServerUrl` | Electric Agents coordinator server URL. |
64
+ | `baseUrl` | Public base URL used when registering the webhook. Defaults to local URL. |
65
+ | `port` | Local webhook server port. |
66
+ | `host` | Bind host. Defaults to `127.0.0.1`. |
67
+ | `workingDirectory` | Directory used by Horton and worker file tools. Defaults to `process.cwd()`. |
68
+ | `mockStreamFn` | Optional test stream function. Lets you run without `ANTHROPIC_API_KEY`. |
69
+ | `webhookPath` | Webhook path. Defaults to `/_electric/builtin-agent-handler`. |
70
+ | `createElectricTools` | Optional factory for extra tools injected into built-in agent handlers. |
71
+ | `extraMcpServers` | MCP servers contributed by the embedder. On name conflict with `mcp.json`, `mcp.json` wins. `authorizationCode` servers are auto-wired with `keychainPersistence`. |
72
+ | `loadProjectMcpConfig` | Load `<workingDirectory>/mcp.json` (and watch it). Off by default — stdio MCP servers can spawn local commands, so the embedder must opt in. The Electron desktop and `electric-ax` CLI opt in. |
73
+ | `mcpOAuthRedirectBase` | Base for OAuth redirect URIs (full URI is `<base>/oauth/callback/<server-name>`). MUST be stable across restarts so DCR client info stays valid; required when listening on `port: 0`. The runtime never listens at this URI — the embedder intercepts the redirect. |
74
+ | `openAuthorizeUrl` | Hook invoked when an `authorizationCode` MCP server first needs user 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. |
75
+ | `onConfigError` | Invoked when applying an MCP config (initial boot or watcher reload) fails. Errors are always logged; this hook is for surfacing them programmatically. |
65
76
 
66
77
  Without `mockStreamFn`, `ANTHROPIC_API_KEY` must be present before the built-in handler starts.
67
78
 
@@ -160,7 +171,7 @@ Environment variables:
160
171
  | `ELECTRIC_AGENTS_BUILTIN_BASE_URL` | Public webhook base URL for the built-in server. |
161
172
  | `ELECTRIC_AGENTS_BUILTIN_HOST` | Bind host. |
162
173
  | `ELECTRIC_AGENTS_BUILTIN_PORT` | Built-in server port. Defaults to `4448`. |
163
- | `ELECTRIC_AGENTS_WORKING_DIRECTORY` | Working directory for file tools and default coder sessions. |
174
+ | `ELECTRIC_AGENTS_WORKING_DIRECTORY` | Working directory for file tools. |
164
175
 
165
176
  ## Built-in Agent APIs
166
177
 
@@ -170,7 +181,6 @@ The built-in agent exports are also available if you want to compose your own ru
170
181
  | ------------------------- | --------------------------------------------------- |
171
182
  | `registerHorton()` | Register the `horton` type on an `EntityRegistry`. |
172
183
  | `registerWorker()` | Register the `worker` type on an `EntityRegistry`. |
173
- | `registerCodingSession()` | Register the `coder` type on an `EntityRegistry`. |
174
184
  | `HORTON_MODEL` | Default model id used by Horton and worker. |
175
185
  | `buildHortonSystemPrompt()` | Build Horton's system prompt for a working directory. |
176
186
  | `createHortonTools()` | Create Horton's base shell/file/search/worker tools. |
@@ -178,4 +188,4 @@ The built-in agent exports are also available if you want to compose your own ru
178
188
  | `WORKER_TOOL_NAMES` | Valid primitive tool names for workers. |
179
189
  | `createHortonDocsSupport()` | Create Horton's docs knowledge-base support. |
180
190
 
181
- For the behavior of `horton`, `worker`, and `coder`, see [Horton](../entities/agents/horton), [Worker](../entities/agents/worker), and [Coder](../entities/agents/coder).
191
+ For the behavior of `horton` and `worker`, see [Horton](../entities/agents/horton) and [Worker](../entities/agents/worker).
@@ -0,0 +1,354 @@
1
+ ---
2
+ title: MCP servers
3
+ titleTemplate: "... - Electric Agents"
4
+ description: >-
5
+ Connect agents to external tools, resources, and prompts via the
6
+ Model Context Protocol. Register servers programmatically through the
7
+ Registry API, declaratively in mcp.json, or globally in the desktop
8
+ app's settings.
9
+ outline: [2, 3]
10
+ ---
11
+
12
+ # MCP servers
13
+
14
+ The runtime ships an embedded **MCP registry** that connects agents to external [Model Context Protocol](https://modelcontextprotocol.io) servers — both locally-spawned `stdio` servers and remote `Streamable HTTP` servers. Tools, resources, and prompts exposed by those servers become available to every entity at the next wake without per-agent wiring.
15
+
16
+ ## Registering servers
17
+
18
+ `Registry` is the primary API. Agent authors call into it directly when they're defining or hosting agents in code. `mcp.json` and the desktop app's `settings.json` are file-based convenience layers that the runtime turns into the same `Registry.applyConfig()` calls under the hood.
19
+
20
+ ### Programmatic — `Registry.addServer()` / `applyConfig()`
21
+
22
+ `BuiltinAgentsServer` exposes the registry through `mcpRegistry`. Add servers from code anywhere it's the right shape — at boot from your own config source, in response to user actions, or per-session for tools an agent should only see during a specific task:
23
+
24
+ ```ts
25
+ import { BuiltinAgentsServer } from "@electric-ax/agents"
26
+
27
+ const server = new BuiltinAgentsServer({
28
+ agentServerUrl: "http://localhost:4437",
29
+ port: 4448,
30
+ workingDirectory: process.cwd(),
31
+ })
32
+
33
+ await server.start()
34
+
35
+ const result = await server.mcpRegistry?.addServer({
36
+ name: "stripe",
37
+ transport: "http",
38
+ url: "https://mcp.stripe.com/mcp",
39
+ auth: {
40
+ mode: "apiKey",
41
+ headerName: "Authorization",
42
+ key: process.env.STRIPE_MCP_KEY!,
43
+ },
44
+ })
45
+ ```
46
+
47
+ `addServer` returns a discriminated [`AddServerResult`](#addserverresult) — `{ state: "ready" | "authenticating" | "error", … }`. The state landscape is described in [Server states](#server-states) below; the full lifecycle (hot-reload, reauthorize, timeouts) lives in [Lifecycle](#lifecycle).
48
+
49
+ The bulk methods are:
50
+
51
+ - `applyConfig(cfg)` — replace the full set of servers. Idempotent on unchanged entries; removes anything not in the supplied config. This is what file-based config layers compile down to.
52
+ - `subscribe(handler)` — push-based view of the live state, including `ready` / `authenticating` / `error` transitions. Useful when an embedder renders its own UI on top of the registry.
53
+ - `reauthorize(name)`, `disable(name)`, `enable(name)`, `removeServer(name)` — single-server lifecycle.
54
+
55
+ Static secrets (`apiKey.key`, `clientCredentials.clientId` / `clientSecret`) are passed inline at the call site — typically read from `process.env`. The runtime never reads environment variables on the embedder's behalf. See [`McpServerConfig`](/docs/agents/reference/mcp-server-config) for the full schema.
56
+
57
+ ### File-based — `mcp.json`
58
+
59
+ For static, project-scoped configuration the runtime can load `mcp.json` from the configured `workingDirectory`, watch it for changes, and hot-reload adds, removes, and reconfigurations through `applyConfig` — exactly as if you'd called the API yourself. In-flight tool calls finish on the old config; new calls pick up the new one.
60
+
61
+ `mcp.json` loading is opt-in: stdio MCP servers spawn local commands, so picking a working directory must not auto-execute config from it. The Electron desktop and the `electric-ax` CLI opt in by default. Library embedders that construct `BuiltinAgentsServer` directly enable it with `loadProjectMcpConfig: true` (which loads `<workingDirectory>/mcp.json` and watches it).
62
+
63
+ `mcp.json` carries structural shape only — no secrets:
64
+
65
+ ```jsonc
66
+ {
67
+ "servers": {
68
+ "honeycomb": {
69
+ "transport": "http",
70
+ "url": "https://mcp.honeycomb.io/mcp",
71
+ "auth": {
72
+ "mode": "authorizationCode",
73
+ "scopes": ["mcp:read", "mcp:write"]
74
+ }
75
+ },
76
+ "internal-api": {
77
+ "transport": "http",
78
+ "url": "https://api.example.com/mcp",
79
+ "auth": {
80
+ "mode": "apiKey",
81
+ "headerName": "X-Api-Key"
82
+ }
83
+ },
84
+ "git-local": {
85
+ "transport": "stdio",
86
+ "command": "npx",
87
+ "args": [
88
+ "-y",
89
+ "@modelcontextprotocol/server-git",
90
+ "--repository",
91
+ "${workspaceRoot}"
92
+ ]
93
+ }
94
+ }
95
+ }
96
+ ```
97
+
98
+ For [`authorizationCode`](#authorization-code-oauth) servers in `mcp.json`, the runtime auto-wires `keychainPersistence` so OAuth tokens survive process restarts via the OS keychain.
99
+
100
+ ### Desktop settings layer
101
+
102
+ The Electron desktop app exposes a second file-based layer: a global `mcp.servers` block in its `settings.json`, applied to every workspace. The shape mirrors `mcp.json` — keyed by server name — so entries can be copy-pasted between the two files. It composes with the workspace `mcp.json` instead of replacing it:
103
+
104
+ - Servers from both files load together when their names don't collide.
105
+ - On a name collision, the workspace `mcp.json` wins (project scope overrides global).
106
+ - `keychainPersistence` is auto-wired for OAuth servers from either source.
107
+
108
+ The `settings.json` lives at:
109
+
110
+ | OS | Path |
111
+ | ------- | ------------------------------------------------------- |
112
+ | macOS | `~/Library/Application Support/Electric Agents/` |
113
+ | Linux | `~/.config/Electric Agents/` |
114
+ | Windows | `%APPDATA%\Electric Agents\` |
115
+
116
+ Example shape:
117
+
118
+ ```jsonc
119
+ {
120
+ "servers": [...],
121
+ "activeServer": {...},
122
+ "workingDirectory": "/Users/me/workspace/foo",
123
+ "apiKeys": {...},
124
+ "mcp": {
125
+ "servers": {
126
+ "linear": {
127
+ "transport": "http",
128
+ "url": "https://mcp.linear.app/sse",
129
+ "auth": { "mode": "authorizationCode", "scopes": ["mcp:read"] }
130
+ }
131
+ }
132
+ }
133
+ }
134
+ ```
135
+
136
+ Programmatic embedders (other than the desktop) pass the resolved set as an array via `BuiltinAgentsServer({ extraMcpServers })` — that's the in-memory shape `settings.json` is rewritten into when the desktop loads it.
137
+
138
+ ## Per-agent allowlist
139
+
140
+ Entity definitions opt into MCP servers explicitly via the `mcp.tools()` helper from `@electric-ax/agents-runtime`:
141
+
142
+ ```ts
143
+ import { mcp } from "@electric-ax/agents-runtime"
144
+
145
+ registry.define("research-agent", {
146
+ async handler(ctx) {
147
+ ctx.useAgent({
148
+ systemPrompt: "...",
149
+ tools: [
150
+ ...ctx.electricTools,
151
+ ...mcp.tools(["sentry", "github"]), // explicit list
152
+ // or: ...mcp.tools() // every registered server
153
+ ],
154
+ })
155
+ await ctx.agent.run()
156
+ },
157
+ })
158
+ ```
159
+
160
+ The resolved tool set is recorded in the agent's manifest at compose time. Tools are exposed to the model with always-prefixed names matching Anthropic's tool-name regex (`^[a-zA-Z0-9_-]{1,128}$`):
161
+
162
+ - Tools: `mcp__sentry__search`, `mcp__github__create_issue`, …
163
+ - Resources: `mcp__<server>__list_resources`, `mcp__<server>__read_resource`
164
+ - Prompts: `mcp__<server>__list_prompts`, `mcp__<server>__get_prompt`
165
+
166
+ Built-in entities `horton` and `worker` opt in to all registered servers via `mcp.tools()`.
167
+
168
+ ## Auth modes
169
+
170
+ Each server declares one auth mode. The runtime keeps a valid token in hand on every call: silent refresh when possible, or a structured `auth_unavailable` error to the agent's model when not.
171
+
172
+ ### `apiKey`
173
+
174
+ ```ts
175
+ auth: {
176
+ mode: "apiKey",
177
+ key: process.env.X_API_KEY!,
178
+ headerName: "X-Api-Key", // default "Authorization"
179
+ valuePrefix: "Bearer ", // optional
180
+ }
181
+ ```
182
+
183
+ The header is sent on every request. Rotate by editing the config; the registry's idempotency check picks up the change and rebuilds the transport on the next reload.
184
+
185
+ ### `clientCredentials`
186
+
187
+ ```ts
188
+ auth: {
189
+ mode: "clientCredentials",
190
+ tokenUrl: "https://auth.example.com/oauth/token",
191
+ clientId: process.env.X_CLIENT_ID!,
192
+ clientSecret: process.env.X_CLIENT_SECRET!,
193
+ scopes: ["mcp:read"],
194
+ }
195
+ ```
196
+
197
+ The runtime exchanges the client credentials for short-lived access tokens silently. No user interaction.
198
+
199
+ ### `authorizationCode` (OAuth)
200
+
201
+ ```ts
202
+ auth: {
203
+ mode: "authorizationCode",
204
+ scopes: ["mcp:read"],
205
+ // optional — pre-registered OAuth client (skips DCR)
206
+ client: { clientId: "...", clientSecret: "..." },
207
+ // optional — pre-existing tokens (skips OAuth flow on boot)
208
+ tokens: { accessToken: "...", refreshToken: "...", expiresAt: 1736e9 },
209
+ // fires on initial auth + every refresh — wire to your persistence
210
+ onTokensChanged: async (t) => { /* persist */ },
211
+ // fires once after RFC 7591 DCR completes
212
+ onClientRegistered: async (c) => { /* persist */ },
213
+ }
214
+ ```
215
+
216
+ The MCP SDK handles PKCE, RFC 7591 Dynamic Client Registration, RFC 9728 Protected Resource Metadata discovery, and 401-retry transparently. The first time a server is used:
217
+
218
+ 1. The runtime captures an authorize URL and surfaces it through the `openAuthorizeUrl(url, server)` hook on `BuiltinAgentsServer`.
219
+ 2. The Electron desktop opens the URL in a sandboxed `BrowserWindow` and intercepts the `redirect_uri` navigation client-side — the redirect URL is never actually fetched, so no HTTP listener is needed.
220
+ 3. The runtime exchanges the captured `code` + `state` for tokens and fires `onTokensChanged`.
221
+
222
+ Subsequent restarts re-seed from persisted tokens; refresh-token rotation happens silently on every call.
223
+
224
+ The redirect URI registered with the auth server during DCR is `<mcpOAuthRedirectBase>/oauth/callback/<server-name>`. Embedders that listen on an ephemeral port (the desktop runs on `port: 0`) MUST pass a stable `mcpOAuthRedirectBase` to `BuiltinAgentsServer` — otherwise the cached DCR client info goes stale on every restart and users have to re-authorize every launch. The desktop sets it to a fixed loopback literal (`http://127.0.0.1:53117`) per RFC 8252 §7.3; nothing actually listens at the port. Headless embedders that use port 0 with persisted credentials need to do the same.
225
+
226
+ #### Persistence helpers
227
+
228
+ `@electric-ax/agents-mcp` ships two opt-in helpers that produce the auth-config slice:
229
+
230
+ ```ts
231
+ import { keychainPersistence, filePersistence } from "@electric-ax/agents-mcp"
232
+
233
+ const honeycomb = await keychainPersistence({ server: "honeycomb" })
234
+
235
+ await mcpRegistry.addServer({
236
+ name: "honeycomb",
237
+ transport: "http",
238
+ url: "https://mcp.honeycomb.io/mcp",
239
+ auth: {
240
+ mode: "authorizationCode",
241
+ scopes: ["mcp:read"],
242
+ ...honeycomb,
243
+ },
244
+ })
245
+ ```
246
+
247
+ | Helper | Backing store | When to use |
248
+ | ---------------------------------- | ---------------------------------------------------------------- | -------------------------------------------------------- |
249
+ | `keychainPersistence({ server })` | OS keychain (macOS `security`, Linux `secret-tool`) | Local dev / desktop apps; tokens encrypted by the OS |
250
+ | `filePersistence({ path, server })` | Mode-`0600` JSON file | CI / containers without an OS keychain |
251
+
252
+ For Vault, SSM, or a custom secret system, write your own `onTokensChanged` and `onClientRegistered` directly. The contract is two callbacks and two optional values.
253
+
254
+ ## Server states
255
+
256
+ Every server entry the registry tracks is in exactly one of five states. The state is the `status` field on `ListedEntry` (returned by `Registry.list()` and emitted on every snapshot through `subscribe`), and it's the discriminator on the `AddServerResult` envelope returned from `addServer` / `applyConfig` / `finishAuth` / `enable`.
257
+
258
+ | State | Meaning | Side data |
259
+ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- |
260
+ | `connecting` | Transport is being built (RFC 9728 discovery, RFC 7591 DCR, stdio spawn, HTTP handshake) or rebuilt after `reauthorize` / `enable`. | — |
261
+ | `authenticating` | An `authorizationCode` server needs the user. The SDK has produced an authorize URL; the embedder's `openAuthorizeUrl` hook fired. | `authUrl: string` |
262
+ | `ready` | Connected. Tools listed. Calls succeed and stream through the bridge. | `toolCount: number`, `tools: [...]` |
263
+ | `error` | Transport, auth-config, or `addServer` validation failure. The entry stays in `list()` so the UI can surface the failure. | `error: { kind, message, details? }` |
264
+ | `disabled` | Operator paused the server via `Registry.disable(name)`. Transport closed; tokens stay in the cache. | — |
265
+
266
+ Transitions are driven by registry methods. The high-level shape:
267
+
268
+ ```
269
+ ┌──────────────┐ success ┌──────────┐
270
+ addServer ──────▶│ connecting │───────────────▶│ ready │
271
+ applyConfig └──────┬───────┘ └────┬─────┘
272
+ enable │ │
273
+ │ no tokens / 401 │
274
+ ▼ │
275
+ ┌──────────────┐ finishAuth │
276
+ │authenticating│───────────────────▶─┘
277
+ └──────┬───────┘ (retries connect)
278
+
279
+ │ unrecoverable
280
+
281
+ ┌──────────────┐
282
+ │ error │
283
+ └──────────────┘
284
+
285
+ reauthorize: any non-disabled ──▶ connecting ──▶ authenticating
286
+ disable: any ──▶ disabled
287
+ enable: disabled ──▶ connecting ──▶ ready (or authenticating, or error)
288
+ removeServer: any ──▶ (entry gone)
289
+ ```
290
+
291
+ A few specifics worth knowing:
292
+
293
+ - **`error` is sticky.** It doesn't auto-recover. Reach `ready` again by calling `addServer` with the same config (idempotency picks up changes), `reauthorize(name)`, or — for transient transport issues — re-running through `applyConfig`. The entry stays in the snapshot the whole time.
294
+ - **`reauthorize` always lands in `connecting` first**, then typically `authenticating` because tokens are intentionally cleared. The mutation is in-place — subscribers never see the entry disappear, so renderers don't flicker.
295
+ - **`disable` is recoverable.** It closes the transport but keeps tokens, hooks, and the entry. `enable` rebuilds the transport from the same config; if tokens are still valid, the next state is `ready` without an OAuth round-trip.
296
+ - **`removeServer` is destructive.** It clears tokens from the in-memory cache (persisted tokens via `onTokensChanged` stay where the operator put them) and removes the entry. There is no UI affordance for it on the desktop — Disable is the recoverable equivalent.
297
+
298
+ For the full per-method API (including `subscribe`, `RegistrySnapshot`, and `RegistryOpts`), see the [`McpRegistry` reference](/docs/agents/reference/mcp-registry).
299
+
300
+ ## Lifecycle
301
+
302
+ ### Hot-reload
303
+
304
+ Editing `mcp.json` (or calling `applyConfig` programmatically) takes effect immediately:
305
+
306
+ - **New server.** Tools available at the next tool-selection step in any active wake; manifests of agents using `mcp.tools()` update at the next compose.
307
+ - **Removed server.** In-flight tool calls complete or fail cleanly; no new calls dispatch; stdio subprocesses terminate after in-flight calls drain.
308
+ - **Reconfigured server.** Takes effect on the next tool call to that server. In-flight calls finish on the old config.
309
+
310
+ `addServer` and `applyConfig` are idempotent on unchanged config — they compare by `(name, url, transport, authMode, scopes, timeoutMs, command, args)` and short-circuit when nothing changed. Spurious file-system events from macOS reload watchers won't tear down healthy connections.
311
+
312
+ ### Re-authorize
313
+
314
+ Calling `Registry.reauthorize(name)` forces a fresh OAuth flow without removing the entry from the registry. The transport is closed, tokens are dropped from the in-memory cache (hooks remain registered), and the SDK produces a new authorize URL that fires through the `openAuthorizeUrl` hook. The entry stays in every snapshot throughout, so subscribers don't see it disappear and reappear.
315
+
316
+ The desktop's **Authorize** button routes through this method. It's enabled when the server is in `authenticating` (initial sign-in) or `error` (recover from a stale-token state). Once the server is `ready` the same button label switches to **Re-authorize** and forces a fresh OAuth flow — useful when refresh-token rotation has stopped working and you want to re-bootstrap without removing the server.
317
+
318
+ ### Per-call timeouts
319
+
320
+ Every MCP tool call has a timeout (default 30 seconds, overridable per server via `timeoutMs`). When exceeded, the bridge cancels the call (JSON-RPC cancellation for stdio servers; HTTP request abort for HTTP servers) and resolves it with a `timeout` error result. The agent's model decides what to do — retry, fall back, abort.
321
+
322
+ The timeout is a hygiene feature, not a long-running-call solution. Tool calls in v1 are synchronous within the wake.
323
+
324
+ ## Connected Services UI (desktop)
325
+
326
+ The Electron desktop ships a **Settings → MCP Servers** page that mirrors `Registry.subscribe` over Electron IPC. Each row shows:
327
+
328
+ - **Name and transport** (stdio / http).
329
+ - **Auth mode** (apiKey / clientCredentials / authorizationCode).
330
+ - **Status** — `connecting`, `authenticating`, `ready`, `error`, or `disabled`.
331
+ - **Tool count + expandable tool list.**
332
+ - **Per-row actions:** Authorize (only when a server is in `authenticating`), Reconnect, Disable / Enable.
333
+
334
+ The page is the operator's primary mechanism for noticing and fixing broken credentials, and the developer's primary surface for kicking off initial OAuth flows. There is no Disconnect action: removal of an entry happens via editing the config file. Disable pauses without losing state and is recoverable from the UI.
335
+
336
+ ## Failure modes
337
+
338
+ The runtime returns a structured error to the agent's model on any tool-call failure it can't transparently recover from:
339
+
340
+ | Kind | Meaning |
341
+ | ------------------- | -------------------------------------------------------------------------------------- |
342
+ | `auth_unavailable` | Silent refresh failed and no credential is usable; operator must reauthorize. |
343
+ | `transport_error` | Server unreachable, connection dropped, malformed response. |
344
+ | `timeout` | Call exceeded its per-call timeout. |
345
+ | `server_error` | The MCP server returned a structured error. |
346
+ | `tool_not_found` | Capability mismatch (e.g. server's tool list changed since compose). |
347
+
348
+ Agents handle these like any other tool error: retry, fall back, give up gracefully, or escalate to the user. The runtime doesn't block tool calls indefinitely waiting for out-of-band recovery.
349
+
350
+ ## Reference
351
+
352
+ - [`McpRegistry`](/docs/agents/reference/mcp-registry) — full API: `addServer`, `applyConfig`, `subscribe`, `reauthorize`, the lifecycle, snapshot envelope, and `RegistryOpts` for custom embedders.
353
+ - [`McpServerConfig`](/docs/agents/reference/mcp-server-config) — schema for the `cfg` argument to `addServer` / `applyConfig`.
354
+ - [`BuiltinAgentsServer` options](/docs/agents/usage/embedded-builtins) — the `extraMcpServers` and `openAuthorizeUrl` options used to wire embedder-specific MCP behavior.
@@ -51,7 +51,6 @@ The context API passed into the handler:
51
51
  | `ctx.sleep()` | Return to idle |
52
52
  | `ctx.mkdb(id, schema)` | Create cross-entity shared state |
53
53
  | `ctx.observe(db(id, schema), opts)` | Join existing shared state |
54
- | `ctx.useCodingAgent(sessionId, opts)` | Spawn or attach to a built-in `coder` session |
55
54
  | `ctx.recordRun()` | Record non-LLM work as a run for `runFinished` observers |
56
55
  | `ctx.setTag(key, value)` | Set a tag on this entity |
57
56
  | `ctx.removeTag(key)` | Remove a tag from this entity |
@@ -176,7 +175,6 @@ See [Managing state](/docs/agents/usage/managing-state).
176
175
  - `opts.wake` -- `'runFinished'`, `{ on: 'runFinished', includeResponse? }`, or `{ on: 'change', collections?, debounceMs?, timeoutMs? }`
177
176
  - **`observe(source, opts)`** -> `EntityHandle | ObservationHandle` -- subscribe via `entity()`, `cron()`, `entities()`, `db()`
178
177
  - **`send(url, payload, opts)`** -- fire-and-forget message
179
- - **`useCodingAgent(sessionId, opts)`** -> `CodingSessionHandle` -- spawn or attach to a built-in Claude Code/Codex session
180
178
  - **`recordRun()`** -> `RunHandle` -- publish run lifecycle for external work
181
179
  - **`sleep()`** -- go idle
182
180
 
@@ -286,6 +284,6 @@ Use the client and embedding APIs when you need to work with agents outside an e
286
284
  | `createAgentsClient()` | Observe entity, membership, or shared-state streams from app code |
287
285
  | `useChat()` | Render an observed `EntityStreamDB` in React |
288
286
  | `createRuntimeServerClient()` | Spawn, message, delete, tag, and schedule entities from services |
289
- | `BuiltinAgentsServer` | Host Horton, worker, and coder in your own process |
287
+ | `BuiltinAgentsServer` | Host Horton and worker in your own process |
290
288
 
291
289
  See [Clients & React](/docs/agents/usage/clients-and-react), [Programmatic runtime client](/docs/agents/usage/programmatic-runtime-client), and [Embedded built-ins](/docs/agents/usage/embedded-builtins).
@@ -202,7 +202,7 @@ await client.deleteSchedule({
202
202
 
203
203
  ## Tags
204
204
 
205
- `setTag()` and `removeTag()` require the entity write token. Handler code should prefer `ctx.setTag()` and `ctx.removeTag()` because the runtime already has the write token.
205
+ `setTag()` and `removeTag()` are primarily for handler/runtime-owned flows that already hold the current claim-scoped write token. External clients should prefer `send()` and write only to an entity's inbox rather than writing entity state directly.
206
206
 
207
207
  ```ts
208
208
  await client.setTag("/horton/onboarding", "title", "Onboarding", writeToken)
@@ -57,10 +57,6 @@ interface HandlerContext<TState extends StateProxy = StateProxy> {
57
57
  id: string,
58
58
  schema: T
59
59
  ) => SharedStateHandle<T>
60
- useCodingAgent: (
61
- sessionId: string,
62
- opts: UseCodingAgentOptions
63
- ) => Promise<CodingSessionHandle>
64
60
  send: (
65
61
  entityUrl: string,
66
62
  payload: unknown,
@@ -98,7 +94,6 @@ interface HandlerContext<TState extends StateProxy = StateProxy> {
98
94
  | `spawn` | Creates a child entity. See [Spawning and coordinating](./spawning-and-coordinating). |
99
95
  | `observe` | Connects to another entity's stream or shared db. See [Reactive observers](../entities/patterns/reactive-observers) and [Shared state](./shared-state). |
100
96
  | `mkdb` | Creates a new shared state stream. See [Shared state](./shared-state). |
101
- | `useCodingAgent` | Spawns or attaches to a built-in `coder` entity backed by Claude Code or Codex. |
102
97
  | `send` | Sends a message to another entity's inbox. Supports delayed delivery via `afterMs`. |
103
98
  | `recordRun` | Records non-LLM work in the built-in `runs` collection so `runFinished` observers are woken. |
104
99
  | `setTag` | Sets a tag on this entity. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@electric-ax/agents",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
4
4
  "description": "Built-in Electric Agents runtimes such as Horton and worker",
5
5
  "repository": {
6
6
  "type": "git",
@@ -28,25 +28,26 @@
28
28
  "./package.json": "./package.json"
29
29
  },
30
30
  "dependencies": {
31
- "@anthropic-ai/sdk": "^0.78.0",
32
31
  "@durable-streams/state": "npm:@electric-ax/durable-streams-state-beta@^0.3.1",
33
32
  "@mariozechner/pi-agent-core": "^0.70.2",
34
33
  "@mariozechner/pi-ai": "^0.70.2",
35
34
  "@sinclair/typebox": "^0.34.48",
36
- "agent-session-protocol": "^0.0.2",
37
35
  "better-sqlite3": "^11.10.0",
38
36
  "nanoid": "^3.3.11",
39
37
  "pino": "^10.3.1",
40
38
  "pino-pretty": "^13.0.0",
41
39
  "sqlite-vec": "^0.1.9",
42
40
  "zod": "^4.3.6",
43
- "@electric-ax/agents-runtime": "0.1.2"
41
+ "@electric-ax/agents-mcp": "0.2.0",
42
+ "@electric-ax/agents-runtime": "0.1.3"
44
43
  },
45
44
  "devDependencies": {
46
45
  "@types/better-sqlite3": "^7.6.13",
47
46
  "@types/node": "^22.19.15",
48
47
  "@vitest/coverage-v8": "^4.1.0",
48
+ "cross-env": "^10.1.0",
49
49
  "tsdown": "^0.9.0",
50
+ "tsx": "^4.19.0",
50
51
  "typescript": "^5.0.0",
51
52
  "vitest": "^4.1.0"
52
53
  },
@@ -61,6 +62,7 @@
61
62
  "scripts": {
62
63
  "build": "tsdown",
63
64
  "dev": "tsdown --watch",
65
+ "start": "cross-env ELECTRIC_AGENTS_SERVER_URL=http://localhost:4437 tsx --watch src/entrypoint.ts",
64
66
  "docs:sync": "node scripts/sync-docs.mjs",
65
67
  "docs:clean": "node scripts/sync-docs.mjs --clean",
66
68
  "test": "vitest run",
@@ -1,99 +0,0 @@
1
- ---
2
- title: Coder
3
- titleTemplate: "... - Electric Agents"
4
- description: >-
5
- Built-in coding-session entity backed by Claude Code or Codex CLI.
6
- outline: [2, 3]
7
- ---
8
-
9
- # Coder
10
-
11
- `coder` is the built-in coding-session entity. It runs a Claude Code or Codex CLI session in a working directory, mirrors the normalized session event stream into entity state, and can be prompted repeatedly across many turns.
12
-
13
- **Source:** [`packages/agents/src/agents/coding-session.ts`](https://github.com/electric-sql/electric/blob/main/packages/agents/src/agents/coding-session.ts)
14
-
15
- ## Spawn args
16
-
17
- ```ts
18
- interface CoderArgs {
19
- agent: "claude" | "codex"
20
- cwd?: string
21
- nativeSessionId?: string
22
- importFrom?: { agent: "claude" | "codex"; sessionId: string }
23
- }
24
- ```
25
-
26
- | Field | Required | Description |
27
- | ----------------- | -------- | ----------- |
28
- | `agent` | Yes | CLI backend to run: `"claude"` or `"codex"`. |
29
- | `cwd` | No | Working directory for the CLI. Defaults to the built-in runtime working directory. |
30
- | `nativeSessionId` | No | Attach to an existing local Claude/Codex session. |
31
- | `importFrom` | No | Import an existing local session into a new session for the selected backend. |
32
-
33
- The built-in runtime registers `coder` during bootstrap. Handler code can also call `registerCodingSession(registry, { defaultWorkingDirectory, cliRunner? })` from `@electric-ax/agents`.
34
-
35
- ## Prompt messages
36
-
37
- The preferred inbox message type is `prompt` with a payload shaped like:
38
-
39
- ```ts
40
- interface PromptMessage {
41
- text: string
42
- }
43
- ```
44
-
45
- Generic messages with the same `{ text }` payload are also processed, so the dashboard and CLI can send prompts without a custom message type.
46
-
47
- ## State collections
48
-
49
- `coder` adds three custom state collections:
50
-
51
- | Collection | Event type | Description |
52
- | --------------- | ----------------------- | ----------- |
53
- | `sessionMeta` | `coding_session_meta` | Current session metadata: selected backend, cwd, status, native session id, and errors. |
54
- | `cursorState` | `coding_session_cursor` | Serialized tail cursor and the last processed inbox key. |
55
- | `events` | `coding_session_event` | Normalized `agent-session-protocol` events mirrored from the CLI session. |
56
-
57
- ## Handler behavior
58
-
59
- 1. Initializes session metadata and cursor state if needed.
60
- 2. Mirrors existing local session history when attaching or importing.
61
- 3. Processes pending prompt messages in inbox order.
62
- 4. Calls `ctx.recordRun()` around each CLI invocation so parents observing with `wake: "runFinished"` are notified.
63
- 5. Mirrors new CLI events into the `events` collection and appends assistant text as the run response.
64
- 6. Updates `sessionMeta.status` to `idle` or `error`.
65
-
66
- ## Handler API
67
-
68
- Inside another entity handler, use `ctx.useCodingAgent()` to spawn or attach to a coder:
69
-
70
- ```ts
71
- const coder = await ctx.useCodingAgent("feature-work", {
72
- agent: "claude",
73
- cwd: process.cwd(),
74
- })
75
-
76
- coder.send("Implement the requested feature and run the tests.")
77
- await coder.run
78
- ```
79
-
80
- `useCodingAgent()` returns a `CodingSessionHandle` with `entityUrl`, `status()`, `meta()`, `send(prompt)`, `run`, `events`, and `messages`.
81
-
82
- ## Horton tools
83
-
84
- Horton usually interacts with coders through:
85
-
86
- | Tool | Purpose |
87
- | -------------- | ------- |
88
- | `spawn_coder` | Creates a new long-lived `coder`, sends the first prompt, and wakes Horton when the reply lands. |
89
- | `prompt_coder` | Sends a follow-up prompt to an existing coder URL. |
90
-
91
- ## Details
92
-
93
- | Property | Value |
94
- | ----------------- | ----- |
95
- | Type name | `coder` |
96
- | Backends | Claude Code and Codex CLI |
97
- | State | `sessionMeta`, `cursorState`, `events` |
98
- | Wake support | Uses `ctx.recordRun()` so `runFinished` observers work |
99
- | Working directory | From spawn args or `registerCodingSession` default |