@classytic/arc 2.15.4 → 2.16.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 (158) hide show
  1. package/README.md +1 -0
  2. package/bin/arc.js +12 -0
  3. package/dist/{BaseController-dx3m2J8V.mjs → BaseController-DlCCTIxJ.mjs} +61 -19
  4. package/dist/{HookSystem-Iiebom92.mjs → HookSystem-Cmf7-Etp.mjs} +8 -4
  5. package/dist/{QueryCache-D41bfdBB.d.mts → QueryCache-SvmT_9ti.d.mts} +1 -1
  6. package/dist/{ResourceRegistry-CTERg_2x.mjs → ResourceRegistry-f48hFk3m.mjs} +52 -9
  7. package/dist/audit/index.d.mts +1 -1
  8. package/dist/audit/index.mjs +4 -2
  9. package/dist/auth/index.d.mts +4 -4
  10. package/dist/auth/index.mjs +4 -4
  11. package/dist/auth/redis-session.d.mts +1 -1
  12. package/dist/{betterAuthOpenApi--M_i87dQ.mjs → betterAuthOpenApi-ClWxaceA.mjs} +10 -6
  13. package/dist/buildHandler-BZX6zzDM.mjs +300 -0
  14. package/dist/cache/index.d.mts +3 -3
  15. package/dist/cache/index.mjs +3 -3
  16. package/dist/{caching-SM8gghN6.mjs → caching-TeHE8G-v.mjs} +1 -1
  17. package/dist/cli/commands/describe.d.mts +35 -1
  18. package/dist/cli/commands/describe.mjs +52 -12
  19. package/dist/cli/commands/docs.d.mts +1 -4
  20. package/dist/cli/commands/docs.mjs +4 -16
  21. package/dist/cli/commands/generate.d.mts +2 -20
  22. package/dist/cli/commands/generate.mjs +1 -546
  23. package/dist/cli/commands/init.d.mts +2 -40
  24. package/dist/cli/commands/init.mjs +1 -3045
  25. package/dist/cli/commands/introspect.mjs +53 -64
  26. package/dist/cli/index.d.mts +2 -2
  27. package/dist/cli/index.mjs +2 -2
  28. package/dist/{constants-Cxde4rpC.mjs → constants-TrJVIJl0.mjs} +7 -0
  29. package/dist/core/index.d.mts +3 -3
  30. package/dist/core/index.mjs +5 -5
  31. package/dist/{core-CvmOqEms.mjs → core-DBJ_j6rX.mjs} +222 -44
  32. package/dist/createActionRouter-DUpN3Dd1.mjs +288 -0
  33. package/dist/{createAggregationRouter-B0bPDf5b.mjs → createAggregationRouter-Dq-TUCuY.mjs} +3 -2
  34. package/dist/{createApp-PFegs47-.mjs → createApp-DNccuhyI.mjs} +16 -14
  35. package/dist/{defineEvent-D5h7EvAx.mjs → defineEvent-DRwY0fYm.mjs} +1 -1
  36. package/dist/docs/index.d.mts +2 -2
  37. package/dist/docs/index.mjs +1 -1
  38. package/dist/{errorHandler-Bk-AGhkU.mjs → errorHandler-DpoXQHZ9.mjs} +17 -14
  39. package/dist/errors-C1lX_jlm.d.mts +91 -0
  40. package/dist/{eventPlugin-CaKTYkYM.mjs → eventPlugin-C2cGqtRO.mjs} +1 -1
  41. package/dist/{eventPlugin-qXpqTebY.d.mts → eventPlugin-CtHC_av1.d.mts} +1 -1
  42. package/dist/events/index.d.mts +3 -3
  43. package/dist/events/index.mjs +5 -5
  44. package/dist/events/transports/redis-stream-entry.d.mts +1 -1
  45. package/dist/events/transports/redis.d.mts +1 -1
  46. package/dist/factory/index.d.mts +1 -1
  47. package/dist/factory/index.mjs +2 -2
  48. package/dist/{fields-COhcH3fk.d.mts → fields-Anj0xdih.d.mts} +1 -1
  49. package/dist/generate-BWFwgcCM.d.mts +38 -0
  50. package/dist/generate-CYac-OLv.mjs +654 -0
  51. package/dist/hooks/index.d.mts +1 -1
  52. package/dist/hooks/index.mjs +1 -1
  53. package/dist/idempotency/index.d.mts +2 -2
  54. package/dist/idempotency/index.mjs +1 -1
  55. package/dist/idempotency/redis.d.mts +1 -1
  56. package/dist/{index-BTqLEvhu.d.mts → index-3oIimXQn.d.mts} +12 -12
  57. package/dist/{index-BstGxcc3.d.mts → index-B-ulKx5P.d.mts} +55 -4
  58. package/dist/{index-BswOSJCE.d.mts → index-CkW0flkU.d.mts} +355 -16
  59. package/dist/index.d.mts +6 -6
  60. package/dist/index.mjs +7 -8
  61. package/dist/init-Dv71MsJr.d.mts +71 -0
  62. package/dist/init-HDvoO9L5.mjs +3098 -0
  63. package/dist/integrations/event-gateway.d.mts +2 -2
  64. package/dist/integrations/event-gateway.mjs +1 -1
  65. package/dist/integrations/index.d.mts +2 -2
  66. package/dist/integrations/jobs.mjs +3 -3
  67. package/dist/integrations/mcp/index.d.mts +239 -7
  68. package/dist/integrations/mcp/index.mjs +2 -528
  69. package/dist/integrations/mcp/testing.d.mts +2 -2
  70. package/dist/integrations/mcp/testing.mjs +6 -10
  71. package/dist/integrations/streamline.mjs +26 -1
  72. package/dist/integrations/websocket-redis.d.mts +1 -1
  73. package/dist/integrations/websocket.d.mts +1 -1
  74. package/dist/integrations/websocket.mjs +1 -0
  75. package/dist/loadResourcesFromEntry-BLMEI2Xa.mjs +51 -0
  76. package/dist/{resourceToTools-tFYUNmM0.mjs → mcpPlugin-7vGV51ED.mjs} +1021 -318
  77. package/dist/{memory-UBydS5ku.mjs → memory-QOLe11D5.mjs} +2 -0
  78. package/dist/middleware/index.d.mts +1 -1
  79. package/dist/middleware/index.mjs +1 -1
  80. package/dist/{openapi-BHXhoX8O.mjs → openapi-34T9yNwd.mjs} +47 -36
  81. package/dist/permissions/index.d.mts +2 -2
  82. package/dist/permissions/index.mjs +1 -1
  83. package/dist/{permissions-ohQyv50e.mjs → permissions-CTxMrreC.mjs} +2 -2
  84. package/dist/{pipe-Zr0KXjQe.mjs → pipe-DiCyvyPN.mjs} +1 -0
  85. package/dist/pipeline/index.d.mts +1 -1
  86. package/dist/pipeline/index.mjs +1 -1
  87. package/dist/plugins/index.d.mts +5 -5
  88. package/dist/plugins/index.mjs +10 -10
  89. package/dist/plugins/response-cache.mjs +5 -5
  90. package/dist/plugins/tracing-entry.d.mts +1 -1
  91. package/dist/plugins/tracing-entry.mjs +1 -1
  92. package/dist/{pluralize-DQgqgifU.mjs → pluralize-B9M8xvy-.mjs} +2 -1
  93. package/dist/presets/filesUpload.d.mts +4 -4
  94. package/dist/presets/filesUpload.mjs +2 -2
  95. package/dist/presets/index.d.mts +1 -1
  96. package/dist/presets/index.mjs +1 -1
  97. package/dist/presets/multiTenant.d.mts +1 -1
  98. package/dist/presets/multiTenant.mjs +4 -3
  99. package/dist/presets/search.d.mts +2 -2
  100. package/dist/presets/search.mjs +1 -1
  101. package/dist/{presets-BbkjdPeH.mjs → presets-C9BE6WaZ.mjs} +2 -2
  102. package/dist/{queryCachePlugin-m1XsgAIJ.mjs → queryCachePlugin-B4XMSSe7.mjs} +2 -2
  103. package/dist/{queryCachePlugin-CqMdLI2-.d.mts → queryCachePlugin-Biqzfbi5.d.mts} +2 -2
  104. package/dist/{redis-DiMkdHEl.d.mts → redis-Cyzrz6SX.d.mts} +1 -1
  105. package/dist/{redis-stream-D6HzR1Z_.d.mts → redis-stream-DT-YjzrB.d.mts} +1 -1
  106. package/dist/registry/index.d.mts +319 -2
  107. package/dist/registry/index.mjs +3 -3
  108. package/dist/registry-BBE23CDj.mjs +576 -0
  109. package/dist/{routerShared-DrOa-26E.mjs → routerShared-CZV5aabX.mjs} +3 -3
  110. package/dist/scope/index.d.mts +3 -3
  111. package/dist/scope/index.mjs +3 -3
  112. package/dist/{sse-Bz-5ZeTt.mjs → sse-BY6sTy4P.mjs} +1 -1
  113. package/dist/testing/index.d.mts +2 -2
  114. package/dist/testing/index.mjs +16 -7
  115. package/dist/testing/storageContract.d.mts +1 -1
  116. package/dist/types/index.d.mts +5 -5
  117. package/dist/types/storage.d.mts +1 -1
  118. package/dist/{types-C_s5moIu.mjs → types-Bi0r0vjG.mjs} +53 -1
  119. package/dist/{types-BQsjgQzS.d.mts → types-BsJMEQ4D.d.mts} +106 -12
  120. package/dist/{types-DrBaUwyV.d.mts → types-D-fYtKjb.d.mts} +33 -10
  121. package/dist/{types-CTYvcwHe.d.mts → types-DVfpSfx2.d.mts} +42 -1
  122. package/dist/utils/index.d.mts +1286 -2
  123. package/dist/utils/index.mjs +1 -1
  124. package/dist/{utils-_h9B3c57.mjs → utils-DC5ycPfr.mjs} +89 -40
  125. package/dist/{buildHandler-CcFOpJLh.mjs → validate-By96rH0r.mjs} +8 -299
  126. package/dist/{versioning-hmkPcDlX.d.mts → versioning-ZwX9tmbS.d.mts} +1 -1
  127. package/package.json +21 -28
  128. package/skills/arc/SKILL.md +300 -706
  129. package/skills/arc/references/auth.md +19 -7
  130. package/skills/arc-code-review/SKILL.md +1 -1
  131. package/skills/arc-code-review/references/arc-cheatsheet.md +100 -322
  132. package/dist/createActionRouter-S3MLVYot.mjs +0 -220
  133. package/dist/index-bRjYu21O.d.mts +0 -1320
  134. package/dist/org/index.d.mts +0 -66
  135. package/dist/org/index.mjs +0 -486
  136. package/dist/org/types.d.mts +0 -82
  137. package/dist/org/types.mjs +0 -1
  138. package/dist/registry-I-ogLgL9.mjs +0 -46
  139. /package/dist/{EventTransport-CT_52aWU.d.mts → EventTransport-C-2oAHtw.d.mts} +0 -0
  140. /package/dist/{EventTransport-DLWoUMHy.mjs → EventTransport-Hxvv5QQz.mjs} +0 -0
  141. /package/dist/{actionPermissions-CyUkQu6O.mjs → actionPermissions-Bjmvn7Eb.mjs} +0 -0
  142. /package/dist/{elevation-BXOWoGCF.d.mts → elevation-0YBpa663.d.mts} +0 -0
  143. /package/dist/{elevation-DgoeTyfX.mjs → elevation-Dci0AYLT.mjs} +0 -0
  144. /package/dist/{errorHandler-DFr45ZG4.d.mts → errorHandler-mHuyWzZE.d.mts} +0 -0
  145. /package/dist/{externalPaths-BD5nw6St.d.mts → externalPaths-DFg-2KTp.d.mts} +0 -0
  146. /package/dist/{interface-beEtJyWM.d.mts → interface-CH0OQudo.d.mts} +0 -0
  147. /package/dist/{interface-DfLGcus7.d.mts → interface-NwJ_qPlY.d.mts} +0 -0
  148. /package/dist/{keys-CGcCbNyu.mjs → keys-DopsCuyQ.mjs} +0 -0
  149. /package/dist/{loadResources-DBMQg_Aj.mjs → loadResources-ChQEj8ih.mjs} +0 -0
  150. /package/dist/{metrics-Qnvwc-LQ.mjs → metrics-TuOmguhi.mjs} +0 -0
  151. /package/dist/{replyHelpers-CK-FNO8E.mjs → replyHelpers-C-gD32oF.mjs} +0 -0
  152. /package/dist/{schemaIR-lYhC2gE5.mjs → schemaIR-Ctc89DSn.mjs} +0 -0
  153. /package/dist/{sessionManager-C4Le_UB3.d.mts → sessionManager-BqFegc0W.d.mts} +0 -0
  154. /package/dist/{storage-Dfzt4VTl.d.mts → storage-D2KZJAmn.d.mts} +0 -0
  155. /package/dist/{store-helpers-BkIN9-vu.mjs → store-helpers-B0sunfZZ.mjs} +0 -0
  156. /package/dist/{tracing-QJVprktp.d.mts → tracing-Dm8n7Cnn.d.mts} +0 -0
  157. /package/dist/{versioning-BUrT5aP4.mjs → versioning-B6mimogM.mjs} +0 -0
  158. /package/dist/{websocket-ChC2rqe1.d.mts → websocket-BkjeGZRn.d.mts} +0 -0
@@ -1,6 +1,4 @@
1
- import { n as fieldRulesToZod, r as createMcpServer, t as resourceToTools } from "../../resourceToTools-tFYUNmM0.mjs";
2
- import { createHash, randomUUID } from "node:crypto";
3
- import fp from "fastify-plugin";
1
+ import { a as defaultCrudDescription, c as mcpHandlerAdapter, d as toCallToolResult, f as toCallToolSuccess, i as fieldRulesToZod, l as permissionDeniedResult, m as createMcpServer, n as mcpPlugin, o as resolveCrudDescription, p as buildRequestContext, r as resourceToTools, s as invokeController, t as filterResourcesForMcp, u as toCallToolError } from "../../mcpPlugin-7vGV51ED.mjs";
4
2
  //#region src/integrations/mcp/defineTool.ts
5
3
  /**
6
4
  * Define a type-safe MCP tool.
@@ -186,528 +184,4 @@ function definePrompt(name, config) {
186
184
  };
187
185
  }
188
186
  //#endregion
189
- //#region src/integrations/mcp/authBridge.ts
190
- /**
191
- * @classytic/arc — MCP Auth Bridge
192
- *
193
- * Resolves MCP session identity from request headers.
194
- * Supports three modes — the user chooses:
195
- *
196
- * 1. `false` — no auth, anonymous access
197
- * 2. `BetterAuthHandler` — OAuth 2.1 via Better Auth
198
- * 3. `McpAuthResolver` — custom function (API key, JWT, gateway headers, etc.)
199
- */
200
- /** Distinguish BetterAuthHandler from McpAuthResolver */
201
- function isBetterAuth(auth) {
202
- return typeof auth === "object" && auth !== null && "api" in auth && "handler" in auth;
203
- }
204
- /**
205
- * Resolve MCP session identity from request headers.
206
- *
207
- * @param headers - HTTP request headers
208
- * @param auth - false | BetterAuthHandler | McpAuthResolver
209
- * @param authCache - Optional short-lived cache to avoid redundant auth lookups
210
- */
211
- async function resolveMcpAuth(headers, auth, authCache) {
212
- if (auth === false) return null;
213
- const cacheKey = authCache ? extractAuthCacheKey(headers) : null;
214
- if (cacheKey && authCache) {
215
- const cached = authCache.get(cacheKey);
216
- if (cached !== void 0) return cached;
217
- }
218
- let result = null;
219
- if (typeof auth === "function") try {
220
- result = await auth(headers);
221
- } catch {
222
- result = null;
223
- }
224
- else if (isBetterAuth(auth)) try {
225
- const session = await auth.api.getMcpSession({ headers });
226
- if (!session?.userId) result = null;
227
- else result = {
228
- userId: session.userId,
229
- organizationId: session.activeOrganizationId,
230
- ...session.clientId ? { clientId: session.clientId } : {},
231
- ...session.scopes ? { scopes: session.scopes.split(" ") } : {}
232
- };
233
- } catch {
234
- result = null;
235
- }
236
- if (cacheKey && authCache) authCache.set(cacheKey, result);
237
- return result;
238
- }
239
- const DEFAULT_AUTH_CACHE_TTL_MS = 5e3;
240
- const DEFAULT_AUTH_CACHE_MAX = 500;
241
- /** Short-lived auth cache to avoid redundant auth resolver calls in stateless mode */
242
- var McpAuthCache = class {
243
- cache = /* @__PURE__ */ new Map();
244
- ttlMs;
245
- maxEntries;
246
- constructor(opts) {
247
- this.ttlMs = opts?.ttlMs ?? DEFAULT_AUTH_CACHE_TTL_MS;
248
- this.maxEntries = opts?.maxEntries ?? DEFAULT_AUTH_CACHE_MAX;
249
- }
250
- get(key) {
251
- const entry = this.cache.get(key);
252
- if (!entry) return void 0;
253
- if (Date.now() > entry.expires) {
254
- this.cache.delete(key);
255
- return;
256
- }
257
- return entry.result;
258
- }
259
- set(key, result) {
260
- if (this.cache.size >= this.maxEntries) {
261
- const now = Date.now();
262
- for (const [k, v] of this.cache) if (now > v.expires) this.cache.delete(k);
263
- if (this.cache.size >= this.maxEntries) {
264
- const firstKey = this.cache.keys().next().value;
265
- if (firstKey) this.cache.delete(firstKey);
266
- }
267
- }
268
- this.cache.set(key, {
269
- result,
270
- expires: Date.now() + this.ttlMs
271
- });
272
- }
273
- };
274
- /**
275
- * Extract a cache key from auth-related headers.
276
- * Uses SHA-256 hash of header values to prevent cache key collisions
277
- * and avoid storing raw credentials in memory.
278
- */
279
- function extractAuthCacheKey(headers) {
280
- if (headers.authorization) return `authz:${hashForCache(headers.authorization)}`;
281
- if (headers["x-api-key"]) return `apikey:${hashForCache(headers["x-api-key"])}`;
282
- return null;
283
- }
284
- function hashForCache(value) {
285
- return createHash("sha256").update(value).digest("hex").slice(0, 32);
286
- }
287
- /**
288
- * Register OAuth 2.1 discovery endpoints for MCP clients.
289
- * Only relevant when using Better Auth — custom auth doesn't need these.
290
- */
291
- async function registerOAuthDiscovery(fastify, auth) {
292
- fastify.get("/.well-known/oauth-authorization-server", async (req, reply) => {
293
- await forwardResponse(reply, await auth.handler(toWebRequest(req)));
294
- });
295
- fastify.get("/.well-known/oauth-protected-resource", async (req, reply) => {
296
- await forwardResponse(reply, await auth.handler(toWebRequest(req)));
297
- });
298
- }
299
- function toWebRequest(req) {
300
- const protocol = req.protocol ?? "http";
301
- const host = req.hostname ?? "localhost";
302
- return new Request(`${protocol}://${host}${req.url}`, {
303
- method: req.method,
304
- headers: req.headers
305
- });
306
- }
307
- async function forwardResponse(reply, response) {
308
- reply.status(response.status);
309
- response.headers.forEach((value, key) => {
310
- if (key.toLowerCase() !== "transfer-encoding") reply.header(key, value);
311
- });
312
- reply.send(await response.text());
313
- }
314
- //#endregion
315
- //#region src/integrations/mcp/schemaResources.ts
316
- /**
317
- * Register MCP Resources for schema discovery.
318
- */
319
- function registerSchemaResources(server, resources, overrides) {
320
- const srv = server;
321
- srv.resource("schemas", "arc://schemas", {
322
- title: "Arc Resource Schemas",
323
- description: "All available resources",
324
- mimeType: "application/json"
325
- }, async () => ({ contents: [{
326
- uri: "arc://schemas",
327
- mimeType: "application/json",
328
- text: JSON.stringify(resources.map((r) => ({
329
- name: r.name,
330
- displayName: r.displayName,
331
- fieldCount: r.schemaOptions?.fieldRules ? Object.keys(r.schemaOptions.fieldRules).length : 0,
332
- operations: getOps(r, overrides?.[r.name]?.operations),
333
- presets: r._appliedPresets ?? []
334
- })), null, 2)
335
- }] }));
336
- for (const r of resources) {
337
- const uri = `arc://schemas/${r.name}`;
338
- const schemaOpts = r.schemaOptions;
339
- srv.resource(`schema-${r.name}`, uri, {
340
- title: `${r.displayName} Schema`,
341
- description: `Schema for ${r.displayName}`,
342
- mimeType: "application/json"
343
- }, async () => ({ contents: [{
344
- uri,
345
- mimeType: "application/json",
346
- text: JSON.stringify({
347
- name: r.name,
348
- displayName: r.displayName,
349
- operations: getOps(r, overrides?.[r.name]?.operations),
350
- fields: r.schemaOptions?.fieldRules ?? {},
351
- filterableFields: schemaOpts?.filterableFields ?? [],
352
- presets: r._appliedPresets ?? []
353
- }, null, 2)
354
- }] }));
355
- }
356
- }
357
- function getOps(r, override) {
358
- let ops = [
359
- "list",
360
- "get",
361
- "create",
362
- "update",
363
- "delete"
364
- ].filter((op) => !r.disabledRoutes?.includes(op));
365
- if (override) ops = ops.filter((op) => override.includes(op));
366
- return ops;
367
- }
368
- //#endregion
369
- //#region src/integrations/mcp/sessionCache.ts
370
- const DEFAULT_TTL_MS = 1800 * 1e3;
371
- const DEFAULT_MAX_SESSIONS = 1e3;
372
- var McpSessionCache = class {
373
- sessions = /* @__PURE__ */ new Map();
374
- ttlMs;
375
- maxSessions;
376
- cleanupTimer = null;
377
- constructor(opts = {}) {
378
- this.ttlMs = opts.ttlMs ?? DEFAULT_TTL_MS;
379
- this.maxSessions = opts.maxSessions ?? DEFAULT_MAX_SESSIONS;
380
- if (this.ttlMs > 0) {
381
- this.cleanupTimer = setInterval(() => this.cleanup(), Math.max(this.ttlMs / 2, 5e3));
382
- if (this.cleanupTimer.unref) this.cleanupTimer.unref();
383
- }
384
- }
385
- /** Get an existing session by ID */
386
- get(sessionId) {
387
- const entry = this.sessions.get(sessionId);
388
- if (!entry) return void 0;
389
- if (Date.now() - entry.lastAccessed > this.ttlMs) {
390
- this.remove(sessionId);
391
- return;
392
- }
393
- return entry;
394
- }
395
- /** Store a new session */
396
- set(sessionId, entry) {
397
- if (this.sessions.size >= this.maxSessions && !this.sessions.has(sessionId)) this.evictOldest();
398
- entry.lastAccessed = Date.now();
399
- this.sessions.set(sessionId, entry);
400
- }
401
- /** Refresh the TTL on a session */
402
- touch(sessionId) {
403
- const entry = this.sessions.get(sessionId);
404
- if (entry) entry.lastAccessed = Date.now();
405
- }
406
- /** Remove and close a session */
407
- remove(sessionId) {
408
- const entry = this.sessions.get(sessionId);
409
- if (entry) {
410
- this.closeTransport(entry);
411
- this.sessions.delete(sessionId);
412
- }
413
- }
414
- /** Remove all expired sessions */
415
- cleanup() {
416
- const now = Date.now();
417
- for (const [id, entry] of this.sessions) if (now - entry.lastAccessed > this.ttlMs) {
418
- this.closeTransport(entry);
419
- this.sessions.delete(id);
420
- }
421
- }
422
- /** Close all sessions and stop cleanup timer */
423
- close() {
424
- if (this.cleanupTimer) {
425
- clearInterval(this.cleanupTimer);
426
- this.cleanupTimer = null;
427
- }
428
- for (const [id, entry] of this.sessions) {
429
- this.closeTransport(entry);
430
- this.sessions.delete(id);
431
- }
432
- }
433
- /** Current session count */
434
- get size() {
435
- return this.sessions.size;
436
- }
437
- /** Evict the oldest (least recently accessed) session */
438
- evictOldest() {
439
- let oldestId = null;
440
- let oldestTime = Infinity;
441
- for (const [id, entry] of this.sessions) if (entry.lastAccessed < oldestTime) {
442
- oldestTime = entry.lastAccessed;
443
- oldestId = id;
444
- }
445
- if (oldestId) this.remove(oldestId);
446
- }
447
- /** Safely close a transport */
448
- closeTransport(entry) {
449
- try {
450
- const transport = entry.transport;
451
- if (transport && typeof transport.close === "function") transport.close();
452
- } catch {}
453
- }
454
- };
455
- //#endregion
456
- //#region src/integrations/mcp/mcpPlugin.ts
457
- /**
458
- * @classytic/arc — MCP Plugin (Level 1)
459
- *
460
- * Fastify plugin that auto-generates MCP tools from Arc resources.
461
- *
462
- * Two transport modes:
463
- * - **Stateless** (default) — fresh server per request, no session tracking.
464
- * Best for production, horizontal scaling, serverless, edge.
465
- * - **Stateful** — sessions cached with TTL, reused across requests.
466
- * Use when you need server-initiated notifications or long-lived connections.
467
- *
468
- * Auth is NOT enforced — the plugin respects whatever auth mode you choose:
469
- * - `auth: false` — no auth, anonymous access (dev/testing/stdio)
470
- * - `auth: betterAuthInstance` — OAuth 2.1 via Better Auth's mcp() plugin
471
- * - `auth: async (headers) => {...}` — custom function (API key, JWT, gateway, etc.)
472
- *
473
- * @example
474
- * ```typescript
475
- * // Stateless (default) — production, scalable
476
- * await app.register(mcpPlugin, { resources, auth: false });
477
- *
478
- * // Stateful — when you need session persistence
479
- * await app.register(mcpPlugin, { resources, stateful: true, sessionTtlMs: 600000 });
480
- *
481
- * // Multiple MCP endpoints scoped to different resource groups
482
- * await app.register(mcpPlugin, { resources: catalogResources, prefix: '/mcp/catalog' });
483
- * await app.register(mcpPlugin, { resources: orderResources, prefix: '/mcp/orders' });
484
- * ```
485
- */
486
- const mcpPluginImpl = async (fastify, options) => {
487
- let StreamableHTTPServerTransport;
488
- try {
489
- StreamableHTTPServerTransport = (await import("@modelcontextprotocol/sdk/server/streamableHttp.js")).StreamableHTTPServerTransport;
490
- } catch {
491
- throw new Error("@modelcontextprotocol/sdk is required for MCP support. Install it: npm install @modelcontextprotocol/sdk");
492
- }
493
- try {
494
- await import("zod");
495
- } catch {
496
- throw new Error("zod is required for MCP tool schemas. Install it: npm install zod");
497
- }
498
- let enabledResources;
499
- if (options.include) {
500
- const includeSet = new Set(options.include);
501
- enabledResources = options.resources.filter((r) => includeSet.has(r.name));
502
- } else {
503
- const excludeSet = new Set(options.exclude ?? []);
504
- enabledResources = options.resources.filter((r) => !excludeSet.has(r.name));
505
- }
506
- const overrides = options.overrides ?? {};
507
- const allTools = enabledResources.flatMap((r) => {
508
- const resOverrides = overrides[r.name] ?? {};
509
- return resourceToTools(r, {
510
- ...resOverrides,
511
- toolNamePrefix: resOverrides.toolNamePrefix ?? options.toolNamePrefix
512
- });
513
- });
514
- if (options.extraTools) allTools.push(...options.extraTools);
515
- fastify.log.info(`mcpPlugin: ${allTools.length} tools from ${enabledResources.length} resources`);
516
- const overrideOpsMap = {};
517
- for (const [name, cfg] of Object.entries(overrides)) overrideOpsMap[name] = { operations: cfg.operations };
518
- const stateful = options.stateful === true;
519
- const cache = stateful ? new McpSessionCache({
520
- ttlMs: options.sessionTtlMs,
521
- maxSessions: options.maxSessions
522
- }) : null;
523
- async function createServerInstance(authRef) {
524
- const server = await createMcpServer({
525
- name: options.serverName ?? "arc-mcp",
526
- version: options.serverVersion ?? "1.0.0",
527
- instructions: options.instructions,
528
- tools: allTools,
529
- prompts: options.extraPrompts
530
- }, authRef);
531
- registerSchemaResources(server, enabledResources, overrideOpsMap);
532
- return server;
533
- }
534
- if (options.auth && isBetterAuth(options.auth)) await registerOAuthDiscovery(fastify, options.auth);
535
- const prefix = options.prefix ?? "/mcp";
536
- fastify.get(`${prefix}/health`, async (_request, reply) => {
537
- reply.send({
538
- status: "ok",
539
- mode: stateful ? "stateful" : "stateless",
540
- tools: allTools.length,
541
- resources: enabledResources.length,
542
- toolNames: allTools.map((t) => t.name),
543
- sessions: cache?.size ?? null
544
- });
545
- });
546
- if (stateful) registerStatefulRoutes(fastify, prefix, options, cache, createServerInstance, StreamableHTTPServerTransport);
547
- else {
548
- const authCache = options.auth && options.authCacheTtlMs !== 0 ? new McpAuthCache({ ttlMs: options.authCacheTtlMs }) : void 0;
549
- registerStatelessRoutes(fastify, prefix, options, createServerInstance, StreamableHTTPServerTransport, authCache);
550
- }
551
- if (cache) fastify.addHook("onClose", async () => cache.close());
552
- const registration = {
553
- sessions: cache,
554
- toolNames: allTools.map((t) => t.name),
555
- resourceNames: enabledResources.map((r) => r.name),
556
- stateful
557
- };
558
- if (!fastify.hasDecorator("mcp")) {
559
- const registrations = /* @__PURE__ */ new Map();
560
- registrations.set(prefix, registration);
561
- const first = () => registrations.values().next().value;
562
- const decorator = {
563
- registrations,
564
- get(p) {
565
- return registrations.get(p);
566
- },
567
- get sessions() {
568
- return first()?.sessions ?? null;
569
- },
570
- get toolNames() {
571
- return first()?.toolNames ?? [];
572
- },
573
- get resourceNames() {
574
- return first()?.resourceNames ?? [];
575
- },
576
- get stateful() {
577
- return first()?.stateful ?? false;
578
- }
579
- };
580
- fastify.decorate("mcp", decorator);
581
- } else {
582
- const existing = fastify.mcp;
583
- if (existing) {
584
- if (existing.registrations.has(prefix)) throw new Error(`mcpPlugin: prefix "${prefix}" is already registered`);
585
- existing.registrations.set(prefix, registration);
586
- }
587
- }
588
- };
589
- function registerStatelessRoutes(fastify, prefix, options, createServer, Transport, authCache) {
590
- fastify.post(prefix, async (request, reply) => {
591
- const authResult = await resolveMcpAuth(request.headers, options.auth ?? false, authCache);
592
- if (!authResult && options.auth) {
593
- fastify.log.warn({
594
- msg: "mcpPlugin: auth failed",
595
- status: 401
596
- });
597
- return reply.code(401).send({
598
- jsonrpc: "2.0",
599
- error: {
600
- code: -32e3,
601
- message: "Unauthorized"
602
- }
603
- });
604
- }
605
- const authRef = { current: authResult };
606
- const transport = new Transport({ sessionIdGenerator: void 0 });
607
- await (await createServer(authRef)).connect(transport);
608
- await transport.handleRequest(request.raw, reply.raw, request.body);
609
- });
610
- fastify.get(prefix, async (_request, reply) => {
611
- reply.code(405).send({
612
- jsonrpc: "2.0",
613
- error: {
614
- code: -32e3,
615
- message: "SSE not available in stateless mode. Use stateful: true for server-initiated messages."
616
- }
617
- });
618
- });
619
- fastify.delete(prefix, async (_request, reply) => {
620
- reply.code(200).send();
621
- });
622
- }
623
- function registerStatefulRoutes(fastify, prefix, options, cache, createServer, Transport) {
624
- /** Check if the requesting principal owns the session */
625
- function isSessionOwner(entry, authResult) {
626
- if (!options.auth || !entry.auth || !authResult) return true;
627
- const prev = entry.auth;
628
- return prev.userId === authResult.userId && prev.organizationId === authResult.organizationId && prev.clientId === authResult.clientId;
629
- }
630
- fastify.post(prefix, async (request, reply) => {
631
- const authResult = await resolveMcpAuth(request.headers, options.auth ?? false);
632
- if (!authResult && options.auth) {
633
- fastify.log.warn({
634
- msg: "mcpPlugin: auth failed",
635
- status: 401
636
- });
637
- return reply.code(401).send({
638
- jsonrpc: "2.0",
639
- error: {
640
- code: -32e3,
641
- message: "Unauthorized"
642
- }
643
- });
644
- }
645
- const sessionId = request.headers["mcp-session-id"];
646
- if (sessionId) {
647
- const entry = cache.get(sessionId);
648
- if (entry) {
649
- if (!isSessionOwner(entry, authResult)) return reply.code(403).send({
650
- jsonrpc: "2.0",
651
- error: {
652
- code: -32e3,
653
- message: "Session ownership mismatch"
654
- }
655
- });
656
- cache.touch(sessionId);
657
- entry.auth = authResult;
658
- entry.authRef.current = authResult;
659
- await entry.transport.handleRequest(request.raw, reply.raw, request.body);
660
- return;
661
- }
662
- }
663
- const authRef = { current: authResult };
664
- const transport = new Transport({
665
- sessionIdGenerator: () => randomUUID(),
666
- onsessioninitialized: (newSessionId) => {
667
- cache.set(newSessionId, {
668
- transport,
669
- lastAccessed: Date.now(),
670
- organizationId: authResult?.organizationId ?? "",
671
- auth: authResult,
672
- authRef
673
- });
674
- }
675
- });
676
- await (await createServer(authRef)).connect(transport);
677
- await transport.handleRequest(request.raw, reply.raw, request.body);
678
- });
679
- fastify.get(prefix, async (request, reply) => {
680
- const sessionId = request.headers["mcp-session-id"];
681
- if (!sessionId) return reply.code(400).send({ error: "Missing Mcp-Session-Id header" });
682
- const entry = cache.get(sessionId);
683
- if (!entry) return reply.code(403).send({ error: "Unauthorized" });
684
- if (options.auth) {
685
- const authResult = await resolveMcpAuth(request.headers, options.auth);
686
- if (!isSessionOwner(entry, authResult)) return reply.code(403).send({ error: "Unauthorized" });
687
- entry.auth = authResult;
688
- entry.authRef.current = authResult;
689
- }
690
- cache.touch(sessionId);
691
- await entry.transport.handleRequest(request.raw, reply.raw);
692
- });
693
- fastify.delete(prefix, async (request, reply) => {
694
- const sessionId = request.headers["mcp-session-id"];
695
- if (!sessionId) return reply.code(400).send({ error: "Missing Mcp-Session-Id header" });
696
- const entry = cache.get(sessionId);
697
- if (!entry) return reply.code(204).send();
698
- if (options.auth) {
699
- const authResult = await resolveMcpAuth(request.headers, options.auth);
700
- if (!isSessionOwner(entry, authResult)) return reply.code(403).send({ error: "Unauthorized" });
701
- entry.auth = authResult;
702
- entry.authRef.current = authResult;
703
- }
704
- cache.remove(sessionId);
705
- reply.code(204).send();
706
- });
707
- }
708
- const mcpPlugin = fp(mcpPluginImpl, {
709
- name: "arc-mcp",
710
- fastify: "5.x"
711
- });
712
- //#endregion
713
- export { bridgeToMcp, buildMcpToolsFromBridges, createMcpServer, customGuard, definePrompt, defineTool, denied, fieldRulesToZod, getOrgId, getUserId, guard, hasOrg, isAuthenticated, isOrg, mcpPlugin, requireAuth, requireOrg, requireOrgId, requireRole, resourceToTools };
187
+ export { bridgeToMcp, buildMcpToolsFromBridges, buildRequestContext, createMcpServer, customGuard, defaultCrudDescription, definePrompt, defineTool, denied, fieldRulesToZod, filterResourcesForMcp, getOrgId, getUserId, guard, hasOrg, invokeController, isAuthenticated, isOrg, mcpHandlerAdapter, mcpPlugin, permissionDeniedResult, requireAuth, requireOrg, requireOrgId, requireRole, resolveCrudDescription, resourceToTools, toCallToolError, toCallToolResult, toCallToolSuccess };
@@ -1,9 +1,9 @@
1
- import { o as McpAuthResult, s as McpPluginOptions } from "../../types-BQsjgQzS.mjs";
1
+ import { c as McpAuthResult, l as McpPluginOptions } from "../../types-BsJMEQ4D.mjs";
2
2
 
3
3
  //#region src/integrations/mcp/testing.d.ts
4
4
  interface TestMcpClientOptions {
5
5
  /** MCP plugin options (resources, overrides, etc.) — same as mcpPlugin config */
6
- pluginOptions?: Pick<McpPluginOptions, "resources" | "overrides" | "include" | "exclude" | "toolNamePrefix" | "extraTools" | "extraPrompts" | "instructions">;
6
+ pluginOptions?: Pick<McpPluginOptions, "resources" | "overrides" | "expose" | "include" | "exclude" | "toolNamePrefix" | "extraTools" | "extraPrompts" | "instructions">;
7
7
  /** Auth identity for the test session */
8
8
  auth?: McpAuthResult | null;
9
9
  /** Server name (default: 'test-mcp') */
@@ -1,4 +1,4 @@
1
- import { r as createMcpServer, t as resourceToTools } from "../../resourceToTools-tFYUNmM0.mjs";
1
+ import { m as createMcpServer, r as resourceToTools, t as filterResourcesForMcp } from "../../mcpPlugin-7vGV51ED.mjs";
2
2
  //#region src/integrations/mcp/testing.ts
3
3
  /**
4
4
  * @classytic/arc/mcp/testing — MCP Test Utilities
@@ -52,15 +52,11 @@ async function createTestMcpClient(options = {}) {
52
52
  const auth = options.auth ?? { userId: "test-user" };
53
53
  const serverName = options.serverName ?? "test-mcp";
54
54
  const overrides = pluginOpts.overrides ?? {};
55
- let enabledResources = pluginOpts.resources ?? [];
56
- if (pluginOpts.include) {
57
- const includeSet = new Set(pluginOpts.include);
58
- enabledResources = enabledResources.filter((r) => includeSet.has(r.name));
59
- } else if (pluginOpts.exclude) {
60
- const excludeSet = new Set(pluginOpts.exclude);
61
- enabledResources = enabledResources.filter((r) => !excludeSet.has(r.name));
62
- }
63
- const tools = enabledResources.flatMap((r) => {
55
+ const tools = filterResourcesForMcp(pluginOpts.resources ?? [], {
56
+ expose: pluginOpts.expose,
57
+ include: pluginOpts.include,
58
+ exclude: pluginOpts.exclude
59
+ }).flatMap((r) => {
64
60
  const resOverrides = overrides[r.name] ?? {};
65
61
  return resourceToTools(r, {
66
62
  ...resOverrides,
@@ -90,7 +90,32 @@ const streamlinePluginImpl = async (fastify, options) => {
90
90
  const routePrefix = `${routeScope}/${id}`;
91
91
  fastify.post(`${routePrefix}/start`, { preHandler: authPreHandler }, async (request, reply) => {
92
92
  if (!await checkPerm("start", request)) throw new ForbiddenError();
93
- const { input, meta, idempotencyKey, priority } = request.body ?? {};
93
+ const body = request.body;
94
+ if (body !== null && body !== void 0 && typeof body !== "object") throw createError(422, `[Arc/Streamline] '/${id}/start' body must be a JSON object.`, {
95
+ code: "arc.streamline.invalid_body",
96
+ workflowId: id
97
+ });
98
+ const envelopeKeys = new Set([
99
+ "input",
100
+ "meta",
101
+ "idempotencyKey",
102
+ "priority"
103
+ ]);
104
+ const bodyRecord = body ?? {};
105
+ const unknownKeys = Object.keys(bodyRecord).filter((k) => !envelopeKeys.has(k));
106
+ const hasInputKey = Object.hasOwn(bodyRecord, "input");
107
+ if (unknownKeys.length > 0 && !hasInputKey) throw createError(422, `[Arc/Streamline] '/${id}/start' expects '{ input: {...} }'. Got top-level keys [${unknownKeys.join(", ")}] but no 'input' key. Wrap your workflow payload: { "input": { ${unknownKeys.map((k) => `"${k}": ...`).join(", ")} } }.`, {
108
+ code: "arc.streamline.missing_input_envelope",
109
+ workflowId: id,
110
+ received: unknownKeys,
111
+ expected: "input"
112
+ });
113
+ if (unknownKeys.length > 0) throw createError(422, `[Arc/Streamline] '/${id}/start' got unknown top-level keys [${unknownKeys.join(", ")}]. Allowed envelope keys: input, meta, idempotencyKey, priority. Did you mean to nest these under 'input'?`, {
114
+ code: "arc.streamline.unknown_envelope_keys",
115
+ workflowId: id,
116
+ unknown: unknownKeys
117
+ });
118
+ const { input, meta, idempotencyKey, priority } = bodyRecord;
94
119
  const tenantOpts = resolveTenantOpts(request);
95
120
  const run = await wf.start(input, {
96
121
  meta,
@@ -1,4 +1,4 @@
1
- import { c as WebSocketAdapter } from "../websocket-ChC2rqe1.mjs";
1
+ import { c as WebSocketAdapter } from "../websocket-BkjeGZRn.mjs";
2
2
 
3
3
  //#region src/integrations/websocket-redis.d.ts
4
4
  interface RedisLike {
@@ -1,2 +1,2 @@
1
- import { a as WebSocketMessage, c as WebSocketAdapter, i as WebSocketClient, n as websocketPlugin, o as WebSocketPluginOptions, r as AuthResult, s as LocalWebSocketAdapter, t as RoomManager } from "../websocket-ChC2rqe1.mjs";
1
+ import { a as WebSocketMessage, c as WebSocketAdapter, i as WebSocketClient, n as websocketPlugin, o as WebSocketPluginOptions, r as AuthResult, s as LocalWebSocketAdapter, t as RoomManager } from "../websocket-BkjeGZRn.mjs";
2
2
  export { AuthResult, LocalWebSocketAdapter, RoomManager, WebSocketAdapter, WebSocketClient, WebSocketMessage, WebSocketPluginOptions, websocketPlugin };
@@ -375,6 +375,7 @@ const websocketPluginImpl = async (fastify, options) => {
375
375
  if (room.startsWith("org:")) {
376
376
  const parts = room.split(":");
377
377
  const orgId = parts[1];
378
+ if (!orgId) return;
378
379
  const actualRoom = parts.slice(2).join(":");
379
380
  rooms.broadcastToOrg(orgId, actualRoom, message);
380
381
  } else rooms.broadcast(room, message);
@@ -0,0 +1,51 @@
1
+ import { t as ResourceRegistry } from "./ResourceRegistry-f48hFk3m.mjs";
2
+ import { resolve } from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ //#region src/cli/utils/loadResourcesFromEntry.ts
5
+ /**
6
+ * Shared loader for CLI commands that need to read a host's resource graph
7
+ * from a built entry file (`introspect`, `docs`, `describe`).
8
+ *
9
+ * Each command historically duplicated the same dance: `pathToFileURL` →
10
+ * dynamic `import()` → walk the module's exports → match anything that
11
+ * looks structurally like a `ResourceDefinition` (carries `name`,
12
+ * `_registryMeta`, `toPlugin`) → register into a fresh `ResourceRegistry`.
13
+ * Extracting the duplication here means a future signature change to
14
+ * the registry surface or the resource-shape sentinel only touches one
15
+ * spot.
16
+ *
17
+ * The structural match (vs `instanceof ResourceDefinition`) is deliberate:
18
+ * the host's entry file is a compiled `dist/` artifact that may have been
19
+ * built against a different arc minor version, so a class-identity check
20
+ * would reject genuinely-compatible objects across version drift.
21
+ */
22
+ /**
23
+ * Load the host's entry file, walk its exports, and register every
24
+ * `ResourceDefinition`-shaped value into a fresh `ResourceRegistry`.
25
+ *
26
+ * Returns `{ registry, registered }`:
27
+ * - `registry` is populated and ready to query (`getAll()`, `getStats()`,
28
+ * `getIntrospection()`).
29
+ * - `registered` is the count — callers can short-circuit with a
30
+ * friendly "no resources found" message when it's zero.
31
+ */
32
+ async function loadResourcesFromEntry(entryPath) {
33
+ const entryModule = await import(pathToFileURL(resolve(process.cwd(), entryPath)).href);
34
+ const registry = new ResourceRegistry();
35
+ let registered = 0;
36
+ const tryRegister = (value) => {
37
+ if (value && typeof value === "object" && "name" in value && "_registryMeta" in value && "toPlugin" in value) {
38
+ const resourceLike = value;
39
+ registry.register(resourceLike, resourceLike._registryMeta ?? {});
40
+ registered++;
41
+ }
42
+ };
43
+ for (const exported of Object.values(entryModule)) if (Array.isArray(exported)) for (const entry of exported) tryRegister(entry);
44
+ else tryRegister(exported);
45
+ return {
46
+ registry,
47
+ registered
48
+ };
49
+ }
50
+ //#endregion
51
+ export { loadResourcesFromEntry as t };