@classytic/arc 2.41.1 → 2.44.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-DZWDsaPa.mjs} +22 -17
- package/dist/{HookSystem-CMF94SEA.mjs → HookSystem-yjEgn7Cm.mjs} +2 -2
- package/dist/{QueryCache-DFA-QrU9.d.mts → QueryCache--5uAuYPW.d.mts} +7 -3
- package/dist/{ResourceRegistry-MGpXoxHZ.mjs → ResourceRegistry-DJxHUpE4.mjs} +44 -13
- package/dist/acquire-BozJ-9yt.mjs +35 -0
- package/dist/actionPermissions-5N04zBL4.mjs +772 -0
- package/dist/audit/index.d.mts +46 -8
- package/dist/audit/index.mjs +48 -26
- package/dist/auth/audit.d.mts +8 -8
- package/dist/auth/audit.mjs +1 -1
- package/dist/auth/index.d.mts +46 -16
- package/dist/auth/index.mjs +134 -156
- package/dist/auth/redis-session.d.mts +4 -5
- package/dist/auth/redis-session.mjs +1 -1
- package/dist/{authorizationDecision-DawKGoXb.mjs → authorizationDecision-wX3SA9w_.mjs} +7 -102
- package/dist/{base-CxrV1LmG.d.mts → base-CZyixEQR.d.mts} +27 -5
- package/dist/boundedMap-DWiKW7lP.mjs +57 -0
- package/dist/{buildHandler-CvxKOc0m.mjs → buildHandler-Bswu_vNb.mjs} +4 -4
- package/dist/cache/index.d.mts +31 -43
- package/dist/cache/index.mjs +4 -4
- package/dist/{caching-DbCBxlgw.mjs → caching-Drp68DIS.mjs} +3 -3
- package/dist/claimed-DYvQoZLv.mjs +8 -0
- package/dist/cleanup/index.d.mts +18 -22
- package/dist/cleanup/index.mjs +27 -73
- package/dist/cli/commands/describe.d.mts +7 -5
- package/dist/cli/commands/describe.mjs +7 -4
- package/dist/cli/commands/docs.d.mts +2 -3
- package/dist/cli/commands/docs.mjs +3 -3
- package/dist/cli/commands/doctor.d.mts +2 -3
- package/dist/cli/commands/doctor.mjs +1 -1
- package/dist/cli/commands/generate.mjs +2 -3
- package/dist/cli/commands/init.mjs +2 -3
- package/dist/cli/commands/introspect.d.mts +2 -3
- package/dist/cli/commands/introspect.mjs +2 -2
- package/dist/cli/index.mjs +3 -3
- package/dist/{constants-D5Ubn1fe.mjs → constants-B2QHb6u6.mjs} +4 -2
- package/dist/context/index.d.mts +11 -14
- 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-COcqo0Cp.mjs} +15 -5
- package/dist/{createActionRouter-DNqoaK81.mjs → createActionRouter-zG13rpPJ.mjs} +97 -39
- package/dist/{createAggregationRouter-BcbQJf10.mjs → createAggregationRouter-CWRz96Vr.mjs} +11 -8
- package/dist/{preloadResources-f3boenVZ.mjs → createApp-BGLYtErt.mjs} +305 -394
- package/dist/{createMcpServer-CbDWkZ1T.d.mts → createMcpServer-DSztofa9.d.mts} +1 -1
- package/dist/{defineAggregation-D_eJjm5A.mjs → defineAggregation--3xguLkk.mjs} +1 -1
- package/dist/{defineResource-DxkMTjRn.mjs → defineResource-gUp9ug66.mjs} +118 -1024
- package/dist/discovery/index.d.mts +6 -7
- package/dist/discovery/index.mjs +1 -1
- package/dist/docs/index.d.mts +11 -5
- package/dist/docs/index.mjs +4 -4
- package/dist/{elevation-CkvG0LG2.mjs → elevation-BG4gnMsJ.mjs} +41 -12
- package/dist/{elevation-BXCfVcNG.d.mts → elevation-BW2J3tNj.d.mts} +1 -2
- package/dist/encryption/index.d.mts +5 -5
- package/dist/encryption/index.mjs +3 -3
- package/dist/{errorHandler-D1GHKGlV.mjs → errorHandler-COHw3N1b.mjs} +26 -5
- package/dist/{errors-BH7YU_9O.mjs → errors-CW4anJBy.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-BweHAVws.mjs} +82 -21
- package/dist/{subscribe-helpers-DatDkcuG.mjs → eventTypes-N4-omQFR.mjs} +151 -17
- package/dist/events/index.d.mts +43 -18
- 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 +5 -6
- package/dist/events/transports/redis.mjs +2 -2
- package/dist/factory/index.d.mts +42 -27
- package/dist/factory/index.mjs +213 -32
- package/dist/{fastify-augmentation-Cq2nHVaW.d.mts → fastify-augmentation-DRlCIWE9.d.mts} +11 -25
- package/dist/{fields-BHrkoF5y.mjs → fields-A5crhsJb.mjs} +1 -1
- package/dist/{fields-I_EeRWND.d.mts → fields-BBSpf8pS.d.mts} +1 -1
- package/dist/filter-merge-DGM-GQ1J.mjs +99 -0
- package/dist/{generate-BLRf2m3y.mjs → generate-BXKo2Gc8.mjs} +2 -2
- package/dist/{headers-BIXWKrzQ.mjs → headers-DKZQKL-Z.mjs} +2 -2
- package/dist/health-BYAwA6YS.mjs +22 -0
- package/dist/{health-ZRHeDHXH.mjs → health-CrqUp3sz.mjs} +115 -55
- package/dist/hooks/index.d.mts +2 -2
- package/dist/hooks/index.mjs +2 -3
- package/dist/idempotency/index.d.mts +36 -10
- package/dist/idempotency/index.mjs +230 -111
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/idempotency/redis.mjs +77 -12
- package/dist/{index-D4u76eQW.d.mts → index-Beg3aNnK.d.mts} +23 -101
- package/dist/{preloadResources-CjS2OfpU.d.mts → index-BkV55WlV.d.mts} +37 -38
- package/dist/{index-BHdZD8zK.d.mts → index-Bxriq3so.d.mts} +84 -21
- package/dist/index-CUJvoyw3.d.mts +510 -0
- package/dist/{index-CbJiYrfv.d.mts → index-Chgz3d0i.d.mts} +1 -1
- package/dist/{index-DHbyPo5j.d.mts → index-WH7xgvxS.d.mts} +304 -254
- package/dist/index.d.mts +9 -9
- package/dist/index.mjs +11 -11
- package/dist/{init-CpLkjU18.mjs → init-BPzK5Lnr.mjs} +1 -1
- package/dist/integrations/event-gateway.d.mts +11 -7
- 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 +102 -89
- package/dist/integrations/mcp/index.mjs +19 -4
- package/dist/integrations/mcp/testing.d.mts +8 -9
- package/dist/integrations/mcp/testing.mjs +2 -2
- package/dist/integrations/streamline.d.mts +85 -144
- package/dist/integrations/streamline.mjs +131 -55
- package/dist/integrations/webhooks.d.mts +83 -143
- package/dist/integrations/webhooks.mjs +36 -80
- package/dist/integrations/websocket-pushref-redis.d.mts +5 -6
- package/dist/integrations/websocket-pushref-redis.mjs +1 -1
- package/dist/integrations/websocket-redis.d.mts +5 -6
- 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-JJzwH4Ro.d.mts} +34 -2
- package/dist/{jsonBody-ELR9lfeU.mjs → jsonBody-YIq-vGLv.mjs} +2 -2
- package/dist/{keys-cpZCtcIE.mjs → keys-CpoiZ8hU.mjs} +7 -18
- package/dist/{loadResources-BPfWiI5s.mjs → loadResources-6jO52-RR.mjs} +3 -3
- package/dist/{loadResourcesFromEntry-ixtfL3J2.mjs → loadResourcesFromEntry-MP8xEppu.mjs} +2 -2
- package/dist/logger/index.d.mts +9 -17
- package/dist/logger/index.mjs +2 -153
- package/dist/logger-D4V1zGLI.mjs +160 -0
- package/dist/{mcpPlugin-BX5PujI0.mjs → mcpPlugin-srPc7ItN.mjs} +446 -170
- package/dist/{memory-Iotz0cCx.mjs → memory-CTa5pOCW.mjs} +36 -34
- package/dist/{metrics-NMsmpRmY.mjs → metrics-DsZrvnpB.mjs} +39 -72
- package/dist/middleware/index.d.mts +5 -5
- package/dist/middleware/index.mjs +3 -3
- package/dist/migrations/index.d.mts +13 -13
- package/dist/migrations/index.mjs +2 -2
- package/dist/{module-DpLJG2_N.mjs → module--L0iBIZz.mjs} +8 -8
- package/dist/mongodb-memory-server-CHhYSq5y.mjs +35539 -0
- package/dist/mongoose-CYJCLyi-.mjs +67451 -0
- package/dist/multiTenant-CAtYlW6H.d.mts +104 -0
- package/dist/{multipartBody-DtRWM6_U.mjs → multipartBody-Cc7eZ9cq.mjs} +46 -7
- package/dist/node-D8UZv_8c.mjs +41236 -0
- package/dist/{openapi-CmFf3yq_.mjs → openapi-CMegfsm8.mjs} +48 -10
- package/dist/outbox-admin/index.d.mts +5 -5
- package/dist/outbox-admin/index.mjs +6 -18
- package/dist/{outbox-module-D2z5WkDY.mjs → outbox-module-Ryk270jd.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-C5ijIz5Q.mjs} +149 -33
- package/dist/{pipe-FMWNheSO.mjs → pipe-D81zMtWs.mjs} +2 -2
- package/dist/pipeline/index.d.mts +7 -7
- package/dist/pipeline/index.mjs +2 -2
- package/dist/plan-CJGMKoui.mjs +565 -0
- package/dist/{BaseController-CYEwjyjN.mjs → plugin-DALd9dcF.mjs} +1269 -346
- package/dist/plugins/index.d.mts +17 -14
- package/dist/plugins/index.mjs +44 -22
- package/dist/plugins/response-cache.d.mts +5 -6
- package/dist/plugins/response-cache.mjs +8 -14
- package/dist/plugins/tracing-entry.mjs +2 -2
- package/dist/{pluralize-CXWZIzTv.mjs → pluralize-COUNYo3d.mjs} +1 -1
- package/dist/presets/filesUpload.d.mts +8 -8
- package/dist/presets/filesUpload.mjs +5 -5
- package/dist/presets/index.d.mts +35 -40
- 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 +9 -10
- package/dist/presets/search.mjs +2 -2
- package/dist/{presets-ZJSPgmT2.mjs → presets-CONf2Qem.mjs} +99 -107
- package/dist/{projection-BzIgzoKZ.mjs → projection-wFoLqdss.mjs} +2 -2
- package/dist/{pushref-registry-Dov9l4qE.d.mts → pushref-registry-UxCkoA4C.d.mts} +1 -1
- package/dist/{queryCachePlugin-DZHPOm9p.mjs → queryCachePlugin-BATECpyh.mjs} +12 -14
- package/dist/{queryCachePlugin-BzqxconG.d.mts → queryCachePlugin-C2lZjTxk.d.mts} +1 -1
- package/dist/{rateLimitKey-BvOvcBDi.mjs → rateLimitKey-DVEZ_UpV.mjs} +1 -1
- package/dist/{realtime-8tIQWZz4.mjs → realtime-xyubMKSB.mjs} +25 -21
- package/dist/{redis-Bzi1y3F9.d.mts → redis-BDgv9Glw.d.mts} +17 -24
- package/dist/redis-stream-BMk-bpQS.d.mts +219 -0
- package/dist/redisShapes-CQksY67q.d.mts +55 -0
- package/dist/registry/index.d.mts +15 -10
- package/dist/registry/index.mjs +4 -4
- package/dist/{registry-DNkgGQcK.mjs → registry-Bdzg-hdm.mjs} +13 -10
- package/dist/{renewingLease-yF8BdW4b.mjs → renewingLease-BFJxGPuh.mjs} +1 -1
- package/dist/{replyHelpers-CNdvMfM4.mjs → replyHelpers-Bnc6lJyy.mjs} +2 -2
- package/dist/{requestContext-cDkAUr-P.mjs → requestContext-C10UeFe5.mjs} +1 -1
- package/dist/requirement-Bu50QALH.mjs +35 -0
- package/dist/requirement-CbiLdQ1n.d.mts +22 -0
- package/dist/resourceRateLimit-Wf6d6FL9.d.mts +30 -0
- package/dist/resourceRateLimit-urWPJxrt.mjs +180 -0
- package/dist/rolldown-runtime-MYrLNGnq.mjs +45 -0
- package/dist/routeValidator-DHtpzrzk.mjs +70 -0
- package/dist/{runtimeCapabilities-CkhS6jn6.mjs → runtimeCapabilities-DIMKMV2G.mjs} +1 -1
- package/dist/{schedules-j6uXXAeJ.mjs → schedules-CKTs5ys8.mjs} +46 -37
- package/dist/{schemaConverter-BVA7cFRG.mjs → schemaConverter-BVjkQ5DD.mjs} +2 -2
- package/dist/{schemaIR-CTvPDoPB.mjs → schemaIR-BA0Hb21Y.mjs} +2 -2
- package/dist/schemas/index.d.mts +20 -10
- package/dist/schemas/index.mjs +8 -3
- package/dist/scim/index.d.mts +13 -13
- package/dist/scim/index.mjs +2 -2
- package/dist/scope/index.d.mts +4 -4
- package/dist/scope/index.mjs +6 -6
- package/dist/{scope-BUi1xV1I.mjs → scope-DINaj7Ax.mjs} +4 -4
- package/dist/shutdown-LpLMH8T1.mjs +59 -0
- package/dist/{simpleEqualityMatcher-C9bzq2gO.mjs → simpleEqualityMatcher-DbwIM1f1.mjs} +1 -1
- package/dist/{sse-BeqVKQKu.mjs → sse-BzJFPX_1.mjs} +11 -15
- package/dist/{sseStream-Ppzo-WUH.mjs → sseStream-DQChzGTs.mjs} +2 -2
- package/dist/{store-helpers-dW71XVWL.mjs → store-helpers-Coj9RhOZ.mjs} +1 -1
- package/dist/{streaming-IcRFjWQU.mjs → streaming-JzB5CAga.mjs} +1 -1
- package/dist/subscriberIdentity-DeyppIif.mjs +28 -0
- package/dist/tenancy-DxV8OuX2.mjs +43 -0
- package/dist/tenantPreparation-CBvvAcAj.mjs +67 -0
- package/dist/testing/cleanupStoreContract.d.mts +4 -5
- package/dist/testing/cleanupStoreContract.mjs +1 -1
- package/dist/testing/index.d.mts +215 -197
- package/dist/testing/index.mjs +289 -214
- package/dist/testing/outboxStoreContract.d.mts +4 -5
- package/dist/testing/outboxStoreContract.mjs +137 -2
- package/dist/testing/requiredPorts.d.mts +16 -0
- package/dist/testing/requiredPorts.mjs +65 -0
- package/dist/testing/storageContract.d.mts +4 -5
- package/dist/testing/storageContract.mjs +1 -1
- package/dist/timing-CntavTlR.mjs +46 -0
- package/dist/{transactionContext-CPbdlnVw.mjs → transactionContext-orvnGu4F.mjs} +1 -1
- package/dist/{typeGuards-BLjPgVBm.mjs → typeGuards-kEaP_WmO.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-DfVDvkdP.d.mts → types-BJ-2XqVF.d.mts} +189 -204
- package/dist/{types-CWQn9-IC.d.mts → types-D3Ixsh00.d.mts} +22 -9
- package/dist/{types-CgenB1gv.mjs → types-fvO6Ovzx.mjs} +4 -4
- package/dist/usage/index.d.mts +5 -6
- package/dist/usage/index.mjs +5 -5
- package/dist/{userHelpers-ChRvkuSZ.mjs → userHelpers-C76zD5pa.mjs} +1 -1
- package/dist/utils/index.d.mts +125 -264
- package/dist/utils/index.mjs +13 -12
- package/dist/{utils-CxcctrAh.mjs → utils-BpkVZ8rB.mjs} +315 -404
- package/dist/{validate-DrYWnOzI.mjs → validate-CWp-lsrm.mjs} +6 -14
- package/dist/{versioning-D6JIK8Tk.d.mts → versioning-Bsb51gGT.d.mts} +28 -13
- package/dist/{versioning-DKkT4hNJ.mjs → versioning-CHhL9P3o.mjs} +4 -4
- package/dist/{websocket-BOXVggMf.d.mts → websocket-Cs4awVyD.d.mts} +1 -1
- package/dist/{workScope-CxKqoIFn.mjs → workScope-n9a7d_lZ.mjs} +2 -2
- package/package.json +57 -59
- 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/rolldown-runtime-eysyzFAF.mjs +0 -15
- package/dist/sync/changelog.d.mts +0 -3
- package/dist/sync/changelog.mjs +0 -3
|
@@ -1,21 +1,29 @@
|
|
|
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.44.0 | MIT | https://classytic.com */
|
|
2
|
+
import { t as arcLog } from "./logger-D4V1zGLI.mjs";
|
|
3
|
+
import { i as NotFoundError, p as isArcError, t as ArcError } from "./errors-CW4anJBy.mjs";
|
|
4
|
+
import { c as BaseController, n as normalizeActionsToRouterConfig, r as resolveActionTargetFence } from "./plugin-DALd9dcF.mjs";
|
|
5
|
+
import { n as requireSingleHeaderValue } from "./headers-DKZQKL-Z.mjs";
|
|
6
|
+
import { b as isElevated, c as getOrgId, g as getUserId, x as isMember } from "./types-fvO6Ovzx.mjs";
|
|
7
|
+
import { a as resolveEffectiveRoles, t as applyFieldReadPermissions } from "./fields-A5crhsJb.mjs";
|
|
8
|
+
import { o as toJsonSchema } from "./schemaConverter-BVjkQ5DD.mjs";
|
|
9
|
+
import { t as buildRequestScopeProjection } from "./projection-wFoLqdss.mjs";
|
|
10
|
+
import { l as resolvePipelineSteps, t as resolveActionPermission } from "./actionPermissions-5N04zBL4.mjs";
|
|
11
|
+
import { a as withoutTenantInjection, r as prepareTenantContext } from "./tenantPreparation-CBvvAcAj.mjs";
|
|
12
|
+
import { r as evaluatePermissionDecision } from "./authorizationDecision-wX3SA9w_.mjs";
|
|
13
|
+
import { n as declareRuntimeCapability } from "./runtimeCapabilities-DIMKMV2G.mjs";
|
|
14
|
+
import { t as executePipeline } from "./pipe-D81zMtWs.mjs";
|
|
15
|
+
import { a as resourceRateLimitWindowMs, i as resolveResourceRateLimiter, t as createMemoryResourceRateLimiter } from "./resourceRateLimit-urWPJxrt.mjs";
|
|
16
|
+
import { n as evictToSize, r as touch, t as assertCapacity } from "./boundedMap-DWiKW7lP.mjs";
|
|
17
|
+
import { t as pluralize } from "./pluralize-COUNYo3d.mjs";
|
|
18
|
+
import { n as createIdempotencyResult, r as operationIdentity, t as resolveIdempotencyTiming } from "./timing-CntavTlR.mjs";
|
|
19
|
+
import { n as acquireIdempotencyKey, r as releaseIdempotencyLock } from "./acquire-BozJ-9yt.mjs";
|
|
20
|
+
import { r as shouldRejectAdditionalProperties, t as normalizeSchemaIR } from "./schemaIR-BA0Hb21Y.mjs";
|
|
21
|
+
import { n as validateAggregations } from "./validate-CWp-lsrm.mjs";
|
|
22
|
+
import { n as executeAggregation } from "./buildHandler-Bswu_vNb.mjs";
|
|
23
|
+
import { ARC_ERROR_CODES, isHttpError, toErrorContract } from "@classytic/repo-core/errors";
|
|
17
24
|
import { createHash, randomUUID } from "node:crypto";
|
|
18
25
|
import fp from "fastify-plugin";
|
|
26
|
+
import { IdempotencyError, fingerprintRequest } from "@classytic/repo-core/idempotency";
|
|
19
27
|
import { z } from "zod";
|
|
20
28
|
//#region src/integrations/mcp/createMcpServer.ts
|
|
21
29
|
/**
|
|
@@ -222,51 +230,55 @@ function buildRequestContext(input, auth, operation, policyFilters, scopeOverrid
|
|
|
222
230
|
},
|
|
223
231
|
...extras?.server ? { server: extras.server } : {}
|
|
224
232
|
};
|
|
233
|
+
const prepared = (ctx) => {
|
|
234
|
+
prepareTenantContext(ctx, extras?.tenantMiddlewares);
|
|
235
|
+
return ctx;
|
|
236
|
+
};
|
|
225
237
|
switch (operation) {
|
|
226
|
-
case "list": return {
|
|
238
|
+
case "list": return prepared({
|
|
227
239
|
...base,
|
|
228
240
|
params: {},
|
|
229
241
|
query: expandOperatorKeys(input),
|
|
230
242
|
body: void 0
|
|
231
|
-
};
|
|
243
|
+
});
|
|
232
244
|
case "get": {
|
|
233
245
|
const { id, ...rest } = input;
|
|
234
|
-
return {
|
|
246
|
+
return prepared({
|
|
235
247
|
...base,
|
|
236
248
|
params: { id: String(id ?? "") },
|
|
237
249
|
query: expandOperatorKeys(rest),
|
|
238
250
|
body: void 0
|
|
239
|
-
};
|
|
251
|
+
});
|
|
240
252
|
}
|
|
241
|
-
case "create": return {
|
|
253
|
+
case "create": return prepared({
|
|
242
254
|
...base,
|
|
243
255
|
params: {},
|
|
244
256
|
query: {},
|
|
245
257
|
body: { ...input }
|
|
246
|
-
};
|
|
258
|
+
});
|
|
247
259
|
case "update": {
|
|
248
260
|
const { id: _id, ...body } = input;
|
|
249
|
-
return {
|
|
261
|
+
return prepared({
|
|
250
262
|
...base,
|
|
251
263
|
params: { id: String(_id ?? "") },
|
|
252
264
|
query: {},
|
|
253
265
|
body
|
|
254
|
-
};
|
|
266
|
+
});
|
|
255
267
|
}
|
|
256
|
-
case "delete": return {
|
|
268
|
+
case "delete": return prepared({
|
|
257
269
|
...base,
|
|
258
270
|
params: { id: String(input.id ?? "") },
|
|
259
271
|
query: {},
|
|
260
272
|
body: void 0
|
|
261
|
-
};
|
|
273
|
+
});
|
|
262
274
|
case "action": {
|
|
263
275
|
const { id: _actionId, ...actionBody } = input;
|
|
264
|
-
return {
|
|
276
|
+
return prepared({
|
|
265
277
|
...base,
|
|
266
278
|
params: { id: String(_actionId ?? "") },
|
|
267
279
|
query: {},
|
|
268
280
|
body: actionBody
|
|
269
|
-
};
|
|
281
|
+
});
|
|
270
282
|
}
|
|
271
283
|
}
|
|
272
284
|
}
|
|
@@ -563,36 +575,137 @@ function createMcpController(resource) {
|
|
|
563
575
|
});
|
|
564
576
|
}
|
|
565
577
|
//#endregion
|
|
566
|
-
//#region src/integrations/mcp/
|
|
578
|
+
//#region src/integrations/mcp/http-parity.ts
|
|
567
579
|
/**
|
|
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.
|
|
580
|
+
* The gates an HTTP route applies that an MCP tool must apply too, because a tool calls the
|
|
581
|
+
* controller or action handler directly and skips the route: idempotent execution, the
|
|
582
|
+
* key-required refusal, the per-caller rate limit and the action audit record.
|
|
595
583
|
*/
|
|
584
|
+
function isIdempotencyStore(value) {
|
|
585
|
+
if (typeof value !== "object" || value === null) return false;
|
|
586
|
+
const s = value;
|
|
587
|
+
return typeof s.get === "function" && typeof s.set === "function" && typeof s.tryLock === "function" && typeof s.unlock === "function";
|
|
588
|
+
}
|
|
589
|
+
/** The wired store, when it is one. */
|
|
590
|
+
function idempotencyStoreOf(store) {
|
|
591
|
+
return isIdempotencyStore(store) ? store : void 0;
|
|
592
|
+
}
|
|
593
|
+
function isFencedStore(store) {
|
|
594
|
+
return typeof store.setIfLocked === "function" && store.supportsFencedCompletion !== false;
|
|
595
|
+
}
|
|
596
|
+
/** Refuse an unsafe standalone store; an explicit migration escape is logged at wiring. */
|
|
597
|
+
function assertMcpIdempotencyFencing(wiring) {
|
|
598
|
+
resolveIdempotencyTiming(wiring?.idempotencyTtlMs, wiring?.idempotencyLockTimeoutMs);
|
|
599
|
+
const store = idempotencyStoreOf(wiring?.idempotencyStore);
|
|
600
|
+
if (!store || isFencedStore(store)) return;
|
|
601
|
+
const message = "[Arc/MCP] Idempotency store must implement atomic setIfLocked completion fencing. Upgrade the store or explicitly allowUnfencedCompletion during migration.";
|
|
602
|
+
if (wiring?.allowUnfencedCompletion !== true) throw new Error(message);
|
|
603
|
+
const logger = wiring.log && typeof wiring.log === "object" ? wiring.log : {};
|
|
604
|
+
const warning = "[Arc/MCP] allowUnfencedCompletion permits an expired worker to overwrite a newer result; migrate the idempotency store to atomic setIfLocked.";
|
|
605
|
+
if (typeof logger.warn === "function") logger.warn.call(wiring.log, warning);
|
|
606
|
+
else arcLog("mcp:idempotency").warn(warning);
|
|
607
|
+
}
|
|
608
|
+
/** Same protocol as the HTTP plugin: cached → replay; locked → 409; else lock → run → record. */
|
|
609
|
+
async function runIdempotentTool(args) {
|
|
610
|
+
const { store, dispatch } = args;
|
|
611
|
+
const { ttlMs, lockTimeoutMs } = resolveIdempotencyTiming(args.idempotencyTtlMs, args.idempotencyLockTimeoutMs);
|
|
612
|
+
if (!isFencedStore(store) && args.allowUnfencedCompletion !== true) return toCallToolError({
|
|
613
|
+
code: ARC_ERROR_CODES.INTERNAL_ERROR,
|
|
614
|
+
message: "Idempotency store requires atomic setIfLocked completion fencing",
|
|
615
|
+
status: 500
|
|
616
|
+
});
|
|
617
|
+
const fullKey = `${args.key}:${operationIdentity(`${args.resourceName}:${args.operation}`, args.scope ?? buildScope(args.session), void 0, "mcp")}`;
|
|
618
|
+
let fingerprint;
|
|
619
|
+
try {
|
|
620
|
+
fingerprint = fingerprintRequest(args.input);
|
|
621
|
+
} catch (error) {
|
|
622
|
+
if (error instanceof IdempotencyError) return toCallToolError({
|
|
623
|
+
code: ARC_ERROR_CODES.IDEMPOTENCY_BODY_UNFINGERPRINTABLE,
|
|
624
|
+
message: error.message,
|
|
625
|
+
status: 400
|
|
626
|
+
});
|
|
627
|
+
throw error;
|
|
628
|
+
}
|
|
629
|
+
const replay = (cached) => {
|
|
630
|
+
if (cached) {
|
|
631
|
+
if (cached.requestFingerprint && cached.requestFingerprint !== fingerprint) return toCallToolError({
|
|
632
|
+
code: ARC_ERROR_CODES.IDEMPOTENCY_KEY_REUSED,
|
|
633
|
+
message: "Idempotency key was already used for a different request body. Use a new key for a new request.",
|
|
634
|
+
status: 422
|
|
635
|
+
});
|
|
636
|
+
return cached.body;
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
const lockId = randomUUID();
|
|
640
|
+
const acquisition = await acquireIdempotencyKey(store, fullKey, lockId, lockTimeoutMs);
|
|
641
|
+
if (acquisition.status === "completed") return replay(acquisition.result);
|
|
642
|
+
if (acquisition.status === "locked") return toCallToolError({
|
|
643
|
+
code: ARC_ERROR_CODES.IDEMPOTENCY_CONFLICT,
|
|
644
|
+
message: "A call with this idempotency key is already in progress",
|
|
645
|
+
status: 409
|
|
646
|
+
});
|
|
647
|
+
let fencedCompletion = false;
|
|
648
|
+
try {
|
|
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
|
+
fencedCompletion = true;
|
|
659
|
+
} else await store.set(fullKey, completion);
|
|
660
|
+
}
|
|
661
|
+
return result;
|
|
662
|
+
} finally {
|
|
663
|
+
await releaseIdempotencyLock(store, fullKey, lockId, fencedCompletion);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
/** The 400 a keyless call to a key-required route gets over HTTP. */
|
|
667
|
+
function idempotencyKeyRequiredResult(toolName) {
|
|
668
|
+
return toCallToolError({
|
|
669
|
+
code: ARC_ERROR_CODES.IDEMPOTENCY_KEY_REQUIRED,
|
|
670
|
+
message: `'${toolName}' moves money or stock: pass \`_idempotencyKey\` and reuse it on a retry.`,
|
|
671
|
+
status: 400
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
function createToolRateLimiter(config, resourceName, limiter = createMemoryResourceRateLimiter()) {
|
|
675
|
+
if (!config) return void 0;
|
|
676
|
+
resourceRateLimitWindowMs(config.timeWindow);
|
|
677
|
+
return async (session, scope) => (await limiter.consume(resourceName, config, { scope: scope ?? buildScope(session) })).allowed;
|
|
678
|
+
}
|
|
679
|
+
function rateLimitedResult(toolName) {
|
|
680
|
+
return toCallToolError({
|
|
681
|
+
code: "arc.rate_limited",
|
|
682
|
+
message: `Too many calls to '${toolName}' — retry later.`,
|
|
683
|
+
status: 429
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
/** `audit.custom(resource, id, action, {}, context)` after a successful action, like the route. */
|
|
687
|
+
async function auditToolAction(audit, args) {
|
|
688
|
+
if (typeof audit !== "object" || audit === null) return;
|
|
689
|
+
const logger = audit;
|
|
690
|
+
if (typeof logger.custom !== "function" || logger._noop === true) return;
|
|
691
|
+
const { session } = args;
|
|
692
|
+
const actorId = session?.userId ?? session?.clientId;
|
|
693
|
+
try {
|
|
694
|
+
await logger.custom.call(audit, args.resourceName, args.id, args.action, { channel: "mcp" }, {
|
|
695
|
+
...actorId !== void 0 ? { actorId } : {},
|
|
696
|
+
...session?.organizationId !== void 0 ? { organizationId: session.organizationId } : {},
|
|
697
|
+
endpoint: `MCP ${args.toolName}`
|
|
698
|
+
});
|
|
699
|
+
} catch (err) {
|
|
700
|
+
const log = typeof args.log === "object" && args.log !== null ? args.log : void 0;
|
|
701
|
+
if (typeof log?.error === "function") log.error.call(log, {
|
|
702
|
+
err,
|
|
703
|
+
tool: args.toolName
|
|
704
|
+
}, "[arc/mcp] action audit write failed");
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
//#endregion
|
|
708
|
+
//#region src/integrations/mcp/invokeController.ts
|
|
596
709
|
/**
|
|
597
710
|
* Assemble the {@link McpContextExtras} for a controller invocation from
|
|
598
711
|
* resource-scoped metadata + app-level wiring. Mirrors what
|
|
@@ -601,8 +714,9 @@ function createMcpController(resource) {
|
|
|
601
714
|
*/
|
|
602
715
|
function buildContextExtras(options) {
|
|
603
716
|
const { wiring, resourceName, permissions, fields, schemaOptions, idField } = options;
|
|
604
|
-
if (!wiring) return
|
|
717
|
+
if (!wiring) return { tenantMiddlewares: options.tenantMiddlewares };
|
|
605
718
|
return {
|
|
719
|
+
tenantMiddlewares: options.tenantMiddlewares,
|
|
606
720
|
arc: {
|
|
607
721
|
resourceName,
|
|
608
722
|
schemaOptions,
|
|
@@ -620,23 +734,6 @@ function buildContextExtras(options) {
|
|
|
620
734
|
};
|
|
621
735
|
}
|
|
622
736
|
/**
|
|
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
737
|
* Invoke a controller method through the MCP synthetic-context path.
|
|
641
738
|
*
|
|
642
739
|
* Runs the same chain CRUD MCP tools run:
|
|
@@ -685,8 +782,9 @@ async function invokeController(controller, op, input, options) {
|
|
|
685
782
|
reason: permResult.reason,
|
|
686
783
|
session
|
|
687
784
|
});
|
|
785
|
+
const reqCtx = buildRequestContext(input, session, op, permResult?.policy, permResult?.scope, buildContextExtras(options));
|
|
786
|
+
if (options.allow && !await options.allow(session, reqCtx.metadata._scope)) return rateLimitedResult(actionName);
|
|
688
787
|
const dispatch = async () => {
|
|
689
|
-
const reqCtx = buildRequestContext(input, session, op, permResult?.policy, permResult?.scope, buildContextExtras(options));
|
|
690
788
|
const response = await method(reqCtx);
|
|
691
789
|
const data = applyMcpReadMasking(response.data, {
|
|
692
790
|
fields,
|
|
@@ -698,25 +796,20 @@ async function invokeController(controller, op, input, options) {
|
|
|
698
796
|
data
|
|
699
797
|
});
|
|
700
798
|
};
|
|
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
|
-
}
|
|
799
|
+
const store = idempotencyStoreOf(wiring?.idempotencyStore);
|
|
800
|
+
if (idempotencyKey && store) return await runIdempotentTool({
|
|
801
|
+
store,
|
|
802
|
+
allowUnfencedCompletion: wiring?.allowUnfencedCompletion,
|
|
803
|
+
idempotencyTtlMs: wiring?.idempotencyTtlMs,
|
|
804
|
+
idempotencyLockTimeoutMs: wiring?.idempotencyLockTimeoutMs,
|
|
805
|
+
key: idempotencyKey,
|
|
806
|
+
resourceName,
|
|
807
|
+
operation: actionName,
|
|
808
|
+
input,
|
|
809
|
+
session,
|
|
810
|
+
scope: reqCtx.metadata._scope,
|
|
811
|
+
dispatch
|
|
812
|
+
});
|
|
720
813
|
return await dispatch();
|
|
721
814
|
} catch (err) {
|
|
722
815
|
return toCallToolError(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -806,7 +899,9 @@ const MUTATING_CRUD_OPS = /* @__PURE__ */ new Set([
|
|
|
806
899
|
]);
|
|
807
900
|
function createCrudHandler(op, controller, resourceName, permissions, fields, extras) {
|
|
808
901
|
const permission = permissions?.[op];
|
|
809
|
-
|
|
902
|
+
const toolName = extras?.toolName ?? `${op}_${resourceName}`;
|
|
903
|
+
return async (input, ctx) => {
|
|
904
|
+
if (extras?.keyRequired && (typeof input._idempotencyKey !== "string" || input._idempotencyKey === "")) return idempotencyKeyRequiredResult(toolName);
|
|
810
905
|
let idempotencyKey;
|
|
811
906
|
let payload = input;
|
|
812
907
|
if ("_idempotencyKey" in input) {
|
|
@@ -816,10 +911,12 @@ function createCrudHandler(op, controller, resourceName, permissions, fields, ex
|
|
|
816
911
|
}
|
|
817
912
|
return invokeController(controller, op, payload, {
|
|
818
913
|
session: ctx.session,
|
|
914
|
+
allow: extras?.allow,
|
|
819
915
|
resourceName,
|
|
820
916
|
permissions: permission,
|
|
821
917
|
fields,
|
|
822
918
|
wiring: extras?.wiring,
|
|
919
|
+
tenantMiddlewares: extras?.tenantMiddlewares,
|
|
823
920
|
schemaOptions: extras?.schemaOptions,
|
|
824
921
|
idField: extras?.idField,
|
|
825
922
|
idempotencyKey
|
|
@@ -1082,7 +1179,7 @@ var McpAuthCache = class {
|
|
|
1082
1179
|
maxEntries;
|
|
1083
1180
|
constructor(opts) {
|
|
1084
1181
|
this.ttlMs = opts?.ttlMs ?? DEFAULT_AUTH_CACHE_TTL_MS;
|
|
1085
|
-
this.maxEntries = opts?.maxEntries ?? DEFAULT_AUTH_CACHE_MAX;
|
|
1182
|
+
this.maxEntries = assertCapacity("McpAuthCache maxEntries", opts?.maxEntries ?? DEFAULT_AUTH_CACHE_MAX);
|
|
1086
1183
|
}
|
|
1087
1184
|
get(key) {
|
|
1088
1185
|
const entry = this.cache.get(key);
|
|
@@ -1097,15 +1194,12 @@ var McpAuthCache = class {
|
|
|
1097
1194
|
if (this.cache.size >= this.maxEntries) {
|
|
1098
1195
|
const now = Date.now();
|
|
1099
1196
|
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
1197
|
}
|
|
1105
|
-
this.cache
|
|
1198
|
+
touch(this.cache, key, {
|
|
1106
1199
|
result,
|
|
1107
1200
|
expires: Date.now() + this.ttlMs
|
|
1108
1201
|
});
|
|
1202
|
+
evictToSize(this.cache, this.maxEntries);
|
|
1109
1203
|
}
|
|
1110
1204
|
};
|
|
1111
1205
|
/**
|
|
@@ -1229,7 +1323,10 @@ function convertActionSchemaToZod(raw) {
|
|
|
1229
1323
|
*/
|
|
1230
1324
|
function createActionToolHandler(actionName, handler, permissions, resourceName, _resourcePermissions, rawSchema, idLess = false, fields, extras) {
|
|
1231
1325
|
const wiring = extras?.wiring;
|
|
1326
|
+
const parity = extras?.parity;
|
|
1327
|
+
const toolName = parity?.toolName ?? `${actionName}_${resourceName}`;
|
|
1232
1328
|
const contextExtras = wiring ? {
|
|
1329
|
+
tenantMiddlewares: extras?.tenantMiddlewares,
|
|
1233
1330
|
arc: {
|
|
1234
1331
|
resourceName,
|
|
1235
1332
|
schemaOptions: extras?.schemaOptions,
|
|
@@ -1244,11 +1341,16 @@ function createActionToolHandler(actionName, handler, permissions, resourceName,
|
|
|
1244
1341
|
audit: wiring.audit,
|
|
1245
1342
|
log: wiring.log
|
|
1246
1343
|
}
|
|
1247
|
-
} :
|
|
1344
|
+
} : { tenantMiddlewares: extras?.tenantMiddlewares };
|
|
1248
1345
|
const ir = rawSchema ? normalizeSchemaIR(rawSchema) : void 0;
|
|
1249
|
-
const
|
|
1250
|
-
|
|
1346
|
+
const strict = ir ? shouldRejectAdditionalProperties(ir) : false;
|
|
1347
|
+
const allowedBaseKeys = [...idLess ? [] : ["id"], ...parity?.keyRequired ? ["_idempotencyKey"] : []];
|
|
1348
|
+
const allowedKeys = strict && ir ? /* @__PURE__ */ new Set([...allowedBaseKeys, ...Object.keys(ir.properties)]) : void 0;
|
|
1349
|
+
return async (rawInput, ctx) => {
|
|
1251
1350
|
const session = ctx.session;
|
|
1351
|
+
const { _idempotencyKey: rawKey, ...input } = rawInput;
|
|
1352
|
+
const idempotencyKey = typeof rawKey === "string" && rawKey !== "" ? rawKey : void 0;
|
|
1353
|
+
if (parity?.keyRequired && idempotencyKey === void 0) return idempotencyKeyRequiredResult(toolName);
|
|
1252
1354
|
if (allowedKeys) {
|
|
1253
1355
|
const extras = Object.keys(input).filter((k) => !allowedKeys.has(k));
|
|
1254
1356
|
if (extras.length > 0) return toCallToolError({
|
|
@@ -1269,18 +1371,62 @@ function createActionToolHandler(actionName, handler, permissions, resourceName,
|
|
|
1269
1371
|
reason: permResult.reason,
|
|
1270
1372
|
session
|
|
1271
1373
|
});
|
|
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
1374
|
try {
|
|
1279
|
-
|
|
1280
|
-
|
|
1375
|
+
const reqCtx = buildRequestContext({
|
|
1376
|
+
...input,
|
|
1377
|
+
action: actionName
|
|
1378
|
+
}, session, "action", permResult?.policy, permResult?.scope, contextExtras);
|
|
1379
|
+
const id = idLess ? "" : typeof input.id === "string" ? input.id : "";
|
|
1380
|
+
const { id: _discardId, ...data } = input;
|
|
1381
|
+
let target;
|
|
1382
|
+
if (!idLess && parity?.fence) {
|
|
1383
|
+
const check = await parity.fence(id, reqCtx);
|
|
1384
|
+
if (!check.visible) throw new NotFoundError(resourceName);
|
|
1385
|
+
target = check.doc;
|
|
1386
|
+
}
|
|
1387
|
+
if (parity?.allow && !await parity.allow(session, reqCtx.metadata._scope)) return rateLimitedResult(toolName);
|
|
1388
|
+
const run = async () => {
|
|
1389
|
+
const withTarget = target === void 0 ? reqCtx : {
|
|
1390
|
+
...reqCtx,
|
|
1391
|
+
arc: {
|
|
1392
|
+
...reqCtx.arc,
|
|
1393
|
+
actionTarget: target
|
|
1394
|
+
}
|
|
1395
|
+
};
|
|
1396
|
+
const req = idempotencyKey === void 0 ? withTarget : {
|
|
1397
|
+
...withTarget,
|
|
1398
|
+
idempotencyKey
|
|
1399
|
+
};
|
|
1400
|
+
const result = await handler(id, data, req);
|
|
1401
|
+
if (parity?.audit) await auditToolAction(wiring?.audit, {
|
|
1402
|
+
resourceName,
|
|
1403
|
+
id,
|
|
1404
|
+
action: actionName,
|
|
1405
|
+
toolName,
|
|
1406
|
+
session,
|
|
1407
|
+
log: wiring?.log
|
|
1408
|
+
});
|
|
1409
|
+
return toCallToolSuccess(applyMcpReadMasking(result, {
|
|
1410
|
+
fields,
|
|
1411
|
+
session,
|
|
1412
|
+
scopeOverride: permResult?.scope
|
|
1413
|
+
}));
|
|
1414
|
+
};
|
|
1415
|
+
const store = idempotencyStoreOf(wiring?.idempotencyStore);
|
|
1416
|
+
if (idempotencyKey !== void 0 && store && !parity?.handlerDedup) return await runIdempotentTool({
|
|
1417
|
+
store,
|
|
1418
|
+
allowUnfencedCompletion: wiring?.allowUnfencedCompletion,
|
|
1419
|
+
idempotencyTtlMs: wiring?.idempotencyTtlMs,
|
|
1420
|
+
idempotencyLockTimeoutMs: wiring?.idempotencyLockTimeoutMs,
|
|
1421
|
+
key: idempotencyKey,
|
|
1422
|
+
resourceName,
|
|
1423
|
+
operation: `action.${actionName}`,
|
|
1424
|
+
input,
|
|
1281
1425
|
session,
|
|
1282
|
-
|
|
1283
|
-
|
|
1426
|
+
scope: reqCtx.metadata._scope,
|
|
1427
|
+
dispatch: run
|
|
1428
|
+
});
|
|
1429
|
+
return await run();
|
|
1284
1430
|
} catch (err) {
|
|
1285
1431
|
return toCallToolError(err instanceof Error ? err : new Error(String(err)));
|
|
1286
1432
|
}
|
|
@@ -1320,7 +1466,8 @@ function buildAggregationTools(args) {
|
|
|
1320
1466
|
resourceName,
|
|
1321
1467
|
repo,
|
|
1322
1468
|
buildOptionsFromSession,
|
|
1323
|
-
fields
|
|
1469
|
+
fields,
|
|
1470
|
+
tenantMiddlewares: args.tenantMiddlewares
|
|
1324
1471
|
})
|
|
1325
1472
|
});
|
|
1326
1473
|
}
|
|
@@ -1372,14 +1519,24 @@ function createAggregationToolHandler(args) {
|
|
|
1372
1519
|
session: ctx.session
|
|
1373
1520
|
});
|
|
1374
1521
|
const filterInput = input.filter ?? {};
|
|
1375
|
-
|
|
1522
|
+
let prepared;
|
|
1523
|
+
try {
|
|
1524
|
+
prepared = buildRequestContext(filterInput, ctx.session, "list", permResult?.policy, permResult?.scope, { tenantMiddlewares: args.tenantMiddlewares });
|
|
1525
|
+
} catch (error) {
|
|
1526
|
+
return toCallToolError(error instanceof Error ? error : new Error(String(error)));
|
|
1527
|
+
}
|
|
1528
|
+
const meta = prepared.metadata;
|
|
1529
|
+
const tenantOptions = {
|
|
1530
|
+
...buildOptionsFromSession(ctx.session),
|
|
1531
|
+
...meta._tenantFields
|
|
1532
|
+
};
|
|
1376
1533
|
const result = await executeAggregation(normalized, {
|
|
1377
1534
|
repo,
|
|
1378
1535
|
buildOptions: () => tenantOptions
|
|
1379
1536
|
}, {
|
|
1380
1537
|
query: filterInput,
|
|
1381
1538
|
tenantOptions,
|
|
1382
|
-
policyFilter:
|
|
1539
|
+
policyFilter: meta._policyFilters
|
|
1383
1540
|
});
|
|
1384
1541
|
if (result.status === 200) {
|
|
1385
1542
|
const body = result.body;
|
|
@@ -1677,6 +1834,73 @@ function mapJsonSchemaTypeToArcType(jsonType) {
|
|
|
1677
1834
|
}
|
|
1678
1835
|
}
|
|
1679
1836
|
//#endregion
|
|
1837
|
+
//#region src/integrations/mcp/route-input-schema.ts
|
|
1838
|
+
/**
|
|
1839
|
+
* Custom-route → MCP tool INPUT schema. The route's own schema is the contract:
|
|
1840
|
+
* an input the REST route would refuse must be refused on MCP, never stripped.
|
|
1841
|
+
*
|
|
1842
|
+
* - A Zod schema that is not a plain object (a union of strict arms) is registered
|
|
1843
|
+
* WHOLE — the MCP SDK validates and advertises a Standard Schema as-is. Flattening
|
|
1844
|
+
* it yields `{}`, and the SDK's `z.object({})` then strips every key the caller sent.
|
|
1845
|
+
* - A flat schema declaring `additionalProperties: false` registers as `z.strictObject`,
|
|
1846
|
+
* so an unknown key is an input error instead of a silently dropped filter. So
|
|
1847
|
+
* does a route with NO schema: a key it was sent could never reach the handler.
|
|
1848
|
+
* - Any other non-object schema (a JSON Schema `anyOf`, or a Zod union beside `:id`
|
|
1849
|
+
* or a second source) throws at tool build: there is no lossless flat form, so
|
|
1850
|
+
* bridge it with `mcpHandler` or opt out with `mcp: false`.
|
|
1851
|
+
*/
|
|
1852
|
+
/** A Zod v4 schema (carries the `_zod` marker and the Standard Schema slot). */
|
|
1853
|
+
function isZodTypeSchema(value) {
|
|
1854
|
+
return value !== null && typeof value === "object" && "_zod" in value && "~standard" in value;
|
|
1855
|
+
}
|
|
1856
|
+
function isRecord(value) {
|
|
1857
|
+
return value !== null && typeof value === "object";
|
|
1858
|
+
}
|
|
1859
|
+
/** The JSON Schema HTTP validates this request slot with (`input` io, as the router converts it). */
|
|
1860
|
+
function requestJsonSchema(schema) {
|
|
1861
|
+
return toJsonSchema(schema, "draft-7", "throw", "input");
|
|
1862
|
+
}
|
|
1863
|
+
/** Does this JSON Schema have a single top-level object a flat shape can carry? */
|
|
1864
|
+
function flattensLosslessly(json) {
|
|
1865
|
+
if (!json) return true;
|
|
1866
|
+
if ("anyOf" in json || "oneOf" in json || "allOf" in json) return false;
|
|
1867
|
+
return json.type === "object" || "properties" in json;
|
|
1868
|
+
}
|
|
1869
|
+
function buildRouteInputSchema(route, hasId) {
|
|
1870
|
+
const routeSchema = isRecord(route.schema) ? route.schema : void 0;
|
|
1871
|
+
const sources = [routeSchema?.body, routeSchema?.querystring].filter(isRecord);
|
|
1872
|
+
const whole = [];
|
|
1873
|
+
let unflattenable = 0;
|
|
1874
|
+
for (const source of sources) {
|
|
1875
|
+
if (flattensLosslessly(requestJsonSchema(source))) continue;
|
|
1876
|
+
unflattenable++;
|
|
1877
|
+
if (isZodTypeSchema(source)) whole.push(source);
|
|
1878
|
+
}
|
|
1879
|
+
const [only] = whole;
|
|
1880
|
+
if (unflattenable > 0) {
|
|
1881
|
+
if (only && sources.length === 1 && !hasId) return only;
|
|
1882
|
+
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\`.`);
|
|
1883
|
+
}
|
|
1884
|
+
const shape = {};
|
|
1885
|
+
if (hasId) shape.id = z.string().describe("Resource ID");
|
|
1886
|
+
let strict = sources.length === 0;
|
|
1887
|
+
for (const source of sources) {
|
|
1888
|
+
if (requestJsonSchema(source)?.additionalProperties === false) strict = true;
|
|
1889
|
+
const ir = normalizeSchemaIR(source);
|
|
1890
|
+
for (const [key, val] of Object.entries(schemaIRToZodShape(ir))) if (!(key in shape)) shape[key] = val;
|
|
1891
|
+
}
|
|
1892
|
+
return strict ? z.strictObject(shape) : shape;
|
|
1893
|
+
}
|
|
1894
|
+
/**
|
|
1895
|
+
* The field shape a tool advertises — the flat record itself, or a whole object
|
|
1896
|
+
* schema's `.shape`. `undefined` for a union, which has no single shape.
|
|
1897
|
+
*/
|
|
1898
|
+
function toolInputShape(schema) {
|
|
1899
|
+
if (schema === void 0) return void 0;
|
|
1900
|
+
if (!isZodTypeSchema(schema)) return schema;
|
|
1901
|
+
return schema instanceof z.ZodObject ? schema.shape : void 0;
|
|
1902
|
+
}
|
|
1903
|
+
//#endregion
|
|
1680
1904
|
//#region src/integrations/mcp/route-tools.ts
|
|
1681
1905
|
/**
|
|
1682
1906
|
* Custom-route → MCP tool generation.
|
|
@@ -1737,6 +1961,7 @@ function createCustomRouteHandler(route, controller, hasId, options) {
|
|
|
1737
1961
|
const { resourceName, operationName, permissions, pipeline, fields, wiring } = options;
|
|
1738
1962
|
const pipelineSteps = resolvePipelineSteps(pipeline, operationName);
|
|
1739
1963
|
const contextExtras = wiring ? {
|
|
1964
|
+
tenantMiddlewares: options.tenantMiddlewares,
|
|
1740
1965
|
arc: {
|
|
1741
1966
|
resourceName,
|
|
1742
1967
|
schemaOptions: options.schemaOptions,
|
|
@@ -1751,7 +1976,7 @@ function createCustomRouteHandler(route, controller, hasId, options) {
|
|
|
1751
1976
|
audit: wiring.audit,
|
|
1752
1977
|
log: wiring.log
|
|
1753
1978
|
}
|
|
1754
|
-
} :
|
|
1979
|
+
} : { tenantMiddlewares: options.tenantMiddlewares };
|
|
1755
1980
|
return async (input, _ctx) => {
|
|
1756
1981
|
const session = _ctx.session;
|
|
1757
1982
|
const permResult = await evaluatePermission(permissions, session, resourceName, operationName, input);
|
|
@@ -1763,6 +1988,7 @@ function createCustomRouteHandler(route, controller, hasId, options) {
|
|
|
1763
1988
|
});
|
|
1764
1989
|
try {
|
|
1765
1990
|
const reqCtx = buildRequestContext(input, session, operationKindForRoute(route.method, hasId), permResult?.policy, permResult?.scope, contextExtras);
|
|
1991
|
+
if (options.allow && !await options.allow(session, reqCtx.metadata._scope)) return rateLimitedResult(operationName);
|
|
1766
1992
|
const emit = (envelope) => {
|
|
1767
1993
|
const data = applyMcpReadMasking(envelope.data, {
|
|
1768
1994
|
fields,
|
|
@@ -1881,10 +2107,18 @@ function resourceToTools(resource, config = {}) {
|
|
|
1881
2107
|
const tools = [];
|
|
1882
2108
|
const prefix = config.toolNamePrefix;
|
|
1883
2109
|
const wiring = config.wiring;
|
|
2110
|
+
assertMcpIdempotencyFencing(wiring);
|
|
2111
|
+
const hasIdempotencyStore = idempotencyStoreOf(wiring?.idempotencyStore) !== void 0;
|
|
2112
|
+
const keyRequiredOps = new Set(resource.idempotency?.required ?? []);
|
|
2113
|
+
const rateLimit = resource.rateLimit;
|
|
2114
|
+
const rateLimiter = config.wiring?.rateLimiter ?? createMemoryResourceRateLimiter();
|
|
1884
2115
|
if (controller) {
|
|
1885
2116
|
let ops = ALL_CRUD_OPS.filter((op) => !resource.disabledRoutes?.includes(op));
|
|
1886
2117
|
if (config.operations) ops = ops.filter((op) => config.operations?.includes(op));
|
|
1887
2118
|
for (const op of ops) {
|
|
2119
|
+
if (!resource.permissions?.[op]) continue;
|
|
2120
|
+
const keyRequired = keyRequiredOps.has(op);
|
|
2121
|
+
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
2122
|
const name = config.names?.[op] ?? (op === "list" ? `${prefix ? `${prefix}_` : ""}list_${pluralize(resource.name)}` : `${prefix ? `${prefix}_` : ""}${op}_${resource.name}`);
|
|
1889
2123
|
const defaultDescription = defaultCrudDescription(op, resource.displayName, hasSoftDelete, {
|
|
1890
2124
|
filterableFields,
|
|
@@ -1900,7 +2134,8 @@ function resourceToTools(resource, config = {}) {
|
|
|
1900
2134
|
maxLimit,
|
|
1901
2135
|
adapterBodies
|
|
1902
2136
|
});
|
|
1903
|
-
if (
|
|
2137
|
+
if (keyRequired) inputSchema._idempotencyKey = z.string().min(1).describe("Required idempotency key — reuse it on a retry; the first result replays.");
|
|
2138
|
+
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
2139
|
tools.push({
|
|
1905
2140
|
name,
|
|
1906
2141
|
description: resolveCrudDescription(config.descriptions?.[op], {
|
|
@@ -1913,11 +2148,15 @@ function resourceToTools(resource, config = {}) {
|
|
|
1913
2148
|
sortableFields
|
|
1914
2149
|
}),
|
|
1915
2150
|
annotations: CRUD_ANNOTATIONS[op],
|
|
1916
|
-
inputSchema,
|
|
2151
|
+
inputSchema: op === "list" || op === "get" ? z.strictObject(inputSchema) : inputSchema,
|
|
1917
2152
|
handler: createCrudHandler(op, controller, resource.name, resource.permissions, resource.fields, {
|
|
1918
2153
|
wiring,
|
|
2154
|
+
tenantMiddlewares: resource.middlewares?.[op],
|
|
1919
2155
|
schemaOptions: resource.schemaOptions,
|
|
1920
|
-
idField: resource.idField
|
|
2156
|
+
idField: resource.idField,
|
|
2157
|
+
keyRequired,
|
|
2158
|
+
allow: createToolRateLimiter(rateLimit, resource.name, rateLimiter),
|
|
2159
|
+
toolName: name
|
|
1921
2160
|
}),
|
|
1922
2161
|
source: `crud:${resource.name}:${op}`
|
|
1923
2162
|
});
|
|
@@ -1941,27 +2180,26 @@ function resourceToTools(resource, config = {}) {
|
|
|
1941
2180
|
const hasId = route.path.includes(":id");
|
|
1942
2181
|
const mcpConfig = typeof route.mcp === "object" && route.mcp !== null ? route.mcp : void 0;
|
|
1943
2182
|
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
|
-
}
|
|
2183
|
+
const toolAnnotations = mcpConfig?.annotations ? { ...mcpConfig.annotations } : route.method.toUpperCase() === "GET" ? { readOnlyHint: true } : { openWorldHint: true };
|
|
2184
|
+
const inputShape = buildRouteInputSchema(route, hasId);
|
|
1956
2185
|
const toolName = prefix ? `${prefix}_${opName}_${resource.name}` : `${opName}_${resource.name}`;
|
|
1957
2186
|
const routeSource = route._presetSource ? `preset:${route._presetSource}:${resource.name}:${opName}` : `route:${resource.name}:${route.method} ${route.path}`;
|
|
2187
|
+
const routePolicy = route.rateLimit !== void 0 ? route.rateLimit : rateLimit;
|
|
2188
|
+
const allowRoute = createToolRateLimiter(routePolicy && typeof routePolicy === "object" ? {
|
|
2189
|
+
...routePolicy,
|
|
2190
|
+
...route.rateLimit ? { bucket: `route:${route.method}:${route.path}` } : {}
|
|
2191
|
+
} : routePolicy, resource.name, rateLimiter);
|
|
2192
|
+
const passthrough = mcpHandler ? createMcpHandlerPassthrough(mcpHandler) : void 0;
|
|
1958
2193
|
tools.push({
|
|
1959
2194
|
name: toolName,
|
|
1960
2195
|
description: toolDescription,
|
|
1961
2196
|
annotations: toolAnnotations,
|
|
1962
2197
|
inputSchema: inputShape,
|
|
1963
2198
|
source: routeSource,
|
|
1964
|
-
handler:
|
|
2199
|
+
handler: passthrough ? async (input, ctx) => {
|
|
2200
|
+
if (allowRoute && !await allowRoute(ctx.session)) return rateLimitedResult(toolName);
|
|
2201
|
+
return passthrough(input, ctx);
|
|
2202
|
+
} : createCustomRouteHandler({
|
|
1965
2203
|
handler: resolvedRouteHandler,
|
|
1966
2204
|
operation: route.operation,
|
|
1967
2205
|
method: route.method,
|
|
@@ -1969,48 +2207,70 @@ function resourceToTools(resource, config = {}) {
|
|
|
1969
2207
|
}, controller, hasId, {
|
|
1970
2208
|
resourceName: resource.name,
|
|
1971
2209
|
operationName: opName,
|
|
2210
|
+
allow: allowRoute,
|
|
1972
2211
|
permissions: route.permissions,
|
|
1973
2212
|
pipeline: resource.pipe,
|
|
1974
2213
|
fields: resource.fields,
|
|
1975
2214
|
wiring,
|
|
2215
|
+
tenantMiddlewares: route.tenantScope ? resource.middlewares?.tenantScope : void 0,
|
|
1976
2216
|
schemaOptions: resource.schemaOptions,
|
|
1977
2217
|
idField: resource.idField
|
|
1978
2218
|
})
|
|
1979
2219
|
});
|
|
1980
2220
|
}
|
|
1981
|
-
if (resource.actions)
|
|
1982
|
-
const
|
|
1983
|
-
|
|
1984
|
-
const
|
|
1985
|
-
const
|
|
1986
|
-
const
|
|
1987
|
-
const
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
const
|
|
1992
|
-
|
|
2221
|
+
if (resource.actions) {
|
|
2222
|
+
const actionConfig = normalizeActionsToRouterConfig(resource.actions, resource.actionPermissions, resource.tag, resource.permissions, resource.name, void 0);
|
|
2223
|
+
const fence = resolveActionTargetFence(resource, actionConfig);
|
|
2224
|
+
const handlerScoped = new Set(actionConfig.handlerScopedActionNames);
|
|
2225
|
+
const keyRequiredActions = new Set(actionConfig.idempotentActionNames);
|
|
2226
|
+
const allow = createToolRateLimiter(rateLimit, resource.name, rateLimiter);
|
|
2227
|
+
for (const [actionName, entry] of Object.entries(resource.actions)) {
|
|
2228
|
+
const def = typeof entry === "function" ? { handler: entry } : entry;
|
|
2229
|
+
if (typeof def !== "function" && "mcp" in def && def.mcp === false) continue;
|
|
2230
|
+
const mcpCfg = typeof def !== "function" && typeof def.mcp === "object" ? def.mcp : void 0;
|
|
2231
|
+
const description = mcpCfg?.description ?? (typeof def !== "function" ? def.description : void 0) ?? `${actionName} action on ${resource.displayName}`;
|
|
2232
|
+
const annotations = mcpCfg?.annotations ? { ...mcpCfg.annotations } : { destructiveHint: true };
|
|
2233
|
+
const idLess = typeof def !== "function" && def.id === false;
|
|
2234
|
+
const inputShape = idLess ? {} : { id: z.string().describe("Resource ID") };
|
|
2235
|
+
const rawSchema = typeof def !== "function" ? def.schema : void 0;
|
|
2236
|
+
if (rawSchema && typeof rawSchema === "object") {
|
|
2237
|
+
const converted = convertActionSchemaToZod(rawSchema);
|
|
2238
|
+
for (const [key, val] of Object.entries(converted)) inputShape[key] = val;
|
|
2239
|
+
}
|
|
2240
|
+
const toolName = prefix ? `${prefix}_${actionName}_${resource.name}` : `${actionName}_${resource.name}`;
|
|
2241
|
+
const keyRequired = keyRequiredActions.has(actionName);
|
|
2242
|
+
const handlerDedup = actionConfig.handlerDedupOwners[actionName] !== void 0;
|
|
2243
|
+
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.`);
|
|
2244
|
+
if (keyRequired) inputShape._idempotencyKey = z.string().min(1).describe("Required idempotency key — reuse it on a retry; the first result replays.");
|
|
2245
|
+
const handler = typeof entry === "function" ? entry : def.handler;
|
|
2246
|
+
const actionPerms = resolveActionPermission({
|
|
2247
|
+
action: entry,
|
|
2248
|
+
resourcePermissions: resource.permissions,
|
|
2249
|
+
resourceActionPermissions: resource.actionPermissions
|
|
2250
|
+
});
|
|
2251
|
+
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.`);
|
|
2252
|
+
tools.push({
|
|
2253
|
+
name: toolName,
|
|
2254
|
+
description: String(description),
|
|
2255
|
+
annotations,
|
|
2256
|
+
inputSchema: inputShape,
|
|
2257
|
+
source: `action:${resource.name}:${actionName}`,
|
|
2258
|
+
handler: createActionToolHandler(actionName, handler, actionPerms, resource.name, resource.permissions, rawSchema, idLess, resource.fields, {
|
|
2259
|
+
wiring,
|
|
2260
|
+
tenantMiddlewares: withoutTenantInjection(resource.middlewares?.tenantScope),
|
|
2261
|
+
schemaOptions: resource.schemaOptions,
|
|
2262
|
+
idField: resource.idField,
|
|
2263
|
+
parity: {
|
|
2264
|
+
toolName,
|
|
2265
|
+
keyRequired,
|
|
2266
|
+
handlerDedup,
|
|
2267
|
+
...fence && !idLess && !handlerScoped.has(actionName) ? { fence } : {},
|
|
2268
|
+
audit: Boolean(resource.audit),
|
|
2269
|
+
...allow ? { allow } : {}
|
|
2270
|
+
}
|
|
2271
|
+
})
|
|
2272
|
+
});
|
|
1993
2273
|
}
|
|
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
2274
|
}
|
|
2015
2275
|
if (resource.aggregations && Object.keys(resource.aggregations).length > 0) {
|
|
2016
2276
|
const repoForAgg = resource.controller?.repository;
|
|
@@ -2036,6 +2296,7 @@ function resourceToTools(resource, config = {}) {
|
|
|
2036
2296
|
schemaOptions: resource.schemaOptions,
|
|
2037
2297
|
repo: repoForAgg,
|
|
2038
2298
|
buildOptionsFromSession,
|
|
2299
|
+
tenantMiddlewares: resource.middlewares?.aggregations,
|
|
2039
2300
|
prefix,
|
|
2040
2301
|
fields: resource.fields
|
|
2041
2302
|
}));
|
|
@@ -2247,7 +2508,14 @@ const mcpPluginImpl = async (fastify, options) => {
|
|
|
2247
2508
|
exclude: options.exclude
|
|
2248
2509
|
});
|
|
2249
2510
|
const fastifyAny = fastify;
|
|
2511
|
+
const rateLimiter = resolveResourceRateLimiter(fastify) ?? createMemoryResourceRateLimiter();
|
|
2512
|
+
if (enabledResources.some((resource) => resource.rateLimit)) declareRuntimeCapability(fastify, {
|
|
2513
|
+
subsystem: "mcp.resource-rate-limit",
|
|
2514
|
+
durability: rateLimiter.durability,
|
|
2515
|
+
detail: "REST and MCP resource budgets require a shared rate-limit backend across replicas."
|
|
2516
|
+
});
|
|
2250
2517
|
const wiring = {
|
|
2518
|
+
rateLimiter,
|
|
2251
2519
|
get hooks() {
|
|
2252
2520
|
return fastifyAny.arc?.hooks;
|
|
2253
2521
|
},
|
|
@@ -2260,6 +2528,15 @@ const mcpPluginImpl = async (fastify, options) => {
|
|
|
2260
2528
|
get log() {
|
|
2261
2529
|
return fastify.log;
|
|
2262
2530
|
},
|
|
2531
|
+
get allowUnfencedCompletion() {
|
|
2532
|
+
return fastifyAny.idempotency?.allowUnfencedCompletion;
|
|
2533
|
+
},
|
|
2534
|
+
get idempotencyTtlMs() {
|
|
2535
|
+
return fastifyAny.idempotency?.ttlMs;
|
|
2536
|
+
},
|
|
2537
|
+
get idempotencyLockTimeoutMs() {
|
|
2538
|
+
return fastifyAny.idempotency?.lockTimeoutMs;
|
|
2539
|
+
},
|
|
2263
2540
|
get idempotencyStore() {
|
|
2264
2541
|
return fastifyAny.idempotency?.store;
|
|
2265
2542
|
}
|
|
@@ -2526,11 +2803,10 @@ function registerStatefulRoutes(fastify, prefix, options, cache, createServer, T
|
|
|
2526
2803
|
});
|
|
2527
2804
|
}
|
|
2528
2805
|
/**
|
|
2529
|
-
* Resolve `expose` / `
|
|
2530
|
-
*
|
|
2806
|
+
* Resolve `expose` / `exclude` into the filtered resource list `mcpPlugin`
|
|
2807
|
+
* should surface. Two mutually exclusive intents:
|
|
2531
2808
|
*
|
|
2532
2809
|
* - `expose`: default-deny allowlist (preferred — new resources auto-deny).
|
|
2533
|
-
* - `include`: legacy alias for `expose`, kept for back-compat.
|
|
2534
2810
|
* - `exclude`: default-allow opt-out (drift-prone — new resources auto-leak).
|
|
2535
2811
|
*
|
|
2536
2812
|
* Conflicting combinations throw with an actionable message so a host that
|
|
@@ -2564,4 +2840,4 @@ const mcpPlugin = fp(mcpPluginImpl, {
|
|
|
2564
2840
|
fastify: "5.x"
|
|
2565
2841
|
});
|
|
2566
2842
|
//#endregion
|
|
2567
|
-
export {
|
|
2843
|
+
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 };
|