@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
@@ -9,10 +9,11 @@
9
9
  * failures were therefore invisible to callers — an optimistic-update layer
10
10
  * (e.g. TanStack DB `onInsert`) could never observe the failure and roll back.
11
11
  *
12
- * `SmrtClientError` carries the HTTP `status` and the parsed error `body` (when
13
- * the response was JSON), so callers can branch on status and surface server
14
- * messages. Both helpers are emitted with a `__smrt` prefix so they cannot
15
- * collide with a collection key in the generated module.
12
+ * `SmrtClientError` carries the HTTP `status` and, for non-5xx responses, the
13
+ * parsed error `body`, so callers can branch on status and surface safe server
14
+ * messages. Server-side failures stay opaque. Both helpers are emitted with a
15
+ * `__smrt` prefix so they cannot collide with a collection key in the generated
16
+ * module.
16
17
  *
17
18
  * WIRE-SHAPE POLICY (#1797, ADR 0001): the server returns BARE JSON — a bare
18
19
  * array for list, a bare object for get/create/update — with snake_case field
@@ -22,5 +23,5 @@
22
23
  * (vite-plugin + prebuild) are written to match this shape. This same
23
24
  * snake_case wire feeds the mobile DTO codegen; do not change the wire here.
24
25
  */
25
- export declare const CLIENT_FETCH_RUNTIME = "class SmrtClientError extends Error {\n constructor(message, status, body) {\n super(message);\n this.name = 'SmrtClientError';\n this.status = status;\n this.body = body;\n }\n}\n\nasync function __smrtParseBody(response) {\n const contentType = response.headers.get('content-type') || '';\n if (contentType.includes('application/json')) {\n try {\n return await response.json();\n } catch {\n return undefined;\n }\n }\n try {\n return await response.text();\n } catch {\n return undefined;\n }\n}\n\n// Rejects on !response.ok with a typed SmrtClientError; otherwise resolves the\n// parsed JSON body exactly as the server sent it (bare array/object, snake_case\n// fields).\nasync function __smrtFetchJson(url, init) {\n const response = await fetch(url, init);\n const body = await __smrtParseBody(response);\n if (!response.ok) {\n const detail =\n body && typeof body === 'object' && body.error ? ': ' + body.error : '';\n throw new SmrtClientError(\n 'Request failed with status ' + response.status + detail,\n response.status,\n body,\n );\n }\n return body;\n}\n\n// DELETE variant: rejects on !response.ok (so a failed delete is observable),\n// otherwise resolves true.\nasync function __smrtFetchOk(url, init) {\n const response = await fetch(url, init);\n if (!response.ok) {\n const body = await __smrtParseBody(response);\n const detail =\n body && typeof body === 'object' && body.error ? ': ' + body.error : '';\n throw new SmrtClientError(\n 'Request failed with status ' + response.status + detail,\n response.status,\n body,\n );\n }\n return true;\n}";
26
+ export declare const CLIENT_FETCH_RUNTIME = "class SmrtClientError extends Error {\n constructor(message, status, body, code) {\n super(message);\n this.name = 'SmrtClientError';\n this.status = status;\n if (body !== undefined) this.body = body;\n if (code) this.code = code;\n }\n}\n\nasync function __smrtParseBody(response) {\n const contentType = response.headers.get('content-type') || '';\n if (contentType.includes('application/json')) {\n try {\n return await response.json();\n } catch {\n return undefined;\n }\n }\n try {\n return await response.text();\n } catch {\n return undefined;\n }\n}\n\n// Generated REST routes retain the legacy { error: string } form while custom\n// action failures use { error: { ok: false, code, message, status? } }. Keep\n// the nested message human-readable and retain its machine-readable code.\nfunction __smrtErrorDetail(body) {\n if (!body || typeof body !== 'object') return {};\n const error = body.error;\n if (typeof error === 'string') return { message: error };\n if (error && typeof error === 'object') {\n return {\n message: typeof error.message === 'string' ? error.message : undefined,\n code: typeof error.code === 'string' ? error.code : undefined,\n };\n }\n return typeof body.message === 'string' ? { message: body.message } : {};\n}\n\n// Rejects on !response.ok with a typed SmrtClientError; otherwise resolves the\n// parsed JSON body exactly as the server sent it (bare array/object, snake_case\n// fields).\nasync function __smrtFetchJson(url, init) {\n const response = await fetch(url, init);\n if (!response.ok) {\n // Server-side failures may carry stack traces or upstream payloads. Do not\n // parse, interpolate, or expose them to browser callers.\n if (response.status >= 500) {\n throw new SmrtClientError(\n 'Request failed with status ' + response.status,\n response.status,\n );\n }\n const body = await __smrtParseBody(response);\n const detail = __smrtErrorDetail(body);\n throw new SmrtClientError(\n 'Request failed with status ' + response.status +\n (detail.message ? ': ' + detail.message : ''),\n response.status,\n body,\n detail.code,\n );\n }\n return __smrtParseBody(response);\n}\n\n// DELETE variant: rejects on !response.ok (so a failed delete is observable),\n// otherwise resolves true.\nasync function __smrtFetchOk(url, init) {\n const response = await fetch(url, init);\n if (!response.ok) {\n // Keep 5xx failures opaque for the same reason as __smrtFetchJson.\n if (response.status >= 500) {\n throw new SmrtClientError(\n 'Request failed with status ' + response.status,\n response.status,\n );\n }\n const body = await __smrtParseBody(response);\n const detail = __smrtErrorDetail(body);\n throw new SmrtClientError(\n 'Request failed with status ' + response.status +\n (detail.message ? ': ' + detail.message : ''),\n response.status,\n body,\n detail.code,\n );\n }\n return true;\n}";
26
27
  //# sourceMappingURL=generated-client-runtime.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"generated-client-runtime.d.ts","sourceRoot":"","sources":["../src/generated-client-runtime.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,oBAAoB,4pDA0D/B,CAAC"}
1
+ {"version":3,"file":"generated-client-runtime.d.ts","sourceRoot":"","sources":["../src/generated-client-runtime.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,oBAAoB,sgGA4F/B,CAAC"}
@@ -10,10 +10,11 @@
10
10
  * failures were therefore invisible to callers — an optimistic-update layer
11
11
  * (e.g. TanStack DB `onInsert`) could never observe the failure and roll back.
12
12
  *
13
- * `SmrtClientError` carries the HTTP `status` and the parsed error `body` (when
14
- * the response was JSON), so callers can branch on status and surface server
15
- * messages. Both helpers are emitted with a `__smrt` prefix so they cannot
16
- * collide with a collection key in the generated module.
13
+ * `SmrtClientError` carries the HTTP `status` and, for non-5xx responses, the
14
+ * parsed error `body`, so callers can branch on status and surface safe server
15
+ * messages. Server-side failures stay opaque. Both helpers are emitted with a
16
+ * `__smrt` prefix so they cannot collide with a collection key in the generated
17
+ * module.
17
18
  *
18
19
  * WIRE-SHAPE POLICY (#1797, ADR 0001): the server returns BARE JSON — a bare
19
20
  * array for list, a bare object for get/create/update — with snake_case field
@@ -24,11 +25,12 @@
24
25
  * snake_case wire feeds the mobile DTO codegen; do not change the wire here.
25
26
  */
26
27
  var CLIENT_FETCH_RUNTIME = `class SmrtClientError extends Error {
27
- constructor(message, status, body) {
28
+ constructor(message, status, body, code) {
28
29
  super(message);
29
30
  this.name = 'SmrtClientError';
30
31
  this.status = status;
31
- this.body = body;
32
+ if (body !== undefined) this.body = body;
33
+ if (code) this.code = code;
32
34
  }
33
35
  }
34
36
 
@@ -48,22 +50,47 @@ async function __smrtParseBody(response) {
48
50
  }
49
51
  }
50
52
 
53
+ // Generated REST routes retain the legacy { error: string } form while custom
54
+ // action failures use { error: { ok: false, code, message, status? } }. Keep
55
+ // the nested message human-readable and retain its machine-readable code.
56
+ function __smrtErrorDetail(body) {
57
+ if (!body || typeof body !== 'object') return {};
58
+ const error = body.error;
59
+ if (typeof error === 'string') return { message: error };
60
+ if (error && typeof error === 'object') {
61
+ return {
62
+ message: typeof error.message === 'string' ? error.message : undefined,
63
+ code: typeof error.code === 'string' ? error.code : undefined,
64
+ };
65
+ }
66
+ return typeof body.message === 'string' ? { message: body.message } : {};
67
+ }
68
+
51
69
  // Rejects on !response.ok with a typed SmrtClientError; otherwise resolves the
52
70
  // parsed JSON body exactly as the server sent it (bare array/object, snake_case
53
71
  // fields).
54
72
  async function __smrtFetchJson(url, init) {
55
73
  const response = await fetch(url, init);
56
- const body = await __smrtParseBody(response);
57
74
  if (!response.ok) {
58
- const detail =
59
- body && typeof body === 'object' && body.error ? ': ' + body.error : '';
75
+ // Server-side failures may carry stack traces or upstream payloads. Do not
76
+ // parse, interpolate, or expose them to browser callers.
77
+ if (response.status >= 500) {
78
+ throw new SmrtClientError(
79
+ 'Request failed with status ' + response.status,
80
+ response.status,
81
+ );
82
+ }
83
+ const body = await __smrtParseBody(response);
84
+ const detail = __smrtErrorDetail(body);
60
85
  throw new SmrtClientError(
61
- 'Request failed with status ' + response.status + detail,
86
+ 'Request failed with status ' + response.status +
87
+ (detail.message ? ': ' + detail.message : ''),
62
88
  response.status,
63
89
  body,
90
+ detail.code,
64
91
  );
65
92
  }
66
- return body;
93
+ return __smrtParseBody(response);
67
94
  }
68
95
 
69
96
  // DELETE variant: rejects on !response.ok (so a failed delete is observable),
@@ -71,13 +98,21 @@ async function __smrtFetchJson(url, init) {
71
98
  async function __smrtFetchOk(url, init) {
72
99
  const response = await fetch(url, init);
73
100
  if (!response.ok) {
101
+ // Keep 5xx failures opaque for the same reason as __smrtFetchJson.
102
+ if (response.status >= 500) {
103
+ throw new SmrtClientError(
104
+ 'Request failed with status ' + response.status,
105
+ response.status,
106
+ );
107
+ }
74
108
  const body = await __smrtParseBody(response);
75
- const detail =
76
- body && typeof body === 'object' && body.error ? ': ' + body.error : '';
109
+ const detail = __smrtErrorDetail(body);
77
110
  throw new SmrtClientError(
78
- 'Request failed with status ' + response.status + detail,
111
+ 'Request failed with status ' + response.status +
112
+ (detail.message ? ': ' + detail.message : ''),
79
113
  response.status,
80
114
  body,
115
+ detail.code,
81
116
  );
82
117
  }
83
118
  return true;
@@ -1 +1 @@
1
- {"version":3,"file":"generated-client-runtime.js","names":[],"sources":["../src/generated-client-runtime.ts"],"sourcesContent":["/**\n * Shared runtime source injected into every generated API client\n * (`smrtPlugin`'s `@happyvertical/smrt-virt-client` and `smrtConsumer`'s\n * `@smrt/client` fallback).\n *\n * The generated fetchers MUST reject on `!response.ok` (#1796): before this,\n * every fetcher resolved `r.json()` regardless of HTTP status, so a 500 (or any\n * error response) resolved successfully with the error payload. Mutation\n * failures were therefore invisible to callers — an optimistic-update layer\n * (e.g. TanStack DB `onInsert`) could never observe the failure and roll back.\n *\n * `SmrtClientError` carries the HTTP `status` and the parsed error `body` (when\n * the response was JSON), so callers can branch on status and surface server\n * messages. Both helpers are emitted with a `__smrt` prefix so they cannot\n * collide with a collection key in the generated module.\n *\n * WIRE-SHAPE POLICY (#1797, ADR 0001): the server returns BARE JSON — a bare\n * array for list, a bare object for get/create/update — with snake_case field\n * names (`created_at`, `updated_at`) exactly as `SmrtObject.toJSON()` emits\n * them. These helpers pass that JSON through unchanged; they do NOT wrap it in\n * an envelope or camelCase the keys. The generated `.d.ts` declarations\n * (vite-plugin + prebuild) are written to match this shape. This same\n * snake_case wire feeds the mobile DTO codegen; do not change the wire here.\n */\nexport const CLIENT_FETCH_RUNTIME = `class SmrtClientError extends Error {\n constructor(message, status, body) {\n super(message);\n this.name = 'SmrtClientError';\n this.status = status;\n this.body = body;\n }\n}\n\nasync function __smrtParseBody(response) {\n const contentType = response.headers.get('content-type') || '';\n if (contentType.includes('application/json')) {\n try {\n return await response.json();\n } catch {\n return undefined;\n }\n }\n try {\n return await response.text();\n } catch {\n return undefined;\n }\n}\n\n// Rejects on !response.ok with a typed SmrtClientError; otherwise resolves the\n// parsed JSON body exactly as the server sent it (bare array/object, snake_case\n// fields).\nasync function __smrtFetchJson(url, init) {\n const response = await fetch(url, init);\n const body = await __smrtParseBody(response);\n if (!response.ok) {\n const detail =\n body && typeof body === 'object' && body.error ? ': ' + body.error : '';\n throw new SmrtClientError(\n 'Request failed with status ' + response.status + detail,\n response.status,\n body,\n );\n }\n return body;\n}\n\n// DELETE variant: rejects on !response.ok (so a failed delete is observable),\n// otherwise resolves true.\nasync function __smrtFetchOk(url, init) {\n const response = await fetch(url, init);\n if (!response.ok) {\n const body = await __smrtParseBody(response);\n const detail =\n body && typeof body === 'object' && body.error ? ': ' + body.error : '';\n throw new SmrtClientError(\n 'Request failed with status ' + response.status + detail,\n response.status,\n body,\n );\n }\n return true;\n}`;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAwBA,IAAa,uBAAuB"}
1
+ {"version":3,"file":"generated-client-runtime.js","names":[],"sources":["../src/generated-client-runtime.ts"],"sourcesContent":["/**\n * Shared runtime source injected into every generated API client\n * (`smrtPlugin`'s `@happyvertical/smrt-virt-client` and `smrtConsumer`'s\n * `@smrt/client` fallback).\n *\n * The generated fetchers MUST reject on `!response.ok` (#1796): before this,\n * every fetcher resolved `r.json()` regardless of HTTP status, so a 500 (or any\n * error response) resolved successfully with the error payload. Mutation\n * failures were therefore invisible to callers — an optimistic-update layer\n * (e.g. TanStack DB `onInsert`) could never observe the failure and roll back.\n *\n * `SmrtClientError` carries the HTTP `status` and, for non-5xx responses, the\n * parsed error `body`, so callers can branch on status and surface safe server\n * messages. Server-side failures stay opaque. Both helpers are emitted with a\n * `__smrt` prefix so they cannot collide with a collection key in the generated\n * module.\n *\n * WIRE-SHAPE POLICY (#1797, ADR 0001): the server returns BARE JSON — a bare\n * array for list, a bare object for get/create/update — with snake_case field\n * names (`created_at`, `updated_at`) exactly as `SmrtObject.toJSON()` emits\n * them. These helpers pass that JSON through unchanged; they do NOT wrap it in\n * an envelope or camelCase the keys. The generated `.d.ts` declarations\n * (vite-plugin + prebuild) are written to match this shape. This same\n * snake_case wire feeds the mobile DTO codegen; do not change the wire here.\n */\nexport const CLIENT_FETCH_RUNTIME = `class SmrtClientError extends Error {\n constructor(message, status, body, code) {\n super(message);\n this.name = 'SmrtClientError';\n this.status = status;\n if (body !== undefined) this.body = body;\n if (code) this.code = code;\n }\n}\n\nasync function __smrtParseBody(response) {\n const contentType = response.headers.get('content-type') || '';\n if (contentType.includes('application/json')) {\n try {\n return await response.json();\n } catch {\n return undefined;\n }\n }\n try {\n return await response.text();\n } catch {\n return undefined;\n }\n}\n\n// Generated REST routes retain the legacy { error: string } form while custom\n// action failures use { error: { ok: false, code, message, status? } }. Keep\n// the nested message human-readable and retain its machine-readable code.\nfunction __smrtErrorDetail(body) {\n if (!body || typeof body !== 'object') return {};\n const error = body.error;\n if (typeof error === 'string') return { message: error };\n if (error && typeof error === 'object') {\n return {\n message: typeof error.message === 'string' ? error.message : undefined,\n code: typeof error.code === 'string' ? error.code : undefined,\n };\n }\n return typeof body.message === 'string' ? { message: body.message } : {};\n}\n\n// Rejects on !response.ok with a typed SmrtClientError; otherwise resolves the\n// parsed JSON body exactly as the server sent it (bare array/object, snake_case\n// fields).\nasync function __smrtFetchJson(url, init) {\n const response = await fetch(url, init);\n if (!response.ok) {\n // Server-side failures may carry stack traces or upstream payloads. Do not\n // parse, interpolate, or expose them to browser callers.\n if (response.status >= 500) {\n throw new SmrtClientError(\n 'Request failed with status ' + response.status,\n response.status,\n );\n }\n const body = await __smrtParseBody(response);\n const detail = __smrtErrorDetail(body);\n throw new SmrtClientError(\n 'Request failed with status ' + response.status +\n (detail.message ? ': ' + detail.message : ''),\n response.status,\n body,\n detail.code,\n );\n }\n return __smrtParseBody(response);\n}\n\n// DELETE variant: rejects on !response.ok (so a failed delete is observable),\n// otherwise resolves true.\nasync function __smrtFetchOk(url, init) {\n const response = await fetch(url, init);\n if (!response.ok) {\n // Keep 5xx failures opaque for the same reason as __smrtFetchJson.\n if (response.status >= 500) {\n throw new SmrtClientError(\n 'Request failed with status ' + response.status,\n response.status,\n );\n }\n const body = await __smrtParseBody(response);\n const detail = __smrtErrorDetail(body);\n throw new SmrtClientError(\n 'Request failed with status ' + response.status +\n (detail.message ? ': ' + detail.message : ''),\n response.status,\n body,\n detail.code,\n );\n }\n return true;\n}`;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,IAAa,uBAAuB"}
@@ -6,11 +6,12 @@ export { CLIGenerator, getCLIHandler, setupCLI } from './cli';
6
6
  export { canonicalReadRepresentation, computeBodyEtag, computeTableVersionEtag, conditionalJsonResponse, ifNoneMatchHasConcreteMatch, ifNoneMatchSatisfied, PRIVATE_READ_CACHE_CONTROL, type ReadCacheControlOptions, resolveReadCacheControl, resolveTenantEtagDiscriminator, versionConditionalResponse, warnIfSharedCacheNeutralized, } from './conditional-get';
7
7
  export { buildCustomActionInputSchema, buildCustomActionInvocationArgs, type CustomActionFailure, type CustomActionMetadata, type CustomActionScope, customActionParameterInputName, normalizeCustomActionFailure, type ResolveCustomActionMetadataOptions, resolveCustomActionMetadata, SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY, } from './custom-action';
8
8
  export { buildChangeEventStream, type ChangeEventStreamOptions, changeEventSubscribersAtCapacity, DEFAULT_EVENTS_HEARTBEAT_MS, DEFAULT_EVENTS_MAX_SUBSCRIBERS, DEFAULT_EVENTS_RETRY_AFTER_SECONDS, eventStreamCapacityExceededResponse, normalizeEventsMaxSubscribers, signalVisibleToTenant, tryReserveChangeEventSubscriberSlot, } from './events-route';
9
- export type { MCPConfig, MCPContext, MCPRequest, MCPResponse, MCPTool, } from './mcp';
10
- export { MCPGenerator } from './mcp';
9
+ export type { MCPConfig, MCPContext, MCPRequest, MCPResponse, MCPTool, MCPToolListCacheHint, MCPToolListCacheOptions, } from './mcp';
10
+ export { MCP_STABLE_CATALOG_TTL_MS, MCPGenerator, resolveMCPToolListCacheHint, sortMCPTools, } from './mcp';
11
11
  export type { APIConfig, APIContext, RestServerConfig } from './rest';
12
12
  export { APIGenerator, computeRuntimeWebManifestHash, createRestServer, startRestServer, } from './rest';
13
13
  export type { OpenAPIConfig } from './swagger';
14
14
  export { generateOpenAPISpec, setupSwaggerUI, } from './swagger';
15
15
  export { runWithTenantGate, setTenantEntryPointRunner, type TenantEntryPointRunner, type TenantGateOptions, } from './tenant-gate';
16
+ export { normalizeTypedHttpError, type TypedHttpFailure, } from './typed-http-error';
16
17
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/generators/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AAEnD,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAG9D,OAAO,EACL,2BAA2B,EAC3B,eAAe,EACf,uBAAuB,EACvB,uBAAuB,EACvB,2BAA2B,EAC3B,oBAAoB,EACpB,0BAA0B,EAC1B,KAAK,uBAAuB,EAC5B,uBAAuB,EACvB,8BAA8B,EAC9B,0BAA0B,EAC1B,4BAA4B,GAC7B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,4BAA4B,EAC5B,+BAA+B,EAC/B,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EACtB,8BAA8B,EAC9B,4BAA4B,EAC5B,KAAK,kCAAkC,EACvC,2BAA2B,EAC3B,qCAAqC,GACtC,MAAM,iBAAiB,CAAC;AAKzB,OAAO,EACL,sBAAsB,EACtB,KAAK,wBAAwB,EAC7B,gCAAgC,EAChC,2BAA2B,EAC3B,8BAA8B,EAC9B,kCAAkC,EAClC,mCAAmC,EACnC,6BAA6B,EAC7B,qBAAqB,EACrB,mCAAmC,GACpC,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,SAAS,EACT,UAAU,EACV,UAAU,EACV,WAAW,EACX,OAAO,GACR,MAAM,OAAO,CAAC;AAEf,OAAO,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AACrC,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAEtE,OAAO,EACL,YAAY,EACZ,6BAA6B,EAC7B,gBAAgB,EAChB,eAAe,GAChB,MAAM,QAAQ,CAAC;AAChB,YAAY,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE/C,OAAO,EACL,mBAAmB,EACnB,cAAc,GACf,MAAM,WAAW,CAAC;AAEnB,OAAO,EACL,iBAAiB,EACjB,yBAAyB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,GACvB,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/generators/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AAEnD,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAG9D,OAAO,EACL,2BAA2B,EAC3B,eAAe,EACf,uBAAuB,EACvB,uBAAuB,EACvB,2BAA2B,EAC3B,oBAAoB,EACpB,0BAA0B,EAC1B,KAAK,uBAAuB,EAC5B,uBAAuB,EACvB,8BAA8B,EAC9B,0BAA0B,EAC1B,4BAA4B,GAC7B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,4BAA4B,EAC5B,+BAA+B,EAC/B,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EACtB,8BAA8B,EAC9B,4BAA4B,EAC5B,KAAK,kCAAkC,EACvC,2BAA2B,EAC3B,qCAAqC,GACtC,MAAM,iBAAiB,CAAC;AAKzB,OAAO,EACL,sBAAsB,EACtB,KAAK,wBAAwB,EAC7B,gCAAgC,EAChC,2BAA2B,EAC3B,8BAA8B,EAC9B,kCAAkC,EAClC,mCAAmC,EACnC,6BAA6B,EAC7B,qBAAqB,EACrB,mCAAmC,GACpC,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,SAAS,EACT,UAAU,EACV,UAAU,EACV,WAAW,EACX,OAAO,EACP,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,OAAO,CAAC;AAEf,OAAO,EACL,yBAAyB,EACzB,YAAY,EACZ,2BAA2B,EAC3B,YAAY,GACb,MAAM,OAAO,CAAC;AACf,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAEtE,OAAO,EACL,YAAY,EACZ,6BAA6B,EAC7B,gBAAgB,EAChB,eAAe,GAChB,MAAM,QAAQ,CAAC;AAChB,YAAY,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE/C,OAAO,EACL,mBAAmB,EACnB,cAAc,GACf,MAAM,WAAW,CAAC;AAEnB,OAAO,EACL,iBAAiB,EACjB,yBAAyB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,GACvB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,uBAAuB,EACvB,KAAK,gBAAgB,GACtB,MAAM,oBAAoB,CAAC"}
@@ -3,7 +3,8 @@ import { runWithTenantGate, setTenantEntryPointRunner } from "./tenant-gate.js";
3
3
  import { CLIGenerator, getCLIHandler, setupCLI } from "./cli.js";
4
4
  import { PRIVATE_READ_CACHE_CONTROL, canonicalReadRepresentation, computeBodyEtag, computeTableVersionEtag, conditionalJsonResponse, ifNoneMatchHasConcreteMatch, ifNoneMatchSatisfied, resolveReadCacheControl, resolveTenantEtagDiscriminator, versionConditionalResponse, warnIfSharedCacheNeutralized } from "./conditional-get.js";
5
5
  import { DEFAULT_EVENTS_HEARTBEAT_MS, DEFAULT_EVENTS_MAX_SUBSCRIBERS, DEFAULT_EVENTS_RETRY_AFTER_SECONDS, buildChangeEventStream, changeEventSubscribersAtCapacity, eventStreamCapacityExceededResponse, normalizeEventsMaxSubscribers, signalVisibleToTenant, tryReserveChangeEventSubscriberSlot } from "./events-route.js";
6
- import { MCPGenerator } from "./mcp.js";
6
+ import { MCPGenerator, MCP_STABLE_CATALOG_TTL_MS, resolveMCPToolListCacheHint, sortMCPTools } from "./mcp.js";
7
+ import { normalizeTypedHttpError } from "./typed-http-error.js";
7
8
  import { APIGenerator, computeRuntimeWebManifestHash, createRestServer, startRestServer } from "./rest.js";
8
9
  import { generateOpenAPISpec, setupSwaggerUI } from "./swagger.js";
9
- export { APIGenerator, CLIGenerator, DEFAULT_EVENTS_HEARTBEAT_MS, DEFAULT_EVENTS_MAX_SUBSCRIBERS, DEFAULT_EVENTS_RETRY_AFTER_SECONDS, MCPGenerator, PRIVATE_READ_CACHE_CONTROL, SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY, buildChangeEventStream, buildCustomActionInputSchema, buildCustomActionInvocationArgs, canonicalReadRepresentation, changeEventSubscribersAtCapacity, computeBodyEtag, computeRuntimeWebManifestHash, computeTableVersionEtag, conditionalJsonResponse, createRestServer, customActionParameterInputName, eventStreamCapacityExceededResponse, generateOpenAPISpec, getCLIHandler, ifNoneMatchHasConcreteMatch, ifNoneMatchSatisfied, normalizeCustomActionFailure, normalizeEventsMaxSubscribers, resolveCustomActionMetadata, resolveReadCacheControl, resolveTenantEtagDiscriminator, runWithTenantGate, setTenantEntryPointRunner, setupCLI, setupSwaggerUI, signalVisibleToTenant, startRestServer, tryReserveChangeEventSubscriberSlot, versionConditionalResponse, warnIfSharedCacheNeutralized };
10
+ export { APIGenerator, CLIGenerator, DEFAULT_EVENTS_HEARTBEAT_MS, DEFAULT_EVENTS_MAX_SUBSCRIBERS, DEFAULT_EVENTS_RETRY_AFTER_SECONDS, MCPGenerator, MCP_STABLE_CATALOG_TTL_MS, PRIVATE_READ_CACHE_CONTROL, SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY, buildChangeEventStream, buildCustomActionInputSchema, buildCustomActionInvocationArgs, canonicalReadRepresentation, changeEventSubscribersAtCapacity, computeBodyEtag, computeRuntimeWebManifestHash, computeTableVersionEtag, conditionalJsonResponse, createRestServer, customActionParameterInputName, eventStreamCapacityExceededResponse, generateOpenAPISpec, getCLIHandler, ifNoneMatchHasConcreteMatch, ifNoneMatchSatisfied, normalizeCustomActionFailure, normalizeEventsMaxSubscribers, normalizeTypedHttpError, resolveCustomActionMetadata, resolveMCPToolListCacheHint, resolveReadCacheControl, resolveTenantEtagDiscriminator, runWithTenantGate, setTenantEntryPointRunner, setupCLI, setupSwaggerUI, signalVisibleToTenant, sortMCPTools, startRestServer, tryReserveChangeEventSubscriberSlot, versionConditionalResponse, warnIfSharedCacheNeutralized };
@@ -18,7 +18,13 @@ export interface RuntimeOptions {
18
18
  name: string;
19
19
  description: string;
20
20
  inputSchema: Record<string, unknown>;
21
+ outputSchema?: Record<string, unknown>;
21
22
  }>;
23
+ /** Cache hint emitted for deploy-static tools/list results. */
24
+ toolListCacheHint?: {
25
+ ttlMs: number;
26
+ cacheScope: 'private' | 'public';
27
+ };
22
28
  /** Internal invocation metadata; never exposed by the MCP tools/list result. */
23
29
  customActions?: Record<string, {
24
30
  scope: CustomActionScope;
@@ -37,6 +43,13 @@ export interface RuntimeOptions {
37
43
  * `SMRT_MCP_ALLOW_CROSS_TENANT` env vars (this server has no auth principal).
38
44
  */
39
45
  tenantScopedObjects?: string[];
46
+ /**
47
+ * Build-time approved STI discriminators, keyed by the lowercased MCP object
48
+ * prefix. The generated runtime uses this instead of searching an initially
49
+ * empty registry, then loads the approved qualified type through the public
50
+ * collection API.
51
+ */
52
+ stiTargets?: Record<string, Record<string, string>>;
40
53
  }
41
54
  /**
42
55
  * Generate runtime bootstrap code for MCP server
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-runtime-template.d.ts","sourceRoot":"","sources":["../../src/generators/mcp-runtime-template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAStD,MAAM,WAAW,cAAc;IAC7B,6CAA6C;IAC7C,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,yBAAyB;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,kCAAkC;IAClC,MAAM,CAAC,EAAE,SAAS,CAAC;IAEnB,8CAA8C;IAC9C,OAAO,CAAC,EAAE,UAAU,CAAC;IAErB,2BAA2B;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB,wDAAwD;IACxD,KAAK,CAAC,EAAE,KAAK,CAAC;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACtC,CAAC,CAAC;IACH,gFAAgF;IAChF,aAAa,CAAC,EAAE,MAAM,CACpB,MAAM,EACN;QACE,KAAK,EAAE,iBAAiB,CAAC;QACzB,QAAQ,EAAE,OAAO,CAAC;QAClB,sEAAsE;QACtE,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;QAC1B,gBAAgB,CAAC,EAAE,OAAO,CAAC;QAC3B,aAAa,EAAE,OAAO,CAAC;KACxB,CACF,CAAC;IAEF;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;CAChC;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,GAAE,cAAmB,GAAG,MAAM,CAib7E;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,GAAE,MAA6B,GACxC,MAAM,CAER;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,MAAM,CASR;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,MAAM,CA0GR"}
1
+ {"version":3,"file":"mcp-runtime-template.d.ts","sourceRoot":"","sources":["../../src/generators/mcp-runtime-template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAStD,MAAM,WAAW,cAAc;IAC7B,6CAA6C;IAC7C,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,yBAAyB;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,kCAAkC;IAClC,MAAM,CAAC,EAAE,SAAS,CAAC;IAEnB,8CAA8C;IAC9C,OAAO,CAAC,EAAE,UAAU,CAAC;IAErB,2BAA2B;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB,wDAAwD;IACxD,KAAK,CAAC,EAAE,KAAK,CAAC;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACrC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACxC,CAAC,CAAC;IACH,+DAA+D;IAC/D,iBAAiB,CAAC,EAAE;QAClB,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,SAAS,GAAG,QAAQ,CAAC;KAClC,CAAC;IACF,gFAAgF;IAChF,aAAa,CAAC,EAAE,MAAM,CACpB,MAAM,EACN;QACE,KAAK,EAAE,iBAAiB,CAAC;QACzB,QAAQ,EAAE,OAAO,CAAC;QAClB,sEAAsE;QACtE,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;QAC1B,gBAAgB,CAAC,EAAE,OAAO,CAAC;QAC3B,aAAa,EAAE,OAAO,CAAC;KACxB,CACF,CAAC;IAEF;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAE/B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACrD;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,GAAE,cAAmB,GAAG,MAAM,CAme7E;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,GAAE,MAA6B,GACxC,MAAM,CAER;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,MAAM,CASR;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,MAAM,CA0GR"}
@@ -12,7 +12,10 @@ function capitalize(str) {
12
12
  * @returns TypeScript code for server entry point
13
13
  */
14
14
  function generateRuntimeBootstrap(options = {}) {
15
- const { name = "smrt-mcp-server", version = "1.0.0", description = "Auto-generated MCP server from SMRT objects", debug = false, tools = [], customActions = {}, tenantScopedObjects = [] } = options;
15
+ const { name = "smrt-mcp-server", version = "1.0.0", description = "Auto-generated MCP server from SMRT objects", debug = false, tools = [], customActions = {}, tenantScopedObjects = [], stiTargets = {}, toolListCacheHint = {
16
+ ttlMs: 864e5,
17
+ cacheScope: "private"
18
+ } } = options;
16
19
  const toolsCode = tools.length > 0 ? JSON.stringify(tools, null, 2) : "[]";
17
20
  const tenantScopedSet = Array.from(new Set(tenantScopedObjects.map((n) => n.toLowerCase())));
18
21
  const hasTenantScoped = tenantScopedSet.length > 0;
@@ -28,13 +31,17 @@ ${indent} const offset = args.offset ?? 0;
28
31
  ${indent} const where = args.where ?? {};
29
32
 
30
33
  ${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {
31
- ${indent} persistence: { type: 'sql', url: process.env.DATABASE_URL || ':memory:' },
34
+ ${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },
32
35
  ${indent} ai: aiConfig
33
36
  ${indent} });
34
37
 
35
38
  ${indent} const items = await collection.list({ where, limit, offset });
36
39
  ${indent} const itemsPublic = items.map((item) => item.toPublicJSON(PUBLIC_JSON_OPTIONS));
37
- ${indent} return { content: [{ type: 'text', text: JSON.stringify(itemsPublic) }] };
40
+ ${indent} const structuredContent = {
41
+ ${indent} data: itemsPublic,
42
+ ${indent} meta: { total: await collection.count({ where }), limit, offset, count: items.length },
43
+ ${indent} };
44
+ ${indent} return successResult(structuredContent, JSON.stringify(itemsPublic));
38
45
  ${indent}}`;
39
46
  case "get": return `${indent}case '${tool.name}': {
40
47
  ${indent} if (!args.id && !args.slug) {
@@ -42,7 +49,7 @@ ${indent} throw new Error('Either id or slug is required');
42
49
  ${indent} }
43
50
 
44
51
  ${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {
45
- ${indent} persistence: { type: 'sql', url: process.env.DATABASE_URL || ':memory:' },
52
+ ${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },
46
53
  ${indent} ai: aiConfig
47
54
  ${indent} });
48
55
 
@@ -53,18 +60,15 @@ ${indent} if (!item) {
53
60
  ${indent} throw new Error('Object not found');
54
61
  ${indent} }
55
62
 
56
- ${indent} return { content: [{ type: 'text', text: JSON.stringify(item.toPublicJSON(PUBLIC_JSON_OPTIONS)) }] };
63
+ ${indent} return successResult(item.toPublicJSON(PUBLIC_JSON_OPTIONS));
57
64
  ${indent}}`;
58
65
  case "create": return `${indent}case '${tool.name}': {
59
- ${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {
60
- ${indent} persistence: { type: 'sql', url: process.env.DATABASE_URL || ':memory:' },
61
- ${indent} ai: aiConfig
62
- ${indent} });
66
+ ${indent} const { collection, objectName: targetObjectName } = await resolveCreateTarget('${objectName}', args, aiConfig);
63
67
 
64
- ${indent} const newItem = await collection.create(applyWritablePolicy('${capitalize(objectName)}', args));
68
+ ${indent} const newItem = await collection.create(applyWritablePolicy(targetObjectName, args));
65
69
  ${indent} await newItem.save();
66
70
 
67
- ${indent} return { content: [{ type: 'text', text: JSON.stringify(newItem.toPublicJSON(PUBLIC_JSON_OPTIONS)) }] };
71
+ ${indent} return successResult(newItem.toPublicJSON(PUBLIC_JSON_OPTIONS));
68
72
  ${indent}}`;
69
73
  case "update": return `${indent}case '${tool.name}': {
70
74
  ${indent} const { id, ...updateData } = args;
@@ -73,7 +77,7 @@ ${indent} throw new Error('ID is required for update');
73
77
  ${indent} }
74
78
 
75
79
  ${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {
76
- ${indent} persistence: { type: 'sql', url: process.env.DATABASE_URL || ':memory:' },
80
+ ${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },
77
81
  ${indent} ai: aiConfig
78
82
  ${indent} });
79
83
 
@@ -85,7 +89,7 @@ ${indent} }
85
89
  ${indent} Object.assign(existing, applyWritablePolicy('${capitalize(objectName)}', updateData));
86
90
  ${indent} await existing.save();
87
91
 
88
- ${indent} return { content: [{ type: 'text', text: JSON.stringify(existing.toPublicJSON(PUBLIC_JSON_OPTIONS)) }] };
92
+ ${indent} return successResult(existing.toPublicJSON(PUBLIC_JSON_OPTIONS));
89
93
  ${indent}}`;
90
94
  case "delete": return `${indent}case '${tool.name}': {
91
95
  ${indent} if (!args.id) {
@@ -93,7 +97,7 @@ ${indent} throw new Error('ID is required for delete');
93
97
  ${indent} }
94
98
 
95
99
  ${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {
96
- ${indent} persistence: { type: 'sql', url: process.env.DATABASE_URL || ':memory:' },
100
+ ${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },
97
101
  ${indent} ai: aiConfig
98
102
  ${indent} });
99
103
 
@@ -104,7 +108,7 @@ ${indent} }
104
108
 
105
109
  ${indent} await toDelete.delete();
106
110
 
107
- ${indent} return { content: [{ type: 'text', text: JSON.stringify({ success: true, message: 'Object deleted successfully' }) }] };
111
+ ${indent} return successResult({ success: true, message: 'Object deleted successfully' });
108
112
  ${indent}}`;
109
113
  default: return `${indent}case '${tool.name}': {
110
114
  ${indent} const actionMeta = CUSTOM_ACTIONS['${tool.name}'] || { scope: 'item', isStatic: false, legacyOptions: true };
@@ -118,7 +122,7 @@ ${indent} throw new Error('Custom action ${action} is collection-scoped and d
118
122
  ${indent} }
119
123
 
120
124
  ${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {
121
- ${indent} persistence: { type: 'sql', url: process.env.DATABASE_URL || ':memory:' },
125
+ ${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },
122
126
  ${indent} ai: aiConfig
123
127
  ${indent} });
124
128
 
@@ -147,14 +151,15 @@ ${indent} ]);
147
151
  ${indent} const result = await actionMethod.call(target, ...methodArgs);
148
152
  ${indent} const failure = normalizeCustomActionFailure(result);
149
153
  ${indent} if (failure) {
150
- ${indent} return {
151
- ${indent} content: [{ type: 'text', text: JSON.stringify({ error: failure }) }],
152
- ${indent} isError: true,
153
- ${indent} _meta: { [SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY]: failure },
154
- ${indent} };
154
+ ${indent} return errorResult(
155
+ ${indent} { error: failure },
156
+ ${indent} JSON.stringify({ error: failure }),
157
+ ${indent} { [SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY]: failure },
158
+ ${indent} );
155
159
  ${indent} }
156
160
 
157
- ${indent} return { content: [{ type: 'text', text: JSON.stringify(toPublicResult(result)) }] };
161
+ ${indent} const publicResult = toPublicResult(result);
162
+ ${indent} return successResult({ data: publicResult }, JSON.stringify(publicResult));
158
163
  ${indent}}`;
159
164
  }
160
165
  }).join("\n\n");
@@ -180,6 +185,8 @@ import {
180
185
  Server,
181
186
  } from '@modelcontextprotocol/server';
182
187
  import { serveStdio } from '@modelcontextprotocol/server/stdio';
188
+ import { existsSync } from 'node:fs';
189
+ import { resolve } from 'node:path';
183
190
  import { pathToFileURL } from 'node:url';
184
191
  import { ObjectRegistry } from '@happyvertical/smrt-core';
185
192
  import { normalizeCustomActionFailure, SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY } from '@happyvertical/smrt-core';
@@ -193,7 +200,9 @@ const DEBUG = ${debug};
193
200
 
194
201
  // Static tool definitions (generated at build time)
195
202
  const TOOLS = ${toolsCode};
203
+ const TOOL_LIST_CACHE_HINT = ${JSON.stringify(toolListCacheHint)};
196
204
  const CUSTOM_ACTIONS = ${JSON.stringify(customActions)};
205
+ const STI_TARGETS: Record<string, Record<string, string>> = ${JSON.stringify(stiTargets)};
197
206
  ${hasTenantScoped ? `
198
207
  // Fail-closed tenant context (#1554): tenant-scoped objects must run inside a
199
208
  // tenant. This stdio server has no auth principal, so the tenant is taken from
@@ -243,6 +252,23 @@ function applyWritablePolicy(objectName: string, data: any): Record<string, any>
243
252
  return result;
244
253
  }
245
254
 
255
+ /** Resolve an advertised STI discriminator to its registered subtype collection. */
256
+ async function resolveCreateTarget(baseObjectName: string, args: Record<string, any>, aiConfig: any) {
257
+ let objectName = baseObjectName;
258
+ const discriminator = args._meta_type;
259
+ const targets = STI_TARGETS[baseObjectName];
260
+ if (typeof discriminator === 'string' && targets) {
261
+ const target = targets[discriminator];
262
+ if (!target) throw new Error('Unknown STI discriminator: ' + discriminator);
263
+ objectName = target;
264
+ }
265
+ const collection = await ObjectRegistry.getCollection(objectName, {
266
+ persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },
267
+ ai: aiConfig,
268
+ });
269
+ return { collection, objectName };
270
+ }
271
+
246
272
  /**
247
273
  * Sensitive-field-safe serialization for custom-action results (#1540).
248
274
  * Recurses through arrays and plain objects so nested SmrtObjects are stripped
@@ -267,6 +293,22 @@ function toPublicResult(value: any, seen: WeakSet<object> = new WeakSet()): any
267
293
  return out;
268
294
  }
269
295
 
296
+ function successResult(structuredContent: any, text = JSON.stringify(structuredContent)) {
297
+ return {
298
+ content: [{ type: 'text', text }],
299
+ structuredContent,
300
+ };
301
+ }
302
+
303
+ function errorResult(structuredContent: any, text: string, _meta?: Record<string, any>) {
304
+ return {
305
+ content: [{ type: 'text', text }],
306
+ isError: true,
307
+ structuredContent,
308
+ ...(_meta ? { _meta } : {}),
309
+ };
310
+ }
311
+
270
312
  /**
271
313
  * Main server startup function
272
314
  */
@@ -282,6 +324,17 @@ ${hasTenantScoped ? `
282
324
  // with no interceptor to enforce it.
283
325
  enableTenancy();
284
326
  ` : ""}
327
+ // Register the application package manifest before resolving generated
328
+ // object names. Generated servers are commonly run from the application
329
+ // package itself, which is not a node_modules dependency of its process.
330
+ const localManifestPaths = [
331
+ resolve(process.cwd(), 'dist', 'manifest.json'),
332
+ resolve(process.cwd(), '.smrt', 'manifest.json'),
333
+ ].filter(existsSync);
334
+ if (localManifestPaths.length > 0) {
335
+ ObjectRegistry.loadAllManifests({ manifestPaths: localManifestPaths });
336
+ }
337
+
285
338
  // Load configuration from environment and .smrt.config files
286
339
  const appConfig = await loadConfig();
287
340
  const aiConfig = appConfig?.ai || {};
@@ -301,6 +354,9 @@ ${hasTenantScoped ? `
301
354
  capabilities: {
302
355
  tools: {},
303
356
  },
357
+ cacheHints: {
358
+ 'tools/list': TOOL_LIST_CACHE_HINT,
359
+ },
304
360
  }
305
361
  );
306
362
 
@@ -311,7 +367,7 @@ ${hasTenantScoped ? `
311
367
  }
312
368
 
313
369
  return {
314
- tools: TOOLS,
370
+ tools: [...TOOLS].sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0),
315
371
  };
316
372
  });
317
373
 
@@ -355,15 +411,10 @@ ${hasTenantScoped ? `
355
411
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
356
412
  console.error(\`[MCP] Tool execution failed: \${toolName}\`, error);
357
413
 
358
- return {
359
- content: [
360
- {
361
- type: 'text',
362
- text: \`Error executing tool \${toolName}: \${errorMessage}\`,
363
- },
364
- ],
365
- isError: true,
366
- };
414
+ return errorResult(
415
+ { error: { message: errorMessage } },
416
+ \`Error executing tool \${toolName}: \${errorMessage}\`,
417
+ );
367
418
  }
368
419
  });
369
420