@classytic/arc 2.2.5 → 2.4.1

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 (174) hide show
  1. package/README.md +187 -18
  2. package/bin/arc.js +11 -3
  3. package/dist/BaseController-CkM5dUh_.mjs +1031 -0
  4. package/dist/{EventTransport-BkUDYZEb.d.mts → EventTransport-wc5hSLik.d.mts} +1 -1
  5. package/dist/{HookSystem-BsGV-j2l.mjs → HookSystem-COkyWztM.mjs} +2 -3
  6. package/dist/{ResourceRegistry-7Ic20ZMw.mjs → ResourceRegistry-DeCIFlix.mjs} +8 -5
  7. package/dist/adapters/index.d.mts +3 -5
  8. package/dist/adapters/index.mjs +2 -3
  9. package/dist/{prisma-DJbMt3yf.mjs → adapters-DTC4Ug66.mjs} +45 -12
  10. package/dist/audit/index.d.mts +4 -7
  11. package/dist/audit/index.mjs +2 -29
  12. package/dist/audit/mongodb.d.mts +1 -4
  13. package/dist/audit/mongodb.mjs +2 -3
  14. package/dist/auth/index.d.mts +7 -9
  15. package/dist/auth/index.mjs +65 -63
  16. package/dist/auth/redis-session.d.mts +1 -1
  17. package/dist/auth/redis-session.mjs +1 -2
  18. package/dist/{betterAuthOpenApi-DjWDddNc.mjs → betterAuthOpenApi-lz0IRbXJ.mjs} +4 -6
  19. package/dist/cache/index.d.mts +23 -23
  20. package/dist/cache/index.mjs +4 -6
  21. package/dist/{caching-GSDJcA6-.mjs → caching-BSXB-Xr7.mjs} +2 -24
  22. package/dist/chunk-BpYLSNr0.mjs +14 -0
  23. package/dist/circuitBreaker-BOBOpN2w.mjs +284 -0
  24. package/dist/circuitBreaker-JP2GdJ4b.d.mts +206 -0
  25. package/dist/cli/commands/describe.mjs +24 -7
  26. package/dist/cli/commands/docs.mjs +6 -7
  27. package/dist/cli/commands/doctor.d.mts +10 -0
  28. package/dist/cli/commands/doctor.mjs +156 -0
  29. package/dist/cli/commands/generate.mjs +66 -17
  30. package/dist/cli/commands/init.mjs +315 -45
  31. package/dist/cli/commands/introspect.mjs +2 -4
  32. package/dist/cli/index.d.mts +1 -10
  33. package/dist/cli/index.mjs +4 -153
  34. package/dist/{constants-DdXFXQtN.mjs → constants-Cxde4rpC.mjs} +1 -2
  35. package/dist/core/index.d.mts +3 -5
  36. package/dist/core/index.mjs +5 -4
  37. package/dist/core-C1XCMtqM.mjs +185 -0
  38. package/dist/{createApp-BKHSl2nT.mjs → createApp-ByWNRsZj.mjs} +65 -36
  39. package/dist/{defineResource-DO9ONe_D.mjs → defineResource-D9aY5Cy6.mjs} +154 -1165
  40. package/dist/discovery/index.mjs +37 -5
  41. package/dist/docs/index.d.mts +6 -9
  42. package/dist/docs/index.mjs +3 -21
  43. package/dist/dynamic/index.d.mts +93 -0
  44. package/dist/dynamic/index.mjs +122 -0
  45. package/dist/{elevation-DSTbVvYj.mjs → elevation-BEdACOLB.mjs} +5 -36
  46. package/dist/{elevation-DGo5shaX.d.mts → elevation-Ca_yveIO.d.mts} +41 -7
  47. package/dist/{errorHandler-C3GY3_ow.mjs → errorHandler--zp54tGc.mjs} +3 -5
  48. package/dist/errorHandler-Do4vVQ1f.d.mts +139 -0
  49. package/dist/{errors-DBANPbGr.mjs → errors-rxhfP7Hf.mjs} +1 -2
  50. package/dist/{eventPlugin-BEOvaDqo.mjs → eventPlugin-Ba00swHF.mjs} +25 -27
  51. package/dist/{eventPlugin-H6wDDjGO.d.mts → eventPlugin-iGrSEmwJ.d.mts} +105 -5
  52. package/dist/events/index.d.mts +72 -7
  53. package/dist/events/index.mjs +216 -4
  54. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  55. package/dist/events/transports/redis-stream-entry.mjs +19 -7
  56. package/dist/events/transports/redis.d.mts +1 -1
  57. package/dist/events/transports/redis.mjs +3 -4
  58. package/dist/factory/index.d.mts +23 -9
  59. package/dist/factory/index.mjs +48 -3
  60. package/dist/{fields-Bi_AVKSo.d.mts → fields-DFwdaWCq.d.mts} +1 -1
  61. package/dist/{fields-CTd_CrKr.mjs → fields-ipsbIRPK.mjs} +1 -2
  62. package/dist/hooks/index.d.mts +1 -3
  63. package/dist/hooks/index.mjs +2 -3
  64. package/dist/idempotency/index.d.mts +5 -5
  65. package/dist/idempotency/index.mjs +3 -7
  66. package/dist/idempotency/mongodb.d.mts +1 -1
  67. package/dist/idempotency/mongodb.mjs +4 -5
  68. package/dist/idempotency/redis.d.mts +1 -1
  69. package/dist/idempotency/redis.mjs +2 -5
  70. package/dist/{fastifyAdapter-CyAA2zlB.d.mts → index-BL8CaQih.d.mts} +56 -57
  71. package/dist/index-Diqcm14c.d.mts +369 -0
  72. package/dist/{prisma-xjhMEq_S.d.mts → index-yhxyjqNb.d.mts} +4 -5
  73. package/dist/index.d.mts +100 -105
  74. package/dist/index.mjs +85 -58
  75. package/dist/integrations/event-gateway.d.mts +1 -1
  76. package/dist/integrations/event-gateway.mjs +8 -4
  77. package/dist/integrations/index.d.mts +4 -2
  78. package/dist/integrations/index.mjs +1 -1
  79. package/dist/integrations/jobs.d.mts +2 -2
  80. package/dist/integrations/jobs.mjs +63 -14
  81. package/dist/integrations/mcp/index.d.mts +219 -0
  82. package/dist/integrations/mcp/index.mjs +572 -0
  83. package/dist/integrations/mcp/testing.d.mts +53 -0
  84. package/dist/integrations/mcp/testing.mjs +104 -0
  85. package/dist/integrations/streamline.mjs +39 -19
  86. package/dist/integrations/webhooks.d.mts +56 -0
  87. package/dist/integrations/webhooks.mjs +139 -0
  88. package/dist/integrations/websocket-redis.d.mts +46 -0
  89. package/dist/integrations/websocket-redis.mjs +50 -0
  90. package/dist/integrations/websocket.d.mts +68 -2
  91. package/dist/integrations/websocket.mjs +96 -13
  92. package/dist/{interface-CSNjltAc.d.mts → interface-B4awm1RJ.d.mts} +2 -2
  93. package/dist/interface-DGmPxakH.d.mts +2213 -0
  94. package/dist/{keys-DhqDRxv3.mjs → keys-qcD-TVJl.mjs} +3 -4
  95. package/dist/{logger-ByrvQWZO.mjs → logger-Dz3j1ItV.mjs} +2 -4
  96. package/dist/{memory-B2v7KrCB.mjs → memory-Cb_7iy9e.mjs} +2 -4
  97. package/dist/metrics-Csh4nsvv.mjs +224 -0
  98. package/dist/migrations/index.mjs +3 -7
  99. package/dist/{mongodb-DNKEExbf.mjs → mongodb-BuQ7fNTg.mjs} +1 -4
  100. package/dist/{mongodb-ClykrfGo.d.mts → mongodb-CUpYfxfD.d.mts} +2 -3
  101. package/dist/{mongodb-Dg8O_gvd.d.mts → mongodb-bga9AbkD.d.mts} +2 -2
  102. package/dist/{openapi-9nB_kiuR.mjs → openapi-CBmZ6EQN.mjs} +4 -21
  103. package/dist/org/index.d.mts +12 -14
  104. package/dist/org/index.mjs +92 -119
  105. package/dist/org/types.d.mts +2 -2
  106. package/dist/org/types.mjs +1 -1
  107. package/dist/permissions/index.d.mts +4 -278
  108. package/dist/permissions/index.mjs +4 -579
  109. package/dist/permissions-CA5zg0yK.mjs +751 -0
  110. package/dist/plugins/index.d.mts +104 -107
  111. package/dist/plugins/index.mjs +203 -313
  112. package/dist/plugins/response-cache.mjs +4 -69
  113. package/dist/plugins/tracing-entry.d.mts +1 -1
  114. package/dist/plugins/tracing-entry.mjs +24 -11
  115. package/dist/{pluralize-CM-jZg7p.mjs → pluralize-CcT6qF0a.mjs} +12 -13
  116. package/dist/policies/index.d.mts +2 -2
  117. package/dist/policies/index.mjs +80 -83
  118. package/dist/presets/index.d.mts +26 -19
  119. package/dist/presets/index.mjs +2 -142
  120. package/dist/presets/multiTenant.d.mts +1 -4
  121. package/dist/presets/multiTenant.mjs +4 -6
  122. package/dist/presets-C9QXJV1u.mjs +422 -0
  123. package/dist/{queryCachePlugin-B6R0d4av.mjs → queryCachePlugin-ClosZdNS.mjs} +6 -27
  124. package/dist/{queryCachePlugin-Q6SYuHZ6.d.mts → queryCachePlugin-DcmETvcB.d.mts} +3 -3
  125. package/dist/queryParser-CgCtsjti.mjs +352 -0
  126. package/dist/{redis-UwjEp8Ea.d.mts → redis-CQ5YxMC5.d.mts} +2 -2
  127. package/dist/{redis-stream-CBg0upHI.d.mts → redis-stream-BW9UKLZM.d.mts} +9 -2
  128. package/dist/registry/index.d.mts +1 -4
  129. package/dist/registry/index.mjs +3 -4
  130. package/dist/{introspectionPlugin-B3JkrjwU.mjs → registry-I-ogLgL9.mjs} +1 -8
  131. package/dist/{requestContext-xi6OKBL-.mjs → requestContext-DYtmNpm5.mjs} +1 -3
  132. package/dist/resourceToTools-B6ZN9Ing.mjs +489 -0
  133. package/dist/rpc/index.d.mts +90 -0
  134. package/dist/rpc/index.mjs +248 -0
  135. package/dist/{schemaConverter-Dtg0Kt9T.mjs → schemaConverter-DjzHpFam.mjs} +1 -2
  136. package/dist/schemas/index.d.mts +30 -30
  137. package/dist/schemas/index.mjs +4 -6
  138. package/dist/scope/index.d.mts +13 -2
  139. package/dist/scope/index.mjs +18 -5
  140. package/dist/{sessionManager-D_iEHjQl.d.mts → sessionManager-wbkYj2HL.d.mts} +2 -2
  141. package/dist/{sse-DkqQ1uxb.mjs → sse-BkViJPlT.mjs} +4 -25
  142. package/dist/testing/index.d.mts +551 -567
  143. package/dist/testing/index.mjs +1744 -1799
  144. package/dist/{tracing-8CEbhF0w.d.mts → tracing-bz_U4EM1.d.mts} +6 -1
  145. package/dist/{typeGuards-DwxA1t_L.mjs → typeGuards-Cj5Rgvlg.mjs} +1 -2
  146. package/dist/types/index.d.mts +4 -946
  147. package/dist/types/index.mjs +2 -4
  148. package/dist/types-BJmgxNbF.d.mts +275 -0
  149. package/dist/{types-RLkFVgaw.d.mts → types-BNUccdcf.d.mts} +2 -2
  150. package/dist/{types-Beqn1Un7.mjs → types-C6TQjtdi.mjs} +30 -2
  151. package/dist/{types-DMSBMkaZ.d.mts → types-Dt0-AI6E.d.mts} +85 -27
  152. package/dist/{types-DelU6kln.mjs → types-ZUu_h0jp.mjs} +1 -2
  153. package/dist/utils/index.d.mts +255 -352
  154. package/dist/utils/index.mjs +7 -6
  155. package/dist/utils-Dc0WhlIl.mjs +594 -0
  156. package/dist/versioning-BzfeHmhj.mjs +37 -0
  157. package/package.json +46 -12
  158. package/skills/arc/SKILL.md +506 -0
  159. package/skills/arc/references/auth.md +250 -0
  160. package/skills/arc/references/events.md +272 -0
  161. package/skills/arc/references/integrations.md +385 -0
  162. package/skills/arc/references/mcp.md +386 -0
  163. package/skills/arc/references/production.md +610 -0
  164. package/skills/arc/references/testing.md +183 -0
  165. package/dist/audited-CGdLiSlE.mjs +0 -140
  166. package/dist/chunk-C7Uep-_p.mjs +0 -20
  167. package/dist/circuitBreaker-DYhWBW_D.mjs +0 -1096
  168. package/dist/errorHandler-CW3OOeYq.d.mts +0 -72
  169. package/dist/interface-DZYNK9bb.d.mts +0 -1112
  170. package/dist/presets-BTeYbw7h.d.mts +0 -57
  171. package/dist/presets-CeFtfDR8.mjs +0 -119
  172. /package/dist/{errors-DAWRdiYP.d.mts → errors-CPpvPHT0.d.mts} +0 -0
  173. /package/dist/{externalPaths-SyPF2tgK.d.mts → externalPaths-DpO-s7r8.d.mts} +0 -0
  174. /package/dist/{interface-DTbsvIWe.d.mts → interface-D_BWALyZ.d.mts} +0 -0
@@ -0,0 +1,489 @@
1
+ import { t as BaseController } from "./BaseController-CkM5dUh_.mjs";
2
+ import { t as pluralize } from "./pluralize-CcT6qF0a.mjs";
3
+ import { z } from "zod";
4
+ //#region src/integrations/mcp/createMcpServer.ts
5
+ /**
6
+ * Create a configured MCP server from declarative config.
7
+ *
8
+ * @param config - Server name, version, tools, prompts
9
+ * @returns McpServer instance (not yet connected to a transport)
10
+ */
11
+ async function createMcpServer(config, authRef) {
12
+ const { McpServer } = await import("@modelcontextprotocol/sdk/server/mcp.js");
13
+ const server = new McpServer({
14
+ name: config.name,
15
+ version: config.version ?? "1.0.0"
16
+ }, config.instructions ? { instructions: config.instructions } : void 0);
17
+ if (config.tools) for (const tool of config.tools) registerTool(server, tool, authRef);
18
+ if (config.prompts) for (const prompt of config.prompts) registerPrompt(server, prompt);
19
+ return server;
20
+ }
21
+ /**
22
+ * Register a ToolDefinition using `server.registerTool()`.
23
+ *
24
+ * The inputSchema is passed as a flat Zod shape `{ field: z.string() }` —
25
+ * the SDK wraps it in z.object() internally. This avoids the Zod v3/v4-mini
26
+ * version mismatch entirely.
27
+ */
28
+ function registerTool(server, tool, authRef) {
29
+ const srv = server;
30
+ const config = {};
31
+ if (tool.title) config.title = tool.title;
32
+ if (tool.description) config.description = tool.description;
33
+ if (tool.inputSchema) config.inputSchema = tool.inputSchema;
34
+ if (tool.outputSchema) config.outputSchema = tool.outputSchema;
35
+ if (tool.annotations) config.annotations = tool.annotations;
36
+ srv.registerTool(tool.name, config, (input, extra) => {
37
+ const ctx = {
38
+ session: authRef?.current ?? null,
39
+ log: async (level, message) => {
40
+ try {
41
+ const notify = extra?.sendNotification;
42
+ if (notify) await notify({
43
+ method: "notifications/message",
44
+ params: {
45
+ level,
46
+ data: message
47
+ }
48
+ });
49
+ } catch {}
50
+ },
51
+ extra
52
+ };
53
+ return tool.handler(input, ctx);
54
+ });
55
+ }
56
+ /** Register a PromptDefinition using `server.registerPrompt()` */
57
+ function registerPrompt(server, prompt) {
58
+ const srv = server;
59
+ const config = {};
60
+ if (prompt.title) config.title = prompt.title;
61
+ if (prompt.description) config.description = prompt.description;
62
+ if (prompt.argsSchema) config.argsSchema = prompt.argsSchema;
63
+ srv.registerPrompt(prompt.name, config, (args) => prompt.handler(args));
64
+ }
65
+ //#endregion
66
+ //#region src/integrations/mcp/fieldRulesToZod.ts
67
+ /**
68
+ * @classytic/arc — fieldRules → Zod Shape Converter
69
+ *
70
+ * Converts Arc's schemaOptions.fieldRules into flat Zod shapes
71
+ * compatible with the MCP SDK's registerTool() inputSchema format.
72
+ *
73
+ * Returns `Record<string, z.ZodTypeAny>` (flat shape), NOT z.object().
74
+ * The SDK wraps it internally.
75
+ *
76
+ * @example
77
+ * ```typescript
78
+ * import { fieldRulesToZod } from '@classytic/arc/mcp';
79
+ *
80
+ * const shape = fieldRulesToZod(resource.schemaOptions.fieldRules, {
81
+ * mode: 'create',
82
+ * hiddenFields: resource.schemaOptions.hiddenFields,
83
+ * });
84
+ * // shape = { name: z.string(), price: z.number() }
85
+ * ```
86
+ */
87
+ const PAGINATION_SHAPE = {
88
+ page: z.number().int().min(1).optional().describe("Page number (1-based)"),
89
+ limit: z.number().int().min(1).max(100).optional().describe("Items per page (max 100)"),
90
+ sort: z.string().optional().describe("Sort field, prefix with - for descending"),
91
+ search: z.string().optional().describe("Full-text search query")
92
+ };
93
+ /**
94
+ * Convert Arc fieldRules to a flat Zod shape.
95
+ *
96
+ * @returns Flat shape `Record<string, z.ZodTypeAny>` — pass directly to defineTool() or registerTool()
97
+ */
98
+ function fieldRulesToZod(fieldRules, options = {}) {
99
+ const { mode = "create", hiddenFields = [], readonlyFields = [], extraHideFields = [] } = options;
100
+ if (mode === "list") return buildListShape(fieldRules, options);
101
+ if (!fieldRules) return {};
102
+ const allHidden = new Set([...hiddenFields, ...extraHideFields]);
103
+ const allReadonly = new Set(readonlyFields);
104
+ const shape = {};
105
+ for (const [name, rule] of Object.entries(fieldRules)) {
106
+ if (rule.systemManaged || rule.hidden || allHidden.has(name)) continue;
107
+ if (allReadonly.has(name)) continue;
108
+ if (mode === "update" && rule.immutable) continue;
109
+ const field = buildFieldSchema(rule);
110
+ if (mode === "update") shape[name] = field.optional();
111
+ else shape[name] = rule.required === true && !rule.optional ? field : field.optional();
112
+ }
113
+ return shape;
114
+ }
115
+ /** Build Zod type for a single field rule */
116
+ function buildFieldSchema(rule) {
117
+ if (rule.enum?.length) {
118
+ const schema = z.enum(rule.enum);
119
+ return rule.description ? schema.describe(rule.description) : schema;
120
+ }
121
+ const base = typeToZod(rule.type);
122
+ if (base instanceof z.ZodString) {
123
+ let s = base;
124
+ if (rule.minLength != null) s = s.min(rule.minLength);
125
+ if (rule.maxLength != null) s = s.max(rule.maxLength);
126
+ if (rule.pattern) try {
127
+ s = s.regex(new RegExp(rule.pattern));
128
+ } catch {}
129
+ return rule.description ? s.describe(rule.description) : s;
130
+ }
131
+ if (base instanceof z.ZodNumber) {
132
+ let n = base;
133
+ if (rule.min != null) n = n.min(rule.min);
134
+ if (rule.max != null) n = n.max(rule.max);
135
+ return rule.description ? n.describe(rule.description) : n;
136
+ }
137
+ return rule.description ? base.describe(rule.description) : base;
138
+ }
139
+ /** Map Arc field type string to base Zod type */
140
+ function typeToZod(type) {
141
+ switch (type) {
142
+ case "string": return z.string();
143
+ case "number": return z.number();
144
+ case "boolean": return z.boolean();
145
+ case "date": return z.string().describe("ISO 8601 date string");
146
+ case "array": return z.array(z.any());
147
+ case "object": return z.record(z.string(), z.any());
148
+ default: return z.string();
149
+ }
150
+ }
151
+ /** Build list/query shape with filterable fields + pagination */
152
+ function buildListShape(fieldRules, options) {
153
+ const { filterableFields = [], hiddenFields = [], extraHideFields = [] } = options;
154
+ const allHidden = new Set([...hiddenFields, ...extraHideFields]);
155
+ const shape = { ...PAGINATION_SHAPE };
156
+ if (fieldRules) for (const name of filterableFields) {
157
+ if (allHidden.has(name)) continue;
158
+ const rule = fieldRules[name];
159
+ if (!rule) continue;
160
+ shape[name] = buildFieldSchema(rule).optional();
161
+ }
162
+ return shape;
163
+ }
164
+ //#endregion
165
+ //#region src/integrations/mcp/buildRequestContext.ts
166
+ /**
167
+ * Build an IRequestContext from MCP tool input and session auth.
168
+ *
169
+ * | Operation | params | query | body |
170
+ * |-----------|------------|----------------------|---------------------|
171
+ * | list | {} | all input fields | undefined |
172
+ * | get | { id } | {} | undefined |
173
+ * | create | {} | {} | all input fields |
174
+ * | update | { id } | {} | input minus id |
175
+ * | delete | { id } | {} | undefined |
176
+ */
177
+ function buildRequestContext(input, auth, operation) {
178
+ const scope = buildScope(auth);
179
+ const base = {
180
+ user: auth ? {
181
+ id: auth.userId,
182
+ _id: auth.userId
183
+ } : null,
184
+ headers: {},
185
+ context: {},
186
+ metadata: {
187
+ _scope: scope,
188
+ _policyFilters: {}
189
+ }
190
+ };
191
+ switch (operation) {
192
+ case "list": return {
193
+ ...base,
194
+ params: {},
195
+ query: { ...input },
196
+ body: void 0
197
+ };
198
+ case "get": return {
199
+ ...base,
200
+ params: { id: String(input.id ?? "") },
201
+ query: {},
202
+ body: void 0
203
+ };
204
+ case "create": return {
205
+ ...base,
206
+ params: {},
207
+ query: {},
208
+ body: { ...input }
209
+ };
210
+ case "update": {
211
+ const { id: _id, ...body } = input;
212
+ return {
213
+ ...base,
214
+ params: { id: String(_id ?? "") },
215
+ query: {},
216
+ body
217
+ };
218
+ }
219
+ case "delete": return {
220
+ ...base,
221
+ params: { id: String(input.id ?? "") },
222
+ query: {},
223
+ body: void 0
224
+ };
225
+ }
226
+ }
227
+ function buildScope(auth) {
228
+ if (!auth) return { kind: "public" };
229
+ if (auth.organizationId) return {
230
+ kind: "member",
231
+ userId: auth.userId,
232
+ userRoles: [],
233
+ organizationId: auth.organizationId,
234
+ orgRoles: []
235
+ };
236
+ return {
237
+ kind: "authenticated",
238
+ userId: auth.userId,
239
+ userRoles: []
240
+ };
241
+ }
242
+ //#endregion
243
+ //#region src/integrations/mcp/resourceToTools.ts
244
+ /**
245
+ * @classytic/arc — Resource → MCP Tools Generator
246
+ *
247
+ * Converts a ResourceDefinition into an array of ToolDefinitions.
248
+ * Core auto-generation logic that powers Level 1 (mcpPlugin).
249
+ *
250
+ * All tool handlers call BaseController methods — same pipeline as REST.
251
+ */
252
+ const ALL_CRUD_OPS = [
253
+ "list",
254
+ "get",
255
+ "create",
256
+ "update",
257
+ "delete"
258
+ ];
259
+ const ANNOTATIONS = {
260
+ list: { readOnlyHint: true },
261
+ get: { readOnlyHint: true },
262
+ create: { destructiveHint: false },
263
+ update: {
264
+ destructiveHint: true,
265
+ idempotentHint: true
266
+ },
267
+ delete: {
268
+ destructiveHint: true,
269
+ idempotentHint: true
270
+ }
271
+ };
272
+ /**
273
+ * Convert a ResourceDefinition into MCP ToolDefinitions.
274
+ *
275
+ * MCP tools call BaseController directly — they bypass HTTP routes entirely.
276
+ * Therefore `disableDefaultRoutes` does NOT affect MCP tool generation;
277
+ * only `disabledRoutes` (the per-operation array) controls which ops are skipped.
278
+ *
279
+ * If the resource has an adapter but no controller (e.g. `disableDefaultRoutes: true`),
280
+ * a lightweight BaseController is auto-created from the adapter for MCP use.
281
+ *
282
+ * @param resource - Arc resource definition
283
+ * @param config - Optional overrides (operations, descriptions, hideFields, prefix, names)
284
+ */
285
+ function resourceToTools(resource, config = {}) {
286
+ const controller = resource.controller ?? (resource.adapter ? createMcpController(resource) : void 0);
287
+ if (!controller) return [];
288
+ const fieldRules = resource.schemaOptions?.fieldRules;
289
+ const hiddenFields = resource.schemaOptions?.hiddenFields;
290
+ const readonlyFields = resource.schemaOptions?.readonlyFields;
291
+ const filterableFields = resource.schemaOptions?.filterableFields ?? resource.queryParser?.allowedFilterFields;
292
+ const sortableFields = resource.queryParser?.allowedSortFields;
293
+ const allowedOperators = resource.queryParser?.allowedOperators;
294
+ const hasSoftDelete = resource._appliedPresets?.includes("softDelete") ?? false;
295
+ let ops = ALL_CRUD_OPS.filter((op) => {
296
+ if (resource.disabledRoutes?.includes(op)) return false;
297
+ return true;
298
+ });
299
+ if (config.operations) ops = ops.filter((op) => config.operations?.includes(op));
300
+ const tools = [];
301
+ const prefix = config.toolNamePrefix;
302
+ for (const op of ops) {
303
+ const name = config.names?.[op] ?? (op === "list" ? `${prefix ? `${prefix}_` : ""}list_${pluralize(resource.name)}` : `${prefix ? `${prefix}_` : ""}${op}_${resource.name}`);
304
+ tools.push({
305
+ name,
306
+ description: config.descriptions?.[op] ?? defaultDescription(op, resource.displayName, hasSoftDelete, {
307
+ filterableFields,
308
+ allowedOperators,
309
+ sortableFields
310
+ }),
311
+ annotations: ANNOTATIONS[op],
312
+ inputSchema: buildInputSchema(op, fieldRules, {
313
+ hiddenFields,
314
+ readonlyFields,
315
+ extraHideFields: config.hideFields,
316
+ filterableFields
317
+ }),
318
+ handler: createHandler(op, controller, resource.name)
319
+ });
320
+ }
321
+ for (const route of resource.additionalRoutes ?? []) {
322
+ const mcpHandler = route.mcpHandler;
323
+ if (!route.wrapHandler && !mcpHandler) continue;
324
+ if (!mcpHandler && ![
325
+ "POST",
326
+ "PUT",
327
+ "PATCH",
328
+ "DELETE"
329
+ ].includes(route.method)) continue;
330
+ const opName = route.operation ?? slugifyRoute(route.method, route.path);
331
+ const hasId = route.path.includes(":id");
332
+ const inputShape = {};
333
+ if (hasId) inputShape.id = z.string().describe("Resource ID");
334
+ if (mcpHandler) tools.push({
335
+ name: prefix ? `${prefix}_${opName}_${resource.name}` : `${opName}_${resource.name}`,
336
+ description: route.summary ?? route.description ?? `${opName} on ${resource.displayName}`,
337
+ annotations: { openWorldHint: true },
338
+ inputSchema: inputShape,
339
+ handler: async (input, _ctx) => {
340
+ try {
341
+ return await mcpHandler(input);
342
+ } catch (err) {
343
+ return {
344
+ content: [{
345
+ type: "text",
346
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
347
+ }],
348
+ isError: true
349
+ };
350
+ }
351
+ }
352
+ });
353
+ else tools.push({
354
+ name: prefix ? `${prefix}_${opName}_${resource.name}` : `${opName}_${resource.name}`,
355
+ description: route.summary ?? route.description ?? `${opName} on ${resource.displayName}`,
356
+ annotations: { openWorldHint: true },
357
+ inputSchema: inputShape,
358
+ handler: createAdditionalRouteHandler(route, controller, hasId)
359
+ });
360
+ }
361
+ return tools;
362
+ }
363
+ function buildInputSchema(op, fieldRules, opts) {
364
+ switch (op) {
365
+ case "list": return fieldRulesToZod(fieldRules, {
366
+ mode: "list",
367
+ ...opts
368
+ });
369
+ case "get": return { id: z.string().describe("Resource ID") };
370
+ case "create": return fieldRulesToZod(fieldRules, {
371
+ mode: "create",
372
+ ...opts
373
+ });
374
+ case "update": return {
375
+ id: z.string().describe("Resource ID"),
376
+ ...fieldRulesToZod(fieldRules, {
377
+ mode: "update",
378
+ ...opts
379
+ })
380
+ };
381
+ case "delete": return { id: z.string().describe("Resource ID") };
382
+ }
383
+ }
384
+ function createHandler(op, controller, resourceName) {
385
+ const ctrl = controller;
386
+ return async (input, ctx) => {
387
+ try {
388
+ const method = ctrl[op];
389
+ if (typeof method !== "function") return {
390
+ content: [{
391
+ type: "text",
392
+ text: `Operation "${op}" not available on ${resourceName}`
393
+ }],
394
+ isError: true
395
+ };
396
+ return toCallToolResult(await method(buildRequestContext(input, ctx.session, op)));
397
+ } catch (err) {
398
+ const msg = err instanceof Error ? err.message : String(err);
399
+ ctx.log("error", `${resourceName}.${op}: ${msg}`).catch(() => {});
400
+ return {
401
+ content: [{
402
+ type: "text",
403
+ text: `Error: ${msg}`
404
+ }],
405
+ isError: true
406
+ };
407
+ }
408
+ };
409
+ }
410
+ function createAdditionalRouteHandler(route, controller, hasId) {
411
+ const ctrl = controller;
412
+ const handlerName = typeof route.handler === "string" ? route.handler : route.operation ?? slugifyRoute(route.method, route.path);
413
+ return async (input, ctx) => {
414
+ try {
415
+ const method = ctrl[handlerName];
416
+ if (typeof method !== "function") return {
417
+ content: [{
418
+ type: "text",
419
+ text: `Handler "${handlerName}" not found on controller`
420
+ }],
421
+ isError: true
422
+ };
423
+ return toCallToolResult(await method(buildRequestContext(input, ctx.session, hasId ? "update" : "create")));
424
+ } catch (err) {
425
+ return {
426
+ content: [{
427
+ type: "text",
428
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`
429
+ }],
430
+ isError: true
431
+ };
432
+ }
433
+ };
434
+ }
435
+ function toCallToolResult(result) {
436
+ if (!result.success) return {
437
+ content: [{
438
+ type: "text",
439
+ text: result.error ?? "Operation failed"
440
+ }],
441
+ isError: true
442
+ };
443
+ const output = result.meta ? {
444
+ data: result.data,
445
+ ...result.meta
446
+ } : result.data;
447
+ return { content: [{
448
+ type: "text",
449
+ text: JSON.stringify(output, null, 2)
450
+ }] };
451
+ }
452
+ function defaultDescription(op, displayName, softDelete, queryMeta) {
453
+ const name = displayName.toLowerCase();
454
+ switch (op) {
455
+ case "list": {
456
+ const parts = [`List ${pluralize(name)} with optional filters and pagination.`];
457
+ if (queryMeta?.filterableFields?.length) parts.push(`Filterable fields: ${queryMeta.filterableFields.join(", ")}.`);
458
+ if (queryMeta?.allowedOperators?.length) parts.push(`Filter operators: ${queryMeta.allowedOperators.join(", ")} (use field[op]=value syntax).`);
459
+ if (queryMeta?.sortableFields?.length) parts.push(`Sortable fields: ${queryMeta.sortableFields.join(", ")}.`);
460
+ return parts.join(" ");
461
+ }
462
+ case "get": return `Get a single ${name} by ID`;
463
+ case "create": return `Create a new ${name}`;
464
+ case "update": return `Update an existing ${name} by ID`;
465
+ case "delete": return softDelete ? `Delete a ${name} by ID (soft delete — marks as deleted, not permanently removed)` : `Delete a ${name} by ID`;
466
+ }
467
+ }
468
+ function slugifyRoute(method, path) {
469
+ const clean = path.replace(/:[^/]+/g, "").replace(/^\/+|\/+$/g, "").replace(/\//g, "_");
470
+ return clean ? `${method.toLowerCase()}_${clean}` : method.toLowerCase();
471
+ }
472
+ /**
473
+ * Auto-create a BaseController from the resource's adapter for MCP use.
474
+ * Called when the resource has an adapter but no controller
475
+ * (e.g. `disableDefaultRoutes: true` skips controller creation in defineResource).
476
+ */
477
+ function createMcpController(resource) {
478
+ const repository = resource.adapter?.repository;
479
+ if (!repository) return void 0;
480
+ return new BaseController(repository, {
481
+ resourceName: resource.name,
482
+ schemaOptions: resource.schemaOptions,
483
+ tenantField: resource.tenantField,
484
+ idField: resource.idField,
485
+ matchesFilter: resource.adapter?.matchesFilter
486
+ });
487
+ }
488
+ //#endregion
489
+ export { fieldRulesToZod as n, createMcpServer as r, resourceToTools as t };
@@ -0,0 +1,90 @@
1
+ import { r as CircuitBreakerOptions } from "../circuitBreaker-JP2GdJ4b.mjs";
2
+
3
+ //#region src/rpc/serviceClient.d.ts
4
+ interface RetryConfig {
5
+ /** Max retry attempts (not counting initial attempt). Default: 2 */
6
+ maxRetries?: number;
7
+ /** Initial backoff delay in ms. Doubles on each retry. Default: 200 */
8
+ backoffMs?: number;
9
+ /** Max backoff cap in ms. Default: 5000 */
10
+ maxBackoffMs?: number;
11
+ /**
12
+ * HTTP status codes to retry on. Default: [502, 503, 504, 408, 429]
13
+ * 4xx errors (except 408, 429) are NOT retried — they are client errors.
14
+ */
15
+ retryableStatuses?: number[];
16
+ }
17
+ interface RequestInfo {
18
+ method: string;
19
+ url: string;
20
+ headers?: Record<string, string>;
21
+ }
22
+ interface ResponseInfo {
23
+ method: string;
24
+ url: string;
25
+ status: number;
26
+ durationMs: number;
27
+ retries: number;
28
+ }
29
+ interface ServiceClientOptions {
30
+ /** Base URL of the remote Arc service (e.g., 'http://catalog-service:3000') */
31
+ baseUrl: string;
32
+ /** Static bearer token, or function that returns one (for rotation) */
33
+ token?: string | (() => string);
34
+ /** Organization ID — sent as x-organization-id header */
35
+ organizationId?: string;
36
+ /**
37
+ * Correlation ID for distributed tracing — sent as x-request-id header.
38
+ * Static string or function (e.g., () => request.id from current request context).
39
+ */
40
+ correlationId?: string | (() => string);
41
+ /** Schema version — sent as x-arc-schema-version header for contract compatibility */
42
+ schemaVersion?: string;
43
+ /** Additional headers sent with every request */
44
+ headers?: Record<string, string>;
45
+ /** Request timeout in ms (default: 10000) */
46
+ timeout?: number;
47
+ /** Retry config for transient failures (default: disabled) */
48
+ retry?: RetryConfig;
49
+ /** Circuit breaker config (default: disabled) */
50
+ circuitBreaker?: Pick<CircuitBreakerOptions, "failureThreshold" | "resetTimeout" | "timeout" | "successThreshold">;
51
+ /** Health check path (default: '/_health/live' — matches Arc's health plugin) */
52
+ healthPath?: string;
53
+ /** Called before each request (for logging, metrics, tracing) */
54
+ onRequest?: (info: RequestInfo) => void;
55
+ /** Called after each response (for logging, metrics, tracing) */
56
+ onResponse?: (info: ResponseInfo) => void;
57
+ }
58
+ interface ResourceClient {
59
+ /** GET /{resource}s?...query */
60
+ list(query?: Record<string, unknown>): Promise<ServiceResponse>;
61
+ /** GET /{resource}s/:id */
62
+ get(id: string): Promise<ServiceResponse>;
63
+ /** POST /{resource}s */
64
+ create(data: Record<string, unknown>): Promise<ServiceResponse>;
65
+ /** PATCH /{resource}s/:id */
66
+ update(id: string, data: Record<string, unknown>): Promise<ServiceResponse>;
67
+ /** DELETE /{resource}s/:id */
68
+ delete(id: string): Promise<ServiceResponse>;
69
+ /** POST /{resource}s/:id/action */
70
+ action(id: string, actionName: string, data?: Record<string, unknown>): Promise<ServiceResponse>;
71
+ }
72
+ interface ServiceResponse<T = any> {
73
+ success: boolean;
74
+ data?: T;
75
+ error?: string;
76
+ message?: string;
77
+ status?: number;
78
+ meta?: Record<string, unknown>;
79
+ }
80
+ interface ServiceClient {
81
+ /** Get a typed resource client for CRUD + actions */
82
+ resource(name: string): ResourceClient;
83
+ /** Raw call to any path (for non-resource endpoints) */
84
+ call(method: string, path: string, body?: unknown): Promise<ServiceResponse>;
85
+ /** Health check — returns true if service is reachable */
86
+ health(): Promise<boolean>;
87
+ }
88
+ declare function createServiceClient(options: ServiceClientOptions): ServiceClient;
89
+ //#endregion
90
+ export { type RequestInfo, type ResourceClient, type ResponseInfo, type RetryConfig, type ServiceClient, type ServiceClientOptions, type ServiceResponse, createServiceClient };