@happyvertical/smrt-core 0.40.59 → 0.40.60
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/dist/generators/mcp-runtime-template.d.ts +8 -0
- package/dist/generators/mcp-runtime-template.d.ts.map +1 -1
- package/dist/generators/mcp-runtime-template.js +75 -31
- package/dist/generators/mcp-runtime-template.js.map +1 -1
- package/dist/generators/mcp.d.ts +32 -18
- package/dist/generators/mcp.d.ts.map +1 -1
- package/dist/generators/mcp.js +367 -168
- package/dist/generators/mcp.js.map +1 -1
- package/dist/generators/tool-schema.d.ts +32 -1
- package/dist/generators/tool-schema.d.ts.map +1 -1
- package/dist/generators/tool-schema.js +108 -36
- package/dist/generators/tool-schema.js.map +1 -1
- package/dist/manifest/static-manifest.js +1 -1
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest/test-manifest-stub.d.ts.map +1 -1
- package/dist/manifest/test-manifest-stub.js +1241 -1
- package/dist/manifest/test-manifest-stub.js.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/prebuild/index.d.ts.map +1 -1
- package/dist/prebuild/index.js +1 -0
- package/dist/prebuild/index.js.map +1 -1
- package/dist/smrt-knowledge.json +3 -3
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +1 -0
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin/web-collections.d.ts +5 -3
- package/dist/vite-plugin/web-collections.d.ts.map +1 -1
- package/dist/vite-plugin/web-collections.js +7 -4
- package/dist/vite-plugin/web-collections.js.map +1 -1
- package/package.json +4 -4
package/dist/generators/mcp.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY,
|
|
1
|
+
import { SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY, buildCustomActionInvocationArgs, customActionParameterInputName, normalizeCustomActionFailure, resolveCustomActionMetadata } from "./custom-action.js";
|
|
2
2
|
import { ObjectRegistry } from "../registry.js";
|
|
3
3
|
import { SmrtCollection } from "../collection.js";
|
|
4
4
|
import { runWithTenantGate } from "./tenant-gate.js";
|
|
5
5
|
import { generateClaudeConfig, generateMCPDocumentation, generateMCPScript, generateRuntimeBootstrap } from "./mcp-runtime-template.js";
|
|
6
|
+
import { buildToolInputSchema, fieldTypeToJsonSchema, finalizeMcpJsonSchema } from "./tool-schema.js";
|
|
6
7
|
import { dirname, resolve } from "node:path";
|
|
7
8
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
8
9
|
//#region src/generators/mcp.ts
|
|
@@ -96,96 +97,32 @@ var MCPGenerator = class {
|
|
|
96
97
|
if (shouldInclude("list")) tools.push({
|
|
97
98
|
name: `${lowerName}_list`,
|
|
98
99
|
description: `List ${objectName} objects with optional filtering`,
|
|
99
|
-
inputSchema:
|
|
100
|
-
|
|
101
|
-
properties: {
|
|
102
|
-
limit: {
|
|
103
|
-
type: "integer",
|
|
104
|
-
description: "Maximum number of items to return",
|
|
105
|
-
default: 50,
|
|
106
|
-
minimum: 1,
|
|
107
|
-
maximum: 1e3
|
|
108
|
-
},
|
|
109
|
-
offset: {
|
|
110
|
-
type: "integer",
|
|
111
|
-
description: "Number of items to skip",
|
|
112
|
-
default: 0,
|
|
113
|
-
minimum: 0
|
|
114
|
-
},
|
|
115
|
-
orderBy: {
|
|
116
|
-
type: "string",
|
|
117
|
-
description: "Field to order by (e.g., \"created_at DESC\")"
|
|
118
|
-
},
|
|
119
|
-
where: {
|
|
120
|
-
type: "object",
|
|
121
|
-
description: "Filter conditions as key-value pairs",
|
|
122
|
-
additionalProperties: true
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
100
|
+
inputSchema: this.buildInputSchema(objectName, "list", fields),
|
|
101
|
+
outputSchema: this.buildOutputSchema(objectName, "list", fields)
|
|
126
102
|
});
|
|
127
103
|
if (shouldInclude("get")) tools.push({
|
|
128
104
|
name: `${lowerName}_get`,
|
|
129
105
|
description: `Get a specific ${objectName} by ID or slug`,
|
|
130
|
-
inputSchema:
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
106
|
+
inputSchema: this.buildInputSchema(objectName, "get", fields),
|
|
107
|
+
outputSchema: this.buildOutputSchema(objectName, "get", fields)
|
|
108
|
+
});
|
|
109
|
+
if (shouldInclude("create")) tools.push({
|
|
110
|
+
name: `${lowerName}_create`,
|
|
111
|
+
description: `Create a new ${objectName}`,
|
|
112
|
+
inputSchema: this.buildInputSchema(objectName, "create", fields),
|
|
113
|
+
outputSchema: this.buildOutputSchema(objectName, "create", fields)
|
|
114
|
+
});
|
|
115
|
+
if (shouldInclude("update")) tools.push({
|
|
116
|
+
name: `${lowerName}_update`,
|
|
117
|
+
description: `Update an existing ${objectName}`,
|
|
118
|
+
inputSchema: this.buildInputSchema(objectName, "update", fields),
|
|
119
|
+
outputSchema: this.buildOutputSchema(objectName, "update", fields)
|
|
144
120
|
});
|
|
145
|
-
if (shouldInclude("create")) {
|
|
146
|
-
const properties = {};
|
|
147
|
-
const required = [];
|
|
148
|
-
for (const [fieldName, field] of fields) {
|
|
149
|
-
properties[fieldName] = this.fieldToMCPSchema(field);
|
|
150
|
-
if (field._meta?.required) required.push(fieldName);
|
|
151
|
-
}
|
|
152
|
-
tools.push({
|
|
153
|
-
name: `${lowerName}_create`,
|
|
154
|
-
description: `Create a new ${objectName}`,
|
|
155
|
-
inputSchema: {
|
|
156
|
-
type: "object",
|
|
157
|
-
properties,
|
|
158
|
-
required
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
if (shouldInclude("update")) {
|
|
163
|
-
const properties = { id: {
|
|
164
|
-
type: "string",
|
|
165
|
-
description: "ID of the object to update"
|
|
166
|
-
} };
|
|
167
|
-
for (const [fieldName, field] of fields) properties[fieldName] = this.fieldToMCPSchema(field);
|
|
168
|
-
tools.push({
|
|
169
|
-
name: `${lowerName}_update`,
|
|
170
|
-
description: `Update an existing ${objectName}`,
|
|
171
|
-
inputSchema: {
|
|
172
|
-
type: "object",
|
|
173
|
-
properties,
|
|
174
|
-
required: ["id"]
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
121
|
if (shouldInclude("delete")) tools.push({
|
|
179
122
|
name: `${lowerName}_delete`,
|
|
180
123
|
description: `Delete a ${objectName} by ID`,
|
|
181
|
-
inputSchema:
|
|
182
|
-
|
|
183
|
-
properties: { id: {
|
|
184
|
-
type: "string",
|
|
185
|
-
description: "ID of the object to delete"
|
|
186
|
-
} },
|
|
187
|
-
required: ["id"]
|
|
188
|
-
}
|
|
124
|
+
inputSchema: this.buildInputSchema(objectName, "delete", fields),
|
|
125
|
+
outputSchema: this.buildOutputSchema(objectName, "delete", fields)
|
|
189
126
|
});
|
|
190
127
|
if (classInfo) {
|
|
191
128
|
const mcpConfig = ObjectRegistry.getConfig(objectName).mcp;
|
|
@@ -227,7 +164,8 @@ var MCPGenerator = class {
|
|
|
227
164
|
return {
|
|
228
165
|
name: `${lowerName}_${methodName}`.toLowerCase(),
|
|
229
166
|
description: `Execute ${methodName} action on ${objectName}`,
|
|
230
|
-
inputSchema:
|
|
167
|
+
inputSchema: this.buildInputSchema(objectName, methodName, ObjectRegistry.getFields(objectName), metadata),
|
|
168
|
+
outputSchema: this.buildOutputSchema(objectName, methodName, ObjectRegistry.getFields(objectName))
|
|
231
169
|
};
|
|
232
170
|
}
|
|
233
171
|
resolveCustomActionMetadata(objectName, action, method, collectionReceiver = false) {
|
|
@@ -254,45 +192,193 @@ var MCPGenerator = class {
|
|
|
254
192
|
return false;
|
|
255
193
|
}
|
|
256
194
|
}
|
|
195
|
+
/** Normalize registry fields for the transport-neutral schema emitter. */
|
|
196
|
+
toToolFields(fields) {
|
|
197
|
+
return Array.from(fields, ([name, field]) => ({
|
|
198
|
+
name,
|
|
199
|
+
type: field.type,
|
|
200
|
+
required: field.required ?? field._meta?.required,
|
|
201
|
+
nullable: field._meta?.nullable === true,
|
|
202
|
+
description: typeof field._meta?.description === "string" ? field._meta.description : void 0,
|
|
203
|
+
default: field._meta?.default,
|
|
204
|
+
maxLength: field._meta?.maxLength,
|
|
205
|
+
minLength: field._meta?.minLength,
|
|
206
|
+
min: field._meta?.min,
|
|
207
|
+
max: field._meta?.max,
|
|
208
|
+
related: field.related
|
|
209
|
+
}));
|
|
210
|
+
}
|
|
257
211
|
/**
|
|
258
|
-
*
|
|
212
|
+
* Add the optional STI discriminator branches to write schemas. The legacy
|
|
213
|
+
* branch preserves existing base-class creates that let SMRT pick the base
|
|
214
|
+
* type; an explicit `_meta_type` selects a known child collection below.
|
|
259
215
|
*/
|
|
260
|
-
|
|
261
|
-
const schema =
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
216
|
+
buildInputSchema(objectName, action, fields, customAction) {
|
|
217
|
+
const schema = buildToolInputSchema(action, this.toToolFields(fields), customAction, ObjectRegistry.getConfig(objectName).idType);
|
|
218
|
+
if (action !== "create" && action !== "update") return schema;
|
|
219
|
+
const variants = this.getStiVariants(objectName);
|
|
220
|
+
if (variants.length === 0) return schema;
|
|
221
|
+
const properties = {
|
|
222
|
+
...schema.properties ?? {},
|
|
223
|
+
_meta_type: {
|
|
224
|
+
type: "string",
|
|
225
|
+
description: "Optional STI discriminator. When provided for create, selects the declared subtype."
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
return finalizeMcpJsonSchema({
|
|
229
|
+
...schema,
|
|
230
|
+
properties,
|
|
231
|
+
oneOf: [{ not: { required: ["_meta_type"] } }, ...variants.map(({ name, discriminator }) => {
|
|
232
|
+
const variantFields = ObjectRegistry.getFields(name);
|
|
233
|
+
return {
|
|
234
|
+
properties: {
|
|
235
|
+
...this.buildFieldSchemaProperties(variantFields),
|
|
236
|
+
_meta_type: { const: discriminator }
|
|
237
|
+
},
|
|
238
|
+
required: ["_meta_type", ...this.toToolFields(variantFields).filter((field) => field.required).map((field) => field.name)]
|
|
239
|
+
};
|
|
240
|
+
})]
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Public output schemas follow the actual `toPublicJSON()` boundary: known
|
|
245
|
+
* non-sensitive fields are described, while `additionalProperties` keeps
|
|
246
|
+
* framework/system fields and application transforms honest.
|
|
247
|
+
*/
|
|
248
|
+
buildOutputSchema(objectName, action, fields) {
|
|
249
|
+
const errorSchema = {
|
|
250
|
+
type: "object",
|
|
251
|
+
properties: { error: {
|
|
252
|
+
type: "object",
|
|
253
|
+
additionalProperties: true
|
|
254
|
+
} },
|
|
255
|
+
required: ["error"]
|
|
256
|
+
};
|
|
257
|
+
if (![
|
|
258
|
+
"list",
|
|
259
|
+
"get",
|
|
260
|
+
"create",
|
|
261
|
+
"update",
|
|
262
|
+
"delete"
|
|
263
|
+
].includes(action)) return finalizeMcpJsonSchema({
|
|
264
|
+
type: "object",
|
|
265
|
+
anyOf: [{
|
|
266
|
+
type: "object",
|
|
267
|
+
properties: { data: {} },
|
|
268
|
+
required: ["data"]
|
|
269
|
+
}, { $ref: "#/$defs/error" }],
|
|
270
|
+
$defs: { error: errorSchema }
|
|
271
|
+
});
|
|
272
|
+
const itemSchema = this.buildPublicItemSchema(objectName, fields);
|
|
273
|
+
if (action === "list") return finalizeMcpJsonSchema({
|
|
274
|
+
type: "object",
|
|
275
|
+
anyOf: [{
|
|
276
|
+
type: "object",
|
|
277
|
+
properties: {
|
|
278
|
+
data: {
|
|
279
|
+
type: "array",
|
|
280
|
+
items: { $ref: "#/$defs/publicItem" }
|
|
281
|
+
},
|
|
282
|
+
meta: {
|
|
283
|
+
type: "object",
|
|
284
|
+
properties: {
|
|
285
|
+
total: {
|
|
286
|
+
type: "integer",
|
|
287
|
+
minimum: 0
|
|
288
|
+
},
|
|
289
|
+
limit: {
|
|
290
|
+
type: "integer",
|
|
291
|
+
minimum: 0
|
|
292
|
+
},
|
|
293
|
+
offset: {
|
|
294
|
+
type: "integer",
|
|
295
|
+
minimum: 0
|
|
296
|
+
},
|
|
297
|
+
count: {
|
|
298
|
+
type: "integer",
|
|
299
|
+
minimum: 0
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
required: [
|
|
303
|
+
"total",
|
|
304
|
+
"limit",
|
|
305
|
+
"offset",
|
|
306
|
+
"count"
|
|
307
|
+
]
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
required: ["data", "meta"]
|
|
311
|
+
}, { $ref: "#/$defs/error" }],
|
|
312
|
+
$defs: {
|
|
313
|
+
publicItem: itemSchema,
|
|
314
|
+
error: errorSchema
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
if (action === "delete") return finalizeMcpJsonSchema({
|
|
318
|
+
type: "object",
|
|
319
|
+
anyOf: [{
|
|
320
|
+
type: "object",
|
|
321
|
+
properties: {
|
|
322
|
+
success: { const: true },
|
|
323
|
+
message: { type: "string" }
|
|
324
|
+
},
|
|
325
|
+
required: ["success", "message"]
|
|
326
|
+
}, { $ref: "#/$defs/error" }],
|
|
327
|
+
$defs: { error: errorSchema }
|
|
328
|
+
});
|
|
329
|
+
return finalizeMcpJsonSchema({
|
|
330
|
+
type: "object",
|
|
331
|
+
anyOf: [itemSchema, { $ref: "#/$defs/error" }],
|
|
332
|
+
$defs: { error: errorSchema }
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
buildPublicItemSchema(objectName, fields) {
|
|
336
|
+
const properties = this.buildFieldSchemaProperties(fields, true);
|
|
337
|
+
const variants = this.getStiVariants(objectName);
|
|
338
|
+
if (variants.length > 0) return { oneOf: variants.map(({ name, discriminator }) => ({
|
|
339
|
+
type: "object",
|
|
340
|
+
properties: {
|
|
341
|
+
...this.buildFieldSchemaProperties(ObjectRegistry.getFields(name), true),
|
|
342
|
+
_meta_type: { const: discriminator }
|
|
343
|
+
},
|
|
344
|
+
required: ["_meta_type"],
|
|
345
|
+
additionalProperties: true
|
|
346
|
+
})) };
|
|
347
|
+
return {
|
|
348
|
+
type: "object",
|
|
349
|
+
properties,
|
|
350
|
+
additionalProperties: true
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
buildFieldSchemaProperties(fields, publicOnly = false) {
|
|
354
|
+
const properties = {};
|
|
355
|
+
for (const [name, field] of fields) {
|
|
356
|
+
if (publicOnly && (field._meta?.sensitive === true || field._meta?.transient === true)) continue;
|
|
357
|
+
const [toolField] = this.toToolFields(/* @__PURE__ */ new Map([[name, field]]));
|
|
358
|
+
if (!toolField) continue;
|
|
359
|
+
properties[name] = { ...fieldTypeToJsonSchema(toolField) };
|
|
293
360
|
}
|
|
294
|
-
|
|
295
|
-
|
|
361
|
+
return properties;
|
|
362
|
+
}
|
|
363
|
+
getStiVariants(objectName) {
|
|
364
|
+
if (ObjectRegistry.getTableStrategy(objectName) !== "sti") return [];
|
|
365
|
+
const base = ObjectRegistry.getClass(objectName);
|
|
366
|
+
const baseNames = new Set([
|
|
367
|
+
objectName,
|
|
368
|
+
base?.name,
|
|
369
|
+
base?.qualifiedName
|
|
370
|
+
].filter((name) => typeof name === "string"));
|
|
371
|
+
const variants = /* @__PURE__ */ new Map();
|
|
372
|
+
for (const [key, info] of ObjectRegistry.getAllClasses()) {
|
|
373
|
+
const name = info.name || key;
|
|
374
|
+
if (!ObjectRegistry.getInheritanceChain(name).some((ancestor) => baseNames.has(ancestor))) continue;
|
|
375
|
+
const discriminator = info.qualifiedName || name;
|
|
376
|
+
variants.set(discriminator, {
|
|
377
|
+
name,
|
|
378
|
+
discriminator
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
return Array.from(variants.values()).sort((left, right) => left.discriminator.localeCompare(right.discriminator));
|
|
296
382
|
}
|
|
297
383
|
/**
|
|
298
384
|
* Handle MCP tool calls
|
|
@@ -319,25 +405,56 @@ var MCPGenerator = class {
|
|
|
319
405
|
if (!classInfo) throw new Error(`Object type '${objectName}' not found`);
|
|
320
406
|
const collection = await this.getCollection(actualObjectName, classInfo);
|
|
321
407
|
const result = await this.executeAction(collection, action, args, actualObjectName);
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
408
|
+
const publicResult = this.toJsonValue(result);
|
|
409
|
+
const structuredContent = this.toStructuredContent(action, publicResult);
|
|
410
|
+
return {
|
|
411
|
+
content: [{
|
|
412
|
+
type: "text",
|
|
413
|
+
text: JSON.stringify(publicResult, null, 2)
|
|
414
|
+
}],
|
|
415
|
+
structuredContent
|
|
416
|
+
};
|
|
326
417
|
} catch (error) {
|
|
327
|
-
if (error instanceof CustomActionFailureError)
|
|
418
|
+
if (error instanceof CustomActionFailureError) {
|
|
419
|
+
const structuredContent = { error: error.failure };
|
|
420
|
+
return {
|
|
421
|
+
content: [{
|
|
422
|
+
type: "text",
|
|
423
|
+
text: JSON.stringify(structuredContent)
|
|
424
|
+
}],
|
|
425
|
+
isError: true,
|
|
426
|
+
structuredContent,
|
|
427
|
+
_meta: { [SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY]: error.failure }
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
431
|
+
return {
|
|
328
432
|
content: [{
|
|
329
433
|
type: "text",
|
|
330
|
-
text:
|
|
434
|
+
text: `Error: ${message}`
|
|
331
435
|
}],
|
|
332
436
|
isError: true,
|
|
333
|
-
|
|
437
|
+
structuredContent: { error: { message } }
|
|
334
438
|
};
|
|
335
|
-
return { content: [{
|
|
336
|
-
type: "text",
|
|
337
|
-
text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
338
|
-
}] };
|
|
339
439
|
}
|
|
340
440
|
}
|
|
441
|
+
/** Convert runtime values to the JSON values MCP structuredContent permits. */
|
|
442
|
+
toJsonValue(value) {
|
|
443
|
+
const serialized = JSON.stringify(value);
|
|
444
|
+
return serialized === void 0 ? null : JSON.parse(serialized);
|
|
445
|
+
}
|
|
446
|
+
/** Build the MCP-required object root without changing legacy text payloads. */
|
|
447
|
+
toStructuredContent(action, publicResult) {
|
|
448
|
+
if (![
|
|
449
|
+
"list",
|
|
450
|
+
"get",
|
|
451
|
+
"create",
|
|
452
|
+
"update",
|
|
453
|
+
"delete"
|
|
454
|
+
].includes(action)) return { data: publicResult };
|
|
455
|
+
if (publicResult === null || typeof publicResult !== "object" || Array.isArray(publicResult)) throw new Error(`Expected object result for MCP ${action} action`);
|
|
456
|
+
return publicResult;
|
|
457
|
+
}
|
|
341
458
|
/**
|
|
342
459
|
* Get or create collection for an object
|
|
343
460
|
*/
|
|
@@ -433,14 +550,24 @@ var MCPGenerator = class {
|
|
|
433
550
|
if (!this.context.user) throw new Error("Authentication required");
|
|
434
551
|
}
|
|
435
552
|
async executeAction(collection, action, args, objectName) {
|
|
553
|
+
let targetCollection = collection;
|
|
554
|
+
let targetObjectName = objectName;
|
|
555
|
+
if (action === "create" && objectName && typeof args._meta_type === "string") {
|
|
556
|
+
const variant = this.getStiVariants(objectName).find((candidate) => candidate.discriminator === args._meta_type);
|
|
557
|
+
if (!variant) throw new Error(`Unknown STI discriminator: ${args._meta_type}`);
|
|
558
|
+
const classInfo = ObjectRegistry.getClass(variant.name);
|
|
559
|
+
if (!classInfo) throw new Error(`STI subtype '${variant.name}' is not registered`);
|
|
560
|
+
targetCollection = await this.getCollection(variant.name, classInfo);
|
|
561
|
+
targetObjectName = variant.name;
|
|
562
|
+
}
|
|
436
563
|
const mutating = action !== "list" && action !== "get";
|
|
437
|
-
this.requireToolAuth(
|
|
564
|
+
this.requireToolAuth(targetObjectName, mutating);
|
|
438
565
|
return runWithTenantGate({
|
|
439
|
-
className:
|
|
566
|
+
className: targetObjectName,
|
|
440
567
|
tenantId: this.context.tenantId,
|
|
441
568
|
allowCrossTenant: this.context.allowCrossTenant,
|
|
442
569
|
surface: "MCP"
|
|
443
|
-
}, () => this.runAction(
|
|
570
|
+
}, () => this.runAction(targetCollection, action, args, targetObjectName));
|
|
444
571
|
}
|
|
445
572
|
/**
|
|
446
573
|
* Derive the set of tenant-scoped object names (lowercased simple names) from
|
|
@@ -647,7 +774,8 @@ var MCPGenerator = class {
|
|
|
647
774
|
debug,
|
|
648
775
|
tools,
|
|
649
776
|
customActions: await this.runtimeCustomActions(tools),
|
|
650
|
-
tenantScopedObjects: await this.tenantScopedObjectNames(tools)
|
|
777
|
+
tenantScopedObjects: await this.tenantScopedObjectNames(tools),
|
|
778
|
+
stiTargets: this.runtimeStiTargets(tools)
|
|
651
779
|
}), "utf-8");
|
|
652
780
|
console.log(`✅ Generated MCP server: ${resolvedPath}`);
|
|
653
781
|
}
|
|
@@ -703,6 +831,28 @@ var MCPGenerator = class {
|
|
|
703
831
|
return metadata;
|
|
704
832
|
}
|
|
705
833
|
/**
|
|
834
|
+
* Emit only the STI discriminator targets advertised by create-tool schemas.
|
|
835
|
+
* Generated processes start with an empty registry, so resolving the
|
|
836
|
+
* qualified target through `getCollection()` both validates the declaration
|
|
837
|
+
* and lets the public registry loader register the selected subtype.
|
|
838
|
+
*/
|
|
839
|
+
runtimeStiTargets(tools) {
|
|
840
|
+
const targets = {};
|
|
841
|
+
const classes = ObjectRegistry.getAllClasses();
|
|
842
|
+
for (const tool of tools) {
|
|
843
|
+
const separator = tool.name.indexOf("_");
|
|
844
|
+
if (separator === -1 || tool.name.slice(separator + 1) !== "create") continue;
|
|
845
|
+
const objectPrefix = tool.name.slice(0, separator);
|
|
846
|
+
const matched = Array.from(classes.entries()).find(([key, info]) => (info.name || key).toLowerCase() === objectPrefix);
|
|
847
|
+
if (!matched) continue;
|
|
848
|
+
const [key, classInfo] = matched;
|
|
849
|
+
const variants = this.getStiVariants(classInfo.name || key);
|
|
850
|
+
if (variants.length === 0) continue;
|
|
851
|
+
targets[objectPrefix] = Object.fromEntries(variants.map((variant) => [variant.discriminator, variant.discriminator]));
|
|
852
|
+
}
|
|
853
|
+
return targets;
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
706
856
|
* Generate modular MCP server structure
|
|
707
857
|
*
|
|
708
858
|
* Creates separate files for tools, handlers, configuration, and main entry point.
|
|
@@ -761,14 +911,15 @@ export const tools: Array<{
|
|
|
761
911
|
name: string;
|
|
762
912
|
description: string;
|
|
763
913
|
inputSchema: any;
|
|
914
|
+
outputSchema: any;
|
|
764
915
|
}> = ${JSON.stringify(tools, null, 2)};
|
|
765
916
|
`;
|
|
766
917
|
}
|
|
767
918
|
/**
|
|
768
919
|
* Generate switch cases for tool execution
|
|
769
920
|
*/
|
|
770
|
-
async generateToolSwitchCases(indent = " ") {
|
|
771
|
-
const tools = await this.generateTools();
|
|
921
|
+
async generateToolSwitchCases(indent = " ", generatedTools) {
|
|
922
|
+
const tools = generatedTools ?? await this.generateTools();
|
|
772
923
|
const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
|
|
773
924
|
return (await Promise.all(tools.map(async (tool) => {
|
|
774
925
|
const separator = tool.name.indexOf("_");
|
|
@@ -781,13 +932,17 @@ ${indent} const offset = args.offset ?? 0;
|
|
|
781
932
|
${indent} const where = args.where ?? {};
|
|
782
933
|
|
|
783
934
|
${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {
|
|
784
|
-
${indent} persistence: { type: '
|
|
935
|
+
${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },
|
|
785
936
|
${indent} ai: aiConfig
|
|
786
937
|
${indent} });
|
|
787
938
|
|
|
788
939
|
${indent} const items = await collection.list({ where, limit, offset });
|
|
789
940
|
${indent} const itemsPublic = items.map((item) => item.toPublicJSON(PUBLIC_JSON_OPTIONS));
|
|
790
|
-
${indent}
|
|
941
|
+
${indent} const structuredContent = {
|
|
942
|
+
${indent} data: itemsPublic,
|
|
943
|
+
${indent} meta: { total: await collection.count({ where }), limit, offset, count: items.length },
|
|
944
|
+
${indent} };
|
|
945
|
+
${indent} return successResult(structuredContent, JSON.stringify(itemsPublic));
|
|
791
946
|
${indent}}`;
|
|
792
947
|
case "get": return `${indent}case '${tool.name}': {
|
|
793
948
|
${indent} if (!args.id && !args.slug) {
|
|
@@ -795,7 +950,7 @@ ${indent} throw new Error('Either id or slug is required');
|
|
|
795
950
|
${indent} }
|
|
796
951
|
|
|
797
952
|
${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {
|
|
798
|
-
${indent} persistence: { type: '
|
|
953
|
+
${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },
|
|
799
954
|
${indent} ai: aiConfig
|
|
800
955
|
${indent} });
|
|
801
956
|
|
|
@@ -806,18 +961,15 @@ ${indent} if (!item) {
|
|
|
806
961
|
${indent} throw new Error('Object not found');
|
|
807
962
|
${indent} }
|
|
808
963
|
|
|
809
|
-
${indent} return
|
|
964
|
+
${indent} return successResult(item.toPublicJSON(PUBLIC_JSON_OPTIONS));
|
|
810
965
|
${indent}}`;
|
|
811
966
|
case "create": return `${indent}case '${tool.name}': {
|
|
812
|
-
${indent} const collection = await
|
|
813
|
-
${indent} persistence: { type: 'sql', url: process.env.DATABASE_URL || ':memory:' },
|
|
814
|
-
${indent} ai: aiConfig
|
|
815
|
-
${indent} });
|
|
967
|
+
${indent} const { collection, objectName: targetObjectName } = await resolveCreateTarget('${objectName}', args, aiConfig);
|
|
816
968
|
|
|
817
|
-
${indent} const newItem = await collection.create(applyWritablePolicy(
|
|
969
|
+
${indent} const newItem = await collection.create(applyWritablePolicy(targetObjectName, args));
|
|
818
970
|
${indent} await newItem.save();
|
|
819
971
|
|
|
820
|
-
${indent} return
|
|
972
|
+
${indent} return successResult(newItem.toPublicJSON(PUBLIC_JSON_OPTIONS));
|
|
821
973
|
${indent}}`;
|
|
822
974
|
case "update": return `${indent}case '${tool.name}': {
|
|
823
975
|
${indent} const { id, ...updateData } = args;
|
|
@@ -826,7 +978,7 @@ ${indent} throw new Error('ID is required for update');
|
|
|
826
978
|
${indent} }
|
|
827
979
|
|
|
828
980
|
${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {
|
|
829
|
-
${indent} persistence: { type: '
|
|
981
|
+
${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },
|
|
830
982
|
${indent} ai: aiConfig
|
|
831
983
|
${indent} });
|
|
832
984
|
|
|
@@ -838,7 +990,7 @@ ${indent} }
|
|
|
838
990
|
${indent} Object.assign(existing, applyWritablePolicy('${capitalize(objectName)}', updateData));
|
|
839
991
|
${indent} await existing.save();
|
|
840
992
|
|
|
841
|
-
${indent} return
|
|
993
|
+
${indent} return successResult(existing.toPublicJSON(PUBLIC_JSON_OPTIONS));
|
|
842
994
|
${indent}}`;
|
|
843
995
|
case "delete": return `${indent}case '${tool.name}': {
|
|
844
996
|
${indent} if (!args.id) {
|
|
@@ -846,7 +998,7 @@ ${indent} throw new Error('ID is required for delete');
|
|
|
846
998
|
${indent} }
|
|
847
999
|
|
|
848
1000
|
${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {
|
|
849
|
-
${indent} persistence: { type: '
|
|
1001
|
+
${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },
|
|
850
1002
|
${indent} ai: aiConfig
|
|
851
1003
|
${indent} });
|
|
852
1004
|
|
|
@@ -857,7 +1009,7 @@ ${indent} }
|
|
|
857
1009
|
|
|
858
1010
|
${indent} await toDelete.delete();
|
|
859
1011
|
|
|
860
|
-
${indent} return
|
|
1012
|
+
${indent} return successResult({ success: true, message: 'Object deleted successfully' });
|
|
861
1013
|
${indent}}`;
|
|
862
1014
|
default: {
|
|
863
1015
|
const matched = Array.from(ObjectRegistry.getAllClasses().entries()).find(([key, info]) => (info.name || key).toLowerCase() === objectName.toLowerCase());
|
|
@@ -878,7 +1030,7 @@ ${indent} throw new Error('Custom action ${action} is collection-scoped and d
|
|
|
878
1030
|
${indent} }
|
|
879
1031
|
|
|
880
1032
|
${indent} const collection = await ObjectRegistry.getCollection(${JSON.stringify(registeredName)}, {
|
|
881
|
-
${indent} persistence: { type: '
|
|
1033
|
+
${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },
|
|
882
1034
|
${indent} ai: aiConfig
|
|
883
1035
|
${indent} });
|
|
884
1036
|
|
|
@@ -900,14 +1052,15 @@ ${indent} const methodArgs = ${methodArgs.startsWith("[") ? methodArgs : `[${me
|
|
|
900
1052
|
${indent} const result = await actionMethod.call(target, ...methodArgs);
|
|
901
1053
|
${indent} const failure = normalizeCustomActionFailure(result);
|
|
902
1054
|
${indent} if (failure) {
|
|
903
|
-
${indent} return
|
|
904
|
-
${indent}
|
|
905
|
-
${indent}
|
|
906
|
-
${indent}
|
|
907
|
-
${indent}
|
|
1055
|
+
${indent} return errorResult(
|
|
1056
|
+
${indent} { error: failure },
|
|
1057
|
+
${indent} JSON.stringify({ error: failure }),
|
|
1058
|
+
${indent} { [SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY]: failure },
|
|
1059
|
+
${indent} );
|
|
908
1060
|
${indent} }
|
|
909
1061
|
|
|
910
|
-
${indent}
|
|
1062
|
+
${indent} const publicResult = toPublicResult(result);
|
|
1063
|
+
${indent} return successResult({ data: publicResult }, JSON.stringify(publicResult));
|
|
911
1064
|
${indent}}`;
|
|
912
1065
|
}
|
|
913
1066
|
}
|
|
@@ -917,7 +1070,9 @@ ${indent}}`;
|
|
|
917
1070
|
* Generate handlers file for modular server
|
|
918
1071
|
*/
|
|
919
1072
|
async generateHandlersFile(tenantScopedObjects = []) {
|
|
920
|
-
const
|
|
1073
|
+
const tools = await this.generateTools();
|
|
1074
|
+
const switchCases = await this.generateToolSwitchCases(" ", tools);
|
|
1075
|
+
const stiTargets = this.runtimeStiTargets(tools);
|
|
921
1076
|
const tenantScopedSet = Array.from(new Set(tenantScopedObjects.map((n) => n.toLowerCase())));
|
|
922
1077
|
const hasTenantScoped = tenantScopedSet.length > 0;
|
|
923
1078
|
return `/**
|
|
@@ -950,6 +1105,7 @@ const PUBLIC_JSON_OPTIONS = {
|
|
|
950
1105
|
.map((permission) => permission.trim())
|
|
951
1106
|
.filter(Boolean),
|
|
952
1107
|
};
|
|
1108
|
+
const STI_TARGETS: Record<string, Record<string, string>> = ${JSON.stringify(stiTargets)};
|
|
953
1109
|
|
|
954
1110
|
/**
|
|
955
1111
|
* Mass-assignment guard (#1540): strip framework/server-managed and
|
|
@@ -984,6 +1140,23 @@ function applyWritablePolicy(objectName: string, data: any): Record<string, any>
|
|
|
984
1140
|
return result;
|
|
985
1141
|
}
|
|
986
1142
|
|
|
1143
|
+
/** Resolve an advertised STI discriminator to its registered subtype collection. */
|
|
1144
|
+
async function resolveCreateTarget(baseObjectName: string, args: Record<string, any>, aiConfig: any) {
|
|
1145
|
+
let objectName = baseObjectName;
|
|
1146
|
+
const discriminator = args._meta_type;
|
|
1147
|
+
const targets = STI_TARGETS[baseObjectName];
|
|
1148
|
+
if (typeof discriminator === 'string' && targets) {
|
|
1149
|
+
const target = targets[discriminator];
|
|
1150
|
+
if (!target) throw new Error('Unknown STI discriminator: ' + discriminator);
|
|
1151
|
+
objectName = target;
|
|
1152
|
+
}
|
|
1153
|
+
const collection = await ObjectRegistry.getCollection(objectName, {
|
|
1154
|
+
persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },
|
|
1155
|
+
ai: aiConfig,
|
|
1156
|
+
});
|
|
1157
|
+
return { collection, objectName };
|
|
1158
|
+
}
|
|
1159
|
+
|
|
987
1160
|
/**
|
|
988
1161
|
* Sensitive-field-safe serialization for custom-action results (#1540).
|
|
989
1162
|
* Recurses through arrays and plain objects so nested SmrtObjects are stripped
|
|
@@ -1008,6 +1181,22 @@ function toPublicResult(value: any, seen: WeakSet<object> = new WeakSet()): any
|
|
|
1008
1181
|
return out;
|
|
1009
1182
|
}
|
|
1010
1183
|
|
|
1184
|
+
function successResult(structuredContent: any, text = JSON.stringify(structuredContent)) {
|
|
1185
|
+
return {
|
|
1186
|
+
content: [{ type: 'text', text }],
|
|
1187
|
+
structuredContent,
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
function errorResult(structuredContent: any, text: string, _meta?: Record<string, any>) {
|
|
1192
|
+
return {
|
|
1193
|
+
content: [{ type: 'text', text }],
|
|
1194
|
+
isError: true,
|
|
1195
|
+
structuredContent,
|
|
1196
|
+
...(_meta ? { _meta } : {}),
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1011
1200
|
/**
|
|
1012
1201
|
* Handle tool call request
|
|
1013
1202
|
*/
|
|
@@ -1043,15 +1232,10 @@ ${hasTenantScoped ? `
|
|
|
1043
1232
|
} catch (error) {
|
|
1044
1233
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
1045
1234
|
|
|
1046
|
-
return
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
text: \`Error executing tool \${name}: \${errorMessage}\`,
|
|
1051
|
-
},
|
|
1052
|
-
],
|
|
1053
|
-
isError: true,
|
|
1054
|
-
};
|
|
1235
|
+
return errorResult(
|
|
1236
|
+
{ error: { message: errorMessage } },
|
|
1237
|
+
\`Error executing tool \${name}: \${errorMessage}\`,
|
|
1238
|
+
);
|
|
1055
1239
|
}
|
|
1056
1240
|
}
|
|
1057
1241
|
`;
|
|
@@ -1070,7 +1254,10 @@ ${hasTenantScoped ? `
|
|
|
1070
1254
|
|
|
1071
1255
|
import { Server } from '@modelcontextprotocol/server';
|
|
1072
1256
|
import { serveStdio } from '@modelcontextprotocol/server/stdio';
|
|
1257
|
+
import { existsSync } from 'node:fs';
|
|
1258
|
+
import { resolve } from 'node:path';
|
|
1073
1259
|
import { pathToFileURL } from 'node:url';
|
|
1260
|
+
import { ObjectRegistry } from '@happyvertical/smrt-core';
|
|
1074
1261
|
import { loadConfig } from '@happyvertical/smrt-config';
|
|
1075
1262
|
import { getDatabase } from '@happyvertical/sql';
|
|
1076
1263
|
import { getAI } from '@happyvertical/ai';
|
|
@@ -1088,6 +1275,17 @@ export async function createServer() {
|
|
|
1088
1275
|
console.error(\`[MCP] Available tools:\`, tools.map(t => t.name).join(', '));
|
|
1089
1276
|
}
|
|
1090
1277
|
|
|
1278
|
+
// Register the application package manifest before resolving generated
|
|
1279
|
+
// object names. Generated servers are commonly run from the application
|
|
1280
|
+
// package itself, which is not a node_modules dependency of its process.
|
|
1281
|
+
const localManifestPaths = [
|
|
1282
|
+
resolve(process.cwd(), 'dist', 'manifest.json'),
|
|
1283
|
+
resolve(process.cwd(), '.smrt', 'manifest.json'),
|
|
1284
|
+
].filter(existsSync);
|
|
1285
|
+
if (localManifestPaths.length > 0) {
|
|
1286
|
+
ObjectRegistry.loadAllManifests({ manifestPaths: localManifestPaths });
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1091
1289
|
// Load configuration from environment and .smrt.config files
|
|
1092
1290
|
const appConfig = await loadConfig();
|
|
1093
1291
|
const aiConfig = appConfig?.ai || {};
|
|
@@ -1116,6 +1314,7 @@ export async function createServer() {
|
|
|
1116
1314
|
name: tool.name,
|
|
1117
1315
|
description: tool.description,
|
|
1118
1316
|
inputSchema: tool.inputSchema,
|
|
1317
|
+
outputSchema: tool.outputSchema,
|
|
1119
1318
|
})),
|
|
1120
1319
|
};
|
|
1121
1320
|
});
|