@exposify/express 0.3.7 → 0.3.9
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/README.md +284 -7
- package/dist/cli.js +295 -0
- package/dist/index.cjs +476 -49
- package/dist/index.d.cts +30 -2
- package/dist/index.d.ts +30 -2
- package/dist/index.js +494 -45
- package/dist/scan-preload.cjs +7120 -0
- package/dist/scan-preload.js +7124 -0
- package/package.json +8 -4
package/dist/index.cjs
CHANGED
|
@@ -20,21 +20,27 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
AIREADY_OPTIONS_TAG: () => AIREADY_OPTIONS_TAG,
|
|
23
24
|
aiReady: () => aiReady,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
completeMcpLogin: () => import_core7.completeMcpLogin,
|
|
26
|
+
createHostedAuthResolver: () => import_core7.createHostedAuthResolver,
|
|
27
|
+
describe: () => import_core7.describe,
|
|
28
|
+
exposed: () => import_core7.exposed,
|
|
29
|
+
hidden: () => import_core7.hidden,
|
|
30
|
+
hostedBroker: () => import_core7.hostedBroker,
|
|
27
31
|
routesFromExpress: () => routesFromExpress
|
|
28
32
|
});
|
|
29
33
|
module.exports = __toCommonJS(index_exports);
|
|
30
|
-
var
|
|
31
|
-
var
|
|
34
|
+
var import_node_path3 = require("path");
|
|
35
|
+
var import_core6 = require("@exposify/core");
|
|
32
36
|
var import_node2 = require("@exposify/core/node");
|
|
33
37
|
|
|
34
38
|
// src/surface.ts
|
|
35
|
-
var
|
|
36
|
-
var
|
|
37
|
-
var
|
|
39
|
+
var import_promises2 = require("fs/promises");
|
|
40
|
+
var import_node_path2 = require("path");
|
|
41
|
+
var import_node_async_hooks = require("async_hooks");
|
|
42
|
+
var import_node_crypto = require("crypto");
|
|
43
|
+
var import_node_http = require("http");
|
|
38
44
|
var import_node = require("@exposify/core/node");
|
|
39
45
|
|
|
40
46
|
// src/introspect.ts
|
|
@@ -59,6 +65,7 @@ function installMountRecorder(routerCtor) {
|
|
|
59
65
|
const wrapper = function(...args) {
|
|
60
66
|
try {
|
|
61
67
|
recordUse(this, args);
|
|
68
|
+
recordTaggedOwner(this, args);
|
|
62
69
|
} catch {
|
|
63
70
|
}
|
|
64
71
|
return originalUse.apply(this, args);
|
|
@@ -68,6 +75,21 @@ function installMountRecorder(routerCtor) {
|
|
|
68
75
|
} catch {
|
|
69
76
|
}
|
|
70
77
|
}
|
|
78
|
+
var MOUNT_OWNER_TAG = /* @__PURE__ */ Symbol.for("exposify.express.recordMountOwner");
|
|
79
|
+
var taggedOwners = /* @__PURE__ */ new WeakMap();
|
|
80
|
+
function recordTaggedOwner(parent, args) {
|
|
81
|
+
const stack = [...args];
|
|
82
|
+
while (stack.length > 0) {
|
|
83
|
+
const arg = stack.pop();
|
|
84
|
+
if (Array.isArray(arg)) stack.push(...arg);
|
|
85
|
+
else if (typeof arg === "function" && arg[MOUNT_OWNER_TAG]) {
|
|
86
|
+
if (!taggedOwners.has(arg)) taggedOwners.set(arg, parent);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function getMountOwner(fn) {
|
|
91
|
+
return taggedOwners.get(fn);
|
|
92
|
+
}
|
|
71
93
|
function getRecordedMounts(handle) {
|
|
72
94
|
if (typeof handle !== "function") return void 0;
|
|
73
95
|
return recordedMounts.get(handle);
|
|
@@ -132,6 +154,7 @@ function flattenHandlers(items) {
|
|
|
132
154
|
}
|
|
133
155
|
|
|
134
156
|
// src/introspect.ts
|
|
157
|
+
var routeHandlers = /* @__PURE__ */ new WeakMap();
|
|
135
158
|
function routesFromExpress(app, opts) {
|
|
136
159
|
const router = routerOf(app);
|
|
137
160
|
const out = [];
|
|
@@ -184,13 +207,15 @@ function walk(stack, prefix, owner, out, walked, unresolved) {
|
|
|
184
207
|
const marker = handlers.map(import_core.getMarker).find(Boolean);
|
|
185
208
|
for (const [method, enabled] of Object.entries(layer.route.methods)) {
|
|
186
209
|
if (!enabled || method === "_all") continue;
|
|
187
|
-
|
|
210
|
+
const route = {
|
|
188
211
|
method: method.toUpperCase(),
|
|
189
212
|
path,
|
|
190
213
|
params: extractParams(path),
|
|
191
214
|
marker,
|
|
192
215
|
description: marker?.kind === "described" ? marker.description : void 0
|
|
193
|
-
}
|
|
216
|
+
};
|
|
217
|
+
routeHandlers.set(route, handlers);
|
|
218
|
+
out.push(route);
|
|
194
219
|
}
|
|
195
220
|
continue;
|
|
196
221
|
}
|
|
@@ -275,59 +300,298 @@ function ownerAndStackOf(handle) {
|
|
|
275
300
|
return void 0;
|
|
276
301
|
}
|
|
277
302
|
|
|
303
|
+
// src/descriptions.ts
|
|
304
|
+
var import_promises = require("fs/promises");
|
|
305
|
+
var import_node_path = require("path");
|
|
306
|
+
var import_core2 = require("@exposify/core");
|
|
307
|
+
var import_core3 = require("@exposify/core");
|
|
308
|
+
async function loadDescriptions(cwd, option, warn) {
|
|
309
|
+
if (option === false) return /* @__PURE__ */ new Map();
|
|
310
|
+
const path = (0, import_node_path.join)(cwd, option ?? import_core2.DESCRIPTIONS_FILE);
|
|
311
|
+
let text;
|
|
312
|
+
try {
|
|
313
|
+
text = await (0, import_promises.readFile)(path, "utf8");
|
|
314
|
+
} catch {
|
|
315
|
+
if (option !== void 0) warn(`descriptions file not found: ${path}`);
|
|
316
|
+
return /* @__PURE__ */ new Map();
|
|
317
|
+
}
|
|
318
|
+
const parsed = (0, import_core2.parseDescriptions)(text);
|
|
319
|
+
if (typeof parsed === "string") {
|
|
320
|
+
warn(`ignoring ${path}: ${parsed}`);
|
|
321
|
+
return /* @__PURE__ */ new Map();
|
|
322
|
+
}
|
|
323
|
+
return new Map(Object.entries(parsed.descriptions).map(([key, entry]) => [key, entry.text.trim()]));
|
|
324
|
+
}
|
|
325
|
+
async function loadOpenApi(cwd, option, warn) {
|
|
326
|
+
if (option === void 0) return void 0;
|
|
327
|
+
if (typeof option === "object") return option;
|
|
328
|
+
const path = (0, import_node_path.join)(cwd, option);
|
|
329
|
+
if (/\.ya?ml$/i.test(path)) {
|
|
330
|
+
warn(`openapi: YAML isn't supported (${path}); pass the parsed document object or a .json file`);
|
|
331
|
+
return void 0;
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
return JSON.parse(await (0, import_promises.readFile)(path, "utf8"));
|
|
335
|
+
} catch (err) {
|
|
336
|
+
warn(`openapi: couldn't read ${path}: ${err.message}`);
|
|
337
|
+
return void 0;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
278
341
|
// src/surface.ts
|
|
342
|
+
var import_core4 = require("@exposify/core");
|
|
343
|
+
var warnOnce = (m) => console.warn(`[agentready] warning: ${m}`);
|
|
344
|
+
var NULL_BODY_STATUSES = /* @__PURE__ */ new Set([101, 103, 204, 205, 304]);
|
|
345
|
+
var IDENTITY_HEADERS = ["host", "forwarded", "x-forwarded-for", "x-forwarded-proto", "x-forwarded-host", "x-forwarded-port"];
|
|
346
|
+
var DISPATCH_HEADER = "x-exposify-dispatch";
|
|
347
|
+
var callerStore = new import_node_async_hooks.AsyncLocalStorage();
|
|
348
|
+
function callerOf(req) {
|
|
349
|
+
const headers = {};
|
|
350
|
+
for (const name of IDENTITY_HEADERS) {
|
|
351
|
+
const value = req.headers[name];
|
|
352
|
+
if (typeof value === "string") headers[name] = value;
|
|
353
|
+
else if (Array.isArray(value)) headers[name] = value.join(", ");
|
|
354
|
+
}
|
|
355
|
+
const socket = req.socket;
|
|
356
|
+
return {
|
|
357
|
+
remoteAddress: socket.remoteAddress,
|
|
358
|
+
remoteFamily: socket.remoteFamily,
|
|
359
|
+
encrypted: socket.encrypted === true,
|
|
360
|
+
headers
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
function runAsCaller(caller, fn) {
|
|
364
|
+
return callerStore.run(caller, fn);
|
|
365
|
+
}
|
|
279
366
|
async function buildExpressSurface(app, cfg, cwd, opts) {
|
|
280
367
|
const info = await readPackageInfo(cwd);
|
|
281
|
-
const appish = app;
|
|
282
|
-
if (!appish.request || !appish.response) {
|
|
283
|
-
console.warn(
|
|
284
|
-
"[agentready] express request/response prototypes not found; light-my-request corruption guard inactive"
|
|
285
|
-
);
|
|
286
|
-
}
|
|
287
|
-
const reqProto = appish.request ? Object.getPrototypeOf(appish.request) : void 0;
|
|
288
|
-
const resProto = appish.response ? Object.getPrototypeOf(appish.response) : void 0;
|
|
289
|
-
const pristineReqProto = reqProto ? Object.getPrototypeOf(reqProto) : void 0;
|
|
290
|
-
const pristineResProto = resProto ? Object.getPrototypeOf(resProto) : void 0;
|
|
291
368
|
return {
|
|
292
369
|
info,
|
|
293
|
-
|
|
370
|
+
// Text precedence: describe() in code > exposify.descriptions.json (reviewed) > OpenAPI > derived.
|
|
371
|
+
routes: (0, import_core3.applyDescriptions)(
|
|
372
|
+
(0, import_core4.applyOpenApi)(routesFromExpress(app, opts), await loadOpenApi(cwd, opts?.openapi, warnOnce)),
|
|
373
|
+
await loadDescriptions(cwd, opts?.descriptions, warnOnce)
|
|
374
|
+
),
|
|
294
375
|
docs: await (0, import_node.loadDocsFromFs)(cfg.docs, cwd),
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
376
|
+
dispatch: appDispatcher(app)
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
var dispatchers = /* @__PURE__ */ new WeakMap();
|
|
380
|
+
function appDispatcher(app) {
|
|
381
|
+
const known = dispatchers.get(app);
|
|
382
|
+
if (known) return known;
|
|
383
|
+
const loopback = lazyLoopback(app);
|
|
384
|
+
const dispatch = async (req) => {
|
|
385
|
+
const url = new URL(req.url);
|
|
386
|
+
const payload = req.body ? Buffer.from(await req.arrayBuffer()) : void 0;
|
|
387
|
+
const caller = callerStore.getStore();
|
|
388
|
+
const headers = {
|
|
389
|
+
...caller?.headers ?? {},
|
|
390
|
+
...Object.fromEntries(req.headers.entries())
|
|
391
|
+
};
|
|
392
|
+
if (payload) headers["content-length"] = String(payload.length);
|
|
393
|
+
const { port, tag, release } = await loopback(caller);
|
|
394
|
+
headers[DISPATCH_HEADER] = tag;
|
|
395
|
+
const res = await new Promise((resolve, reject) => {
|
|
396
|
+
const outgoing = (0, import_node_http.request)(
|
|
397
|
+
{ host: "127.0.0.1", port, method: req.method, path: url.pathname + url.search, headers, agent: false },
|
|
398
|
+
resolve
|
|
399
|
+
);
|
|
400
|
+
outgoing.on("error", reject);
|
|
401
|
+
outgoing.end(payload);
|
|
402
|
+
}).catch((err) => {
|
|
403
|
+
release();
|
|
404
|
+
throw err;
|
|
405
|
+
});
|
|
406
|
+
const chunks = [];
|
|
407
|
+
for await (const chunk of res) chunks.push(chunk);
|
|
408
|
+
const responseHeaders = new Headers();
|
|
409
|
+
for (const [name, value] of Object.entries(res.headers)) {
|
|
410
|
+
if (Array.isArray(value)) for (const v of value) responseHeaders.append(name, v);
|
|
411
|
+
else if (value !== void 0) responseHeaders.set(name, value);
|
|
412
|
+
}
|
|
413
|
+
const status = res.statusCode ?? 502;
|
|
414
|
+
return new Response(NULL_BODY_STATUSES.has(status) ? null : Buffer.concat(chunks), {
|
|
415
|
+
status,
|
|
416
|
+
headers: responseHeaders
|
|
417
|
+
});
|
|
418
|
+
};
|
|
419
|
+
dispatchers.set(app, dispatch);
|
|
420
|
+
return dispatch;
|
|
421
|
+
}
|
|
422
|
+
function lazyLoopback(app) {
|
|
423
|
+
const secret = (0, import_node_crypto.randomBytes)(32);
|
|
424
|
+
const pending = /* @__PURE__ */ new Map();
|
|
425
|
+
let nextId = 0;
|
|
426
|
+
let port = null;
|
|
427
|
+
const listener = (req, res) => {
|
|
428
|
+
const raw = req.headers[DISPATCH_HEADER];
|
|
429
|
+
delete req.headers[DISPATCH_HEADER];
|
|
430
|
+
for (let i = req.rawHeaders.length - 2; i >= 0; i -= 2) {
|
|
431
|
+
if (req.rawHeaders[i].toLowerCase() === DISPATCH_HEADER) req.rawHeaders.splice(i, 2);
|
|
312
432
|
}
|
|
433
|
+
const [key, id] = typeof raw === "string" ? raw.split(".") : [];
|
|
434
|
+
const presented = Buffer.from(key ?? "", "hex");
|
|
435
|
+
if (!id || presented.length !== secret.length || !(0, import_node_crypto.timingSafeEqual)(presented, secret) || !pending.has(id)) {
|
|
436
|
+
res.writeHead(403).end();
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
const caller = pending.get(id);
|
|
440
|
+
pending.delete(id);
|
|
441
|
+
if (caller) {
|
|
442
|
+
const socket = req.socket;
|
|
443
|
+
Object.defineProperty(socket, "remoteAddress", { value: caller.remoteAddress, configurable: true });
|
|
444
|
+
Object.defineProperty(socket, "remoteFamily", { value: caller.remoteFamily, configurable: true });
|
|
445
|
+
if (caller.encrypted) Object.defineProperty(socket, "encrypted", { value: true, configurable: true });
|
|
446
|
+
}
|
|
447
|
+
app(req, res);
|
|
448
|
+
};
|
|
449
|
+
return async (caller) => {
|
|
450
|
+
port ??= new Promise((resolve, reject) => {
|
|
451
|
+
const server = (0, import_node_http.createServer)(listener);
|
|
452
|
+
server.once("error", reject);
|
|
453
|
+
server.listen(0, "127.0.0.1", () => {
|
|
454
|
+
server.unref();
|
|
455
|
+
resolve(server.address().port);
|
|
456
|
+
});
|
|
457
|
+
}).catch((err) => {
|
|
458
|
+
port = null;
|
|
459
|
+
throw err;
|
|
460
|
+
});
|
|
461
|
+
const resolved = await port;
|
|
462
|
+
const id = String(nextId++);
|
|
463
|
+
pending.set(id, caller);
|
|
464
|
+
return { port: resolved, tag: `${secret.toString("hex")}.${id}`, release: () => pending.delete(id) };
|
|
313
465
|
};
|
|
314
466
|
}
|
|
315
467
|
async function readPackageInfo(cwd) {
|
|
316
468
|
try {
|
|
317
|
-
const pkg = JSON.parse(await (0,
|
|
469
|
+
const pkg = JSON.parse(await (0, import_promises2.readFile)((0, import_node_path2.join)(cwd, "package.json"), "utf8"));
|
|
318
470
|
return { name: pkg.name ?? "app", version: pkg.version ?? "0.0.0" };
|
|
319
471
|
} catch {
|
|
320
472
|
return { name: "app", version: "0.0.0" };
|
|
321
473
|
}
|
|
322
474
|
}
|
|
323
475
|
|
|
476
|
+
// src/login.ts
|
|
477
|
+
var import_core5 = require("@exposify/core");
|
|
478
|
+
var SESSION_CODE = /\bres\.cookie\(|\.session\b|\.login\(|\blogIn\(|set-cookie|setHeader\(\s*["']set-cookie|\bjwt\.sign\(|new\s+SignJWT|\bsign\(|createSession|issueToken|generateToken/i;
|
|
479
|
+
function bodyFieldsOf(source) {
|
|
480
|
+
const names = /* @__PURE__ */ new Set();
|
|
481
|
+
for (const m of source.matchAll(/\b\w+\.body\.([A-Za-z_$][\w$]*)/g)) names.add(m[1]);
|
|
482
|
+
for (const m of source.matchAll(/\b\w+\.body\[\s*["'`]([^"'`]+)["'`]\s*\]/g)) names.add(m[1]);
|
|
483
|
+
for (const m of source.matchAll(/(?:const|let|var)\s*\{([^}]*)\}\s*=\s*\w+\.body\b/g)) {
|
|
484
|
+
for (const part of m[1].split(",")) {
|
|
485
|
+
const name = part.split(/[:=]/)[0].trim().replace(/^\.\.\./, "");
|
|
486
|
+
if (/^[A-Za-z_$][\w$]*$/.test(name)) names.add(name);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return [...names];
|
|
490
|
+
}
|
|
491
|
+
function sourceOf(fn) {
|
|
492
|
+
try {
|
|
493
|
+
return typeof fn === "function" ? Function.prototype.toString.call(fn) : "";
|
|
494
|
+
} catch {
|
|
495
|
+
return "";
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
function parsesJson(app) {
|
|
499
|
+
const stack = (app?._router ?? app?.router)?.stack;
|
|
500
|
+
if (!Array.isArray(stack)) return null;
|
|
501
|
+
const names = stack.map((l) => l.name ?? "");
|
|
502
|
+
if (names.includes("jsonParser")) return true;
|
|
503
|
+
if (names.includes("urlencodedParser")) return false;
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
function findExpressLogin(app, mounts) {
|
|
507
|
+
let routes;
|
|
508
|
+
try {
|
|
509
|
+
routes = routesFromExpress(app, { mounts });
|
|
510
|
+
} catch {
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
const candidates = [];
|
|
514
|
+
for (const route of routes) {
|
|
515
|
+
if (route.method !== "POST" || route.params.length > 0 || import_core5.NOT_A_LOGIN.test(route.path)) continue;
|
|
516
|
+
const handlers = routeHandlers.get(route) ?? [];
|
|
517
|
+
const own = sourceOf(handlers.at(-1));
|
|
518
|
+
const all = handlers.map(sourceOf).join("\n");
|
|
519
|
+
const fields = (0, import_core5.loginFields)(bodyFieldsOf(own));
|
|
520
|
+
if (!fields) continue;
|
|
521
|
+
const named = import_core5.LOGIN_NAME.test(route.path);
|
|
522
|
+
if (!named && !SESSION_CODE.test(all)) continue;
|
|
523
|
+
candidates.push({ login: { path: route.path, fields, body: parsesJson(app) === false ? "form" : "json" }, named });
|
|
524
|
+
}
|
|
525
|
+
if (candidates.length === 0) return null;
|
|
526
|
+
candidates.sort((a, b) => Number(b.named) - Number(a.named) || a.login.fields.length - b.login.fields.length);
|
|
527
|
+
return { login: candidates[0].login, others: candidates.slice(1).map((c) => `POST ${c.login.path}`) };
|
|
528
|
+
}
|
|
529
|
+
|
|
324
530
|
// src/index.ts
|
|
325
|
-
var
|
|
531
|
+
var import_core7 = require("@exposify/core");
|
|
532
|
+
var AIREADY_OPTIONS_TAG = /* @__PURE__ */ Symbol.for("exposify.express.aiReadyOptions");
|
|
326
533
|
var MCP_PATH = "/mcp";
|
|
534
|
+
var PRM_ROOT = "/.well-known/oauth-protected-resource";
|
|
535
|
+
var LOG_PREFIX = "[agentready]";
|
|
536
|
+
function toolsSummary(routes, docs, cfg) {
|
|
537
|
+
const counts = (0, import_core6.countTools)(routes, cfg);
|
|
538
|
+
const details = counts.hiddenWrites > 0 ? [`${counts.hiddenWrites} write route${counts.hiddenWrites === 1 ? "" : "s"} hidden (opt in with exposed())`] : [];
|
|
539
|
+
if (cfg.expose === "*") details.unshift('all write routes exposed (expose: "*")');
|
|
540
|
+
return { tools: (0, import_core6.routeToolsPhrase)(counts, docs), details, empty: counts.reads + counts.writes === 0 };
|
|
541
|
+
}
|
|
542
|
+
var NO_TOOLS_FIX = "register app.use(aiReady()) before your routers, list routers mounted earlier in the `mounts` option, or check whether routes are added later (e.g. after an await) -- those appear on the first /mcp request";
|
|
543
|
+
function prmPaths(resource) {
|
|
544
|
+
const resourcePath = new URL(resource).pathname;
|
|
545
|
+
return /* @__PURE__ */ new Set([PRM_ROOT, `${PRM_ROOT}${MCP_PATH}`, resourcePath === "/" ? PRM_ROOT : `${PRM_ROOT}${resourcePath}`]);
|
|
546
|
+
}
|
|
547
|
+
async function sendWebResponse(res, response) {
|
|
548
|
+
const body = response.body ? await response.text() : void 0;
|
|
549
|
+
res.writeHead(response.status, Object.fromEntries(response.headers)).end(body);
|
|
550
|
+
}
|
|
327
551
|
function aiReady(options = {}) {
|
|
328
|
-
const cfg = (0,
|
|
552
|
+
const cfg = (0, import_core6.resolveOptions)(options);
|
|
553
|
+
const gate = cfg.oauth ? (0, import_core6.createOAuthGate)(cfg.oauth) : null;
|
|
554
|
+
const metadataPaths = cfg.oauth ? prmPaths(cfg.oauth.resource) : /* @__PURE__ */ new Set();
|
|
555
|
+
const brokerGate = !gate && cfg.docsAuth === "required" ? (0, import_core6.createBrokerGate)(cfg.auth) : null;
|
|
556
|
+
const resourceOf = (req, path, mcpPath) => {
|
|
557
|
+
const original = String(req.originalUrl ?? req.url).split("?")[0];
|
|
558
|
+
const prefix = original.endsWith(path) ? original.slice(0, original.length - path.length) : "";
|
|
559
|
+
const local = `${req.protocol ?? "http"}://${req.headers.host ?? "localhost"}${prefix}${mcpPath}`;
|
|
560
|
+
return brokerGate.resourceFor((0, import_core6.publicRequestUrl)(local, headersOf(req)));
|
|
561
|
+
};
|
|
329
562
|
const cwd = options.cwd ?? process.cwd();
|
|
330
563
|
const mounts = options.mounts;
|
|
564
|
+
const brokerInfo = (0, import_core6.hostedResolverInfo)(cfg.auth);
|
|
565
|
+
let loginFound = null;
|
|
566
|
+
const loginOf = (app) => {
|
|
567
|
+
if (!brokerInfo || options.login === false) return null;
|
|
568
|
+
if (!loginFound) {
|
|
569
|
+
const given = options.login;
|
|
570
|
+
const value = given ? { path: given.path, fields: given.fields.map((name) => ({ name, kind: (0, import_core6.fieldKind)(name) })), body: given.body ?? "json" } : findExpressLogin(app, mounts)?.login ?? null;
|
|
571
|
+
loginFound = { value };
|
|
572
|
+
if (value) {
|
|
573
|
+
cfg.signedOutPaths = [value.path];
|
|
574
|
+
if (logReady) console.log(`${LOG_PREFIX} AI assistants sign in with your app's own login (POST ${value.path}) at ${MCP_PATH}/login`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return loginFound.value;
|
|
578
|
+
};
|
|
579
|
+
const appLoginFor = (req, login) => ({
|
|
580
|
+
label: `POST ${login.path}`,
|
|
581
|
+
fields: login.fields,
|
|
582
|
+
signedOutPaths: [login.path],
|
|
583
|
+
attempt: (values) => {
|
|
584
|
+
const body = login.body === "form" ? new URLSearchParams(values).toString() : JSON.stringify(values);
|
|
585
|
+
const request = new Request(`http://app.internal${login.path}`, {
|
|
586
|
+
method: "POST",
|
|
587
|
+
headers: { "content-type": login.body === "form" ? "application/x-www-form-urlencoded" : "application/json", ...req.headers["user-agent"] ? { "user-agent": String(req.headers["user-agent"]) } : {} },
|
|
588
|
+
body
|
|
589
|
+
});
|
|
590
|
+
return runAsCaller(callerOf(req), () => appDispatcher(req.app)(request)).then(import_core6.loginResultFromResponse);
|
|
591
|
+
}
|
|
592
|
+
});
|
|
593
|
+
const descriptions = options.descriptions;
|
|
594
|
+
const openapi = options.openapi;
|
|
331
595
|
try {
|
|
332
596
|
installMountRecorder();
|
|
333
597
|
} catch {
|
|
@@ -335,46 +599,209 @@ function aiReady(options = {}) {
|
|
|
335
599
|
let handlerPromise = null;
|
|
336
600
|
let disabled = false;
|
|
337
601
|
let warned = false;
|
|
338
|
-
|
|
602
|
+
const logReady = (0, import_core6.startupLogEnabled)(options.startupLog);
|
|
603
|
+
const auth = (0, import_core6.describeAuth)(cfg.auth, cfg.oauth);
|
|
604
|
+
let reported = null;
|
|
605
|
+
const logSurface = (routes, docs, note) => {
|
|
606
|
+
const summary = toolsSummary(routes, docs, cfg);
|
|
607
|
+
const key = [summary.tools, ...summary.details, docs].join("|");
|
|
608
|
+
if (key === reported) return;
|
|
609
|
+
const first = reported === null;
|
|
610
|
+
reported = key;
|
|
611
|
+
if (summary.empty) {
|
|
612
|
+
console.warn((0, import_core6.formatWarning)(LOG_PREFIX, `MCP is up at ${MCP_PATH} but exposes no route tools`, NO_TOOLS_FIX));
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
if (!logReady) return;
|
|
616
|
+
const details = [...summary.details, ...note && !first ? [note] : []];
|
|
617
|
+
console.log((0, import_core6.formatReady)(LOG_PREFIX, { endpoint: MCP_PATH, tools: summary.tools, details, docs, auth }));
|
|
618
|
+
};
|
|
619
|
+
const middleware = (req, res, next) => {
|
|
339
620
|
const path = req.path ?? req.url.split("?")[0];
|
|
621
|
+
if (brokerInfo && (path === `${MCP_PATH}/login` || path === `${MCP_PATH}/login/`)) {
|
|
622
|
+
const login = loginOf(req.app);
|
|
623
|
+
if (!login) return next();
|
|
624
|
+
void serveLoginPage(req, res, login).catch((err) => {
|
|
625
|
+
console.warn("[agentready] sign-in form failed:", err);
|
|
626
|
+
if (!res.headersSent) res.writeHead(503).end("sign-in is unavailable");
|
|
627
|
+
});
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
if (gate && metadataPaths.has(path)) {
|
|
631
|
+
void sendWebResponse(res, gate.metadataResponse(req.method));
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (brokerGate && (path === PRM_ROOT || path === `${PRM_ROOT}${MCP_PATH}`)) {
|
|
635
|
+
const resource = resourceOf(req, path, MCP_PATH);
|
|
636
|
+
void (0, import_core6.reportAppLogin)(cfg.auth, resource, loginOf(req.app)).then(() => sendWebResponse(res, brokerGate.metadataResponse(req.method, resource)));
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
340
639
|
if (path !== MCP_PATH && !path.startsWith(`${MCP_PATH}/`)) return next();
|
|
341
640
|
if (disabled) {
|
|
342
641
|
res.writeHead(503).end("agentready is disabled due to an internal error");
|
|
343
642
|
return;
|
|
344
643
|
}
|
|
345
|
-
|
|
346
|
-
|
|
644
|
+
const authorized = gate ? gate.check(headersOf(req)).then(async (result) => {
|
|
645
|
+
if (result.ok) return { claims: result.claims };
|
|
646
|
+
await sendWebResponse(res, result.response);
|
|
647
|
+
return null;
|
|
648
|
+
}, (err) => {
|
|
649
|
+
console.warn("[agentready] oauth gate failed:", err);
|
|
650
|
+
if (!res.headersSent) res.writeHead(503).end("authorization check failed");
|
|
651
|
+
return null;
|
|
652
|
+
}) : brokerGate ? (0, import_core6.reportAppLogin)(cfg.auth, resourceOf(req, path, MCP_PATH), loginOf(req.app)).then(() => brokerGate.check(headersOf(req), resourceOf(req, path, MCP_PATH))).then(async (result) => {
|
|
653
|
+
if (result.ok) return {};
|
|
654
|
+
await sendWebResponse(res, result.response);
|
|
655
|
+
return null;
|
|
656
|
+
}, (err) => {
|
|
657
|
+
console.warn("[agentready] broker check failed:", err);
|
|
658
|
+
if (!res.headersSent) res.writeHead(503).end("authorization check failed");
|
|
659
|
+
return null;
|
|
660
|
+
}) : Promise.resolve({});
|
|
661
|
+
authorized.then((authContext) => {
|
|
662
|
+
if (!authContext) return;
|
|
663
|
+
handlerPromise ??= initialize(
|
|
664
|
+
req.app,
|
|
665
|
+
cfg,
|
|
666
|
+
cwd,
|
|
667
|
+
mounts,
|
|
668
|
+
{ descriptions, openapi },
|
|
669
|
+
(surface) => logSurface(surface.routes, surface.docs.length, "updated: routes registered after startup")
|
|
670
|
+
);
|
|
671
|
+
return handlerPromise.then(
|
|
672
|
+
(handler) => (
|
|
673
|
+
// Tool calls made while handling this request reach the app with
|
|
674
|
+
// this caller's address and forwarding headers (see surface.ts).
|
|
675
|
+
runAsCaller(callerOf(req), () => handler(req, res, authContext))
|
|
676
|
+
)
|
|
677
|
+
);
|
|
678
|
+
}).catch((err) => {
|
|
347
679
|
disabled = true;
|
|
348
680
|
handlerPromise = null;
|
|
349
681
|
if (!warned) {
|
|
350
682
|
warned = true;
|
|
351
|
-
console.
|
|
683
|
+
console.error(
|
|
684
|
+
(0, import_core6.formatFailed)(
|
|
685
|
+
LOG_PREFIX,
|
|
686
|
+
errorMessage(err),
|
|
687
|
+
`${MCP_PATH} now answers 503 until the process restarts (your own routes are unaffected); full error below`
|
|
688
|
+
),
|
|
689
|
+
err
|
|
690
|
+
);
|
|
352
691
|
}
|
|
353
692
|
if (!res.headersSent) res.writeHead(503).end("agentready failed to initialize");
|
|
354
693
|
});
|
|
355
694
|
};
|
|
695
|
+
async function serveLoginPage(req, res, login) {
|
|
696
|
+
const original = String(req.originalUrl ?? req.url);
|
|
697
|
+
const local = `${req.protocol ?? "http"}://${req.headers.host ?? "localhost"}${original}`;
|
|
698
|
+
const url = new URL((0, import_core6.publicRequestUrl)(local, headersOf(req)));
|
|
699
|
+
url.search = new URL(local).search;
|
|
700
|
+
let body;
|
|
701
|
+
if (req.method === "POST") {
|
|
702
|
+
if (req.body && typeof req.body === "object" && Object.keys(req.body).length > 0) body = new URLSearchParams(req.body).toString();
|
|
703
|
+
else if (typeof req.body === "string") body = req.body;
|
|
704
|
+
else {
|
|
705
|
+
const chunks = [];
|
|
706
|
+
let size = 0;
|
|
707
|
+
for await (const chunk of req) {
|
|
708
|
+
size += chunk.length;
|
|
709
|
+
if (size > 16 * 1024) break;
|
|
710
|
+
chunks.push(chunk);
|
|
711
|
+
}
|
|
712
|
+
body = Buffer.concat(chunks).toString("utf8");
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
const request = new Request(url, { method: req.method, headers: headersOf(req), ...body !== void 0 ? { body } : {} });
|
|
716
|
+
const forwarded = String(req.headers["x-forwarded-for"] ?? "").split(",")[0]?.trim();
|
|
717
|
+
const ip = forwarded || req.socket?.remoteAddress;
|
|
718
|
+
const response = await (0, import_core6.handleLoginPage)(request, appLoginFor(req, login), brokerInfo, {
|
|
719
|
+
path: url.pathname,
|
|
720
|
+
secure: url.protocol === "https:",
|
|
721
|
+
origin: url.origin,
|
|
722
|
+
...ip ? { ip } : {}
|
|
723
|
+
});
|
|
724
|
+
await sendWebResponse(res, response);
|
|
725
|
+
}
|
|
726
|
+
middleware[MOUNT_OWNER_TAG] = true;
|
|
727
|
+
middleware[AIREADY_OPTIONS_TAG] = options;
|
|
728
|
+
setImmediate(() => {
|
|
729
|
+
void startupCheck().catch(() => {
|
|
730
|
+
});
|
|
731
|
+
});
|
|
732
|
+
async function startupCheck() {
|
|
733
|
+
const owner = getMountOwner(middleware);
|
|
734
|
+
if (!owner) {
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
const probe = gate ? gate.probe() : Promise.resolve(null);
|
|
738
|
+
try {
|
|
739
|
+
const routes = routesFromExpress({ _router: owner }, { mounts });
|
|
740
|
+
const docs = await (0, import_node2.loadDocsFromFs)(cfg.docs, cwd);
|
|
741
|
+
logSurface(routes, docs.length);
|
|
742
|
+
} catch (err) {
|
|
743
|
+
console.error(
|
|
744
|
+
(0, import_core6.formatFailed)(
|
|
745
|
+
LOG_PREFIX,
|
|
746
|
+
errorMessage(err),
|
|
747
|
+
`your app keeps serving its own routes; ${MCP_PATH} will retry on its first request and answer 503 if it fails again`
|
|
748
|
+
),
|
|
749
|
+
err
|
|
750
|
+
);
|
|
751
|
+
}
|
|
752
|
+
const problem = await probe;
|
|
753
|
+
if (problem && cfg.oauth) {
|
|
754
|
+
console.warn(
|
|
755
|
+
(0, import_core6.formatWarning)(
|
|
756
|
+
LOG_PREFIX,
|
|
757
|
+
`can't verify access tokens from oauth.issuer ${cfg.oauth.issuer}: ${problem}`,
|
|
758
|
+
"make oauth.issuer match the tokens' `iss` exactly (trailing slash included) and check it's reachable from this server; until then MCP clients get 503"
|
|
759
|
+
)
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
return middleware;
|
|
356
764
|
}
|
|
357
|
-
|
|
765
|
+
function errorMessage(err) {
|
|
766
|
+
return err instanceof Error ? err.message : String(err);
|
|
767
|
+
}
|
|
768
|
+
function headersOf(req) {
|
|
769
|
+
const headers = new Headers();
|
|
770
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
771
|
+
if (typeof value === "string") headers.set(name, value);
|
|
772
|
+
else if (Array.isArray(value)) headers.set(name, value.join(", "));
|
|
773
|
+
}
|
|
774
|
+
return headers;
|
|
775
|
+
}
|
|
776
|
+
async function initialize(app, cfg, cwd, mounts, extras, onReady) {
|
|
358
777
|
let surface = null;
|
|
359
778
|
let kbPromise = null;
|
|
360
779
|
const getSurface = async () => {
|
|
361
|
-
surface ??= await buildExpressSurface(app, cfg, cwd, { mounts });
|
|
780
|
+
surface ??= await buildExpressSurface(app, cfg, cwd, { mounts, ...extras });
|
|
362
781
|
return surface;
|
|
363
782
|
};
|
|
364
783
|
const getKb = async () => {
|
|
365
784
|
const s = await getSurface();
|
|
366
|
-
kbPromise ??= (0, import_node2.loadOrBuildKb)(s.docs, (0,
|
|
785
|
+
kbPromise ??= (0, import_node2.loadOrBuildKb)(s.docs, (0, import_node_path3.join)(cwd, ".exposify"));
|
|
367
786
|
return kbPromise;
|
|
368
787
|
};
|
|
369
|
-
await getSurface();
|
|
788
|
+
const built = await getSurface();
|
|
370
789
|
await getKb();
|
|
790
|
+
try {
|
|
791
|
+
onReady(built);
|
|
792
|
+
} catch {
|
|
793
|
+
}
|
|
371
794
|
return (0, import_node2.createNodeMcpHandler)({ getSurface, getKb, cfg });
|
|
372
795
|
}
|
|
373
796
|
// Annotate the CommonJS export names for ESM import in node:
|
|
374
797
|
0 && (module.exports = {
|
|
798
|
+
AIREADY_OPTIONS_TAG,
|
|
375
799
|
aiReady,
|
|
800
|
+
completeMcpLogin,
|
|
801
|
+
createHostedAuthResolver,
|
|
376
802
|
describe,
|
|
377
803
|
exposed,
|
|
378
804
|
hidden,
|
|
805
|
+
hostedBroker,
|
|
379
806
|
routesFromExpress
|
|
380
807
|
});
|