@questpie/mcp 3.16.0 → 3.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2394 @@
1
+ import { createAdapterContext, createContextFactory, defaultOperationScope, evaluateRouteAccess, executeJsonRoute, extractAppServices, introspectCollection, introspectGlobal, introspectRoutes, isJsonRoute, umbrellaScopeFor } from "questpie";
2
+ import { randomUUID } from "node:crypto";
3
+ import { types } from "node:util";
4
+ import { snapshotBoundedBrokerValue } from "questpie/executor";
5
+ import { z } from "zod";
6
+ import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import { ListToolsRequestSchema, ToolSchema } from "@modelcontextprotocol/sdk/types.js";
8
+
9
+ //#region src/server/mcp-tool.ts
10
+ var McpToolBuilder = class {
11
+ constructor(name, config) {
12
+ this.name = name;
13
+ this.config = config;
14
+ }
15
+ handler(handler) {
16
+ return Object.freeze({
17
+ __brand: "mcpTool",
18
+ name: this.name,
19
+ config: this.config,
20
+ handler
21
+ });
22
+ }
23
+ };
24
+ /**
25
+ * Define a custom MCP tool, contributed by a module the same way collections and
26
+ * routes are (via `ModuleDefinition.mcpTools`). `mcpTool(name, config).handler(fn)`
27
+ * returns a frozen, branded {@link McpToolDefinition}; the CRUD/route tools the
28
+ * framework auto-generates are the same shape.
29
+ *
30
+ * `config` mirrors the MCP tool contract: `inputSchema`/`outputSchema` (Zod),
31
+ * `title`/`description`/`annotations` for discovery, plus the two QUESTPIE
32
+ * authorization hooks that compose exactly like the built-in tools:
33
+ * - `access` — required explicit opt-in and an {@link McpAccessRule} run against the caller's transport /
34
+ * accessMode / session (and, for an OAuth caller, its scopes). Same RBAC-style
35
+ * gate the CRUD tools use.
36
+ * - `scopes` — the OAuth scopes an `oauth` caller must hold ({@link McpToolConfig.scopes}).
37
+ * Custom tools have no default scope mapping (unlike CRUD, there is no
38
+ * resource/operation to derive one from), so it is required; use `false` for
39
+ * an explicit no-OAuth-scope policy. The shipped scope gate ({@link scopeGateAllows}) enforces this at both
40
+ * `tools/list` (hidden) and `tools/call` (denied); `system`/`user` callers
41
+ * carry no scopes and skip it, so the effective bound is always `scopes ∩ RBAC`.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * export const ping = mcpTool("ops.ping", {
46
+ * access: ({ session }) => !!session,
47
+ * description: "Health probe",
48
+ * inputSchema: z.object({}),
49
+ * scopes: "routes:ops/ping:invoke", // only OAuth callers holding this scope
50
+ * }).handler(async () => ({ content: [{ type: "text", text: "pong" }] }));
51
+ * ```
52
+ */
53
+ function mcpTool(name, config) {
54
+ return new McpToolBuilder(name, config);
55
+ }
56
+ function isMcpTool(value) {
57
+ return !!value && typeof value === "object" && value["__brand"] === "mcpTool" && typeof value.handler === "function";
58
+ }
59
+
60
+ //#endregion
61
+ //#region src/server/principal.ts
62
+ const hasOwnData = (descriptors, key) => {
63
+ const descriptor = descriptors[key];
64
+ return !!descriptor && "value" in descriptor;
65
+ };
66
+ const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
67
+ const nonEmptyString = (value) => typeof value === "string" && value.length > 0;
68
+ const isString = (value) => typeof value === "string";
69
+ function hasOwnString(value, key) {
70
+ try {
71
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
72
+ return !!descriptor && "value" in descriptor && nonEmptyString(descriptor.value);
73
+ } catch {
74
+ return false;
75
+ }
76
+ }
77
+ /**
78
+ * Validate the complete MCP-facing principal discriminant without invoking
79
+ * getters or reading inherited properties.
80
+ */
81
+ function validateMcpPrincipal(value) {
82
+ if (!isObject(value)) return null;
83
+ try {
84
+ const descriptors = Object.getOwnPropertyDescriptors(value);
85
+ if (!hasOwnData(descriptors, "kind")) return null;
86
+ if (descriptors.kind.value === "system") return Object.keys(descriptors).length === 1 ? { kind: "system" } : null;
87
+ if (descriptors.kind.value === "user") {
88
+ if (Object.keys(descriptors).some((key) => ![
89
+ "kind",
90
+ "user",
91
+ "session"
92
+ ].includes(key)) || !hasOwnData(descriptors, "user") || !isObject(descriptors.user.value) || !hasOwnString(descriptors.user.value, "id") || !hasOwnData(descriptors, "session") || !isObject(descriptors.session.value) || !hasOwnString(descriptors.session.value, "id")) return null;
93
+ return { kind: "user" };
94
+ }
95
+ if (descriptors.kind.value === "oauth") {
96
+ if (Object.keys(descriptors).some((key) => ![
97
+ "kind",
98
+ "user",
99
+ "clientId",
100
+ "scopes",
101
+ "tokenId"
102
+ ].includes(key)) || !hasOwnData(descriptors, "user") || !isObject(descriptors.user.value) || !hasOwnString(descriptors.user.value, "id") || !hasOwnData(descriptors, "clientId") || !nonEmptyString(descriptors.clientId.value) || !hasOwnData(descriptors, "tokenId") || !isString(descriptors.tokenId.value) || !hasOwnData(descriptors, "scopes") || !Array.isArray(descriptors.scopes.value) || !descriptors.scopes.value.every(nonEmptyString)) return null;
103
+ return {
104
+ kind: "oauth",
105
+ scopes: [...descriptors.scopes.value]
106
+ };
107
+ }
108
+ return null;
109
+ } catch {
110
+ return null;
111
+ }
112
+ }
113
+
114
+ //#endregion
115
+ //#region src/server/policy.ts
116
+ const DEFAULT_MCP_CONFIG = {
117
+ crud: {
118
+ maxLimit: 100,
119
+ collections: {},
120
+ globals: {}
121
+ },
122
+ routes: { routes: {} },
123
+ resources: {},
124
+ http: {
125
+ accessMode: "user",
126
+ enableJsonResponse: true
127
+ },
128
+ stdio: { trustedMaintenance: false },
129
+ execution: {}
130
+ };
131
+ function isPlainObject(value) {
132
+ return !!value && typeof value === "object" && !Array.isArray(value);
133
+ }
134
+ const UNSAFE_CONFIG_KEYS = new Set([
135
+ "__proto__",
136
+ "prototype",
137
+ "constructor"
138
+ ]);
139
+ function ownDataEntries(value) {
140
+ if (!isPlainObject(value)) return [];
141
+ try {
142
+ return Object.entries(Object.getOwnPropertyDescriptors(value)).filter(([key, descriptor]) => !UNSAFE_CONFIG_KEYS.has(key) && "value" in descriptor).map(([key, descriptor]) => [key, descriptor.value]);
143
+ } catch {
144
+ return [];
145
+ }
146
+ }
147
+ function safeCloneConfigValue(value) {
148
+ if (Array.isArray(value)) return value.map((entry) => safeCloneConfigValue(entry));
149
+ if (!isPlainObject(value)) return value;
150
+ const clone = Object.create(null);
151
+ for (const [key, entry] of ownDataEntries(value)) clone[key] = safeCloneConfigValue(entry);
152
+ return clone;
153
+ }
154
+ function mergeDeep(base, override) {
155
+ const out = safeCloneConfigValue(base);
156
+ for (const [key, value] of ownDataEntries(override)) if (isPlainObject(value) && isPlainObject(out[key])) out[key] = mergeDeep(out[key], value);
157
+ else if (value !== void 0) out[key] = safeCloneConfigValue(value);
158
+ return out;
159
+ }
160
+ function ownValue(record, key) {
161
+ if (!record || !Object.prototype.hasOwnProperty.call(record, key)) return;
162
+ return record[key];
163
+ }
164
+ function replaceOwnEntries(base, override) {
165
+ const next = safeCloneConfigValue(base ?? {});
166
+ for (const [key, value] of ownDataEntries(override)) next[key] = safeCloneConfigValue(value);
167
+ return next;
168
+ }
169
+ function resolveMcpConfig(app, override) {
170
+ const appConfig = app.state?.config?.mcp ?? {};
171
+ const safeOverride = safeCloneConfigValue(override);
172
+ const resolved = mergeDeep(mergeDeep(DEFAULT_MCP_CONFIG, appConfig), safeOverride);
173
+ if (safeOverride?.crud?.collections) resolved.crud = {
174
+ ...resolved.crud,
175
+ collections: replaceOwnEntries(resolved.crud?.collections, safeOverride.crud.collections)
176
+ };
177
+ if (safeOverride?.crud?.globals) resolved.crud = {
178
+ ...resolved.crud,
179
+ globals: replaceOwnEntries(resolved.crud?.globals, safeOverride.crud.globals)
180
+ };
181
+ if (safeOverride?.routes?.routes) resolved.routes = {
182
+ ...resolved.routes,
183
+ routes: replaceOwnEntries(resolved.routes?.routes, safeOverride.routes.routes)
184
+ };
185
+ return resolved;
186
+ }
187
+ /**
188
+ * Resolve workload authority maps atomically. A caller-provided map replaces
189
+ * the corresponding app map instead of inheriting entries or nested authority.
190
+ */
191
+ function resolveWorkloadMcpConfig(app, override) {
192
+ const resolved = resolveMcpConfig(app, override);
193
+ if (!override) return resolved;
194
+ const safeOverride = safeCloneConfigValue(override);
195
+ if (Object.prototype.hasOwnProperty.call(safeOverride.crud ?? {}, "collections")) resolved.crud = {
196
+ ...resolved.crud,
197
+ collections: safeCloneConfigValue(safeOverride.crud?.collections ?? {})
198
+ };
199
+ if (Object.prototype.hasOwnProperty.call(safeOverride.crud ?? {}, "globals")) resolved.crud = {
200
+ ...resolved.crud,
201
+ globals: safeCloneConfigValue(safeOverride.crud?.globals ?? {})
202
+ };
203
+ if (Object.prototype.hasOwnProperty.call(safeOverride.routes ?? {}, "routes")) resolved.routes = {
204
+ ...resolved.routes,
205
+ routes: safeCloneConfigValue(safeOverride.routes?.routes ?? {})
206
+ };
207
+ for (const kind of [
208
+ "collections",
209
+ "globals",
210
+ "routes"
211
+ ]) if (Object.prototype.hasOwnProperty.call(safeOverride.resources ?? {}, kind)) resolved.resources = {
212
+ ...resolved.resources,
213
+ [kind]: safeCloneConfigValue(safeOverride.resources?.[kind] ?? {})
214
+ };
215
+ return resolved;
216
+ }
217
+ function resolveEntityPolicy(config, kind, name, _transport = "http") {
218
+ const crud = ownValue(config, "crud");
219
+ const routes = ownValue(config, "routes");
220
+ if (kind === "route") return normalizePolicy({ expose: false }, ownValue(ownValue(routes, "routes"), name));
221
+ if (kind === "global") return normalizePolicy({ expose: false }, ownValue(ownValue(crud, "globals"), name));
222
+ return normalizePolicy({ expose: false }, ownValue(ownValue(crud, "collections"), name));
223
+ }
224
+ function normalizePolicy(defaults, override) {
225
+ override = safeCloneConfigValue(override);
226
+ if (override === false) return {
227
+ expose: false,
228
+ operations: {},
229
+ operationScopes: {},
230
+ operationWorkloads: {}
231
+ };
232
+ const base = {
233
+ expose: defaults.expose ?? true,
234
+ operations: { ...defaults.operations ?? {} },
235
+ requiredScopes: defaults.requiredScopes,
236
+ operationScopes: { ...defaults.operationScopes ?? {} },
237
+ workload: defaults.workload,
238
+ operationWorkloads: { ...defaults.operationWorkloads ?? {} },
239
+ fields: defaults.fields,
240
+ description: defaults.description
241
+ };
242
+ if (override === void 0) return base;
243
+ if (override === true) return {
244
+ ...base,
245
+ expose: true
246
+ };
247
+ return {
248
+ ...base,
249
+ ...override,
250
+ expose: override.expose ?? true,
251
+ operations: {
252
+ ...base.operations,
253
+ ...override.operations ?? {}
254
+ },
255
+ requiredScopes: override.requiredScopes ?? base.requiredScopes,
256
+ operationScopes: {
257
+ ...base.operationScopes,
258
+ ...override.operationScopes ?? {}
259
+ },
260
+ workload: override.workload ?? base.workload,
261
+ operationWorkloads: {
262
+ ...base.operationWorkloads,
263
+ ...override.operationWorkloads ?? {}
264
+ }
265
+ };
266
+ }
267
+ function workloadRequirementForOperation(policy, operation) {
268
+ return policy.operationWorkloads[operation] ?? policy.workload;
269
+ }
270
+ function operationRule(policy, operation) {
271
+ return policy.operations[operation];
272
+ }
273
+ /** Normalize an {@link McpRequiredScopes} declaration to a scope list. */
274
+ function normalizeRequiredScopes(required) {
275
+ if (required === void 0 || required === false) return [];
276
+ return typeof required === "string" ? [required] : [...required];
277
+ }
278
+ /**
279
+ * Resolve the scopes an OAuth caller must hold for a given entity operation.
280
+ *
281
+ * Precedence (most specific wins), each level is declarative data:
282
+ * 1. `policy.operationScopes[operationName]` — per-operation override.
283
+ * 2. `policy.requiredScopes` — entity-level requirement (all operations).
284
+ * 3. the default `<resource>:<name>:<verb>` mapping derived from the kind.
285
+ *
286
+ * A level set to `false` explicitly requires no scope (stops the fallback).
287
+ * Returns a (possibly empty) scope list; empty means "no scope required". This
288
+ * resolves the requirement; the deny/register decision that consumes it is
289
+ * {@link scopeGateAllows}.
290
+ */
291
+ function requiredScopesForOperation(policy, kind, name, operationName, operationKind) {
292
+ const perOperation = policy.operationScopes[operationName];
293
+ if (perOperation !== void 0) return normalizeRequiredScopes(perOperation);
294
+ if (policy.requiredScopes !== void 0) return normalizeRequiredScopes(policy.requiredScopes);
295
+ return [defaultOperationScope(kind, name, operationKind)];
296
+ }
297
+ /**
298
+ * The consented scopes carried by the request, or `undefined` when the caller
299
+ * is not OAuth-authenticated. Reads `ctx.principal` (populated by MO6): only the
300
+ * `oauth` principal carries `scopes`; `user` and `system` do not.
301
+ */
302
+ function scopesFromContext(ctx) {
303
+ const principal = ctx.principal;
304
+ if (principal === void 0 || principal === null) return void 0;
305
+ const validated = validateMcpPrincipal(principal);
306
+ if (!validated) return null;
307
+ return validated.kind === "oauth" ? validated.scopes : void 0;
308
+ }
309
+ /**
310
+ * The OAuth scope gate (MO8, extended by the umbrella model — LOCKED #2).
311
+ *
312
+ * Given the scopes the caller holds (from {@link scopesFromContext}) and the
313
+ * scopes an operation requires (from {@link requiredScopesForOperation} or a
314
+ * custom tool's `config.scopes`), decide whether the scope gate passes.
315
+ *
316
+ * Semantics:
317
+ * - `heldScopes === undefined` → the caller is not OAuth-authenticated (`user`,
318
+ * `system`, or unauthenticated). The scope gate does not apply and PASSES;
319
+ * authorization is left entirely to RBAC / MCP rules. This is what keeps the
320
+ * first-party admin (`user`) and stdio (`system`) paths unchanged.
321
+ * - Otherwise the caller is an `oauth` principal: the gate PASSES iff EVERY
322
+ * required scope is satisfied — a required scope is satisfied when the caller
323
+ * holds it directly OR holds its applicable coarse umbrella
324
+ * ({@link umbrellaScopeFor}). An empty `required` needs nothing, so it passes
325
+ * for everyone.
326
+ *
327
+ * The umbrella widening is deliberately narrow — it can only satisfy a
328
+ * granular `<resource>:<name>:read|write` with the matching `<resource>:read`
329
+ * or `<resource>:write` umbrella. `read` and `write` are independent (holding
330
+ * `collections:write` never satisfies a `<name>:read` requirement, and vice
331
+ * versa), and there is NO umbrella for `:delete` or `routes:…:invoke`. So the
332
+ * umbrella grants convenience without ever over-granting across verb, resource
333
+ * kind, or into delete/invoke.
334
+ *
335
+ * This gate is **additive** — it can only ever REMOVE access from an `oauth`
336
+ * caller. It never grants access on its own: callers still run the MCP rule and
337
+ * the collection/global `.access()` RBAC independently, and all must pass. The
338
+ * effective `oauth` permission is therefore `scopes ∩ RBAC`.
339
+ */
340
+ function scopeGateAllows(heldScopes, required) {
341
+ if (heldScopes === null) return false;
342
+ if (heldScopes === void 0) return true;
343
+ if (required.length === 0) return true;
344
+ const held = new Set(heldScopes);
345
+ return required.every((scope) => {
346
+ if (held.has(scope)) return true;
347
+ const umbrella = umbrellaScopeFor(scope);
348
+ return umbrella !== void 0 && held.has(umbrella);
349
+ });
350
+ }
351
+ async function evaluateMcpRule(rule, options) {
352
+ const scopes = scopesFromContext(options.ctx);
353
+ if (scopes === null) return false;
354
+ if (rule === void 0) return true;
355
+ if (typeof rule === "boolean") return rule;
356
+ return rule({
357
+ transport: options.transport,
358
+ accessMode: options.accessMode,
359
+ session: options.ctx.session,
360
+ scopes,
361
+ ctx: options.ctx
362
+ });
363
+ }
364
+
365
+ //#endregion
366
+ //#region src/server/catalog.ts
367
+ const COLLECTION_OPERATION_KINDS = {
368
+ list: "read",
369
+ count: "read",
370
+ get: "read",
371
+ create: "write",
372
+ update: "write",
373
+ delete: "delete"
374
+ };
375
+ const GLOBAL_OPERATION_KINDS = {
376
+ get: "read",
377
+ update: "write"
378
+ };
379
+ const sanitizeRouteKey = (key) => key.replace(/[:/[\].]+/g, ".").replace(/\.+/g, ".").replace(/^\./, "").replace(/\.$/, "");
380
+ const operationIsExplicitlyEnabled = (policy, operation) => {
381
+ const rule = operationRule(policy, operation);
382
+ return rule !== void 0 && rule !== false;
383
+ };
384
+ const addRequiredScopes = (target, required) => {
385
+ if (required === void 0 || required === false) return;
386
+ const scopes = typeof required === "string" ? [required] : required;
387
+ for (const scope of scopes) if (!target.includes(scope)) target.push(scope);
388
+ };
389
+ const hasOwn$1 = (record, key) => !!record && Object.prototype.hasOwnProperty.call(record, key);
390
+ function ownSection(record, key) {
391
+ return hasOwn$1(record, key) ? record[key] : void 0;
392
+ }
393
+ function snapshotWorkload(requirement) {
394
+ if (!requirement) return void 0;
395
+ return Object.freeze({
396
+ capabilities: Object.freeze([...requirement.capabilities]),
397
+ ...requirement.handoff ? { handoff: requirement.handoff } : {}
398
+ });
399
+ }
400
+ function snapshotPolicy(policy) {
401
+ const operations = Object.create(null);
402
+ for (const [name, rule] of Object.entries(policy.operations)) operations[name] = rule;
403
+ const operationScopes = Object.create(null);
404
+ for (const [name, scopes] of Object.entries(policy.operationScopes)) operationScopes[name] = Array.isArray(scopes) ? Object.freeze([...scopes]) : scopes;
405
+ const operationWorkloads = Object.create(null);
406
+ for (const [name, workload] of Object.entries(policy.operationWorkloads)) {
407
+ const snapshot = snapshotWorkload(workload);
408
+ if (snapshot) operationWorkloads[name] = snapshot;
409
+ }
410
+ return Object.freeze({
411
+ ...policy,
412
+ operations: Object.freeze(operations),
413
+ requiredScopes: Array.isArray(policy.requiredScopes) ? Object.freeze([...policy.requiredScopes]) : policy.requiredScopes,
414
+ operationScopes: Object.freeze(operationScopes),
415
+ workload: snapshotWorkload(policy.workload),
416
+ operationWorkloads: Object.freeze(operationWorkloads),
417
+ fields: policy.fields ? Object.freeze({
418
+ include: policy.fields.include ? Object.freeze([...policy.fields.include]) : void 0,
419
+ exclude: policy.fields.exclude ? Object.freeze([...policy.fields.exclude]) : void 0
420
+ }) : void 0
421
+ });
422
+ }
423
+ function snapshotTool(candidate) {
424
+ const config = Object.freeze({
425
+ ...candidate.config,
426
+ scopes: Array.isArray(candidate.config.scopes) ? Object.freeze([...candidate.config.scopes]) : candidate.config.scopes,
427
+ workload: snapshotWorkload(candidate.config.workload),
428
+ annotations: candidate.config.annotations ? Object.freeze({ ...candidate.config.annotations }) : void 0,
429
+ _meta: candidate.config["_meta"] ? Object.freeze({ ...candidate.config["_meta"] }) : void 0
430
+ });
431
+ return Object.freeze({
432
+ __brand: "mcpTool",
433
+ name: candidate.name,
434
+ config,
435
+ handler: candidate.handler
436
+ });
437
+ }
438
+ function readonlyMap(source) {
439
+ return Object.freeze({
440
+ get size() {
441
+ return source.size;
442
+ },
443
+ get: (key) => source.get(key),
444
+ has: (key) => source.has(key),
445
+ entries: () => source.entries(),
446
+ keys: () => source.keys(),
447
+ values: () => source.values(),
448
+ forEach: (callback, thisArg) => {
449
+ const view = readonlyMap(source);
450
+ source.forEach((value, key) => callback.call(thisArg, value, key, view));
451
+ },
452
+ [Symbol.iterator]: () => source[Symbol.iterator](),
453
+ [Symbol.toStringTag]: "Map"
454
+ });
455
+ }
456
+ function readonlySet(source) {
457
+ return Object.freeze({
458
+ get size() {
459
+ return source.size;
460
+ },
461
+ has: (value) => source.has(value),
462
+ entries: () => source.entries(),
463
+ keys: () => source.keys(),
464
+ values: () => source.values(),
465
+ forEach: (callback, thisArg) => {
466
+ const view = readonlySet(source);
467
+ source.forEach((value) => callback.call(thisArg, value, value, view));
468
+ },
469
+ [Symbol.iterator]: () => source[Symbol.iterator](),
470
+ [Symbol.toStringTag]: "Set"
471
+ });
472
+ }
473
+ function resolveMcpCatalog(app, config) {
474
+ const collections = /* @__PURE__ */ new Map();
475
+ const globals = /* @__PURE__ */ new Map();
476
+ const routes = /* @__PURE__ */ new Map();
477
+ const customTools = /* @__PURE__ */ new Map();
478
+ const oauthScopes = [];
479
+ const umbrellaScopes = [];
480
+ const releasedToolNames = /* @__PURE__ */ new Set();
481
+ const claimToolName = (name) => {
482
+ if (releasedToolNames.has(name)) throw new Error(`Duplicate MCP tool name: ${name}`);
483
+ releasedToolNames.add(name);
484
+ };
485
+ for (const name of Object.keys(app.getCollections())) {
486
+ const policy = snapshotPolicy(resolveEntityPolicy(config, "collection", name));
487
+ if (!policy.expose) continue;
488
+ const operations = Object.entries(COLLECTION_OPERATION_KINDS).filter(([operation]) => operationIsExplicitlyEnabled(policy, operation)).map(([operation, operationKind]) => {
489
+ const required = requiredScopesForOperation(policy, "collection", name, operation, operationKind);
490
+ addRequiredScopes(oauthScopes, required);
491
+ for (const scope of required) {
492
+ const umbrella = umbrellaScopeFor(scope);
493
+ if (umbrella && !umbrellaScopes.includes(umbrella)) umbrellaScopes.push(umbrella);
494
+ }
495
+ return operation;
496
+ });
497
+ if (operations.length > 0) {
498
+ for (const operation of operations) claimToolName(`collections.${name}.${operation}`);
499
+ collections.set(name, Object.freeze({
500
+ policy,
501
+ operations: Object.freeze([...operations])
502
+ }));
503
+ }
504
+ }
505
+ for (const name of Object.keys(app.getGlobals())) {
506
+ const policy = snapshotPolicy(resolveEntityPolicy(config, "global", name));
507
+ if (!policy.expose) continue;
508
+ const operations = Object.entries(GLOBAL_OPERATION_KINDS).filter(([operation]) => operationIsExplicitlyEnabled(policy, operation)).map(([operation, operationKind]) => {
509
+ const required = requiredScopesForOperation(policy, "global", name, operation, operationKind);
510
+ addRequiredScopes(oauthScopes, required);
511
+ for (const scope of required) {
512
+ const umbrella = umbrellaScopeFor(scope);
513
+ if (umbrella && !umbrellaScopes.includes(umbrella)) umbrellaScopes.push(umbrella);
514
+ }
515
+ return operation;
516
+ });
517
+ if (operations.length > 0) {
518
+ for (const operation of operations) claimToolName(`globals.${name}.${operation}`);
519
+ globals.set(name, Object.freeze({
520
+ policy,
521
+ operations: Object.freeze([...operations])
522
+ }));
523
+ }
524
+ }
525
+ for (const route$1 of introspectRoutes(app)) {
526
+ if (!isJsonRoute(route$1.definition) || route$1.meta?.mcp?.expose !== true) continue;
527
+ const policy = snapshotPolicy(resolveEntityPolicy(config, "route", route$1.key));
528
+ if (!policy.expose || !operationIsExplicitlyEnabled(policy, "execute")) continue;
529
+ addRequiredScopes(oauthScopes, requiredScopesForOperation(policy, "route", route$1.key, "execute", "invoke"));
530
+ const toolName = route$1.meta.mcp.name ?? `routes.${sanitizeRouteKey(route$1.key)}`;
531
+ claimToolName(toolName);
532
+ routes.set(route$1.key, Object.freeze({
533
+ policy,
534
+ operations: Object.freeze(["execute"]),
535
+ route: route$1,
536
+ toolName
537
+ }));
538
+ }
539
+ const configuredTools = app.state?.mcpTools ?? {};
540
+ for (const [key, candidate] of Object.entries(configuredTools)) {
541
+ if (!isMcpTool(candidate) || candidate.config.access === void 0 || candidate.config.scopes === void 0) continue;
542
+ if (candidate.config.access === false) continue;
543
+ const tool = snapshotTool(candidate);
544
+ const name = tool.name || key;
545
+ claimToolName(name);
546
+ customTools.set(name, Object.freeze({ tool }));
547
+ addRequiredScopes(oauthScopes, tool.config.scopes);
548
+ }
549
+ const resourceConfig = ownSection(config, "resources");
550
+ const configuredResourceCollections = ownSection(resourceConfig, "collections");
551
+ const configuredResourceGlobals = ownSection(resourceConfig, "globals");
552
+ const configuredResourceRoutes = ownSection(resourceConfig, "routes");
553
+ const resourceCollections = new Set([...collections.entries()].filter(([name, entry]) => hasOwn$1(configuredResourceCollections, name) && configuredResourceCollections?.[name] === true && entry.operations.some((operation) => [
554
+ "list",
555
+ "count",
556
+ "get"
557
+ ].includes(operation))).map(([name]) => name));
558
+ const resourceGlobals = new Set([...globals.entries()].filter(([name, entry]) => hasOwn$1(configuredResourceGlobals, name) && configuredResourceGlobals?.[name] === true && entry.operations.includes("get")).map(([name]) => name));
559
+ const resourceRoutes = new Set([...routes.keys()].filter((name) => hasOwn$1(configuredResourceRoutes, name) && configuredResourceRoutes?.[name] === true));
560
+ return Object.freeze({
561
+ collections: readonlyMap(collections),
562
+ globals: readonlyMap(globals),
563
+ routes: readonlyMap(routes),
564
+ customTools: readonlyMap(customTools),
565
+ resources: Object.freeze({
566
+ collections: readonlySet(resourceCollections),
567
+ globals: readonlySet(resourceGlobals),
568
+ routes: readonlySet(resourceRoutes)
569
+ }),
570
+ oauth: Object.freeze({
571
+ scopes: Object.freeze([...oauthScopes, ...umbrellaScopes]),
572
+ scopesSupported: Object.freeze([...umbrellaScopes])
573
+ })
574
+ });
575
+ }
576
+
577
+ //#endregion
578
+ //#region src/server/execution-boundary.ts
579
+ const DEFAULT_MCP_EXECUTION_LIMITS = Object.freeze({
580
+ maxInputBytes: 64 * 1024,
581
+ maxInputDepth: 16,
582
+ maxOutputDepth: 64,
583
+ maxValueNodes: 1e4,
584
+ maxOutputBytes: 1024 * 1024,
585
+ timeoutMs: 3e4,
586
+ maxConcurrency: 64,
587
+ maxConcurrencyPerPrincipal: 8,
588
+ maxTools: 512,
589
+ maxResources: 512
590
+ });
591
+ const ABSOLUTE_MCP_EXECUTION_LIMITS = Object.freeze({
592
+ maxInputBytes: 1024 * 1024,
593
+ maxInputDepth: 64,
594
+ maxOutputDepth: 64,
595
+ maxValueNodes: 1e5,
596
+ maxOutputBytes: 4 * 1024 * 1024,
597
+ timeoutMs: 5 * 6e4,
598
+ maxConcurrency: 1024,
599
+ maxConcurrencyPerPrincipal: 1024,
600
+ maxTools: 2048,
601
+ maxResources: 2048
602
+ });
603
+ const PUBLIC_ERROR_META_KEY = "questpie/error";
604
+ const PUBLIC_ERROR_CODES = new Set([
605
+ "access_denied",
606
+ "invalid_input",
607
+ "input_too_large",
608
+ "output_too_large",
609
+ "timeout",
610
+ "cancelled",
611
+ "busy",
612
+ "internal"
613
+ ]);
614
+ var McpPublicError = class extends Error {
615
+ code;
616
+ correlationId;
617
+ constructor(code, correlationId = randomUUID()) {
618
+ super(`${publicMessage(code)} (${code}; correlationId=${correlationId})`);
619
+ this.name = "McpPublicError";
620
+ this.code = code;
621
+ this.correlationId = correlationId;
622
+ }
623
+ };
624
+ var McpAccessDeniedError = class extends Error {
625
+ constructor() {
626
+ super("MCP access denied");
627
+ this.name = "McpAccessDeniedError";
628
+ }
629
+ };
630
+ /** Read a public boundary code without touching an error message or accessor. */
631
+ function mcpPublicErrorCode(error) {
632
+ if (!error || typeof error !== "object" || types.isProxy(error)) return;
633
+ try {
634
+ const descriptor = Object.getOwnPropertyDescriptor(error, "code");
635
+ return descriptor && "value" in descriptor && typeof descriptor.value === "string" && PUBLIC_ERROR_CODES.has(descriptor.value) ? descriptor.value : void 0;
636
+ } catch {
637
+ return;
638
+ }
639
+ }
640
+ function positiveInteger(value, fallback, maximum, name) {
641
+ if (value === void 0) return fallback;
642
+ if (!Number.isSafeInteger(value) || value <= 0 || value > maximum) throw new Error(`Invalid MCP execution limit: ${name}`);
643
+ return value;
644
+ }
645
+ function resolveMcpExecutionLimits(config) {
646
+ const limits = {
647
+ maxInputBytes: positiveInteger(config?.maxInputBytes, DEFAULT_MCP_EXECUTION_LIMITS.maxInputBytes, ABSOLUTE_MCP_EXECUTION_LIMITS.maxInputBytes, "maxInputBytes"),
648
+ maxInputDepth: positiveInteger(config?.maxInputDepth, DEFAULT_MCP_EXECUTION_LIMITS.maxInputDepth, ABSOLUTE_MCP_EXECUTION_LIMITS.maxInputDepth, "maxInputDepth"),
649
+ maxOutputDepth: positiveInteger(config?.maxOutputDepth, DEFAULT_MCP_EXECUTION_LIMITS.maxOutputDepth, ABSOLUTE_MCP_EXECUTION_LIMITS.maxOutputDepth, "maxOutputDepth"),
650
+ maxValueNodes: positiveInteger(config?.maxValueNodes, DEFAULT_MCP_EXECUTION_LIMITS.maxValueNodes, ABSOLUTE_MCP_EXECUTION_LIMITS.maxValueNodes, "maxValueNodes"),
651
+ maxOutputBytes: positiveInteger(config?.maxOutputBytes, DEFAULT_MCP_EXECUTION_LIMITS.maxOutputBytes, ABSOLUTE_MCP_EXECUTION_LIMITS.maxOutputBytes, "maxOutputBytes"),
652
+ timeoutMs: positiveInteger(config?.timeoutMs, DEFAULT_MCP_EXECUTION_LIMITS.timeoutMs, ABSOLUTE_MCP_EXECUTION_LIMITS.timeoutMs, "timeoutMs"),
653
+ maxConcurrency: positiveInteger(config?.maxConcurrency, DEFAULT_MCP_EXECUTION_LIMITS.maxConcurrency, ABSOLUTE_MCP_EXECUTION_LIMITS.maxConcurrency, "maxConcurrency"),
654
+ maxConcurrencyPerPrincipal: positiveInteger(config?.maxConcurrencyPerPrincipal, DEFAULT_MCP_EXECUTION_LIMITS.maxConcurrencyPerPrincipal, ABSOLUTE_MCP_EXECUTION_LIMITS.maxConcurrencyPerPrincipal, "maxConcurrencyPerPrincipal"),
655
+ maxTools: positiveInteger(config?.maxTools, DEFAULT_MCP_EXECUTION_LIMITS.maxTools, ABSOLUTE_MCP_EXECUTION_LIMITS.maxTools, "maxTools"),
656
+ maxResources: positiveInteger(config?.maxResources, DEFAULT_MCP_EXECUTION_LIMITS.maxResources, ABSOLUTE_MCP_EXECUTION_LIMITS.maxResources, "maxResources")
657
+ };
658
+ if (limits.maxConcurrencyPerPrincipal > limits.maxConcurrency) throw new Error("Invalid MCP execution limit: maxConcurrencyPerPrincipal");
659
+ return Object.freeze(limits);
660
+ }
661
+ function jsonStringBytes(value, maximum) {
662
+ if (maximum < 0) return 1;
663
+ if (maximum < 2 || value.length + 2 > maximum) return maximum + 1;
664
+ let bytes = 2;
665
+ for (let index = 0; index < value.length; index += 1) {
666
+ const code = value.charCodeAt(index);
667
+ if (code === 34 || code === 92) bytes += 2;
668
+ else if (code <= 31) bytes += code === 8 || code === 9 || code === 10 || code === 12 || code === 13 ? 2 : 6;
669
+ else if (code <= 127) bytes += 1;
670
+ else if (code <= 2047) bytes += 2;
671
+ else if (code >= 55296 && code <= 56319 && index + 1 < value.length && value.charCodeAt(index + 1) >= 56320 && value.charCodeAt(index + 1) <= 57343) {
672
+ bytes += 4;
673
+ index += 1;
674
+ } else if (code >= 55296 && code <= 57343) bytes += 6;
675
+ else bytes += 3;
676
+ if (bytes > maximum) return maximum + 1;
677
+ }
678
+ return bytes;
679
+ }
680
+ function isJsonContainer(value) {
681
+ if (!value || typeof value !== "object" || types.isProxy(value)) return false;
682
+ try {
683
+ const prototype = Object.getPrototypeOf(value);
684
+ return Array.isArray(value) ? prototype === Array.prototype : prototype === Object.prototype || prototype === null;
685
+ } catch {
686
+ return false;
687
+ }
688
+ }
689
+ function isJsonDate(value) {
690
+ if (!(value instanceof Date) || types.isProxy(value)) return false;
691
+ try {
692
+ return Object.getPrototypeOf(value) === Date.prototype && Number.isFinite(Date.prototype.getTime.call(value));
693
+ } catch {
694
+ return false;
695
+ }
696
+ }
697
+ function isArrayIndex(key, length) {
698
+ const index = Number(key);
699
+ return Number.isSafeInteger(index) && index >= 0 && index < length && String(index) === key;
700
+ }
701
+ function assertBoundedMcpValue(value, limits, code, correlationId = randomUUID()) {
702
+ const pending = [{
703
+ value,
704
+ depth: 0
705
+ }];
706
+ let nodes = 0;
707
+ let bytes = 0;
708
+ while (pending.length > 0) {
709
+ const next = pending.pop();
710
+ nodes += 1;
711
+ if (nodes > limits.maxValueNodes || next.depth > limits.maxValueDepth) throw new McpPublicError(code, correlationId);
712
+ const current = next.value;
713
+ if (current === null || typeof current === "boolean") bytes += 5;
714
+ else if (typeof current === "number") {
715
+ if (typeof current === "number" && !Number.isFinite(current)) throw new McpPublicError(code, correlationId);
716
+ bytes += 32;
717
+ } else if (typeof current === "string") bytes += jsonStringBytes(current, limits.maxBytes - bytes);
718
+ else if (typeof current === "bigint") bytes += jsonStringBytes(current.toString(), limits.maxBytes - bytes);
719
+ else if (isJsonDate(current)) bytes += 32;
720
+ else if (isJsonContainer(current)) {
721
+ let descriptors;
722
+ try {
723
+ descriptors = Object.getOwnPropertyDescriptors(current);
724
+ } catch {
725
+ throw new McpPublicError(code, correlationId);
726
+ }
727
+ const arrayLength = Array.isArray(current) ? current.length : void 0;
728
+ let arrayIndexes = 0;
729
+ bytes += 2;
730
+ for (const [key, descriptor] of Object.entries(descriptors)) {
731
+ if (key === "toJSON") throw new McpPublicError(code, correlationId);
732
+ if (arrayLength !== void 0 && isArrayIndex(key, arrayLength)) {
733
+ arrayIndexes += 1;
734
+ if (!("value" in descriptor)) throw new McpPublicError(code, correlationId);
735
+ pending.push({
736
+ value: descriptor.value,
737
+ depth: next.depth + 1
738
+ });
739
+ continue;
740
+ }
741
+ if (!descriptor.enumerable) continue;
742
+ if (!("value" in descriptor)) throw new McpPublicError(code, correlationId);
743
+ bytes += jsonStringBytes(key, limits.maxBytes - bytes) + 4;
744
+ pending.push({
745
+ value: descriptor.value,
746
+ depth: next.depth + 1
747
+ });
748
+ }
749
+ if (arrayLength !== void 0) {
750
+ if (arrayLength > limits.maxValueNodes) throw new McpPublicError(code, correlationId);
751
+ const holes = arrayLength - arrayIndexes;
752
+ nodes += holes;
753
+ bytes += Math.max(0, arrayLength - 1) + holes * 4;
754
+ }
755
+ } else if (current !== void 0) throw new McpPublicError(code, correlationId);
756
+ if (nodes > limits.maxValueNodes || bytes > limits.maxBytes) throw new McpPublicError(code, correlationId);
757
+ }
758
+ }
759
+ function snapshotBoundedMcpValue(value, limits, code, correlationId = randomUUID()) {
760
+ const snapshot = snapshotBoundedBrokerValue(value === void 0 ? null : value, limits.maxBytes, limits.maxValueDepth, limits.maxValueNodes);
761
+ if (!snapshot.ok) throw new McpPublicError(code, correlationId);
762
+ return snapshot.value;
763
+ }
764
+ function publicMessage(code) {
765
+ return code === "access_denied" ? "MCP access denied" : "MCP operation failed";
766
+ }
767
+ function classifyError(error, correlationId) {
768
+ if (!!error && (typeof error === "object" || typeof error === "function") && types.isProxy(error)) return new McpPublicError("internal", correlationId);
769
+ if (error instanceof McpPublicError) return error;
770
+ if (error instanceof McpAccessDeniedError || isLegacyAccessDenied(error)) return new McpPublicError("access_denied", correlationId);
771
+ if (error instanceof z.ZodError) return new McpPublicError("invalid_input", correlationId);
772
+ return new McpPublicError("internal", correlationId);
773
+ }
774
+ function isLegacyAccessDenied(error) {
775
+ if (!error || typeof error !== "object") return false;
776
+ try {
777
+ const descriptor = Object.getOwnPropertyDescriptor(error, "message");
778
+ return !!descriptor && "value" in descriptor && descriptor.value === "MCP access denied";
779
+ } catch {
780
+ return false;
781
+ }
782
+ }
783
+ function diagnosticError(error) {
784
+ try {
785
+ return Object.freeze({ kind: error instanceof Error ? "Error" : "Unknown" });
786
+ } catch {
787
+ return Object.freeze({ kind: "Unknown" });
788
+ }
789
+ }
790
+ function dataProperty(value, key) {
791
+ if (!value || typeof value !== "object" || types.isProxy(value)) return;
792
+ try {
793
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
794
+ return descriptor && "value" in descriptor ? descriptor.value : void 0;
795
+ } catch {
796
+ return;
797
+ }
798
+ }
799
+ function principalKey(ctx, transport, accessMode, workloadConcurrencyKey) {
800
+ if (accessMode === "system") return `${transport}:system`;
801
+ if (transport === "workload" && typeof workloadConcurrencyKey === "string" && workloadConcurrencyKey.length > 0) return `workload:${workloadConcurrencyKey.slice(0, 256)}`;
802
+ const principal = dataProperty(ctx, "principal");
803
+ const kind = dataProperty(principal, "kind");
804
+ if (kind === "oauth") {
805
+ const tokenId = dataProperty(principal, "tokenId");
806
+ if (typeof tokenId === "string" && tokenId.length > 0) return `oauth:${tokenId.slice(0, 256)}`;
807
+ const clientId = dataProperty(principal, "clientId");
808
+ const userId = dataProperty(dataProperty(principal, "user"), "id");
809
+ return `oauth:${String(clientId).slice(0, 128)}:${String(userId).slice(0, 128)}`;
810
+ }
811
+ if (kind === "user") {
812
+ const sessionId = dataProperty(dataProperty(principal, "session"), "id");
813
+ if (typeof sessionId === "string" && sessionId.length > 0) return `user:${sessionId.slice(0, 256)}`;
814
+ }
815
+ return `${transport}:anonymous`;
816
+ }
817
+ function safeRequestId(value) {
818
+ if (typeof value === "number") return Number.isSafeInteger(value) ? value : void 0;
819
+ return /^[\w.:-]{1,128}$/.test(value) ? value : void 0;
820
+ }
821
+ function toMcpToolError(error) {
822
+ const resolved = error instanceof McpPublicError ? error : classifyError(error, randomUUID());
823
+ return {
824
+ isError: true,
825
+ content: [{
826
+ type: "text",
827
+ text: publicMessage(resolved.code)
828
+ }],
829
+ _meta: { [PUBLIC_ERROR_META_KEY]: {
830
+ code: resolved.code,
831
+ correlationId: resolved.correlationId
832
+ } }
833
+ };
834
+ }
835
+ var McpExecutionGate = class {
836
+ active = 0;
837
+ activeByPrincipal = /* @__PURE__ */ new Map();
838
+ diagnosticActive = false;
839
+ };
840
+ var McpExecutionBoundary = class {
841
+ limits;
842
+ constructor(config = {}, gate = new McpExecutionGate()) {
843
+ this.config = config;
844
+ this.gate = gate;
845
+ this.limits = resolveMcpExecutionLimits(config);
846
+ }
847
+ emitDiagnostic(event) {
848
+ if (!this.config.onDiagnostic || this.gate.diagnosticActive) return;
849
+ this.gate.diagnosticActive = true;
850
+ Promise.resolve().then(() => this.config.onDiagnostic(Object.freeze(event))).catch(() => void 0).finally(() => {
851
+ this.gate.diagnosticActive = false;
852
+ });
853
+ }
854
+ acquireGlobal(correlationId) {
855
+ if (this.gate.active >= this.limits.maxConcurrency) throw new McpPublicError("busy", correlationId);
856
+ this.gate.active += 1;
857
+ return () => {
858
+ this.gate.active -= 1;
859
+ };
860
+ }
861
+ acquirePrincipal(key, correlationId) {
862
+ const active = this.gate.activeByPrincipal.get(key) ?? 0;
863
+ if (active >= this.limits.maxConcurrencyPerPrincipal) throw new McpPublicError("busy", correlationId);
864
+ this.gate.activeByPrincipal.set(key, active + 1);
865
+ return () => {
866
+ const next = (this.gate.activeByPrincipal.get(key) ?? 1) - 1;
867
+ if (next === 0) this.gate.activeByPrincipal.delete(key);
868
+ else this.gate.activeByPrincipal.set(key, next);
869
+ };
870
+ }
871
+ async execute(options) {
872
+ const startedAt = Date.now();
873
+ const correlationId = randomUUID();
874
+ const requestId = safeRequestId(options.extra.requestId);
875
+ let rawError;
876
+ try {
877
+ assertBoundedMcpValue(options.input, {
878
+ maxBytes: this.limits.maxInputBytes,
879
+ maxValueDepth: this.limits.maxInputDepth,
880
+ maxValueNodes: this.limits.maxValueNodes
881
+ }, "input_too_large", correlationId);
882
+ if (options.extra.signal.aborted) throw new McpPublicError("cancelled", correlationId);
883
+ const releaseGlobal = this.acquireGlobal(correlationId);
884
+ const controller = new AbortController();
885
+ let timedOut = false;
886
+ const cancel = () => controller.abort(options.extra.signal.reason);
887
+ options.extra.signal.addEventListener("abort", cancel, { once: true });
888
+ const timer = setTimeout(() => {
889
+ timedOut = true;
890
+ controller.abort(/* @__PURE__ */ new Error("MCP execution deadline exceeded"));
891
+ }, this.limits.timeoutMs);
892
+ let releasePrincipal;
893
+ const settled = Promise.resolve().then(async () => {
894
+ const ctx = await options.authorize({
895
+ signal: controller.signal,
896
+ requestId: options.extra.requestId,
897
+ correlationId
898
+ });
899
+ if (controller.signal.aborted) throw new McpPublicError(timedOut ? "timeout" : "cancelled", correlationId);
900
+ releasePrincipal = this.acquirePrincipal(principalKey(ctx, options.transport, options.accessMode, options.concurrencyKey?.()), correlationId);
901
+ return snapshotBoundedMcpValue(await options.invoke({
902
+ ctx,
903
+ signal: controller.signal,
904
+ requestId: options.extra.requestId,
905
+ correlationId
906
+ }), {
907
+ maxBytes: this.limits.maxOutputBytes,
908
+ maxValueDepth: this.limits.maxOutputDepth,
909
+ maxValueNodes: this.limits.maxValueNodes
910
+ }, "output_too_large", correlationId);
911
+ }).finally(() => {
912
+ releasePrincipal?.();
913
+ releaseGlobal();
914
+ });
915
+ settled.catch(() => void 0);
916
+ const aborted = new Promise((_resolve, reject) => {
917
+ const onAbort = () => reject(new McpPublicError(timedOut ? "timeout" : "cancelled", correlationId));
918
+ if (controller.signal.aborted) onAbort();
919
+ else controller.signal.addEventListener("abort", onAbort, { once: true });
920
+ });
921
+ try {
922
+ const result = await Promise.race([settled, aborted]);
923
+ this.emitDiagnostic({
924
+ correlationId,
925
+ ...requestId !== void 0 ? { requestId } : {},
926
+ transport: options.transport,
927
+ operation: options.operation.slice(0, 256),
928
+ durationMs: Date.now() - startedAt,
929
+ outcome: "completed"
930
+ });
931
+ return result;
932
+ } finally {
933
+ clearTimeout(timer);
934
+ options.extra.signal.removeEventListener("abort", cancel);
935
+ }
936
+ } catch (error) {
937
+ rawError = error;
938
+ const publicError = classifyError(error, correlationId);
939
+ this.emitDiagnostic({
940
+ correlationId: publicError.correlationId,
941
+ ...requestId !== void 0 ? { requestId } : {},
942
+ transport: options.transport,
943
+ operation: options.operation.slice(0, 256),
944
+ durationMs: Date.now() - startedAt,
945
+ outcome: "rejected",
946
+ code: publicError.code,
947
+ ...publicError.code === "internal" ? { internalError: diagnosticError(rawError) } : {}
948
+ });
949
+ throw publicError;
950
+ }
951
+ }
952
+ };
953
+
954
+ //#endregion
955
+ //#region src/server/release.ts
956
+ const releases = /* @__PURE__ */ new WeakMap();
957
+ const executionGates = /* @__PURE__ */ new WeakMap();
958
+ function executionGate(app) {
959
+ const key = app;
960
+ const existing = executionGates.get(key);
961
+ if (existing) return existing;
962
+ const created = new McpExecutionGate();
963
+ executionGates.set(key, created);
964
+ return created;
965
+ }
966
+ function freezeConfig(value, seen = /* @__PURE__ */ new WeakSet()) {
967
+ if (!value || typeof value !== "object" || seen.has(value)) return value;
968
+ seen.add(value);
969
+ for (const entry of Object.values(value)) freezeConfig(entry, seen);
970
+ return Object.freeze(value);
971
+ }
972
+ function createRelease(app, config, options) {
973
+ const frozenConfig = freezeConfig(options?.resources === false ? {
974
+ ...config,
975
+ resources: {
976
+ collections: Object.create(null),
977
+ globals: Object.create(null),
978
+ routes: Object.create(null)
979
+ }
980
+ } : config);
981
+ const catalog = resolveMcpCatalog(app, frozenConfig);
982
+ const execution = new McpExecutionBoundary(frozenConfig.execution, executionGate(app));
983
+ const toolCount = [...catalog.collections.values()].reduce((total, entry) => total + entry.operations.length, 0) + [...catalog.globals.values()].reduce((total, entry) => total + entry.operations.length, 0) + catalog.routes.size + catalog.customTools.size;
984
+ const resourceCount = catalog.resources.collections.size + catalog.resources.globals.size + catalog.resources.routes.size;
985
+ if (toolCount > execution.limits.maxTools) throw new Error("MCP released tool count exceeds configured limit");
986
+ if (resourceCount > execution.limits.maxResources) throw new Error("MCP released resource count exceeds configured limit");
987
+ return Object.freeze({
988
+ config: frozenConfig,
989
+ catalog,
990
+ execution
991
+ });
992
+ }
993
+ /** App-owned release snapshot shared by OAuth advertisement and default servers. */
994
+ function getAppMcpRelease(app) {
995
+ const key = app;
996
+ const existing = releases.get(key);
997
+ if (existing) return existing;
998
+ const created = createRelease(app, resolveMcpConfig(app));
999
+ releases.set(key, created);
1000
+ return created;
1001
+ }
1002
+ /** Explicit programmatic overrides are isolated snapshots, never OAuth state. */
1003
+ function createIsolatedMcpRelease(app, override) {
1004
+ return createRelease(app, resolveMcpConfig(app, override));
1005
+ }
1006
+ /** Workload authority is isolated and resources remain unavailable. */
1007
+ function createWorkloadMcpRelease(app, override) {
1008
+ return createRelease(app, resolveWorkloadMcpConfig(app, override), { resources: false });
1009
+ }
1010
+
1011
+ //#endregion
1012
+ //#region src/server/zod-json-schema.ts
1013
+ function jsonSchemaCompatibleSchema(schema) {
1014
+ if (!schema) return void 0;
1015
+ if (canConvertToJsonSchema(schema)) return schema;
1016
+ const def = schema._def;
1017
+ switch (def?.type) {
1018
+ case "date": return z.string().describe("ISO date or date-time string");
1019
+ case "optional": return jsonSchemaCompatibleSchema(def.innerType)?.optional() ?? z.unknown();
1020
+ case "nullable": return jsonSchemaCompatibleSchema(def.innerType)?.nullable() ?? z.unknown();
1021
+ case "default":
1022
+ case "prefault":
1023
+ case "catch": return jsonSchemaCompatibleSchema(def.innerType)?.optional() ?? z.unknown();
1024
+ case "array": return z.array(jsonSchemaCompatibleSchema(def.element) ?? z.unknown());
1025
+ case "record": {
1026
+ const keySchema = jsonSchemaCompatibleSchema(def.keyType);
1027
+ return z.record(keySchema instanceof z.ZodString ? keySchema : z.string(), jsonSchemaCompatibleSchema(def.valueType) ?? z.unknown());
1028
+ }
1029
+ case "union": {
1030
+ const options = Array.isArray(def.options) ? def.options.map((option) => jsonSchemaCompatibleSchema(option) ?? z.unknown()) : [];
1031
+ if (options.length === 0) return z.unknown();
1032
+ if (options.length === 1) return options[0];
1033
+ return z.union(options);
1034
+ }
1035
+ case "object": {
1036
+ const shape = schema.shape;
1037
+ const nextShape = {};
1038
+ for (const [key, value] of Object.entries(shape)) nextShape[key] = jsonSchemaCompatibleSchema(value) ?? z.unknown();
1039
+ const objectSchema = z.object(nextShape);
1040
+ if (def.catchall && def.catchall._def?.type !== "never") return objectSchema.catchall(jsonSchemaCompatibleSchema(def.catchall) ?? z.unknown());
1041
+ return objectSchema;
1042
+ }
1043
+ default: return z.unknown();
1044
+ }
1045
+ }
1046
+ function toJsonSchema(schema) {
1047
+ const compatible = jsonSchemaCompatibleSchema(schema);
1048
+ if (!compatible) return void 0;
1049
+ try {
1050
+ return z.toJSONSchema(compatible, { reused: "ref" });
1051
+ } catch {
1052
+ return;
1053
+ }
1054
+ }
1055
+ function toToolInputJsonSchema(schema) {
1056
+ const parsed = ToolSchema.shape.inputSchema.safeParse(toJsonSchema(schema));
1057
+ return parsed.success ? parsed.data : {
1058
+ type: "object",
1059
+ properties: {}
1060
+ };
1061
+ }
1062
+ function toToolOutputJsonSchema(schema) {
1063
+ const parsed = ToolSchema.shape.outputSchema.safeParse(toJsonSchema(schema));
1064
+ return parsed.success ? parsed.data : void 0;
1065
+ }
1066
+ function canConvertToJsonSchema(schema) {
1067
+ try {
1068
+ z.toJSONSchema(schema);
1069
+ return true;
1070
+ } catch {
1071
+ return false;
1072
+ }
1073
+ }
1074
+
1075
+ //#endregion
1076
+ //#region src/server/field-policy.ts
1077
+ const SYSTEM_FIELD_NAMES = [
1078
+ "id",
1079
+ "createdAt",
1080
+ "updatedAt",
1081
+ "deletedAt",
1082
+ "_status"
1083
+ ];
1084
+ const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
1085
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
1086
+ const hasFieldPolicy = (policy) => (policy.fields?.include?.length ?? 0) > 0 || (policy.fields?.exclude?.length ?? 0) > 0;
1087
+ const allowedKeysFor = (keys, policy) => {
1088
+ const initial = policy.fields?.include?.length ? policy.fields.include : Array.from(keys);
1089
+ const allowed = new Set(initial);
1090
+ for (const key of policy.fields?.exclude ?? []) allowed.delete(key);
1091
+ return allowed;
1092
+ };
1093
+ function filterRecordFields(value, policy) {
1094
+ if (!hasFieldPolicy(policy) || !isRecord(value)) return value;
1095
+ const allowed = allowedKeysFor(Object.keys(value), policy);
1096
+ const next = {};
1097
+ for (const key of allowed) if (hasOwn(value, key)) next[key] = value[key];
1098
+ return next;
1099
+ }
1100
+ function filterCrudResultFields(value, policy) {
1101
+ if (!hasFieldPolicy(policy)) return value;
1102
+ if (Array.isArray(value)) return value.map((item) => filterCrudResultFields(item, policy));
1103
+ if (!isRecord(value)) return value;
1104
+ if (Array.isArray(value.docs)) return {
1105
+ ...value,
1106
+ docs: value.docs.map((doc) => filterRecordFields(doc, policy))
1107
+ };
1108
+ if (isRecord(value.data)) return {
1109
+ ...value,
1110
+ data: filterRecordFields(value.data, policy)
1111
+ };
1112
+ return filterRecordFields(value, policy);
1113
+ }
1114
+ function filterEntitySchemaFields(schema, policy) {
1115
+ if (!hasFieldPolicy(policy) || !isRecord(schema.fields)) return schema;
1116
+ const allowed = allowedKeysFor(Object.keys(schema.fields), policy);
1117
+ const fields = {};
1118
+ for (const key of allowed) if (hasOwn(schema.fields, key)) fields[key] = schema.fields[key];
1119
+ const next = {
1120
+ ...schema,
1121
+ fields
1122
+ };
1123
+ if (isRecord(schema.validation)) {
1124
+ const validation = schema.validation;
1125
+ next.validation = filterValidationSchemas(validation, allowed);
1126
+ }
1127
+ return next;
1128
+ }
1129
+ function filterValidationSchemas(validation, allowed) {
1130
+ const next = { ...validation };
1131
+ for (const [key, schema] of Object.entries(validation)) next[key] = filterJsonSchemaProperties(schema, allowed);
1132
+ return next;
1133
+ }
1134
+ function filterJsonSchemaProperties(schema, allowed) {
1135
+ if (!isRecord(schema) || !isRecord(schema.properties)) return schema;
1136
+ const jsonSchema = schema;
1137
+ const properties = {};
1138
+ for (const key of allowed) if (hasOwn(jsonSchema.properties, key)) properties[key] = jsonSchema.properties[key];
1139
+ return {
1140
+ ...jsonSchema,
1141
+ properties,
1142
+ required: Array.isArray(jsonSchema.required) ? jsonSchema.required.filter((key) => allowed.has(key)) : jsonSchema.required
1143
+ };
1144
+ }
1145
+ function createCollectionDataSchema(collection, operation, policy) {
1146
+ const validation = collection.state?.validation;
1147
+ return filterZodObjectSchema(operation === "create" ? validation?.insertSchema : validation?.updateSchema, policy, entityFieldNames(collection), entityFieldDefinitions(collection));
1148
+ }
1149
+ function createGlobalDataSchema(global, policy) {
1150
+ const validation = global.state?.validation;
1151
+ return filterZodObjectSchema(validation?.updateSchema, policy, entityFieldNames(global), entityFieldDefinitions(global));
1152
+ }
1153
+ function filterZodObjectSchema(schema, policy, knownFields, fieldDefinitions) {
1154
+ if (!schema || !(schema instanceof z.ZodObject)) {
1155
+ const allowed$1 = allowedKeysFor(knownFields ?? [], policy);
1156
+ return z.object(Object.fromEntries([...allowed$1].map((key) => [key, z.unknown().optional()]))).strict();
1157
+ }
1158
+ const shape = schema.shape;
1159
+ if (!(hasFieldPolicy(policy) || !!knownFields?.length)) return schema;
1160
+ const allowed = allowedKeysFor(knownFields ?? Object.keys(shape), policy);
1161
+ const nextShape = {};
1162
+ for (const key of allowed) if (hasOwn(shape, key)) nextShape[key] = jsonCompatibleFieldSchema(fieldDefinitions?.[key], shape[key]) ?? z.unknown();
1163
+ return z.object(nextShape).strict();
1164
+ }
1165
+ function jsonCompatibleFieldSchema(fieldDefinition, schema) {
1166
+ if (!isRecord(fieldDefinition) || !isRecord(fieldDefinition._state)) return jsonSchemaCompatibleSchema(schema);
1167
+ const state = fieldDefinition._state;
1168
+ const type = state.type;
1169
+ let external;
1170
+ if (type === "datetime" || type === "date") {
1171
+ const item = type === "datetime" ? z.iso.datetime({ offset: true }) : z.iso.date();
1172
+ if (state.isArray === true) {
1173
+ let arraySchema = z.array(item);
1174
+ if (typeof state.minItems === "number") arraySchema = arraySchema.min(state.minItems);
1175
+ if (typeof state.maxItems === "number") arraySchema = arraySchema.max(state.maxItems);
1176
+ external = arraySchema;
1177
+ } else external = item;
1178
+ } else if (type === "object" && isRecord(state.nestedFields)) {
1179
+ const nestedShape = {};
1180
+ for (const [key, nestedDefinition] of Object.entries(state.nestedFields)) {
1181
+ if (!isRecord(nestedDefinition) || typeof nestedDefinition.toZodSchema !== "function") continue;
1182
+ nestedShape[key] = jsonCompatibleFieldSchema(nestedDefinition, nestedDefinition.toZodSchema()) ?? z.unknown();
1183
+ }
1184
+ external = z.object(nestedShape);
1185
+ } else return jsonSchemaCompatibleSchema(schema);
1186
+ if (!external) return void 0;
1187
+ if (schema.isNullable()) external = external.nullable();
1188
+ if (schema.isOptional()) external = external.optional();
1189
+ return external;
1190
+ }
1191
+ function entityFieldNames(entity) {
1192
+ const fields = entity.state?.fields;
1193
+ if (!isRecord(fields)) return void 0;
1194
+ return Object.keys(fields);
1195
+ }
1196
+ function entityFieldDefinitions(entity) {
1197
+ const fieldDefinitions = entity.state?.fieldDefinitions;
1198
+ return isRecord(fieldDefinitions) ? fieldDefinitions : void 0;
1199
+ }
1200
+ function allowedEntityFieldNames(entity, policy) {
1201
+ const entityFields = entityFieldNames(entity) ?? [];
1202
+ return [...allowedKeysFor(policy.fields?.include?.length ? entityFields : [...new Set([...entityFields, ...SYSTEM_FIELD_NAMES])], policy)];
1203
+ }
1204
+ function allowedEntityRelationNames(entity, policy) {
1205
+ const fields = entity.state?.fields;
1206
+ if (!isRecord(fields)) return [];
1207
+ const allowed = new Set(allowedEntityFieldNames(entity, policy));
1208
+ return Object.entries(fields).filter(([name, field]) => {
1209
+ if (!allowed.has(name) || !isRecord(field)) return false;
1210
+ const fieldState = field["_state"];
1211
+ const state = isRecord(fieldState) ? fieldState : void 0;
1212
+ return state?.type === "relation" || state?.type === "upload";
1213
+ }).map(([name]) => name);
1214
+ }
1215
+
1216
+ //#endregion
1217
+ //#region src/server/runtime.ts
1218
+ function createRuntimeScope(app, options, execution, workload) {
1219
+ const transport = options.transport ?? "http";
1220
+ const accessMode = options.accessMode ?? "user";
1221
+ const createContext = createContextFactory(app);
1222
+ let requestContextPromise;
1223
+ return {
1224
+ app,
1225
+ transport,
1226
+ accessMode,
1227
+ request: options.request,
1228
+ workload,
1229
+ execution,
1230
+ async getContext() {
1231
+ if (options.ctx) return options.ctx;
1232
+ if (options.request) {
1233
+ requestContextPromise ??= createAdapterContext(app, options.request, { accessMode }).then((adapterContext) => ({
1234
+ ...adapterContext.appContext,
1235
+ request: options.request
1236
+ }));
1237
+ return requestContextPromise;
1238
+ }
1239
+ return createContext({ accessMode });
1240
+ }
1241
+ };
1242
+ }
1243
+ function toRequestContext(ctx, accessMode) {
1244
+ return {
1245
+ session: ctx.session,
1246
+ principal: ctx.principal,
1247
+ locale: ctx.locale,
1248
+ defaultLocale: ctx.defaultLocale,
1249
+ localeFallback: ctx.localeFallback,
1250
+ accessMode: ctx.accessMode ?? accessMode,
1251
+ stage: ctx.stage,
1252
+ requestId: ctx.requestId,
1253
+ traceId: ctx.traceId,
1254
+ db: ctx.db,
1255
+ "~contextExtensions": ctx["~contextExtensions"]
1256
+ };
1257
+ }
1258
+ const MAX_MCP_RUNTIME_JSON_BYTES = 4 * 1024 * 1024;
1259
+ const UTF8_ENCODER = new TextEncoder();
1260
+ function runtimeSnapshot(value) {
1261
+ return snapshotBoundedMcpValue(value, {
1262
+ maxBytes: MAX_MCP_RUNTIME_JSON_BYTES,
1263
+ maxValueDepth: 64,
1264
+ maxValueNodes: 1e5
1265
+ }, "output_too_large");
1266
+ }
1267
+ function stringifyRuntimeSnapshot(value) {
1268
+ const serialized = JSON.stringify(value, null, 2);
1269
+ if (UTF8_ENCODER.encode(serialized).byteLength > MAX_MCP_RUNTIME_JSON_BYTES) throw new McpPublicError("output_too_large");
1270
+ return serialized;
1271
+ }
1272
+ function safeJsonStringify(value) {
1273
+ return stringifyRuntimeSnapshot(runtimeSnapshot(value));
1274
+ }
1275
+ function toToolResult(value) {
1276
+ const snapshot = runtimeSnapshot(value);
1277
+ return {
1278
+ structuredContent: snapshot && typeof snapshot === "object" && !Array.isArray(snapshot) ? snapshot : { value: snapshot },
1279
+ content: [{
1280
+ type: "text",
1281
+ text: stringifyRuntimeSnapshot(snapshot)
1282
+ }]
1283
+ };
1284
+ }
1285
+ function toToolError(error) {
1286
+ return toMcpToolError(error);
1287
+ }
1288
+ function jsonResource(uri, value) {
1289
+ return { contents: [{
1290
+ uri,
1291
+ mimeType: "application/json",
1292
+ text: safeJsonStringify(value)
1293
+ }] };
1294
+ }
1295
+
1296
+ //#endregion
1297
+ //#region src/server/workload-boundary.ts
1298
+ const PUBLIC_ACCESS_DENIED = "MCP access denied";
1299
+ function isProxy(value) {
1300
+ return !!value && (typeof value === "object" || typeof value === "function") && types.isProxy(value);
1301
+ }
1302
+ function readMethod(owner, name) {
1303
+ if (!owner || typeof owner !== "object" && typeof owner !== "function") return null;
1304
+ if (isProxy(owner)) return null;
1305
+ try {
1306
+ let candidate = owner;
1307
+ while (candidate) {
1308
+ if (isProxy(candidate)) return null;
1309
+ const descriptor = Object.getOwnPropertyDescriptor(candidate, name);
1310
+ if (descriptor) {
1311
+ if (!("value" in descriptor) || typeof descriptor.value !== "function") return null;
1312
+ return descriptor.value.bind(owner);
1313
+ }
1314
+ candidate = Object.getPrototypeOf(candidate);
1315
+ }
1316
+ return null;
1317
+ } catch {
1318
+ return null;
1319
+ }
1320
+ }
1321
+ function createWorkloadMcpBoundary(options) {
1322
+ try {
1323
+ if (!options || typeof options !== "object" || isProxy(options) || !("envelope" in options) || options.envelope === void 0) throw new Error(PUBLIC_ACCESS_DENIED);
1324
+ const concurrencyKey = options.concurrencyKey === void 0 ? `boundary:${randomUUID()}` : typeof options.concurrencyKey === "string" && /^[\w.:-]{1,256}$/.test(options.concurrencyKey) ? `consumer:${options.concurrencyKey}` : null;
1325
+ if (!concurrencyKey) throw new Error(PUBLIC_ACCESS_DENIED);
1326
+ const authorize = readMethod(options.authorizer, "authorize");
1327
+ const bindContext = readMethod(options.contextBinder, "bind");
1328
+ if (!authorize || !bindContext) throw new Error(PUBLIC_ACCESS_DENIED);
1329
+ let executeHandoff;
1330
+ if (options.handoff) {
1331
+ const method = readMethod(options.handoff, "execute");
1332
+ if (!method) throw new Error(PUBLIC_ACCESS_DENIED);
1333
+ executeHandoff = method;
1334
+ }
1335
+ return {
1336
+ envelope: options.envelope,
1337
+ concurrencyKey,
1338
+ authorize,
1339
+ bindContext,
1340
+ audit: options.audit,
1341
+ executeHandoff,
1342
+ discoveryTools: []
1343
+ };
1344
+ } catch {
1345
+ throw new Error(PUBLIC_ACCESS_DENIED);
1346
+ }
1347
+ }
1348
+ function snapshotAuthorization(value) {
1349
+ try {
1350
+ if (!value || typeof value !== "object" || Array.isArray(value) || isProxy(value)) return null;
1351
+ const descriptors = Object.getOwnPropertyDescriptors(value);
1352
+ const context = descriptors.context;
1353
+ const attribution = descriptors.attribution;
1354
+ if (!context || !("value" in context)) return null;
1355
+ if (attribution && !("value" in attribution)) return null;
1356
+ if (isProxy(context.value) || attribution && isProxy(attribution.value)) return null;
1357
+ return Object.freeze({
1358
+ context: context.value,
1359
+ ...attribution ? { attribution: attribution.value } : {}
1360
+ });
1361
+ } catch {
1362
+ return null;
1363
+ }
1364
+ }
1365
+ function isBoundContext(value) {
1366
+ try {
1367
+ if (!value || typeof value !== "object" || Array.isArray(value) || isProxy(value)) return false;
1368
+ const descriptors = Object.getOwnPropertyDescriptors(value);
1369
+ const accessMode = descriptors.accessMode;
1370
+ const db = descriptors.db;
1371
+ const principal = descriptors.principal;
1372
+ return !!accessMode && "value" in accessMode && accessMode.value === "user" && !!db && "value" in db && !!db.value && (!principal || "value" in principal && principal.value?.kind !== "system");
1373
+ } catch {
1374
+ return false;
1375
+ }
1376
+ }
1377
+ function normalizeRequirement(requirement) {
1378
+ try {
1379
+ if (!requirement || typeof requirement !== "object" || isProxy(requirement) || !Array.isArray(requirement.capabilities) || requirement.capabilities.length === 0 || requirement.capabilities.length > 64 || requirement.capabilities.some((capability) => isProxy(capability)) || !requirement.capabilities.every((capability) => typeof capability === "string" && capability.length > 0 && capability.length <= 256) || requirement.handoff !== void 0 && (typeof requirement.handoff !== "string" || requirement.handoff.length === 0 || requirement.handoff.length > 256)) return null;
1380
+ return Object.freeze({
1381
+ capabilities: Object.freeze([...requirement.capabilities]),
1382
+ ...requirement.handoff ? { handoff: requirement.handoff } : {}
1383
+ });
1384
+ } catch {
1385
+ return null;
1386
+ }
1387
+ }
1388
+ function normalizeTool(identity, requirement) {
1389
+ return Object.freeze({
1390
+ kind: identity.kind,
1391
+ name: identity.name,
1392
+ operation: identity.operation,
1393
+ intent: identity.intent,
1394
+ transport: "workload",
1395
+ capabilities: Object.freeze([...requirement.capabilities]),
1396
+ ...requirement.handoff ? { handoff: requirement.handoff } : {}
1397
+ });
1398
+ }
1399
+ async function auditDecision(boundary, event) {
1400
+ try {
1401
+ await boundary.audit?.(event);
1402
+ } catch {
1403
+ throw new Error(PUBLIC_ACCESS_DENIED);
1404
+ }
1405
+ }
1406
+ async function authorizeWorkload(boundary, phase, identity, requirement, control) {
1407
+ const cancelled = () => control?.signal.aborted === true;
1408
+ if (cancelled()) return null;
1409
+ const normalized = normalizeRequirement(requirement);
1410
+ if (!normalized || normalized.handoff && !boundary.executeHandoff) {
1411
+ await auditDecision(boundary, {
1412
+ phase,
1413
+ decision: "denied",
1414
+ reason: "authorization_denied"
1415
+ });
1416
+ return null;
1417
+ }
1418
+ const tool = normalizeTool(identity, normalized);
1419
+ let result;
1420
+ try {
1421
+ result = await boundary.authorize({
1422
+ phase,
1423
+ envelope: boundary.envelope,
1424
+ tool
1425
+ }, control);
1426
+ } catch {
1427
+ result = null;
1428
+ }
1429
+ if (cancelled()) return null;
1430
+ const authorization = snapshotAuthorization(result);
1431
+ if (!authorization) {
1432
+ await auditDecision(boundary, {
1433
+ phase,
1434
+ decision: "denied",
1435
+ reason: "authorization_invalid"
1436
+ });
1437
+ return null;
1438
+ }
1439
+ if (cancelled()) return null;
1440
+ let context;
1441
+ try {
1442
+ context = await boundary.bindContext({
1443
+ authorizationContext: authorization.context,
1444
+ attribution: authorization.attribution,
1445
+ tool
1446
+ }, control);
1447
+ } catch {
1448
+ context = null;
1449
+ }
1450
+ if (cancelled()) return null;
1451
+ if (!isBoundContext(context)) {
1452
+ await auditDecision(boundary, {
1453
+ phase,
1454
+ decision: "denied",
1455
+ reason: "context_invalid"
1456
+ });
1457
+ return null;
1458
+ }
1459
+ if (cancelled()) return null;
1460
+ await auditDecision(boundary, {
1461
+ phase,
1462
+ decision: "allowed",
1463
+ toolName: tool.name,
1464
+ attribution: authorization.attribution
1465
+ });
1466
+ if (cancelled()) return null;
1467
+ return Object.freeze({
1468
+ authorization,
1469
+ concurrencyKey: boundary.concurrencyKey,
1470
+ context,
1471
+ tool
1472
+ });
1473
+ }
1474
+ function registerWorkloadDiscoveryTool(boundary, tool, identity, requirement, allows) {
1475
+ const normalized = normalizeRequirement(requirement);
1476
+ if (!normalized) return;
1477
+ boundary.discoveryTools.push({
1478
+ tool: Object.freeze({ ...tool }),
1479
+ identity: Object.freeze({ ...identity }),
1480
+ requirement: normalized,
1481
+ allows
1482
+ });
1483
+ }
1484
+ async function listWorkloadTools(boundary, execution, extra) {
1485
+ const tools = [];
1486
+ const controller = new AbortController();
1487
+ let timedOut = false;
1488
+ const cancel = () => controller.abort(extra.signal.reason);
1489
+ if (extra.signal.aborted) cancel();
1490
+ else extra.signal.addEventListener("abort", cancel, { once: true });
1491
+ const timer = setTimeout(() => {
1492
+ timedOut = true;
1493
+ controller.abort(/* @__PURE__ */ new Error("MCP discovery deadline exceeded"));
1494
+ }, execution.limits.timeoutMs);
1495
+ const boundedExtra = {
1496
+ ...extra,
1497
+ signal: controller.signal
1498
+ };
1499
+ try {
1500
+ for (const candidate of boundary.discoveryTools) try {
1501
+ let workloadAuthorization;
1502
+ const tool = await execution.execute({
1503
+ operation: "tools/list",
1504
+ transport: "workload",
1505
+ accessMode: "user",
1506
+ input: { name: candidate.tool.name },
1507
+ extra: boundedExtra,
1508
+ authorize: async (control) => {
1509
+ workloadAuthorization = await authorizeWorkload(boundary, "discovery", candidate.identity, candidate.requirement, control);
1510
+ if (!workloadAuthorization || !await candidate.allows(workloadAuthorization.context)) throw new McpAccessDeniedError();
1511
+ return workloadAuthorization.context;
1512
+ },
1513
+ concurrencyKey: () => workloadAuthorization?.concurrencyKey,
1514
+ invoke: async () => candidate.tool
1515
+ });
1516
+ tools.push(tool);
1517
+ } catch (error) {
1518
+ const code = mcpPublicErrorCode(error);
1519
+ if (code === "access_denied" || code === "internal") continue;
1520
+ if (timedOut && code === "cancelled") throw new McpPublicError("timeout");
1521
+ throw error;
1522
+ }
1523
+ assertBoundedMcpValue({ tools }, {
1524
+ maxBytes: execution.limits.maxOutputBytes,
1525
+ maxValueDepth: execution.limits.maxOutputDepth,
1526
+ maxValueNodes: execution.limits.maxValueNodes
1527
+ }, "output_too_large");
1528
+ return { tools };
1529
+ } finally {
1530
+ clearTimeout(timer);
1531
+ extra.signal.removeEventListener("abort", cancel);
1532
+ }
1533
+ }
1534
+ async function executeWorkloadTool(boundary, authorized, metadata, execution, invoke) {
1535
+ if (!authorized.tool.handoff) return invoke();
1536
+ if (!boundary.executeHandoff) throw new Error(PUBLIC_ACCESS_DENIED);
1537
+ try {
1538
+ return await boundary.executeHandoff({
1539
+ authorizationContext: authorized.authorization.context,
1540
+ attribution: authorized.authorization.attribution,
1541
+ toolName: authorized.tool.name,
1542
+ capability: authorized.tool.handoff,
1543
+ tool: authorized.tool,
1544
+ metadata,
1545
+ signal: execution.signal,
1546
+ requestId: execution.requestId,
1547
+ correlationId: execution.correlationId,
1548
+ invoke
1549
+ });
1550
+ } catch {
1551
+ throw new Error(PUBLIC_ACCESS_DENIED);
1552
+ }
1553
+ }
1554
+
1555
+ //#endregion
1556
+ //#region src/server/crud-tools.ts
1557
+ const idSchema = z.union([z.string(), z.number()]);
1558
+ const deleteSchema = z.object({ id: idSchema }).strict();
1559
+ const MAX_QUERY_FIELDS = 64;
1560
+ const MAX_WHERE_DEPTH = 3;
1561
+ const MAX_LOGICAL_CLAUSES = 16;
1562
+ const MAX_OPERATOR_VALUES = 100;
1563
+ const MAX_OFFSET = 1e4;
1564
+ const scalarQueryValue = z.union([
1565
+ z.string().max(4096),
1566
+ z.number().finite(),
1567
+ z.boolean(),
1568
+ z.null()
1569
+ ]);
1570
+ const queryValue = z.union([scalarQueryValue, z.array(scalarQueryValue).max(MAX_OPERATOR_VALUES)]);
1571
+ const operatorSchema = z.object(Object.fromEntries([
1572
+ "eq",
1573
+ "ne",
1574
+ "not",
1575
+ "gt",
1576
+ "gte",
1577
+ "lt",
1578
+ "lte",
1579
+ "in",
1580
+ "notIn",
1581
+ "like",
1582
+ "ilike",
1583
+ "notLike",
1584
+ "notIlike",
1585
+ "contains",
1586
+ "startsWith",
1587
+ "endsWith",
1588
+ "isNull",
1589
+ "isNotNull",
1590
+ "arrayOverlaps",
1591
+ "arrayContained",
1592
+ "arrayContains"
1593
+ ].map((operator) => [operator, queryValue.optional()]))).strict();
1594
+ const fieldCondition = z.union([queryValue, operatorSchema]).meta({ id: "QuestpieMcpFieldCondition" });
1595
+ function boundedFields(fields) {
1596
+ return fields.slice(0, MAX_QUERY_FIELDS);
1597
+ }
1598
+ function whereSchema(fields, depth = 0) {
1599
+ const shape = Object.fromEntries(boundedFields(fields).map((field) => [field, fieldCondition.optional()]));
1600
+ if (depth < MAX_WHERE_DEPTH) {
1601
+ const nested = whereSchema(fields, depth + 1);
1602
+ shape.AND = z.array(nested).max(MAX_LOGICAL_CLAUSES).optional();
1603
+ shape.OR = z.array(nested).max(MAX_LOGICAL_CLAUSES).optional();
1604
+ shape.NOT = nested.optional();
1605
+ }
1606
+ return z.object(shape).strict();
1607
+ }
1608
+ function columnsSchema(fields) {
1609
+ return z.object(Object.fromEntries(boundedFields(fields).map((field) => [field, z.boolean().optional()]))).strict();
1610
+ }
1611
+ function sortSchema(fields) {
1612
+ return z.object(Object.fromEntries(boundedFields(fields).map((field) => [field, z.enum(["asc", "desc"]).optional()]))).strict();
1613
+ }
1614
+ function relationLoadSchema(relations) {
1615
+ return z.object(Object.fromEntries(boundedFields(relations).map((field) => [field, z.literal(true).optional()]))).strict();
1616
+ }
1617
+ function collectionReadSchemas(collection, policy) {
1618
+ const fields = allowedEntityFieldNames(collection, policy);
1619
+ const common = {
1620
+ with: relationLoadSchema(allowedEntityRelationNames(collection, policy)).optional(),
1621
+ columns: columnsSchema(fields).optional(),
1622
+ locale: z.string().max(64).optional(),
1623
+ includeDeleted: z.boolean().optional(),
1624
+ stage: z.string().max(64).optional()
1625
+ };
1626
+ return {
1627
+ list: z.object({
1628
+ where: whereSchema(fields).optional(),
1629
+ sort: sortSchema(fields).optional(),
1630
+ limit: z.number().int().positive().max(1e4).optional(),
1631
+ page: z.number().int().positive().max(1e3).optional(),
1632
+ offset: z.number().int().nonnegative().max(MAX_OFFSET).optional(),
1633
+ ...common
1634
+ }).strict(),
1635
+ count: z.object({
1636
+ where: whereSchema(fields).optional(),
1637
+ includeDeleted: z.boolean().optional()
1638
+ }).strict(),
1639
+ get: z.object({
1640
+ id: idSchema,
1641
+ ...common
1642
+ }).strict()
1643
+ };
1644
+ }
1645
+ function globalOperationBaseSchema(global, policy) {
1646
+ const fields = allowedEntityFieldNames(global, policy);
1647
+ return {
1648
+ with: relationLoadSchema(allowedEntityRelationNames(global, policy)).optional(),
1649
+ columns: columnsSchema(fields).optional(),
1650
+ locale: z.string().max(64).optional(),
1651
+ stage: z.string().max(64).optional()
1652
+ };
1653
+ }
1654
+ const COLLECTION_OPERATIONS = [
1655
+ {
1656
+ name: "list",
1657
+ kind: "read",
1658
+ description: (name) => `List ${name} records.`,
1659
+ execute: (crud, input, ctx, maxLimit) => crud.find({
1660
+ ...input,
1661
+ limit: Math.min(input.limit ?? maxLimit, maxLimit)
1662
+ }, ctx)
1663
+ },
1664
+ {
1665
+ name: "count",
1666
+ kind: "read",
1667
+ description: (name) => `Count ${name} records.`,
1668
+ execute: (crud, input, ctx) => crud.count(input, ctx)
1669
+ },
1670
+ {
1671
+ name: "get",
1672
+ kind: "read",
1673
+ description: (name) => `Get one ${name} record by id.`,
1674
+ execute: (crud, input, ctx) => {
1675
+ const { id, ...options } = input;
1676
+ return crud.findOne({
1677
+ ...options,
1678
+ where: { id }
1679
+ }, ctx);
1680
+ }
1681
+ },
1682
+ {
1683
+ name: "create",
1684
+ kind: "write",
1685
+ description: (name) => `Create a ${name} record.`,
1686
+ execute: (crud, input, ctx) => crud.create(input.data, ctx)
1687
+ },
1688
+ {
1689
+ name: "update",
1690
+ kind: "write",
1691
+ description: (name) => `Update a ${name} record by id.`,
1692
+ execute: (crud, input, ctx) => crud.updateById({
1693
+ id: input.id,
1694
+ data: input.data
1695
+ }, ctx)
1696
+ },
1697
+ {
1698
+ name: "delete",
1699
+ kind: "delete",
1700
+ description: (name) => `Delete a ${name} record by id.`,
1701
+ execute: (crud, input, ctx) => crud.deleteById({ id: input.id }, ctx)
1702
+ }
1703
+ ];
1704
+ const GLOBAL_OPERATIONS = [{
1705
+ name: "get",
1706
+ kind: "read",
1707
+ description: (name) => `Get the ${name} global.`,
1708
+ execute: (crud, input, ctx) => crud.get(input, ctx)
1709
+ }, {
1710
+ name: "update",
1711
+ kind: "write",
1712
+ description: (name) => `Update the ${name} global.`,
1713
+ execute: (crud, input, ctx) => {
1714
+ const { data, ...options } = input;
1715
+ return crud.update(data, ctx, options);
1716
+ }
1717
+ }];
1718
+ function collectionOperationSchema(operationName, collection, policy) {
1719
+ if (operationName === "create") return z.object({ data: createCollectionDataSchema(collection, "create", policy) }).strict();
1720
+ if (operationName === "update") return z.object({
1721
+ id: idSchema,
1722
+ data: createCollectionDataSchema(collection, "update", policy)
1723
+ }).strict();
1724
+ return collectionReadSchemas(collection, policy)[operationName] ?? deleteSchema;
1725
+ }
1726
+ function globalOperationSchema(operationName, global, policy) {
1727
+ if (operationName === "update") return z.object({
1728
+ data: createGlobalDataSchema(global, policy),
1729
+ ...globalOperationBaseSchema(global, policy)
1730
+ }).strict();
1731
+ return z.object(globalOperationBaseSchema(global, policy)).strict();
1732
+ }
1733
+ async function questpieAllows(scope, ctx, kind, name, operationName) {
1734
+ if (scope.accessMode === "system") return true;
1735
+ const crudContext = {
1736
+ db: ctx.db ?? scope.app.db,
1737
+ session: ctx.session,
1738
+ locale: ctx.locale,
1739
+ accessMode: scope.accessMode,
1740
+ stage: ctx.stage
1741
+ };
1742
+ if (kind === "collection") {
1743
+ const collection = scope.app.getCollections()[name];
1744
+ const schema$1 = await introspectCollection(collection, crudContext, scope.app);
1745
+ if (!schema$1.access.visible) return false;
1746
+ const mappedOperation$1 = operationName === "create" || operationName === "update" || operationName === "delete" ? operationName : "read";
1747
+ const result$1 = schema$1.access.operations[mappedOperation$1];
1748
+ return !!result$1 && result$1.allowed !== false;
1749
+ }
1750
+ const global = scope.app.getGlobals()[name];
1751
+ const schema = await introspectGlobal(global, crudContext, scope.app);
1752
+ if (!schema.access.visible) return false;
1753
+ const mappedOperation = operationName === "update" ? "update" : "read";
1754
+ const result = schema.access.operations[mappedOperation];
1755
+ return !!result && result.allowed !== false;
1756
+ }
1757
+ async function allowsOperation(scope, ctx, policy, kind, entityName, operationName, operationKind) {
1758
+ if (!await evaluateMcpRule(operationRule(policy, operationName), {
1759
+ transport: scope.transport,
1760
+ accessMode: scope.accessMode,
1761
+ ctx
1762
+ })) return false;
1763
+ if (!scopeGateAllows(scopesFromContext(ctx), requiredScopesForOperation(policy, kind, entityName, operationName, operationKind))) return false;
1764
+ return questpieAllows(scope, ctx, kind, entityName, operationName);
1765
+ }
1766
+ async function shouldRegister(scope, policy, kind, entityName, operationName, operationKind) {
1767
+ if (!policy.expose) return false;
1768
+ if (scope.workload && !workloadRequirementForOperation(policy, operationName)) return false;
1769
+ if (scope.workload) return true;
1770
+ return allowsOperation(scope, await scope.getContext(), policy, kind, entityName, operationName, operationKind);
1771
+ }
1772
+ async function registerCrudTools(server, scope, config, catalog) {
1773
+ const maxLimit = config.crud?.maxLimit ?? 100;
1774
+ const collections = scope.app.getCollections();
1775
+ const globals = scope.app.getGlobals();
1776
+ for (const [name, catalogEntry] of catalog.collections) {
1777
+ const collection = collections[name];
1778
+ const policy = catalogEntry.policy;
1779
+ const crud = scope.app.collections[name];
1780
+ for (const operation of COLLECTION_OPERATIONS) {
1781
+ if (!catalogEntry.operations.includes(operation.name)) continue;
1782
+ const toolName = `collections.${name}.${operation.name}`;
1783
+ const inputSchema = collectionOperationSchema(operation.name, collection, policy);
1784
+ const annotations = {
1785
+ readOnlyHint: operation.kind === "read",
1786
+ destructiveHint: operation.kind === "delete",
1787
+ idempotentHint: operation.name === "update" || operation.name === "delete"
1788
+ };
1789
+ const workloadRequirement = workloadRequirementForOperation(policy, operation.name);
1790
+ const workloadIdentity = {
1791
+ kind: "collection",
1792
+ name: toolName,
1793
+ operation: operation.name,
1794
+ intent: operation.kind === "read" && !workloadRequirement?.handoff ? "read" : "effect"
1795
+ };
1796
+ const allows = (ctx) => allowsOperation(scope, ctx, policy, "collection", name, operation.name, operation.kind);
1797
+ if (!await shouldRegister(scope, policy, "collection", name, operation.name, operation.kind)) continue;
1798
+ server.registerTool(toolName, {
1799
+ description: policy.description ?? operation.description(name),
1800
+ inputSchema,
1801
+ annotations
1802
+ }, async (input, extra) => {
1803
+ try {
1804
+ let workloadAuthorization;
1805
+ return await scope.execution.execute({
1806
+ operation: toolName,
1807
+ transport: scope.transport,
1808
+ accessMode: scope.accessMode,
1809
+ input,
1810
+ extra,
1811
+ authorize: async (control) => {
1812
+ workloadAuthorization = scope.workload ? await authorizeWorkload(scope.workload, "call", workloadIdentity, workloadRequirement, control) : void 0;
1813
+ if (scope.workload && !workloadAuthorization) throw new McpAccessDeniedError();
1814
+ const ctx = workloadAuthorization?.context ?? await scope.getContext();
1815
+ if (!await allows(ctx)) throw new McpAccessDeniedError();
1816
+ return ctx;
1817
+ },
1818
+ concurrencyKey: () => workloadAuthorization?.concurrencyKey,
1819
+ invoke: async ({ ctx, signal, requestId, correlationId }) => {
1820
+ const invoke = async () => {
1821
+ return toToolResult(filterCrudResultFields(await operation.execute(crud, input, ctx, maxLimit), policy));
1822
+ };
1823
+ if (scope.workload && workloadAuthorization && workloadRequirement) return executeWorkloadTool(scope.workload, workloadAuthorization, void 0, {
1824
+ signal,
1825
+ requestId,
1826
+ correlationId
1827
+ }, invoke);
1828
+ return invoke();
1829
+ }
1830
+ });
1831
+ } catch (error) {
1832
+ return toToolError(error);
1833
+ }
1834
+ });
1835
+ if (scope.workload) registerWorkloadDiscoveryTool(scope.workload, {
1836
+ name: toolName,
1837
+ description: policy.description ?? operation.description(name),
1838
+ inputSchema: toToolInputJsonSchema(inputSchema),
1839
+ annotations
1840
+ }, workloadIdentity, workloadRequirement, allows);
1841
+ }
1842
+ }
1843
+ for (const [name, catalogEntry] of catalog.globals) {
1844
+ const global = globals[name];
1845
+ const policy = catalogEntry.policy;
1846
+ const crud = scope.app.globals[name];
1847
+ for (const operation of GLOBAL_OPERATIONS) {
1848
+ if (!catalogEntry.operations.includes(operation.name)) continue;
1849
+ const toolName = `globals.${name}.${operation.name}`;
1850
+ const inputSchema = globalOperationSchema(operation.name, global, policy);
1851
+ const annotations = {
1852
+ readOnlyHint: operation.kind === "read",
1853
+ idempotentHint: operation.name === "update"
1854
+ };
1855
+ const workloadRequirement = workloadRequirementForOperation(policy, operation.name);
1856
+ const workloadIdentity = {
1857
+ kind: "global",
1858
+ name: toolName,
1859
+ operation: operation.name,
1860
+ intent: operation.kind === "read" && !workloadRequirement?.handoff ? "read" : "effect"
1861
+ };
1862
+ const allows = (ctx) => allowsOperation(scope, ctx, policy, "global", name, operation.name, operation.kind);
1863
+ if (!await shouldRegister(scope, policy, "global", name, operation.name, operation.kind)) continue;
1864
+ server.registerTool(toolName, {
1865
+ description: policy.description ?? operation.description(name),
1866
+ inputSchema,
1867
+ annotations
1868
+ }, async (input, extra) => {
1869
+ try {
1870
+ let workloadAuthorization;
1871
+ return await scope.execution.execute({
1872
+ operation: toolName,
1873
+ transport: scope.transport,
1874
+ accessMode: scope.accessMode,
1875
+ input,
1876
+ extra,
1877
+ authorize: async (control) => {
1878
+ workloadAuthorization = scope.workload ? await authorizeWorkload(scope.workload, "call", workloadIdentity, workloadRequirement, control) : void 0;
1879
+ if (scope.workload && !workloadAuthorization) throw new McpAccessDeniedError();
1880
+ const ctx = workloadAuthorization?.context ?? await scope.getContext();
1881
+ if (!await allows(ctx)) throw new McpAccessDeniedError();
1882
+ return ctx;
1883
+ },
1884
+ concurrencyKey: () => workloadAuthorization?.concurrencyKey,
1885
+ invoke: async ({ ctx, signal, requestId, correlationId }) => {
1886
+ const invoke = async () => {
1887
+ return toToolResult(filterCrudResultFields(await operation.execute(crud, input, ctx), policy));
1888
+ };
1889
+ if (scope.workload && workloadAuthorization && workloadRequirement) return executeWorkloadTool(scope.workload, workloadAuthorization, void 0, {
1890
+ signal,
1891
+ requestId,
1892
+ correlationId
1893
+ }, invoke);
1894
+ return invoke();
1895
+ }
1896
+ });
1897
+ } catch (error) {
1898
+ return toToolError(error);
1899
+ }
1900
+ });
1901
+ if (scope.workload) registerWorkloadDiscoveryTool(scope.workload, {
1902
+ name: toolName,
1903
+ description: policy.description ?? operation.description(name),
1904
+ inputSchema: toToolInputJsonSchema(inputSchema),
1905
+ annotations
1906
+ }, workloadIdentity, workloadRequirement, allows);
1907
+ }
1908
+ }
1909
+ }
1910
+
1911
+ //#endregion
1912
+ //#region src/server/custom-tools.ts
1913
+ function customToolWorkloadIdentity(toolName, tool) {
1914
+ return {
1915
+ kind: "custom",
1916
+ name: toolName,
1917
+ operation: "execute",
1918
+ intent: tool.config.workload?.handoff || tool.config.annotations?.readOnlyHint !== true ? "effect" : "read"
1919
+ };
1920
+ }
1921
+ function customToolDescriptor(toolName, tool) {
1922
+ const inputSchema = customToolInputSchema(tool);
1923
+ const discoveryOutputSchema = toToolOutputJsonSchema(jsonSchemaCompatibleSchema(tool.config.outputSchema));
1924
+ return {
1925
+ name: toolName,
1926
+ title: tool.config.title,
1927
+ description: tool.config.description,
1928
+ inputSchema: toToolInputJsonSchema(inputSchema),
1929
+ ...discoveryOutputSchema ? { outputSchema: discoveryOutputSchema } : {},
1930
+ annotations: tool.config.annotations,
1931
+ _meta: tool.config["_meta"]
1932
+ };
1933
+ }
1934
+ function customToolInputSchema(tool) {
1935
+ return jsonSchemaCompatibleSchema(tool.config.inputSchema) ?? z.object({}).strict();
1936
+ }
1937
+ function customToolAllows(scope, tool, ctx) {
1938
+ const requiredScopes = normalizeRequiredScopes(tool.config.scopes);
1939
+ return Promise.resolve(evaluateMcpRule(tool.config.access, {
1940
+ transport: scope.transport,
1941
+ accessMode: scope.accessMode,
1942
+ ctx
1943
+ })).then((allowed) => allowed && scopeGateAllows(scopesFromContext(ctx), requiredScopes));
1944
+ }
1945
+ async function validateCustomToolOutput(tool, result) {
1946
+ if (!tool.config.outputSchema || result.isError) return result;
1947
+ const parsed = await tool.config.outputSchema.safeParseAsync(result.structuredContent);
1948
+ if (!parsed.success) throw new Error("MCP custom tool output failed validation");
1949
+ return {
1950
+ ...result,
1951
+ structuredContent: parsed.data
1952
+ };
1953
+ }
1954
+ async function executeCustomToolCall(scope, toolName, tool, input, extra) {
1955
+ const workloadIdentity = customToolWorkloadIdentity(toolName, tool);
1956
+ let workloadAuthorization;
1957
+ return scope.execution.execute({
1958
+ operation: toolName,
1959
+ transport: scope.transport,
1960
+ accessMode: scope.accessMode,
1961
+ input,
1962
+ extra,
1963
+ authorize: async (control) => {
1964
+ workloadAuthorization = scope.workload ? await authorizeWorkload(scope.workload, "call", workloadIdentity, tool.config.workload, control) : void 0;
1965
+ if (scope.workload && !workloadAuthorization) throw new McpAccessDeniedError();
1966
+ const callCtx = workloadAuthorization?.context ?? await scope.getContext();
1967
+ if (!await customToolAllows(scope, tool, callCtx)) throw new McpAccessDeniedError();
1968
+ return callCtx;
1969
+ },
1970
+ concurrencyKey: () => workloadAuthorization?.concurrencyKey,
1971
+ invoke: async ({ ctx, signal, requestId, correlationId }) => {
1972
+ const parsedInput = tool.config.inputSchema ? await tool.config.inputSchema.parseAsync(input) : await z.object({}).strict().parseAsync(input);
1973
+ const invoke = () => tool.handler({
1974
+ input: parsedInput,
1975
+ ctx,
1976
+ transport: scope.transport,
1977
+ accessMode: scope.accessMode,
1978
+ request: scope.request,
1979
+ signal,
1980
+ requestId,
1981
+ correlationId
1982
+ });
1983
+ if (scope.workload && workloadAuthorization && tool.config.workload) return validateCustomToolOutput(tool, await executeWorkloadTool(scope.workload, workloadAuthorization, void 0, {
1984
+ signal,
1985
+ requestId,
1986
+ correlationId
1987
+ }, invoke));
1988
+ return validateCustomToolOutput(tool, await invoke());
1989
+ }
1990
+ });
1991
+ }
1992
+ async function registerCustomTools(server, scope, catalog) {
1993
+ for (const [toolName, { tool }] of catalog.customTools) {
1994
+ const workloadIdentity = customToolWorkloadIdentity(toolName, tool);
1995
+ if (scope.workload && !tool.config.workload) continue;
1996
+ const allows = (ctx) => customToolAllows(scope, tool, ctx);
1997
+ if (!scope.workload) {
1998
+ if (!await allows(await scope.getContext())) continue;
1999
+ }
2000
+ const inputSchema = customToolInputSchema(tool);
2001
+ const outputSchema = jsonSchemaCompatibleSchema(tool.config.outputSchema);
2002
+ server.registerTool(toolName, {
2003
+ title: tool.config.title,
2004
+ description: tool.config.description,
2005
+ inputSchema,
2006
+ outputSchema,
2007
+ annotations: tool.config.annotations,
2008
+ _meta: tool.config["_meta"]
2009
+ }, async (input, extra) => {
2010
+ try {
2011
+ return await executeCustomToolCall(scope, toolName, tool, input, extra);
2012
+ } catch (error) {
2013
+ return toToolError(error);
2014
+ }
2015
+ });
2016
+ if (scope.workload) registerWorkloadDiscoveryTool(scope.workload, customToolDescriptor(toolName, tool), workloadIdentity, tool.config.workload, allows);
2017
+ }
2018
+ }
2019
+
2020
+ //#endregion
2021
+ //#region src/server/resources.ts
2022
+ function fallbackExtra(requestId) {
2023
+ return {
2024
+ signal: new AbortController().signal,
2025
+ requestId
2026
+ };
2027
+ }
2028
+ function executeResource(scope, operation, input, extra, invoke) {
2029
+ return scope.execution.execute({
2030
+ operation,
2031
+ transport: scope.transport,
2032
+ accessMode: scope.accessMode,
2033
+ input,
2034
+ extra,
2035
+ authorize: scope.getContext,
2036
+ invoke
2037
+ });
2038
+ }
2039
+ async function collectionSchema(scope, name) {
2040
+ const ctx = await scope.getContext();
2041
+ const collection = scope.app.getCollections()[name];
2042
+ return introspectCollection(collection, {
2043
+ db: ctx.db ?? scope.app.db,
2044
+ session: ctx.session,
2045
+ locale: ctx.locale,
2046
+ accessMode: scope.accessMode,
2047
+ stage: ctx.stage
2048
+ }, scope.app);
2049
+ }
2050
+ async function globalSchema(scope, name) {
2051
+ const ctx = await scope.getContext();
2052
+ const global = scope.app.getGlobals()[name];
2053
+ return introspectGlobal(global, {
2054
+ db: ctx.db ?? scope.app.db,
2055
+ session: ctx.session,
2056
+ locale: ctx.locale,
2057
+ accessMode: scope.accessMode,
2058
+ stage: ctx.stage
2059
+ }, scope.app);
2060
+ }
2061
+ async function visibleCollections(scope, catalog) {
2062
+ const entries = [];
2063
+ for (const name of catalog.resources.collections) {
2064
+ const catalogEntry = catalog.collections.get(name);
2065
+ if (!catalogEntry) continue;
2066
+ const policy = catalogEntry.policy;
2067
+ const operation = catalogEntry.operations.find((candidate) => [
2068
+ "list",
2069
+ "count",
2070
+ "get"
2071
+ ].includes(candidate)) ?? "list";
2072
+ const ctx = await scope.getContext();
2073
+ if (!await evaluateMcpRule(operationRule(policy, operation), {
2074
+ transport: scope.transport,
2075
+ accessMode: scope.accessMode,
2076
+ ctx
2077
+ }) || !scopeGateAllows(scopesFromContext(ctx), requiredScopesForOperation(policy, "collection", name, operation, "read"))) continue;
2078
+ const schema = await collectionSchema(scope, name);
2079
+ if (scope.accessMode === "system" || schema.access.visible && schema.access.operations.read.allowed !== false) entries.push(filterEntitySchemaFields(schema, policy));
2080
+ }
2081
+ return entries;
2082
+ }
2083
+ async function visibleGlobals(scope, catalog) {
2084
+ const entries = [];
2085
+ for (const name of catalog.resources.globals) {
2086
+ const catalogEntry = catalog.globals.get(name);
2087
+ if (!catalogEntry) continue;
2088
+ const policy = catalogEntry.policy;
2089
+ const ctx = await scope.getContext();
2090
+ if (!await evaluateMcpRule(operationRule(policy, "get"), {
2091
+ transport: scope.transport,
2092
+ accessMode: scope.accessMode,
2093
+ ctx
2094
+ }) || !scopeGateAllows(scopesFromContext(ctx), requiredScopesForOperation(policy, "global", name, "get", "read"))) continue;
2095
+ const schema = await globalSchema(scope, name);
2096
+ if (scope.accessMode === "system" || schema.access.visible && schema.access.operations.read.allowed !== false) entries.push(filterEntitySchemaFields(schema, policy));
2097
+ }
2098
+ return entries;
2099
+ }
2100
+ function routeToolInputSchema(route$1) {
2101
+ if (!isJsonRoute(route$1.definition)) return void 0;
2102
+ const params = route$1.params ?? [];
2103
+ if (params.length === 0) return route$1.definition.schema;
2104
+ const shape = {};
2105
+ for (const param of params) shape[param] = z.string();
2106
+ return z.object({
2107
+ params: z.object(shape),
2108
+ input: route$1.definition.schema
2109
+ });
2110
+ }
2111
+ async function routeCatalog(scope, catalog) {
2112
+ const ctx = await scope.getContext();
2113
+ const services = extractAppServices(scope.app, {
2114
+ db: ctx.db ?? scope.app.db,
2115
+ session: ctx.session,
2116
+ locale: ctx.locale,
2117
+ accessMode: ctx.accessMode,
2118
+ principal: ctx.principal
2119
+ });
2120
+ const routes = [];
2121
+ for (const routeKey of catalog.resources.routes) {
2122
+ const catalogEntry = catalog.routes.get(routeKey);
2123
+ if (!catalogEntry) continue;
2124
+ const route$1 = catalogEntry.route;
2125
+ if (!isJsonRoute(route$1.definition)) continue;
2126
+ const policy = catalogEntry.policy;
2127
+ if (!await evaluateMcpRule(operationRule(policy, "execute"), {
2128
+ transport: scope.transport,
2129
+ accessMode: scope.accessMode,
2130
+ ctx
2131
+ }) || !scopeGateAllows(scopesFromContext(ctx), requiredScopesForOperation(policy, "route", routeKey, "execute", "invoke"))) continue;
2132
+ const params = Object.fromEntries((route$1.params ?? []).map((param) => [param, ""]));
2133
+ if (!await evaluateRouteAccess(route$1.definition.access, {
2134
+ ...services,
2135
+ ...ctx["~contextExtensions"] ?? {},
2136
+ locale: ctx.locale,
2137
+ request: scope.request,
2138
+ params
2139
+ })) continue;
2140
+ routes.push({
2141
+ key: route$1.key,
2142
+ path: route$1.path,
2143
+ methods: route$1.methods,
2144
+ mode: route$1.mode,
2145
+ params: route$1.params,
2146
+ meta: route$1.meta,
2147
+ hasInputSchema: isJsonRoute(route$1.definition),
2148
+ hasOutputSchema: isJsonRoute(route$1.definition) && !!route$1.definition.outputSchema,
2149
+ inputSchema: toJsonSchema(route$1.definition.schema),
2150
+ outputSchema: toJsonSchema(route$1.definition.outputSchema),
2151
+ toolInputSchema: toJsonSchema(routeToolInputSchema(route$1))
2152
+ });
2153
+ }
2154
+ return routes;
2155
+ }
2156
+ function resourceEntry(uri, name, description) {
2157
+ return {
2158
+ uri,
2159
+ name,
2160
+ mimeType: "application/json",
2161
+ description
2162
+ };
2163
+ }
2164
+ function registerSchemaResources(server, scope, catalog) {
2165
+ if (catalog.resources.collections.size > 0) server.registerResource("questpie-schema-collections", "questpie://schema/collections", {
2166
+ title: "QUESTPIE collections schema",
2167
+ mimeType: "application/json"
2168
+ }, async (uri, extra) => executeResource(scope, "resources.collections.read", { uri: uri.toString() }, extra, async () => jsonResource(uri.toString(), await visibleCollections(scope, catalog))));
2169
+ if (catalog.resources.collections.size > 0) server.registerResource("questpie-schema-collection", new ResourceTemplate("questpie://schema/collections/{name}", {
2170
+ list: async (extra) => executeResource(scope, "resources.collections.list", {}, extra, async () => ({ resources: (await visibleCollections(scope, catalog)).map((schema) => resourceEntry(`questpie://schema/collections/${schema.name}`, `Collection ${schema.name}`, schema.description ? String(schema.description) : void 0)) })),
2171
+ complete: { name: async (value) => executeResource(scope, "resources.collections.complete", { value }, fallbackExtra("resources.collections.complete"), async () => (await visibleCollections(scope, catalog)).map((schema) => schema.name).filter((name) => name.startsWith(value))) }
2172
+ }), {
2173
+ title: "QUESTPIE collection schema",
2174
+ mimeType: "application/json"
2175
+ }, async (uri, variables, extra) => executeResource(scope, "resources.collection.read", {
2176
+ uri: uri.toString(),
2177
+ variables
2178
+ }, extra, async () => {
2179
+ const name = String(variables.name);
2180
+ const schema = (await visibleCollections(scope, catalog)).find((item) => item.name === name);
2181
+ if (!schema) throw new Error("MCP resource is not exposed");
2182
+ return jsonResource(uri.toString(), schema);
2183
+ }));
2184
+ if (catalog.resources.globals.size > 0) server.registerResource("questpie-schema-globals", "questpie://schema/globals", {
2185
+ title: "QUESTPIE globals schema",
2186
+ mimeType: "application/json"
2187
+ }, async (uri, extra) => executeResource(scope, "resources.globals.read", { uri: uri.toString() }, extra, async () => jsonResource(uri.toString(), await visibleGlobals(scope, catalog))));
2188
+ if (catalog.resources.globals.size > 0) server.registerResource("questpie-schema-global", new ResourceTemplate("questpie://schema/globals/{name}", {
2189
+ list: async (extra) => executeResource(scope, "resources.globals.list", {}, extra, async () => ({ resources: (await visibleGlobals(scope, catalog)).map((schema) => resourceEntry(`questpie://schema/globals/${schema.name}`, `Global ${schema.name}`, schema.description ? String(schema.description) : void 0)) })),
2190
+ complete: { name: async (value) => executeResource(scope, "resources.globals.complete", { value }, fallbackExtra("resources.globals.complete"), async () => (await visibleGlobals(scope, catalog)).map((schema) => schema.name).filter((name) => name.startsWith(value))) }
2191
+ }), {
2192
+ title: "QUESTPIE global schema",
2193
+ mimeType: "application/json"
2194
+ }, async (uri, variables, extra) => executeResource(scope, "resources.global.read", {
2195
+ uri: uri.toString(),
2196
+ variables
2197
+ }, extra, async () => {
2198
+ const name = String(variables.name);
2199
+ const schema = (await visibleGlobals(scope, catalog)).find((item) => item.name === name);
2200
+ if (!schema) throw new Error("MCP resource is not exposed");
2201
+ return jsonResource(uri.toString(), schema);
2202
+ }));
2203
+ if (catalog.resources.routes.size > 0) server.registerResource("questpie-schema-routes", "questpie://schema/routes", {
2204
+ title: "QUESTPIE route schema",
2205
+ mimeType: "application/json"
2206
+ }, async (uri, extra) => executeResource(scope, "resources.routes.read", { uri: uri.toString() }, extra, async () => jsonResource(uri.toString(), await routeCatalog(scope, catalog))));
2207
+ if (catalog.resources.routes.size > 0) server.registerResource("questpie-schema-route", new ResourceTemplate("questpie://schema/routes/{key}", {
2208
+ list: async (extra) => executeResource(scope, "resources.routes.list", {}, extra, async () => ({ resources: (await routeCatalog(scope, catalog)).map((route$1) => resourceEntry(`questpie://schema/routes/${encodeURIComponent(route$1.key)}`, `Route ${route$1.key}`, route$1.meta?.description)) })),
2209
+ complete: { key: async (value) => executeResource(scope, "resources.routes.complete", { value }, fallbackExtra("resources.routes.complete"), async () => (await routeCatalog(scope, catalog)).map((route$1) => route$1.key).filter((key) => key.startsWith(value))) }
2210
+ }), {
2211
+ title: "QUESTPIE route schema",
2212
+ mimeType: "application/json"
2213
+ }, async (uri, variables, extra) => executeResource(scope, "resources.route.read", {
2214
+ uri: uri.toString(),
2215
+ variables
2216
+ }, extra, async () => {
2217
+ const key = decodeURIComponent(String(variables.key));
2218
+ const route$1 = (await routeCatalog(scope, catalog)).find((item) => item.key === key);
2219
+ if (!route$1) throw new Error("MCP resource is not exposed");
2220
+ return jsonResource(uri.toString(), route$1);
2221
+ }));
2222
+ }
2223
+
2224
+ //#endregion
2225
+ //#region src/server/route-tools.ts
2226
+ function routeInputSchema(route$1) {
2227
+ if (!isJsonRoute(route$1.definition)) return z.object({});
2228
+ if (route$1.params.length === 0) return jsonSchemaCompatibleSchema(route$1.definition.schema) ?? z.object({});
2229
+ const paramsShape = {};
2230
+ for (const param of route$1.params) paramsShape[param] = z.string();
2231
+ return z.object({
2232
+ params: z.object(paramsShape).strict(),
2233
+ input: jsonSchemaCompatibleSchema(route$1.definition.schema) ?? z.object({})
2234
+ }).strict();
2235
+ }
2236
+ async function registerRouteTools(server, scope, catalog) {
2237
+ for (const [routeKey, catalogEntry] of catalog.routes) {
2238
+ const route$1 = catalogEntry.route;
2239
+ const definition = route$1.definition;
2240
+ if (!isJsonRoute(definition)) continue;
2241
+ const mcpMeta = route$1.meta?.mcp;
2242
+ if (!mcpMeta) continue;
2243
+ const policy = catalogEntry.policy;
2244
+ const name = catalogEntry.toolName;
2245
+ const workloadRequirement = workloadRequirementForOperation(policy, "execute");
2246
+ if (scope.workload && !workloadRequirement) continue;
2247
+ const inputSchema = routeInputSchema(route$1);
2248
+ const outputSchema = jsonSchemaCompatibleSchema(definition.outputSchema);
2249
+ const annotations = mcpMeta.annotations;
2250
+ const workloadIdentity = {
2251
+ kind: "route",
2252
+ name,
2253
+ operation: "execute",
2254
+ intent: workloadRequirement?.handoff || annotations?.readOnlyHint !== true ? "effect" : "read"
2255
+ };
2256
+ const allows = async (ctx) => {
2257
+ if (!(await evaluateMcpRule(operationRule(policy, "execute"), {
2258
+ transport: scope.transport,
2259
+ accessMode: scope.accessMode,
2260
+ ctx
2261
+ }) && scopeGateAllows(scopesFromContext(ctx), requiredScopesForOperation(policy, "route", routeKey, "execute", "invoke")))) return false;
2262
+ const services = extractAppServices(scope.app, {
2263
+ db: ctx.db ?? scope.app.db,
2264
+ session: ctx.session,
2265
+ locale: ctx.locale,
2266
+ accessMode: ctx.accessMode,
2267
+ principal: ctx.principal
2268
+ });
2269
+ const params = Object.fromEntries(route$1.params.map((param) => [param, ""]));
2270
+ return evaluateRouteAccess(definition.access, {
2271
+ ...services,
2272
+ ...ctx["~contextExtensions"] ?? {},
2273
+ locale: ctx.locale,
2274
+ request: scope.request,
2275
+ params
2276
+ });
2277
+ };
2278
+ if (!scope.workload) {
2279
+ if (!await allows(await scope.getContext())) continue;
2280
+ }
2281
+ server.registerTool(name, {
2282
+ title: mcpMeta.title ?? route$1.meta?.title,
2283
+ description: mcpMeta.description ?? route$1.meta?.description,
2284
+ inputSchema,
2285
+ outputSchema,
2286
+ annotations
2287
+ }, async (input, extra) => {
2288
+ try {
2289
+ let workloadAuthorization;
2290
+ return await scope.execution.execute({
2291
+ operation: name,
2292
+ transport: scope.transport,
2293
+ accessMode: scope.accessMode,
2294
+ input,
2295
+ extra,
2296
+ authorize: async (control) => {
2297
+ workloadAuthorization = scope.workload ? await authorizeWorkload(scope.workload, "call", workloadIdentity, workloadRequirement, control) : void 0;
2298
+ if (scope.workload && !workloadAuthorization) throw new McpAccessDeniedError();
2299
+ const routeCtx = workloadAuthorization?.context ?? await scope.getContext();
2300
+ if (!await allows(routeCtx)) throw new McpAccessDeniedError();
2301
+ return routeCtx;
2302
+ },
2303
+ concurrencyKey: () => workloadAuthorization?.concurrencyKey,
2304
+ invoke: async ({ ctx, signal, requestId, correlationId }) => {
2305
+ const invocation = route$1.params.length > 0 ? z.object({
2306
+ input: z.unknown(),
2307
+ params: z.record(z.string(), z.string())
2308
+ }).parse(input) : {
2309
+ input,
2310
+ params: {}
2311
+ };
2312
+ const requestContext = toRequestContext(ctx, scope.accessMode);
2313
+ const executionRequest = new Request(scope.request?.url ?? "http://questpie.local/mcp", {
2314
+ method: scope.request?.method ?? "POST",
2315
+ headers: scope.request?.headers,
2316
+ signal
2317
+ });
2318
+ const invoke = async () => toToolResult(await executeJsonRoute(scope.app, definition, invocation.input, requestContext, executionRequest, invocation.params));
2319
+ if (scope.workload && workloadAuthorization && workloadRequirement) return executeWorkloadTool(scope.workload, workloadAuthorization, void 0, {
2320
+ signal,
2321
+ requestId,
2322
+ correlationId
2323
+ }, invoke);
2324
+ return invoke();
2325
+ }
2326
+ });
2327
+ } catch (error) {
2328
+ return toToolError(error);
2329
+ }
2330
+ });
2331
+ if (scope.workload) {
2332
+ const discoveryOutputSchema = toToolOutputJsonSchema(outputSchema);
2333
+ registerWorkloadDiscoveryTool(scope.workload, {
2334
+ name,
2335
+ title: mcpMeta.title ?? route$1.meta?.title,
2336
+ description: mcpMeta.description ?? route$1.meta?.description,
2337
+ inputSchema: toToolInputJsonSchema(inputSchema),
2338
+ ...discoveryOutputSchema ? { outputSchema: discoveryOutputSchema } : {},
2339
+ annotations
2340
+ }, workloadIdentity, workloadRequirement, allows);
2341
+ }
2342
+ }
2343
+ }
2344
+
2345
+ //#endregion
2346
+ //#region src/server/create-server.ts
2347
+ async function createServer(app, options, workload) {
2348
+ const release = workload ? createWorkloadMcpRelease(app, options.config) : options.config ? createIsolatedMcpRelease(app, options.config) : getAppMcpRelease(app);
2349
+ const { config, catalog } = release;
2350
+ const transport = options.transport ?? "http";
2351
+ const accessMode = transport === "stdio" ? resolveStdioAccessMode(options, config.stdio?.trustedMaintenance) : "user";
2352
+ const scope = createRuntimeScope(app, {
2353
+ ...options,
2354
+ transport,
2355
+ accessMode,
2356
+ config
2357
+ }, release.execution, workload);
2358
+ const server = new McpServer({
2359
+ name: config.name ?? "questpie",
2360
+ version: config.version ?? "0.0.0"
2361
+ }, { capabilities: {
2362
+ tools: {},
2363
+ ...catalog.resources.collections.size > 0 || catalog.resources.globals.size > 0 || catalog.resources.routes.size > 0 ? { resources: {} } : {}
2364
+ } });
2365
+ await registerCrudTools(server, scope, config, catalog);
2366
+ await registerRouteTools(server, scope, catalog);
2367
+ registerSchemaResources(server, scope, catalog);
2368
+ await registerCustomTools(server, scope, catalog);
2369
+ if (workload) server.server.setRequestHandler(ListToolsRequestSchema, (_request, extra) => listWorkloadTools(workload, release.execution, extra));
2370
+ else if ([...catalog.collections.values()].every((entry) => entry.operations.length === 0) && [...catalog.globals.values()].every((entry) => entry.operations.length === 0) && catalog.routes.size === 0 && catalog.customTools.size === 0) server.server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: [] }));
2371
+ return server;
2372
+ }
2373
+ function resolveStdioAccessMode(options, trustedMaintenance) {
2374
+ if (trustedMaintenance === true) {
2375
+ if (options.ctx !== void 0 || options.request !== void 0 || options.accessMode === "user") throw new Error("Trusted-maintenance stdio cannot be combined with request authority");
2376
+ return "system";
2377
+ }
2378
+ if (options.ctx?.accessMode === "user" && options.accessMode !== "system") return "user";
2379
+ throw new Error("Stdio MCP requires explicit authority: provide a user-mode ctx or configure stdio.trustedMaintenance");
2380
+ }
2381
+ async function createMcpServer(app, options = {}) {
2382
+ return createServer(app, options);
2383
+ }
2384
+ async function createWorkloadMcpServer(app, options) {
2385
+ const boundary = createWorkloadMcpBoundary(options);
2386
+ return createServer(app, {
2387
+ transport: "workload",
2388
+ accessMode: "user",
2389
+ config: options.config
2390
+ }, boundary);
2391
+ }
2392
+
2393
+ //#endregion
2394
+ export { validateMcpPrincipal as C, scopesFromContext as S, resolveMcpCatalog as _, customToolWorkloadIdentity as a, requiredScopesForOperation as b, createWorkloadMcpBoundary as c, getAppMcpRelease as d, McpAccessDeniedError as f, toMcpToolError as g, mcpPublicErrorCode as h, customToolDescriptor as i, createRuntimeScope as l, assertBoundedMcpValue as m, createWorkloadMcpServer as n, executeCustomToolCall as o, McpPublicError as p, customToolAllows as r, authorizeWorkload as s, createMcpServer as t, createWorkloadMcpRelease as u, defaultOperationScope as v, mcpTool as w, scopeGateAllows as x, normalizeRequiredScopes as y };