@happyvertical/smrt-core 0.40.59 → 0.40.61

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.
Files changed (56) hide show
  1. package/dist/generated-client-runtime.d.ts +6 -5
  2. package/dist/generated-client-runtime.d.ts.map +1 -1
  3. package/dist/generated-client-runtime.js +49 -14
  4. package/dist/generated-client-runtime.js.map +1 -1
  5. package/dist/generators/index.d.ts +3 -2
  6. package/dist/generators/index.d.ts.map +1 -1
  7. package/dist/generators/index.js +3 -2
  8. package/dist/generators/mcp-runtime-template.d.ts +13 -0
  9. package/dist/generators/mcp-runtime-template.d.ts.map +1 -1
  10. package/dist/generators/mcp-runtime-template.js +83 -32
  11. package/dist/generators/mcp-runtime-template.js.map +1 -1
  12. package/dist/generators/mcp.d.ts +79 -18
  13. package/dist/generators/mcp.d.ts.map +1 -1
  14. package/dist/generators/mcp.js +429 -177
  15. package/dist/generators/mcp.js.map +1 -1
  16. package/dist/generators/rest.d.ts.map +1 -1
  17. package/dist/generators/rest.js +3 -0
  18. package/dist/generators/rest.js.map +1 -1
  19. package/dist/generators/tool-schema.d.ts +32 -1
  20. package/dist/generators/tool-schema.d.ts.map +1 -1
  21. package/dist/generators/tool-schema.js +108 -36
  22. package/dist/generators/tool-schema.js.map +1 -1
  23. package/dist/generators/typed-http-error.d.ts +16 -0
  24. package/dist/generators/typed-http-error.d.ts.map +1 -0
  25. package/dist/generators/typed-http-error.js +17 -0
  26. package/dist/generators/typed-http-error.js.map +1 -0
  27. package/dist/generators.js +3 -2
  28. package/dist/index.js +3 -2
  29. package/dist/manifest/static-manifest.js +1 -1
  30. package/dist/manifest/static-manifest.js.map +1 -1
  31. package/dist/manifest/store.js +1 -1
  32. package/dist/manifest.json +1 -1
  33. package/dist/prebuild/index.d.ts.map +1 -1
  34. package/dist/prebuild/index.js +14 -1
  35. package/dist/prebuild/index.js.map +1 -1
  36. package/dist/registry/types.d.ts +15 -0
  37. package/dist/registry/types.d.ts.map +1 -1
  38. package/dist/smrt-knowledge.json +3 -3
  39. package/dist/vite-plugin/index.d.ts +2 -0
  40. package/dist/vite-plugin/index.d.ts.map +1 -1
  41. package/dist/vite-plugin/index.js +25 -11
  42. package/dist/vite-plugin/index.js.map +1 -1
  43. package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
  44. package/dist/vite-plugin/sveltekit-generator.js +94 -26
  45. package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
  46. package/dist/vite-plugin/web-collections.d.ts +6 -3
  47. package/dist/vite-plugin/web-collections.d.ts.map +1 -1
  48. package/dist/vite-plugin/web-collections.js +24 -6
  49. package/dist/vite-plugin/web-collections.js.map +1 -1
  50. package/package.json +5 -5
  51. package/dist/manifest/test-manifest-loader.d.ts +0 -3
  52. package/dist/manifest/test-manifest-loader.d.ts.map +0 -1
  53. package/dist/manifest/test-manifest-stub.d.ts +0 -4
  54. package/dist/manifest/test-manifest-stub.d.ts.map +0 -1
  55. package/dist/manifest/test-manifest-stub.js +0 -75816
  56. package/dist/manifest/test-manifest-stub.js.map +0 -1
@@ -1,8 +1,9 @@
1
- import { SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY, buildCustomActionInputSchema, buildCustomActionInvocationArgs, customActionParameterInputName, normalizeCustomActionFailure, resolveCustomActionMetadata } from "./custom-action.js";
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
@@ -11,6 +12,27 @@ import { mkdir, writeFile } from "node:fs/promises";
11
12
  *
12
13
  * Exposes smrt objects as AI tools for Claude, GPT, and other AI models
13
14
  */
15
+ var MCP_STABLE_CATALOG_TTL_MS = 864e5;
16
+ /**
17
+ * Resolve the generated tools/list cache policy at generation time.
18
+ *
19
+ * A shared cache may otherwise serve one tenant's tool catalog to another, so
20
+ * public caching is deliberately double opt-in and unavailable when a
21
+ * generated server exposes any tenant-scoped object.
22
+ */
23
+ function resolveMCPToolListCacheHint(options, hasTenantScopedTools) {
24
+ const ttlMs = options?.ttlMs ?? 864e5;
25
+ if (!Number.isSafeInteger(ttlMs) || ttlMs < 0) throw new RangeError("MCP tools/list cache ttlMs must be a non-negative safe integer.");
26
+ if (options?.cacheScope !== void 0 && options.cacheScope !== "private" && options.cacheScope !== "public") throw new RangeError("MCP tools/list cacheScope must be 'private' or 'public'.");
27
+ return {
28
+ ttlMs,
29
+ cacheScope: !hasTenantScopedTools && options?.cacheScope === "public" && options.publicCatalog === true ? "public" : "private"
30
+ };
31
+ }
32
+ /** Return a copied, canonical tool sequence for byte-stable tools/list output. */
33
+ function sortMCPTools(tools) {
34
+ return [...tools].sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
35
+ }
14
36
  var CustomActionFailureError = class extends Error {
15
37
  failure;
16
38
  constructor(failure) {
@@ -83,7 +105,7 @@ var MCPGenerator = class {
83
105
  const objectTools = await this.generateObjectTools(simpleName, shouldInclude);
84
106
  tools.push(...objectTools);
85
107
  }
86
- return tools;
108
+ return sortMCPTools(tools);
87
109
  }
88
110
  /**
89
111
  * Generate tools for a specific object
@@ -96,96 +118,32 @@ var MCPGenerator = class {
96
118
  if (shouldInclude("list")) tools.push({
97
119
  name: `${lowerName}_list`,
98
120
  description: `List ${objectName} objects with optional filtering`,
99
- inputSchema: {
100
- type: "object",
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
- }
121
+ inputSchema: this.buildInputSchema(objectName, "list", fields),
122
+ outputSchema: this.buildOutputSchema(objectName, "list", fields)
126
123
  });
127
124
  if (shouldInclude("get")) tools.push({
128
125
  name: `${lowerName}_get`,
129
126
  description: `Get a specific ${objectName} by ID or slug`,
130
- inputSchema: {
131
- type: "object",
132
- properties: {
133
- id: {
134
- type: "string",
135
- description: "Unique identifier of the object"
136
- },
137
- slug: {
138
- type: "string",
139
- description: "URL-friendly identifier of the object"
140
- }
141
- },
142
- required: ["id"]
143
- }
127
+ inputSchema: this.buildInputSchema(objectName, "get", fields),
128
+ outputSchema: this.buildOutputSchema(objectName, "get", fields)
129
+ });
130
+ if (shouldInclude("create")) tools.push({
131
+ name: `${lowerName}_create`,
132
+ description: `Create a new ${objectName}`,
133
+ inputSchema: this.buildInputSchema(objectName, "create", fields),
134
+ outputSchema: this.buildOutputSchema(objectName, "create", fields)
135
+ });
136
+ if (shouldInclude("update")) tools.push({
137
+ name: `${lowerName}_update`,
138
+ description: `Update an existing ${objectName}`,
139
+ inputSchema: this.buildInputSchema(objectName, "update", fields),
140
+ outputSchema: this.buildOutputSchema(objectName, "update", fields)
144
141
  });
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
142
  if (shouldInclude("delete")) tools.push({
179
143
  name: `${lowerName}_delete`,
180
144
  description: `Delete a ${objectName} by ID`,
181
- inputSchema: {
182
- type: "object",
183
- properties: { id: {
184
- type: "string",
185
- description: "ID of the object to delete"
186
- } },
187
- required: ["id"]
188
- }
145
+ inputSchema: this.buildInputSchema(objectName, "delete", fields),
146
+ outputSchema: this.buildOutputSchema(objectName, "delete", fields)
189
147
  });
190
148
  if (classInfo) {
191
149
  const mcpConfig = ObjectRegistry.getConfig(objectName).mcp;
@@ -227,7 +185,8 @@ var MCPGenerator = class {
227
185
  return {
228
186
  name: `${lowerName}_${methodName}`.toLowerCase(),
229
187
  description: `Execute ${methodName} action on ${objectName}`,
230
- inputSchema: buildCustomActionInputSchema(metadata)
188
+ inputSchema: this.buildInputSchema(objectName, methodName, ObjectRegistry.getFields(objectName), metadata),
189
+ outputSchema: this.buildOutputSchema(objectName, methodName, ObjectRegistry.getFields(objectName))
231
190
  };
232
191
  }
233
192
  resolveCustomActionMetadata(objectName, action, method, collectionReceiver = false) {
@@ -254,45 +213,193 @@ var MCPGenerator = class {
254
213
  return false;
255
214
  }
256
215
  }
216
+ /** Normalize registry fields for the transport-neutral schema emitter. */
217
+ toToolFields(fields) {
218
+ return Array.from(fields, ([name, field]) => ({
219
+ name,
220
+ type: field.type,
221
+ required: field.required ?? field._meta?.required,
222
+ nullable: field._meta?.nullable === true,
223
+ description: typeof field._meta?.description === "string" ? field._meta.description : void 0,
224
+ default: field._meta?.default,
225
+ maxLength: field._meta?.maxLength,
226
+ minLength: field._meta?.minLength,
227
+ min: field._meta?.min,
228
+ max: field._meta?.max,
229
+ related: field.related
230
+ }));
231
+ }
257
232
  /**
258
- * Convert field definition to MCP schema
233
+ * Add the optional STI discriminator branches to write schemas. The legacy
234
+ * branch preserves existing base-class creates that let SMRT pick the base
235
+ * type; an explicit `_meta_type` selects a known child collection below.
259
236
  */
260
- fieldToMCPSchema(field) {
261
- const schema = { description: field._meta?.description || `${field.type} field` };
262
- switch (field.type) {
263
- case "text":
264
- schema.type = "string";
265
- if (field._meta?.maxLength) schema.maxLength = field._meta.maxLength;
266
- if (field._meta?.minLength) schema.minLength = field._meta.minLength;
267
- break;
268
- case "integer":
269
- schema.type = "integer";
270
- if (field._meta?.min !== void 0) schema.minimum = field._meta.min;
271
- if (field._meta?.max !== void 0) schema.maximum = field._meta.max;
272
- break;
273
- case "decimal":
274
- schema.type = "number";
275
- if (field._meta?.min !== void 0) schema.minimum = field._meta.min;
276
- if (field._meta?.max !== void 0) schema.maximum = field._meta.max;
277
- break;
278
- case "boolean":
279
- schema.type = "boolean";
280
- break;
281
- case "datetime":
282
- schema.type = "string";
283
- schema.format = "date-time";
284
- break;
285
- case "json":
286
- schema.type = "object";
287
- break;
288
- case "foreignKey":
289
- schema.type = "string";
290
- if (!field._meta?.description) schema.description = `ID of related ${field.related || "object"}`;
291
- break;
292
- default: schema.type = "string";
237
+ buildInputSchema(objectName, action, fields, customAction) {
238
+ const schema = buildToolInputSchema(action, this.toToolFields(fields), customAction, ObjectRegistry.getConfig(objectName).idType);
239
+ if (action !== "create" && action !== "update") return schema;
240
+ const variants = this.getStiVariants(objectName);
241
+ if (variants.length === 0) return schema;
242
+ const properties = {
243
+ ...schema.properties ?? {},
244
+ _meta_type: {
245
+ type: "string",
246
+ description: "Optional STI discriminator. When provided for create, selects the declared subtype."
247
+ }
248
+ };
249
+ return finalizeMcpJsonSchema({
250
+ ...schema,
251
+ properties,
252
+ oneOf: [{ not: { required: ["_meta_type"] } }, ...variants.map(({ name, discriminator }) => {
253
+ const variantFields = ObjectRegistry.getFields(name);
254
+ return {
255
+ properties: {
256
+ ...this.buildFieldSchemaProperties(variantFields),
257
+ _meta_type: { const: discriminator }
258
+ },
259
+ required: ["_meta_type", ...this.toToolFields(variantFields).filter((field) => field.required).map((field) => field.name)]
260
+ };
261
+ })]
262
+ });
263
+ }
264
+ /**
265
+ * Public output schemas follow the actual `toPublicJSON()` boundary: known
266
+ * non-sensitive fields are described, while `additionalProperties` keeps
267
+ * framework/system fields and application transforms honest.
268
+ */
269
+ buildOutputSchema(objectName, action, fields) {
270
+ const errorSchema = {
271
+ type: "object",
272
+ properties: { error: {
273
+ type: "object",
274
+ additionalProperties: true
275
+ } },
276
+ required: ["error"]
277
+ };
278
+ if (![
279
+ "list",
280
+ "get",
281
+ "create",
282
+ "update",
283
+ "delete"
284
+ ].includes(action)) return finalizeMcpJsonSchema({
285
+ type: "object",
286
+ anyOf: [{
287
+ type: "object",
288
+ properties: { data: {} },
289
+ required: ["data"]
290
+ }, { $ref: "#/$defs/error" }],
291
+ $defs: { error: errorSchema }
292
+ });
293
+ const itemSchema = this.buildPublicItemSchema(objectName, fields);
294
+ if (action === "list") return finalizeMcpJsonSchema({
295
+ type: "object",
296
+ anyOf: [{
297
+ type: "object",
298
+ properties: {
299
+ data: {
300
+ type: "array",
301
+ items: { $ref: "#/$defs/publicItem" }
302
+ },
303
+ meta: {
304
+ type: "object",
305
+ properties: {
306
+ total: {
307
+ type: "integer",
308
+ minimum: 0
309
+ },
310
+ limit: {
311
+ type: "integer",
312
+ minimum: 0
313
+ },
314
+ offset: {
315
+ type: "integer",
316
+ minimum: 0
317
+ },
318
+ count: {
319
+ type: "integer",
320
+ minimum: 0
321
+ }
322
+ },
323
+ required: [
324
+ "total",
325
+ "limit",
326
+ "offset",
327
+ "count"
328
+ ]
329
+ }
330
+ },
331
+ required: ["data", "meta"]
332
+ }, { $ref: "#/$defs/error" }],
333
+ $defs: {
334
+ publicItem: itemSchema,
335
+ error: errorSchema
336
+ }
337
+ });
338
+ if (action === "delete") return finalizeMcpJsonSchema({
339
+ type: "object",
340
+ anyOf: [{
341
+ type: "object",
342
+ properties: {
343
+ success: { const: true },
344
+ message: { type: "string" }
345
+ },
346
+ required: ["success", "message"]
347
+ }, { $ref: "#/$defs/error" }],
348
+ $defs: { error: errorSchema }
349
+ });
350
+ return finalizeMcpJsonSchema({
351
+ type: "object",
352
+ anyOf: [itemSchema, { $ref: "#/$defs/error" }],
353
+ $defs: { error: errorSchema }
354
+ });
355
+ }
356
+ buildPublicItemSchema(objectName, fields) {
357
+ const properties = this.buildFieldSchemaProperties(fields, true);
358
+ const variants = this.getStiVariants(objectName);
359
+ if (variants.length > 0) return { oneOf: variants.map(({ name, discriminator }) => ({
360
+ type: "object",
361
+ properties: {
362
+ ...this.buildFieldSchemaProperties(ObjectRegistry.getFields(name), true),
363
+ _meta_type: { const: discriminator }
364
+ },
365
+ required: ["_meta_type"],
366
+ additionalProperties: true
367
+ })) };
368
+ return {
369
+ type: "object",
370
+ properties,
371
+ additionalProperties: true
372
+ };
373
+ }
374
+ buildFieldSchemaProperties(fields, publicOnly = false) {
375
+ const properties = {};
376
+ for (const [name, field] of fields) {
377
+ if (publicOnly && (field._meta?.sensitive === true || field._meta?.transient === true)) continue;
378
+ const [toolField] = this.toToolFields(/* @__PURE__ */ new Map([[name, field]]));
379
+ if (!toolField) continue;
380
+ properties[name] = { ...fieldTypeToJsonSchema(toolField) };
293
381
  }
294
- if (field._meta?.default !== void 0) schema.default = field._meta.default;
295
- return schema;
382
+ return properties;
383
+ }
384
+ getStiVariants(objectName) {
385
+ if (ObjectRegistry.getTableStrategy(objectName) !== "sti") return [];
386
+ const base = ObjectRegistry.getClass(objectName);
387
+ const baseNames = new Set([
388
+ objectName,
389
+ base?.name,
390
+ base?.qualifiedName
391
+ ].filter((name) => typeof name === "string"));
392
+ const variants = /* @__PURE__ */ new Map();
393
+ for (const [key, info] of ObjectRegistry.getAllClasses()) {
394
+ const name = info.name || key;
395
+ if (!ObjectRegistry.getInheritanceChain(name).some((ancestor) => baseNames.has(ancestor))) continue;
396
+ const discriminator = info.qualifiedName || name;
397
+ variants.set(discriminator, {
398
+ name,
399
+ discriminator
400
+ });
401
+ }
402
+ return Array.from(variants.values()).sort((left, right) => left.discriminator.localeCompare(right.discriminator));
296
403
  }
297
404
  /**
298
405
  * Handle MCP tool calls
@@ -319,25 +426,56 @@ var MCPGenerator = class {
319
426
  if (!classInfo) throw new Error(`Object type '${objectName}' not found`);
320
427
  const collection = await this.getCollection(actualObjectName, classInfo);
321
428
  const result = await this.executeAction(collection, action, args, actualObjectName);
322
- return { content: [{
323
- type: "text",
324
- text: JSON.stringify(result, null, 2)
325
- }] };
429
+ const publicResult = this.toJsonValue(result);
430
+ const structuredContent = this.toStructuredContent(action, publicResult);
431
+ return {
432
+ content: [{
433
+ type: "text",
434
+ text: JSON.stringify(publicResult, null, 2)
435
+ }],
436
+ structuredContent
437
+ };
326
438
  } catch (error) {
327
- if (error instanceof CustomActionFailureError) return {
439
+ if (error instanceof CustomActionFailureError) {
440
+ const structuredContent = { error: error.failure };
441
+ return {
442
+ content: [{
443
+ type: "text",
444
+ text: JSON.stringify(structuredContent)
445
+ }],
446
+ isError: true,
447
+ structuredContent,
448
+ _meta: { [SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY]: error.failure }
449
+ };
450
+ }
451
+ const message = error instanceof Error ? error.message : "Unknown error";
452
+ return {
328
453
  content: [{
329
454
  type: "text",
330
- text: JSON.stringify({ error: error.failure })
455
+ text: `Error: ${message}`
331
456
  }],
332
457
  isError: true,
333
- _meta: { [SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY]: error.failure }
458
+ structuredContent: { error: { message } }
334
459
  };
335
- return { content: [{
336
- type: "text",
337
- text: `Error: ${error instanceof Error ? error.message : "Unknown error"}`
338
- }] };
339
460
  }
340
461
  }
462
+ /** Convert runtime values to the JSON values MCP structuredContent permits. */
463
+ toJsonValue(value) {
464
+ const serialized = JSON.stringify(value);
465
+ return serialized === void 0 ? null : JSON.parse(serialized);
466
+ }
467
+ /** Build the MCP-required object root without changing legacy text payloads. */
468
+ toStructuredContent(action, publicResult) {
469
+ if (![
470
+ "list",
471
+ "get",
472
+ "create",
473
+ "update",
474
+ "delete"
475
+ ].includes(action)) return { data: publicResult };
476
+ if (publicResult === null || typeof publicResult !== "object" || Array.isArray(publicResult)) throw new Error(`Expected object result for MCP ${action} action`);
477
+ return publicResult;
478
+ }
341
479
  /**
342
480
  * Get or create collection for an object
343
481
  */
@@ -433,14 +571,24 @@ var MCPGenerator = class {
433
571
  if (!this.context.user) throw new Error("Authentication required");
434
572
  }
435
573
  async executeAction(collection, action, args, objectName) {
574
+ let targetCollection = collection;
575
+ let targetObjectName = objectName;
576
+ if (action === "create" && objectName && typeof args._meta_type === "string") {
577
+ const variant = this.getStiVariants(objectName).find((candidate) => candidate.discriminator === args._meta_type);
578
+ if (!variant) throw new Error(`Unknown STI discriminator: ${args._meta_type}`);
579
+ const classInfo = ObjectRegistry.getClass(variant.name);
580
+ if (!classInfo) throw new Error(`STI subtype '${variant.name}' is not registered`);
581
+ targetCollection = await this.getCollection(variant.name, classInfo);
582
+ targetObjectName = variant.name;
583
+ }
436
584
  const mutating = action !== "list" && action !== "get";
437
- this.requireToolAuth(objectName, mutating);
585
+ this.requireToolAuth(targetObjectName, mutating);
438
586
  return runWithTenantGate({
439
- className: objectName,
587
+ className: targetObjectName,
440
588
  tenantId: this.context.tenantId,
441
589
  allowCrossTenant: this.context.allowCrossTenant,
442
590
  surface: "MCP"
443
- }, () => this.runAction(collection, action, args, objectName));
591
+ }, () => this.runAction(targetCollection, action, args, targetObjectName));
444
592
  }
445
593
  /**
446
594
  * Derive the set of tenant-scoped object names (lowercased simple names) from
@@ -482,6 +630,29 @@ var MCPGenerator = class {
482
630
  return Array.from(scoped);
483
631
  }
484
632
  /**
633
+ * Whether a catalog contains a tenant-scoped class for cache isolation.
634
+ *
635
+ * Unlike the emitted runtime tenant gate, cache visibility must also fail
636
+ * closed for core-declared `@smrt({ tenantScoped })` models when the optional
637
+ * tenancy package is not installed. The registry covers that form, while
638
+ * `tenantScopedObjectNames()` covers the tenancy-owned decorator form.
639
+ */
640
+ async hasTenantScopedTools(tools) {
641
+ if ((await this.tenantScopedObjectNames(tools)).length > 0) return true;
642
+ for (const tool of tools) {
643
+ const [objectName] = tool.name.split("_");
644
+ if (!objectName) continue;
645
+ for (const [key, info] of ObjectRegistry.getAllClasses()) {
646
+ const simpleName = info.name || key;
647
+ if (simpleName.toLowerCase() === objectName.toLowerCase()) {
648
+ if (ObjectRegistry.isTenantScoped(simpleName)) return true;
649
+ break;
650
+ }
651
+ }
652
+ }
653
+ return false;
654
+ }
655
+ /**
485
656
  * Execute a resolved MCP action (CRUD or custom) against a collection. Always
486
657
  * invoked inside the tenant gate established by {@link executeAction}.
487
658
  */
@@ -638,6 +809,8 @@ var MCPGenerator = class {
638
809
  if (modular) await this.generateModularServer(outputDir, serverName, serverVersion, debug);
639
810
  else {
640
811
  const tools = await this.generateTools();
812
+ const tenantScopedObjects = await this.tenantScopedObjectNames(tools);
813
+ const hasTenantScopedTools = tenantScopedObjects.length > 0 || await this.hasTenantScopedTools(tools);
641
814
  await writeFile(resolvedPath, generateRuntimeBootstrap({
642
815
  name: serverName,
643
816
  version: serverVersion,
@@ -647,7 +820,9 @@ var MCPGenerator = class {
647
820
  debug,
648
821
  tools,
649
822
  customActions: await this.runtimeCustomActions(tools),
650
- tenantScopedObjects: await this.tenantScopedObjectNames(tools)
823
+ tenantScopedObjects,
824
+ stiTargets: this.runtimeStiTargets(tools),
825
+ toolListCacheHint: resolveMCPToolListCacheHint(this.config.cache?.toolsList, hasTenantScopedTools)
651
826
  }), "utf-8");
652
827
  console.log(`✅ Generated MCP server: ${resolvedPath}`);
653
828
  }
@@ -703,6 +878,28 @@ var MCPGenerator = class {
703
878
  return metadata;
704
879
  }
705
880
  /**
881
+ * Emit only the STI discriminator targets advertised by create-tool schemas.
882
+ * Generated processes start with an empty registry, so resolving the
883
+ * qualified target through `getCollection()` both validates the declaration
884
+ * and lets the public registry loader register the selected subtype.
885
+ */
886
+ runtimeStiTargets(tools) {
887
+ const targets = {};
888
+ const classes = ObjectRegistry.getAllClasses();
889
+ for (const tool of tools) {
890
+ const separator = tool.name.indexOf("_");
891
+ if (separator === -1 || tool.name.slice(separator + 1) !== "create") continue;
892
+ const objectPrefix = tool.name.slice(0, separator);
893
+ const matched = Array.from(classes.entries()).find(([key, info]) => (info.name || key).toLowerCase() === objectPrefix);
894
+ if (!matched) continue;
895
+ const [key, classInfo] = matched;
896
+ const variants = this.getStiVariants(classInfo.name || key);
897
+ if (variants.length === 0) continue;
898
+ targets[objectPrefix] = Object.fromEntries(variants.map((variant) => [variant.discriminator, variant.discriminator]));
899
+ }
900
+ return targets;
901
+ }
902
+ /**
706
903
  * Generate modular MCP server structure
707
904
  *
708
905
  * Creates separate files for tools, handlers, configuration, and main entry point.
@@ -721,15 +918,17 @@ var MCPGenerator = class {
721
918
  const configPath = resolve(outputDir, "config.ts");
722
919
  await writeFile(configPath, this.generateConfigFile(serverName, serverVersion, debug), "utf-8");
723
920
  console.log(`✅ Generated config: ${configPath}`);
921
+ const generatedTools = await this.generateTools();
724
922
  const toolsPath = resolve(toolsDir, "index.ts");
725
- await writeFile(toolsPath, await this.generateToolsFile(), "utf-8");
923
+ await writeFile(toolsPath, this.generateToolsFile(generatedTools), "utf-8");
726
924
  console.log(`✅ Generated tools: ${toolsPath}`);
727
925
  const handlersPath = resolve(handlersDir, "index.ts");
728
- const tenantScopedObjects = await this.tenantScopedObjectNames(await this.generateTools());
926
+ const tenantScopedObjects = await this.tenantScopedObjectNames(generatedTools);
927
+ const hasTenantScopedTools = tenantScopedObjects.length > 0 || await this.hasTenantScopedTools(generatedTools);
729
928
  await writeFile(handlersPath, await this.generateHandlersFile(tenantScopedObjects), "utf-8");
730
929
  console.log(`✅ Generated handlers: ${handlersPath}`);
731
930
  const indexPath = resolve(outputDir, "index.js");
732
- await writeFile(indexPath, this.generateModularIndex(), "utf-8");
931
+ await writeFile(indexPath, this.generateModularIndex(resolveMCPToolListCacheHint(this.config.cache?.toolsList, hasTenantScopedTools)), "utf-8");
733
932
  console.log(`✅ Generated MCP server: ${indexPath}`);
734
933
  }
735
934
  /**
@@ -750,8 +949,7 @@ export const DEBUG = ${debug};
750
949
  /**
751
950
  * Generate tools definitions file for modular server
752
951
  */
753
- async generateToolsFile() {
754
- const tools = await this.generateTools();
952
+ generateToolsFile(tools) {
755
953
  return `/**
756
954
  * MCP Tools Definitions
757
955
  * Auto-generated from SMRT objects
@@ -761,14 +959,15 @@ export const tools: Array<{
761
959
  name: string;
762
960
  description: string;
763
961
  inputSchema: any;
962
+ outputSchema: any;
764
963
  }> = ${JSON.stringify(tools, null, 2)};
765
964
  `;
766
965
  }
767
966
  /**
768
967
  * Generate switch cases for tool execution
769
968
  */
770
- async generateToolSwitchCases(indent = " ") {
771
- const tools = await this.generateTools();
969
+ async generateToolSwitchCases(indent = " ", generatedTools) {
970
+ const tools = generatedTools ?? await this.generateTools();
772
971
  const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
773
972
  return (await Promise.all(tools.map(async (tool) => {
774
973
  const separator = tool.name.indexOf("_");
@@ -781,13 +980,17 @@ ${indent} const offset = args.offset ?? 0;
781
980
  ${indent} const where = args.where ?? {};
782
981
 
783
982
  ${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {
784
- ${indent} persistence: { type: 'sql', url: process.env.DATABASE_URL || ':memory:' },
983
+ ${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },
785
984
  ${indent} ai: aiConfig
786
985
  ${indent} });
787
986
 
788
987
  ${indent} const items = await collection.list({ where, limit, offset });
789
988
  ${indent} const itemsPublic = items.map((item) => item.toPublicJSON(PUBLIC_JSON_OPTIONS));
790
- ${indent} return { content: [{ type: 'text', text: JSON.stringify(itemsPublic) }] };
989
+ ${indent} const structuredContent = {
990
+ ${indent} data: itemsPublic,
991
+ ${indent} meta: { total: await collection.count({ where }), limit, offset, count: items.length },
992
+ ${indent} };
993
+ ${indent} return successResult(structuredContent, JSON.stringify(itemsPublic));
791
994
  ${indent}}`;
792
995
  case "get": return `${indent}case '${tool.name}': {
793
996
  ${indent} if (!args.id && !args.slug) {
@@ -795,7 +998,7 @@ ${indent} throw new Error('Either id or slug is required');
795
998
  ${indent} }
796
999
 
797
1000
  ${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {
798
- ${indent} persistence: { type: 'sql', url: process.env.DATABASE_URL || ':memory:' },
1001
+ ${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },
799
1002
  ${indent} ai: aiConfig
800
1003
  ${indent} });
801
1004
 
@@ -806,18 +1009,15 @@ ${indent} if (!item) {
806
1009
  ${indent} throw new Error('Object not found');
807
1010
  ${indent} }
808
1011
 
809
- ${indent} return { content: [{ type: 'text', text: JSON.stringify(item.toPublicJSON(PUBLIC_JSON_OPTIONS)) }] };
1012
+ ${indent} return successResult(item.toPublicJSON(PUBLIC_JSON_OPTIONS));
810
1013
  ${indent}}`;
811
1014
  case "create": return `${indent}case '${tool.name}': {
812
- ${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {
813
- ${indent} persistence: { type: 'sql', url: process.env.DATABASE_URL || ':memory:' },
814
- ${indent} ai: aiConfig
815
- ${indent} });
1015
+ ${indent} const { collection, objectName: targetObjectName } = await resolveCreateTarget('${objectName}', args, aiConfig);
816
1016
 
817
- ${indent} const newItem = await collection.create(applyWritablePolicy('${capitalize(objectName)}', args));
1017
+ ${indent} const newItem = await collection.create(applyWritablePolicy(targetObjectName, args));
818
1018
  ${indent} await newItem.save();
819
1019
 
820
- ${indent} return { content: [{ type: 'text', text: JSON.stringify(newItem.toPublicJSON(PUBLIC_JSON_OPTIONS)) }] };
1020
+ ${indent} return successResult(newItem.toPublicJSON(PUBLIC_JSON_OPTIONS));
821
1021
  ${indent}}`;
822
1022
  case "update": return `${indent}case '${tool.name}': {
823
1023
  ${indent} const { id, ...updateData } = args;
@@ -826,7 +1026,7 @@ ${indent} throw new Error('ID is required for update');
826
1026
  ${indent} }
827
1027
 
828
1028
  ${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {
829
- ${indent} persistence: { type: 'sql', url: process.env.DATABASE_URL || ':memory:' },
1029
+ ${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },
830
1030
  ${indent} ai: aiConfig
831
1031
  ${indent} });
832
1032
 
@@ -838,7 +1038,7 @@ ${indent} }
838
1038
  ${indent} Object.assign(existing, applyWritablePolicy('${capitalize(objectName)}', updateData));
839
1039
  ${indent} await existing.save();
840
1040
 
841
- ${indent} return { content: [{ type: 'text', text: JSON.stringify(existing.toPublicJSON(PUBLIC_JSON_OPTIONS)) }] };
1041
+ ${indent} return successResult(existing.toPublicJSON(PUBLIC_JSON_OPTIONS));
842
1042
  ${indent}}`;
843
1043
  case "delete": return `${indent}case '${tool.name}': {
844
1044
  ${indent} if (!args.id) {
@@ -846,7 +1046,7 @@ ${indent} throw new Error('ID is required for delete');
846
1046
  ${indent} }
847
1047
 
848
1048
  ${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {
849
- ${indent} persistence: { type: 'sql', url: process.env.DATABASE_URL || ':memory:' },
1049
+ ${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },
850
1050
  ${indent} ai: aiConfig
851
1051
  ${indent} });
852
1052
 
@@ -857,7 +1057,7 @@ ${indent} }
857
1057
 
858
1058
  ${indent} await toDelete.delete();
859
1059
 
860
- ${indent} return { content: [{ type: 'text', text: JSON.stringify({ success: true, message: 'Object deleted successfully' }) }] };
1060
+ ${indent} return successResult({ success: true, message: 'Object deleted successfully' });
861
1061
  ${indent}}`;
862
1062
  default: {
863
1063
  const matched = Array.from(ObjectRegistry.getAllClasses().entries()).find(([key, info]) => (info.name || key).toLowerCase() === objectName.toLowerCase());
@@ -878,7 +1078,7 @@ ${indent} throw new Error('Custom action ${action} is collection-scoped and d
878
1078
  ${indent} }
879
1079
 
880
1080
  ${indent} const collection = await ObjectRegistry.getCollection(${JSON.stringify(registeredName)}, {
881
- ${indent} persistence: { type: 'sql', url: process.env.DATABASE_URL || ':memory:' },
1081
+ ${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },
882
1082
  ${indent} ai: aiConfig
883
1083
  ${indent} });
884
1084
 
@@ -900,14 +1100,15 @@ ${indent} const methodArgs = ${methodArgs.startsWith("[") ? methodArgs : `[${me
900
1100
  ${indent} const result = await actionMethod.call(target, ...methodArgs);
901
1101
  ${indent} const failure = normalizeCustomActionFailure(result);
902
1102
  ${indent} if (failure) {
903
- ${indent} return {
904
- ${indent} content: [{ type: 'text', text: JSON.stringify({ error: failure }) }],
905
- ${indent} isError: true,
906
- ${indent} _meta: { [SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY]: failure },
907
- ${indent} };
1103
+ ${indent} return errorResult(
1104
+ ${indent} { error: failure },
1105
+ ${indent} JSON.stringify({ error: failure }),
1106
+ ${indent} { [SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY]: failure },
1107
+ ${indent} );
908
1108
  ${indent} }
909
1109
 
910
- ${indent} return { content: [{ type: 'text', text: JSON.stringify(toPublicResult(result)) }] };
1110
+ ${indent} const publicResult = toPublicResult(result);
1111
+ ${indent} return successResult({ data: publicResult }, JSON.stringify(publicResult));
911
1112
  ${indent}}`;
912
1113
  }
913
1114
  }
@@ -917,7 +1118,9 @@ ${indent}}`;
917
1118
  * Generate handlers file for modular server
918
1119
  */
919
1120
  async generateHandlersFile(tenantScopedObjects = []) {
920
- const switchCases = await this.generateToolSwitchCases(" ");
1121
+ const tools = await this.generateTools();
1122
+ const switchCases = await this.generateToolSwitchCases(" ", tools);
1123
+ const stiTargets = this.runtimeStiTargets(tools);
921
1124
  const tenantScopedSet = Array.from(new Set(tenantScopedObjects.map((n) => n.toLowerCase())));
922
1125
  const hasTenantScoped = tenantScopedSet.length > 0;
923
1126
  return `/**
@@ -950,6 +1153,7 @@ const PUBLIC_JSON_OPTIONS = {
950
1153
  .map((permission) => permission.trim())
951
1154
  .filter(Boolean),
952
1155
  };
1156
+ const STI_TARGETS: Record<string, Record<string, string>> = ${JSON.stringify(stiTargets)};
953
1157
 
954
1158
  /**
955
1159
  * Mass-assignment guard (#1540): strip framework/server-managed and
@@ -984,6 +1188,23 @@ function applyWritablePolicy(objectName: string, data: any): Record<string, any>
984
1188
  return result;
985
1189
  }
986
1190
 
1191
+ /** Resolve an advertised STI discriminator to its registered subtype collection. */
1192
+ async function resolveCreateTarget(baseObjectName: string, args: Record<string, any>, aiConfig: any) {
1193
+ let objectName = baseObjectName;
1194
+ const discriminator = args._meta_type;
1195
+ const targets = STI_TARGETS[baseObjectName];
1196
+ if (typeof discriminator === 'string' && targets) {
1197
+ const target = targets[discriminator];
1198
+ if (!target) throw new Error('Unknown STI discriminator: ' + discriminator);
1199
+ objectName = target;
1200
+ }
1201
+ const collection = await ObjectRegistry.getCollection(objectName, {
1202
+ persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },
1203
+ ai: aiConfig,
1204
+ });
1205
+ return { collection, objectName };
1206
+ }
1207
+
987
1208
  /**
988
1209
  * Sensitive-field-safe serialization for custom-action results (#1540).
989
1210
  * Recurses through arrays and plain objects so nested SmrtObjects are stripped
@@ -1008,6 +1229,22 @@ function toPublicResult(value: any, seen: WeakSet<object> = new WeakSet()): any
1008
1229
  return out;
1009
1230
  }
1010
1231
 
1232
+ function successResult(structuredContent: any, text = JSON.stringify(structuredContent)) {
1233
+ return {
1234
+ content: [{ type: 'text', text }],
1235
+ structuredContent,
1236
+ };
1237
+ }
1238
+
1239
+ function errorResult(structuredContent: any, text: string, _meta?: Record<string, any>) {
1240
+ return {
1241
+ content: [{ type: 'text', text }],
1242
+ isError: true,
1243
+ structuredContent,
1244
+ ...(_meta ? { _meta } : {}),
1245
+ };
1246
+ }
1247
+
1011
1248
  /**
1012
1249
  * Handle tool call request
1013
1250
  */
@@ -1043,15 +1280,10 @@ ${hasTenantScoped ? `
1043
1280
  } catch (error) {
1044
1281
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
1045
1282
 
1046
- return {
1047
- content: [
1048
- {
1049
- type: 'text',
1050
- text: \`Error executing tool \${name}: \${errorMessage}\`,
1051
- },
1052
- ],
1053
- isError: true,
1054
- };
1283
+ return errorResult(
1284
+ { error: { message: errorMessage } },
1285
+ \`Error executing tool \${name}: \${errorMessage}\`,
1286
+ );
1055
1287
  }
1056
1288
  }
1057
1289
  `;
@@ -1059,7 +1291,7 @@ ${hasTenantScoped ? `
1059
1291
  /**
1060
1292
  * Generate modular index file (main entry point)
1061
1293
  */
1062
- generateModularIndex() {
1294
+ generateModularIndex(toolListCacheHint) {
1063
1295
  return `#!/usr/bin/env node
1064
1296
  /**
1065
1297
  * Auto-generated MCP Server
@@ -1070,7 +1302,10 @@ ${hasTenantScoped ? `
1070
1302
 
1071
1303
  import { Server } from '@modelcontextprotocol/server';
1072
1304
  import { serveStdio } from '@modelcontextprotocol/server/stdio';
1305
+ import { existsSync } from 'node:fs';
1306
+ import { resolve } from 'node:path';
1073
1307
  import { pathToFileURL } from 'node:url';
1308
+ import { ObjectRegistry } from '@happyvertical/smrt-core';
1074
1309
  import { loadConfig } from '@happyvertical/smrt-config';
1075
1310
  import { getDatabase } from '@happyvertical/sql';
1076
1311
  import { getAI } from '@happyvertical/ai';
@@ -1079,6 +1314,8 @@ import { SERVER_NAME, SERVER_VERSION, DEBUG } from './config.js';
1079
1314
  import { tools } from './tools/index.js';
1080
1315
  import { handleToolCall } from './handlers/index.js';
1081
1316
 
1317
+ const TOOL_LIST_CACHE_HINT = ${JSON.stringify(toolListCacheHint)};
1318
+
1082
1319
  /**
1083
1320
  * Main server startup function
1084
1321
  */
@@ -1088,6 +1325,17 @@ export async function createServer() {
1088
1325
  console.error(\`[MCP] Available tools:\`, tools.map(t => t.name).join(', '));
1089
1326
  }
1090
1327
 
1328
+ // Register the application package manifest before resolving generated
1329
+ // object names. Generated servers are commonly run from the application
1330
+ // package itself, which is not a node_modules dependency of its process.
1331
+ const localManifestPaths = [
1332
+ resolve(process.cwd(), 'dist', 'manifest.json'),
1333
+ resolve(process.cwd(), '.smrt', 'manifest.json'),
1334
+ ].filter(existsSync);
1335
+ if (localManifestPaths.length > 0) {
1336
+ ObjectRegistry.loadAllManifests({ manifestPaths: localManifestPaths });
1337
+ }
1338
+
1091
1339
  // Load configuration from environment and .smrt.config files
1092
1340
  const appConfig = await loadConfig();
1093
1341
  const aiConfig = appConfig?.ai || {};
@@ -1102,6 +1350,9 @@ export async function createServer() {
1102
1350
  capabilities: {
1103
1351
  tools: {},
1104
1352
  },
1353
+ cacheHints: {
1354
+ 'tools/list': TOOL_LIST_CACHE_HINT,
1355
+ },
1105
1356
  }
1106
1357
  );
1107
1358
 
@@ -1112,10 +1363,11 @@ export async function createServer() {
1112
1363
  }
1113
1364
 
1114
1365
  return {
1115
- tools: tools.map(tool => ({
1366
+ tools: [...tools].sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0).map(tool => ({
1116
1367
  name: tool.name,
1117
1368
  description: tool.description,
1118
1369
  inputSchema: tool.inputSchema,
1370
+ outputSchema: tool.outputSchema,
1119
1371
  })),
1120
1372
  };
1121
1373
  });
@@ -1163,6 +1415,6 @@ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href)
1163
1415
  }
1164
1416
  };
1165
1417
  //#endregion
1166
- export { MCPGenerator };
1418
+ export { MCPGenerator, MCP_STABLE_CATALOG_TTL_MS, resolveMCPToolListCacheHint, sortMCPTools };
1167
1419
 
1168
1420
  //# sourceMappingURL=mcp.js.map