@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.
- package/README.md +187 -18
- package/bin/arc.js +11 -3
- package/dist/BaseController-CkM5dUh_.mjs +1031 -0
- package/dist/{EventTransport-BkUDYZEb.d.mts → EventTransport-wc5hSLik.d.mts} +1 -1
- package/dist/{HookSystem-BsGV-j2l.mjs → HookSystem-COkyWztM.mjs} +2 -3
- package/dist/{ResourceRegistry-7Ic20ZMw.mjs → ResourceRegistry-DeCIFlix.mjs} +8 -5
- package/dist/adapters/index.d.mts +3 -5
- package/dist/adapters/index.mjs +2 -3
- package/dist/{prisma-DJbMt3yf.mjs → adapters-DTC4Ug66.mjs} +45 -12
- package/dist/audit/index.d.mts +4 -7
- package/dist/audit/index.mjs +2 -29
- package/dist/audit/mongodb.d.mts +1 -4
- package/dist/audit/mongodb.mjs +2 -3
- package/dist/auth/index.d.mts +7 -9
- package/dist/auth/index.mjs +65 -63
- package/dist/auth/redis-session.d.mts +1 -1
- package/dist/auth/redis-session.mjs +1 -2
- package/dist/{betterAuthOpenApi-DjWDddNc.mjs → betterAuthOpenApi-lz0IRbXJ.mjs} +4 -6
- package/dist/cache/index.d.mts +23 -23
- package/dist/cache/index.mjs +4 -6
- package/dist/{caching-GSDJcA6-.mjs → caching-BSXB-Xr7.mjs} +2 -24
- package/dist/chunk-BpYLSNr0.mjs +14 -0
- package/dist/circuitBreaker-BOBOpN2w.mjs +284 -0
- package/dist/circuitBreaker-JP2GdJ4b.d.mts +206 -0
- package/dist/cli/commands/describe.mjs +24 -7
- package/dist/cli/commands/docs.mjs +6 -7
- package/dist/cli/commands/doctor.d.mts +10 -0
- package/dist/cli/commands/doctor.mjs +156 -0
- package/dist/cli/commands/generate.mjs +66 -17
- package/dist/cli/commands/init.mjs +315 -45
- package/dist/cli/commands/introspect.mjs +2 -4
- package/dist/cli/index.d.mts +1 -10
- package/dist/cli/index.mjs +4 -153
- package/dist/{constants-DdXFXQtN.mjs → constants-Cxde4rpC.mjs} +1 -2
- package/dist/core/index.d.mts +3 -5
- package/dist/core/index.mjs +5 -4
- package/dist/core-C1XCMtqM.mjs +185 -0
- package/dist/{createApp-BKHSl2nT.mjs → createApp-ByWNRsZj.mjs} +65 -36
- package/dist/{defineResource-DO9ONe_D.mjs → defineResource-D9aY5Cy6.mjs} +154 -1165
- package/dist/discovery/index.mjs +37 -5
- package/dist/docs/index.d.mts +6 -9
- package/dist/docs/index.mjs +3 -21
- package/dist/dynamic/index.d.mts +93 -0
- package/dist/dynamic/index.mjs +122 -0
- package/dist/{elevation-DSTbVvYj.mjs → elevation-BEdACOLB.mjs} +5 -36
- package/dist/{elevation-DGo5shaX.d.mts → elevation-Ca_yveIO.d.mts} +41 -7
- package/dist/{errorHandler-C3GY3_ow.mjs → errorHandler--zp54tGc.mjs} +3 -5
- package/dist/errorHandler-Do4vVQ1f.d.mts +139 -0
- package/dist/{errors-DBANPbGr.mjs → errors-rxhfP7Hf.mjs} +1 -2
- package/dist/{eventPlugin-BEOvaDqo.mjs → eventPlugin-Ba00swHF.mjs} +25 -27
- package/dist/{eventPlugin-H6wDDjGO.d.mts → eventPlugin-iGrSEmwJ.d.mts} +105 -5
- package/dist/events/index.d.mts +72 -7
- package/dist/events/index.mjs +216 -4
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis-stream-entry.mjs +19 -7
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/events/transports/redis.mjs +3 -4
- package/dist/factory/index.d.mts +23 -9
- package/dist/factory/index.mjs +48 -3
- package/dist/{fields-Bi_AVKSo.d.mts → fields-DFwdaWCq.d.mts} +1 -1
- package/dist/{fields-CTd_CrKr.mjs → fields-ipsbIRPK.mjs} +1 -2
- package/dist/hooks/index.d.mts +1 -3
- package/dist/hooks/index.mjs +2 -3
- package/dist/idempotency/index.d.mts +5 -5
- package/dist/idempotency/index.mjs +3 -7
- package/dist/idempotency/mongodb.d.mts +1 -1
- package/dist/idempotency/mongodb.mjs +4 -5
- package/dist/idempotency/redis.d.mts +1 -1
- package/dist/idempotency/redis.mjs +2 -5
- package/dist/{fastifyAdapter-CyAA2zlB.d.mts → index-BL8CaQih.d.mts} +56 -57
- package/dist/index-Diqcm14c.d.mts +369 -0
- package/dist/{prisma-xjhMEq_S.d.mts → index-yhxyjqNb.d.mts} +4 -5
- package/dist/index.d.mts +100 -105
- package/dist/index.mjs +85 -58
- package/dist/integrations/event-gateway.d.mts +1 -1
- package/dist/integrations/event-gateway.mjs +8 -4
- package/dist/integrations/index.d.mts +4 -2
- package/dist/integrations/index.mjs +1 -1
- package/dist/integrations/jobs.d.mts +2 -2
- package/dist/integrations/jobs.mjs +63 -14
- package/dist/integrations/mcp/index.d.mts +219 -0
- package/dist/integrations/mcp/index.mjs +572 -0
- package/dist/integrations/mcp/testing.d.mts +53 -0
- package/dist/integrations/mcp/testing.mjs +104 -0
- package/dist/integrations/streamline.mjs +39 -19
- package/dist/integrations/webhooks.d.mts +56 -0
- package/dist/integrations/webhooks.mjs +139 -0
- package/dist/integrations/websocket-redis.d.mts +46 -0
- package/dist/integrations/websocket-redis.mjs +50 -0
- package/dist/integrations/websocket.d.mts +68 -2
- package/dist/integrations/websocket.mjs +96 -13
- package/dist/{interface-CSNjltAc.d.mts → interface-B4awm1RJ.d.mts} +2 -2
- package/dist/interface-DGmPxakH.d.mts +2213 -0
- package/dist/{keys-DhqDRxv3.mjs → keys-qcD-TVJl.mjs} +3 -4
- package/dist/{logger-ByrvQWZO.mjs → logger-Dz3j1ItV.mjs} +2 -4
- package/dist/{memory-B2v7KrCB.mjs → memory-Cb_7iy9e.mjs} +2 -4
- package/dist/metrics-Csh4nsvv.mjs +224 -0
- package/dist/migrations/index.mjs +3 -7
- package/dist/{mongodb-DNKEExbf.mjs → mongodb-BuQ7fNTg.mjs} +1 -4
- package/dist/{mongodb-ClykrfGo.d.mts → mongodb-CUpYfxfD.d.mts} +2 -3
- package/dist/{mongodb-Dg8O_gvd.d.mts → mongodb-bga9AbkD.d.mts} +2 -2
- package/dist/{openapi-9nB_kiuR.mjs → openapi-CBmZ6EQN.mjs} +4 -21
- package/dist/org/index.d.mts +12 -14
- package/dist/org/index.mjs +92 -119
- package/dist/org/types.d.mts +2 -2
- package/dist/org/types.mjs +1 -1
- package/dist/permissions/index.d.mts +4 -278
- package/dist/permissions/index.mjs +4 -579
- package/dist/permissions-CA5zg0yK.mjs +751 -0
- package/dist/plugins/index.d.mts +104 -107
- package/dist/plugins/index.mjs +203 -313
- package/dist/plugins/response-cache.mjs +4 -69
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +24 -11
- package/dist/{pluralize-CM-jZg7p.mjs → pluralize-CcT6qF0a.mjs} +12 -13
- package/dist/policies/index.d.mts +2 -2
- package/dist/policies/index.mjs +80 -83
- package/dist/presets/index.d.mts +26 -19
- package/dist/presets/index.mjs +2 -142
- package/dist/presets/multiTenant.d.mts +1 -4
- package/dist/presets/multiTenant.mjs +4 -6
- package/dist/presets-C9QXJV1u.mjs +422 -0
- package/dist/{queryCachePlugin-B6R0d4av.mjs → queryCachePlugin-ClosZdNS.mjs} +6 -27
- package/dist/{queryCachePlugin-Q6SYuHZ6.d.mts → queryCachePlugin-DcmETvcB.d.mts} +3 -3
- package/dist/queryParser-CgCtsjti.mjs +352 -0
- package/dist/{redis-UwjEp8Ea.d.mts → redis-CQ5YxMC5.d.mts} +2 -2
- package/dist/{redis-stream-CBg0upHI.d.mts → redis-stream-BW9UKLZM.d.mts} +9 -2
- package/dist/registry/index.d.mts +1 -4
- package/dist/registry/index.mjs +3 -4
- package/dist/{introspectionPlugin-B3JkrjwU.mjs → registry-I-ogLgL9.mjs} +1 -8
- package/dist/{requestContext-xi6OKBL-.mjs → requestContext-DYtmNpm5.mjs} +1 -3
- package/dist/resourceToTools-B6ZN9Ing.mjs +489 -0
- package/dist/rpc/index.d.mts +90 -0
- package/dist/rpc/index.mjs +248 -0
- package/dist/{schemaConverter-Dtg0Kt9T.mjs → schemaConverter-DjzHpFam.mjs} +1 -2
- package/dist/schemas/index.d.mts +30 -30
- package/dist/schemas/index.mjs +4 -6
- package/dist/scope/index.d.mts +13 -2
- package/dist/scope/index.mjs +18 -5
- package/dist/{sessionManager-D_iEHjQl.d.mts → sessionManager-wbkYj2HL.d.mts} +2 -2
- package/dist/{sse-DkqQ1uxb.mjs → sse-BkViJPlT.mjs} +4 -25
- package/dist/testing/index.d.mts +551 -567
- package/dist/testing/index.mjs +1744 -1799
- package/dist/{tracing-8CEbhF0w.d.mts → tracing-bz_U4EM1.d.mts} +6 -1
- package/dist/{typeGuards-DwxA1t_L.mjs → typeGuards-Cj5Rgvlg.mjs} +1 -2
- package/dist/types/index.d.mts +4 -946
- package/dist/types/index.mjs +2 -4
- package/dist/types-BJmgxNbF.d.mts +275 -0
- package/dist/{types-RLkFVgaw.d.mts → types-BNUccdcf.d.mts} +2 -2
- package/dist/{types-Beqn1Un7.mjs → types-C6TQjtdi.mjs} +30 -2
- package/dist/{types-DMSBMkaZ.d.mts → types-Dt0-AI6E.d.mts} +85 -27
- package/dist/{types-DelU6kln.mjs → types-ZUu_h0jp.mjs} +1 -2
- package/dist/utils/index.d.mts +255 -352
- package/dist/utils/index.mjs +7 -6
- package/dist/utils-Dc0WhlIl.mjs +594 -0
- package/dist/versioning-BzfeHmhj.mjs +37 -0
- package/package.json +46 -12
- package/skills/arc/SKILL.md +506 -0
- package/skills/arc/references/auth.md +250 -0
- package/skills/arc/references/events.md +272 -0
- package/skills/arc/references/integrations.md +385 -0
- package/skills/arc/references/mcp.md +386 -0
- package/skills/arc/references/production.md +610 -0
- package/skills/arc/references/testing.md +183 -0
- package/dist/audited-CGdLiSlE.mjs +0 -140
- package/dist/chunk-C7Uep-_p.mjs +0 -20
- package/dist/circuitBreaker-DYhWBW_D.mjs +0 -1096
- package/dist/errorHandler-CW3OOeYq.d.mts +0 -72
- package/dist/interface-DZYNK9bb.d.mts +0 -1112
- package/dist/presets-BTeYbw7h.d.mts +0 -57
- package/dist/presets-CeFtfDR8.mjs +0 -119
- /package/dist/{errors-DAWRdiYP.d.mts → errors-CPpvPHT0.d.mts} +0 -0
- /package/dist/{externalPaths-SyPF2tgK.d.mts → externalPaths-DpO-s7r8.d.mts} +0 -0
- /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.
|