@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,489 @@
|
|
|
1
|
+
import { t as BaseController } from "./BaseController-CkM5dUh_.mjs";
|
|
2
|
+
import { t as pluralize } from "./pluralize-CcT6qF0a.mjs";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
//#region src/integrations/mcp/createMcpServer.ts
|
|
5
|
+
/**
|
|
6
|
+
* Create a configured MCP server from declarative config.
|
|
7
|
+
*
|
|
8
|
+
* @param config - Server name, version, tools, prompts
|
|
9
|
+
* @returns McpServer instance (not yet connected to a transport)
|
|
10
|
+
*/
|
|
11
|
+
async function createMcpServer(config, authRef) {
|
|
12
|
+
const { McpServer } = await import("@modelcontextprotocol/sdk/server/mcp.js");
|
|
13
|
+
const server = new McpServer({
|
|
14
|
+
name: config.name,
|
|
15
|
+
version: config.version ?? "1.0.0"
|
|
16
|
+
}, config.instructions ? { instructions: config.instructions } : void 0);
|
|
17
|
+
if (config.tools) for (const tool of config.tools) registerTool(server, tool, authRef);
|
|
18
|
+
if (config.prompts) for (const prompt of config.prompts) registerPrompt(server, prompt);
|
|
19
|
+
return server;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Register a ToolDefinition using `server.registerTool()`.
|
|
23
|
+
*
|
|
24
|
+
* The inputSchema is passed as a flat Zod shape `{ field: z.string() }` —
|
|
25
|
+
* the SDK wraps it in z.object() internally. This avoids the Zod v3/v4-mini
|
|
26
|
+
* version mismatch entirely.
|
|
27
|
+
*/
|
|
28
|
+
function registerTool(server, tool, authRef) {
|
|
29
|
+
const srv = server;
|
|
30
|
+
const config = {};
|
|
31
|
+
if (tool.title) config.title = tool.title;
|
|
32
|
+
if (tool.description) config.description = tool.description;
|
|
33
|
+
if (tool.inputSchema) config.inputSchema = tool.inputSchema;
|
|
34
|
+
if (tool.outputSchema) config.outputSchema = tool.outputSchema;
|
|
35
|
+
if (tool.annotations) config.annotations = tool.annotations;
|
|
36
|
+
srv.registerTool(tool.name, config, (input, extra) => {
|
|
37
|
+
const ctx = {
|
|
38
|
+
session: authRef?.current ?? null,
|
|
39
|
+
log: async (level, message) => {
|
|
40
|
+
try {
|
|
41
|
+
const notify = extra?.sendNotification;
|
|
42
|
+
if (notify) await notify({
|
|
43
|
+
method: "notifications/message",
|
|
44
|
+
params: {
|
|
45
|
+
level,
|
|
46
|
+
data: message
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
} catch {}
|
|
50
|
+
},
|
|
51
|
+
extra
|
|
52
|
+
};
|
|
53
|
+
return tool.handler(input, ctx);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/** Register a PromptDefinition using `server.registerPrompt()` */
|
|
57
|
+
function registerPrompt(server, prompt) {
|
|
58
|
+
const srv = server;
|
|
59
|
+
const config = {};
|
|
60
|
+
if (prompt.title) config.title = prompt.title;
|
|
61
|
+
if (prompt.description) config.description = prompt.description;
|
|
62
|
+
if (prompt.argsSchema) config.argsSchema = prompt.argsSchema;
|
|
63
|
+
srv.registerPrompt(prompt.name, config, (args) => prompt.handler(args));
|
|
64
|
+
}
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region src/integrations/mcp/fieldRulesToZod.ts
|
|
67
|
+
/**
|
|
68
|
+
* @classytic/arc — fieldRules → Zod Shape Converter
|
|
69
|
+
*
|
|
70
|
+
* Converts Arc's schemaOptions.fieldRules into flat Zod shapes
|
|
71
|
+
* compatible with the MCP SDK's registerTool() inputSchema format.
|
|
72
|
+
*
|
|
73
|
+
* Returns `Record<string, z.ZodTypeAny>` (flat shape), NOT z.object().
|
|
74
|
+
* The SDK wraps it internally.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```typescript
|
|
78
|
+
* import { fieldRulesToZod } from '@classytic/arc/mcp';
|
|
79
|
+
*
|
|
80
|
+
* const shape = fieldRulesToZod(resource.schemaOptions.fieldRules, {
|
|
81
|
+
* mode: 'create',
|
|
82
|
+
* hiddenFields: resource.schemaOptions.hiddenFields,
|
|
83
|
+
* });
|
|
84
|
+
* // shape = { name: z.string(), price: z.number() }
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
const PAGINATION_SHAPE = {
|
|
88
|
+
page: z.number().int().min(1).optional().describe("Page number (1-based)"),
|
|
89
|
+
limit: z.number().int().min(1).max(100).optional().describe("Items per page (max 100)"),
|
|
90
|
+
sort: z.string().optional().describe("Sort field, prefix with - for descending"),
|
|
91
|
+
search: z.string().optional().describe("Full-text search query")
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Convert Arc fieldRules to a flat Zod shape.
|
|
95
|
+
*
|
|
96
|
+
* @returns Flat shape `Record<string, z.ZodTypeAny>` — pass directly to defineTool() or registerTool()
|
|
97
|
+
*/
|
|
98
|
+
function fieldRulesToZod(fieldRules, options = {}) {
|
|
99
|
+
const { mode = "create", hiddenFields = [], readonlyFields = [], extraHideFields = [] } = options;
|
|
100
|
+
if (mode === "list") return buildListShape(fieldRules, options);
|
|
101
|
+
if (!fieldRules) return {};
|
|
102
|
+
const allHidden = new Set([...hiddenFields, ...extraHideFields]);
|
|
103
|
+
const allReadonly = new Set(readonlyFields);
|
|
104
|
+
const shape = {};
|
|
105
|
+
for (const [name, rule] of Object.entries(fieldRules)) {
|
|
106
|
+
if (rule.systemManaged || rule.hidden || allHidden.has(name)) continue;
|
|
107
|
+
if (allReadonly.has(name)) continue;
|
|
108
|
+
if (mode === "update" && rule.immutable) continue;
|
|
109
|
+
const field = buildFieldSchema(rule);
|
|
110
|
+
if (mode === "update") shape[name] = field.optional();
|
|
111
|
+
else shape[name] = rule.required === true && !rule.optional ? field : field.optional();
|
|
112
|
+
}
|
|
113
|
+
return shape;
|
|
114
|
+
}
|
|
115
|
+
/** Build Zod type for a single field rule */
|
|
116
|
+
function buildFieldSchema(rule) {
|
|
117
|
+
if (rule.enum?.length) {
|
|
118
|
+
const schema = z.enum(rule.enum);
|
|
119
|
+
return rule.description ? schema.describe(rule.description) : schema;
|
|
120
|
+
}
|
|
121
|
+
const base = typeToZod(rule.type);
|
|
122
|
+
if (base instanceof z.ZodString) {
|
|
123
|
+
let s = base;
|
|
124
|
+
if (rule.minLength != null) s = s.min(rule.minLength);
|
|
125
|
+
if (rule.maxLength != null) s = s.max(rule.maxLength);
|
|
126
|
+
if (rule.pattern) try {
|
|
127
|
+
s = s.regex(new RegExp(rule.pattern));
|
|
128
|
+
} catch {}
|
|
129
|
+
return rule.description ? s.describe(rule.description) : s;
|
|
130
|
+
}
|
|
131
|
+
if (base instanceof z.ZodNumber) {
|
|
132
|
+
let n = base;
|
|
133
|
+
if (rule.min != null) n = n.min(rule.min);
|
|
134
|
+
if (rule.max != null) n = n.max(rule.max);
|
|
135
|
+
return rule.description ? n.describe(rule.description) : n;
|
|
136
|
+
}
|
|
137
|
+
return rule.description ? base.describe(rule.description) : base;
|
|
138
|
+
}
|
|
139
|
+
/** Map Arc field type string to base Zod type */
|
|
140
|
+
function typeToZod(type) {
|
|
141
|
+
switch (type) {
|
|
142
|
+
case "string": return z.string();
|
|
143
|
+
case "number": return z.number();
|
|
144
|
+
case "boolean": return z.boolean();
|
|
145
|
+
case "date": return z.string().describe("ISO 8601 date string");
|
|
146
|
+
case "array": return z.array(z.any());
|
|
147
|
+
case "object": return z.record(z.string(), z.any());
|
|
148
|
+
default: return z.string();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/** Build list/query shape with filterable fields + pagination */
|
|
152
|
+
function buildListShape(fieldRules, options) {
|
|
153
|
+
const { filterableFields = [], hiddenFields = [], extraHideFields = [] } = options;
|
|
154
|
+
const allHidden = new Set([...hiddenFields, ...extraHideFields]);
|
|
155
|
+
const shape = { ...PAGINATION_SHAPE };
|
|
156
|
+
if (fieldRules) for (const name of filterableFields) {
|
|
157
|
+
if (allHidden.has(name)) continue;
|
|
158
|
+
const rule = fieldRules[name];
|
|
159
|
+
if (!rule) continue;
|
|
160
|
+
shape[name] = buildFieldSchema(rule).optional();
|
|
161
|
+
}
|
|
162
|
+
return shape;
|
|
163
|
+
}
|
|
164
|
+
//#endregion
|
|
165
|
+
//#region src/integrations/mcp/buildRequestContext.ts
|
|
166
|
+
/**
|
|
167
|
+
* Build an IRequestContext from MCP tool input and session auth.
|
|
168
|
+
*
|
|
169
|
+
* | Operation | params | query | body |
|
|
170
|
+
* |-----------|------------|----------------------|---------------------|
|
|
171
|
+
* | list | {} | all input fields | undefined |
|
|
172
|
+
* | get | { id } | {} | undefined |
|
|
173
|
+
* | create | {} | {} | all input fields |
|
|
174
|
+
* | update | { id } | {} | input minus id |
|
|
175
|
+
* | delete | { id } | {} | undefined |
|
|
176
|
+
*/
|
|
177
|
+
function buildRequestContext(input, auth, operation) {
|
|
178
|
+
const scope = buildScope(auth);
|
|
179
|
+
const base = {
|
|
180
|
+
user: auth ? {
|
|
181
|
+
id: auth.userId,
|
|
182
|
+
_id: auth.userId
|
|
183
|
+
} : null,
|
|
184
|
+
headers: {},
|
|
185
|
+
context: {},
|
|
186
|
+
metadata: {
|
|
187
|
+
_scope: scope,
|
|
188
|
+
_policyFilters: {}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
switch (operation) {
|
|
192
|
+
case "list": return {
|
|
193
|
+
...base,
|
|
194
|
+
params: {},
|
|
195
|
+
query: { ...input },
|
|
196
|
+
body: void 0
|
|
197
|
+
};
|
|
198
|
+
case "get": return {
|
|
199
|
+
...base,
|
|
200
|
+
params: { id: String(input.id ?? "") },
|
|
201
|
+
query: {},
|
|
202
|
+
body: void 0
|
|
203
|
+
};
|
|
204
|
+
case "create": return {
|
|
205
|
+
...base,
|
|
206
|
+
params: {},
|
|
207
|
+
query: {},
|
|
208
|
+
body: { ...input }
|
|
209
|
+
};
|
|
210
|
+
case "update": {
|
|
211
|
+
const { id: _id, ...body } = input;
|
|
212
|
+
return {
|
|
213
|
+
...base,
|
|
214
|
+
params: { id: String(_id ?? "") },
|
|
215
|
+
query: {},
|
|
216
|
+
body
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
case "delete": return {
|
|
220
|
+
...base,
|
|
221
|
+
params: { id: String(input.id ?? "") },
|
|
222
|
+
query: {},
|
|
223
|
+
body: void 0
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function buildScope(auth) {
|
|
228
|
+
if (!auth) return { kind: "public" };
|
|
229
|
+
if (auth.organizationId) return {
|
|
230
|
+
kind: "member",
|
|
231
|
+
userId: auth.userId,
|
|
232
|
+
userRoles: [],
|
|
233
|
+
organizationId: auth.organizationId,
|
|
234
|
+
orgRoles: []
|
|
235
|
+
};
|
|
236
|
+
return {
|
|
237
|
+
kind: "authenticated",
|
|
238
|
+
userId: auth.userId,
|
|
239
|
+
userRoles: []
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
//#endregion
|
|
243
|
+
//#region src/integrations/mcp/resourceToTools.ts
|
|
244
|
+
/**
|
|
245
|
+
* @classytic/arc — Resource → MCP Tools Generator
|
|
246
|
+
*
|
|
247
|
+
* Converts a ResourceDefinition into an array of ToolDefinitions.
|
|
248
|
+
* Core auto-generation logic that powers Level 1 (mcpPlugin).
|
|
249
|
+
*
|
|
250
|
+
* All tool handlers call BaseController methods — same pipeline as REST.
|
|
251
|
+
*/
|
|
252
|
+
const ALL_CRUD_OPS = [
|
|
253
|
+
"list",
|
|
254
|
+
"get",
|
|
255
|
+
"create",
|
|
256
|
+
"update",
|
|
257
|
+
"delete"
|
|
258
|
+
];
|
|
259
|
+
const ANNOTATIONS = {
|
|
260
|
+
list: { readOnlyHint: true },
|
|
261
|
+
get: { readOnlyHint: true },
|
|
262
|
+
create: { destructiveHint: false },
|
|
263
|
+
update: {
|
|
264
|
+
destructiveHint: true,
|
|
265
|
+
idempotentHint: true
|
|
266
|
+
},
|
|
267
|
+
delete: {
|
|
268
|
+
destructiveHint: true,
|
|
269
|
+
idempotentHint: true
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
/**
|
|
273
|
+
* Convert a ResourceDefinition into MCP ToolDefinitions.
|
|
274
|
+
*
|
|
275
|
+
* MCP tools call BaseController directly — they bypass HTTP routes entirely.
|
|
276
|
+
* Therefore `disableDefaultRoutes` does NOT affect MCP tool generation;
|
|
277
|
+
* only `disabledRoutes` (the per-operation array) controls which ops are skipped.
|
|
278
|
+
*
|
|
279
|
+
* If the resource has an adapter but no controller (e.g. `disableDefaultRoutes: true`),
|
|
280
|
+
* a lightweight BaseController is auto-created from the adapter for MCP use.
|
|
281
|
+
*
|
|
282
|
+
* @param resource - Arc resource definition
|
|
283
|
+
* @param config - Optional overrides (operations, descriptions, hideFields, prefix, names)
|
|
284
|
+
*/
|
|
285
|
+
function resourceToTools(resource, config = {}) {
|
|
286
|
+
const controller = resource.controller ?? (resource.adapter ? createMcpController(resource) : void 0);
|
|
287
|
+
if (!controller) return [];
|
|
288
|
+
const fieldRules = resource.schemaOptions?.fieldRules;
|
|
289
|
+
const hiddenFields = resource.schemaOptions?.hiddenFields;
|
|
290
|
+
const readonlyFields = resource.schemaOptions?.readonlyFields;
|
|
291
|
+
const filterableFields = resource.schemaOptions?.filterableFields ?? resource.queryParser?.allowedFilterFields;
|
|
292
|
+
const sortableFields = resource.queryParser?.allowedSortFields;
|
|
293
|
+
const allowedOperators = resource.queryParser?.allowedOperators;
|
|
294
|
+
const hasSoftDelete = resource._appliedPresets?.includes("softDelete") ?? false;
|
|
295
|
+
let ops = ALL_CRUD_OPS.filter((op) => {
|
|
296
|
+
if (resource.disabledRoutes?.includes(op)) return false;
|
|
297
|
+
return true;
|
|
298
|
+
});
|
|
299
|
+
if (config.operations) ops = ops.filter((op) => config.operations?.includes(op));
|
|
300
|
+
const tools = [];
|
|
301
|
+
const prefix = config.toolNamePrefix;
|
|
302
|
+
for (const op of ops) {
|
|
303
|
+
const name = config.names?.[op] ?? (op === "list" ? `${prefix ? `${prefix}_` : ""}list_${pluralize(resource.name)}` : `${prefix ? `${prefix}_` : ""}${op}_${resource.name}`);
|
|
304
|
+
tools.push({
|
|
305
|
+
name,
|
|
306
|
+
description: config.descriptions?.[op] ?? defaultDescription(op, resource.displayName, hasSoftDelete, {
|
|
307
|
+
filterableFields,
|
|
308
|
+
allowedOperators,
|
|
309
|
+
sortableFields
|
|
310
|
+
}),
|
|
311
|
+
annotations: ANNOTATIONS[op],
|
|
312
|
+
inputSchema: buildInputSchema(op, fieldRules, {
|
|
313
|
+
hiddenFields,
|
|
314
|
+
readonlyFields,
|
|
315
|
+
extraHideFields: config.hideFields,
|
|
316
|
+
filterableFields
|
|
317
|
+
}),
|
|
318
|
+
handler: createHandler(op, controller, resource.name)
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
for (const route of resource.additionalRoutes ?? []) {
|
|
322
|
+
const mcpHandler = route.mcpHandler;
|
|
323
|
+
if (!route.wrapHandler && !mcpHandler) continue;
|
|
324
|
+
if (!mcpHandler && ![
|
|
325
|
+
"POST",
|
|
326
|
+
"PUT",
|
|
327
|
+
"PATCH",
|
|
328
|
+
"DELETE"
|
|
329
|
+
].includes(route.method)) continue;
|
|
330
|
+
const opName = route.operation ?? slugifyRoute(route.method, route.path);
|
|
331
|
+
const hasId = route.path.includes(":id");
|
|
332
|
+
const inputShape = {};
|
|
333
|
+
if (hasId) inputShape.id = z.string().describe("Resource ID");
|
|
334
|
+
if (mcpHandler) tools.push({
|
|
335
|
+
name: prefix ? `${prefix}_${opName}_${resource.name}` : `${opName}_${resource.name}`,
|
|
336
|
+
description: route.summary ?? route.description ?? `${opName} on ${resource.displayName}`,
|
|
337
|
+
annotations: { openWorldHint: true },
|
|
338
|
+
inputSchema: inputShape,
|
|
339
|
+
handler: async (input, _ctx) => {
|
|
340
|
+
try {
|
|
341
|
+
return await mcpHandler(input);
|
|
342
|
+
} catch (err) {
|
|
343
|
+
return {
|
|
344
|
+
content: [{
|
|
345
|
+
type: "text",
|
|
346
|
+
text: `Error: ${err instanceof Error ? err.message : String(err)}`
|
|
347
|
+
}],
|
|
348
|
+
isError: true
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
else tools.push({
|
|
354
|
+
name: prefix ? `${prefix}_${opName}_${resource.name}` : `${opName}_${resource.name}`,
|
|
355
|
+
description: route.summary ?? route.description ?? `${opName} on ${resource.displayName}`,
|
|
356
|
+
annotations: { openWorldHint: true },
|
|
357
|
+
inputSchema: inputShape,
|
|
358
|
+
handler: createAdditionalRouteHandler(route, controller, hasId)
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
return tools;
|
|
362
|
+
}
|
|
363
|
+
function buildInputSchema(op, fieldRules, opts) {
|
|
364
|
+
switch (op) {
|
|
365
|
+
case "list": return fieldRulesToZod(fieldRules, {
|
|
366
|
+
mode: "list",
|
|
367
|
+
...opts
|
|
368
|
+
});
|
|
369
|
+
case "get": return { id: z.string().describe("Resource ID") };
|
|
370
|
+
case "create": return fieldRulesToZod(fieldRules, {
|
|
371
|
+
mode: "create",
|
|
372
|
+
...opts
|
|
373
|
+
});
|
|
374
|
+
case "update": return {
|
|
375
|
+
id: z.string().describe("Resource ID"),
|
|
376
|
+
...fieldRulesToZod(fieldRules, {
|
|
377
|
+
mode: "update",
|
|
378
|
+
...opts
|
|
379
|
+
})
|
|
380
|
+
};
|
|
381
|
+
case "delete": return { id: z.string().describe("Resource ID") };
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function createHandler(op, controller, resourceName) {
|
|
385
|
+
const ctrl = controller;
|
|
386
|
+
return async (input, ctx) => {
|
|
387
|
+
try {
|
|
388
|
+
const method = ctrl[op];
|
|
389
|
+
if (typeof method !== "function") return {
|
|
390
|
+
content: [{
|
|
391
|
+
type: "text",
|
|
392
|
+
text: `Operation "${op}" not available on ${resourceName}`
|
|
393
|
+
}],
|
|
394
|
+
isError: true
|
|
395
|
+
};
|
|
396
|
+
return toCallToolResult(await method(buildRequestContext(input, ctx.session, op)));
|
|
397
|
+
} catch (err) {
|
|
398
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
399
|
+
ctx.log("error", `${resourceName}.${op}: ${msg}`).catch(() => {});
|
|
400
|
+
return {
|
|
401
|
+
content: [{
|
|
402
|
+
type: "text",
|
|
403
|
+
text: `Error: ${msg}`
|
|
404
|
+
}],
|
|
405
|
+
isError: true
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
function createAdditionalRouteHandler(route, controller, hasId) {
|
|
411
|
+
const ctrl = controller;
|
|
412
|
+
const handlerName = typeof route.handler === "string" ? route.handler : route.operation ?? slugifyRoute(route.method, route.path);
|
|
413
|
+
return async (input, ctx) => {
|
|
414
|
+
try {
|
|
415
|
+
const method = ctrl[handlerName];
|
|
416
|
+
if (typeof method !== "function") return {
|
|
417
|
+
content: [{
|
|
418
|
+
type: "text",
|
|
419
|
+
text: `Handler "${handlerName}" not found on controller`
|
|
420
|
+
}],
|
|
421
|
+
isError: true
|
|
422
|
+
};
|
|
423
|
+
return toCallToolResult(await method(buildRequestContext(input, ctx.session, hasId ? "update" : "create")));
|
|
424
|
+
} catch (err) {
|
|
425
|
+
return {
|
|
426
|
+
content: [{
|
|
427
|
+
type: "text",
|
|
428
|
+
text: `Error: ${err instanceof Error ? err.message : String(err)}`
|
|
429
|
+
}],
|
|
430
|
+
isError: true
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
function toCallToolResult(result) {
|
|
436
|
+
if (!result.success) return {
|
|
437
|
+
content: [{
|
|
438
|
+
type: "text",
|
|
439
|
+
text: result.error ?? "Operation failed"
|
|
440
|
+
}],
|
|
441
|
+
isError: true
|
|
442
|
+
};
|
|
443
|
+
const output = result.meta ? {
|
|
444
|
+
data: result.data,
|
|
445
|
+
...result.meta
|
|
446
|
+
} : result.data;
|
|
447
|
+
return { content: [{
|
|
448
|
+
type: "text",
|
|
449
|
+
text: JSON.stringify(output, null, 2)
|
|
450
|
+
}] };
|
|
451
|
+
}
|
|
452
|
+
function defaultDescription(op, displayName, softDelete, queryMeta) {
|
|
453
|
+
const name = displayName.toLowerCase();
|
|
454
|
+
switch (op) {
|
|
455
|
+
case "list": {
|
|
456
|
+
const parts = [`List ${pluralize(name)} with optional filters and pagination.`];
|
|
457
|
+
if (queryMeta?.filterableFields?.length) parts.push(`Filterable fields: ${queryMeta.filterableFields.join(", ")}.`);
|
|
458
|
+
if (queryMeta?.allowedOperators?.length) parts.push(`Filter operators: ${queryMeta.allowedOperators.join(", ")} (use field[op]=value syntax).`);
|
|
459
|
+
if (queryMeta?.sortableFields?.length) parts.push(`Sortable fields: ${queryMeta.sortableFields.join(", ")}.`);
|
|
460
|
+
return parts.join(" ");
|
|
461
|
+
}
|
|
462
|
+
case "get": return `Get a single ${name} by ID`;
|
|
463
|
+
case "create": return `Create a new ${name}`;
|
|
464
|
+
case "update": return `Update an existing ${name} by ID`;
|
|
465
|
+
case "delete": return softDelete ? `Delete a ${name} by ID (soft delete — marks as deleted, not permanently removed)` : `Delete a ${name} by ID`;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
function slugifyRoute(method, path) {
|
|
469
|
+
const clean = path.replace(/:[^/]+/g, "").replace(/^\/+|\/+$/g, "").replace(/\//g, "_");
|
|
470
|
+
return clean ? `${method.toLowerCase()}_${clean}` : method.toLowerCase();
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Auto-create a BaseController from the resource's adapter for MCP use.
|
|
474
|
+
* Called when the resource has an adapter but no controller
|
|
475
|
+
* (e.g. `disableDefaultRoutes: true` skips controller creation in defineResource).
|
|
476
|
+
*/
|
|
477
|
+
function createMcpController(resource) {
|
|
478
|
+
const repository = resource.adapter?.repository;
|
|
479
|
+
if (!repository) return void 0;
|
|
480
|
+
return new BaseController(repository, {
|
|
481
|
+
resourceName: resource.name,
|
|
482
|
+
schemaOptions: resource.schemaOptions,
|
|
483
|
+
tenantField: resource.tenantField,
|
|
484
|
+
idField: resource.idField,
|
|
485
|
+
matchesFilter: resource.adapter?.matchesFilter
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
//#endregion
|
|
489
|
+
export { fieldRulesToZod as n, createMcpServer as r, resourceToTools as t };
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { r as CircuitBreakerOptions } from "../circuitBreaker-JP2GdJ4b.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/rpc/serviceClient.d.ts
|
|
4
|
+
interface RetryConfig {
|
|
5
|
+
/** Max retry attempts (not counting initial attempt). Default: 2 */
|
|
6
|
+
maxRetries?: number;
|
|
7
|
+
/** Initial backoff delay in ms. Doubles on each retry. Default: 200 */
|
|
8
|
+
backoffMs?: number;
|
|
9
|
+
/** Max backoff cap in ms. Default: 5000 */
|
|
10
|
+
maxBackoffMs?: number;
|
|
11
|
+
/**
|
|
12
|
+
* HTTP status codes to retry on. Default: [502, 503, 504, 408, 429]
|
|
13
|
+
* 4xx errors (except 408, 429) are NOT retried — they are client errors.
|
|
14
|
+
*/
|
|
15
|
+
retryableStatuses?: number[];
|
|
16
|
+
}
|
|
17
|
+
interface RequestInfo {
|
|
18
|
+
method: string;
|
|
19
|
+
url: string;
|
|
20
|
+
headers?: Record<string, string>;
|
|
21
|
+
}
|
|
22
|
+
interface ResponseInfo {
|
|
23
|
+
method: string;
|
|
24
|
+
url: string;
|
|
25
|
+
status: number;
|
|
26
|
+
durationMs: number;
|
|
27
|
+
retries: number;
|
|
28
|
+
}
|
|
29
|
+
interface ServiceClientOptions {
|
|
30
|
+
/** Base URL of the remote Arc service (e.g., 'http://catalog-service:3000') */
|
|
31
|
+
baseUrl: string;
|
|
32
|
+
/** Static bearer token, or function that returns one (for rotation) */
|
|
33
|
+
token?: string | (() => string);
|
|
34
|
+
/** Organization ID — sent as x-organization-id header */
|
|
35
|
+
organizationId?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Correlation ID for distributed tracing — sent as x-request-id header.
|
|
38
|
+
* Static string or function (e.g., () => request.id from current request context).
|
|
39
|
+
*/
|
|
40
|
+
correlationId?: string | (() => string);
|
|
41
|
+
/** Schema version — sent as x-arc-schema-version header for contract compatibility */
|
|
42
|
+
schemaVersion?: string;
|
|
43
|
+
/** Additional headers sent with every request */
|
|
44
|
+
headers?: Record<string, string>;
|
|
45
|
+
/** Request timeout in ms (default: 10000) */
|
|
46
|
+
timeout?: number;
|
|
47
|
+
/** Retry config for transient failures (default: disabled) */
|
|
48
|
+
retry?: RetryConfig;
|
|
49
|
+
/** Circuit breaker config (default: disabled) */
|
|
50
|
+
circuitBreaker?: Pick<CircuitBreakerOptions, "failureThreshold" | "resetTimeout" | "timeout" | "successThreshold">;
|
|
51
|
+
/** Health check path (default: '/_health/live' — matches Arc's health plugin) */
|
|
52
|
+
healthPath?: string;
|
|
53
|
+
/** Called before each request (for logging, metrics, tracing) */
|
|
54
|
+
onRequest?: (info: RequestInfo) => void;
|
|
55
|
+
/** Called after each response (for logging, metrics, tracing) */
|
|
56
|
+
onResponse?: (info: ResponseInfo) => void;
|
|
57
|
+
}
|
|
58
|
+
interface ResourceClient {
|
|
59
|
+
/** GET /{resource}s?...query */
|
|
60
|
+
list(query?: Record<string, unknown>): Promise<ServiceResponse>;
|
|
61
|
+
/** GET /{resource}s/:id */
|
|
62
|
+
get(id: string): Promise<ServiceResponse>;
|
|
63
|
+
/** POST /{resource}s */
|
|
64
|
+
create(data: Record<string, unknown>): Promise<ServiceResponse>;
|
|
65
|
+
/** PATCH /{resource}s/:id */
|
|
66
|
+
update(id: string, data: Record<string, unknown>): Promise<ServiceResponse>;
|
|
67
|
+
/** DELETE /{resource}s/:id */
|
|
68
|
+
delete(id: string): Promise<ServiceResponse>;
|
|
69
|
+
/** POST /{resource}s/:id/action */
|
|
70
|
+
action(id: string, actionName: string, data?: Record<string, unknown>): Promise<ServiceResponse>;
|
|
71
|
+
}
|
|
72
|
+
interface ServiceResponse<T = any> {
|
|
73
|
+
success: boolean;
|
|
74
|
+
data?: T;
|
|
75
|
+
error?: string;
|
|
76
|
+
message?: string;
|
|
77
|
+
status?: number;
|
|
78
|
+
meta?: Record<string, unknown>;
|
|
79
|
+
}
|
|
80
|
+
interface ServiceClient {
|
|
81
|
+
/** Get a typed resource client for CRUD + actions */
|
|
82
|
+
resource(name: string): ResourceClient;
|
|
83
|
+
/** Raw call to any path (for non-resource endpoints) */
|
|
84
|
+
call(method: string, path: string, body?: unknown): Promise<ServiceResponse>;
|
|
85
|
+
/** Health check — returns true if service is reachable */
|
|
86
|
+
health(): Promise<boolean>;
|
|
87
|
+
}
|
|
88
|
+
declare function createServiceClient(options: ServiceClientOptions): ServiceClient;
|
|
89
|
+
//#endregion
|
|
90
|
+
export { type RequestInfo, type ResourceClient, type ResponseInfo, type RetryConfig, type ServiceClient, type ServiceClientOptions, type ServiceResponse, createServiceClient };
|