@electric-ax/agents-mcp 0.2.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.
@@ -0,0 +1,515 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { OAuthClientInformationMixed } from "@modelcontextprotocol/sdk/shared/auth.js";
3
+ import { OAuthClientProvider } from "@modelcontextprotocol/sdk/client/auth.js";
4
+
5
+ //#region src/types.d.ts
6
+ /** Persisted-token shape — surfaces in OAuth callbacks (`onTokensChanged`). */
7
+ /** Persisted-token shape — surfaces in OAuth callbacks (`onTokensChanged`). */
8
+ interface OAuthTokens {
9
+ accessToken: string;
10
+ refreshToken?: string;
11
+ /** Unix seconds. */
12
+ expiresAt?: number;
13
+ tokenType?: string;
14
+ scope?: string;
15
+ }
16
+ /** DCR-registered (or pre-registered) OAuth client — surfaces in `onClientRegistered`. */
17
+ interface OAuthClientInfo {
18
+ clientId: string;
19
+ clientSecret?: string;
20
+ redirectUris?: string[];
21
+ /** Unix seconds. */
22
+ registeredAt?: number;
23
+ }
24
+ type McpAuthMode = `none` | `apiKey` | `clientCredentials` | `authorizationCode`;
25
+ type McpAuthConfig = {
26
+ mode: `none`;
27
+ } | {
28
+ mode: `apiKey`;
29
+ /** Raw secret. Inline at the call site (e.g. `process.env.X_API_KEY`). */
30
+ key: string;
31
+ headerName?: string;
32
+ valuePrefix?: string;
33
+ } | {
34
+ mode: `clientCredentials`;
35
+ tokenUrl: string;
36
+ /** Inline at the call site (e.g. `process.env.X_CLIENT_ID`). */
37
+ clientId: string;
38
+ /** Inline at the call site (e.g. `process.env.X_CLIENT_SECRET`). */
39
+ clientSecret: string;
40
+ scopes?: string[];
41
+ audience?: string;
42
+ resource?: string;
43
+ } | {
44
+ mode: `authorizationCode`;
45
+ scopes?: string[];
46
+ resource?: string;
47
+ /** Override redirect URI; default `${publicUrl}/oauth/callback/<server>`. */
48
+ redirectUri?: string;
49
+ /**
50
+ * Pre-registered OAuth client. When present, RFC 7591 Dynamic Client
51
+ * Registration is skipped. Sourced from the operator's secret system.
52
+ */
53
+ client?: OAuthClientInfo;
54
+ /**
55
+ * Pre-existing tokens to seed the registry's in-process cache on
56
+ * boot. When present, the OAuth flow is skipped and the SDK uses
57
+ * these directly. Refresh-token rotation still happens transparently.
58
+ */
59
+ tokens?: OAuthTokens;
60
+ /**
61
+ * Fires after initial-auth and on every refresh-token rotation.
62
+ * Wire to a persistence layer (keychain, file, vault, ...) if you
63
+ * want tokens to survive process restarts. Optional — without it,
64
+ * tokens live only for the lifetime of the registry.
65
+ */
66
+ onTokensChanged?: (tokens: OAuthTokens) => void | Promise<void>;
67
+ /**
68
+ * Fires once after Dynamic Client Registration completes. Pair
69
+ * with `client` on the next boot to skip DCR.
70
+ */
71
+ onClientRegistered?: (client: OAuthClientInfo) => void | Promise<void>;
72
+ /**
73
+ * Reference into a per-process map of pre-built OAuthClientProvider
74
+ * instances. Escape hatch for embedders with non-standard requirements
75
+ * (mTLS, OIDC quirks, etc.).
76
+ */
77
+ oauthProviderRef?: string;
78
+ };
79
+ interface McpHttpServerConfig {
80
+ name: string;
81
+ transport: `http`;
82
+ url: string;
83
+ auth: McpAuthConfig;
84
+ /** Per-server timeout override in ms. Default 30000. */
85
+ timeoutMs?: number;
86
+ }
87
+ interface McpStdioServerConfig {
88
+ name: string;
89
+ transport: `stdio`;
90
+ command: string;
91
+ args?: string[];
92
+ env?: Record<string, string>;
93
+ auth?: McpAuthConfig;
94
+ /** Per-server timeout override in ms. Default 30000. */
95
+ timeoutMs?: number;
96
+ }
97
+ type McpServerConfig = McpHttpServerConfig | McpStdioServerConfig;
98
+ type McpServerStatus = `connecting` | `authenticating` | `ready` | `error` | `disabled`;
99
+ type McpToolErrorKind = `auth_unavailable` | `transport_error` | `timeout` | `server_error` | `tool_not_found`;
100
+ interface McpToolError {
101
+ kind: McpToolErrorKind;
102
+ message: string;
103
+ details?: unknown;
104
+ }
105
+ type AddServerResult = {
106
+ state: `ready`;
107
+ id: string;
108
+ toolCount: number;
109
+ } | {
110
+ state: `authenticating`;
111
+ id: string;
112
+ authUrl: string;
113
+ } | {
114
+ state: `error`;
115
+ id: string;
116
+ error: McpToolError;
117
+ }; //#endregion
118
+ //#region src/tools.d.ts
119
+ declare const MCP_TOOLS_SENTINEL: unique symbol;
120
+ interface McpToolsSentinel {
121
+ [MCP_TOOLS_SENTINEL]: true;
122
+ /** `undefined` means every registered server. */
123
+ allowlist?: string[];
124
+ }
125
+ declare function isMcpToolsSentinel(x: unknown): x is McpToolsSentinel;
126
+ declare const mcp: {
127
+ /**
128
+ * Returns a sentinel array for `tools: [...mcp.tools()]`.
129
+ * Resolution happens at wake time via the runtime's tool-provider
130
+ * hook. Pass an array to restrict to specific servers; omit for
131
+ * every registered server.
132
+ */
133
+ tools(allowlist?: string[]): McpToolsSentinel[];
134
+ };
135
+ declare function filterByAllowlist(serverNames: string[], allowlist: string[] | undefined): string[];
136
+
137
+ //#endregion
138
+ //#region src/credentials/auth-store.d.ts
139
+ /**
140
+ * Internal token + client cache used by the registry. Created per-registry,
141
+ * never crossed by the public API. Seeded from `auth.tokens` / `auth.client`
142
+ * on `addServer`; mutated as the SDK refreshes / completes DCR. Calls into
143
+ * the per-server `onTokensChanged` / `onClientRegistered` callbacks declared
144
+ * on the auth config so the operator can persist if they want.
145
+ */
146
+ interface AuthStore {
147
+ getOAuthTokens(server: string): OAuthTokens | undefined;
148
+ saveOAuthTokens(server: string, tokens: OAuthTokens): Promise<void>;
149
+ getOAuthClientInfo(server: string): OAuthClientInfo | undefined;
150
+ saveOAuthClientInfo(server: string, info: OAuthClientInfo): Promise<void>;
151
+ }
152
+ /** Per-server hooks registered on `addServer` and invoked on cache mutations. */
153
+ interface AuthStoreHooks {
154
+ onTokensChanged?: (tokens: OAuthTokens) => void | Promise<void>;
155
+ onClientRegistered?: (client: OAuthClientInfo) => void | Promise<void>;
156
+ }
157
+ interface InternalAuthStore extends AuthStore {
158
+ /** Pre-seed the cache with tokens read from `auth.tokens`. */
159
+ seedTokens(server: string, tokens: OAuthTokens): void;
160
+ /** Pre-seed the cache with a pre-registered OAuth client. */
161
+ seedClient(server: string, client: OAuthClientInfo): void;
162
+ /** Register per-server hooks declared on the auth config. */
163
+ registerHooks(server: string, hooks: AuthStoreHooks): void;
164
+ /**
165
+ * Drop only the cached tokens + DCR client info for a server. Hooks
166
+ * stay registered, so future `saveOAuthTokens` / `saveOAuthClientInfo`
167
+ * calls still notify the operator's persistence callbacks. Used by
168
+ * `Registry.reauthorize` to force a fresh OAuth flow without losing
169
+ * the persistence wiring.
170
+ */
171
+ clearCredentials(server: string): void;
172
+ /** Drop everything we know about a server (used by `removeServer`). */
173
+ forget(server: string): void;
174
+ }
175
+
176
+ //#endregion
177
+ //#region src/transports/types.d.ts
178
+ interface McpTransport {
179
+ client: Client;
180
+ connect(): Promise<void>;
181
+ close(): Promise<void>;
182
+ }
183
+
184
+ //#endregion
185
+ //#region src/auth/sdk-provider.d.ts
186
+ /**
187
+ * Adapter that implements the MCP SDK's `OAuthClientProvider` and reads/writes
188
+ * the registry's internal `AuthStore`. The SDK handles PKCE, DCR (RFC 7591),
189
+ * discovery (RFC 9728), token exchange, refresh, and 401-retry; this adapter
190
+ * just plumbs reads and writes. Cross-process persistence (if any) happens
191
+ * via the `onTokensChanged` / `onClientRegistered` hooks the registry wires
192
+ * into the store from the per-server auth config.
193
+ */
194
+ interface SdkOAuthProvider extends OAuthClientProvider {
195
+ /** Always implemented — persists the DCR response via the internal AuthStore. */
196
+ saveClientInformation(clientInformation: OAuthClientInformationMixed): void | Promise<void>;
197
+ /** Returns the most recent authorize URL captured by redirectToAuthorization. */
198
+ peekAuthUrl(): string | undefined;
199
+ /** Resets the captured authorize URL. */
200
+ clearAuthUrl(): void;
201
+ }
202
+
203
+ //#endregion
204
+ //#region src/config/loader.d.ts
205
+ interface McpConfig {
206
+ servers: McpServerConfig[];
207
+ raw: unknown;
208
+ }
209
+ declare function parseConfig(raw: unknown, env?: NodeJS.ProcessEnv): McpConfig;
210
+ declare function loadConfig(path: string, env?: NodeJS.ProcessEnv): Promise<McpConfig>;
211
+
212
+ //#endregion
213
+ //#region src/registry.d.ts
214
+ interface Entry {
215
+ config: McpServerConfig;
216
+ configHash: string;
217
+ status: McpServerStatus;
218
+ error?: McpToolError;
219
+ authUrl?: string;
220
+ transport?: McpTransport;
221
+ tools: Array<{
222
+ name: string;
223
+ description?: string;
224
+ inputSchema: unknown;
225
+ }>;
226
+ capabilities?: unknown;
227
+ provider?: SdkOAuthProvider;
228
+ }
229
+ interface RegistryOpts {
230
+ /** Base URL of this registry process, used to construct OAuth redirect URIs. */
231
+ publicUrl?: string;
232
+ transportFactoryOverride?: (cfg: McpServerConfig, hp?: HeaderProvider, provider?: SdkOAuthProvider) => McpTransport;
233
+ /**
234
+ * Called when an authorizationCode-flow server first needs the user
235
+ * to consent — receives the SDK-generated authorize URL plus the
236
+ * server name. The default implementation logs the URL; the desktop
237
+ * app overrides this to open the URL in a sandboxed BrowserWindow
238
+ * (and intercept the redirect_uri navigation to call `finishAuth`).
239
+ *
240
+ * Headless / non-Electron embedders that want to drive the flow
241
+ * themselves can read the URL from the `authenticating` envelope of
242
+ * `addServer` instead — this hook is purely a convenience for
243
+ * "open this URL right now" callers.
244
+ */
245
+ openAuthorizeUrl?: (url: string, server: string) => void;
246
+ /**
247
+ * Internal hook used by tests to seed / inspect the registry's private
248
+ * auth store. Not part of the public contract — production callers should
249
+ * use the per-server `auth.tokens` / `auth.client` fields and the
250
+ * `onTokensChanged` / `onClientRegistered` hooks instead.
251
+ * @internal
252
+ */
253
+ authStore?: InternalAuthStore;
254
+ }
255
+ type HeaderProvider = () => Promise<{
256
+ name: string;
257
+ value: string;
258
+ } | undefined>;
259
+ interface ListedEntry {
260
+ name: string;
261
+ status: McpServerStatus;
262
+ toolCount: number;
263
+ /** `http` or `stdio`. Surfaces in UI for badges + per-status affordances. */
264
+ transport?: string;
265
+ /** `none` | `apiKey` | `clientCredentials` | `authorizationCode`. */
266
+ authMode?: string;
267
+ authUrl?: string;
268
+ error?: McpToolError;
269
+ tools: Entry[`tools`];
270
+ capabilities?: unknown;
271
+ }
272
+ /** State snapshot delivered to subscribers on every registry mutation. */
273
+ interface RegistrySnapshot {
274
+ /** Monotonic per-registry counter; useful for detecting dropped events. */
275
+ seq: number;
276
+ servers: ReadonlyArray<ListedEntry>;
277
+ }
278
+ type RegistrySubscriber = (snapshot: RegistrySnapshot) => void;
279
+ interface Registry {
280
+ addServer(cfg: McpServerConfig): Promise<AddServerResult>;
281
+ applyConfig(cfg: McpConfig): Promise<AddServerResult[]>;
282
+ removeServer(name: string): Promise<void>;
283
+ list(): ReadonlyArray<ListedEntry>;
284
+ get(name: string): Entry | undefined;
285
+ finishAuth(serverName: string, code: string, state?: string): Promise<AddServerResult>;
286
+ disable(name: string): Promise<void>;
287
+ enable(name: string): Promise<AddServerResult>;
288
+ /**
289
+ * Force a fresh OAuth flow for a server. Closes the current transport,
290
+ * forgets cached tokens (and DCR client info) for this server, and
291
+ * rebuilds the transport in place — keeping the entry visible in
292
+ * snapshots throughout, so renderers don't see a brief
293
+ * "server gone, server back" blip the way `removeServer` + `addServer`
294
+ * would. The SDK provider then has nothing to authenticate with on the
295
+ * next connect, and surfaces a fresh authorize URL via the
296
+ * `openAuthorizeUrl` hook.
297
+ *
298
+ * No-op when the server is unknown, not authorizationCode, or
299
+ * disabled.
300
+ */
301
+ reauthorize(name: string): Promise<void>;
302
+ /**
303
+ * Subscribe to registry state changes. The handler is invoked
304
+ * synchronously with the current snapshot on subscribe (so callers
305
+ * can render an initial UI without a round-trip), and again on every
306
+ * mutation: addServer / removeServer / applyConfig / finishAuth /
307
+ * disable / enable / connection-state transitions.
308
+ *
309
+ * Returns an unsubscribe function.
310
+ */
311
+ subscribe(handler: RegistrySubscriber): () => void;
312
+ /**
313
+ * Close every transport, clear all entries, and emit a final empty
314
+ * snapshot to subscribers.
315
+ */
316
+ close(): Promise<void>;
317
+ }
318
+ declare function createRegistry(opts: RegistryOpts): Registry;
319
+
320
+ //#endregion
321
+ //#region src/config/watcher.d.ts
322
+ interface WatchOpts {
323
+ onChange: (cfg: McpConfig) => void;
324
+ onError?: (err: unknown) => void;
325
+ debounceMs?: number;
326
+ env?: NodeJS.ProcessEnv;
327
+ }
328
+ /**
329
+ * Start watching `path` for changes. Each modification triggers
330
+ * `loadConfig(path)` (debounced) and forwards the parsed config to
331
+ * `onChange`, or any error to `onError`. The caller is responsible
332
+ * for performing the initial load — `watchConfig` only sets up the
333
+ * subscription so the caller can fully await its first apply before
334
+ * subsequent change events start firing.
335
+ */
336
+ declare function watchConfig(path: string, opts: WatchOpts): Promise<() => void>;
337
+
338
+ //#endregion
339
+ //#region src/bridge/tool-bridge.d.ts
340
+ declare function prefixToolName(server: string, tool: string): string;
341
+ /**
342
+ * Coerce an MCP tool's inputSchema into a shape downstream LLM adapters can
343
+ * consume safely. Some servers send `{ type: 'object' }` with no `properties`
344
+ * for no-arg tools; pi-agent-core walks `inputSchema.properties` and crashes on
345
+ * undefined. We default `properties` to `{}` and `required` to `[]` for object
346
+ * schemas; non-object schemas pass through unchanged.
347
+ */
348
+
349
+ interface BridgeToolOpts {
350
+ server: string;
351
+ tool: {
352
+ name: string;
353
+ description?: string;
354
+ inputSchema: unknown;
355
+ };
356
+ /**
357
+ * Subset of the MCP SDK Client used here.
358
+ *
359
+ * The real signature of Client.callTool in @modelcontextprotocol/sdk ≥ 1.10 is:
360
+ * callTool(params, resultSchema?, options?)
361
+ * where the SECOND argument is a Zod schema (defaults to CallToolResultSchema) and
362
+ * the THIRD argument is a RequestOptions bag that may include { signal, onProgress }.
363
+ *
364
+ * We model this correctly here so that invoke() never passes signal/onProgress as
365
+ * the resultSchema (which would cause "v3Schema.safeParse is not a function").
366
+ */
367
+ client: {
368
+ callTool: (args: {
369
+ name: string;
370
+ arguments?: unknown;
371
+ }, resultSchema?: unknown, opts?: {
372
+ onProgress?: (p: unknown) => void;
373
+ signal?: AbortSignal;
374
+ }) => Promise<unknown>;
375
+ };
376
+ timeoutMs?: number;
377
+ /** Optional progress notification callback forwarded to the SDK. */
378
+ onProgress?: (p: unknown) => void;
379
+ /** Optional AbortSignal forwarded to the SDK callTool. */
380
+ signal?: AbortSignal;
381
+ }
382
+ interface BridgedTool {
383
+ name: string;
384
+ server: string;
385
+ description?: string;
386
+ /** MCP wire shape — JSON schema. */
387
+ inputSchema: unknown;
388
+ /** pi-ai/pi-agent expects `parameters` (same JSON schema, different field). */
389
+ parameters: unknown;
390
+ /** Display label used by pi-agent UI bridges. */
391
+ label: string;
392
+ /** Direct MCP-style call. */
393
+ call(args: unknown): Promise<unknown>;
394
+ /** pi-agent execute signature. Wraps `call` and returns AgentToolResult-shaped output. */
395
+ execute: (toolCallId: string, params: unknown, signal?: AbortSignal) => Promise<{
396
+ content: Array<{
397
+ type: string;
398
+ text?: string;
399
+ }>;
400
+ details: unknown;
401
+ }>;
402
+ }
403
+ declare function bridgeMcpTool(opts: BridgeToolOpts): BridgedTool;
404
+
405
+ //#endregion
406
+ //#region src/bridge/resource-bridge.d.ts
407
+ interface BuildResourceToolsOpts {
408
+ server: string;
409
+ client: {
410
+ listResources: () => Promise<unknown>;
411
+ readResource: (args: {
412
+ uri: string;
413
+ }) => Promise<unknown>;
414
+ };
415
+ timeoutMs?: number;
416
+ }
417
+ declare function buildResourceTools(opts: BuildResourceToolsOpts): BridgedTool[];
418
+
419
+ //#endregion
420
+ //#region src/bridge/prompt-bridge.d.ts
421
+ interface BuildPromptToolsOpts {
422
+ server: string;
423
+ client: {
424
+ listPrompts: () => Promise<unknown>;
425
+ getPrompt: (args: {
426
+ name: string;
427
+ arguments?: Record<string, unknown>;
428
+ }) => Promise<unknown>;
429
+ };
430
+ timeoutMs?: number;
431
+ }
432
+ declare function buildPromptTools(opts: BuildPromptToolsOpts): BridgedTool[];
433
+
434
+ //#endregion
435
+ //#region src/persistence/keychain.d.ts
436
+ interface KeychainBackend {
437
+ get(service: string, account: string): Promise<string | undefined>;
438
+ set(service: string, account: string, value: string): Promise<void>;
439
+ }
440
+ interface KeychainPersistenceOpts {
441
+ /** Server name; used as the account-id portion of the keychain entry. */
442
+ server: string;
443
+ /** Keychain service identifier. Default `'electric-agents'`. */
444
+ service?: string;
445
+ /** Override for tests — inject a backend instead of shelling out. */
446
+ backend?: KeychainBackend;
447
+ }
448
+ /**
449
+ * Opt-in helper for OAuth-mode `auth` configs. Loads any persisted tokens
450
+ * and DCR client info from the OS keychain on startup, and returns the
451
+ * matching `onTokensChanged` / `onClientRegistered` callbacks so the SDK
452
+ * writes refreshed material back.
453
+ *
454
+ * const honeycomb = await keychainPersistence({ server: 'honeycomb' })
455
+ * await mcpRegistry.addServer({
456
+ * name: 'honeycomb',
457
+ * transport: 'http',
458
+ * url: 'https://mcp.honeycomb.io/mcp',
459
+ * auth: {
460
+ * mode: 'authorizationCode',
461
+ * flow: 'browser',
462
+ * scopes: ['mcp:read'],
463
+ * ...honeycomb,
464
+ * },
465
+ * })
466
+ *
467
+ * Backend is chosen by `process.platform`:
468
+ * - darwin → `/usr/bin/security` (no extra deps)
469
+ * - linux → `secret-tool` from libsecret-tools (apt: libsecret-tools)
470
+ * - win32 → not implemented yet — falls back to no-op callbacks
471
+ *
472
+ * If the chosen CLI isn't installed (e.g. minimal Linux container without
473
+ * libsecret), reads/writes throw on first use; the registry surfaces
474
+ * that as a connect-time error and the OAuth flow continues without
475
+ * persistence.
476
+ */
477
+ declare function keychainPersistence(opts: KeychainPersistenceOpts): Promise<{
478
+ tokens?: OAuthTokens;
479
+ client?: OAuthClientInfo;
480
+ onTokensChanged: (t: OAuthTokens) => Promise<void>;
481
+ onClientRegistered: (c: OAuthClientInfo) => Promise<void>;
482
+ }>;
483
+
484
+ //#endregion
485
+ //#region src/persistence/file.d.ts
486
+ interface FilePersistenceOpts {
487
+ /** Path on disk; mode-0600 JSON. Created on first write. */
488
+ path: string;
489
+ /** Server name (key inside the file). */
490
+ server: string;
491
+ }
492
+ /**
493
+ * Opt-in helper for OAuth-mode `auth` configs. Mirrors `keychainPersistence`
494
+ * but persists to a JSON file on disk (mode 0600). Right tool when no OS
495
+ * keychain is available — CI runners, minimal Linux containers, etc.
496
+ *
497
+ * const honeycomb = await filePersistence({
498
+ * path: './.electric-agents/credentials.json',
499
+ * server: 'honeycomb',
500
+ * })
501
+ * await mcpRegistry.addServer({ ..., auth: { ..., ...honeycomb } })
502
+ */
503
+ declare function filePersistence(opts: FilePersistenceOpts): Promise<{
504
+ tokens?: OAuthTokens;
505
+ client?: OAuthClientInfo;
506
+ onTokensChanged: (t: OAuthTokens) => Promise<void>;
507
+ onClientRegistered: (c: OAuthClientInfo) => Promise<void>;
508
+ }>;
509
+
510
+ //#endregion
511
+ //#region src/index.d.ts
512
+ declare const VERSION = "0.1.0";
513
+
514
+ //#endregion
515
+ export { AddServerResult, BridgeToolOpts, BridgedTool, BuildPromptToolsOpts, BuildResourceToolsOpts, FilePersistenceOpts, HeaderProvider, KeychainPersistenceOpts, ListedEntry, MCP_TOOLS_SENTINEL, McpAuthConfig, McpAuthMode, McpConfig, McpHttpServerConfig, McpServerConfig, McpServerStatus, McpStdioServerConfig, McpToolError, McpToolErrorKind, McpToolsSentinel, OAuthClientInfo, OAuthTokens, Registry, RegistryOpts, RegistrySnapshot, RegistrySubscriber, VERSION, WatchOpts, bridgeMcpTool, buildPromptTools, buildResourceTools, createRegistry, filePersistence, filterByAllowlist, isMcpToolsSentinel, keychainPersistence, loadConfig, mcp, parseConfig, prefixToolName, watchConfig };