@classytic/arc 2.10.3 → 2.11.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.
Files changed (153) hide show
  1. package/README.md +1 -1
  2. package/dist/{BaseController-CbKKIflT.mjs → BaseController-JNV08qOT.mjs} +595 -537
  3. package/dist/{queryCachePlugin-BKbWjgDG.d.mts → QueryCache-DOBNHBE0.d.mts} +2 -32
  4. package/dist/actionPermissions-C8YYU92K.mjs +22 -0
  5. package/dist/adapters/index.d.mts +2 -2
  6. package/dist/adapters/index.mjs +1 -1
  7. package/dist/{adapters-BXY4i-hw.mjs → adapters-D0tT2Tyo.mjs} +54 -0
  8. package/dist/audit/index.d.mts +2 -2
  9. package/dist/audit/index.mjs +15 -17
  10. package/dist/auth/index.d.mts +4 -4
  11. package/dist/auth/index.mjs +3 -3
  12. package/dist/auth/redis-session.d.mts +1 -1
  13. package/dist/{betterAuthOpenApi-BBRVhjQN.mjs → betterAuthOpenApi-DwxtK3uG.mjs} +1 -1
  14. package/dist/cache/index.d.mts +3 -2
  15. package/dist/cache/index.mjs +3 -3
  16. package/dist/cli/commands/docs.mjs +2 -2
  17. package/dist/cli/commands/generate.mjs +37 -27
  18. package/dist/cli/commands/init.mjs +47 -34
  19. package/dist/cli/commands/introspect.mjs +1 -1
  20. package/dist/context/index.d.mts +58 -0
  21. package/dist/context/index.mjs +2 -0
  22. package/dist/core/index.d.mts +3 -3
  23. package/dist/core/index.mjs +4 -3
  24. package/dist/core-DXdSSFW-.mjs +1037 -0
  25. package/dist/createActionRouter-BwaSM0No.mjs +166 -0
  26. package/dist/{createApp-BuvPma24.mjs → createApp-DvNYEhpb.mjs} +118 -36
  27. package/dist/docs/index.d.mts +2 -2
  28. package/dist/docs/index.mjs +1 -1
  29. package/dist/{elevation-C7hgL_aI.mjs → elevation-DOFoxoDs.mjs} +1 -1
  30. package/dist/errorHandler-Co3lnVmJ.d.mts +114 -0
  31. package/dist/{eventPlugin-DCUjuiQT.mjs → eventPlugin--5HIkdPU.mjs} +1 -1
  32. package/dist/{eventPlugin-CxWgpd6K.d.mts → eventPlugin-CUNjYYRY.d.mts} +1 -1
  33. package/dist/events/index.d.mts +4 -4
  34. package/dist/events/index.mjs +69 -51
  35. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  36. package/dist/events/transports/redis.d.mts +1 -1
  37. package/dist/factory/index.d.mts +1 -1
  38. package/dist/factory/index.mjs +2 -2
  39. package/dist/{fields-Lo1VUDpt.d.mts → fields-C8Y0XLAu.d.mts} +1 -1
  40. package/dist/hooks/index.d.mts +1 -1
  41. package/dist/hooks/index.mjs +1 -1
  42. package/dist/idempotency/index.d.mts +3 -3
  43. package/dist/idempotency/index.mjs +38 -27
  44. package/dist/idempotency/redis.d.mts +1 -1
  45. package/dist/{index-ChIw3776.d.mts → index-BYCqHCVu.d.mts} +4 -4
  46. package/dist/{index-Cl0uoKd5.d.mts → index-Cm0vUrr_.d.mts} +2100 -1688
  47. package/dist/{index-DStwgFUK.d.mts → index-DAushRTt.d.mts} +29 -10
  48. package/dist/index-DsJ1MNfC.d.mts +1179 -0
  49. package/dist/{index-8qw4y6ff.d.mts → index-t8pLpPFW.d.mts} +13 -10
  50. package/dist/index.d.mts +7 -251
  51. package/dist/index.mjs +8 -128
  52. package/dist/integrations/event-gateway.d.mts +2 -2
  53. package/dist/integrations/event-gateway.mjs +1 -1
  54. package/dist/integrations/index.d.mts +2 -2
  55. package/dist/integrations/mcp/index.d.mts +2 -2
  56. package/dist/integrations/mcp/index.mjs +1 -1
  57. package/dist/integrations/mcp/testing.d.mts +1 -1
  58. package/dist/integrations/mcp/testing.mjs +1 -1
  59. package/dist/integrations/streamline.d.mts +46 -5
  60. package/dist/integrations/streamline.mjs +50 -21
  61. package/dist/integrations/websocket-redis.d.mts +1 -1
  62. package/dist/integrations/websocket.d.mts +2 -154
  63. package/dist/integrations/websocket.mjs +292 -224
  64. package/dist/{keys-qcD-TVJl.mjs → keys-CARyUjiR.mjs} +2 -0
  65. package/dist/{loadResources-BAzJItAJ.mjs → loadResources-YNwKHvRA.mjs} +3 -1
  66. package/dist/logger/index.d.mts +81 -0
  67. package/dist/{logger-DLg8-Ueg.mjs → logger/index.mjs} +1 -6
  68. package/dist/middleware/index.d.mts +109 -0
  69. package/dist/middleware/index.mjs +70 -0
  70. package/dist/multipartBody-CvTR1Un6.mjs +123 -0
  71. package/dist/{openapi-B5F8AddX.mjs → openapi-C0L9ar7m.mjs} +9 -7
  72. package/dist/org/index.d.mts +2 -2
  73. package/dist/permissions/index.d.mts +2 -2
  74. package/dist/permissions/index.mjs +1 -3
  75. package/dist/{permissions-Dk6mshja.mjs → permissions-B4vU9L0Q.mjs} +220 -2
  76. package/dist/pipe-DVoIheVC.mjs +62 -0
  77. package/dist/pipeline/index.d.mts +62 -0
  78. package/dist/pipeline/index.mjs +53 -0
  79. package/dist/plugins/index.d.mts +25 -5
  80. package/dist/plugins/index.mjs +10 -10
  81. package/dist/plugins/response-cache.mjs +1 -1
  82. package/dist/plugins/tracing-entry.d.mts +1 -1
  83. package/dist/plugins/tracing-entry.mjs +42 -24
  84. package/dist/presets/filesUpload.d.mts +4 -4
  85. package/dist/presets/filesUpload.mjs +255 -1
  86. package/dist/presets/index.d.mts +1 -1
  87. package/dist/presets/index.mjs +2 -2
  88. package/dist/presets/multiTenant.d.mts +1 -1
  89. package/dist/presets/multiTenant.mjs +48 -8
  90. package/dist/presets/search.d.mts +2 -2
  91. package/dist/presets/search.mjs +1 -1
  92. package/dist/{presets-fLJVXdVn.mjs → presets-k604Lj99.mjs} +1 -1
  93. package/dist/queryCachePlugin-BUXBSm4F.d.mts +34 -0
  94. package/dist/{queryCachePlugin-DQCEfJis.mjs → queryCachePlugin-Bq6bO6vc.mjs} +3 -3
  95. package/dist/{redis-DqyeggCa.d.mts → redis-Cm1gnRDf.d.mts} +1 -1
  96. package/dist/{redis-stream-CakIQmwR.d.mts → redis-stream-CM8TXTix.d.mts} +1 -1
  97. package/dist/registry/index.d.mts +1 -1
  98. package/dist/registry/index.mjs +2 -2
  99. package/dist/{requestContext-xHIKedG6.mjs → requestContext-CfRkaxwf.mjs} +1 -1
  100. package/dist/{resourceToTools-BElv3xPT.mjs → resourceToTools--okX6QBr.mjs} +534 -415
  101. package/dist/routerShared-DeESFp4a.mjs +515 -0
  102. package/dist/schemaIR-BlG9bY7v.mjs +137 -0
  103. package/dist/scope/index.d.mts +2 -2
  104. package/dist/scope/index.mjs +1 -1
  105. package/dist/{sse-yBCgOLGu.mjs → sse-V7aXc3bW.mjs} +1 -1
  106. package/dist/{store-helpers-ZCSMJJAX.mjs → store-helpers-BhrzxvyQ.mjs} +4 -0
  107. package/dist/testing/index.d.mts +367 -711
  108. package/dist/testing/index.mjs +646 -1434
  109. package/dist/testing/storageContract.d.mts +1 -1
  110. package/dist/{tracing-65B51Dw3.d.mts → tracing-DokiEsuz.d.mts} +9 -4
  111. package/dist/types/index.d.mts +5 -5
  112. package/dist/types/index.mjs +1 -3
  113. package/dist/types/storage.d.mts +1 -1
  114. package/dist/{types-Co8k3NyS.d.mts → types-CgikqKAj.d.mts} +133 -21
  115. package/dist/{types-Btdda02s.d.mts → types-D9NqiYIw.d.mts} +1 -1
  116. package/dist/utils/index.d.mts +2 -898
  117. package/dist/utils/index.mjs +4 -5
  118. package/dist/utils-D3Yxnrwr.mjs +1639 -0
  119. package/dist/versioning-M9lNLhO8.d.mts +117 -0
  120. package/dist/websocket-CyJ1VIFI.d.mts +186 -0
  121. package/package.json +26 -8
  122. package/skills/arc/SKILL.md +124 -39
  123. package/skills/arc/references/testing.md +212 -183
  124. package/dist/applyPermissionResult-QhV1Pa-g.mjs +0 -37
  125. package/dist/core-CcR01lup.mjs +0 -1411
  126. package/dist/createActionRouter-Bp_5c_2b.mjs +0 -249
  127. package/dist/errorHandler-DRQ3EqfL.d.mts +0 -218
  128. package/dist/errors-CCSsMpXE.d.mts +0 -140
  129. package/dist/fields-bxkeltzz.mjs +0 -126
  130. package/dist/filesUpload-t21LS-py.mjs +0 -377
  131. package/dist/queryParser-DBqBB6AC.mjs +0 -352
  132. package/dist/types-Csi3FLfq.mjs +0 -27
  133. package/dist/utils-B2fNOD_i.mjs +0 -929
  134. /package/dist/{EventTransport-CUw5NNWe.d.mts → EventTransport-CfVEGaEl.d.mts} +0 -0
  135. /package/dist/{HookSystem-BNYKnrXF.mjs → HookSystem-CGsMd6oK.mjs} +0 -0
  136. /package/dist/{ResourceRegistry-BPd6NQDm.mjs → ResourceRegistry-DkAeAuTX.mjs} +0 -0
  137. /package/dist/{caching-CBpK_SCM.mjs → caching-CheW3m-S.mjs} +0 -0
  138. /package/dist/{elevation-C5SwtkAn.d.mts → elevation-s5ykdNHr.d.mts} +0 -0
  139. /package/dist/{errorHandler-Bb49BvPD.mjs → errorHandler-BQm8ZxTK.mjs} +0 -0
  140. /package/dist/{externalPaths-BQ8QijNH.d.mts → externalPaths-Bapitwvd.d.mts} +0 -0
  141. /package/dist/{interface-CSbZdv_3.d.mts → interface-CkkWm5uR.d.mts} +0 -0
  142. /package/dist/{interface-D218ikEo.d.mts → interface-Da0r7Lna.d.mts} +0 -0
  143. /package/dist/{memory-B5Amv9A1.mjs → memory-DikHSvWa.mjs} +0 -0
  144. /package/dist/{metrics-DuhiSEZI.mjs → metrics-Csh4nsvv.mjs} +0 -0
  145. /package/dist/{pluralize-A0tWEl1K.mjs → pluralize-BneOJkpi.mjs} +0 -0
  146. /package/dist/{registry-B3lRFBWo.mjs → registry-D63ee7fl.mjs} +0 -0
  147. /package/dist/{replyHelpers-CXtJDAZ0.mjs → replyHelpers-ByllIXXV.mjs} +0 -0
  148. /package/dist/{schemaConverter-BxFDdtXu.mjs → schemaConverter-B0oKLuqI.mjs} +0 -0
  149. /package/dist/{sessionManager-BkzVU8h2.d.mts → sessionManager-D-oNWHz3.d.mts} +0 -0
  150. /package/dist/{storage-CVk_SEn2.d.mts → storage-BwGQXUpd.d.mts} +0 -0
  151. /package/dist/{typeGuards-Cj5Rgvlg.mjs → typeGuards-CcFZXgU7.mjs} +0 -0
  152. /package/dist/{types-BD85MlEK.d.mts → types-tgR4Pt8F.d.mts} +0 -0
  153. /package/dist/{versioning-C2U_bLY0.mjs → versioning-CGPjkqAg.mjs} +0 -0
@@ -1,6 +1,10 @@
1
- import { t as BaseController } from "./BaseController-CbKKIflT.mjs";
2
- import { n as normalizePermissionResult } from "./applyPermissionResult-QhV1Pa-g.mjs";
3
- import { t as pluralize } from "./pluralize-A0tWEl1K.mjs";
1
+ import { t as BaseController } from "./BaseController-JNV08qOT.mjs";
2
+ import { A as normalizePermissionResult } from "./permissions-B4vU9L0Q.mjs";
3
+ import { u as resolvePipelineSteps } from "./routerShared-DeESFp4a.mjs";
4
+ import { t as executePipeline } from "./pipe-DVoIheVC.mjs";
5
+ import { t as resolveActionPermission } from "./actionPermissions-C8YYU92K.mjs";
6
+ import { i as shouldRejectAdditionalProperties, r as schemaIRToZodShape, t as normalizeSchemaIR } from "./schemaIR-BlG9bY7v.mjs";
7
+ import { t as pluralize } from "./pluralize-BneOJkpi.mjs";
4
8
  import { z } from "zod";
5
9
  //#region src/integrations/mcp/createMcpServer.ts
6
10
  /**
@@ -373,6 +377,246 @@ function buildScope(auth) {
373
377
  };
374
378
  }
375
379
  //#endregion
380
+ //#region src/integrations/mcp/tool-helpers.ts
381
+ /**
382
+ * Evaluate a resource's permission check in MCP context.
383
+ *
384
+ * Returns the full normalized `PermissionResult` so the caller can honor
385
+ * ALL side-effects (filters + scope) consistently with CRUD/action routes.
386
+ * Returns `null` when no permission is defined (= allow, no side effects).
387
+ *
388
+ * Promoting booleans to `PermissionResult` via the shared
389
+ * `normalizePermissionResult` helper keeps the contract aligned with the
390
+ * rest of arc — one normalization path for every call site.
391
+ */
392
+ async function evaluatePermission(check, session, resource, action, input) {
393
+ if (!check) return null;
394
+ const user = session ? {
395
+ id: session.userId,
396
+ _id: session.userId,
397
+ ...session
398
+ } : null;
399
+ return normalizePermissionResult(await check({
400
+ user,
401
+ request: {
402
+ user,
403
+ headers: {},
404
+ params: {},
405
+ query: {},
406
+ body: input
407
+ },
408
+ resource,
409
+ action,
410
+ resourceId: typeof input.id === "string" ? input.id : void 0,
411
+ params: {},
412
+ data: input
413
+ }));
414
+ }
415
+ /**
416
+ * Convert a controller response envelope into an MCP `CallToolResult`.
417
+ * Carries `meta` into the serialized payload so consumers see pagination
418
+ * totals, stripped-field arrays, etc.
419
+ */
420
+ function toCallToolResult(result) {
421
+ if (!result.success) return {
422
+ content: [{
423
+ type: "text",
424
+ text: result.error ?? "Operation failed"
425
+ }],
426
+ isError: true
427
+ };
428
+ const output = result.meta ? {
429
+ data: result.data,
430
+ ...result.meta
431
+ } : result.data;
432
+ return { content: [{
433
+ type: "text",
434
+ text: JSON.stringify(output, null, 2)
435
+ }] };
436
+ }
437
+ /**
438
+ * Auto-create a BaseController from the resource's adapter for MCP use.
439
+ * Called when the resource has an adapter but no controller
440
+ * (e.g. `disableDefaultRoutes: true` skips controller creation in
441
+ * `defineResource`).
442
+ */
443
+ function createMcpController(resource) {
444
+ const repository = resource.adapter?.repository;
445
+ if (!repository) return void 0;
446
+ return new BaseController(repository, {
447
+ resourceName: resource.name,
448
+ schemaOptions: resource.schemaOptions,
449
+ tenantField: resource.tenantField,
450
+ idField: resource.idField,
451
+ matchesFilter: resource.adapter?.matchesFilter
452
+ });
453
+ }
454
+ //#endregion
455
+ //#region src/integrations/mcp/action-tools.ts
456
+ /**
457
+ * Convert an action's `schema` field into a Zod shape for MCP input.
458
+ *
459
+ * Delegates to the shared schema IR ([../../core/schemaIR.ts]). Same
460
+ * normalization path AJV sees on the HTTP side via `buildActionBodySchema`,
461
+ * so authors get one schema declaration for both surfaces. If the author
462
+ * declares `additionalProperties: false`, the flag is preserved on the IR;
463
+ * the MCP tool handler enforces it at request time (MCP's flat-shape input
464
+ * format can't express strict mode natively — see [./types.ts]).
465
+ */
466
+ function convertActionSchemaToZod(raw) {
467
+ return schemaIRToZodShape(normalizeSchemaIR(raw));
468
+ }
469
+ /**
470
+ * Build an MCP tool handler for a declarative action.
471
+ *
472
+ * Uses the SAME `evaluatePermission()` + `buildRequestContext()` as CRUD
473
+ * tools — single code path for permission side effects, scope construction,
474
+ * and request-context assembly. This eliminates the DRY/drift risk flagged
475
+ * in the 2.10.8 review: REST and MCP action tools share identical
476
+ * context-building machinery.
477
+ */
478
+ function createActionToolHandler(actionName, handler, permissions, resourceName, _resourcePermissions, rawSchema) {
479
+ const ir = rawSchema ? normalizeSchemaIR(rawSchema) : void 0;
480
+ const allowedKeys = (ir ? shouldRejectAdditionalProperties(ir) : false) && ir ? new Set(["id", ...Object.keys(ir.properties)]) : void 0;
481
+ return async (input, ctx) => {
482
+ const session = ctx.session;
483
+ if (allowedKeys) {
484
+ const extras = Object.keys(input).filter((k) => !allowedKeys.has(k));
485
+ if (extras.length > 0) return {
486
+ content: [{
487
+ type: "text",
488
+ text: JSON.stringify({
489
+ success: false,
490
+ error: `Unknown properties not allowed: ${extras.join(", ")}`,
491
+ details: {
492
+ action: actionName,
493
+ unexpected: extras
494
+ }
495
+ })
496
+ }],
497
+ isError: true
498
+ };
499
+ }
500
+ const permResult = await evaluatePermission(permissions, session, resourceName, actionName, input);
501
+ if (permResult && !permResult.granted) return {
502
+ content: [{
503
+ type: "text",
504
+ text: JSON.stringify({
505
+ success: false,
506
+ error: permResult.reason ?? `Permission denied for action '${actionName}'`
507
+ })
508
+ }],
509
+ isError: true
510
+ };
511
+ const reqCtx = buildRequestContext({
512
+ ...input,
513
+ action: actionName
514
+ }, session, "action", permResult?.filters, permResult?.scope);
515
+ const id = typeof input.id === "string" ? input.id : "";
516
+ const { id: _discardId, ...data } = input;
517
+ try {
518
+ const result = await handler(id, data, reqCtx);
519
+ return { content: [{
520
+ type: "text",
521
+ text: JSON.stringify({
522
+ success: true,
523
+ data: result
524
+ })
525
+ }] };
526
+ } catch (err) {
527
+ return {
528
+ content: [{
529
+ type: "text",
530
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
531
+ }],
532
+ isError: true
533
+ };
534
+ }
535
+ };
536
+ }
537
+ //#endregion
538
+ //#region src/integrations/mcp/crud-tools.ts
539
+ const ALL_CRUD_OPS = [
540
+ "list",
541
+ "get",
542
+ "create",
543
+ "update",
544
+ "delete"
545
+ ];
546
+ const CRUD_ANNOTATIONS = {
547
+ list: { readOnlyHint: true },
548
+ get: { readOnlyHint: true },
549
+ create: { destructiveHint: false },
550
+ update: {
551
+ destructiveHint: true,
552
+ idempotentHint: true
553
+ },
554
+ delete: {
555
+ destructiveHint: true,
556
+ idempotentHint: true
557
+ }
558
+ };
559
+ /**
560
+ * Build a handler that dispatches to the controller method for `op`,
561
+ * passing through arc's MCP → IRequestContext adapter. Permission check
562
+ * runs first and short-circuits with a structured tool error on denial.
563
+ */
564
+ function createCrudHandler(op, controller, resourceName, permissions) {
565
+ const ctrl = controller;
566
+ return async (input, ctx) => {
567
+ try {
568
+ const method = ctrl[op];
569
+ if (typeof method !== "function") return {
570
+ content: [{
571
+ type: "text",
572
+ text: `Operation "${op}" not available on ${resourceName}`
573
+ }],
574
+ isError: true
575
+ };
576
+ const permResult = await evaluatePermission(permissions?.[op], ctx.session, resourceName, op, input);
577
+ if (permResult && !permResult.granted) return {
578
+ content: [{
579
+ type: "text",
580
+ text: `Permission denied: ${op} on ${resourceName}${permResult.reason ? ` — ${permResult.reason}` : ""}`
581
+ }],
582
+ isError: true
583
+ };
584
+ return toCallToolResult(await method(buildRequestContext(input, ctx.session, op, permResult?.filters, permResult?.scope)));
585
+ } catch (err) {
586
+ const msg = err instanceof Error ? err.message : String(err);
587
+ ctx.log("error", `${resourceName}.${op}: ${msg}`).catch(() => {});
588
+ return {
589
+ content: [{
590
+ type: "text",
591
+ text: `Error: ${msg}`
592
+ }],
593
+ isError: true
594
+ };
595
+ }
596
+ };
597
+ }
598
+ /**
599
+ * Default description for a CRUD tool. Enriches list descriptions with the
600
+ * configured filter/sort metadata so MCP clients can see what's queryable
601
+ * without reading the resource source.
602
+ */
603
+ function defaultCrudDescription(op, displayName, softDelete, queryMeta) {
604
+ const name = displayName.toLowerCase();
605
+ switch (op) {
606
+ case "list": {
607
+ const parts = [`List ${pluralize(name)} with optional filters and pagination.`];
608
+ if (queryMeta?.filterableFields?.length) parts.push(`Filterable fields: ${queryMeta.filterableFields.join(", ")}.`);
609
+ if (queryMeta?.allowedOperators?.length) parts.push(`Filter operators: ${queryMeta.allowedOperators.join(", ")} (use field[op]=value syntax).`);
610
+ if (queryMeta?.sortableFields?.length) parts.push(`Sortable fields: ${queryMeta.sortableFields.join(", ")}.`);
611
+ return parts.join(" ");
612
+ }
613
+ case "get": return `Get a single ${name} by ID`;
614
+ case "create": return `Create a new ${name}`;
615
+ case "update": return `Update an existing ${name} by ID`;
616
+ case "delete": return softDelete ? `Delete a ${name} by ID (soft delete — marks as deleted, not permanently removed)` : `Delete a ${name} by ID`;
617
+ }
618
+ }
619
+ //#endregion
376
620
  //#region src/integrations/mcp/jsonSchemaToZod.ts
377
621
  /**
378
622
  * @classytic/arc — JSON Schema → Zod shape converter
@@ -513,195 +757,61 @@ function applyDescription(zodType, prop) {
513
757
  return zodType;
514
758
  }
515
759
  //#endregion
516
- //#region src/integrations/mcp/resourceToTools.ts
760
+ //#region src/integrations/mcp/input-schema.ts
517
761
  /**
518
- * @classytic/arc Resource MCP Tools Generator
762
+ * MCP tool input schema generation.
519
763
  *
520
- * Converts a ResourceDefinition into an array of ToolDefinitions.
521
- * Core auto-generation logic that powers Level 1 (mcpPlugin).
764
+ * One of four internal units extracted from `resourceToTools.ts` in
765
+ * v2.11.0. Owns the translation from arc's fieldRules / adapter-generated
766
+ * body schemas into the Zod shapes MCP tools expect.
522
767
  *
523
- * All tool handlers call BaseController methods — same pipeline as REST.
768
+ * Exports:
769
+ * - `buildInputSchema` — the switch on CRUD op that picks between the
770
+ * fieldRules path and the high-fidelity adapter-body path.
771
+ * - `getAdapterBodies` — pulls `createBody` / `updateBody` from the
772
+ * adapter once so callers can reuse them for both paths.
773
+ * - `deriveFieldRulesFromAdapter` — fallback FieldRules derivation for
774
+ * the list/filter path when the host didn't supply explicit rules.
524
775
  */
525
- const ALL_CRUD_OPS = [
526
- "list",
527
- "get",
528
- "create",
529
- "update",
530
- "delete"
531
- ];
532
- const ANNOTATIONS = {
533
- list: { readOnlyHint: true },
534
- get: { readOnlyHint: true },
535
- create: { destructiveHint: false },
536
- update: {
537
- destructiveHint: true,
538
- idempotentHint: true
539
- },
540
- delete: {
541
- destructiveHint: true,
542
- idempotentHint: true
776
+ function buildInputSchema(op, fieldRules, opts) {
777
+ switch (op) {
778
+ case "list": return fieldRulesToZod(fieldRules, {
779
+ mode: "list",
780
+ ...opts
781
+ });
782
+ case "get": return getIdShape();
783
+ case "create":
784
+ if (!fieldRules && opts.adapterBodies?.createBody) {
785
+ const shape = jsonSchemaToZodShape(opts.adapterBodies.createBody, "create");
786
+ if (shape) return shape;
787
+ }
788
+ return fieldRulesToZod(fieldRules, {
789
+ mode: "create",
790
+ ...opts
791
+ });
792
+ case "update": {
793
+ const idShape = getIdShape();
794
+ if (!fieldRules && opts.adapterBodies?.updateBody) {
795
+ const shape = jsonSchemaToZodShape(opts.adapterBodies.updateBody, "update");
796
+ if (shape) return {
797
+ ...idShape,
798
+ ...shape
799
+ };
800
+ }
801
+ return {
802
+ ...idShape,
803
+ ...fieldRulesToZod(fieldRules, {
804
+ mode: "update",
805
+ ...opts
806
+ })
807
+ };
808
+ }
809
+ case "delete": return getIdShape();
543
810
  }
544
- };
545
- /**
546
- * Convert a ResourceDefinition into MCP ToolDefinitions.
547
- *
548
- * MCP tools call BaseController directly — they bypass HTTP routes entirely.
549
- * Therefore `disableDefaultRoutes` does NOT affect MCP tool generation;
550
- * only `disabledRoutes` (the per-operation array) controls which ops are skipped.
551
- *
552
- * If the resource has an adapter but no controller (e.g. `disableDefaultRoutes: true`),
553
- * a lightweight BaseController is auto-created from the adapter for MCP use.
554
- *
555
- * @param resource - Arc resource definition
556
- * @param config - Optional overrides (operations, descriptions, hideFields, prefix, names)
557
- */
558
- function resourceToTools(resource, config = {}) {
559
- const controller = resource.controller ?? (resource.adapter ? createMcpController(resource) : void 0);
560
- const explicitFieldRules = resource.schemaOptions?.fieldRules;
561
- const hiddenFields = resource.schemaOptions?.hiddenFields;
562
- const readonlyFields = resource.schemaOptions?.readonlyFields;
563
- const adapterBodies = explicitFieldRules ? void 0 : getAdapterBodies(resource);
564
- const fieldRules = explicitFieldRules ?? deriveFieldRulesFromAdapter(resource);
565
- const filterableFields = resource.schemaOptions?.filterableFields ?? resource.queryParser?.allowedFilterFields;
566
- const sortableFields = resource.queryParser?.allowedSortFields;
567
- const allowedOperators = resource.queryParser?.allowedOperators;
568
- const hasSoftDelete = resource._appliedPresets?.includes("softDelete") ?? false;
569
- const tools = [];
570
- const prefix = config.toolNamePrefix;
571
- if (controller) {
572
- let ops = ALL_CRUD_OPS.filter((op) => {
573
- if (resource.disabledRoutes?.includes(op)) return false;
574
- return true;
575
- });
576
- if (config.operations) ops = ops.filter((op) => config.operations?.includes(op));
577
- for (const op of ops) {
578
- const name = config.names?.[op] ?? (op === "list" ? `${prefix ? `${prefix}_` : ""}list_${pluralize(resource.name)}` : `${prefix ? `${prefix}_` : ""}${op}_${resource.name}`);
579
- tools.push({
580
- name,
581
- description: config.descriptions?.[op] ?? defaultDescription(op, resource.displayName, hasSoftDelete, {
582
- filterableFields,
583
- allowedOperators,
584
- sortableFields
585
- }),
586
- annotations: ANNOTATIONS[op],
587
- inputSchema: buildInputSchema(op, fieldRules, {
588
- hiddenFields,
589
- readonlyFields,
590
- extraHideFields: config.hideFields,
591
- filterableFields,
592
- allowedOperators,
593
- adapterBodies
594
- }),
595
- handler: createHandler(op, controller, resource.name, resource.permissions)
596
- });
597
- }
598
- }
599
- for (const route of resource.routes ?? []) {
600
- if (route.mcp === false) continue;
601
- const mcpHandler = route.mcpHandler;
602
- if (!!route.raw && !mcpHandler) continue;
603
- if (!mcpHandler && ![
604
- "POST",
605
- "PUT",
606
- "PATCH",
607
- "DELETE"
608
- ].includes(route.method)) continue;
609
- if (!mcpHandler && typeof route.handler === "string" && !controller) continue;
610
- const opName = route.operation ?? slugifyRoute(route.method, route.path);
611
- const hasId = route.path.includes(":id");
612
- const mcpConfig = typeof route.mcp === "object" && route.mcp !== null ? route.mcp : void 0;
613
- const toolDescription = mcpConfig?.description ?? route.summary ?? route.description ?? `${opName} on ${resource.displayName}`;
614
- const toolAnnotations = mcpConfig?.annotations ? { ...mcpConfig.annotations } : { openWorldHint: true };
615
- const inputShape = {};
616
- if (hasId) inputShape.id = z.string().describe("Resource ID");
617
- if (mcpHandler) tools.push({
618
- name: prefix ? `${prefix}_${opName}_${resource.name}` : `${opName}_${resource.name}`,
619
- description: toolDescription,
620
- annotations: toolAnnotations,
621
- inputSchema: inputShape,
622
- handler: async (input, _ctx) => {
623
- try {
624
- return await mcpHandler(input);
625
- } catch (err) {
626
- return {
627
- content: [{
628
- type: "text",
629
- text: `Error: ${err instanceof Error ? err.message : String(err)}`
630
- }],
631
- isError: true
632
- };
633
- }
634
- }
635
- });
636
- else tools.push({
637
- name: prefix ? `${prefix}_${opName}_${resource.name}` : `${opName}_${resource.name}`,
638
- description: toolDescription,
639
- annotations: toolAnnotations,
640
- inputSchema: inputShape,
641
- handler: createCustomRouteHandler(route, controller, hasId)
642
- });
643
- }
644
- if (resource.actions) for (const [actionName, entry] of Object.entries(resource.actions)) {
645
- const def = typeof entry === "function" ? { handler: entry } : entry;
646
- if (typeof def !== "function" && "mcp" in def && def.mcp === false) continue;
647
- const mcpCfg = typeof def !== "function" && typeof def.mcp === "object" ? def.mcp : void 0;
648
- const description = mcpCfg?.description ?? (typeof def !== "function" ? def.description : void 0) ?? `${actionName} action on ${resource.displayName}`;
649
- const annotations = mcpCfg?.annotations ? { ...mcpCfg.annotations } : { destructiveHint: true };
650
- const inputShape = { id: z.string().describe("Resource ID") };
651
- const rawSchema = typeof def !== "function" ? def.schema : void 0;
652
- if (rawSchema && typeof rawSchema === "object") {
653
- const converted = convertActionSchemaToZod(rawSchema);
654
- for (const [key, val] of Object.entries(converted)) inputShape[key] = val;
655
- }
656
- const toolName = prefix ? `${prefix}_${actionName}_${resource.name}` : `${actionName}_${resource.name}`;
657
- const handler = typeof entry === "function" ? entry : def.handler;
658
- const actionPerms = (typeof def !== "function" ? def.permissions : void 0) ?? resource.actionPermissions;
659
- tools.push({
660
- name: toolName,
661
- description: String(description),
662
- annotations,
663
- inputSchema: inputShape,
664
- handler: createActionToolHandler(actionName, handler, actionPerms, resource.name, resource.permissions)
665
- });
666
- }
667
- return tools;
668
- }
669
- function buildInputSchema(op, fieldRules, opts) {
670
- switch (op) {
671
- case "list": return fieldRulesToZod(fieldRules, {
672
- mode: "list",
673
- ...opts
674
- });
675
- case "get": return { id: z.string().describe("Resource ID") };
676
- case "create":
677
- if (!fieldRules && opts.adapterBodies?.createBody) {
678
- const shape = jsonSchemaToZodShape(opts.adapterBodies.createBody, "create");
679
- if (shape) return shape;
680
- }
681
- return fieldRulesToZod(fieldRules, {
682
- mode: "create",
683
- ...opts
684
- });
685
- case "update": {
686
- const idShape = { id: z.string().describe("Resource ID") };
687
- if (!fieldRules && opts.adapterBodies?.updateBody) {
688
- const shape = jsonSchemaToZodShape(opts.adapterBodies.updateBody, "update");
689
- if (shape) return {
690
- ...idShape,
691
- ...shape
692
- };
693
- }
694
- return {
695
- ...idShape,
696
- ...fieldRulesToZod(fieldRules, {
697
- mode: "update",
698
- ...opts
699
- })
700
- };
701
- }
702
- case "delete": return { id: z.string().describe("Resource ID") };
703
- }
704
- }
811
+ }
812
+ function getIdShape() {
813
+ return { id: z.string().describe("Resource ID") };
814
+ }
705
815
  /**
706
816
  * Pull the adapter's `createBody` / `updateBody` schemas, if any.
707
817
  * Returns `undefined` when the adapter doesn't generate schemas or throws.
@@ -724,115 +834,6 @@ function getAdapterBodies(resource) {
724
834
  return;
725
835
  }
726
836
  }
727
- function createHandler(op, controller, resourceName, permissions) {
728
- const ctrl = controller;
729
- return async (input, ctx) => {
730
- try {
731
- const method = ctrl[op];
732
- if (typeof method !== "function") return {
733
- content: [{
734
- type: "text",
735
- text: `Operation "${op}" not available on ${resourceName}`
736
- }],
737
- isError: true
738
- };
739
- const permResult = await evaluatePermission(permissions?.[op], ctx.session, resourceName, op, input);
740
- if (permResult && !permResult.granted) return {
741
- content: [{
742
- type: "text",
743
- text: `Permission denied: ${op} on ${resourceName}${permResult.reason ? ` — ${permResult.reason}` : ""}`
744
- }],
745
- isError: true
746
- };
747
- return toCallToolResult(await method(buildRequestContext(input, ctx.session, op, permResult?.filters, permResult?.scope)));
748
- } catch (err) {
749
- const msg = err instanceof Error ? err.message : String(err);
750
- ctx.log("error", `${resourceName}.${op}: ${msg}`).catch(() => {});
751
- return {
752
- content: [{
753
- type: "text",
754
- text: `Error: ${msg}`
755
- }],
756
- isError: true
757
- };
758
- }
759
- };
760
- }
761
- function createCustomRouteHandler(route, controller, hasId) {
762
- const ctrl = controller;
763
- const handlerName = typeof route.handler === "string" ? route.handler : route.operation ?? slugifyRoute(route.method, route.path);
764
- return async (input, ctx) => {
765
- try {
766
- if (typeof route.handler === "function") {
767
- const reqCtx = buildRequestContext(input, ctx.session, hasId ? "update" : "create");
768
- const fn = route.handler;
769
- const out = await fn(reqCtx);
770
- return toCallToolResult(out !== null && typeof out === "object" && "success" in out ? out : {
771
- success: true,
772
- data: out
773
- });
774
- }
775
- if (!ctrl) return {
776
- content: [{
777
- type: "text",
778
- text: `Handler "${handlerName}" has no controller available`
779
- }],
780
- isError: true
781
- };
782
- const method = ctrl[handlerName];
783
- if (typeof method !== "function") return {
784
- content: [{
785
- type: "text",
786
- text: `Handler "${handlerName}" not found on controller`
787
- }],
788
- isError: true
789
- };
790
- return toCallToolResult(await method(buildRequestContext(input, ctx.session, hasId ? "update" : "create")));
791
- } catch (err) {
792
- return {
793
- content: [{
794
- type: "text",
795
- text: `Error: ${err instanceof Error ? err.message : String(err)}`
796
- }],
797
- isError: true
798
- };
799
- }
800
- };
801
- }
802
- /**
803
- * Evaluate a resource's permission check in MCP context.
804
- *
805
- * Returns the full normalized `PermissionResult` so the caller can honor
806
- * ALL side-effects (filters + scope) consistently with CRUD/action routes.
807
- * Returns `null` when no permission is defined (= allow, no side effects).
808
- *
809
- * Promoting booleans to `PermissionResult` via the shared `normalizePermissionResult`
810
- * helper keeps the contract aligned with the rest of Arc — there is a single
811
- * normalization path for every call site.
812
- */
813
- async function evaluatePermission(check, session, resource, action, input) {
814
- if (!check) return null;
815
- const user = session ? {
816
- id: session.userId,
817
- _id: session.userId,
818
- ...session
819
- } : null;
820
- return normalizePermissionResult(await check({
821
- user,
822
- request: {
823
- user,
824
- headers: {},
825
- params: {},
826
- query: {},
827
- body: input
828
- },
829
- resource,
830
- action,
831
- resourceId: typeof input.id === "string" ? input.id : void 0,
832
- params: {},
833
- data: input
834
- }));
835
- }
836
837
  /**
837
838
  * Derive a fieldRules-shaped object from the adapter's auto-generated body
838
839
  * schemas. Used as a fallback when the resource doesn't supply explicit
@@ -890,141 +891,90 @@ function mapJsonSchemaTypeToArcType(jsonType) {
890
891
  default: return "string";
891
892
  }
892
893
  }
893
- function toCallToolResult(result) {
894
- if (!result.success) return {
895
- content: [{
896
- type: "text",
897
- text: result.error ?? "Operation failed"
898
- }],
899
- isError: true
900
- };
901
- const output = result.meta ? {
902
- data: result.data,
903
- ...result.meta
904
- } : result.data;
905
- return { content: [{
906
- type: "text",
907
- text: JSON.stringify(output, null, 2)
908
- }] };
909
- }
910
- function defaultDescription(op, displayName, softDelete, queryMeta) {
911
- const name = displayName.toLowerCase();
912
- switch (op) {
913
- case "list": {
914
- const parts = [`List ${pluralize(name)} with optional filters and pagination.`];
915
- if (queryMeta?.filterableFields?.length) parts.push(`Filterable fields: ${queryMeta.filterableFields.join(", ")}.`);
916
- if (queryMeta?.allowedOperators?.length) parts.push(`Filter operators: ${queryMeta.allowedOperators.join(", ")} (use field[op]=value syntax).`);
917
- if (queryMeta?.sortableFields?.length) parts.push(`Sortable fields: ${queryMeta.sortableFields.join(", ")}.`);
918
- return parts.join(" ");
919
- }
920
- case "get": return `Get a single ${name} by ID`;
921
- case "create": return `Create a new ${name}`;
922
- case "update": return `Update an existing ${name} by ID`;
923
- case "delete": return softDelete ? `Delete a ${name} by ID (soft delete — marks as deleted, not permanently removed)` : `Delete a ${name} by ID`;
924
- }
925
- }
926
- function slugifyRoute(method, path) {
927
- const clean = path.replace(/:[^/]+/g, "").replace(/^\/+|\/+$/g, "").replace(/\//g, "_");
928
- return clean ? `${method.toLowerCase()}_${clean}` : method.toLowerCase();
929
- }
930
- /**
931
- * Auto-create a BaseController from the resource's adapter for MCP use.
932
- * Called when the resource has an adapter but no controller
933
- * (e.g. `disableDefaultRoutes: true` skips controller creation in defineResource).
934
- */
935
- function createMcpController(resource) {
936
- const repository = resource.adapter?.repository;
937
- if (!repository) return void 0;
938
- return new BaseController(repository, {
939
- resourceName: resource.name,
940
- schemaOptions: resource.schemaOptions,
941
- tenantField: resource.tenantField,
942
- idField: resource.idField,
943
- matchesFilter: resource.adapter?.matchesFilter
944
- });
945
- }
894
+ //#endregion
895
+ //#region src/integrations/mcp/route-tools.ts
946
896
  /**
947
- * Convert an action schema (JSON Schema, Zod, or legacy field map) to a Zod
948
- * shape for MCP tool input. This mirrors `normalizeActionSchema` in
949
- * `createActionRouter.ts` but produces Zod types for the MCP SDK.
897
+ * Custom-route MCP tool generation.
898
+ *
899
+ * Converts arc's `routes[]` entries (declared via `defineResource({
900
+ * routes: [...] })`) into MCP tools. Three handler shapes are supported:
901
+ *
902
+ * 1. `mcpHandler` (full bypass) — caller-supplied function owns the whole
903
+ * tool result; pipeline is not invoked.
904
+ * 2. Function handler with `raw: false/undefined` — arc's pipeline wrapper
905
+ * runs normally, and the envelope is serialized into the tool result.
906
+ * 3. String handler — looks up a method on the controller by name.
950
907
  */
951
- function convertActionSchemaToZod(raw) {
952
- if ("_zod" in raw && typeof raw.shape === "object") return { ...raw.shape };
953
- if ((raw.type === "object" || "properties" in raw) && typeof raw.properties === "object" && raw.properties !== null) {
954
- const props = raw.properties;
955
- return jsonSchemaPropsToZod(props, new Set(Array.isArray(raw.required) ? raw.required : []));
956
- }
957
- const result = {};
958
- for (const [fieldName, fieldSchema] of Object.entries(raw)) {
959
- if (fieldName === "type" || fieldName === "properties" || fieldName === "required") continue;
960
- if (!fieldSchema || typeof fieldSchema !== "object") continue;
961
- const fs = fieldSchema;
962
- const desc = typeof fs.description === "string" ? fs.description : `${fieldName} field`;
963
- const isOptional = fs.required === false;
964
- const base = jsonSchemaTypeToZod(fs);
965
- result[fieldName] = isOptional ? base.optional().describe(desc) : base.describe(desc);
966
- }
967
- return result;
968
- }
969
- function jsonSchemaPropsToZod(props, requiredSet) {
970
- const result = {};
971
- for (const [name, schema] of Object.entries(props)) {
972
- const desc = typeof schema.description === "string" ? schema.description : name;
973
- const base = jsonSchemaTypeToZod(schema);
974
- result[name] = requiredSet.has(name) ? base.describe(desc) : base.optional().describe(desc);
975
- }
976
- return result;
977
- }
978
- function jsonSchemaTypeToZod(schema) {
979
- const type = typeof schema.type === "string" ? schema.type : "string";
980
- if (Array.isArray(schema.enum) && schema.enum.length > 0) return z.enum(schema.enum);
981
- switch (type) {
982
- case "number":
983
- case "integer": return z.number();
984
- case "boolean": return z.boolean();
985
- case "array": return z.array(z.unknown());
986
- case "object": return z.record(z.string(), z.unknown());
987
- default: return z.string();
988
- }
989
- }
990
908
  /**
991
- * Create an MCP tool handler for a declarative action.
909
+ * Build an MCP tool handler for a custom route.
910
+ *
911
+ * Enforces the same contract as the REST route:
912
+ * 1. **Permission evaluation** via the shared `evaluatePermission` — the
913
+ * exact path CRUD and action MCP tools use. Filters and scope from a
914
+ * `PermissionResult` thread through `buildRequestContext`.
915
+ * 2. **Pipeline integration** — function handlers run inside
916
+ * `executePipeline` with the same steps the HTTP path resolves.
917
+ * 3. **Controller dispatch** for string handlers.
992
918
  *
993
- * Uses the SAME `evaluatePermission()` and `buildRequestContext()` as
994
- * CRUD tools single code path for permission side effects, scope
995
- * construction, and request context assembly. This eliminates the
996
- * DRY/drift risk flagged in the review: REST and MCP action tools now
997
- * share identical context-building machinery.
919
+ * `hasId` signals whether the route path uses `:id`, which determines
920
+ * whether we treat the call as an update-shaped or create-shaped request
921
+ * when hydrating the request context.
998
922
  */
999
- function createActionToolHandler(actionName, handler, permissions, resourceName, _resourcePermissions) {
1000
- return async (input, ctx) => {
1001
- const session = ctx.session;
1002
- const permResult = await evaluatePermission(permissions, session, resourceName, actionName, input);
923
+ function createCustomRouteHandler(route, controller, hasId, options) {
924
+ const ctrl = controller;
925
+ const handlerName = typeof route.handler === "string" ? route.handler : route.operation ?? slugifyRoute(route.method, route.path);
926
+ const { resourceName, operationName, permissions, pipeline } = options;
927
+ const pipelineSteps = resolvePipelineSteps(pipeline, operationName);
928
+ return async (input, _ctx) => {
929
+ const session = _ctx.session;
930
+ const permResult = await evaluatePermission(permissions, session, resourceName, operationName, input);
1003
931
  if (permResult && !permResult.granted) return {
1004
932
  content: [{
1005
933
  type: "text",
1006
934
  text: JSON.stringify({
1007
935
  success: false,
1008
- error: permResult.reason ?? `Permission denied for action '${actionName}'`
936
+ error: permResult.reason ?? (session ? `Permission denied for '${operationName}'` : "Authentication required")
1009
937
  })
1010
938
  }],
1011
939
  isError: true
1012
940
  };
1013
- const reqCtx = buildRequestContext({
1014
- ...input,
1015
- action: actionName
1016
- }, session, "action", permResult?.filters, permResult?.scope);
1017
- const id = typeof input.id === "string" ? input.id : "";
1018
- const { id: _discardId, ...data } = input;
1019
941
  try {
1020
- const result = await handler(id, data, reqCtx);
1021
- return { content: [{
1022
- type: "text",
1023
- text: JSON.stringify({
942
+ const reqCtx = buildRequestContext(input, session, hasId ? "update" : "create", permResult?.filters, permResult?.scope);
943
+ if (typeof route.handler === "function") {
944
+ const fn = route.handler;
945
+ if (pipelineSteps.length > 0) return toCallToolResult(await executePipeline(pipelineSteps, {
946
+ ...reqCtx,
947
+ resource: resourceName,
948
+ operation: operationName
949
+ }, async (ctx) => {
950
+ const raw = await fn(ctx);
951
+ return raw !== null && typeof raw === "object" && "success" in raw ? raw : {
952
+ success: true,
953
+ data: raw
954
+ };
955
+ }, operationName));
956
+ const out = await fn(reqCtx);
957
+ return toCallToolResult(out !== null && typeof out === "object" && "success" in out ? out : {
1024
958
  success: true,
1025
- data: result
1026
- })
1027
- }] };
959
+ data: out
960
+ });
961
+ }
962
+ if (!ctrl) return {
963
+ content: [{
964
+ type: "text",
965
+ text: `Handler "${handlerName}" has no controller available`
966
+ }],
967
+ isError: true
968
+ };
969
+ const method = ctrl[handlerName];
970
+ if (typeof method !== "function") return {
971
+ content: [{
972
+ type: "text",
973
+ text: `Handler "${handlerName}" not found on controller`
974
+ }],
975
+ isError: true
976
+ };
977
+ return toCallToolResult(await method(reqCtx));
1028
978
  } catch (err) {
1029
979
  return {
1030
980
  content: [{
@@ -1036,5 +986,174 @@ function createActionToolHandler(actionName, handler, permissions, resourceName,
1036
986
  }
1037
987
  };
1038
988
  }
989
+ /**
990
+ * Build an MCP tool handler around a caller-supplied `mcpHandler` — no
991
+ * pipeline, no envelope translation, the function owns the whole
992
+ * `CallToolResult`. Only surfaces errors as tool-error results.
993
+ */
994
+ function createMcpHandlerPassthrough(mcpHandler) {
995
+ return async (input) => {
996
+ try {
997
+ return await mcpHandler(input);
998
+ } catch (err) {
999
+ return {
1000
+ content: [{
1001
+ type: "text",
1002
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
1003
+ }],
1004
+ isError: true
1005
+ };
1006
+ }
1007
+ };
1008
+ }
1009
+ /**
1010
+ * Slugify `{method, path}` into a readable tool-operation name when the
1011
+ * route definition doesn't supply an explicit `operation`.
1012
+ */
1013
+ function slugifyRoute(method, path) {
1014
+ const clean = path.replace(/:[^/]+/g, "").replace(/^\/+|\/+$/g, "").replace(/\//g, "_");
1015
+ return clean ? `${method.toLowerCase()}_${clean}` : method.toLowerCase();
1016
+ }
1017
+ //#endregion
1018
+ //#region src/integrations/mcp/resourceToTools.ts
1019
+ /**
1020
+ * @classytic/arc — Resource → MCP Tools orchestrator.
1021
+ *
1022
+ * Top-level entry point for generating `ToolDefinition[]` from a
1023
+ * `ResourceDefinition`. Delegates the heavy lifting to four focused
1024
+ * internal units (v2.11.0 split):
1025
+ *
1026
+ * - [input-schema.ts](./input-schema.ts) — CRUD input-shape generation
1027
+ * - [crud-tools.ts](./crud-tools.ts) — CRUD handler + annotations + descriptions
1028
+ * - [route-tools.ts](./route-tools.ts) — custom-route → tool translation
1029
+ * - [action-tools.ts](./action-tools.ts) — declarative-action → tool translation
1030
+ *
1031
+ * This file's job is purely orchestration: pick the controller, gather
1032
+ * field rules once, and loop over CRUD / routes / actions delegating
1033
+ * each tool's construction to the matching unit.
1034
+ *
1035
+ * All tool handlers call BaseController methods — same pipeline as REST.
1036
+ */
1037
+ /**
1038
+ * Convert a ResourceDefinition into MCP ToolDefinitions.
1039
+ *
1040
+ * MCP tools call BaseController directly — they bypass HTTP routes entirely.
1041
+ * Therefore `disableDefaultRoutes` does NOT affect MCP tool generation;
1042
+ * only `disabledRoutes` (the per-operation array) controls which ops are skipped.
1043
+ *
1044
+ * If the resource has an adapter but no controller (e.g. `disableDefaultRoutes: true`),
1045
+ * a lightweight BaseController is auto-created from the adapter for MCP use.
1046
+ *
1047
+ * @param resource - Arc resource definition
1048
+ * @param config - Optional overrides (operations, descriptions, hideFields, prefix, names)
1049
+ */
1050
+ function resourceToTools(resource, config = {}) {
1051
+ const controller = resource.controller ?? (resource.adapter ? createMcpController(resource) : void 0);
1052
+ const explicitFieldRules = resource.schemaOptions?.fieldRules;
1053
+ const hiddenFields = resource.schemaOptions?.hiddenFields;
1054
+ const readonlyFields = resource.schemaOptions?.readonlyFields;
1055
+ const adapterBodies = explicitFieldRules ? void 0 : getAdapterBodies(resource);
1056
+ const fieldRules = explicitFieldRules ?? deriveFieldRulesFromAdapter(resource);
1057
+ const filterableFields = resource.schemaOptions?.filterableFields ?? resource.queryParser?.allowedFilterFields;
1058
+ const sortableFields = resource.queryParser?.allowedSortFields;
1059
+ const allowedOperators = resource.queryParser?.allowedOperators;
1060
+ const hasSoftDelete = resource._appliedPresets?.includes("softDelete") ?? false;
1061
+ const tools = [];
1062
+ const prefix = config.toolNamePrefix;
1063
+ if (controller) {
1064
+ let ops = ALL_CRUD_OPS.filter((op) => !resource.disabledRoutes?.includes(op));
1065
+ if (config.operations) ops = ops.filter((op) => config.operations?.includes(op));
1066
+ for (const op of ops) {
1067
+ const name = config.names?.[op] ?? (op === "list" ? `${prefix ? `${prefix}_` : ""}list_${pluralize(resource.name)}` : `${prefix ? `${prefix}_` : ""}${op}_${resource.name}`);
1068
+ tools.push({
1069
+ name,
1070
+ description: config.descriptions?.[op] ?? defaultCrudDescription(op, resource.displayName, hasSoftDelete, {
1071
+ filterableFields,
1072
+ allowedOperators,
1073
+ sortableFields
1074
+ }),
1075
+ annotations: CRUD_ANNOTATIONS[op],
1076
+ inputSchema: buildInputSchema(op, fieldRules, {
1077
+ hiddenFields,
1078
+ readonlyFields,
1079
+ extraHideFields: config.hideFields,
1080
+ filterableFields,
1081
+ allowedOperators,
1082
+ adapterBodies
1083
+ }),
1084
+ handler: createCrudHandler(op, controller, resource.name, resource.permissions)
1085
+ });
1086
+ }
1087
+ }
1088
+ for (const route of resource.routes ?? []) {
1089
+ if (route.mcp === false) continue;
1090
+ const mcpHandler = route.mcpHandler;
1091
+ if (!!route.raw && !mcpHandler) continue;
1092
+ if (!mcpHandler && ![
1093
+ "POST",
1094
+ "PUT",
1095
+ "PATCH",
1096
+ "DELETE"
1097
+ ].includes(route.method)) continue;
1098
+ if (!mcpHandler && typeof route.handler === "string" && !controller) continue;
1099
+ const opName = route.operation ?? slugifyRoute(route.method, route.path);
1100
+ const hasId = route.path.includes(":id");
1101
+ const mcpConfig = typeof route.mcp === "object" && route.mcp !== null ? route.mcp : void 0;
1102
+ const toolDescription = mcpConfig?.description ?? route.summary ?? route.description ?? `${opName} on ${resource.displayName}`;
1103
+ const toolAnnotations = mcpConfig?.annotations ? { ...mcpConfig.annotations } : { openWorldHint: true };
1104
+ const inputShape = {};
1105
+ if (hasId) inputShape.id = z.string().describe("Resource ID");
1106
+ const routeSchema = route.schema;
1107
+ if (routeSchema?.body) {
1108
+ const ir = normalizeSchemaIR(routeSchema.body);
1109
+ for (const [key, val] of Object.entries(schemaIRToZodShape(ir))) inputShape[key] = val;
1110
+ }
1111
+ if (routeSchema?.querystring) {
1112
+ const ir = normalizeSchemaIR(routeSchema.querystring);
1113
+ for (const [key, val] of Object.entries(schemaIRToZodShape(ir))) if (!(key in inputShape)) inputShape[key] = val;
1114
+ }
1115
+ const toolName = prefix ? `${prefix}_${opName}_${resource.name}` : `${opName}_${resource.name}`;
1116
+ tools.push({
1117
+ name: toolName,
1118
+ description: toolDescription,
1119
+ annotations: toolAnnotations,
1120
+ inputSchema: inputShape,
1121
+ handler: mcpHandler ? createMcpHandlerPassthrough(mcpHandler) : createCustomRouteHandler(route, controller, hasId, {
1122
+ resourceName: resource.name,
1123
+ operationName: opName,
1124
+ permissions: route.permissions,
1125
+ pipeline: resource.pipe
1126
+ })
1127
+ });
1128
+ }
1129
+ if (resource.actions) for (const [actionName, entry] of Object.entries(resource.actions)) {
1130
+ const def = typeof entry === "function" ? { handler: entry } : entry;
1131
+ if (typeof def !== "function" && "mcp" in def && def.mcp === false) continue;
1132
+ const mcpCfg = typeof def !== "function" && typeof def.mcp === "object" ? def.mcp : void 0;
1133
+ const description = mcpCfg?.description ?? (typeof def !== "function" ? def.description : void 0) ?? `${actionName} action on ${resource.displayName}`;
1134
+ const annotations = mcpCfg?.annotations ? { ...mcpCfg.annotations } : { destructiveHint: true };
1135
+ const inputShape = { id: z.string().describe("Resource ID") };
1136
+ const rawSchema = typeof def !== "function" ? def.schema : void 0;
1137
+ if (rawSchema && typeof rawSchema === "object") {
1138
+ const converted = convertActionSchemaToZod(rawSchema);
1139
+ for (const [key, val] of Object.entries(converted)) inputShape[key] = val;
1140
+ }
1141
+ const toolName = prefix ? `${prefix}_${actionName}_${resource.name}` : `${actionName}_${resource.name}`;
1142
+ const handler = typeof entry === "function" ? entry : def.handler;
1143
+ const actionPerms = resolveActionPermission({
1144
+ action: entry,
1145
+ resourcePermissions: resource.permissions,
1146
+ resourceActionPermissions: resource.actionPermissions
1147
+ });
1148
+ tools.push({
1149
+ name: toolName,
1150
+ description: String(description),
1151
+ annotations,
1152
+ inputSchema: inputShape,
1153
+ handler: createActionToolHandler(actionName, handler, actionPerms, resource.name, resource.permissions, rawSchema)
1154
+ });
1155
+ }
1156
+ return tools;
1157
+ }
1039
1158
  //#endregion
1040
1159
  export { fieldRulesToZod as n, createMcpServer as r, resourceToTools as t };