@classytic/arc 2.41.1 → 2.43.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/{EventTransport-CbUKDzZZ.d.mts → EventTransport-CiicKz2Z.d.mts} +1 -1
- package/dist/{EventTransport-DSqD_Xsg.mjs → EventTransport-tzA7CAB4.mjs} +22 -17
- package/dist/{HookSystem-CMF94SEA.mjs → HookSystem-B6UaKRbF.mjs} +2 -2
- package/dist/{QueryCache-DFA-QrU9.d.mts → QueryCache--5uAuYPW.d.mts} +7 -3
- package/dist/{ResourceRegistry-MGpXoxHZ.mjs → ResourceRegistry-CuCARu2B.mjs} +44 -13
- package/dist/actionPermissions-BotX1YOa.mjs +776 -0
- package/dist/audit/index.d.mts +43 -5
- package/dist/audit/index.mjs +48 -26
- package/dist/auth/audit.mjs +1 -1
- package/dist/auth/index.d.mts +43 -13
- package/dist/auth/index.mjs +130 -155
- package/dist/auth/redis-session.mjs +1 -1
- package/dist/{authorizationDecision-DawKGoXb.mjs → authorizationDecision-DQYrkl5f.mjs} +7 -102
- package/dist/{base-CxrV1LmG.d.mts → base-CZyixEQR.d.mts} +27 -5
- package/dist/boundedMap-BULzfXYU.mjs +57 -0
- package/dist/{buildHandler-CvxKOc0m.mjs → buildHandler-CRQ8EcX5.mjs} +4 -4
- package/dist/cache/index.d.mts +22 -34
- package/dist/cache/index.mjs +4 -4
- package/dist/{caching-DbCBxlgw.mjs → caching-Cc2M1DwO.mjs} +3 -3
- package/dist/claimed-B10RTfkM.mjs +8 -0
- package/dist/cleanup/index.d.mts +6 -10
- package/dist/cleanup/index.mjs +27 -73
- package/dist/cli/commands/describe.d.mts +4 -2
- package/dist/cli/commands/describe.mjs +7 -4
- package/dist/cli/commands/docs.mjs +3 -3
- package/dist/cli/commands/doctor.mjs +1 -1
- package/dist/cli/commands/generate.mjs +2 -2
- package/dist/cli/commands/init.mjs +2 -2
- package/dist/cli/commands/introspect.mjs +2 -2
- package/dist/cli/index.mjs +3 -3
- package/dist/{constants-D5Ubn1fe.mjs → constants-Cu7Mx7_U.mjs} +4 -2
- package/dist/context/index.d.mts +2 -5
- package/dist/context/index.mjs +18 -38
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +8 -8
- package/dist/{core-5lazhva9.mjs → core-Cb0LjccA.mjs} +5 -4
- package/dist/{createActionRouter-DNqoaK81.mjs → createActionRouter-xjqonMRO.mjs} +87 -39
- package/dist/{createAggregationRouter-BcbQJf10.mjs → createAggregationRouter-zRYWw_5D.mjs} +11 -8
- package/dist/{preloadResources-f3boenVZ.mjs → createApp-D-953ZAq.mjs} +299 -390
- package/dist/{createMcpServer-CbDWkZ1T.d.mts → createMcpServer-C9cxS8zt.d.mts} +1 -1
- package/dist/{defineAggregation-D_eJjm5A.mjs → defineAggregation-Bm8RjMbq.mjs} +1 -1
- package/dist/{defineResource-DxkMTjRn.mjs → defineResource-DIC7JZGU.mjs} +118 -1024
- package/dist/discovery/index.mjs +1 -1
- package/dist/docs/index.d.mts +10 -4
- package/dist/docs/index.mjs +4 -4
- package/dist/{elevation-BXCfVcNG.d.mts → elevation-BW2J3tNj.d.mts} +1 -2
- package/dist/{elevation-CkvG0LG2.mjs → elevation-DG8SrLN-.mjs} +41 -12
- package/dist/encryption/index.mjs +3 -3
- package/dist/{errorHandler-D1GHKGlV.mjs → errorHandler-BnfDviL1.mjs} +26 -5
- package/dist/{errors-BH7YU_9O.mjs → errors-CtrywMHP.mjs} +58 -23
- package/dist/{errors-BGdVwP25.d.mts → errors-JYc9vwqE.d.mts} +9 -0
- package/dist/{eventPlugin-B52abJ8W.d.mts → eventPlugin-BtmA4-3P.d.mts} +65 -9
- package/dist/{eventPlugin-zZY-unhr.mjs → eventPlugin-Q6BhzHzs.mjs} +82 -21
- package/dist/{subscribe-helpers-DatDkcuG.mjs → eventTypes-Cdhm6ZlV.mjs} +151 -17
- package/dist/events/index.d.mts +33 -8
- package/dist/events/index.mjs +49 -53
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis-stream-entry.mjs +139 -157
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/events/transports/redis.mjs +2 -2
- package/dist/factory/index.d.mts +32 -17
- package/dist/factory/index.mjs +213 -32
- package/dist/{fastify-augmentation-Cq2nHVaW.d.mts → fastify-augmentation-DhUJ-mvo.d.mts} +11 -25
- package/dist/{fields-I_EeRWND.d.mts → fields-BBSpf8pS.d.mts} +1 -1
- package/dist/{fields-BHrkoF5y.mjs → fields-ToPYpMqS.mjs} +1 -1
- package/dist/filter-merge-IpGywvym.mjs +99 -0
- package/dist/{generate-BLRf2m3y.mjs → generate-DRTkLlIl.mjs} +2 -2
- package/dist/{headers-BIXWKrzQ.mjs → headers-BbkIF6vA.mjs} +2 -2
- package/dist/health-D-Ft6JM6.mjs +22 -0
- package/dist/{health-ZRHeDHXH.mjs → health-DqbsG-_w.mjs} +115 -55
- package/dist/hooks/index.d.mts +2 -2
- package/dist/hooks/index.mjs +2 -2
- package/dist/idempotency/index.d.mts +31 -7
- package/dist/idempotency/index.mjs +155 -98
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/idempotency/redis.mjs +48 -11
- package/dist/{index-BHdZD8zK.d.mts → index-Bxriq3so.d.mts} +84 -21
- package/dist/{preloadResources-CjS2OfpU.d.mts → index-C8zVGYsk.d.mts} +37 -38
- package/dist/{index-D4u76eQW.d.mts → index-CRoPtISf.d.mts} +23 -73
- package/dist/{index-CbJiYrfv.d.mts → index-Chgz3d0i.d.mts} +1 -1
- package/dist/index-JdbZAIsm.d.mts +510 -0
- package/dist/{index-DHbyPo5j.d.mts → index-mqaqZNpe.d.mts} +253 -254
- package/dist/index.d.mts +8 -8
- package/dist/index.mjs +11 -11
- package/dist/{init-CpLkjU18.mjs → init-B_oUYDXI.mjs} +1 -1
- package/dist/integrations/event-gateway.d.mts +8 -3
- package/dist/integrations/event-gateway.mjs +4 -4
- package/dist/integrations/index.d.mts +3 -3
- package/dist/integrations/index.mjs +1 -1
- package/dist/integrations/jobs/index.d.mts +1 -1
- package/dist/integrations/jobs/index.mjs +18 -5
- package/dist/integrations/mcp/index.d.mts +72 -59
- package/dist/integrations/mcp/index.mjs +19 -4
- package/dist/integrations/mcp/testing.d.mts +2 -2
- package/dist/integrations/mcp/testing.mjs +2 -2
- package/dist/integrations/streamline.d.mts +78 -136
- package/dist/integrations/streamline.mjs +116 -54
- package/dist/integrations/webhooks.d.mts +60 -120
- package/dist/integrations/webhooks.mjs +36 -80
- package/dist/integrations/websocket-pushref-redis.d.mts +1 -1
- package/dist/integrations/websocket-pushref-redis.mjs +1 -1
- package/dist/integrations/websocket-redis.d.mts +1 -1
- package/dist/integrations/websocket-redis.mjs +1 -1
- package/dist/integrations/websocket.d.mts +3 -3
- package/dist/integrations/websocket.mjs +2 -2
- package/dist/{interface-DEWNOMza.d.mts → interface-C0395yUN.d.mts} +13 -1
- package/dist/{jsonBody-ELR9lfeU.mjs → jsonBody-B4YVGcql.mjs} +2 -2
- package/dist/{keys-cpZCtcIE.mjs → keys-B2KtgMLJ.mjs} +7 -18
- package/dist/{loadResources-BPfWiI5s.mjs → loadResources-Dn4lalw2.mjs} +3 -3
- package/dist/{loadResourcesFromEntry-ixtfL3J2.mjs → loadResourcesFromEntry-3sLYbuse.mjs} +2 -2
- package/dist/logger/index.d.mts +1 -8
- package/dist/logger/index.mjs +3 -153
- package/dist/logger-DXjfIqjV.mjs +160 -0
- package/dist/{mcpPlugin-BX5PujI0.mjs → mcpPlugin-C08HalN5.mjs} +433 -170
- package/dist/{memory-Iotz0cCx.mjs → memory-9Cp7611C.mjs} +36 -34
- package/dist/{metrics-NMsmpRmY.mjs → metrics-CWI11eRu.mjs} +39 -72
- package/dist/middleware/index.d.mts +2 -2
- package/dist/middleware/index.mjs +3 -3
- package/dist/migrations/index.mjs +2 -2
- package/dist/{module-DpLJG2_N.mjs → module-BSvOQC7E.mjs} +8 -8
- package/dist/multiTenant-cY0FkDiA.d.mts +104 -0
- package/dist/{multipartBody-DtRWM6_U.mjs → multipartBody-CXM2a-Dy.mjs} +46 -7
- package/dist/{openapi-CmFf3yq_.mjs → openapi-DT_G4VhY.mjs} +48 -10
- package/dist/outbox-admin/index.d.mts +3 -3
- package/dist/outbox-admin/index.mjs +6 -18
- package/dist/{outbox-module-D2z5WkDY.mjs → outbox-module-Bjgzl6tg.mjs} +203 -33
- package/dist/{outbox-module-BHdUnuuK.d.mts → outbox-module-gZCHoE4H.d.mts} +79 -6
- package/dist/permissions/index.d.mts +5 -4
- package/dist/permissions/index.mjs +8 -6
- package/dist/{permissions-D8TBH44E.mjs → permissions-Cm0uYHvd.mjs} +149 -33
- package/dist/{pipe-FMWNheSO.mjs → pipe-BqeGT6UO.mjs} +2 -2
- package/dist/pipeline/index.d.mts +2 -2
- package/dist/pipeline/index.mjs +2 -2
- package/dist/plan-FVR6LP59.mjs +565 -0
- package/dist/{BaseController-CYEwjyjN.mjs → plugin-BccET1tL.mjs} +1256 -345
- package/dist/plugins/index.d.mts +14 -11
- package/dist/plugins/index.mjs +44 -22
- package/dist/plugins/response-cache.mjs +8 -14
- package/dist/plugins/tracing-entry.mjs +2 -2
- package/dist/{pluralize-CXWZIzTv.mjs → pluralize-CVIOcgMF.mjs} +1 -1
- package/dist/presets/filesUpload.d.mts +2 -2
- package/dist/presets/filesUpload.mjs +5 -5
- package/dist/presets/index.d.mts +26 -31
- package/dist/presets/index.mjs +4 -4
- package/dist/presets/multiTenant.d.mts +2 -107
- package/dist/presets/multiTenant.mjs +22 -162
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +2 -2
- package/dist/{presets-ZJSPgmT2.mjs → presets-Cpvj6fGO.mjs} +99 -107
- package/dist/{projection-BzIgzoKZ.mjs → projection-Ce4aQhQk.mjs} +2 -2
- package/dist/{pushref-registry-Dov9l4qE.d.mts → pushref-registry-UxCkoA4C.d.mts} +1 -1
- package/dist/{queryCachePlugin-BzqxconG.d.mts → queryCachePlugin-C2lZjTxk.d.mts} +1 -1
- package/dist/{queryCachePlugin-DZHPOm9p.mjs → queryCachePlugin-CFWNH8aT.mjs} +12 -14
- package/dist/{rateLimitKey-BvOvcBDi.mjs → rateLimitKey-C4O-kPm0.mjs} +1 -1
- package/dist/{realtime-8tIQWZz4.mjs → realtime-DmPE9S54.mjs} +25 -21
- package/dist/redis-stream-BMk-bpQS.d.mts +219 -0
- package/dist/{redis-Bzi1y3F9.d.mts → redis-vLvtcGy_.d.mts} +13 -24
- package/dist/redisShapes-CQksY67q.d.mts +55 -0
- package/dist/registry/index.d.mts +9 -4
- package/dist/registry/index.mjs +4 -4
- package/dist/{registry-DNkgGQcK.mjs → registry-Bcz9G1bh.mjs} +10 -8
- package/dist/{renewingLease-yF8BdW4b.mjs → renewingLease-qODa7KJQ.mjs} +1 -1
- package/dist/{replyHelpers-CNdvMfM4.mjs → replyHelpers-CG_VtVgS.mjs} +2 -2
- package/dist/{requestContext-cDkAUr-P.mjs → requestContext-Dsu9P1bV.mjs} +1 -1
- package/dist/requirement-CbiLdQ1n.d.mts +22 -0
- package/dist/requirement-KDdoRcOP.mjs +35 -0
- package/dist/resourceRateLimit-Wf6d6FL9.d.mts +30 -0
- package/dist/resourceRateLimit-dSjVr1pk.mjs +180 -0
- package/dist/{rolldown-runtime-eysyzFAF.mjs → rolldown-runtime-D1w9Y1VJ.mjs} +1 -1
- package/dist/routeValidator-D36SW8qt.mjs +70 -0
- package/dist/{runtimeCapabilities-CkhS6jn6.mjs → runtimeCapabilities-0tccjRlj.mjs} +1 -1
- package/dist/{schedules-j6uXXAeJ.mjs → schedules-C-OpmHBq.mjs} +46 -37
- package/dist/{schemaConverter-BVA7cFRG.mjs → schemaConverter-BORek3O-.mjs} +2 -2
- package/dist/{schemaIR-CTvPDoPB.mjs → schemaIR-C7Vbp71I.mjs} +2 -2
- package/dist/schemas/index.d.mts +10 -0
- package/dist/schemas/index.mjs +8 -3
- package/dist/scim/index.mjs +2 -2
- package/dist/scope/index.d.mts +2 -2
- package/dist/scope/index.mjs +6 -6
- package/dist/{scope-BUi1xV1I.mjs → scope-DQthe88b.mjs} +4 -4
- package/dist/shutdown-MqA81z_P.mjs +59 -0
- package/dist/{simpleEqualityMatcher-C9bzq2gO.mjs → simpleEqualityMatcher-auW8QjXN.mjs} +1 -1
- package/dist/{sse-BeqVKQKu.mjs → sse-CYzQBVFe.mjs} +11 -15
- package/dist/{sseStream-Ppzo-WUH.mjs → sseStream-B5_Afiqn.mjs} +2 -2
- package/dist/{store-helpers-dW71XVWL.mjs → store-helpers-BOzk-CuX.mjs} +1 -1
- package/dist/{streaming-IcRFjWQU.mjs → streaming-DjTQhZrN.mjs} +1 -1
- package/dist/subscriberIdentity-CW39uA8G.mjs +28 -0
- package/dist/tenancy--ed9eH_z.mjs +43 -0
- package/dist/tenantPreparation-BKon6Yox.mjs +67 -0
- package/dist/testing/cleanupStoreContract.mjs +1 -1
- package/dist/testing/index.d.mts +193 -175
- package/dist/testing/index.mjs +286 -212
- package/dist/testing/outboxStoreContract.mjs +137 -2
- package/dist/testing/requiredPorts.d.mts +17 -0
- package/dist/testing/requiredPorts.mjs +65 -0
- package/dist/testing/storageContract.mjs +1 -1
- package/dist/timing-qczZ0YC-.mjs +46 -0
- package/dist/{transactionContext-CPbdlnVw.mjs → transactionContext-DjFkrNG6.mjs} +1 -1
- package/dist/{typeGuards-BLjPgVBm.mjs → typeGuards-CySF12G-.mjs} +1 -1
- package/dist/types/index.d.mts +4 -4
- package/dist/types/index.mjs +1 -1
- package/dist/types/storage.mjs +1 -1
- package/dist/{types-CWQn9-IC.d.mts → types-B4Aq4Sdj.d.mts} +22 -9
- package/dist/{types-DfVDvkdP.d.mts → types-BJ-2XqVF.d.mts} +189 -204
- package/dist/{types-CgenB1gv.mjs → types-Bk9NOgOC.mjs} +4 -4
- package/dist/usage/index.d.mts +2 -3
- package/dist/usage/index.mjs +5 -5
- package/dist/{userHelpers-ChRvkuSZ.mjs → userHelpers-aklawRaK.mjs} +1 -1
- package/dist/utils/index.d.mts +84 -223
- package/dist/utils/index.mjs +13 -12
- package/dist/{utils-CxcctrAh.mjs → utils-ec1oTo1n.mjs} +315 -404
- package/dist/{validate-DrYWnOzI.mjs → validate-9yzHC1e2.mjs} +6 -14
- package/dist/{versioning-D6JIK8Tk.d.mts → versioning-Bsb51gGT.d.mts} +28 -13
- package/dist/{versioning-DKkT4hNJ.mjs → versioning-fhhWHV74.mjs} +4 -4
- package/dist/{websocket-BOXVggMf.d.mts → websocket-Cs4awVyD.d.mts} +1 -1
- package/dist/{workScope-CxKqoIFn.mjs → workScope-zpPyge4t.mjs} +2 -2
- package/package.json +16 -12
- package/skills/arc/SKILL.md +13 -4
- package/skills/arc/references/auth.md +8 -5
- package/skills/arc/references/integrations.md +1 -1
- package/skills/arc/references/mcp.md +8 -6
- package/skills/arc/references/multi-tenancy.md +19 -14
- package/skills/arc/references/production.md +27 -10
- package/dist/actionPermissions-BoPGQ5-4.mjs +0 -350
- package/dist/index-CFfLKMH4.d.mts +0 -290
- package/dist/interface-YzIbF7Ii.mjs +0 -17
- package/dist/redis-stream-BPKscsTB.d.mts +0 -260
- package/dist/registerRoute-Bsnk6NiZ.mjs +0 -364
- package/dist/sync/changelog.d.mts +0 -3
- package/dist/sync/changelog.mjs +0 -3
|
@@ -1,21 +1,28 @@
|
|
|
1
|
-
/*! @classytic/arc v2.
|
|
2
|
-
import {
|
|
3
|
-
import { p as isArcError, t as ArcError } from "./errors-
|
|
4
|
-
import { n as
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { a as resolveEffectiveRoles, t as applyFieldReadPermissions } from "./fields-
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { t as
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
1
|
+
/*! @classytic/arc v2.43.0 | MIT | https://classytic.com */
|
|
2
|
+
import { t as arcLog } from "./logger-DXjfIqjV.mjs";
|
|
3
|
+
import { i as NotFoundError, p as isArcError, t as ArcError } from "./errors-CtrywMHP.mjs";
|
|
4
|
+
import { c as BaseController, n as normalizeActionsToRouterConfig, r as resolveActionTargetFence } from "./plugin-BccET1tL.mjs";
|
|
5
|
+
import { n as requireSingleHeaderValue } from "./headers-BbkIF6vA.mjs";
|
|
6
|
+
import { b as isElevated, c as getOrgId, g as getUserId, x as isMember } from "./types-Bk9NOgOC.mjs";
|
|
7
|
+
import { a as resolveEffectiveRoles, t as applyFieldReadPermissions } from "./fields-ToPYpMqS.mjs";
|
|
8
|
+
import { o as toJsonSchema } from "./schemaConverter-BORek3O-.mjs";
|
|
9
|
+
import { t as buildRequestScopeProjection } from "./projection-Ce4aQhQk.mjs";
|
|
10
|
+
import { l as resolvePipelineSteps, t as resolveActionPermission } from "./actionPermissions-BotX1YOa.mjs";
|
|
11
|
+
import { a as withoutTenantInjection, r as prepareTenantContext } from "./tenantPreparation-BKon6Yox.mjs";
|
|
12
|
+
import { r as evaluatePermissionDecision } from "./authorizationDecision-DQYrkl5f.mjs";
|
|
13
|
+
import { n as declareRuntimeCapability } from "./runtimeCapabilities-0tccjRlj.mjs";
|
|
14
|
+
import { t as executePipeline } from "./pipe-BqeGT6UO.mjs";
|
|
15
|
+
import { a as resourceRateLimitWindowMs, i as resolveResourceRateLimiter, t as createMemoryResourceRateLimiter } from "./resourceRateLimit-dSjVr1pk.mjs";
|
|
16
|
+
import { n as evictToSize, r as touch, t as assertCapacity } from "./boundedMap-BULzfXYU.mjs";
|
|
17
|
+
import { t as pluralize } from "./pluralize-CVIOcgMF.mjs";
|
|
18
|
+
import { n as createIdempotencyResult, r as operationIdentity, t as resolveIdempotencyTiming } from "./timing-qczZ0YC-.mjs";
|
|
19
|
+
import { r as shouldRejectAdditionalProperties, t as normalizeSchemaIR } from "./schemaIR-C7Vbp71I.mjs";
|
|
20
|
+
import { n as validateAggregations } from "./validate-9yzHC1e2.mjs";
|
|
21
|
+
import { n as executeAggregation } from "./buildHandler-CRQ8EcX5.mjs";
|
|
22
|
+
import { ARC_ERROR_CODES, isHttpError, toErrorContract } from "@classytic/repo-core/errors";
|
|
17
23
|
import { createHash, randomUUID } from "node:crypto";
|
|
18
24
|
import fp from "fastify-plugin";
|
|
25
|
+
import { IdempotencyError, fingerprintRequest } from "@classytic/repo-core/idempotency";
|
|
19
26
|
import { z } from "zod";
|
|
20
27
|
//#region src/integrations/mcp/createMcpServer.ts
|
|
21
28
|
/**
|
|
@@ -222,51 +229,55 @@ function buildRequestContext(input, auth, operation, policyFilters, scopeOverrid
|
|
|
222
229
|
},
|
|
223
230
|
...extras?.server ? { server: extras.server } : {}
|
|
224
231
|
};
|
|
232
|
+
const prepared = (ctx) => {
|
|
233
|
+
prepareTenantContext(ctx, extras?.tenantMiddlewares);
|
|
234
|
+
return ctx;
|
|
235
|
+
};
|
|
225
236
|
switch (operation) {
|
|
226
|
-
case "list": return {
|
|
237
|
+
case "list": return prepared({
|
|
227
238
|
...base,
|
|
228
239
|
params: {},
|
|
229
240
|
query: expandOperatorKeys(input),
|
|
230
241
|
body: void 0
|
|
231
|
-
};
|
|
242
|
+
});
|
|
232
243
|
case "get": {
|
|
233
244
|
const { id, ...rest } = input;
|
|
234
|
-
return {
|
|
245
|
+
return prepared({
|
|
235
246
|
...base,
|
|
236
247
|
params: { id: String(id ?? "") },
|
|
237
248
|
query: expandOperatorKeys(rest),
|
|
238
249
|
body: void 0
|
|
239
|
-
};
|
|
250
|
+
});
|
|
240
251
|
}
|
|
241
|
-
case "create": return {
|
|
252
|
+
case "create": return prepared({
|
|
242
253
|
...base,
|
|
243
254
|
params: {},
|
|
244
255
|
query: {},
|
|
245
256
|
body: { ...input }
|
|
246
|
-
};
|
|
257
|
+
});
|
|
247
258
|
case "update": {
|
|
248
259
|
const { id: _id, ...body } = input;
|
|
249
|
-
return {
|
|
260
|
+
return prepared({
|
|
250
261
|
...base,
|
|
251
262
|
params: { id: String(_id ?? "") },
|
|
252
263
|
query: {},
|
|
253
264
|
body
|
|
254
|
-
};
|
|
265
|
+
});
|
|
255
266
|
}
|
|
256
|
-
case "delete": return {
|
|
267
|
+
case "delete": return prepared({
|
|
257
268
|
...base,
|
|
258
269
|
params: { id: String(input.id ?? "") },
|
|
259
270
|
query: {},
|
|
260
271
|
body: void 0
|
|
261
|
-
};
|
|
272
|
+
});
|
|
262
273
|
case "action": {
|
|
263
274
|
const { id: _actionId, ...actionBody } = input;
|
|
264
|
-
return {
|
|
275
|
+
return prepared({
|
|
265
276
|
...base,
|
|
266
277
|
params: { id: String(_actionId ?? "") },
|
|
267
278
|
query: {},
|
|
268
279
|
body: actionBody
|
|
269
|
-
};
|
|
280
|
+
});
|
|
270
281
|
}
|
|
271
282
|
}
|
|
272
283
|
}
|
|
@@ -563,36 +574,137 @@ function createMcpController(resource) {
|
|
|
563
574
|
});
|
|
564
575
|
}
|
|
565
576
|
//#endregion
|
|
566
|
-
//#region src/integrations/mcp/
|
|
577
|
+
//#region src/integrations/mcp/http-parity.ts
|
|
567
578
|
/**
|
|
568
|
-
*
|
|
569
|
-
*
|
|
570
|
-
*
|
|
571
|
-
* controller methods without going through Fastify's preHandler chain.
|
|
572
|
-
* Before this module landed, every call site reinvented the same shim:
|
|
573
|
-
* fabricate `params` / `body` / `headers`, project the session into a
|
|
574
|
-
* `RequestScope`, run the permission check by hand, translate thrown errors
|
|
575
|
-
* to the MCP `CallToolResult` shape. The drift across those copies is what
|
|
576
|
-
* the mentora review flagged as the "buildCtx synthetic-context hack."
|
|
577
|
-
*
|
|
578
|
-
* Two public surfaces, layered so callers reach for the smallest piece that
|
|
579
|
-
* does the job:
|
|
580
|
-
*
|
|
581
|
-
* - {@link invokeController} full pipeline: permission → context →
|
|
582
|
-
* dispatch → canonical envelope/error
|
|
583
|
-
* conversion. Returns `CallToolResult`.
|
|
584
|
-
* - {@link mcpHandlerAdapter} thin try/catch wrapper that turns any
|
|
585
|
-
* async function into a `ToolDefinition`
|
|
586
|
-
* handler with canonical error shape. Use
|
|
587
|
-
* when you want to compose multiple
|
|
588
|
-
* controller calls or weave in custom logic
|
|
589
|
-
* between them.
|
|
590
|
-
*
|
|
591
|
-
* Both honor the same {@link buildRequestContext} primitive — re-exported
|
|
592
|
-
* from this module so authors who want the synthetic context as data (e.g.
|
|
593
|
-
* to pass into `executePipeline`) can grab it without depending on an
|
|
594
|
-
* internal subpath.
|
|
579
|
+
* The gates an HTTP route applies that an MCP tool must apply too, because a tool calls the
|
|
580
|
+
* controller or action handler directly and skips the route: idempotent execution, the
|
|
581
|
+
* key-required refusal, the per-caller rate limit and the action audit record.
|
|
595
582
|
*/
|
|
583
|
+
function isIdempotencyStore(value) {
|
|
584
|
+
if (typeof value !== "object" || value === null) return false;
|
|
585
|
+
const s = value;
|
|
586
|
+
return typeof s.get === "function" && typeof s.set === "function" && typeof s.tryLock === "function" && typeof s.unlock === "function";
|
|
587
|
+
}
|
|
588
|
+
/** The wired store, when it is one. */
|
|
589
|
+
function idempotencyStoreOf(store) {
|
|
590
|
+
return isIdempotencyStore(store) ? store : void 0;
|
|
591
|
+
}
|
|
592
|
+
function isFencedStore(store) {
|
|
593
|
+
return typeof store.setIfLocked === "function" && store.supportsFencedCompletion !== false;
|
|
594
|
+
}
|
|
595
|
+
/** Refuse an unsafe standalone store; an explicit migration escape is logged at wiring. */
|
|
596
|
+
function assertMcpIdempotencyFencing(wiring) {
|
|
597
|
+
resolveIdempotencyTiming(wiring?.idempotencyTtlMs, wiring?.idempotencyLockTimeoutMs);
|
|
598
|
+
const store = idempotencyStoreOf(wiring?.idempotencyStore);
|
|
599
|
+
if (!store || isFencedStore(store)) return;
|
|
600
|
+
const message = "[Arc/MCP] Idempotency store must implement atomic setIfLocked completion fencing. Upgrade the store or explicitly allowUnfencedCompletion during migration.";
|
|
601
|
+
if (wiring?.allowUnfencedCompletion !== true) throw new Error(message);
|
|
602
|
+
const logger = wiring.log && typeof wiring.log === "object" ? wiring.log : {};
|
|
603
|
+
const warning = "[Arc/MCP] allowUnfencedCompletion permits an expired worker to overwrite a newer result; migrate the idempotency store to atomic setIfLocked.";
|
|
604
|
+
if (typeof logger.warn === "function") logger.warn.call(wiring.log, warning);
|
|
605
|
+
else arcLog("mcp:idempotency").warn(warning);
|
|
606
|
+
}
|
|
607
|
+
/** Same protocol as the HTTP plugin: cached → replay; locked → 409; else lock → run → record. */
|
|
608
|
+
async function runIdempotentTool(args) {
|
|
609
|
+
const { store, dispatch } = args;
|
|
610
|
+
const { ttlMs, lockTimeoutMs } = resolveIdempotencyTiming(args.idempotencyTtlMs, args.idempotencyLockTimeoutMs);
|
|
611
|
+
if (!isFencedStore(store) && args.allowUnfencedCompletion !== true) return toCallToolError({
|
|
612
|
+
code: ARC_ERROR_CODES.INTERNAL_ERROR,
|
|
613
|
+
message: "Idempotency store requires atomic setIfLocked completion fencing",
|
|
614
|
+
status: 500
|
|
615
|
+
});
|
|
616
|
+
const fullKey = `${args.key}:${operationIdentity(`${args.resourceName}:${args.operation}`, args.scope ?? buildScope(args.session), void 0, "mcp")}`;
|
|
617
|
+
let fingerprint;
|
|
618
|
+
try {
|
|
619
|
+
fingerprint = fingerprintRequest(args.input);
|
|
620
|
+
} catch (error) {
|
|
621
|
+
if (error instanceof IdempotencyError) return toCallToolError({
|
|
622
|
+
code: ARC_ERROR_CODES.IDEMPOTENCY_BODY_UNFINGERPRINTABLE,
|
|
623
|
+
message: error.message,
|
|
624
|
+
status: 400
|
|
625
|
+
});
|
|
626
|
+
throw error;
|
|
627
|
+
}
|
|
628
|
+
const replay = (cached) => {
|
|
629
|
+
if (cached) {
|
|
630
|
+
if (cached.requestFingerprint && cached.requestFingerprint !== fingerprint) return toCallToolError({
|
|
631
|
+
code: ARC_ERROR_CODES.IDEMPOTENCY_KEY_REUSED,
|
|
632
|
+
message: "Idempotency key was already used for a different request body. Use a new key for a new request.",
|
|
633
|
+
status: 422
|
|
634
|
+
});
|
|
635
|
+
return cached.body;
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
const cachedResult = replay(await store.get(fullKey));
|
|
639
|
+
if (cachedResult) return cachedResult;
|
|
640
|
+
const lockId = randomUUID();
|
|
641
|
+
if (!await store.tryLock(fullKey, lockId, lockTimeoutMs)) return toCallToolError({
|
|
642
|
+
code: ARC_ERROR_CODES.IDEMPOTENCY_CONFLICT,
|
|
643
|
+
message: "A call with this idempotency key is already in progress",
|
|
644
|
+
status: 409
|
|
645
|
+
});
|
|
646
|
+
try {
|
|
647
|
+
const completed = replay(await store.get(fullKey));
|
|
648
|
+
if (completed) return completed;
|
|
649
|
+
const result = JSON.parse(JSON.stringify(await dispatch()));
|
|
650
|
+
if (!result.isError) {
|
|
651
|
+
const completion = createIdempotencyResult(200, result, {}, ttlMs, fingerprint);
|
|
652
|
+
if (store.setIfLocked && store.supportsFencedCompletion !== false) {
|
|
653
|
+
if (!await store.setIfLocked(fullKey, lockId, completion)) return toCallToolError({
|
|
654
|
+
code: ARC_ERROR_CODES.IDEMPOTENCY_CONFLICT,
|
|
655
|
+
message: "The idempotency completion lease was lost; success could not be recorded",
|
|
656
|
+
status: 409
|
|
657
|
+
});
|
|
658
|
+
} else await store.set(fullKey, completion);
|
|
659
|
+
}
|
|
660
|
+
return result;
|
|
661
|
+
} finally {
|
|
662
|
+
await store.unlock(fullKey, lockId);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
/** The 400 a keyless call to a key-required route gets over HTTP. */
|
|
666
|
+
function idempotencyKeyRequiredResult(toolName) {
|
|
667
|
+
return toCallToolError({
|
|
668
|
+
code: ARC_ERROR_CODES.IDEMPOTENCY_KEY_REQUIRED,
|
|
669
|
+
message: `'${toolName}' moves money or stock: pass \`_idempotencyKey\` and reuse it on a retry.`,
|
|
670
|
+
status: 400
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
function createToolRateLimiter(config, resourceName, limiter = createMemoryResourceRateLimiter()) {
|
|
674
|
+
if (!config) return void 0;
|
|
675
|
+
resourceRateLimitWindowMs(config.timeWindow);
|
|
676
|
+
return async (session, scope) => (await limiter.consume(resourceName, config, { scope: scope ?? buildScope(session) })).allowed;
|
|
677
|
+
}
|
|
678
|
+
function rateLimitedResult(toolName) {
|
|
679
|
+
return toCallToolError({
|
|
680
|
+
code: "arc.rate_limited",
|
|
681
|
+
message: `Too many calls to '${toolName}' — retry later.`,
|
|
682
|
+
status: 429
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
/** `audit.custom(resource, id, action, {}, context)` after a successful action, like the route. */
|
|
686
|
+
async function auditToolAction(audit, args) {
|
|
687
|
+
if (typeof audit !== "object" || audit === null) return;
|
|
688
|
+
const logger = audit;
|
|
689
|
+
if (typeof logger.custom !== "function" || logger._noop === true) return;
|
|
690
|
+
const { session } = args;
|
|
691
|
+
const actorId = session?.userId ?? session?.clientId;
|
|
692
|
+
try {
|
|
693
|
+
await logger.custom.call(audit, args.resourceName, args.id, args.action, { channel: "mcp" }, {
|
|
694
|
+
...actorId !== void 0 ? { actorId } : {},
|
|
695
|
+
...session?.organizationId !== void 0 ? { organizationId: session.organizationId } : {},
|
|
696
|
+
endpoint: `MCP ${args.toolName}`
|
|
697
|
+
});
|
|
698
|
+
} catch (err) {
|
|
699
|
+
const log = typeof args.log === "object" && args.log !== null ? args.log : void 0;
|
|
700
|
+
if (typeof log?.error === "function") log.error.call(log, {
|
|
701
|
+
err,
|
|
702
|
+
tool: args.toolName
|
|
703
|
+
}, "[arc/mcp] action audit write failed");
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
//#endregion
|
|
707
|
+
//#region src/integrations/mcp/invokeController.ts
|
|
596
708
|
/**
|
|
597
709
|
* Assemble the {@link McpContextExtras} for a controller invocation from
|
|
598
710
|
* resource-scoped metadata + app-level wiring. Mirrors what
|
|
@@ -601,8 +713,9 @@ function createMcpController(resource) {
|
|
|
601
713
|
*/
|
|
602
714
|
function buildContextExtras(options) {
|
|
603
715
|
const { wiring, resourceName, permissions, fields, schemaOptions, idField } = options;
|
|
604
|
-
if (!wiring) return
|
|
716
|
+
if (!wiring) return { tenantMiddlewares: options.tenantMiddlewares };
|
|
605
717
|
return {
|
|
718
|
+
tenantMiddlewares: options.tenantMiddlewares,
|
|
606
719
|
arc: {
|
|
607
720
|
resourceName,
|
|
608
721
|
schemaOptions,
|
|
@@ -620,23 +733,6 @@ function buildContextExtras(options) {
|
|
|
620
733
|
};
|
|
621
734
|
}
|
|
622
735
|
/**
|
|
623
|
-
* Stable fingerprint of a tool invocation — sorted-key JSON, hashed.
|
|
624
|
-
* Scopes an idempotency key to (resource, action, input, caller) so a
|
|
625
|
-
* reused key with DIFFERENT input is a fresh execution, matching the HTTP
|
|
626
|
-
* plugin's method+url+bodyHash+user fingerprint semantics.
|
|
627
|
-
*/
|
|
628
|
-
function invocationFingerprint(resourceName, actionName, input, session) {
|
|
629
|
-
const sortKeys = (value) => {
|
|
630
|
-
if (Array.isArray(value)) return value.map(sortKeys);
|
|
631
|
-
if (value !== null && typeof value === "object") return Object.fromEntries(Object.keys(value).sort().map((k) => [k, sortKeys(value[k])]));
|
|
632
|
-
return value;
|
|
633
|
-
};
|
|
634
|
-
return `mcp:${resourceName}:${actionName}:${createHash("sha256").update(JSON.stringify(sortKeys(input))).digest("hex").substring(0, 16)}:u=${session?.userId ?? session?.clientId ?? "anon"}`;
|
|
635
|
-
}
|
|
636
|
-
/** TTLs matching the HTTP idempotency plugin's defaults. */
|
|
637
|
-
const MCP_IDEMPOTENCY_TTL_MS = 864e5;
|
|
638
|
-
const MCP_IDEMPOTENCY_LOCK_MS = 1e4;
|
|
639
|
-
/**
|
|
640
736
|
* Invoke a controller method through the MCP synthetic-context path.
|
|
641
737
|
*
|
|
642
738
|
* Runs the same chain CRUD MCP tools run:
|
|
@@ -685,8 +781,9 @@ async function invokeController(controller, op, input, options) {
|
|
|
685
781
|
reason: permResult.reason,
|
|
686
782
|
session
|
|
687
783
|
});
|
|
784
|
+
const reqCtx = buildRequestContext(input, session, op, permResult?.policy, permResult?.scope, buildContextExtras(options));
|
|
785
|
+
if (options.allow && !await options.allow(session, reqCtx.metadata._scope)) return rateLimitedResult(actionName);
|
|
688
786
|
const dispatch = async () => {
|
|
689
|
-
const reqCtx = buildRequestContext(input, session, op, permResult?.policy, permResult?.scope, buildContextExtras(options));
|
|
690
787
|
const response = await method(reqCtx);
|
|
691
788
|
const data = applyMcpReadMasking(response.data, {
|
|
692
789
|
fields,
|
|
@@ -698,25 +795,20 @@ async function invokeController(controller, op, input, options) {
|
|
|
698
795
|
data
|
|
699
796
|
});
|
|
700
797
|
};
|
|
701
|
-
const store = wiring?.idempotencyStore;
|
|
702
|
-
if (idempotencyKey && store) {
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
return result;
|
|
716
|
-
} finally {
|
|
717
|
-
await store.unlock(fullKey, lockId);
|
|
718
|
-
}
|
|
719
|
-
}
|
|
798
|
+
const store = idempotencyStoreOf(wiring?.idempotencyStore);
|
|
799
|
+
if (idempotencyKey && store) return await runIdempotentTool({
|
|
800
|
+
store,
|
|
801
|
+
allowUnfencedCompletion: wiring?.allowUnfencedCompletion,
|
|
802
|
+
idempotencyTtlMs: wiring?.idempotencyTtlMs,
|
|
803
|
+
idempotencyLockTimeoutMs: wiring?.idempotencyLockTimeoutMs,
|
|
804
|
+
key: idempotencyKey,
|
|
805
|
+
resourceName,
|
|
806
|
+
operation: actionName,
|
|
807
|
+
input,
|
|
808
|
+
session,
|
|
809
|
+
scope: reqCtx.metadata._scope,
|
|
810
|
+
dispatch
|
|
811
|
+
});
|
|
720
812
|
return await dispatch();
|
|
721
813
|
} catch (err) {
|
|
722
814
|
return toCallToolError(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -806,7 +898,9 @@ const MUTATING_CRUD_OPS = /* @__PURE__ */ new Set([
|
|
|
806
898
|
]);
|
|
807
899
|
function createCrudHandler(op, controller, resourceName, permissions, fields, extras) {
|
|
808
900
|
const permission = permissions?.[op];
|
|
809
|
-
|
|
901
|
+
const toolName = extras?.toolName ?? `${op}_${resourceName}`;
|
|
902
|
+
return async (input, ctx) => {
|
|
903
|
+
if (extras?.keyRequired && (typeof input._idempotencyKey !== "string" || input._idempotencyKey === "")) return idempotencyKeyRequiredResult(toolName);
|
|
810
904
|
let idempotencyKey;
|
|
811
905
|
let payload = input;
|
|
812
906
|
if ("_idempotencyKey" in input) {
|
|
@@ -816,10 +910,12 @@ function createCrudHandler(op, controller, resourceName, permissions, fields, ex
|
|
|
816
910
|
}
|
|
817
911
|
return invokeController(controller, op, payload, {
|
|
818
912
|
session: ctx.session,
|
|
913
|
+
allow: extras?.allow,
|
|
819
914
|
resourceName,
|
|
820
915
|
permissions: permission,
|
|
821
916
|
fields,
|
|
822
917
|
wiring: extras?.wiring,
|
|
918
|
+
tenantMiddlewares: extras?.tenantMiddlewares,
|
|
823
919
|
schemaOptions: extras?.schemaOptions,
|
|
824
920
|
idField: extras?.idField,
|
|
825
921
|
idempotencyKey
|
|
@@ -1082,7 +1178,7 @@ var McpAuthCache = class {
|
|
|
1082
1178
|
maxEntries;
|
|
1083
1179
|
constructor(opts) {
|
|
1084
1180
|
this.ttlMs = opts?.ttlMs ?? DEFAULT_AUTH_CACHE_TTL_MS;
|
|
1085
|
-
this.maxEntries = opts?.maxEntries ?? DEFAULT_AUTH_CACHE_MAX;
|
|
1181
|
+
this.maxEntries = assertCapacity("McpAuthCache maxEntries", opts?.maxEntries ?? DEFAULT_AUTH_CACHE_MAX);
|
|
1086
1182
|
}
|
|
1087
1183
|
get(key) {
|
|
1088
1184
|
const entry = this.cache.get(key);
|
|
@@ -1097,15 +1193,12 @@ var McpAuthCache = class {
|
|
|
1097
1193
|
if (this.cache.size >= this.maxEntries) {
|
|
1098
1194
|
const now = Date.now();
|
|
1099
1195
|
for (const [k, v] of this.cache) if (now > v.expires) this.cache.delete(k);
|
|
1100
|
-
if (this.cache.size >= this.maxEntries) {
|
|
1101
|
-
const firstKey = this.cache.keys().next().value;
|
|
1102
|
-
if (firstKey) this.cache.delete(firstKey);
|
|
1103
|
-
}
|
|
1104
1196
|
}
|
|
1105
|
-
this.cache
|
|
1197
|
+
touch(this.cache, key, {
|
|
1106
1198
|
result,
|
|
1107
1199
|
expires: Date.now() + this.ttlMs
|
|
1108
1200
|
});
|
|
1201
|
+
evictToSize(this.cache, this.maxEntries);
|
|
1109
1202
|
}
|
|
1110
1203
|
};
|
|
1111
1204
|
/**
|
|
@@ -1229,7 +1322,10 @@ function convertActionSchemaToZod(raw) {
|
|
|
1229
1322
|
*/
|
|
1230
1323
|
function createActionToolHandler(actionName, handler, permissions, resourceName, _resourcePermissions, rawSchema, idLess = false, fields, extras) {
|
|
1231
1324
|
const wiring = extras?.wiring;
|
|
1325
|
+
const parity = extras?.parity;
|
|
1326
|
+
const toolName = parity?.toolName ?? `${actionName}_${resourceName}`;
|
|
1232
1327
|
const contextExtras = wiring ? {
|
|
1328
|
+
tenantMiddlewares: extras?.tenantMiddlewares,
|
|
1233
1329
|
arc: {
|
|
1234
1330
|
resourceName,
|
|
1235
1331
|
schemaOptions: extras?.schemaOptions,
|
|
@@ -1244,11 +1340,16 @@ function createActionToolHandler(actionName, handler, permissions, resourceName,
|
|
|
1244
1340
|
audit: wiring.audit,
|
|
1245
1341
|
log: wiring.log
|
|
1246
1342
|
}
|
|
1247
|
-
} :
|
|
1343
|
+
} : { tenantMiddlewares: extras?.tenantMiddlewares };
|
|
1248
1344
|
const ir = rawSchema ? normalizeSchemaIR(rawSchema) : void 0;
|
|
1249
|
-
const
|
|
1250
|
-
|
|
1345
|
+
const strict = ir ? shouldRejectAdditionalProperties(ir) : false;
|
|
1346
|
+
const allowedBaseKeys = [...idLess ? [] : ["id"], ...parity?.keyRequired ? ["_idempotencyKey"] : []];
|
|
1347
|
+
const allowedKeys = strict && ir ? /* @__PURE__ */ new Set([...allowedBaseKeys, ...Object.keys(ir.properties)]) : void 0;
|
|
1348
|
+
return async (rawInput, ctx) => {
|
|
1251
1349
|
const session = ctx.session;
|
|
1350
|
+
const { _idempotencyKey: rawKey, ...input } = rawInput;
|
|
1351
|
+
const idempotencyKey = typeof rawKey === "string" && rawKey !== "" ? rawKey : void 0;
|
|
1352
|
+
if (parity?.keyRequired && idempotencyKey === void 0) return idempotencyKeyRequiredResult(toolName);
|
|
1252
1353
|
if (allowedKeys) {
|
|
1253
1354
|
const extras = Object.keys(input).filter((k) => !allowedKeys.has(k));
|
|
1254
1355
|
if (extras.length > 0) return toCallToolError({
|
|
@@ -1269,18 +1370,50 @@ function createActionToolHandler(actionName, handler, permissions, resourceName,
|
|
|
1269
1370
|
reason: permResult.reason,
|
|
1270
1371
|
session
|
|
1271
1372
|
});
|
|
1272
|
-
const reqCtx = buildRequestContext({
|
|
1273
|
-
...input,
|
|
1274
|
-
action: actionName
|
|
1275
|
-
}, session, "action", permResult?.policy, permResult?.scope, contextExtras);
|
|
1276
|
-
const id = idLess ? "" : typeof input.id === "string" ? input.id : "";
|
|
1277
|
-
const { id: _discardId, ...data } = input;
|
|
1278
1373
|
try {
|
|
1279
|
-
|
|
1280
|
-
|
|
1374
|
+
const reqCtx = buildRequestContext({
|
|
1375
|
+
...input,
|
|
1376
|
+
action: actionName
|
|
1377
|
+
}, session, "action", permResult?.policy, permResult?.scope, contextExtras);
|
|
1378
|
+
const id = idLess ? "" : typeof input.id === "string" ? input.id : "";
|
|
1379
|
+
const { id: _discardId, ...data } = input;
|
|
1380
|
+
if (!idLess && parity?.fence && !await parity.fence(id, reqCtx)) throw new NotFoundError(resourceName);
|
|
1381
|
+
if (parity?.allow && !await parity.allow(session, reqCtx.metadata._scope)) return rateLimitedResult(toolName);
|
|
1382
|
+
const run = async () => {
|
|
1383
|
+
const req = idempotencyKey === void 0 ? reqCtx : {
|
|
1384
|
+
...reqCtx,
|
|
1385
|
+
idempotencyKey
|
|
1386
|
+
};
|
|
1387
|
+
const result = await handler(id, data, req);
|
|
1388
|
+
if (parity?.audit) await auditToolAction(wiring?.audit, {
|
|
1389
|
+
resourceName,
|
|
1390
|
+
id,
|
|
1391
|
+
action: actionName,
|
|
1392
|
+
toolName,
|
|
1393
|
+
session,
|
|
1394
|
+
log: wiring?.log
|
|
1395
|
+
});
|
|
1396
|
+
return toCallToolSuccess(applyMcpReadMasking(result, {
|
|
1397
|
+
fields,
|
|
1398
|
+
session,
|
|
1399
|
+
scopeOverride: permResult?.scope
|
|
1400
|
+
}));
|
|
1401
|
+
};
|
|
1402
|
+
const store = idempotencyStoreOf(wiring?.idempotencyStore);
|
|
1403
|
+
if (idempotencyKey !== void 0 && store && !parity?.handlerDedup) return await runIdempotentTool({
|
|
1404
|
+
store,
|
|
1405
|
+
allowUnfencedCompletion: wiring?.allowUnfencedCompletion,
|
|
1406
|
+
idempotencyTtlMs: wiring?.idempotencyTtlMs,
|
|
1407
|
+
idempotencyLockTimeoutMs: wiring?.idempotencyLockTimeoutMs,
|
|
1408
|
+
key: idempotencyKey,
|
|
1409
|
+
resourceName,
|
|
1410
|
+
operation: `action.${actionName}`,
|
|
1411
|
+
input,
|
|
1281
1412
|
session,
|
|
1282
|
-
|
|
1283
|
-
|
|
1413
|
+
scope: reqCtx.metadata._scope,
|
|
1414
|
+
dispatch: run
|
|
1415
|
+
});
|
|
1416
|
+
return await run();
|
|
1284
1417
|
} catch (err) {
|
|
1285
1418
|
return toCallToolError(err instanceof Error ? err : new Error(String(err)));
|
|
1286
1419
|
}
|
|
@@ -1320,7 +1453,8 @@ function buildAggregationTools(args) {
|
|
|
1320
1453
|
resourceName,
|
|
1321
1454
|
repo,
|
|
1322
1455
|
buildOptionsFromSession,
|
|
1323
|
-
fields
|
|
1456
|
+
fields,
|
|
1457
|
+
tenantMiddlewares: args.tenantMiddlewares
|
|
1324
1458
|
})
|
|
1325
1459
|
});
|
|
1326
1460
|
}
|
|
@@ -1372,14 +1506,24 @@ function createAggregationToolHandler(args) {
|
|
|
1372
1506
|
session: ctx.session
|
|
1373
1507
|
});
|
|
1374
1508
|
const filterInput = input.filter ?? {};
|
|
1375
|
-
|
|
1509
|
+
let prepared;
|
|
1510
|
+
try {
|
|
1511
|
+
prepared = buildRequestContext(filterInput, ctx.session, "list", permResult?.policy, permResult?.scope, { tenantMiddlewares: args.tenantMiddlewares });
|
|
1512
|
+
} catch (error) {
|
|
1513
|
+
return toCallToolError(error instanceof Error ? error : new Error(String(error)));
|
|
1514
|
+
}
|
|
1515
|
+
const meta = prepared.metadata;
|
|
1516
|
+
const tenantOptions = {
|
|
1517
|
+
...buildOptionsFromSession(ctx.session),
|
|
1518
|
+
...meta._tenantFields
|
|
1519
|
+
};
|
|
1376
1520
|
const result = await executeAggregation(normalized, {
|
|
1377
1521
|
repo,
|
|
1378
1522
|
buildOptions: () => tenantOptions
|
|
1379
1523
|
}, {
|
|
1380
1524
|
query: filterInput,
|
|
1381
1525
|
tenantOptions,
|
|
1382
|
-
policyFilter:
|
|
1526
|
+
policyFilter: meta._policyFilters
|
|
1383
1527
|
});
|
|
1384
1528
|
if (result.status === 200) {
|
|
1385
1529
|
const body = result.body;
|
|
@@ -1677,6 +1821,73 @@ function mapJsonSchemaTypeToArcType(jsonType) {
|
|
|
1677
1821
|
}
|
|
1678
1822
|
}
|
|
1679
1823
|
//#endregion
|
|
1824
|
+
//#region src/integrations/mcp/route-input-schema.ts
|
|
1825
|
+
/**
|
|
1826
|
+
* Custom-route → MCP tool INPUT schema. The route's own schema is the contract:
|
|
1827
|
+
* an input the REST route would refuse must be refused on MCP, never stripped.
|
|
1828
|
+
*
|
|
1829
|
+
* - A Zod schema that is not a plain object (a union of strict arms) is registered
|
|
1830
|
+
* WHOLE — the MCP SDK validates and advertises a Standard Schema as-is. Flattening
|
|
1831
|
+
* it yields `{}`, and the SDK's `z.object({})` then strips every key the caller sent.
|
|
1832
|
+
* - A flat schema declaring `additionalProperties: false` registers as `z.strictObject`,
|
|
1833
|
+
* so an unknown key is an input error instead of a silently dropped filter. So
|
|
1834
|
+
* does a route with NO schema: a key it was sent could never reach the handler.
|
|
1835
|
+
* - Any other non-object schema (a JSON Schema `anyOf`, or a Zod union beside `:id`
|
|
1836
|
+
* or a second source) throws at tool build: there is no lossless flat form, so
|
|
1837
|
+
* bridge it with `mcpHandler` or opt out with `mcp: false`.
|
|
1838
|
+
*/
|
|
1839
|
+
/** A Zod v4 schema (carries the `_zod` marker and the Standard Schema slot). */
|
|
1840
|
+
function isZodTypeSchema(value) {
|
|
1841
|
+
return value !== null && typeof value === "object" && "_zod" in value && "~standard" in value;
|
|
1842
|
+
}
|
|
1843
|
+
function isRecord(value) {
|
|
1844
|
+
return value !== null && typeof value === "object";
|
|
1845
|
+
}
|
|
1846
|
+
/** The JSON Schema HTTP validates this request slot with (`input` io, as the router converts it). */
|
|
1847
|
+
function requestJsonSchema(schema) {
|
|
1848
|
+
return toJsonSchema(schema, "draft-7", "throw", "input");
|
|
1849
|
+
}
|
|
1850
|
+
/** Does this JSON Schema have a single top-level object a flat shape can carry? */
|
|
1851
|
+
function flattensLosslessly(json) {
|
|
1852
|
+
if (!json) return true;
|
|
1853
|
+
if ("anyOf" in json || "oneOf" in json || "allOf" in json) return false;
|
|
1854
|
+
return json.type === "object" || "properties" in json;
|
|
1855
|
+
}
|
|
1856
|
+
function buildRouteInputSchema(route, hasId) {
|
|
1857
|
+
const routeSchema = isRecord(route.schema) ? route.schema : void 0;
|
|
1858
|
+
const sources = [routeSchema?.body, routeSchema?.querystring].filter(isRecord);
|
|
1859
|
+
const whole = [];
|
|
1860
|
+
let unflattenable = 0;
|
|
1861
|
+
for (const source of sources) {
|
|
1862
|
+
if (flattensLosslessly(requestJsonSchema(source))) continue;
|
|
1863
|
+
unflattenable++;
|
|
1864
|
+
if (isZodTypeSchema(source)) whole.push(source);
|
|
1865
|
+
}
|
|
1866
|
+
const [only] = whole;
|
|
1867
|
+
if (unflattenable > 0) {
|
|
1868
|
+
if (only && sources.length === 1 && !hasId) return only;
|
|
1869
|
+
throw new Error(`[Arc/MCP] Route '${route.method} ${route.path}': a non-object input schema (union) is bridged only as a Zod schema that is the route's sole input with no ':id' — anything else loses validation. Declare \`mcpHandler\` for this route or set \`mcp: false\`.`);
|
|
1870
|
+
}
|
|
1871
|
+
const shape = {};
|
|
1872
|
+
if (hasId) shape.id = z.string().describe("Resource ID");
|
|
1873
|
+
let strict = sources.length === 0;
|
|
1874
|
+
for (const source of sources) {
|
|
1875
|
+
if (requestJsonSchema(source)?.additionalProperties === false) strict = true;
|
|
1876
|
+
const ir = normalizeSchemaIR(source);
|
|
1877
|
+
for (const [key, val] of Object.entries(schemaIRToZodShape(ir))) if (!(key in shape)) shape[key] = val;
|
|
1878
|
+
}
|
|
1879
|
+
return strict ? z.strictObject(shape) : shape;
|
|
1880
|
+
}
|
|
1881
|
+
/**
|
|
1882
|
+
* The field shape a tool advertises — the flat record itself, or a whole object
|
|
1883
|
+
* schema's `.shape`. `undefined` for a union, which has no single shape.
|
|
1884
|
+
*/
|
|
1885
|
+
function toolInputShape(schema) {
|
|
1886
|
+
if (schema === void 0) return void 0;
|
|
1887
|
+
if (!isZodTypeSchema(schema)) return schema;
|
|
1888
|
+
return schema instanceof z.ZodObject ? schema.shape : void 0;
|
|
1889
|
+
}
|
|
1890
|
+
//#endregion
|
|
1680
1891
|
//#region src/integrations/mcp/route-tools.ts
|
|
1681
1892
|
/**
|
|
1682
1893
|
* Custom-route → MCP tool generation.
|
|
@@ -1737,6 +1948,7 @@ function createCustomRouteHandler(route, controller, hasId, options) {
|
|
|
1737
1948
|
const { resourceName, operationName, permissions, pipeline, fields, wiring } = options;
|
|
1738
1949
|
const pipelineSteps = resolvePipelineSteps(pipeline, operationName);
|
|
1739
1950
|
const contextExtras = wiring ? {
|
|
1951
|
+
tenantMiddlewares: options.tenantMiddlewares,
|
|
1740
1952
|
arc: {
|
|
1741
1953
|
resourceName,
|
|
1742
1954
|
schemaOptions: options.schemaOptions,
|
|
@@ -1751,7 +1963,7 @@ function createCustomRouteHandler(route, controller, hasId, options) {
|
|
|
1751
1963
|
audit: wiring.audit,
|
|
1752
1964
|
log: wiring.log
|
|
1753
1965
|
}
|
|
1754
|
-
} :
|
|
1966
|
+
} : { tenantMiddlewares: options.tenantMiddlewares };
|
|
1755
1967
|
return async (input, _ctx) => {
|
|
1756
1968
|
const session = _ctx.session;
|
|
1757
1969
|
const permResult = await evaluatePermission(permissions, session, resourceName, operationName, input);
|
|
@@ -1763,6 +1975,7 @@ function createCustomRouteHandler(route, controller, hasId, options) {
|
|
|
1763
1975
|
});
|
|
1764
1976
|
try {
|
|
1765
1977
|
const reqCtx = buildRequestContext(input, session, operationKindForRoute(route.method, hasId), permResult?.policy, permResult?.scope, contextExtras);
|
|
1978
|
+
if (options.allow && !await options.allow(session, reqCtx.metadata._scope)) return rateLimitedResult(operationName);
|
|
1766
1979
|
const emit = (envelope) => {
|
|
1767
1980
|
const data = applyMcpReadMasking(envelope.data, {
|
|
1768
1981
|
fields,
|
|
@@ -1881,10 +2094,18 @@ function resourceToTools(resource, config = {}) {
|
|
|
1881
2094
|
const tools = [];
|
|
1882
2095
|
const prefix = config.toolNamePrefix;
|
|
1883
2096
|
const wiring = config.wiring;
|
|
2097
|
+
assertMcpIdempotencyFencing(wiring);
|
|
2098
|
+
const hasIdempotencyStore = idempotencyStoreOf(wiring?.idempotencyStore) !== void 0;
|
|
2099
|
+
const keyRequiredOps = new Set(resource.idempotency?.required ?? []);
|
|
2100
|
+
const rateLimit = resource.rateLimit;
|
|
2101
|
+
const rateLimiter = config.wiring?.rateLimiter ?? createMemoryResourceRateLimiter();
|
|
1884
2102
|
if (controller) {
|
|
1885
2103
|
let ops = ALL_CRUD_OPS.filter((op) => !resource.disabledRoutes?.includes(op));
|
|
1886
2104
|
if (config.operations) ops = ops.filter((op) => config.operations?.includes(op));
|
|
1887
2105
|
for (const op of ops) {
|
|
2106
|
+
if (!resource.permissions?.[op]) continue;
|
|
2107
|
+
const keyRequired = keyRequiredOps.has(op);
|
|
2108
|
+
if (keyRequired && !hasIdempotencyStore) throw new Error(`[Arc/MCP] Resource '${resource.name}': '${op}' declares idempotency.required but no idempotency store is wired — register idempotencyPlugin before mcpPlugin, or the key would be demanded and never deduplicated.`);
|
|
1888
2109
|
const name = config.names?.[op] ?? (op === "list" ? `${prefix ? `${prefix}_` : ""}list_${pluralize(resource.name)}` : `${prefix ? `${prefix}_` : ""}${op}_${resource.name}`);
|
|
1889
2110
|
const defaultDescription = defaultCrudDescription(op, resource.displayName, hasSoftDelete, {
|
|
1890
2111
|
filterableFields,
|
|
@@ -1900,7 +2121,8 @@ function resourceToTools(resource, config = {}) {
|
|
|
1900
2121
|
maxLimit,
|
|
1901
2122
|
adapterBodies
|
|
1902
2123
|
});
|
|
1903
|
-
if (
|
|
2124
|
+
if (keyRequired) inputSchema._idempotencyKey = z.string().min(1).describe("Required idempotency key — reuse it on a retry; the first result replays.");
|
|
2125
|
+
else if (hasIdempotencyStore && 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.");
|
|
1904
2126
|
tools.push({
|
|
1905
2127
|
name,
|
|
1906
2128
|
description: resolveCrudDescription(config.descriptions?.[op], {
|
|
@@ -1913,11 +2135,15 @@ function resourceToTools(resource, config = {}) {
|
|
|
1913
2135
|
sortableFields
|
|
1914
2136
|
}),
|
|
1915
2137
|
annotations: CRUD_ANNOTATIONS[op],
|
|
1916
|
-
inputSchema,
|
|
2138
|
+
inputSchema: op === "list" || op === "get" ? z.strictObject(inputSchema) : inputSchema,
|
|
1917
2139
|
handler: createCrudHandler(op, controller, resource.name, resource.permissions, resource.fields, {
|
|
1918
2140
|
wiring,
|
|
2141
|
+
tenantMiddlewares: resource.middlewares?.[op],
|
|
1919
2142
|
schemaOptions: resource.schemaOptions,
|
|
1920
|
-
idField: resource.idField
|
|
2143
|
+
idField: resource.idField,
|
|
2144
|
+
keyRequired,
|
|
2145
|
+
allow: createToolRateLimiter(rateLimit, resource.name, rateLimiter),
|
|
2146
|
+
toolName: name
|
|
1921
2147
|
}),
|
|
1922
2148
|
source: `crud:${resource.name}:${op}`
|
|
1923
2149
|
});
|
|
@@ -1941,27 +2167,26 @@ function resourceToTools(resource, config = {}) {
|
|
|
1941
2167
|
const hasId = route.path.includes(":id");
|
|
1942
2168
|
const mcpConfig = typeof route.mcp === "object" && route.mcp !== null ? route.mcp : void 0;
|
|
1943
2169
|
const toolDescription = mcpConfig?.description ?? route.summary ?? route.description ?? `${opName} on ${resource.displayName}`;
|
|
1944
|
-
const toolAnnotations = mcpConfig?.annotations ? { ...mcpConfig.annotations } : { openWorldHint: true };
|
|
1945
|
-
const inputShape =
|
|
1946
|
-
if (hasId) inputShape.id = z.string().describe("Resource ID");
|
|
1947
|
-
const routeSchema = route.schema;
|
|
1948
|
-
if (routeSchema?.body) {
|
|
1949
|
-
const ir = normalizeSchemaIR(routeSchema.body);
|
|
1950
|
-
for (const [key, val] of Object.entries(schemaIRToZodShape(ir))) inputShape[key] = val;
|
|
1951
|
-
}
|
|
1952
|
-
if (routeSchema?.querystring) {
|
|
1953
|
-
const ir = normalizeSchemaIR(routeSchema.querystring);
|
|
1954
|
-
for (const [key, val] of Object.entries(schemaIRToZodShape(ir))) if (!(key in inputShape)) inputShape[key] = val;
|
|
1955
|
-
}
|
|
2170
|
+
const toolAnnotations = mcpConfig?.annotations ? { ...mcpConfig.annotations } : route.method.toUpperCase() === "GET" ? { readOnlyHint: true } : { openWorldHint: true };
|
|
2171
|
+
const inputShape = buildRouteInputSchema(route, hasId);
|
|
1956
2172
|
const toolName = prefix ? `${prefix}_${opName}_${resource.name}` : `${opName}_${resource.name}`;
|
|
1957
2173
|
const routeSource = route._presetSource ? `preset:${route._presetSource}:${resource.name}:${opName}` : `route:${resource.name}:${route.method} ${route.path}`;
|
|
2174
|
+
const routePolicy = route.rateLimit !== void 0 ? route.rateLimit : rateLimit;
|
|
2175
|
+
const allowRoute = createToolRateLimiter(routePolicy && typeof routePolicy === "object" ? {
|
|
2176
|
+
...routePolicy,
|
|
2177
|
+
...route.rateLimit ? { bucket: `route:${route.method}:${route.path}` } : {}
|
|
2178
|
+
} : routePolicy, resource.name, rateLimiter);
|
|
2179
|
+
const passthrough = mcpHandler ? createMcpHandlerPassthrough(mcpHandler) : void 0;
|
|
1958
2180
|
tools.push({
|
|
1959
2181
|
name: toolName,
|
|
1960
2182
|
description: toolDescription,
|
|
1961
2183
|
annotations: toolAnnotations,
|
|
1962
2184
|
inputSchema: inputShape,
|
|
1963
2185
|
source: routeSource,
|
|
1964
|
-
handler:
|
|
2186
|
+
handler: passthrough ? async (input, ctx) => {
|
|
2187
|
+
if (allowRoute && !await allowRoute(ctx.session)) return rateLimitedResult(toolName);
|
|
2188
|
+
return passthrough(input, ctx);
|
|
2189
|
+
} : createCustomRouteHandler({
|
|
1965
2190
|
handler: resolvedRouteHandler,
|
|
1966
2191
|
operation: route.operation,
|
|
1967
2192
|
method: route.method,
|
|
@@ -1969,48 +2194,70 @@ function resourceToTools(resource, config = {}) {
|
|
|
1969
2194
|
}, controller, hasId, {
|
|
1970
2195
|
resourceName: resource.name,
|
|
1971
2196
|
operationName: opName,
|
|
2197
|
+
allow: allowRoute,
|
|
1972
2198
|
permissions: route.permissions,
|
|
1973
2199
|
pipeline: resource.pipe,
|
|
1974
2200
|
fields: resource.fields,
|
|
1975
2201
|
wiring,
|
|
2202
|
+
tenantMiddlewares: route.tenantScope ? resource.middlewares?.tenantScope : void 0,
|
|
1976
2203
|
schemaOptions: resource.schemaOptions,
|
|
1977
2204
|
idField: resource.idField
|
|
1978
2205
|
})
|
|
1979
2206
|
});
|
|
1980
2207
|
}
|
|
1981
|
-
if (resource.actions)
|
|
1982
|
-
const
|
|
1983
|
-
|
|
1984
|
-
const
|
|
1985
|
-
const
|
|
1986
|
-
const
|
|
1987
|
-
const
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
const
|
|
1992
|
-
|
|
2208
|
+
if (resource.actions) {
|
|
2209
|
+
const actionConfig = normalizeActionsToRouterConfig(resource.actions, resource.actionPermissions, resource.tag, resource.permissions, resource.name, void 0);
|
|
2210
|
+
const fence = resolveActionTargetFence(resource, actionConfig);
|
|
2211
|
+
const handlerScoped = new Set(actionConfig.handlerScopedActionNames);
|
|
2212
|
+
const keyRequiredActions = new Set(actionConfig.idempotentActionNames);
|
|
2213
|
+
const allow = createToolRateLimiter(rateLimit, resource.name, rateLimiter);
|
|
2214
|
+
for (const [actionName, entry] of Object.entries(resource.actions)) {
|
|
2215
|
+
const def = typeof entry === "function" ? { handler: entry } : entry;
|
|
2216
|
+
if (typeof def !== "function" && "mcp" in def && def.mcp === false) continue;
|
|
2217
|
+
const mcpCfg = typeof def !== "function" && typeof def.mcp === "object" ? def.mcp : void 0;
|
|
2218
|
+
const description = mcpCfg?.description ?? (typeof def !== "function" ? def.description : void 0) ?? `${actionName} action on ${resource.displayName}`;
|
|
2219
|
+
const annotations = mcpCfg?.annotations ? { ...mcpCfg.annotations } : { destructiveHint: true };
|
|
2220
|
+
const idLess = typeof def !== "function" && def.id === false;
|
|
2221
|
+
const inputShape = idLess ? {} : { id: z.string().describe("Resource ID") };
|
|
2222
|
+
const rawSchema = typeof def !== "function" ? def.schema : void 0;
|
|
2223
|
+
if (rawSchema && typeof rawSchema === "object") {
|
|
2224
|
+
const converted = convertActionSchemaToZod(rawSchema);
|
|
2225
|
+
for (const [key, val] of Object.entries(converted)) inputShape[key] = val;
|
|
2226
|
+
}
|
|
2227
|
+
const toolName = prefix ? `${prefix}_${actionName}_${resource.name}` : `${actionName}_${resource.name}`;
|
|
2228
|
+
const keyRequired = keyRequiredActions.has(actionName);
|
|
2229
|
+
const handlerDedup = actionConfig.handlerDedupOwners[actionName] !== void 0;
|
|
2230
|
+
if (keyRequired && !handlerDedup && !hasIdempotencyStore) throw new Error(`[Arc/MCP] Resource '${resource.name}': action '${actionName}' declares idempotency.required but no idempotency store is wired — register idempotencyPlugin before mcpPlugin, or the key would be demanded and never deduplicated.`);
|
|
2231
|
+
if (keyRequired) inputShape._idempotencyKey = z.string().min(1).describe("Required idempotency key — reuse it on a retry; the first result replays.");
|
|
2232
|
+
const handler = typeof entry === "function" ? entry : def.handler;
|
|
2233
|
+
const actionPerms = resolveActionPermission({
|
|
2234
|
+
action: entry,
|
|
2235
|
+
resourcePermissions: resource.permissions,
|
|
2236
|
+
resourceActionPermissions: resource.actionPermissions
|
|
2237
|
+
});
|
|
2238
|
+
if (!actionPerms) throw new Error(`[Arc/MCP] Resource '${resource.name}': action '${actionName}' has no permission gate and the resource defines no \`permissions.update\` fallback. Declare one of:\n - \`actions.${actionName}.permissions: <PermissionCheck>\` (per-action)\n - \`actionPermissions: <PermissionCheck>\` (resource-wide)\n - \`permissions.update: <PermissionCheck>\` (inherited by actions)\nUse \`allowPublic()\` if you genuinely want the action unauthenticated.`);
|
|
2239
|
+
tools.push({
|
|
2240
|
+
name: toolName,
|
|
2241
|
+
description: String(description),
|
|
2242
|
+
annotations,
|
|
2243
|
+
inputSchema: inputShape,
|
|
2244
|
+
source: `action:${resource.name}:${actionName}`,
|
|
2245
|
+
handler: createActionToolHandler(actionName, handler, actionPerms, resource.name, resource.permissions, rawSchema, idLess, resource.fields, {
|
|
2246
|
+
wiring,
|
|
2247
|
+
tenantMiddlewares: withoutTenantInjection(resource.middlewares?.tenantScope),
|
|
2248
|
+
schemaOptions: resource.schemaOptions,
|
|
2249
|
+
idField: resource.idField,
|
|
2250
|
+
parity: {
|
|
2251
|
+
toolName,
|
|
2252
|
+
keyRequired,
|
|
2253
|
+
handlerDedup,
|
|
2254
|
+
...fence && !idLess && !handlerScoped.has(actionName) ? { fence } : {},
|
|
2255
|
+
audit: Boolean(resource.audit),
|
|
2256
|
+
...allow ? { allow } : {}
|
|
2257
|
+
}
|
|
2258
|
+
})
|
|
2259
|
+
});
|
|
1993
2260
|
}
|
|
1994
|
-
const toolName = prefix ? `${prefix}_${actionName}_${resource.name}` : `${actionName}_${resource.name}`;
|
|
1995
|
-
const handler = typeof entry === "function" ? entry : def.handler;
|
|
1996
|
-
const actionPerms = resolveActionPermission({
|
|
1997
|
-
action: entry,
|
|
1998
|
-
resourcePermissions: resource.permissions,
|
|
1999
|
-
resourceActionPermissions: resource.actionPermissions
|
|
2000
|
-
});
|
|
2001
|
-
if (!actionPerms) throw new Error(`[Arc/MCP] Resource '${resource.name}': action '${actionName}' has no permission gate and the resource defines no \`permissions.update\` fallback. Declare one of:\n - \`actions.${actionName}.permissions: <PermissionCheck>\` (per-action)\n - \`actionPermissions: <PermissionCheck>\` (resource-wide)\n - \`permissions.update: <PermissionCheck>\` (inherited by actions)\nUse \`allowPublic()\` if you genuinely want the action unauthenticated.`);
|
|
2002
|
-
tools.push({
|
|
2003
|
-
name: toolName,
|
|
2004
|
-
description: String(description),
|
|
2005
|
-
annotations,
|
|
2006
|
-
inputSchema: inputShape,
|
|
2007
|
-
source: `action:${resource.name}:${actionName}`,
|
|
2008
|
-
handler: createActionToolHandler(actionName, handler, actionPerms, resource.name, resource.permissions, rawSchema, idLess, resource.fields, {
|
|
2009
|
-
wiring,
|
|
2010
|
-
schemaOptions: resource.schemaOptions,
|
|
2011
|
-
idField: resource.idField
|
|
2012
|
-
})
|
|
2013
|
-
});
|
|
2014
2261
|
}
|
|
2015
2262
|
if (resource.aggregations && Object.keys(resource.aggregations).length > 0) {
|
|
2016
2263
|
const repoForAgg = resource.controller?.repository;
|
|
@@ -2036,6 +2283,7 @@ function resourceToTools(resource, config = {}) {
|
|
|
2036
2283
|
schemaOptions: resource.schemaOptions,
|
|
2037
2284
|
repo: repoForAgg,
|
|
2038
2285
|
buildOptionsFromSession,
|
|
2286
|
+
tenantMiddlewares: resource.middlewares?.aggregations,
|
|
2039
2287
|
prefix,
|
|
2040
2288
|
fields: resource.fields
|
|
2041
2289
|
}));
|
|
@@ -2247,7 +2495,14 @@ const mcpPluginImpl = async (fastify, options) => {
|
|
|
2247
2495
|
exclude: options.exclude
|
|
2248
2496
|
});
|
|
2249
2497
|
const fastifyAny = fastify;
|
|
2498
|
+
const rateLimiter = resolveResourceRateLimiter(fastify) ?? createMemoryResourceRateLimiter();
|
|
2499
|
+
if (enabledResources.some((resource) => resource.rateLimit)) declareRuntimeCapability(fastify, {
|
|
2500
|
+
subsystem: "mcp.resource-rate-limit",
|
|
2501
|
+
durability: rateLimiter.durability,
|
|
2502
|
+
detail: "REST and MCP resource budgets require a shared rate-limit backend across replicas."
|
|
2503
|
+
});
|
|
2250
2504
|
const wiring = {
|
|
2505
|
+
rateLimiter,
|
|
2251
2506
|
get hooks() {
|
|
2252
2507
|
return fastifyAny.arc?.hooks;
|
|
2253
2508
|
},
|
|
@@ -2260,6 +2515,15 @@ const mcpPluginImpl = async (fastify, options) => {
|
|
|
2260
2515
|
get log() {
|
|
2261
2516
|
return fastify.log;
|
|
2262
2517
|
},
|
|
2518
|
+
get allowUnfencedCompletion() {
|
|
2519
|
+
return fastifyAny.idempotency?.allowUnfencedCompletion;
|
|
2520
|
+
},
|
|
2521
|
+
get idempotencyTtlMs() {
|
|
2522
|
+
return fastifyAny.idempotency?.ttlMs;
|
|
2523
|
+
},
|
|
2524
|
+
get idempotencyLockTimeoutMs() {
|
|
2525
|
+
return fastifyAny.idempotency?.lockTimeoutMs;
|
|
2526
|
+
},
|
|
2263
2527
|
get idempotencyStore() {
|
|
2264
2528
|
return fastifyAny.idempotency?.store;
|
|
2265
2529
|
}
|
|
@@ -2526,11 +2790,10 @@ function registerStatefulRoutes(fastify, prefix, options, cache, createServer, T
|
|
|
2526
2790
|
});
|
|
2527
2791
|
}
|
|
2528
2792
|
/**
|
|
2529
|
-
* Resolve `expose` / `
|
|
2530
|
-
*
|
|
2793
|
+
* Resolve `expose` / `exclude` into the filtered resource list `mcpPlugin`
|
|
2794
|
+
* should surface. Two mutually exclusive intents:
|
|
2531
2795
|
*
|
|
2532
2796
|
* - `expose`: default-deny allowlist (preferred — new resources auto-deny).
|
|
2533
|
-
* - `include`: legacy alias for `expose`, kept for back-compat.
|
|
2534
2797
|
* - `exclude`: default-allow opt-out (drift-prone — new resources auto-leak).
|
|
2535
2798
|
*
|
|
2536
2799
|
* Conflicting combinations throw with an actionable message so a host that
|
|
@@ -2564,4 +2827,4 @@ const mcpPlugin = fp(mcpPluginImpl, {
|
|
|
2564
2827
|
fastify: "5.x"
|
|
2565
2828
|
});
|
|
2566
2829
|
//#endregion
|
|
2567
|
-
export {
|
|
2830
|
+
export { resolveToolCollisions as _, toolInputShape as a, resolveCrudDescription as c, permissionDeniedResult as d, toCallToolError as f, createMcpServer as g, buildRequestContext as h, isZodTypeSchema as i, invokeController as l, toCallToolSuccess as m, mcpPlugin as n, fieldRulesToZod as o, toCallToolResult as p, resourceToTools as r, defaultCrudDescription as s, filterResourcesForMcp as t, mcpHandlerAdapter as u };
|