@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/LICENSE +177 -0
- package/dist/index.cjs +1169 -0
- package/dist/index.d.cts +515 -0
- package/dist/index.d.ts +515 -0
- package/dist/index.js +1131 -0
- package/package.json +48 -0
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
|