@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,386 @@
1
+ # Arc MCP Integration
2
+
3
+ Expose Arc resources as MCP tools for AI agents. Two levels: zero-config auto-generation or fully custom tool definitions.
4
+
5
+ **Requires:** `@modelcontextprotocol/sdk` (peer dep), `zod` (peer dep)
6
+
7
+ ```bash
8
+ npm install @modelcontextprotocol/sdk zod
9
+ ```
10
+
11
+ ## Level 1 — Auto-Generate from Resources
12
+
13
+ ```typescript
14
+ import { mcpPlugin } from '@classytic/arc/mcp';
15
+
16
+ await app.register(mcpPlugin, {
17
+ resources: [productResource, taskResource],
18
+ auth: false,
19
+ exclude: ['credential'],
20
+ overrides: { product: { operations: ['list', 'get'] } },
21
+ });
22
+ ```
23
+
24
+ Per resource, generates up to 5 tools: `list_{plural}`, `get_{name}`, `create_{name}`, `update_{name}`, `delete_{name}`.
25
+
26
+ Tool handlers call `BaseController` — same pipeline as REST (auth, org-scoping, hooks, field permissions, cache).
27
+
28
+ ### McpPluginOptions
29
+
30
+ | Option | Type | Default | Description |
31
+ |--------|------|---------|-------------|
32
+ | `resources` | `ResourceDefinition[]` | required | Resources to expose |
33
+ | `auth` | `BetterAuthHandler \| McpAuthResolver \| false` | `false` | Auth mode (see Auth section) |
34
+ | `prefix` | `string` | `'/mcp'` | MCP endpoint path |
35
+ | `serverName` | `string` | `'arc-mcp'` | Server identity |
36
+ | `serverVersion` | `string` | `'1.0.0'` | Server version |
37
+ | `instructions` | `string` | — | LLM guidance on tool usage |
38
+ | `exclude` | `string[]` | — | Resource names to exclude |
39
+ | `toolNamePrefix` | `string` | — | Prefix: `'crm'` → `crm_list_products` |
40
+ | `overrides` | `Record<string, McpResourceConfig>` | — | Per-resource operation/field overrides |
41
+ | `extraTools` | `ToolDefinition[]` | — | Hand-written tools alongside auto-generated |
42
+ | `extraPrompts` | `PromptDefinition[]` | — | Custom prompts |
43
+ | `stateful` | `boolean` | `false` | `false` = stateless (default, scalable). `true` = session-cached. |
44
+ | `sessionTtlMs` | `number` | `1800000` | Session TTL (stateful only) |
45
+ | `maxSessions` | `number` | `1000` | Max concurrent sessions (stateful only) |
46
+
47
+ ### Tool Annotations (auto-set)
48
+
49
+ | Operation | Annotations |
50
+ |-----------|-------------|
51
+ | `list`, `get` | `readOnlyHint: true` |
52
+ | `create` | `destructiveHint: false` |
53
+ | `update`, `delete` | `destructiveHint: true, idempotentHint: true` |
54
+
55
+ ### Multiple MCP Endpoints
56
+
57
+ Mount separate servers scoped to different resource groups:
58
+
59
+ ```typescript
60
+ await app.register(mcpPlugin, { resources: catalogResources, prefix: '/mcp/catalog' });
61
+ await app.register(mcpPlugin, { resources: orderResources, prefix: '/mcp/orders' });
62
+ ```
63
+
64
+ ## Auth — Three Modes
65
+
66
+ Arc doesn't enforce an auth strategy. You choose what fits.
67
+
68
+ ### 1. No Auth (dev/testing/stdio)
69
+
70
+ ```typescript
71
+ await app.register(mcpPlugin, { resources, auth: false });
72
+ ```
73
+
74
+ All tools open. Every request gets `{ userId: 'anonymous' }`.
75
+
76
+ ### 2. Better Auth OAuth 2.1 (production SaaS)
77
+
78
+ Full MCP spec-compliant OAuth 2.1: authorization code + PKCE, token exchange, dynamic client registration.
79
+
80
+ ```typescript
81
+ // auth.config.ts — add mcp() plugin to Better Auth
82
+ import { mcp } from 'better-auth/plugins';
83
+ betterAuth({ plugins: [mcp({ loginPage: '/login' })] });
84
+
85
+ // app.ts
86
+ await app.register(mcpPlugin, { resources, auth: getAuth() });
87
+ ```
88
+
89
+ Auto-registers discovery endpoints:
90
+ - `GET /.well-known/oauth-authorization-server` (RFC 8414)
91
+ - `GET /.well-known/oauth-protected-resource` (RFC 9728)
92
+
93
+ The auth flow:
94
+ ```
95
+ MCP Client → discovers OAuth endpoints → registers → user authorizes → gets token
96
+ MCP Client → POST /mcp (Authorization: Bearer <token>)
97
+ Arc → auth.api.getMcpSession({ headers }) → { userId, organizationId }
98
+ Arc → BaseController scopes by org automatically
99
+ ```
100
+
101
+ ### 3. Custom Auth Function (API key, gateway, static org)
102
+
103
+ Pass an `McpAuthResolver` — a function that receives headers and returns identity:
104
+
105
+ ```typescript
106
+ type McpAuthResolver = (headers: Record<string, string | undefined>) =>
107
+ Promise<McpAuthResult | null> | McpAuthResult | null;
108
+ ```
109
+
110
+ Return `{ userId, organizationId? }` to allow. Return `null` to reject (401).
111
+
112
+ ```typescript
113
+ // API key
114
+ auth: async (headers) => {
115
+ if (headers['x-api-key'] !== process.env.MCP_API_KEY) return null;
116
+ return { userId: 'service', organizationId: 'org-123' };
117
+ },
118
+
119
+ // Gateway-validated JWT (token already verified upstream)
120
+ auth: async (headers) => {
121
+ const userId = headers['x-user-id'];
122
+ const orgId = headers['x-org-id'];
123
+ return userId ? { userId, organizationId: orgId } : null;
124
+ },
125
+
126
+ // Static org (trusted internal network)
127
+ auth: async () => ({ userId: 'internal', organizationId: 'org-main' }),
128
+
129
+ // Bearer token with custom validation
130
+ auth: async (headers) => {
131
+ const token = headers['authorization']?.replace('Bearer ', '');
132
+ if (!token) return null;
133
+ const payload = await verifyJwt(token);
134
+ return payload ? { userId: payload.sub, organizationId: payload.org } : null;
135
+ },
136
+ ```
137
+
138
+ ### Multi-Tenancy
139
+
140
+ The `organizationId` from auth flows into BaseController's org-scoping automatically:
141
+
142
+ ```
143
+ auth resolver returns { userId: 'alice', organizationId: 'org-a' }
144
+ → buildRequestContext sets _scope: { kind: 'member', organizationId: 'org-a' }
145
+ → QueryResolver adds { organizationId: 'org-a' } to every query
146
+ → Agent only sees org-a's data — no cross-tenant leaks
147
+ ```
148
+
149
+ Works with any `tenantField` (`organizationId`, `workspaceId`, `teamId`). Resources with `tenantField: false` are visible to all authenticated users.
150
+
151
+ ## Level 2 — Custom Tools
152
+
153
+ ```typescript
154
+ import { createMcpServer, defineTool, definePrompt } from '@classytic/arc/mcp';
155
+ import { z } from 'zod';
156
+
157
+ const server = await createMcpServer({
158
+ name: 'social-automation',
159
+ version: '1.0.0',
160
+ instructions: 'Use list_providers first.',
161
+ tools: [
162
+ defineTool('send_notification', {
163
+ description: 'Send a notification to a user or channel',
164
+ input: {
165
+ channel: z.enum(['email', 'telegram', 'whatsapp']),
166
+ recipient: z.string(),
167
+ message: z.string(),
168
+ },
169
+ annotations: { openWorldHint: true },
170
+ handler: async ({ channel, recipient, message }) => {
171
+ const result = await notificationService.send(channel, recipient, message);
172
+ return { content: [{ type: 'text', text: JSON.stringify(result) }] };
173
+ },
174
+ }),
175
+ ],
176
+ prompts: [
177
+ definePrompt('content_calendar', {
178
+ description: 'Plan a content calendar',
179
+ args: { platforms: z.string(), theme: z.string().optional() },
180
+ handler: ({ platforms, theme }) => ({
181
+ messages: [{ role: 'user', content: { type: 'text', text: `Plan content for ${platforms}` } }],
182
+ }),
183
+ }),
184
+ ],
185
+ });
186
+ ```
187
+
188
+ ### defineTool() — Input is a flat Zod shape
189
+
190
+ Pass `{ field: z.string() }`, NOT `z.object(...)`. The SDK wraps it internally.
191
+
192
+ ```typescript
193
+ defineTool('get_weather', {
194
+ description: 'Get weather for a city',
195
+ input: { city: z.string(), units: z.enum(['celsius', 'fahrenheit']).optional() },
196
+ annotations: { readOnlyHint: true, openWorldHint: true },
197
+ handler: async ({ city, units }) => ({
198
+ content: [{ type: 'text', text: JSON.stringify(await weatherApi.get(city, units)) }],
199
+ }),
200
+ });
201
+ ```
202
+
203
+ ## Project Structure
204
+
205
+ Resources stay in `src/resources/`. Custom MCP tools co-locate with their resource:
206
+
207
+ ```
208
+ src/resources/
209
+ product/
210
+ product.model.ts
211
+ product.resource.ts
212
+ product.mcp.ts ← custom MCP tools (optional)
213
+ order/
214
+ order.model.ts
215
+ order.resource.ts
216
+ order.mcp.ts ← domain-specific tools (fulfill, cancel, track)
217
+ ```
218
+
219
+ Generate with CLI: `arc generate resource order --mcp` or `arc generate mcp analytics`
220
+
221
+ Wire in app.ts:
222
+
223
+ ```typescript
224
+ import { fulfillOrderTool } from './resources/order/order.mcp.js';
225
+
226
+ await app.register(mcpPlugin, {
227
+ resources,
228
+ extraTools: [fulfillOrderTool],
229
+ });
230
+ ```
231
+
232
+ ## Guards — Permissions for Custom Tools
233
+
234
+ Auto-generated tools go through BaseController (permissions enforced automatically). Custom tools need explicit guards:
235
+
236
+ ```typescript
237
+ import { defineTool, guard, requireAuth, requireOrg, requireRole, customGuard } from '@classytic/arc/mcp';
238
+ ```
239
+
240
+ ### guard() Wrapper — Compose Guards
241
+
242
+ ```typescript
243
+ // Admin-only tool
244
+ defineTool('delete_all', {
245
+ description: 'Delete all records',
246
+ handler: guard(requireAuth, requireOrg, requireRole('admin'), async (input, ctx) => {
247
+ // Only runs if: authenticated + has org + has admin role
248
+ return { content: [{ type: 'text', text: 'Deleted' }] };
249
+ }),
250
+ });
251
+ ```
252
+
253
+ ### Built-in Guards
254
+
255
+ | Guard | Rejects when |
256
+ |-------|-------------|
257
+ | `requireAuth` | No session or anonymous |
258
+ | `requireOrg` | No `organizationId` in session |
259
+ | `requireRole('admin')` | User lacks the role (checks `session.roles`) |
260
+ | `requireRole('admin', 'editor')` | User has neither role (OR logic) |
261
+ | `requireOrgId('org-x')` | Session org doesn't match |
262
+ | `customGuard(fn, msg)` | Predicate returns false |
263
+
264
+ ### Inline Checks
265
+
266
+ For conditional logic inside handlers instead of wrapping:
267
+
268
+ ```typescript
269
+ import { isAuthenticated, hasOrg, getUserId, getOrgId, denied } from '@classytic/arc/mcp';
270
+
271
+ defineTool('flexible_action', {
272
+ description: 'Does different things based on auth',
273
+ handler: async (input, ctx) => {
274
+ if (!isAuthenticated(ctx)) return denied('Login required');
275
+
276
+ const userId = getUserId(ctx);
277
+ const orgId = getOrgId(ctx);
278
+
279
+ // Different behavior for different orgs
280
+ if (orgId === 'org-premium') {
281
+ // premium features
282
+ }
283
+
284
+ return { content: [{ type: 'text', text: `Done by ${userId}` }] };
285
+ },
286
+ });
287
+ ```
288
+
289
+ ### Custom Guard
290
+
291
+ ```typescript
292
+ const businessHours = customGuard(
293
+ () => { const h = new Date().getHours(); return h >= 9 && h < 17; },
294
+ 'Only available during business hours (9-5)',
295
+ );
296
+
297
+ const maxRequestsGuard = customGuard(
298
+ async (ctx) => await rateLimiter.check(ctx.session?.userId),
299
+ 'Rate limit exceeded',
300
+ );
301
+
302
+ defineTool('sensitive_op', {
303
+ description: 'Time-restricted operation',
304
+ handler: guard(requireAuth, businessHours, async (input, ctx) => { ... }),
305
+ });
306
+ ```
307
+
308
+ ### Auth Resolver with Roles
309
+
310
+ For guards to check roles, your auth resolver must return them:
311
+
312
+ ```typescript
313
+ auth: async (headers) => {
314
+ const token = await verifyJwt(headers['authorization']);
315
+ return {
316
+ userId: token.sub,
317
+ organizationId: token.org,
318
+ roles: token.roles, // ← requireRole() checks this
319
+ orgRoles: token.orgRoles, // ← for org-level permissions
320
+ };
321
+ },
322
+ ```
323
+
324
+ ## fieldRulesToZod — Schema Conversion
325
+
326
+ Convert Arc's `schemaOptions.fieldRules` to flat Zod shapes for custom tools:
327
+
328
+ ```typescript
329
+ import { fieldRulesToZod } from '@classytic/arc/mcp';
330
+
331
+ const createShape = fieldRulesToZod(resource.schemaOptions.fieldRules, {
332
+ mode: 'create', // 'create' | 'update' | 'list'
333
+ hiddenFields: ['internalScore'],
334
+ readonlyFields: ['slug'],
335
+ });
336
+ // → { name: z.string(), price: z.number(), category: z.enum([...]) }
337
+ ```
338
+
339
+ ## Schema Discovery — MCP Resources
340
+
341
+ Auto-registered for agent discovery:
342
+
343
+ - `arc://schemas` — list all resources with field counts, operations, presets
344
+ - `arc://schemas/{name}` — full schema for a specific resource
345
+
346
+ ## Testing with Claude CLI
347
+
348
+ ```bash
349
+ claude mcp add --transport http my-api http://localhost:3000/mcp
350
+ echo "List all products" | claude -p --allowedTools "mcp__my-api__*"
351
+ claude mcp remove my-api
352
+ ```
353
+
354
+ ## Transport Modes
355
+
356
+ ### Stateless (default) — Production
357
+
358
+ Fresh server per request. No session tracking, no memory overhead. Best for horizontal scaling, serverless, edge.
359
+
360
+ ```typescript
361
+ await app.register(mcpPlugin, { resources, auth: false });
362
+ // stateful defaults to false — stateless mode
363
+ ```
364
+
365
+ - `POST /mcp` — each request gets a fresh server + transport
366
+ - `GET /mcp` — returns 405 (no SSE in stateless mode)
367
+ - `DELETE /mcp` — no-op
368
+
369
+ ### Stateful — When Needed
370
+
371
+ Sessions cached with TTL. Use when you need server-initiated notifications or long-lived connections.
372
+
373
+ ```typescript
374
+ await app.register(mcpPlugin, {
375
+ resources,
376
+ stateful: true, // enable session persistence
377
+ sessionTtlMs: 600000, // 10 min TTL
378
+ maxSessions: 500, // max concurrent sessions
379
+ });
380
+ ```
381
+
382
+ - `POST /mcp` — reuses session via `Mcp-Session-Id` header, or creates new
383
+ - `GET /mcp` — SSE stream for server-initiated messages
384
+ - `DELETE /mcp` — terminates session
385
+
386
+ Sessions: lazily created, TTL-cached, LRU-evicted at max capacity, auto-cleaned on shutdown.