@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.
package/dist/index.cjs ADDED
@@ -0,0 +1,1169 @@
1
+ "use strict";
2
+ //#region rolldown:runtime
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+
24
+ //#endregion
25
+ const __modelcontextprotocol_sdk_client_index_js = __toESM(require("@modelcontextprotocol/sdk/client/index.js"));
26
+ const __modelcontextprotocol_sdk_client_streamableHttp_js = __toESM(require("@modelcontextprotocol/sdk/client/streamableHttp.js"));
27
+ const __modelcontextprotocol_sdk_client_stdio_js = __toESM(require("@modelcontextprotocol/sdk/client/stdio.js"));
28
+ const node_fs_promises = __toESM(require("node:fs/promises"));
29
+ const node_fs = __toESM(require("node:fs"));
30
+ const node_child_process = __toESM(require("node:child_process"));
31
+ const node_path = __toESM(require("node:path"));
32
+
33
+ //#region src/tools.ts
34
+ const MCP_TOOLS_SENTINEL = Symbol.for(`@electric-ax/agents-mcp/tools-sentinel`);
35
+ function isMcpToolsSentinel(x) {
36
+ return !!x && typeof x === `object` && x[MCP_TOOLS_SENTINEL] === true;
37
+ }
38
+ const mcp = { tools(allowlist) {
39
+ return [{
40
+ [MCP_TOOLS_SENTINEL]: true,
41
+ allowlist
42
+ }];
43
+ } };
44
+ function filterByAllowlist(serverNames, allowlist) {
45
+ if (allowlist === void 0) return [...serverNames];
46
+ const set = new Set(allowlist);
47
+ return serverNames.filter((n) => set.has(n));
48
+ }
49
+
50
+ //#endregion
51
+ //#region src/credentials/auth-store.ts
52
+ function createAuthStore() {
53
+ const tokens = new Map();
54
+ const clients = new Map();
55
+ const hooks = new Map();
56
+ return {
57
+ seedTokens(server, t) {
58
+ tokens.set(server, t);
59
+ },
60
+ seedClient(server, c) {
61
+ clients.set(server, c);
62
+ },
63
+ registerHooks(server, h) {
64
+ hooks.set(server, h);
65
+ },
66
+ clearCredentials(server) {
67
+ tokens.delete(server);
68
+ clients.delete(server);
69
+ },
70
+ forget(server) {
71
+ tokens.delete(server);
72
+ clients.delete(server);
73
+ hooks.delete(server);
74
+ },
75
+ getOAuthTokens(server) {
76
+ return tokens.get(server);
77
+ },
78
+ async saveOAuthTokens(server, t) {
79
+ tokens.set(server, t);
80
+ const h = hooks.get(server);
81
+ if (h?.onTokensChanged) await h.onTokensChanged(t);
82
+ },
83
+ getOAuthClientInfo(server) {
84
+ return clients.get(server);
85
+ },
86
+ async saveOAuthClientInfo(server, c) {
87
+ clients.set(server, c);
88
+ const h = hooks.get(server);
89
+ if (h?.onClientRegistered) await h.onClientRegistered(c);
90
+ }
91
+ };
92
+ }
93
+
94
+ //#endregion
95
+ //#region src/transports/http.ts
96
+ function createHttpTransport(opts) {
97
+ const fetchImpl = opts.fetchImpl ?? fetch;
98
+ const transport = new __modelcontextprotocol_sdk_client_streamableHttp_js.StreamableHTTPClientTransport(new URL(opts.url), {
99
+ authProvider: opts.authProvider,
100
+ fetch: opts.headerProvider ? async (url, init) => {
101
+ const headers = new Headers(init?.headers);
102
+ const h = await opts.headerProvider();
103
+ if (h) headers.set(h.name, h.value);
104
+ return fetchImpl(url, {
105
+ ...init,
106
+ headers
107
+ });
108
+ } : fetchImpl
109
+ });
110
+ const client = new __modelcontextprotocol_sdk_client_index_js.Client({
111
+ name: `@electric-ax/agents-mcp`,
112
+ version: `0.1.0`
113
+ }, { capabilities: {} });
114
+ return {
115
+ client,
116
+ async connect() {
117
+ await client.connect(transport);
118
+ },
119
+ async close() {
120
+ await client.close();
121
+ }
122
+ };
123
+ }
124
+
125
+ //#endregion
126
+ //#region src/transports/stdio.ts
127
+ function createStdioTransport(opts) {
128
+ const transport = new __modelcontextprotocol_sdk_client_stdio_js.StdioClientTransport({
129
+ command: opts.command,
130
+ args: opts.args ?? [],
131
+ env: opts.env
132
+ });
133
+ const client = new __modelcontextprotocol_sdk_client_index_js.Client({
134
+ name: `@electric-ax/agents-mcp`,
135
+ version: `0.1.0`
136
+ }, { capabilities: {} });
137
+ return {
138
+ client,
139
+ async connect() {
140
+ await client.connect(transport);
141
+ },
142
+ async close() {
143
+ await client.close();
144
+ }
145
+ };
146
+ }
147
+
148
+ //#endregion
149
+ //#region src/auth/api-key.ts
150
+ function buildApiKeyHeader(apiKey, opts = {}) {
151
+ return {
152
+ name: opts.headerName ?? `Authorization`,
153
+ value: (opts.valuePrefix ?? ``) + apiKey
154
+ };
155
+ }
156
+
157
+ //#endregion
158
+ //#region src/auth/sdk-provider.ts
159
+ function createSdkOAuthProvider(opts) {
160
+ const redirect = opts.redirectUri ?? `${opts.publicUrl.replace(/\/$/, ``)}/oauth/callback/${opts.server}`;
161
+ let codeVerifier;
162
+ let lastAuthUrl;
163
+ const toSdkTokens = (t) => ({
164
+ access_token: t.accessToken,
165
+ refresh_token: t.refreshToken,
166
+ expires_in: t.expiresAt ? Math.max(0, t.expiresAt - Math.floor(Date.now() / 1e3)) : void 0,
167
+ token_type: t.tokenType ?? `Bearer`,
168
+ scope: t.scope
169
+ });
170
+ const fromSdkTokens = (t) => ({
171
+ accessToken: t.access_token,
172
+ refreshToken: t.refresh_token,
173
+ expiresAt: t.expires_in ? Math.floor(Date.now() / 1e3) + t.expires_in : void 0,
174
+ tokenType: t.token_type,
175
+ scope: t.scope
176
+ });
177
+ const toSdkClientInfo = (c) => ({
178
+ client_id: c.clientId,
179
+ client_secret: c.clientSecret,
180
+ redirect_uris: c.redirectUris ?? [redirect]
181
+ });
182
+ const fromSdkClientInfo = (c) => ({
183
+ clientId: c.client_id,
184
+ clientSecret: c.client_secret,
185
+ redirectUris: c.redirect_uris?.map(String),
186
+ registeredAt: Math.floor(Date.now() / 1e3)
187
+ });
188
+ return {
189
+ get redirectUrl() {
190
+ return redirect;
191
+ },
192
+ get clientMetadata() {
193
+ return {
194
+ client_name: `@electric-ax/agents-mcp`,
195
+ redirect_uris: [redirect],
196
+ grant_types: [`authorization_code`, `refresh_token`],
197
+ response_types: [`code`],
198
+ token_endpoint_auth_method: `client_secret_post`,
199
+ scope: opts.scopes?.join(` `)
200
+ };
201
+ },
202
+ async clientInformation() {
203
+ const saved = opts.authStore.getOAuthClientInfo(opts.server);
204
+ return saved ? toSdkClientInfo(saved) : void 0;
205
+ },
206
+ async saveClientInformation(info) {
207
+ await opts.authStore.saveOAuthClientInfo(opts.server, fromSdkClientInfo(info));
208
+ },
209
+ async tokens() {
210
+ const saved = opts.authStore.getOAuthTokens(opts.server);
211
+ return saved ? toSdkTokens(saved) : void 0;
212
+ },
213
+ async saveTokens(tokens) {
214
+ await opts.authStore.saveOAuthTokens(opts.server, fromSdkTokens(tokens));
215
+ },
216
+ redirectToAuthorization(url) {
217
+ lastAuthUrl = url.toString();
218
+ },
219
+ saveCodeVerifier(v) {
220
+ codeVerifier = v;
221
+ },
222
+ async codeVerifier() {
223
+ if (!codeVerifier) throw new Error(`No PKCE codeVerifier set for "${opts.server}"`);
224
+ return codeVerifier;
225
+ },
226
+ peekAuthUrl() {
227
+ return lastAuthUrl;
228
+ },
229
+ clearAuthUrl() {
230
+ lastAuthUrl = void 0;
231
+ }
232
+ };
233
+ }
234
+
235
+ //#endregion
236
+ //#region src/auth/client-credentials.ts
237
+ /**
238
+ * Minimal OAuthClientProvider implementing only what's needed for clientCredentials:
239
+ * lazy fetches a token on `tokens()`. The SDK's transport uses `tokens()` to attach
240
+ * Authorization headers and re-calls on 401.
241
+ */
242
+ function createClientCredentialsProvider(opts) {
243
+ let cached;
244
+ return {
245
+ get redirectUrl() {
246
+ return ``;
247
+ },
248
+ get clientMetadata() {
249
+ return {};
250
+ },
251
+ async clientInformation() {
252
+ return {
253
+ client_id: opts.clientId,
254
+ client_secret: opts.clientSecret
255
+ };
256
+ },
257
+ async saveClientInformation() {},
258
+ async tokens() {
259
+ const now = Math.floor(Date.now() / 1e3);
260
+ if (cached && cached.expiresAt - 30 > now) return {
261
+ access_token: cached.access_token,
262
+ token_type: `Bearer`
263
+ };
264
+ const body = new URLSearchParams({
265
+ grant_type: `client_credentials`,
266
+ client_id: opts.clientId,
267
+ client_secret: opts.clientSecret
268
+ });
269
+ if (opts.scopes?.length) body.set(`scope`, opts.scopes.join(` `));
270
+ if (opts.audience) body.set(`audience`, opts.audience);
271
+ if (opts.resource) body.set(`resource`, opts.resource);
272
+ const res = await fetch(opts.tokenUrl, {
273
+ method: `POST`,
274
+ headers: { "Content-Type": `application/x-www-form-urlencoded` },
275
+ body
276
+ });
277
+ if (!res.ok) throw new Error(`clientCredentials token endpoint ${res.status}`);
278
+ const json = await res.json();
279
+ cached = {
280
+ access_token: json.access_token,
281
+ expiresAt: now + (json.expires_in ?? 300)
282
+ };
283
+ return {
284
+ access_token: json.access_token,
285
+ token_type: `Bearer`
286
+ };
287
+ },
288
+ async saveTokens() {},
289
+ redirectToAuthorization() {},
290
+ saveCodeVerifier() {},
291
+ async codeVerifier() {
292
+ return ``;
293
+ }
294
+ };
295
+ }
296
+
297
+ //#endregion
298
+ //#region src/registry.ts
299
+ function hashConfig(c) {
300
+ const parts = [
301
+ c.name,
302
+ c.transport,
303
+ c.url ?? ``,
304
+ c.auth?.mode ?? `none`,
305
+ String(c.timeoutMs ?? ``)
306
+ ];
307
+ if (c.auth && (c.auth.mode === `authorizationCode` || c.auth.mode === `clientCredentials`)) parts.push((c.auth.scopes ?? []).slice().sort().join(`,`));
308
+ if (c.transport === `stdio`) parts.push(c.command, (c.args ?? []).join(` `));
309
+ return parts.join(`|`);
310
+ }
311
+ function makeError(kind, message) {
312
+ return {
313
+ kind,
314
+ message
315
+ };
316
+ }
317
+ function createRegistry(opts) {
318
+ const entries = new Map();
319
+ const authStore = opts.authStore ?? createAuthStore();
320
+ const buildTransport = async (cfg) => {
321
+ if (cfg.transport === `stdio`) {
322
+ if (opts.transportFactoryOverride) return { transport: opts.transportFactoryOverride(cfg) };
323
+ return { transport: createStdioTransport({
324
+ name: cfg.name,
325
+ command: cfg.command,
326
+ args: cfg.args,
327
+ env: cfg.env
328
+ }) };
329
+ }
330
+ if (cfg.auth.mode === `none`) {
331
+ if (opts.transportFactoryOverride) return { transport: opts.transportFactoryOverride(cfg) };
332
+ return { transport: createHttpTransport({
333
+ name: cfg.name,
334
+ url: cfg.url
335
+ }) };
336
+ }
337
+ if (cfg.auth.mode === `apiKey`) {
338
+ if (opts.transportFactoryOverride) return { transport: opts.transportFactoryOverride(cfg) };
339
+ if (!cfg.auth.key) return { error: makeError(`auth_unavailable`, `no auth.key for ${cfg.name} (apiKey mode requires the secret inline)`) };
340
+ const header = buildApiKeyHeader(cfg.auth.key, {
341
+ headerName: cfg.auth.headerName,
342
+ valuePrefix: cfg.auth.valuePrefix
343
+ });
344
+ const headerProvider = async () => header;
345
+ return { transport: createHttpTransport({
346
+ name: cfg.name,
347
+ url: cfg.url,
348
+ headerProvider
349
+ }) };
350
+ }
351
+ if (cfg.auth.mode === `authorizationCode`) {
352
+ const publicUrl = opts.publicUrl ?? `http://localhost`;
353
+ const provider = createSdkOAuthProvider({
354
+ server: cfg.name,
355
+ publicUrl,
356
+ authStore,
357
+ scopes: cfg.auth.scopes,
358
+ redirectUri: cfg.auth.redirectUri,
359
+ resource: cfg.auth.resource
360
+ });
361
+ if (opts.transportFactoryOverride) return {
362
+ transport: opts.transportFactoryOverride(cfg, void 0, provider),
363
+ provider
364
+ };
365
+ return {
366
+ transport: createHttpTransport({
367
+ name: cfg.name,
368
+ url: cfg.url,
369
+ authProvider: provider
370
+ }),
371
+ provider
372
+ };
373
+ }
374
+ if (cfg.auth.mode === `clientCredentials`) {
375
+ if (!cfg.auth.clientId || !cfg.auth.clientSecret) return { error: makeError(`auth_unavailable`, `clientCredentials mode requires auth.clientId and auth.clientSecret inline for ${cfg.name}`) };
376
+ const ccProvider = createClientCredentialsProvider({
377
+ tokenUrl: cfg.auth.tokenUrl,
378
+ clientId: cfg.auth.clientId,
379
+ clientSecret: cfg.auth.clientSecret,
380
+ scopes: cfg.auth.scopes,
381
+ audience: cfg.auth.audience,
382
+ resource: cfg.auth.resource
383
+ });
384
+ if (opts.transportFactoryOverride) return { transport: opts.transportFactoryOverride(cfg, void 0, void 0) };
385
+ return { transport: createHttpTransport({
386
+ name: cfg.name,
387
+ url: cfg.url,
388
+ authProvider: ccProvider
389
+ }) };
390
+ }
391
+ return { error: makeError(`auth_unavailable`, `auth.mode=${cfg.auth.mode} not implemented`) };
392
+ };
393
+ const connectAndList = async (entry, provider) => {
394
+ if (!entry.transport) return {
395
+ state: `error`,
396
+ id: entry.config.name,
397
+ error: entry.error ?? makeError(`transport_error`, `no transport`)
398
+ };
399
+ try {
400
+ await entry.transport.connect();
401
+ const out = await entry.transport.client.listTools();
402
+ entry.tools = out.tools.map((t) => ({
403
+ name: t.name,
404
+ description: t.description,
405
+ inputSchema: t.inputSchema
406
+ }));
407
+ entry.capabilities = entry.transport.client.getServerCapabilities?.();
408
+ entry.status = `ready`;
409
+ notify();
410
+ return {
411
+ state: `ready`,
412
+ id: entry.config.name,
413
+ toolCount: entry.tools.length
414
+ };
415
+ } catch (err) {
416
+ const authUrl = provider?.peekAuthUrl();
417
+ if (authUrl) {
418
+ entry.status = `authenticating`;
419
+ entry.authUrl = authUrl;
420
+ entry.provider = provider;
421
+ notify();
422
+ try {
423
+ opts.openAuthorizeUrl?.(authUrl, entry.config.name);
424
+ } catch {}
425
+ return {
426
+ state: `authenticating`,
427
+ id: entry.config.name,
428
+ authUrl
429
+ };
430
+ }
431
+ entry.status = `error`;
432
+ const e = makeError(`transport_error`, err.message);
433
+ entry.error = e;
434
+ notify();
435
+ return {
436
+ state: `error`,
437
+ id: entry.config.name,
438
+ error: e
439
+ };
440
+ }
441
+ };
442
+ let seq = 0;
443
+ const subscribers = new Set();
444
+ const snapshot = () => ({
445
+ seq: ++seq,
446
+ servers: registry.list()
447
+ });
448
+ const notify = () => {
449
+ if (subscribers.size === 0) return;
450
+ const snap = snapshot();
451
+ for (const h of subscribers) try {
452
+ h(snap);
453
+ } catch {}
454
+ };
455
+ const registry = {
456
+ subscribe(handler) {
457
+ subscribers.add(handler);
458
+ try {
459
+ handler({
460
+ seq: 0,
461
+ servers: registry.list()
462
+ });
463
+ } catch {}
464
+ return () => {
465
+ subscribers.delete(handler);
466
+ };
467
+ },
468
+ async addServer(cfg) {
469
+ const existing = entries.get(cfg.name);
470
+ const hash = hashConfig(cfg);
471
+ if (existing && existing.configHash === hash && existing.status === `ready`) return {
472
+ state: `ready`,
473
+ id: cfg.name,
474
+ toolCount: existing.tools.length
475
+ };
476
+ if (existing) await Promise.resolve(existing.transport?.close()).catch(() => {});
477
+ if (cfg.auth?.mode === `authorizationCode`) {
478
+ if (cfg.auth.tokens) authStore.seedTokens(cfg.name, cfg.auth.tokens);
479
+ if (cfg.auth.client) authStore.seedClient(cfg.name, cfg.auth.client);
480
+ authStore.registerHooks(cfg.name, {
481
+ onTokensChanged: cfg.auth.onTokensChanged,
482
+ onClientRegistered: cfg.auth.onClientRegistered
483
+ });
484
+ }
485
+ const built = await buildTransport(cfg);
486
+ const entry = {
487
+ config: cfg,
488
+ configHash: hash,
489
+ status: built.transport ? `connecting` : `error`,
490
+ transport: built.transport,
491
+ error: built.error,
492
+ authUrl: built.authUrl,
493
+ tools: [],
494
+ provider: built.provider
495
+ };
496
+ entries.set(cfg.name, entry);
497
+ if (built.error) {
498
+ notify();
499
+ return {
500
+ state: `error`,
501
+ id: cfg.name,
502
+ error: built.error
503
+ };
504
+ }
505
+ if (built.authUrl) {
506
+ entry.status = `authenticating`;
507
+ notify();
508
+ try {
509
+ opts.openAuthorizeUrl?.(built.authUrl, cfg.name);
510
+ } catch {}
511
+ return {
512
+ state: `authenticating`,
513
+ id: cfg.name,
514
+ authUrl: built.authUrl
515
+ };
516
+ }
517
+ return await connectAndList(entry, built.provider);
518
+ },
519
+ async applyConfig(cfg) {
520
+ const seen = new Set(cfg.servers.map((s) => s.name));
521
+ const results = [];
522
+ for (const s of cfg.servers) results.push(await registry.addServer(s));
523
+ for (const name of [...entries.keys()]) if (!seen.has(name)) await registry.removeServer(name);
524
+ return results;
525
+ },
526
+ async removeServer(name) {
527
+ const e = entries.get(name);
528
+ if (!e) return;
529
+ await Promise.resolve(e.transport?.close()).catch(() => {});
530
+ entries.delete(name);
531
+ authStore.forget(name);
532
+ notify();
533
+ },
534
+ list() {
535
+ return [...entries.values()].map((e) => ({
536
+ name: e.config.name,
537
+ status: e.status,
538
+ toolCount: e.tools.length,
539
+ transport: e.config.transport,
540
+ authMode: e.config.auth?.mode,
541
+ authUrl: e.authUrl,
542
+ error: e.error,
543
+ tools: e.tools,
544
+ capabilities: e.capabilities
545
+ }));
546
+ },
547
+ get(name) {
548
+ return entries.get(name);
549
+ },
550
+ async finishAuth(serverName, code, _state) {
551
+ const e = entries.get(serverName);
552
+ if (!e) throw new Error(`unknown server "${serverName}"`);
553
+ const provider = e.provider;
554
+ if (!provider) throw new Error(`server "${serverName}" has no OAuth provider`);
555
+ const serverUrl = e.config.url;
556
+ if (!serverUrl) throw new Error(`server "${serverName}" has no URL — cannot complete token exchange`);
557
+ const { auth } = await import(`@modelcontextprotocol/sdk/client/auth.js`);
558
+ await auth(provider, {
559
+ serverUrl,
560
+ authorizationCode: code
561
+ });
562
+ provider.clearAuthUrl();
563
+ return await registry.addServer(e.config);
564
+ },
565
+ async disable(name) {
566
+ const e = entries.get(name);
567
+ if (!e) throw new Error(`unknown server "${name}"`);
568
+ await Promise.resolve(e.transport?.close()).catch(() => {});
569
+ e.transport = void 0;
570
+ e.tools = [];
571
+ e.authUrl = void 0;
572
+ e.status = `disabled`;
573
+ e.error = void 0;
574
+ notify();
575
+ },
576
+ async enable(name) {
577
+ const e = entries.get(name);
578
+ if (!e) throw new Error(`unknown server "${name}"`);
579
+ if (e.status !== `disabled`) return {
580
+ state: `ready`,
581
+ id: name,
582
+ toolCount: e.tools.length
583
+ };
584
+ return await registry.addServer(e.config);
585
+ },
586
+ async reauthorize(name) {
587
+ const e = entries.get(name);
588
+ if (!e) return;
589
+ if (e.config.auth?.mode !== `authorizationCode`) return;
590
+ if (e.status === `disabled`) return;
591
+ await Promise.resolve(e.transport?.close()).catch(() => {});
592
+ authStore.clearCredentials(name);
593
+ e.status = `connecting`;
594
+ e.tools = [];
595
+ e.capabilities = void 0;
596
+ e.authUrl = void 0;
597
+ e.error = void 0;
598
+ notify();
599
+ const built = await buildTransport(e.config);
600
+ e.transport = built.transport;
601
+ e.error = built.error;
602
+ e.authUrl = built.authUrl;
603
+ e.provider = built.provider;
604
+ if (built.error || !built.transport) {
605
+ e.status = `error`;
606
+ notify();
607
+ return;
608
+ }
609
+ if (built.authUrl) {
610
+ e.status = `authenticating`;
611
+ notify();
612
+ try {
613
+ opts.openAuthorizeUrl?.(built.authUrl, name);
614
+ } catch {}
615
+ return;
616
+ }
617
+ await connectAndList(e, built.provider);
618
+ },
619
+ async close() {
620
+ const transports = [...entries.values()].map((e) => e.transport).filter((t) => Boolean(t));
621
+ await Promise.all(transports.map((t) => Promise.resolve(t.close()).catch(() => {})));
622
+ for (const name of [...entries.keys()]) authStore.forget(name);
623
+ entries.clear();
624
+ notify();
625
+ }
626
+ };
627
+ return registry;
628
+ }
629
+
630
+ //#endregion
631
+ //#region src/config/env-expand.ts
632
+ const RE = /\$\{env:([A-Za-z_][A-Za-z0-9_]*)\}/g;
633
+ function expandString(s, env) {
634
+ const missing = [];
635
+ const value = s.replace(RE, (_, name) => {
636
+ const v = env[name];
637
+ if (v === void 0) {
638
+ missing.push(name);
639
+ return ``;
640
+ }
641
+ return v;
642
+ });
643
+ return {
644
+ value,
645
+ missing
646
+ };
647
+ }
648
+ function expandEnv(s, env = process.env) {
649
+ return expandString(s, env).value;
650
+ }
651
+ expandEnv.detailed = (s, env = process.env) => expandString(s, env);
652
+ expandEnv.deep = function deep(input, env = process.env) {
653
+ if (typeof input === `string`) return expandString(input, env).value;
654
+ if (Array.isArray(input)) return input.map((x) => deep(x, env));
655
+ if (input && typeof input === `object`) {
656
+ const out = {};
657
+ for (const [k, v] of Object.entries(input)) out[k] = deep(v, env);
658
+ return out;
659
+ }
660
+ return input;
661
+ };
662
+
663
+ //#endregion
664
+ //#region src/config/loader.ts
665
+ const KNOWN_AUTH_MODES = new Set([
666
+ `none`,
667
+ `apiKey`,
668
+ `clientCredentials`,
669
+ `authorizationCode`
670
+ ]);
671
+ const FORBIDDEN_REF_KEYS = [
672
+ `valueRef`,
673
+ `clientIdRef`,
674
+ `clientSecretRef`
675
+ ];
676
+ function fail(msg) {
677
+ throw new Error(`mcp.json: ${msg}`);
678
+ }
679
+ function parseConfig(raw, env = process.env) {
680
+ if (!raw || typeof raw !== `object`) fail(`not an object`);
681
+ const top = Object.keys(raw);
682
+ for (const k of top) if (k !== `servers`) fail(`unknown top-level field "${k}"`);
683
+ const serversObj = raw.servers;
684
+ if (!serversObj || typeof serversObj !== `object`) fail(`missing "servers" object`);
685
+ const servers = [];
686
+ for (const [name, entry] of Object.entries(serversObj)) {
687
+ if (!entry || typeof entry !== `object`) fail(`server "${name}" not an object`);
688
+ const e = entry;
689
+ if (e.transport !== `http` && e.transport !== `stdio`) fail(`server "${name}" transport must be 'http' or 'stdio'`);
690
+ const auth = e.auth ?? { mode: `none` };
691
+ if (typeof auth.mode !== `string` || !KNOWN_AUTH_MODES.has(auth.mode)) fail(`server "${name}" auth.mode invalid`);
692
+ for (const k of FORBIDDEN_REF_KEYS) if (k in auth) fail(`server "${name}" uses forbidden "${k}" — secrets are not configured in mcp.json (pass them inline on the auth config at the call site)`);
693
+ if (e.transport === `http`) {
694
+ if (typeof e.url !== `string`) fail(`server "${name}" missing url`);
695
+ servers.push({
696
+ name,
697
+ transport: `http`,
698
+ url: expandEnv(e.url, env),
699
+ auth: expandEnv.deep(auth, env),
700
+ timeoutMs: typeof e.timeoutMs === `number` ? e.timeoutMs : void 0
701
+ });
702
+ } else {
703
+ if (typeof e.command !== `string`) fail(`server "${name}" missing command`);
704
+ const args = Array.isArray(e.args) ? e.args.map((a) => expandEnv(String(a), env)) : [];
705
+ servers.push({
706
+ name,
707
+ transport: `stdio`,
708
+ command: expandEnv(e.command, env),
709
+ args,
710
+ env: e.env && typeof e.env === `object` ? Object.fromEntries(Object.entries(e.env).map(([k, v]) => [k, expandEnv(String(v), env)])) : void 0,
711
+ auth: expandEnv.deep(auth, env),
712
+ timeoutMs: typeof e.timeoutMs === `number` ? e.timeoutMs : void 0
713
+ });
714
+ }
715
+ }
716
+ return {
717
+ servers,
718
+ raw
719
+ };
720
+ }
721
+ async function loadConfig(path$1, env = process.env) {
722
+ const text = await node_fs_promises.default.readFile(path$1, `utf-8`);
723
+ return parseConfig(JSON.parse(text), env);
724
+ }
725
+
726
+ //#endregion
727
+ //#region src/config/watcher.ts
728
+ /**
729
+ * Start watching `path` for changes. Each modification triggers
730
+ * `loadConfig(path)` (debounced) and forwards the parsed config to
731
+ * `onChange`, or any error to `onError`. The caller is responsible
732
+ * for performing the initial load — `watchConfig` only sets up the
733
+ * subscription so the caller can fully await its first apply before
734
+ * subsequent change events start firing.
735
+ */
736
+ async function watchConfig(path$1, opts) {
737
+ const debounce = opts.debounceMs ?? 200;
738
+ let timer;
739
+ const reload = async () => {
740
+ try {
741
+ const cfg = await loadConfig(path$1, opts.env);
742
+ opts.onChange(cfg);
743
+ } catch (err) {
744
+ opts.onError?.(err);
745
+ }
746
+ };
747
+ const watcher = node_fs.default.watch(path$1, () => {
748
+ if (timer) clearTimeout(timer);
749
+ timer = setTimeout(reload, debounce);
750
+ });
751
+ return () => {
752
+ if (timer) clearTimeout(timer);
753
+ watcher.close();
754
+ };
755
+ }
756
+
757
+ //#endregion
758
+ //#region src/transports/timeout.ts
759
+ const DEFAULT_TIMEOUT_MS = 3e4;
760
+ var TimeoutError = class extends Error {
761
+ kind = `timeout`;
762
+ constructor(ms) {
763
+ super(`MCP tool call timed out after ${ms}ms`);
764
+ }
765
+ };
766
+ function withTimeout(p, ms) {
767
+ let timer;
768
+ const guard = new Promise((_, reject) => {
769
+ timer = setTimeout(() => reject(new TimeoutError(ms)), ms);
770
+ });
771
+ return Promise.race([p, guard]).finally(() => {
772
+ if (timer) clearTimeout(timer);
773
+ });
774
+ }
775
+
776
+ //#endregion
777
+ //#region src/bridge/tool-bridge.ts
778
+ const PREFIX = `mcp`;
779
+ const MAX_LEN = 128;
780
+ function sanitize(name) {
781
+ return name.replace(/[^A-Za-z0-9_-]/g, `_`);
782
+ }
783
+ function prefixToolName(server, tool) {
784
+ const full = `${PREFIX}__${sanitize(server)}__${sanitize(tool)}`;
785
+ return full.length > MAX_LEN ? full.slice(0, MAX_LEN) : full;
786
+ }
787
+ /**
788
+ * Coerce an MCP tool's inputSchema into a shape downstream LLM adapters can
789
+ * consume safely. Some servers send `{ type: 'object' }` with no `properties`
790
+ * for no-arg tools; pi-agent-core walks `inputSchema.properties` and crashes on
791
+ * undefined. We default `properties` to `{}` and `required` to `[]` for object
792
+ * schemas; non-object schemas pass through unchanged.
793
+ */
794
+ function normalizeInputSchema(schema) {
795
+ if (!schema || typeof schema !== `object`) return {
796
+ type: `object`,
797
+ properties: {},
798
+ required: []
799
+ };
800
+ const s = schema;
801
+ if (s.type !== `object`) return schema;
802
+ if (s.properties && typeof s.properties === `object`) return schema;
803
+ return {
804
+ ...s,
805
+ properties: {},
806
+ required: Array.isArray(s.required) ? s.required : []
807
+ };
808
+ }
809
+ /**
810
+ * Build a BridgedTool from a synthetic (non-MCP-server-backed) call. Used by
811
+ * the resource and prompt bridges. Caller supplies the JSON schema directly.
812
+ */
813
+ function makeSyntheticBridgedTool(opts) {
814
+ return {
815
+ name: opts.name,
816
+ server: opts.server,
817
+ description: opts.description,
818
+ inputSchema: opts.schema,
819
+ parameters: opts.schema,
820
+ label: opts.label,
821
+ async call(args) {
822
+ return await opts.run(args);
823
+ },
824
+ async execute(_toolCallId, params, signal) {
825
+ const result = await opts.run(params, signal);
826
+ return {
827
+ content: Array.isArray(result?.content) ? result.content : [],
828
+ details: result
829
+ };
830
+ }
831
+ };
832
+ }
833
+ function bridgeMcpTool(opts) {
834
+ const name = prefixToolName(opts.server, opts.tool.name);
835
+ const schema = normalizeInputSchema(opts.tool.inputSchema);
836
+ const ms = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
837
+ const invoke = async (args, extra) => {
838
+ const onProgress = extra?.onProgress ?? opts.onProgress;
839
+ const signal = extra?.signal ?? opts.signal;
840
+ const callArgs = onProgress !== void 0 || signal !== void 0 ? [
841
+ {
842
+ name: opts.tool.name,
843
+ arguments: args
844
+ },
845
+ void 0,
846
+ {
847
+ onProgress,
848
+ signal
849
+ }
850
+ ] : [{
851
+ name: opts.tool.name,
852
+ arguments: args
853
+ }];
854
+ return await withTimeout(opts.client.callTool(...callArgs), ms);
855
+ };
856
+ return {
857
+ name,
858
+ server: opts.server,
859
+ description: opts.tool.description,
860
+ inputSchema: schema,
861
+ parameters: schema,
862
+ label: opts.tool.name,
863
+ async call(args) {
864
+ try {
865
+ return await invoke(args);
866
+ } catch (err) {
867
+ const e = err;
868
+ if (e.kind === `timeout`) throw err;
869
+ const wrapped = {
870
+ kind: `transport_error`,
871
+ message: e.message ?? String(err)
872
+ };
873
+ throw wrapped;
874
+ }
875
+ },
876
+ async execute(_toolCallId, params, signal) {
877
+ const result = await invoke(params, { signal });
878
+ return {
879
+ content: Array.isArray(result?.content) ? result.content : [],
880
+ details: result
881
+ };
882
+ }
883
+ };
884
+ }
885
+
886
+ //#endregion
887
+ //#region src/bridge/resource-bridge.ts
888
+ function buildResourceTools(opts) {
889
+ const ms = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
890
+ return [makeSyntheticBridgedTool({
891
+ name: prefixToolName(opts.server, `list_resources`),
892
+ server: opts.server,
893
+ label: `list_resources`,
894
+ description: `List resources on ${opts.server}`,
895
+ schema: {
896
+ type: `object`,
897
+ properties: {},
898
+ required: []
899
+ },
900
+ run: () => withTimeout(opts.client.listResources(), ms)
901
+ }), makeSyntheticBridgedTool({
902
+ name: prefixToolName(opts.server, `read_resource`),
903
+ server: opts.server,
904
+ label: `read_resource`,
905
+ description: `Read a resource from ${opts.server}`,
906
+ schema: {
907
+ type: `object`,
908
+ properties: { uri: { type: `string` } },
909
+ required: [`uri`]
910
+ },
911
+ run: (args) => withTimeout(opts.client.readResource(args), ms)
912
+ })];
913
+ }
914
+
915
+ //#endregion
916
+ //#region src/bridge/prompt-bridge.ts
917
+ function buildPromptTools(opts) {
918
+ const ms = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
919
+ return [makeSyntheticBridgedTool({
920
+ name: prefixToolName(opts.server, `list_prompts`),
921
+ server: opts.server,
922
+ label: `list_prompts`,
923
+ description: `List prompts on ${opts.server}`,
924
+ schema: {
925
+ type: `object`,
926
+ properties: {},
927
+ required: []
928
+ },
929
+ run: () => withTimeout(opts.client.listPrompts(), ms)
930
+ }), makeSyntheticBridgedTool({
931
+ name: prefixToolName(opts.server, `get_prompt`),
932
+ server: opts.server,
933
+ label: `get_prompt`,
934
+ description: `Get a prompt template from ${opts.server}`,
935
+ schema: {
936
+ type: `object`,
937
+ properties: {
938
+ name: { type: `string` },
939
+ arguments: {
940
+ type: `object`,
941
+ additionalProperties: true
942
+ }
943
+ },
944
+ required: [`name`]
945
+ },
946
+ run: (args) => withTimeout(opts.client.getPrompt(args), ms)
947
+ })];
948
+ }
949
+
950
+ //#endregion
951
+ //#region src/persistence/keychain.ts
952
+ async function run(cmd, args, stdin) {
953
+ return new Promise((resolve) => {
954
+ const proc = (0, node_child_process.spawn)(cmd, args, { stdio: [
955
+ `pipe`,
956
+ `pipe`,
957
+ `pipe`
958
+ ] });
959
+ const out = [];
960
+ const err = [];
961
+ proc.stdout.on(`data`, (b) => {
962
+ out.push(b);
963
+ });
964
+ proc.stderr.on(`data`, (b) => {
965
+ err.push(b);
966
+ });
967
+ proc.on(`error`, (e) => {
968
+ resolve({
969
+ stdout: ``,
970
+ stderr: e.message,
971
+ code: -1
972
+ });
973
+ });
974
+ proc.on(`close`, (code) => {
975
+ resolve({
976
+ stdout: Buffer.concat(out).toString(`utf8`),
977
+ stderr: Buffer.concat(err).toString(`utf8`),
978
+ code
979
+ });
980
+ });
981
+ if (stdin != null) proc.stdin.end(stdin);
982
+ else proc.stdin.end();
983
+ });
984
+ }
985
+ const macosBackend = {
986
+ async get(service, account) {
987
+ const r = await run(`security`, [
988
+ `find-generic-password`,
989
+ `-s`,
990
+ service,
991
+ `-a`,
992
+ account,
993
+ `-w`
994
+ ]);
995
+ if (r.code === 0) return r.stdout.replace(/\n$/, ``);
996
+ if (/could not be found/i.test(r.stderr)) return void 0;
997
+ throw new Error(`security find-generic-password failed: ${r.stderr.trim()}`);
998
+ },
999
+ async set(service, account, value) {
1000
+ const r = await run(`security`, [
1001
+ `add-generic-password`,
1002
+ `-s`,
1003
+ service,
1004
+ `-a`,
1005
+ account,
1006
+ `-w`,
1007
+ value,
1008
+ `-U`
1009
+ ]);
1010
+ if (r.code !== 0) throw new Error(`security add-generic-password failed: ${r.stderr.trim()}`);
1011
+ }
1012
+ };
1013
+ const linuxBackend = {
1014
+ async get(service, account) {
1015
+ const r = await run(`secret-tool`, [
1016
+ `lookup`,
1017
+ `service`,
1018
+ service,
1019
+ `account`,
1020
+ account
1021
+ ]);
1022
+ if (r.code === 0) return r.stdout.replace(/\n$/, ``);
1023
+ if (r.code === 1 && r.stderr.trim() === ``) return void 0;
1024
+ throw new Error(`secret-tool lookup failed: ${r.stderr.trim()}`);
1025
+ },
1026
+ async set(service, account, value) {
1027
+ const r = await run(`secret-tool`, [
1028
+ `store`,
1029
+ `--label`,
1030
+ `${service}/${account}`,
1031
+ `service`,
1032
+ service,
1033
+ `account`,
1034
+ account
1035
+ ], value);
1036
+ if (r.code !== 0) throw new Error(`secret-tool store failed: ${r.stderr.trim()}`);
1037
+ }
1038
+ };
1039
+ function pickBackend() {
1040
+ if (process.platform === `darwin`) return macosBackend;
1041
+ if (process.platform === `linux`) return linuxBackend;
1042
+ return void 0;
1043
+ }
1044
+ /**
1045
+ * Opt-in helper for OAuth-mode `auth` configs. Loads any persisted tokens
1046
+ * and DCR client info from the OS keychain on startup, and returns the
1047
+ * matching `onTokensChanged` / `onClientRegistered` callbacks so the SDK
1048
+ * writes refreshed material back.
1049
+ *
1050
+ * const honeycomb = await keychainPersistence({ server: 'honeycomb' })
1051
+ * await mcpRegistry.addServer({
1052
+ * name: 'honeycomb',
1053
+ * transport: 'http',
1054
+ * url: 'https://mcp.honeycomb.io/mcp',
1055
+ * auth: {
1056
+ * mode: 'authorizationCode',
1057
+ * flow: 'browser',
1058
+ * scopes: ['mcp:read'],
1059
+ * ...honeycomb,
1060
+ * },
1061
+ * })
1062
+ *
1063
+ * Backend is chosen by `process.platform`:
1064
+ * - darwin → `/usr/bin/security` (no extra deps)
1065
+ * - linux → `secret-tool` from libsecret-tools (apt: libsecret-tools)
1066
+ * - win32 → not implemented yet — falls back to no-op callbacks
1067
+ *
1068
+ * If the chosen CLI isn't installed (e.g. minimal Linux container without
1069
+ * libsecret), reads/writes throw on first use; the registry surfaces
1070
+ * that as a connect-time error and the OAuth flow continues without
1071
+ * persistence.
1072
+ */
1073
+ async function keychainPersistence(opts) {
1074
+ const service = opts.service ?? `electric-agents`;
1075
+ const backend = opts.backend ?? pickBackend();
1076
+ if (!backend) {
1077
+ console.warn(`[agents-mcp] keychainPersistence: ${process.platform} not supported yet — ${opts.server} OAuth tokens will not persist`);
1078
+ return {
1079
+ onTokensChanged: async () => {},
1080
+ onClientRegistered: async () => {}
1081
+ };
1082
+ }
1083
+ const [tokensRaw, clientRaw] = await Promise.all([backend.get(service, `tokens:${opts.server}`), backend.get(service, `client:${opts.server}`)]);
1084
+ return {
1085
+ tokens: tokensRaw ? JSON.parse(tokensRaw) : void 0,
1086
+ client: clientRaw ? JSON.parse(clientRaw) : void 0,
1087
+ onTokensChanged: async (t) => {
1088
+ await backend.set(service, `tokens:${opts.server}`, JSON.stringify(t));
1089
+ },
1090
+ onClientRegistered: async (c) => {
1091
+ await backend.set(service, `client:${opts.server}`, JSON.stringify(c));
1092
+ }
1093
+ };
1094
+ }
1095
+
1096
+ //#endregion
1097
+ //#region src/persistence/file.ts
1098
+ async function readSafe(file) {
1099
+ try {
1100
+ const stat = await node_fs_promises.default.stat(file);
1101
+ if ((stat.mode & 511) !== 384) throw new Error(`${file} has permissions ${(stat.mode & 511).toString(8)}; refusing to read (require 0600).`);
1102
+ const text = await node_fs_promises.default.readFile(file, `utf-8`);
1103
+ return text.trim() ? JSON.parse(text) : {};
1104
+ } catch (err) {
1105
+ if (err.code === `ENOENT`) return {};
1106
+ throw err;
1107
+ }
1108
+ }
1109
+ async function writeSafe(file, data) {
1110
+ await node_fs_promises.default.mkdir(node_path.default.dirname(file), { recursive: true });
1111
+ const tmp = `${file}.tmp`;
1112
+ await node_fs_promises.default.writeFile(tmp, JSON.stringify(data, null, 2), { mode: 384 });
1113
+ await node_fs_promises.default.rename(tmp, file);
1114
+ }
1115
+ /**
1116
+ * Opt-in helper for OAuth-mode `auth` configs. Mirrors `keychainPersistence`
1117
+ * but persists to a JSON file on disk (mode 0600). Right tool when no OS
1118
+ * keychain is available — CI runners, minimal Linux containers, etc.
1119
+ *
1120
+ * const honeycomb = await filePersistence({
1121
+ * path: './.electric-agents/credentials.json',
1122
+ * server: 'honeycomb',
1123
+ * })
1124
+ * await mcpRegistry.addServer({ ..., auth: { ..., ...honeycomb } })
1125
+ */
1126
+ async function filePersistence(opts) {
1127
+ const data = await readSafe(opts.path);
1128
+ return {
1129
+ tokens: data.tokens?.[opts.server],
1130
+ client: data.client?.[opts.server],
1131
+ onTokensChanged: async (t) => {
1132
+ const cur = await readSafe(opts.path);
1133
+ cur.tokens = {
1134
+ ...cur.tokens ?? {},
1135
+ [opts.server]: t
1136
+ };
1137
+ await writeSafe(opts.path, cur);
1138
+ },
1139
+ onClientRegistered: async (c) => {
1140
+ const cur = await readSafe(opts.path);
1141
+ cur.client = {
1142
+ ...cur.client ?? {},
1143
+ [opts.server]: c
1144
+ };
1145
+ await writeSafe(opts.path, cur);
1146
+ }
1147
+ };
1148
+ }
1149
+
1150
+ //#endregion
1151
+ //#region src/index.ts
1152
+ const VERSION = `0.1.0`;
1153
+
1154
+ //#endregion
1155
+ exports.MCP_TOOLS_SENTINEL = MCP_TOOLS_SENTINEL
1156
+ exports.VERSION = VERSION
1157
+ exports.bridgeMcpTool = bridgeMcpTool
1158
+ exports.buildPromptTools = buildPromptTools
1159
+ exports.buildResourceTools = buildResourceTools
1160
+ exports.createRegistry = createRegistry
1161
+ exports.filePersistence = filePersistence
1162
+ exports.filterByAllowlist = filterByAllowlist
1163
+ exports.isMcpToolsSentinel = isMcpToolsSentinel
1164
+ exports.keychainPersistence = keychainPersistence
1165
+ exports.loadConfig = loadConfig
1166
+ exports.mcp = mcp
1167
+ exports.parseConfig = parseConfig
1168
+ exports.prefixToolName = prefixToolName
1169
+ exports.watchConfig = watchConfig