@classytic/arc 2.22.0 → 2.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{BaseController-C6FlRWxF.mjs → BaseController-BQ8KNvWC.mjs} +3 -2
- package/dist/{ResourceRegistry-7RnrEWNP.mjs → ResourceRegistry-y9Hnypus.mjs} +1 -0
- package/dist/{actionPermissions-ze70xXJi.mjs → actionPermissions-lszoJle1.mjs} +1 -1
- package/dist/applyPermissionResult-Tw4iXtlf.mjs +264 -0
- package/dist/audit/index.d.mts +1 -1
- package/dist/auth/index.d.mts +1 -1
- package/dist/auth/index.mjs +2 -2
- package/dist/cli/commands/describe.d.mts +1 -1
- package/dist/cli/commands/describe.mjs +1 -1
- package/dist/cli/commands/docs.mjs +2 -2
- package/dist/cli/commands/init.mjs +1 -1
- package/dist/cli/commands/introspect.mjs +1 -1
- package/dist/cli/index.mjs +1 -1
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +3 -3
- package/dist/{core-b6fgFgnd.mjs → core-DQ6rDTyH.mjs} +10 -9
- package/dist/{createActionRouter-h1cEEaEX.mjs → createActionRouter-BuOv-m4Y.mjs} +3 -3
- package/dist/{createAggregationRouter-B1C0kvTL.mjs → createAggregationRouter-CgJv_BU_.mjs} +1 -1
- package/dist/{createApp-Ci7yG5gv.mjs → createApp-CjOeWJ9_.mjs} +11 -2
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +1 -1
- package/dist/{errorHandler-BTzXSwIg.mjs → errorHandler-Bq2OeEQL.mjs} +10 -7
- package/dist/events/index.mjs +5 -2
- package/dist/factory/index.d.mts +20 -4
- package/dist/factory/index.mjs +31 -11
- package/dist/{fastify-augmentation-9_fuetB4.d.mts → fastify-augmentation-g_f9omSc.d.mts} +1 -1
- package/dist/hooks/index.d.mts +1 -1
- package/dist/idempotency/index.d.mts +7 -0
- package/dist/idempotency/index.mjs +5 -17
- package/dist/{index-DbtgCOCj.d.mts → index-BBsvQt7U.d.mts} +26 -4
- package/dist/{index-BEu4wjbL.d.mts → index-Ba10fdKp.d.mts} +11 -2
- package/dist/{index-D8MKYGDa.d.mts → index-CC5ruDGh.d.mts} +2 -2
- package/dist/{index-ogIHvOjo.d.mts → index-wMoWBLkn.d.mts} +67 -1
- package/dist/index.d.mts +4 -4
- package/dist/index.mjs +6 -5
- package/dist/{init-DleK3p9f.mjs → init-CjO4p_q1.mjs} +5 -2
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/mcp/index.d.mts +66 -3
- package/dist/integrations/mcp/index.mjs +1 -1
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/webhooks.d.mts +123 -3
- package/dist/integrations/webhooks.mjs +160 -5
- package/dist/interface-n5hpKg1d.mjs +16 -0
- package/dist/{loadResourcesFromEntry-OMCvIEE7.mjs → loadResourcesFromEntry-kvGT6EcO.mjs} +1 -1
- package/dist/{mcpPlugin-ByA2tf6D.mjs → mcpPlugin-BtzIL_kM.mjs} +308 -39
- package/dist/middleware/index.d.mts +1 -1
- package/dist/{openapi-BRpMZD7C.mjs → openapi-jSDx-VDv.mjs} +3 -3
- package/dist/permissions/index.d.mts +2 -2
- package/dist/permissions/index.mjs +3 -2
- package/dist/{permissions-BJMuKMl7.mjs → permissions-tbxFHxjZ.mjs} +77 -264
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/plugins/index.d.mts +6 -6
- package/dist/plugins/index.mjs +5 -4
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/presets/filesUpload.d.mts +1 -1
- package/dist/presets/filesUpload.mjs +1 -1
- package/dist/presets/index.d.mts +1 -1
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/search.d.mts +1 -1
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-gw0ciIUD.mjs → presets-BOe4Ugkl.mjs} +1 -1
- package/dist/realtime-I8OrfnMR.mjs +375 -0
- package/dist/{registerRoute-xHc42fwB.mjs → registerRoute-CWO6mdYe.mjs} +1 -1
- package/dist/registry/index.d.mts +2 -2
- package/dist/registry/index.mjs +1 -1
- package/dist/{schemaConverter-Ds5reSGs.mjs → schemaConverter-DJvodiHj.mjs} +4 -1
- package/dist/{schemaIR-B-6SPe7g.mjs → schemaIR-Ca7ySNPx.mjs} +1 -1
- package/dist/scope/index.mjs +1 -1
- package/dist/scope-LONQ3mfZ.mjs +37 -0
- package/dist/simpleEqualityMatcher-Cg05HPsn.mjs +68 -0
- package/dist/{sse-CfUjwjDk.mjs → sse-0u4P4-OI.mjs} +7 -30
- package/dist/sseStream-BQ7Qaynp.mjs +57 -0
- package/dist/testing/index.d.mts +2 -2
- package/dist/testing/index.mjs +2 -2
- package/dist/types/index.d.mts +1 -1
- package/dist/{types-Bc42CUyk.d.mts → types-5OuqeTDX.d.mts} +37 -2
- package/dist/utils/index.d.mts +62 -2
- package/dist/utils/index.mjs +5 -3
- package/dist/{utils-FhJ88TJE.mjs → utils-CZyeRXTh.mjs} +21 -69
- package/dist/{versioning-DLwklPlG.d.mts → versioning-D6PvCwpq.d.mts} +43 -4
- package/package.json +64 -4
- package/skills/arc/references/production.md +734 -696
- package/dist/scope-k15RXfcq.mjs +0 -64
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { p as isArcError, t as ArcError } from "./errors-DkO8Nk6-.mjs";
|
|
2
|
-
import { c as getOrgId, g as getUserId } from "./types-bHblZ9YN.mjs";
|
|
3
|
-
import { t as BaseController } from "./BaseController-
|
|
4
|
-
import {
|
|
2
|
+
import { b as isElevated, c as getOrgId, g as getUserId, x as isMember } from "./types-bHblZ9YN.mjs";
|
|
3
|
+
import { t as BaseController } from "./BaseController-BQ8KNvWC.mjs";
|
|
4
|
+
import { i as applyFieldReadPermissions, r as normalizePermissionResult, s as resolveEffectiveRoles } from "./applyPermissionResult-Tw4iXtlf.mjs";
|
|
5
5
|
import { t as executePipeline } from "./pipe-BOLmGIxa.mjs";
|
|
6
|
-
import { i as resolvePipelineSteps, t as resolveActionPermission } from "./actionPermissions-
|
|
6
|
+
import { d as buildRequestScopeProjection, i as resolvePipelineSteps, t as resolveActionPermission } from "./actionPermissions-lszoJle1.mjs";
|
|
7
7
|
import { n as validateAggregations } from "./validate-C2mGgSXg.mjs";
|
|
8
8
|
import { n as executeAggregation } from "./buildHandler-D_2ULWxZ.mjs";
|
|
9
9
|
import { t as pluralize } from "./pluralize-BhrgwZb9.mjs";
|
|
10
|
-
import {
|
|
10
|
+
import { t as createIdempotencyResult } from "./interface-n5hpKg1d.mjs";
|
|
11
|
+
import { r as shouldRejectAdditionalProperties, t as normalizeSchemaIR } from "./schemaIR-Ca7ySNPx.mjs";
|
|
11
12
|
import { createHash, randomUUID } from "node:crypto";
|
|
12
13
|
import { isHttpError, toErrorContract } from "@classytic/repo-core/errors";
|
|
13
14
|
import fp from "fastify-plugin";
|
|
@@ -160,6 +161,13 @@ function registerPrompt(server, prompt) {
|
|
|
160
161
|
//#endregion
|
|
161
162
|
//#region src/integrations/mcp/buildRequestContext.ts
|
|
162
163
|
/**
|
|
164
|
+
* @classytic/arc — MCP → IRequestContext Bridge
|
|
165
|
+
*
|
|
166
|
+
* Builds an IRequestContext from MCP tool input + session identity.
|
|
167
|
+
* Ensures MCP tool handlers go through the same BaseController pipeline
|
|
168
|
+
* as REST requests (AccessControl, BodySanitizer, QueryResolver, HookSystem).
|
|
169
|
+
*/
|
|
170
|
+
/**
|
|
163
171
|
* Build an IRequestContext from MCP tool input and session auth.
|
|
164
172
|
*
|
|
165
173
|
* | Operation | params | query | body |
|
|
@@ -177,7 +185,7 @@ function registerPrompt(server, prompt) {
|
|
|
177
185
|
* the session-derived scope is `public` (i.e. MCP called with `auth: false`).
|
|
178
186
|
* An authenticated session scope is never overwritten.
|
|
179
187
|
*/
|
|
180
|
-
function buildRequestContext(input, auth, operation, policyFilters, scopeOverride) {
|
|
188
|
+
function buildRequestContext(input, auth, operation, policyFilters, scopeOverride, extras) {
|
|
181
189
|
const sessionScope = buildScope(auth);
|
|
182
190
|
const scope = scopeOverride && sessionScope.kind === "public" ? scopeOverride : sessionScope;
|
|
183
191
|
const base = {
|
|
@@ -188,10 +196,13 @@ function buildRequestContext(input, auth, operation, policyFilters, scopeOverrid
|
|
|
188
196
|
} : null,
|
|
189
197
|
headers: {},
|
|
190
198
|
context: {},
|
|
199
|
+
scope: buildRequestScopeProjection(scope),
|
|
191
200
|
metadata: {
|
|
192
201
|
_scope: scope,
|
|
193
|
-
_policyFilters: policyFilters ?? {}
|
|
194
|
-
|
|
202
|
+
_policyFilters: policyFilters ?? {},
|
|
203
|
+
...extras?.arc ? { arc: extras.arc } : {}
|
|
204
|
+
},
|
|
205
|
+
...extras?.server ? { server: extras.server } : {}
|
|
195
206
|
};
|
|
196
207
|
switch (operation) {
|
|
197
208
|
case "list": return {
|
|
@@ -375,6 +386,42 @@ async function evaluatePermission(check, session, resource, action, input) {
|
|
|
375
386
|
}));
|
|
376
387
|
}
|
|
377
388
|
/**
|
|
389
|
+
* Apply field-level READ permissions to an MCP tool payload — the same
|
|
390
|
+
* masking `sendControllerResponse` applies on the HTTP wire and the
|
|
391
|
+
* realtime plugin applies per frame. Without this, a resource declaring
|
|
392
|
+
* `fields: { salary: fields.visibleTo(['admin']) }` masked salary over
|
|
393
|
+
* REST and SSE but leaked it verbatim through every MCP tool.
|
|
394
|
+
*
|
|
395
|
+
* Mirrors the HTTP adapter's semantics exactly:
|
|
396
|
+
* - Elevated scope (platform admin) bypasses masking — consistent with
|
|
397
|
+
* `requireOrgRole()` and `BodySanitizer` bypass logic.
|
|
398
|
+
* - Effective roles = session roles ∪ org roles (member scope only),
|
|
399
|
+
* via the shared `resolveEffectiveRoles`.
|
|
400
|
+
* - `scopeOverride` (a `PermissionResult.scope`) follows the same
|
|
401
|
+
* non-downgrade rule as `buildRequestContext`: honored only when the
|
|
402
|
+
* session-derived scope is `public`.
|
|
403
|
+
*
|
|
404
|
+
* Handles the three payload shapes MCP tools emit: a single record, an
|
|
405
|
+
* array of records, and a paginated result object carrying an inner
|
|
406
|
+
* `data: T[]`. Scalars and `null` pass through untouched.
|
|
407
|
+
*/
|
|
408
|
+
function applyMcpReadMasking(data, options) {
|
|
409
|
+
const { fields, session, scopeOverride } = options;
|
|
410
|
+
if (!fields || data === null || typeof data !== "object") return data;
|
|
411
|
+
const sessionScope = buildScope(session);
|
|
412
|
+
const scope = scopeOverride && sessionScope.kind === "public" ? scopeOverride : sessionScope;
|
|
413
|
+
if (isElevated(scope)) return data;
|
|
414
|
+
const roles = resolveEffectiveRoles(session?.roles ?? [], isMember(scope) ? scope.orgRoles : []);
|
|
415
|
+
const maskItem = (item) => item !== null && typeof item === "object" && !Array.isArray(item) ? applyFieldReadPermissions(item, fields, roles) : item;
|
|
416
|
+
if (Array.isArray(data)) return data.map(maskItem);
|
|
417
|
+
const inner = data.data;
|
|
418
|
+
if (Array.isArray(inner)) return {
|
|
419
|
+
...data,
|
|
420
|
+
data: inner.map(maskItem)
|
|
421
|
+
};
|
|
422
|
+
return maskItem(data);
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
378
425
|
* Convert a controller response envelope into an MCP `CallToolResult`.
|
|
379
426
|
* Carries `meta` into the serialized payload so consumers see pagination
|
|
380
427
|
* totals, stripped-field arrays, etc.
|
|
@@ -476,6 +523,78 @@ function createMcpController(resource) {
|
|
|
476
523
|
//#endregion
|
|
477
524
|
//#region src/integrations/mcp/invokeController.ts
|
|
478
525
|
/**
|
|
526
|
+
* @classytic/arc — Non-HTTP controller invocation for MCP / jobs / workflows.
|
|
527
|
+
*
|
|
528
|
+
* Internal callers (MCP tools, scheduled jobs, workflow steps) need to invoke
|
|
529
|
+
* controller methods without going through Fastify's preHandler chain.
|
|
530
|
+
* Before this module landed, every call site reinvented the same shim:
|
|
531
|
+
* fabricate `params` / `body` / `headers`, project the session into a
|
|
532
|
+
* `RequestScope`, run the permission check by hand, translate thrown errors
|
|
533
|
+
* to the MCP `CallToolResult` shape. The drift across those copies is what
|
|
534
|
+
* the mentora review flagged as the "buildCtx synthetic-context hack."
|
|
535
|
+
*
|
|
536
|
+
* Two public surfaces, layered so callers reach for the smallest piece that
|
|
537
|
+
* does the job:
|
|
538
|
+
*
|
|
539
|
+
* - {@link invokeController} full pipeline: permission → context →
|
|
540
|
+
* dispatch → canonical envelope/error
|
|
541
|
+
* conversion. Returns `CallToolResult`.
|
|
542
|
+
* - {@link mcpHandlerAdapter} thin try/catch wrapper that turns any
|
|
543
|
+
* async function into a `ToolDefinition`
|
|
544
|
+
* handler with canonical error shape. Use
|
|
545
|
+
* when you want to compose multiple
|
|
546
|
+
* controller calls or weave in custom logic
|
|
547
|
+
* between them.
|
|
548
|
+
*
|
|
549
|
+
* Both honor the same {@link buildRequestContext} primitive — re-exported
|
|
550
|
+
* from this module so authors who want the synthetic context as data (e.g.
|
|
551
|
+
* to pass into `executePipeline`) can grab it without depending on an
|
|
552
|
+
* internal subpath.
|
|
553
|
+
*/
|
|
554
|
+
/**
|
|
555
|
+
* Assemble the {@link McpContextExtras} for a controller invocation from
|
|
556
|
+
* resource-scoped metadata + app-level wiring. Mirrors what
|
|
557
|
+
* `buildArcDecorator` (metadata.arc) and `createRequestContext` (server
|
|
558
|
+
* accessor) assemble on the HTTP path.
|
|
559
|
+
*/
|
|
560
|
+
function buildContextExtras(options) {
|
|
561
|
+
const { wiring, resourceName, permissions, fields, schemaOptions, idField } = options;
|
|
562
|
+
if (!wiring) return void 0;
|
|
563
|
+
return {
|
|
564
|
+
arc: {
|
|
565
|
+
resourceName,
|
|
566
|
+
schemaOptions,
|
|
567
|
+
permissions,
|
|
568
|
+
hooks: wiring.hooks,
|
|
569
|
+
events: wiring.events,
|
|
570
|
+
fields,
|
|
571
|
+
idField
|
|
572
|
+
},
|
|
573
|
+
server: {
|
|
574
|
+
events: wiring.events,
|
|
575
|
+
audit: wiring.audit,
|
|
576
|
+
log: wiring.log
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Stable fingerprint of a tool invocation — sorted-key JSON, hashed.
|
|
582
|
+
* Scopes an idempotency key to (resource, action, input, caller) so a
|
|
583
|
+
* reused key with DIFFERENT input is a fresh execution, matching the HTTP
|
|
584
|
+
* plugin's method+url+bodyHash+user fingerprint semantics.
|
|
585
|
+
*/
|
|
586
|
+
function invocationFingerprint(resourceName, actionName, input, session) {
|
|
587
|
+
const sortKeys = (value) => {
|
|
588
|
+
if (Array.isArray(value)) return value.map(sortKeys);
|
|
589
|
+
if (value !== null && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map((k) => [k, sortKeys(value[k])]));
|
|
590
|
+
return value;
|
|
591
|
+
};
|
|
592
|
+
return `mcp:${resourceName}:${actionName}:${createHash("sha256").update(JSON.stringify(sortKeys(input))).digest("hex").substring(0, 16)}:u=${session?.userId ?? session?.clientId ?? "anon"}`;
|
|
593
|
+
}
|
|
594
|
+
/** TTLs matching the HTTP idempotency plugin's defaults. */
|
|
595
|
+
const MCP_IDEMPOTENCY_TTL_MS = 1440 * 60 * 1e3;
|
|
596
|
+
const MCP_IDEMPOTENCY_LOCK_MS = 1e4;
|
|
597
|
+
/**
|
|
479
598
|
* Invoke a controller method through the MCP synthetic-context path.
|
|
480
599
|
*
|
|
481
600
|
* Runs the same chain CRUD MCP tools run:
|
|
@@ -508,7 +627,7 @@ function createMcpController(resource) {
|
|
|
508
627
|
* checks compose.
|
|
509
628
|
*/
|
|
510
629
|
async function invokeController(controller, op, input, options) {
|
|
511
|
-
const { session, resourceName, actionName = op, permissions, methodName = op } = options;
|
|
630
|
+
const { session, resourceName, actionName = op, permissions, methodName = op, fields, wiring, idempotencyKey } = options;
|
|
512
631
|
const ctrl = controller;
|
|
513
632
|
try {
|
|
514
633
|
const method = ctrl[methodName];
|
|
@@ -524,7 +643,39 @@ async function invokeController(controller, op, input, options) {
|
|
|
524
643
|
reason: permResult.reason,
|
|
525
644
|
session
|
|
526
645
|
});
|
|
527
|
-
|
|
646
|
+
const dispatch = async () => {
|
|
647
|
+
const reqCtx = buildRequestContext(input, session, op, permResult?.filters, permResult?.scope, buildContextExtras(options));
|
|
648
|
+
const response = await method(reqCtx);
|
|
649
|
+
const data = applyMcpReadMasking(response.data, {
|
|
650
|
+
fields,
|
|
651
|
+
session,
|
|
652
|
+
scopeOverride: permResult?.scope
|
|
653
|
+
});
|
|
654
|
+
return toCallToolResult(data === response.data ? response : {
|
|
655
|
+
...response,
|
|
656
|
+
data
|
|
657
|
+
});
|
|
658
|
+
};
|
|
659
|
+
const store = wiring?.idempotencyStore;
|
|
660
|
+
if (idempotencyKey && store) {
|
|
661
|
+
const fullKey = `${idempotencyKey}:${invocationFingerprint(resourceName, actionName, input, session)}`;
|
|
662
|
+
const cached = await store.get(fullKey);
|
|
663
|
+
if (cached) return cached.body;
|
|
664
|
+
const lockId = randomUUID();
|
|
665
|
+
if (!await store.tryLock(fullKey, lockId, MCP_IDEMPOTENCY_LOCK_MS)) return toCallToolError({
|
|
666
|
+
code: "arc.idempotency_conflict",
|
|
667
|
+
message: "A call with this idempotency key is already in progress",
|
|
668
|
+
status: 409
|
|
669
|
+
});
|
|
670
|
+
try {
|
|
671
|
+
const result = await dispatch();
|
|
672
|
+
if (!result.isError) await store.set(fullKey, createIdempotencyResult(200, result, {}, MCP_IDEMPOTENCY_TTL_MS));
|
|
673
|
+
return result;
|
|
674
|
+
} finally {
|
|
675
|
+
await store.unlock(fullKey, lockId);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return await dispatch();
|
|
528
679
|
} catch (err) {
|
|
529
680
|
return toCallToolError(err instanceof Error ? err : new Error(String(err)));
|
|
530
681
|
}
|
|
@@ -605,13 +756,33 @@ const CRUD_ANNOTATIONS = {
|
|
|
605
756
|
* build + envelope/error conversion all live in one place — this factory
|
|
606
757
|
* only resolves the op-specific permission and threads it through.
|
|
607
758
|
*/
|
|
608
|
-
|
|
759
|
+
/** Ops where an idempotency key is meaningful (mutations). */
|
|
760
|
+
const MUTATING_CRUD_OPS = /* @__PURE__ */ new Set([
|
|
761
|
+
"create",
|
|
762
|
+
"update",
|
|
763
|
+
"delete"
|
|
764
|
+
]);
|
|
765
|
+
function createCrudHandler(op, controller, resourceName, permissions, fields, extras) {
|
|
609
766
|
const permission = permissions?.[op];
|
|
610
|
-
return (input, ctx) =>
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
767
|
+
return (input, ctx) => {
|
|
768
|
+
let idempotencyKey;
|
|
769
|
+
let payload = input;
|
|
770
|
+
if ("_idempotencyKey" in input) {
|
|
771
|
+
const { _idempotencyKey, ...rest } = input;
|
|
772
|
+
payload = rest;
|
|
773
|
+
if (MUTATING_CRUD_OPS.has(op) && typeof _idempotencyKey === "string" && _idempotencyKey) idempotencyKey = _idempotencyKey;
|
|
774
|
+
}
|
|
775
|
+
return invokeController(controller, op, payload, {
|
|
776
|
+
session: ctx.session,
|
|
777
|
+
resourceName,
|
|
778
|
+
permissions: permission,
|
|
779
|
+
fields,
|
|
780
|
+
wiring: extras?.wiring,
|
|
781
|
+
schemaOptions: extras?.schemaOptions,
|
|
782
|
+
idField: extras?.idField,
|
|
783
|
+
idempotencyKey
|
|
784
|
+
});
|
|
785
|
+
};
|
|
615
786
|
}
|
|
616
787
|
/**
|
|
617
788
|
* Default description for a CRUD tool. Enriches list descriptions with the
|
|
@@ -1008,7 +1179,24 @@ function convertActionSchemaToZod(raw) {
|
|
|
1008
1179
|
* in the 2.10.8 review: REST and MCP action tools share identical
|
|
1009
1180
|
* context-building machinery.
|
|
1010
1181
|
*/
|
|
1011
|
-
function createActionToolHandler(actionName, handler, permissions, resourceName, _resourcePermissions, rawSchema, idLess = false) {
|
|
1182
|
+
function createActionToolHandler(actionName, handler, permissions, resourceName, _resourcePermissions, rawSchema, idLess = false, fields, extras) {
|
|
1183
|
+
const wiring = extras?.wiring;
|
|
1184
|
+
const contextExtras = wiring ? {
|
|
1185
|
+
arc: {
|
|
1186
|
+
resourceName,
|
|
1187
|
+
schemaOptions: extras?.schemaOptions,
|
|
1188
|
+
permissions,
|
|
1189
|
+
hooks: wiring.hooks,
|
|
1190
|
+
events: wiring.events,
|
|
1191
|
+
fields,
|
|
1192
|
+
idField: extras?.idField
|
|
1193
|
+
},
|
|
1194
|
+
server: {
|
|
1195
|
+
events: wiring.events,
|
|
1196
|
+
audit: wiring.audit,
|
|
1197
|
+
log: wiring.log
|
|
1198
|
+
}
|
|
1199
|
+
} : void 0;
|
|
1012
1200
|
const ir = rawSchema ? normalizeSchemaIR(rawSchema) : void 0;
|
|
1013
1201
|
const allowedKeys = (ir ? shouldRejectAdditionalProperties(ir) : false) && ir ? /* @__PURE__ */ new Set([...idLess ? [] : ["id"], ...Object.keys(ir.properties)]) : void 0;
|
|
1014
1202
|
return async (input, ctx) => {
|
|
@@ -1036,11 +1224,15 @@ function createActionToolHandler(actionName, handler, permissions, resourceName,
|
|
|
1036
1224
|
const reqCtx = buildRequestContext({
|
|
1037
1225
|
...input,
|
|
1038
1226
|
action: actionName
|
|
1039
|
-
}, session, "action", permResult?.filters, permResult?.scope);
|
|
1227
|
+
}, session, "action", permResult?.filters, permResult?.scope, contextExtras);
|
|
1040
1228
|
const id = idLess ? "" : typeof input.id === "string" ? input.id : "";
|
|
1041
1229
|
const { id: _discardId, ...data } = input;
|
|
1042
1230
|
try {
|
|
1043
|
-
return toCallToolSuccess(await handler(id, data, reqCtx)
|
|
1231
|
+
return toCallToolSuccess(applyMcpReadMasking(await handler(id, data, reqCtx), {
|
|
1232
|
+
fields,
|
|
1233
|
+
session,
|
|
1234
|
+
scopeOverride: permResult?.scope
|
|
1235
|
+
}));
|
|
1044
1236
|
} catch (err) {
|
|
1045
1237
|
return toCallToolError(err instanceof Error ? err : new Error(String(err)));
|
|
1046
1238
|
}
|
|
@@ -1055,7 +1247,7 @@ function createActionToolHandler(actionName, handler, permissions, resourceName,
|
|
|
1055
1247
|
* every aggregation opts out via `mcp: false`.
|
|
1056
1248
|
*/
|
|
1057
1249
|
function buildAggregationTools(args) {
|
|
1058
|
-
const { resourceName, displayName, aggregations, schemaOptions, repo, buildOptionsFromSession, prefix } = args;
|
|
1250
|
+
const { resourceName, displayName, aggregations, schemaOptions, repo, buildOptionsFromSession, prefix, fields } = args;
|
|
1059
1251
|
if (!aggregations || Object.keys(aggregations).length === 0) return [];
|
|
1060
1252
|
const normalized = validateAggregations(resourceName, aggregations, schemaOptions);
|
|
1061
1253
|
const tools = [];
|
|
@@ -1079,7 +1271,8 @@ function buildAggregationTools(args) {
|
|
|
1079
1271
|
permissions: config.permissions,
|
|
1080
1272
|
resourceName,
|
|
1081
1273
|
repo,
|
|
1082
|
-
buildOptionsFromSession
|
|
1274
|
+
buildOptionsFromSession,
|
|
1275
|
+
fields
|
|
1083
1276
|
})
|
|
1084
1277
|
});
|
|
1085
1278
|
}
|
|
@@ -1121,7 +1314,7 @@ function buildAggregationInputSchema(norm) {
|
|
|
1121
1314
|
return shape;
|
|
1122
1315
|
}
|
|
1123
1316
|
function createAggregationToolHandler(args) {
|
|
1124
|
-
const { normalized, permissions, resourceName, repo, buildOptionsFromSession } = args;
|
|
1317
|
+
const { normalized, permissions, resourceName, repo, buildOptionsFromSession, fields } = args;
|
|
1125
1318
|
return async (input, ctx) => {
|
|
1126
1319
|
const permResult = await evaluatePermission(permissions, ctx.session, resourceName, `aggregation:${normalized.name}`, input);
|
|
1127
1320
|
if (permResult && !permResult.granted) return permissionDeniedResult({
|
|
@@ -1140,7 +1333,21 @@ function createAggregationToolHandler(args) {
|
|
|
1140
1333
|
query: filterInput,
|
|
1141
1334
|
tenantOptions
|
|
1142
1335
|
});
|
|
1143
|
-
if (result.status === 200)
|
|
1336
|
+
if (result.status === 200) {
|
|
1337
|
+
const body = result.body;
|
|
1338
|
+
if (fields && Array.isArray(body?.rows)) {
|
|
1339
|
+
const rows = applyMcpReadMasking(body.rows, {
|
|
1340
|
+
fields,
|
|
1341
|
+
session: ctx.session,
|
|
1342
|
+
scopeOverride: permResult?.scope
|
|
1343
|
+
});
|
|
1344
|
+
return toCallToolSuccess({
|
|
1345
|
+
...body,
|
|
1346
|
+
rows
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
return toCallToolSuccess(result.body);
|
|
1350
|
+
}
|
|
1144
1351
|
return toCallToolError(result.body);
|
|
1145
1352
|
};
|
|
1146
1353
|
}
|
|
@@ -1480,8 +1687,24 @@ function operationKindForRoute(method, hasId) {
|
|
|
1480
1687
|
function createCustomRouteHandler(route, controller, hasId, options) {
|
|
1481
1688
|
const ctrl = controller;
|
|
1482
1689
|
const handlerName = typeof route.handler === "string" ? route.handler : route.operation ?? slugifyRoute(route.method, route.path);
|
|
1483
|
-
const { resourceName, operationName, permissions, pipeline } = options;
|
|
1690
|
+
const { resourceName, operationName, permissions, pipeline, fields, wiring } = options;
|
|
1484
1691
|
const pipelineSteps = resolvePipelineSteps(pipeline, operationName);
|
|
1692
|
+
const contextExtras = wiring ? {
|
|
1693
|
+
arc: {
|
|
1694
|
+
resourceName,
|
|
1695
|
+
schemaOptions: options.schemaOptions,
|
|
1696
|
+
permissions,
|
|
1697
|
+
hooks: wiring.hooks,
|
|
1698
|
+
events: wiring.events,
|
|
1699
|
+
fields,
|
|
1700
|
+
idField: options.idField
|
|
1701
|
+
},
|
|
1702
|
+
server: {
|
|
1703
|
+
events: wiring.events,
|
|
1704
|
+
audit: wiring.audit,
|
|
1705
|
+
log: wiring.log
|
|
1706
|
+
}
|
|
1707
|
+
} : void 0;
|
|
1485
1708
|
return async (input, _ctx) => {
|
|
1486
1709
|
const session = _ctx.session;
|
|
1487
1710
|
const permResult = await evaluatePermission(permissions, session, resourceName, operationName, input);
|
|
@@ -1492,7 +1715,18 @@ function createCustomRouteHandler(route, controller, hasId, options) {
|
|
|
1492
1715
|
session
|
|
1493
1716
|
});
|
|
1494
1717
|
try {
|
|
1495
|
-
const reqCtx = buildRequestContext(input, session, operationKindForRoute(route.method, hasId), permResult?.filters, permResult?.scope);
|
|
1718
|
+
const reqCtx = buildRequestContext(input, session, operationKindForRoute(route.method, hasId), permResult?.filters, permResult?.scope, contextExtras);
|
|
1719
|
+
const emit = (envelope) => {
|
|
1720
|
+
const data = applyMcpReadMasking(envelope.data, {
|
|
1721
|
+
fields,
|
|
1722
|
+
session,
|
|
1723
|
+
scopeOverride: permResult?.scope
|
|
1724
|
+
});
|
|
1725
|
+
return toCallToolResult(data === envelope.data ? envelope : {
|
|
1726
|
+
...envelope,
|
|
1727
|
+
data
|
|
1728
|
+
});
|
|
1729
|
+
};
|
|
1496
1730
|
if (typeof route.handler === "function") {
|
|
1497
1731
|
const fn = route.handler;
|
|
1498
1732
|
if (pipelineSteps.length > 0) {
|
|
@@ -1501,13 +1735,13 @@ function createCustomRouteHandler(route, controller, hasId, options) {
|
|
|
1501
1735
|
resource: resourceName,
|
|
1502
1736
|
operation: operationName
|
|
1503
1737
|
};
|
|
1504
|
-
return
|
|
1738
|
+
return emit(await executePipeline(pipelineSteps, pipeCtx, async (ctx) => {
|
|
1505
1739
|
const raw = await fn(ctx);
|
|
1506
1740
|
return raw !== null && typeof raw === "object" && "data" in raw ? raw : { data: raw };
|
|
1507
1741
|
}, operationName));
|
|
1508
1742
|
}
|
|
1509
1743
|
const out = await fn(reqCtx);
|
|
1510
|
-
return
|
|
1744
|
+
return emit(out !== null && typeof out === "object" && "data" in out ? out : { data: out });
|
|
1511
1745
|
}
|
|
1512
1746
|
if (!ctrl) return {
|
|
1513
1747
|
content: [{
|
|
@@ -1524,7 +1758,7 @@ function createCustomRouteHandler(route, controller, hasId, options) {
|
|
|
1524
1758
|
}],
|
|
1525
1759
|
isError: true
|
|
1526
1760
|
};
|
|
1527
|
-
return
|
|
1761
|
+
return emit(await method(reqCtx));
|
|
1528
1762
|
} catch (err) {
|
|
1529
1763
|
return toCallToolError(err instanceof Error ? err : new Error(String(err)));
|
|
1530
1764
|
}
|
|
@@ -1598,6 +1832,7 @@ function resourceToTools(resource, config = {}) {
|
|
|
1598
1832
|
const hasSoftDelete = resource._appliedPresets?.includes("softDelete") ?? false;
|
|
1599
1833
|
const tools = [];
|
|
1600
1834
|
const prefix = config.toolNamePrefix;
|
|
1835
|
+
const wiring = config.wiring;
|
|
1601
1836
|
if (controller) {
|
|
1602
1837
|
let ops = ALL_CRUD_OPS.filter((op) => !resource.disabledRoutes?.includes(op));
|
|
1603
1838
|
if (config.operations) ops = ops.filter((op) => config.operations?.includes(op));
|
|
@@ -1608,6 +1843,15 @@ function resourceToTools(resource, config = {}) {
|
|
|
1608
1843
|
allowedOperators,
|
|
1609
1844
|
sortableFields
|
|
1610
1845
|
});
|
|
1846
|
+
const inputSchema = buildInputSchema(op, fieldRules, {
|
|
1847
|
+
hiddenFields,
|
|
1848
|
+
readonlyFields,
|
|
1849
|
+
extraHideFields: config.hideFields,
|
|
1850
|
+
filterableFields,
|
|
1851
|
+
allowedOperators,
|
|
1852
|
+
adapterBodies
|
|
1853
|
+
});
|
|
1854
|
+
if (wiring?.idempotencyStore && op !== "list" && op !== "get") inputSchema._idempotencyKey = z.string().optional().describe("Optional idempotency key — retrying with the same key and input replays the first successful result instead of re-executing.");
|
|
1611
1855
|
tools.push({
|
|
1612
1856
|
name,
|
|
1613
1857
|
description: resolveCrudDescription(config.descriptions?.[op], {
|
|
@@ -1620,15 +1864,12 @@ function resourceToTools(resource, config = {}) {
|
|
|
1620
1864
|
sortableFields
|
|
1621
1865
|
}),
|
|
1622
1866
|
annotations: CRUD_ANNOTATIONS[op],
|
|
1623
|
-
inputSchema
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
allowedOperators,
|
|
1629
|
-
adapterBodies
|
|
1867
|
+
inputSchema,
|
|
1868
|
+
handler: createCrudHandler(op, controller, resource.name, resource.permissions, resource.fields, {
|
|
1869
|
+
wiring,
|
|
1870
|
+
schemaOptions: resource.schemaOptions,
|
|
1871
|
+
idField: resource.idField
|
|
1630
1872
|
}),
|
|
1631
|
-
handler: createCrudHandler(op, controller, resource.name, resource.permissions),
|
|
1632
1873
|
source: `crud:${resource.name}:${op}`
|
|
1633
1874
|
});
|
|
1634
1875
|
}
|
|
@@ -1680,7 +1921,11 @@ function resourceToTools(resource, config = {}) {
|
|
|
1680
1921
|
resourceName: resource.name,
|
|
1681
1922
|
operationName: opName,
|
|
1682
1923
|
permissions: route.permissions,
|
|
1683
|
-
pipeline: resource.pipe
|
|
1924
|
+
pipeline: resource.pipe,
|
|
1925
|
+
fields: resource.fields,
|
|
1926
|
+
wiring,
|
|
1927
|
+
schemaOptions: resource.schemaOptions,
|
|
1928
|
+
idField: resource.idField
|
|
1684
1929
|
})
|
|
1685
1930
|
});
|
|
1686
1931
|
}
|
|
@@ -1711,7 +1956,11 @@ function resourceToTools(resource, config = {}) {
|
|
|
1711
1956
|
annotations,
|
|
1712
1957
|
inputSchema: inputShape,
|
|
1713
1958
|
source: `action:${resource.name}:${actionName}`,
|
|
1714
|
-
handler: createActionToolHandler(actionName, handler, actionPerms, resource.name, resource.permissions, rawSchema, idLess
|
|
1959
|
+
handler: createActionToolHandler(actionName, handler, actionPerms, resource.name, resource.permissions, rawSchema, idLess, resource.fields, {
|
|
1960
|
+
wiring,
|
|
1961
|
+
schemaOptions: resource.schemaOptions,
|
|
1962
|
+
idField: resource.idField
|
|
1963
|
+
})
|
|
1715
1964
|
});
|
|
1716
1965
|
}
|
|
1717
1966
|
if (resource.aggregations && Object.keys(resource.aggregations).length > 0) {
|
|
@@ -1738,7 +1987,8 @@ function resourceToTools(resource, config = {}) {
|
|
|
1738
1987
|
schemaOptions: resource.schemaOptions,
|
|
1739
1988
|
repo: repoForAgg,
|
|
1740
1989
|
buildOptionsFromSession,
|
|
1741
|
-
prefix
|
|
1990
|
+
prefix,
|
|
1991
|
+
fields: resource.fields
|
|
1742
1992
|
}));
|
|
1743
1993
|
}
|
|
1744
1994
|
return tools;
|
|
@@ -1948,10 +2198,29 @@ const mcpPluginImpl = async (fastify, options) => {
|
|
|
1948
2198
|
include: options.include,
|
|
1949
2199
|
exclude: options.exclude
|
|
1950
2200
|
});
|
|
2201
|
+
const fastifyAny = fastify;
|
|
2202
|
+
const wiring = {
|
|
2203
|
+
get hooks() {
|
|
2204
|
+
return fastifyAny.arc?.hooks;
|
|
2205
|
+
},
|
|
2206
|
+
get events() {
|
|
2207
|
+
return fastifyAny.events;
|
|
2208
|
+
},
|
|
2209
|
+
get audit() {
|
|
2210
|
+
return fastifyAny.audit;
|
|
2211
|
+
},
|
|
2212
|
+
get log() {
|
|
2213
|
+
return fastify.log;
|
|
2214
|
+
},
|
|
2215
|
+
get idempotencyStore() {
|
|
2216
|
+
return fastifyAny.idempotency?.store;
|
|
2217
|
+
}
|
|
2218
|
+
};
|
|
1951
2219
|
const overrides = options.overrides ?? {};
|
|
1952
2220
|
const allTools = enabledResources.flatMap((r) => {
|
|
1953
2221
|
const resOverrides = overrides[r.name] ?? {};
|
|
1954
2222
|
return resourceToTools(r, {
|
|
2223
|
+
wiring,
|
|
1955
2224
|
...resOverrides,
|
|
1956
2225
|
toolNamePrefix: resOverrides.toolNamePrefix ?? options.toolNamePrefix
|
|
1957
2226
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { I as MiddlewareHandler, L as RequestWithExtras, Q as MiddlewareConfig } from "../index-
|
|
1
|
+
import { I as MiddlewareHandler, L as RequestWithExtras, Q as MiddlewareConfig } from "../index-BBsvQt7U.mjs";
|
|
2
2
|
import { RouteHandlerMethod } from "fastify";
|
|
3
3
|
//#region src/middleware/middleware.d.ts
|
|
4
4
|
interface NamedMiddleware {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { t as getUserRoles } from "./types-XTApa8b6.mjs";
|
|
2
|
-
import { n as convertRouteSchema } from "./schemaConverter-
|
|
3
|
-
import { t as resolveActionPermission } from "./actionPermissions-
|
|
2
|
+
import { n as convertRouteSchema } from "./schemaConverter-DJvodiHj.mjs";
|
|
3
|
+
import { t as resolveActionPermission } from "./actionPermissions-lszoJle1.mjs";
|
|
4
4
|
import { t as pluralize } from "./pluralize-BhrgwZb9.mjs";
|
|
5
|
-
import { t as buildActionBodySchema } from "./createActionRouter-
|
|
5
|
+
import { t as buildActionBodySchema } from "./createActionRouter-BuOv-m4Y.mjs";
|
|
6
6
|
import fp from "fastify-plugin";
|
|
7
7
|
//#region src/docs/openapi/canonical-schemas.ts
|
|
8
8
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { a as getUserRoles, i as UserBase, n as PermissionContext, o as normalizeRoles, r as PermissionResult, t as PermissionCheck } from "../types-Csxga7d3.mjs";
|
|
2
2
|
import { a as applyFieldWritePermissions, i as applyFieldReadPermissions, n as FieldPermissionMap, o as fields, r as FieldPermissionType, s as resolveEffectiveRoles, t as FieldPermission } from "../fields-D3lhgzhD.mjs";
|
|
3
|
-
import { A as
|
|
4
|
-
export { type ConnectEventsOptions, type DynamicPermissionMatrix, type DynamicPermissionMatrixConfig, type FieldPermission, type FieldPermissionMap, type FieldPermissionType, type PermissionCheck, type PermissionContext, type PermissionEventBus, type PermissionResult, type RequireAgentScopeOptions, type RequireMandateOptions, type RoleHierarchy, type UserBase, adminOnly, allOf, allowPublic, anyOf, applyFieldReadPermissions, applyFieldWritePermissions, applyPermissionResult, authenticated, createDynamicPermissionMatrix, createOrgPermissions, createRoleHierarchy, denyAll, fields, fullPublic, getUserRoles, normalizePermissionResult, normalizeRoles, not, ownerWithAdminBypass, permissionMatrix, presets_d_exports as permissions, publicRead, publicReadAdminWrite, readOnly, requireAgentScope, requireAuth, requireDPoP, requireMandate, requireOrgInScope, requireOrgMembership, requireOrgRole, requireOwnership, requireRoles, requireScopeContext, requireServiceScope, requireTeamMembership, resolveEffectiveRoles, roles, when };
|
|
3
|
+
import { A as allOf, B as applyPermissionResult, C as requireGrant, D as PermissionEventBus, E as DynamicPermissionMatrixConfig, F as requireAuth, G as requireDPoP, H as RequireAgentScopeOptions, I as requireOwnership, K as requireMandate, L as requireRoles, M as anyOf, N as denyAll, O as createDynamicPermissionMatrix, P as not, R as roles, S as modeSatisfies, T as DynamicPermissionMatrix, U as RequireMandateOptions, V as normalizePermissionResult, W as requireAgentScope, _ as createRoleHierarchy, a as permissionMatrix, b as GrantResolution, c as publicReadAdminWrite, d as requireOrgMembership, f as requireOrgRole, g as RoleHierarchy, h as requireTeamMembership, i as ownerWithAdminBypass, j as allowPublic, k as createOrgPermissions, l as readOnly, m as requireServiceScope, n as authenticated, o as presets_d_exports, p as requireScopeContext, r as fullPublic, s as publicRead, t as adminOnly, u as requireOrgInScope, v as GRANT_MODES, w as ConnectEventsOptions, x as RequireGrantOptions, y as GrantMode, z as when } from "../index-wMoWBLkn.mjs";
|
|
4
|
+
export { type ConnectEventsOptions, type DynamicPermissionMatrix, type DynamicPermissionMatrixConfig, type FieldPermission, type FieldPermissionMap, type FieldPermissionType, GRANT_MODES, type GrantMode, type GrantResolution, type PermissionCheck, type PermissionContext, type PermissionEventBus, type PermissionResult, type RequireAgentScopeOptions, type RequireGrantOptions, type RequireMandateOptions, type RoleHierarchy, type UserBase, adminOnly, allOf, allowPublic, anyOf, applyFieldReadPermissions, applyFieldWritePermissions, applyPermissionResult, authenticated, createDynamicPermissionMatrix, createOrgPermissions, createRoleHierarchy, denyAll, fields, fullPublic, getUserRoles, modeSatisfies, normalizePermissionResult, normalizeRoles, not, ownerWithAdminBypass, permissionMatrix, presets_d_exports as permissions, publicRead, publicReadAdminWrite, readOnly, requireAgentScope, requireAuth, requireDPoP, requireGrant, requireMandate, requireOrgInScope, requireOrgMembership, requireOrgRole, requireOwnership, requireRoles, requireScopeContext, requireServiceScope, requireTeamMembership, resolveEffectiveRoles, roles, when };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as applyFieldWritePermissions, i as applyFieldReadPermissions, o as fields, r as normalizePermissionResult, s as resolveEffectiveRoles, t as applyPermissionResult } from "../applyPermissionResult-Tw4iXtlf.mjs";
|
|
2
2
|
import { n as normalizeRoles, t as getUserRoles } from "../types-XTApa8b6.mjs";
|
|
3
|
-
|
|
3
|
+
import { A as roles, C as allowPublic, D as requireAuth, E as not, M as requireAgentScope, N as requireDPoP, O as requireOwnership, P as requireMandate, S as allOf, T as denyAll, _ as requireOrgMembership, a as permissionMatrix, b as requireServiceScope, c as publicReadAdminWrite, d as GRANT_MODES, f as modeSatisfies, g as requireOrgInScope, h as createOrgPermissions, i as ownerWithAdminBypass, j as when, k as requireRoles, l as readOnly, m as createDynamicPermissionMatrix, n as authenticated, o as presets_exports, p as requireGrant, r as fullPublic, s as publicRead, t as adminOnly, u as createRoleHierarchy, v as requireOrgRole, w as anyOf, x as requireTeamMembership, y as requireScopeContext } from "../permissions-tbxFHxjZ.mjs";
|
|
4
|
+
export { GRANT_MODES, adminOnly, allOf, allowPublic, anyOf, applyFieldReadPermissions, applyFieldWritePermissions, applyPermissionResult, authenticated, createDynamicPermissionMatrix, createOrgPermissions, createRoleHierarchy, denyAll, fields, fullPublic, getUserRoles, modeSatisfies, normalizePermissionResult, normalizeRoles, not, ownerWithAdminBypass, permissionMatrix, presets_exports as permissions, publicRead, publicReadAdminWrite, readOnly, requireAgentScope, requireAuth, requireDPoP, requireGrant, requireMandate, requireOrgInScope, requireOrgMembership, requireOrgRole, requireOwnership, requireRoles, requireScopeContext, requireServiceScope, requireTeamMembership, resolveEffectiveRoles, roles, when };
|