@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 +22 -1
- package/dist/index.mjs +1 -1
- package/dist/integrations/mcp/index.d.mts +49 -1
- package/dist/integrations/mcp/index.mjs +77 -18
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/package.json +1 -1
- package/skills/arc/SKILL.md +22 -0
- package/skills/arc/references/mcp.md +37 -0
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.
|
|
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.
|
|
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.
|
|
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
package/skills/arc/SKILL.md
CHANGED
|
@@ -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:
|