@classytic/arc 2.8.3 → 2.8.4

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Database-agnostic resource framework for Fastify. Define resources, get CRUD routes, permissions, presets, caching, events, OpenAPI, and MCP tools — without boilerplate.
4
4
 
5
- **v2.8.3** | Fastify 5+ | Node.js 22+ | ESM only | 278+ test files, 3844+ tests
5
+ **v2.8.4** | Fastify 5+ | Node.js 22+ | ESM only | 279+ test files, 3867+ tests
6
6
 
7
7
  ## Install
8
8
 
@@ -694,6 +694,27 @@ npx @classytic/arc doctor # Health check
694
694
  | `@classytic/arc/docs` | OpenAPI generation |
695
695
  | `@classytic/arc/cli` | CLI commands (programmatic) |
696
696
 
697
+ ## v2.8.4 Highlights
698
+
699
+ - **MCP ↔ AI SDK bridge** — expose AI SDK `tool()` definitions over MCP without duplicating code. `bridgeToMcp(bridge)` adapts any AI SDK tool into an MCP tool with automatic auth, guard delegation, and `{ error } → isError` envelope translation. `buildMcpToolsFromBridges(bridges, { include, exclude })` registers a whole catalog at once with per-environment filtering.
700
+
701
+ ```typescript
702
+ import { bridgeToMcp, buildMcpToolsFromBridges, type McpBridge } from '@classytic/arc/mcp';
703
+
704
+ export const triggerJobBridge: McpBridge = {
705
+ name: 'trigger_job',
706
+ description: 'Start a job.',
707
+ inputSchema: { phase: z.enum(['investigate', 'fix']) },
708
+ annotations: { destructiveHint: true },
709
+ buildTool: (ctx) => buildTriggerJobTool(getUserId(ctx) ?? ''),
710
+ };
711
+
712
+ await app.register(mcpPlugin, {
713
+ resources,
714
+ extraTools: buildMcpToolsFromBridges([triggerJobBridge]),
715
+ });
716
+ ```
717
+
697
718
  ## v2.8.1 Highlights
698
719
 
699
720
  - **Per-action discriminated validation** — `actions` schemas now enforce required fields via a `oneOf` body schema; missing inputs are rejected at the HTTP layer by AJV (no more silent bypass)
package/dist/index.mjs CHANGED
@@ -128,6 +128,6 @@ function transform(name, handlerOrOptions) {
128
128
  }
129
129
  //#endregion
130
130
  //#region src/index.ts
131
- const version = "2.8.3";
131
+ const version = "2.8.4";
132
132
  //#endregion
133
133
  export { ArcError, BaseController, CRUD_OPERATIONS, DEFAULT_ID_FIELD, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, DEFAULT_SORT, DEFAULT_TENANT_FIELD, DEFAULT_UPDATE_METHOD, ForbiddenError, HOOK_OPERATIONS, HOOK_PHASES, MAX_FILTER_DEPTH, MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MUTATION_OPERATIONS, MongooseAdapter, NotFoundError, PrismaAdapter, RESERVED_QUERY_PARAMS, ResourceDefinition, SYSTEM_FIELDS, UnauthorizedError, ValidationError, adminOnly, allOf, allowPublic, anyOf, applyFieldReadPermissions, applyFieldWritePermissions, arcLog, assertValidConfig, authenticated, configureArcLogger, createDomainError, createDynamicPermissionMatrix, createMongooseAdapter, createOrgPermissions, createPrismaAdapter, defineResource, defineResourceVariants, denyAll, envelope, fields, formatValidationErrors, fullPublic, getControllerScope, guard, intercept, middleware, ownerWithAdminBypass, presets_exports as permissions, pipe, publicRead, publicReadAdminWrite, readOnly, requestContext, requireAuth, requireOrgInScope, requireOrgMembership, requireOrgRole, requireOwnership, requireRoles, requireScopeContext, requireServiceScope, requireTeamMembership, sortMiddlewares, transform, validateResourceConfig, version, when };
@@ -3,6 +3,54 @@ import { a as McpAuthResolver, c as McpResourceConfig, d as SessionEntry, f as T
3
3
  import { FastifyPluginAsync } from "fastify";
4
4
  import { z } from "zod";
5
5
 
6
+ //#region src/integrations/mcp/aiSdkBridge.d.ts
7
+ /** Minimal AI SDK tool shape we need to invoke. */
8
+ interface AiSdkExecutable {
9
+ execute: (input: unknown, options?: unknown) => Promise<unknown>;
10
+ }
11
+ interface McpBridge {
12
+ /** MCP tool name. */
13
+ name: string;
14
+ /** LLM-facing description. */
15
+ description: string;
16
+ /** Zod input schema — matches the AI SDK tool's inputSchema. */
17
+ inputSchema: Record<string, z.ZodType>;
18
+ /** MCP annotations — same shape as `defineTool`. */
19
+ annotations?: ToolAnnotations;
20
+ /**
21
+ * Build the AI SDK tool from MCP session context. Called per-request.
22
+ * The caller injects deps (companyId, projectId, etc.) from `ctx`.
23
+ */
24
+ buildTool: (ctx: ToolContext) => AiSdkExecutable;
25
+ /**
26
+ * Optional pre-execution guard. Return an error message to reject, or
27
+ * `null` to proceed. Runs after `isAuthenticated`.
28
+ */
29
+ guard?: (ctx: ToolContext) => string | null;
30
+ }
31
+ /** Convert a McpBridge into a registered MCP tool. */
32
+ declare function bridgeToMcp(bridge: McpBridge): ToolDefinition;
33
+ interface BuildMcpToolsFromBridgesOptions {
34
+ /** If set, only bridges whose `name` is in this array are registered. */
35
+ include?: string[];
36
+ /** If set, bridges whose `name` is in this array are skipped. */
37
+ exclude?: string[];
38
+ }
39
+ /**
40
+ * Take a list of McpBridge objects and produce a ready-to-register MCP tool
41
+ * array, with optional include/exclude filtering for per-environment config.
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * // All bridges
46
+ * extraTools: [...buildMcpToolsFromBridges(allBridges)]
47
+ *
48
+ * // Read-only deployment — hide destructive tools
49
+ * extraTools: [...buildMcpToolsFromBridges(allBridges, { exclude: ['trigger_job'] })]
50
+ * ```
51
+ */
52
+ declare function buildMcpToolsFromBridges(bridges: readonly McpBridge[], options?: BuildMcpToolsFromBridgesOptions): ToolDefinition[];
53
+ //#endregion
6
54
  //#region src/integrations/mcp/createMcpServer.d.ts
7
55
  /**
8
56
  * Mutable auth ref — updated per-request by mcpPlugin.
@@ -218,4 +266,4 @@ interface ResourceToToolsConfig extends McpResourceConfig {
218
266
  */
219
267
  declare function resourceToTools(resource: ResourceDefinition, config?: ResourceToToolsConfig): ToolDefinition[];
220
268
  //#endregion
221
- export { type AuthRef, type BetterAuthHandler, type CallToolResult, type CreateMcpServerConfig, type CrudOperation, type DefinePromptConfig, type DefineToolConfig, type FieldRuleEntry, type FieldRulesToZodOptions, type McpAuthResolver, type McpAuthResult, type McpGuard, type McpPluginOptions, type McpResourceConfig, type McpServerInstance, type PromptDefinition, type PromptResult, type ResourceToToolsConfig, type ToolAnnotations, type ToolContext, type ToolDefinition, createMcpServer, customGuard, definePrompt, defineTool, denied, fieldRulesToZod, getOrgId, getUserId, guard, hasOrg, isAuthenticated, isOrg, mcpPlugin, requireAuth, requireOrg, requireOrgId, requireRole, resourceToTools };
269
+ export { type AuthRef, type BetterAuthHandler, type BuildMcpToolsFromBridgesOptions, type CallToolResult, type CreateMcpServerConfig, type CrudOperation, type DefinePromptConfig, type DefineToolConfig, type FieldRuleEntry, type FieldRulesToZodOptions, type McpAuthResolver, type McpAuthResult, type McpBridge, type McpGuard, type McpPluginOptions, type McpResourceConfig, type McpServerInstance, type PromptDefinition, type PromptResult, type ResourceToToolsConfig, type ToolAnnotations, type ToolContext, type ToolDefinition, bridgeToMcp, buildMcpToolsFromBridges, createMcpServer, customGuard, definePrompt, defineTool, denied, fieldRulesToZod, getOrgId, getUserId, guard, hasOrg, isAuthenticated, isOrg, mcpPlugin, requireAuth, requireOrg, requireOrgId, requireRole, resourceToTools };
@@ -1,23 +1,6 @@
1
1
  import { n as fieldRulesToZod, r as createMcpServer, t as resourceToTools } from "../../resourceToTools-O_HwWXFa.mjs";
2
2
  import { createHash } from "node:crypto";
3
3
  import fp from "fastify-plugin";
4
- //#region src/integrations/mcp/definePrompt.ts
5
- /**
6
- * Define a type-safe MCP prompt.
7
- *
8
- * @param name - Prompt name (snake_case recommended)
9
- * @param config - Description, args schema, handler
10
- */
11
- function definePrompt(name, config) {
12
- return {
13
- name,
14
- description: config.description,
15
- title: config.title,
16
- argsSchema: config.args,
17
- handler: config.handler
18
- };
19
- }
20
- //#endregion
21
4
  //#region src/integrations/mcp/defineTool.ts
22
5
  /**
23
6
  * Define a type-safe MCP tool.
@@ -127,6 +110,82 @@ function customGuard(check, errorMessage) {
127
110
  };
128
111
  }
129
112
  //#endregion
113
+ //#region src/integrations/mcp/aiSdkBridge.ts
114
+ /** Serialize an AI SDK tool result into MCP's text-content envelope. */
115
+ function toMcpEnvelope(result) {
116
+ if (result && typeof result === "object" && "error" in result) {
117
+ const msg = result.error;
118
+ return {
119
+ content: [{
120
+ type: "text",
121
+ text: typeof msg === "string" ? msg : JSON.stringify(msg)
122
+ }],
123
+ isError: true
124
+ };
125
+ }
126
+ return { content: [{
127
+ type: "text",
128
+ text: typeof result === "string" ? result : JSON.stringify(result, null, 2)
129
+ }] };
130
+ }
131
+ /** Convert a McpBridge into a registered MCP tool. */
132
+ function bridgeToMcp(bridge) {
133
+ return defineTool(bridge.name, {
134
+ description: bridge.description,
135
+ input: bridge.inputSchema,
136
+ annotations: bridge.annotations,
137
+ handler: async (input, ctx) => {
138
+ if (!isAuthenticated(ctx)) return denied("Authentication required");
139
+ if (bridge.guard) {
140
+ const reason = bridge.guard(ctx);
141
+ if (reason) return denied(reason);
142
+ }
143
+ try {
144
+ return toMcpEnvelope(await bridge.buildTool(ctx).execute(input));
145
+ } catch (err) {
146
+ return denied(err instanceof Error ? err.message : String(err));
147
+ }
148
+ }
149
+ });
150
+ }
151
+ /**
152
+ * Take a list of McpBridge objects and produce a ready-to-register MCP tool
153
+ * array, with optional include/exclude filtering for per-environment config.
154
+ *
155
+ * @example
156
+ * ```typescript
157
+ * // All bridges
158
+ * extraTools: [...buildMcpToolsFromBridges(allBridges)]
159
+ *
160
+ * // Read-only deployment — hide destructive tools
161
+ * extraTools: [...buildMcpToolsFromBridges(allBridges, { exclude: ['trigger_job'] })]
162
+ * ```
163
+ */
164
+ function buildMcpToolsFromBridges(bridges, options = {}) {
165
+ return bridges.filter((bridge) => {
166
+ if (options.include) return options.include.includes(bridge.name);
167
+ if (options.exclude) return !options.exclude.includes(bridge.name);
168
+ return true;
169
+ }).map(bridgeToMcp);
170
+ }
171
+ //#endregion
172
+ //#region src/integrations/mcp/definePrompt.ts
173
+ /**
174
+ * Define a type-safe MCP prompt.
175
+ *
176
+ * @param name - Prompt name (snake_case recommended)
177
+ * @param config - Description, args schema, handler
178
+ */
179
+ function definePrompt(name, config) {
180
+ return {
181
+ name,
182
+ description: config.description,
183
+ title: config.title,
184
+ argsSchema: config.args,
185
+ handler: config.handler
186
+ };
187
+ }
188
+ //#endregion
130
189
  //#region src/integrations/mcp/authBridge.ts
131
190
  /**
132
191
  * @classytic/arc — MCP Auth Bridge
@@ -572,4 +631,4 @@ const mcpPlugin = fp(mcpPluginImpl, {
572
631
  fastify: "5.x"
573
632
  });
574
633
  //#endregion
575
- export { createMcpServer, customGuard, definePrompt, defineTool, denied, fieldRulesToZod, getOrgId, getUserId, guard, hasOrg, isAuthenticated, isOrg, mcpPlugin, requireAuth, requireOrg, requireOrgId, requireRole, resourceToTools };
634
+ export { bridgeToMcp, buildMcpToolsFromBridges, createMcpServer, customGuard, definePrompt, defineTool, denied, fieldRulesToZod, getOrgId, getUserId, guard, hasOrg, isAuthenticated, isOrg, mcpPlugin, requireAuth, requireOrg, requireOrgId, requireRole, resourceToTools };
@@ -44,7 +44,7 @@ try {
44
44
  function createTracerProvider(options) {
45
45
  if (!isAvailable) return null;
46
46
  const { serviceName = "@classytic/arc", serviceVersion, exporterUrl = "http://localhost:4318/v1/traces" } = options;
47
- const resolvedVersion = serviceVersion ?? "2.8.3";
47
+ const resolvedVersion = serviceVersion ?? "2.8.4";
48
48
  const exporter = new OTLPTraceExporter({ url: exporterUrl });
49
49
  const provider = new NodeTracerProvider({ resource: { attributes: {
50
50
  "service.name": serviceName,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classytic/arc",
3
- "version": "2.8.3",
3
+ "version": "2.8.4",
4
4
  "description": "Resource-oriented backend framework for Fastify — clean, minimal, powerful, tree-shakable",
5
5
  "type": "module",
6
6
  "exports": {
@@ -856,6 +856,28 @@ auth: async (headers) => ({
856
856
 
857
857
  **Guards** for custom tools: `guard(requireAuth, requireOrg, requireRole('admin'), handler)`
858
858
 
859
+ **AI SDK bridge** (v2.8.4+) — expose AI SDK `tool()` definitions over MCP without duplicating glue. Handles auth, guards, `{ error } → isError` translation, and thrown-error mapping:
860
+
861
+ ```typescript
862
+ import { bridgeToMcp, buildMcpToolsFromBridges, getUserId, hasOrg, type McpBridge } from '@classytic/arc/mcp';
863
+
864
+ export const triggerJobBridge: McpBridge = {
865
+ name: 'trigger_job',
866
+ description: 'Start a job.',
867
+ inputSchema: { phase: z.enum(['investigate', 'fix']) },
868
+ annotations: { destructiveHint: true },
869
+ buildTool: (ctx) => buildTriggerJobTool(getUserId(ctx) ?? ''),
870
+ guard: (ctx) => (hasOrg(ctx) ? null : 'Organization scope required'),
871
+ };
872
+
873
+ await app.register(mcpPlugin, {
874
+ resources,
875
+ extraTools: buildMcpToolsFromBridges([triggerJobBridge], {
876
+ exclude: process.env.DEPLOYMENT === 'readonly' ? ['trigger_job'] : [],
877
+ }),
878
+ });
879
+ ```
880
+
859
881
  **Service scope**: When `clientId` is set in auth result, MCP produces `kind: "service"` RequestScope — works with `requireServiceScope()`, `getClientId()`, `getServiceScopes()`. No synthetic userId needed for machine principals.
860
882
 
861
883
  **Multi-tenancy**: `organizationId` from auth flows into BaseController org-scoping automatically.
@@ -430,6 +430,43 @@ const createShape = fieldRulesToZod(resource.schemaOptions.fieldRules, {
430
430
  // → { name: z.string(), price: z.number(), category: z.enum([...]) }
431
431
  ```
432
432
 
433
+ ## AI SDK Bridge (v2.8.4+)
434
+
435
+ Expose AI SDK `tool()` definitions over MCP without duplicating glue. The bridge handles `isAuthenticated` rejection, optional custom guards, `{ error }` → `isError: true` envelope translation, and thrown-error mapping.
436
+
437
+ ```typescript
438
+ import { bridgeToMcp, buildMcpToolsFromBridges, getUserId, hasOrg, type McpBridge } from '@classytic/arc/mcp';
439
+ import { tool } from 'ai';
440
+ import { z } from 'zod';
441
+
442
+ function buildTriggerJobTool(companyId: string) {
443
+ return tool({
444
+ description: 'Start a job.',
445
+ inputSchema: z.object({ phase: z.enum(['investigate', 'fix']) }),
446
+ execute: async ({ phase }) => ({ jobId: `${companyId}-${phase}-${Date.now()}` }),
447
+ });
448
+ }
449
+
450
+ export const triggerJobBridge: McpBridge = {
451
+ name: 'trigger_job',
452
+ description: 'Start a job.',
453
+ inputSchema: { phase: z.enum(['investigate', 'fix']) },
454
+ annotations: { destructiveHint: true },
455
+ buildTool: (ctx) => buildTriggerJobTool(getUserId(ctx) ?? ''),
456
+ guard: (ctx) => (hasOrg(ctx) ? null : 'Organization scope required'),
457
+ };
458
+
459
+ await app.register(mcpPlugin, {
460
+ resources,
461
+ extraTools: buildMcpToolsFromBridges([triggerJobBridge], {
462
+ // Per-environment filtering — read-only deployments hide destructive tools
463
+ exclude: process.env.DEPLOYMENT === 'readonly' ? ['trigger_job'] : [],
464
+ }),
465
+ });
466
+ ```
467
+
468
+ `buildMcpToolsFromBridges` also accepts `{ include: [...] }` for strict allowlists. `buildTool` is called fresh per request — safe for per-tenant dep resolution. `McpBridge.annotations` is the same `ToolAnnotations` shape as `defineTool`.
469
+
433
470
  ## Schema Discovery — MCP Resources
434
471
 
435
472
  Auto-registered for agent discovery: