@happyvertical/smrt-core 0.54.2 → 0.54.3

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 (138) hide show
  1. package/AGENTS.md +6 -1
  2. package/README.md +8 -0
  3. package/agents/change-feed.md +7 -12
  4. package/agents/generators.md +13 -0
  5. package/agents/object-runtime.md +8 -0
  6. package/agents/schema-paths.md +14 -0
  7. package/dist/audit-context.d.ts +4 -0
  8. package/dist/audit-context.d.ts.map +1 -0
  9. package/dist/audit-context.js +16 -0
  10. package/dist/audit-context.js.map +1 -0
  11. package/dist/audit.d.ts +34 -0
  12. package/dist/audit.d.ts.map +1 -0
  13. package/dist/audit.js +63 -0
  14. package/dist/audit.js.map +1 -0
  15. package/dist/browser.d.ts +2 -1
  16. package/dist/browser.d.ts.map +1 -1
  17. package/dist/browser.js +4 -3
  18. package/dist/cascade.d.ts +1 -1
  19. package/dist/cascade.d.ts.map +1 -1
  20. package/dist/cascade.js +10 -7
  21. package/dist/cascade.js.map +1 -1
  22. package/dist/change-feed.js +1 -1
  23. package/dist/collection.d.ts +9 -2
  24. package/dist/collection.d.ts.map +1 -1
  25. package/dist/collection.js +59 -4
  26. package/dist/collection.js.map +1 -1
  27. package/dist/embedded-write-queue.d.ts +2 -0
  28. package/dist/embedded-write-queue.d.ts.map +1 -1
  29. package/dist/embedded-write-queue.js +12 -5
  30. package/dist/embedded-write-queue.js.map +1 -1
  31. package/dist/generators/changes-route.js +1 -1
  32. package/dist/generators/mcp-runtime-template.d.ts +2 -0
  33. package/dist/generators/mcp-runtime-template.d.ts.map +1 -1
  34. package/dist/generators/mcp-runtime-template.js +18 -16
  35. package/dist/generators/mcp-runtime-template.js.map +1 -1
  36. package/dist/generators/mcp-tool-name.d.ts +6 -0
  37. package/dist/generators/mcp-tool-name.d.ts.map +1 -1
  38. package/dist/generators/mcp-tool-name.js.map +1 -1
  39. package/dist/generators/mcp.d.ts +34 -1
  40. package/dist/generators/mcp.d.ts.map +1 -1
  41. package/dist/generators/mcp.js +148 -110
  42. package/dist/generators/mcp.js.map +1 -1
  43. package/dist/generators/rest.d.ts.map +1 -1
  44. package/dist/generators/rest.js +16 -7
  45. package/dist/generators/rest.js.map +1 -1
  46. package/dist/index.d.ts +3 -1
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +9 -7
  49. package/dist/junction.d.ts +29 -10
  50. package/dist/junction.d.ts.map +1 -1
  51. package/dist/junction.js +31 -12
  52. package/dist/junction.js.map +1 -1
  53. package/dist/knowledge.js +1 -1
  54. package/dist/knowledge.js.map +1 -1
  55. package/dist/manifest/manifest-loader.d.ts.map +1 -1
  56. package/dist/manifest/manifest-loader.js +1 -1
  57. package/dist/manifest/manifest-loader.js.map +1 -1
  58. package/dist/manifest/static-manifest.d.ts.map +1 -1
  59. package/dist/manifest/static-manifest.js +106 -9
  60. package/dist/manifest/static-manifest.js.map +1 -1
  61. package/dist/manifest/store.d.ts +8 -0
  62. package/dist/manifest/store.d.ts.map +1 -1
  63. package/dist/manifest/store.js +12 -2
  64. package/dist/manifest/store.js.map +1 -1
  65. package/dist/manifest.json +119 -9
  66. package/dist/migrations/differ.d.ts +3 -3
  67. package/dist/migrations/differ.d.ts.map +1 -1
  68. package/dist/migrations/differ.js +8 -4
  69. package/dist/migrations/differ.js.map +1 -1
  70. package/dist/object.d.ts +1 -0
  71. package/dist/object.d.ts.map +1 -1
  72. package/dist/object.js +19 -7
  73. package/dist/object.js.map +1 -1
  74. package/dist/registry/class-registration.d.ts.map +1 -1
  75. package/dist/registry/class-registration.js +75 -10
  76. package/dist/registry/class-registration.js.map +1 -1
  77. package/dist/registry/collection-resolution.d.ts +2 -2
  78. package/dist/registry/collection-resolution.d.ts.map +1 -1
  79. package/dist/registry/collection-resolution.js +2 -1
  80. package/dist/registry/collection-resolution.js.map +1 -1
  81. package/dist/registry/collision-policy.d.ts +8 -0
  82. package/dist/registry/collision-policy.d.ts.map +1 -1
  83. package/dist/registry/collision-policy.js +2 -2
  84. package/dist/registry/collision-policy.js.map +1 -1
  85. package/dist/registry/framework-base-classes.d.ts.map +1 -1
  86. package/dist/registry/framework-base-classes.js +2 -1
  87. package/dist/registry/framework-base-classes.js.map +1 -1
  88. package/dist/registry/schema-builder.d.ts.map +1 -1
  89. package/dist/registry/schema-builder.js +33 -4
  90. package/dist/registry/schema-builder.js.map +1 -1
  91. package/dist/registry/shared-state.d.ts +6 -0
  92. package/dist/registry/shared-state.d.ts.map +1 -1
  93. package/dist/registry/shared-state.js +12 -1
  94. package/dist/registry/shared-state.js.map +1 -1
  95. package/dist/registry/types.d.ts +7 -1
  96. package/dist/registry/types.d.ts.map +1 -1
  97. package/dist/registry.d.ts +7 -1
  98. package/dist/registry.d.ts.map +1 -1
  99. package/dist/registry.js +25 -3
  100. package/dist/registry.js.map +1 -1
  101. package/dist/scanner/manifest-generator.d.ts.map +1 -1
  102. package/dist/scanner/manifest-generator.js +5 -1
  103. package/dist/scanner/manifest-generator.js.map +1 -1
  104. package/dist/scanner/types.d.ts +8 -0
  105. package/dist/scanner/types.d.ts.map +1 -1
  106. package/dist/scanner/types.js.map +1 -1
  107. package/dist/schema/ddl/duckdb-strategy.d.ts.map +1 -1
  108. package/dist/schema/ddl/duckdb-strategy.js +1 -0
  109. package/dist/schema/ddl/duckdb-strategy.js.map +1 -1
  110. package/dist/schema/ddl/null-equal-index.d.ts.map +1 -1
  111. package/dist/schema/ddl/null-equal-index.js +4 -2
  112. package/dist/schema/ddl/null-equal-index.js.map +1 -1
  113. package/dist/schema/generator.d.ts +3 -0
  114. package/dist/schema/generator.d.ts.map +1 -1
  115. package/dist/schema/generator.js +38 -0
  116. package/dist/schema/generator.js.map +1 -1
  117. package/dist/smrt-knowledge.json +74 -20
  118. package/dist/sync/apply.d.ts +2 -0
  119. package/dist/sync/apply.d.ts.map +1 -1
  120. package/dist/sync/apply.js +7 -3
  121. package/dist/sync/apply.js.map +1 -1
  122. package/dist/utils/scanner-module.d.ts +9 -0
  123. package/dist/utils/scanner-module.d.ts.map +1 -1
  124. package/dist/vite-plugin/index.d.ts +14 -0
  125. package/dist/vite-plugin/index.d.ts.map +1 -1
  126. package/dist/vite-plugin/index.js +58 -18
  127. package/dist/vite-plugin/index.js.map +1 -1
  128. package/dist/vite-plugin/package-stamp.d.ts +41 -0
  129. package/dist/vite-plugin/package-stamp.d.ts.map +1 -0
  130. package/dist/vite-plugin/package-stamp.js +33 -0
  131. package/dist/vite-plugin/package-stamp.js.map +1 -0
  132. package/dist/vite-plugin/sveltekit-generator.js +5 -3
  133. package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
  134. package/dist/vite-plugin/sync-apply-route.d.ts.map +1 -1
  135. package/dist/vite-plugin/sync-apply-route.js +10 -0
  136. package/dist/vite-plugin/sync-apply-route.js.map +1 -1
  137. package/dist/vite-plugin.js +2 -2
  138. package/package.json +10 -10
@@ -1,4 +1,6 @@
1
1
  import { type DatabaseInterface } from '@happyvertical/sql';
2
+ /** Whether this exact handle belongs to a currently active framework transaction. */
3
+ export declare function isFrameworkTransactionHandle(db: DatabaseInterface): boolean;
2
4
  interface QueueableDatabase {
3
5
  url?: string;
4
6
  }
@@ -1 +1 @@
1
- {"version":3,"file":"embedded-write-queue.d.ts","sourceRoot":"","sources":["../src/embedded-write-queue.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,iBAAiB,EAEvB,MAAM,oBAAoB,CAAC;AA0C5B,UAAU,iBAAiB;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,iBAAiB,GAAG,OAAO,CAEjE;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,iBAAiB,GAAG,OAAO,CAEjE;AAED;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAAC,EAAE,EAAE,iBAAiB,GAAG,OAAO,CAG3E;AA0BD;;;GAGG;AACH,wBAAsB,sBAAsB,CAAC,CAAC,EAC5C,EAAE,EAAE,iBAAiB,EACrB,SAAS,EAAE,OAAO,EAClB,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAC1B,OAAO,CAAC,CAAC,CAAC,CAwBZ;AAED;;;;;GAKG;AACH,wBAAsB,4BAA4B,CAAC,CAAC,EAClD,EAAE,EAAE,iBAAiB,EACrB,SAAS,EAAE,OAAO,EAClB,SAAS,EAAE,CAAC,WAAW,EAAE,iBAAiB,KAAK,OAAO,CAAC,CAAC,CAAC,EACzD,sBAAsB,UAAQ,GAC7B,OAAO,CAAC,CAAC,CAAC,CAgCZ"}
1
+ {"version":3,"file":"embedded-write-queue.d.ts","sourceRoot":"","sources":["../src/embedded-write-queue.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,iBAAiB,EAEvB,MAAM,oBAAoB,CAAC;AA6C5B,qFAAqF;AACrF,wBAAgB,4BAA4B,CAAC,EAAE,EAAE,iBAAiB,GAAG,OAAO,CAE3E;AAED,UAAU,iBAAiB;IACzB,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,iBAAiB,GAAG,OAAO,CAEjE;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,iBAAiB,GAAG,OAAO,CAEjE;AAED;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAAC,EAAE,EAAE,iBAAiB,GAAG,OAAO,CAG3E;AA0BD;;;GAGG;AACH,wBAAsB,sBAAsB,CAAC,CAAC,EAC5C,EAAE,EAAE,iBAAiB,EACrB,SAAS,EAAE,OAAO,EAClB,SAAS,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAC1B,OAAO,CAAC,CAAC,CAAC,CAwBZ;AAED;;;;;GAKG;AACH,wBAAsB,4BAA4B,CAAC,CAAC,EAClD,EAAE,EAAE,iBAAiB,EACrB,SAAS,EAAE,OAAO,EAClB,SAAS,EAAE,CAAC,WAAW,EAAE,iBAAiB,KAAK,OAAO,CAAC,CAAC,CAAC,EACzD,sBAAsB,UAAQ,GAC7B,OAAO,CAAC,CAAC,CAAC,CAqCZ"}
@@ -1,5 +1,5 @@
1
- import { AsyncLocalStorage } from "node:async_hooks";
2
1
  import { NestedTransactionError } from "@happyvertical/sql";
2
+ import { AsyncLocalStorage } from "node:async_hooks";
3
3
  //#region src/embedded-write-queue.ts
4
4
  /**
5
5
  * In-process write serialization for embedded database engines (#2360).
@@ -39,6 +39,11 @@ import { NestedTransactionError } from "@happyvertical/sql";
39
39
  var urlQueues = /* @__PURE__ */ new Map();
40
40
  var handleQueues = /* @__PURE__ */ new WeakMap();
41
41
  var activeQueueKeys = new AsyncLocalStorage();
42
+ var frameworkTransactions = new AsyncLocalStorage();
43
+ /** Whether this exact handle belongs to a currently active framework transaction. */
44
+ function isFrameworkTransactionHandle(db) {
45
+ return frameworkTransactions.getStore()?.has(db) === true;
46
+ }
42
47
  /**
43
48
  * Whether `db` is an embedded engine (SQLite/DuckDB/JSON — anything that is
44
49
  * not PostgreSQL). Handles without a URL are treated as embedded: serializing
@@ -127,9 +132,11 @@ async function withEmbeddedWriteTransaction(db, serialize, operation, reuseUnsup
127
132
  try {
128
133
  return await transaction((bound) => {
129
134
  entered = true;
130
- if (!serialize) return operation(bound);
131
- const active = activeQueueKeys.getStore();
132
- return activeQueueKeys.run(/* @__PURE__ */ new Set([...active ?? [], queueKey(bound)]), () => operation(bound));
135
+ return frameworkTransactions.run(/* @__PURE__ */ new Set([...frameworkTransactions.getStore() ?? [], bound]), () => {
136
+ if (!serialize) return operation(bound);
137
+ const active = activeQueueKeys.getStore();
138
+ return activeQueueKeys.run(/* @__PURE__ */ new Set([...active ?? [], queueKey(bound)]), () => operation(bound));
139
+ });
133
140
  });
134
141
  } catch (error) {
135
142
  if (reuseUnsupportedNested && !entered && error instanceof NestedTransactionError) return operation(db);
@@ -138,6 +145,6 @@ async function withEmbeddedWriteTransaction(db, serialize, operation, reuseUnsup
138
145
  });
139
146
  }
140
147
  //#endregion
141
- export { isEmbeddedDatabase, isPostgresDatabase, usesEmbeddedRevisionFallback, withEmbeddedWriteQueue, withEmbeddedWriteTransaction };
148
+ export { isEmbeddedDatabase, isFrameworkTransactionHandle, isPostgresDatabase, usesEmbeddedRevisionFallback, withEmbeddedWriteQueue, withEmbeddedWriteTransaction };
142
149
 
143
150
  //# sourceMappingURL=embedded-write-queue.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"embedded-write-queue.js","names":[],"sources":["../src/embedded-write-queue.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks';\nimport {\n type DatabaseInterface,\n NestedTransactionError,\n} from '@happyvertical/sql';\n\n/**\n * In-process write serialization for embedded database engines (#2360).\n *\n * File-backed SQLite (libsql) allows one writer at a time and the SDK opens a\n * NEW connection for every null-aware upsert attempt (`transaction(\"write\")`),\n * with no busy timeout on either connection. Any other write on the root\n * connection while such a transaction is open — most commonly the change-feed\n * append that follows every model save — collides as `SQLITE_BUSY`, and under\n * sustained concurrency (a `Promise.all` of creates) the two sides livelock\n * through every retry tier. Before the tenant-led conflict target (#2360) the\n * null-aware path was rare (custom conflict keys with NULL values, #1246);\n * with `tenant_id` leading every tenant-scoped default key, EVERY NULL-tenant\n * (global) create takes it, so the collision became the steady state.\n *\n * The queue makes the process a single writer per embedded database for:\n *\n * - a `save()` whose upsert conflict values contain NULL (the only shape the\n * SDK routes through a second-connection transaction), and\n * - the change-feed append (a root-connection write that follows every save).\n * - every model save and delete, so the embedded revision-CAS compare/upsert\n * fallback cannot race an ordinary save, natural-key create, or delete.\n * - a complete root `SmrtObject.withTransaction()` callback, with writes on\n * its bound handle re-entering the same hold instead of self-deadlocking.\n *\n * Ordinary save/delete holds contain database calls only — never an\n * interceptor or hook. Explicit transaction callbacks are caller-controlled,\n * so their bound model writes re-enter the existing hold. PostgreSQL is never\n * serialized here:\n * its null-aware upsert already arbitrates through advisory locks\n * server-side, and cross-process writers exist that an in-process queue could\n * not order anyway (which is also why this queue is a mitigation for embedded\n * engines' in-process writers, not a cross-process guarantee — embedded\n * engines have no cross-process writers in supported deployments).\n */\n\n/** Queue tail per database identity (URL string, or the handle itself). */\nconst urlQueues = new Map<string, Promise<unknown>>();\nconst handleQueues = new WeakMap<object, Promise<unknown>>();\nconst activeQueueKeys = new AsyncLocalStorage<ReadonlySet<string | object>>();\n\ninterface QueueableDatabase {\n url?: string;\n}\n\n/**\n * Whether `db` is an embedded engine (SQLite/DuckDB/JSON — anything that is\n * not PostgreSQL). Handles without a URL are treated as embedded: serializing\n * them is at worst a throughput cost, while not serializing an embedded\n * handle reintroduces the livelock.\n */\nexport function isEmbeddedDatabase(db: QueueableDatabase): boolean {\n return !isPostgresDatabase(db);\n}\n\n/**\n * Whether `db` is a PostgreSQL handle.\n *\n * The revision compare-and-swap predicate is dialect-specific (#2620): only\n * PostgreSQL needs the millisecond-truncating, timezone-tolerant guard, and\n * only PostgreSQL can evaluate `date_trunc`.\n */\nexport function isPostgresDatabase(db: QueueableDatabase): boolean {\n return /^postgres(?:ql)?:/iu.test(db?.url ?? '');\n}\n\n/**\n * Whether revision compare-and-swap needs the process-local read/upsert path.\n *\n * DuckDB/JSON cannot express the timestamp predicate through the generic\n * update API, and local SQLite has only the process's writers in supported\n * deployments. Remote LibSQL/Turso is different: other processes may write\n * the same database, so it must use one conditional UPDATE at the server.\n */\nexport function usesEmbeddedRevisionFallback(db: QueueableDatabase): boolean {\n const url = db?.url ?? '';\n return isEmbeddedDatabase(db) && !/^(?:https?|libsql):\\/\\//iu.test(url);\n}\n\n/**\n * Whether `url` identifies a SQLite/DuckDB in-memory database that is\n * private to its own connection, as opposed to shared state.\n *\n * Bare `:memory:` and `file::memory:` (no query string) each mint a fresh,\n * connection-private database -- `resolveDuckDBUrl(':memory:')` returns it\n * unchanged, with no per-instance suffix -- so two independent handles that\n * both happen to carry this literal string are NOT the same database and\n * must not share a write-queue identity (#2707). A `cache=shared` query\n * parameter (e.g. `file::memory:?cache=shared`) is the opposite case: it is\n * SQLite's own mechanism for making an in-memory database genuinely shared\n * across connections in the process, so a URL carrying it must keep the\n * ordinary string-keyed (shared) behavior.\n */\nfunction isPerConnectionMemoryUrl(url: string): boolean {\n if (url.includes('cache=shared')) return false;\n return url === ':memory:' || url === 'file::memory:';\n}\n\nfunction queueKey(db: QueueableDatabase): string | object {\n if (typeof db?.url !== 'string' || db.url.length === 0) return db;\n return isPerConnectionMemoryUrl(db.url) ? db : db.url;\n}\n\n/**\n * Run `operation` after every previously queued write on the same database\n * has settled. When `serialize` is false the operation runs immediately.\n */\nexport async function withEmbeddedWriteQueue<T>(\n db: QueueableDatabase,\n serialize: boolean,\n operation: () => Promise<T>,\n): Promise<T> {\n if (!serialize) return operation();\n\n const key = queueKey(db);\n const active = activeQueueKeys.getStore();\n if (active?.has(key)) return operation();\n const previous =\n (typeof key === 'string' ? urlQueues.get(key) : handleQueues.get(key)) ??\n Promise.resolve();\n const run = () =>\n activeQueueKeys.run(new Set([...(active ?? []), key]), operation);\n const next = previous.then(run, run);\n // The stored tail must never reject, or one failed write would poison the\n // queue for every later writer.\n const tail = next.catch(() => undefined);\n if (typeof key === 'string') {\n urlQueues.set(key, tail);\n void tail.finally(() => {\n if (urlQueues.get(key) === tail) urlQueues.delete(key);\n });\n } else {\n handleQueues.set(key, tail);\n }\n return next;\n}\n\n/**\n * Serialize a complete embedded root transaction with ordinary model writes.\n * The transaction handle is registered as a reentrant alias so saves and\n * change-feed appends inside the callback do not queue behind their own root\n * transaction.\n */\nexport async function withEmbeddedWriteTransaction<T>(\n db: DatabaseInterface,\n serialize: boolean,\n operation: (transaction: DatabaseInterface) => Promise<T>,\n reuseUnsupportedNested = false,\n): Promise<T> {\n const transaction = db.transaction?.bind(db);\n if (!transaction) {\n throw new Error('Database transaction support is required');\n }\n return withEmbeddedWriteQueue(db, serialize, async () => {\n let entered = false;\n try {\n return await transaction((bound) => {\n entered = true;\n if (!serialize) return operation(bound);\n const active = activeQueueKeys.getStore();\n return activeQueueKeys.run(\n new Set([...(active ?? []), queueKey(bound)]),\n () => operation(bound),\n );\n });\n } catch (error) {\n // The SDK explicitly refuses no-savepoint nesting before invoking the\n // callback or touching the enclosing transaction. A helper that needs\n // the caller's atomic scope can use that existing handle directly. Never\n // replay work when the callback ran, even if it threw the same error.\n if (\n reuseUnsupportedNested &&\n !entered &&\n error instanceof NestedTransactionError\n ) {\n return operation(db);\n }\n throw error;\n }\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,IAAM,4BAAY,IAAI,IAA8B;AACpD,IAAM,+BAAe,IAAI,QAAkC;AAC3D,IAAM,kBAAkB,IAAI,kBAAgD;;;;;;;AAY5E,SAAgB,mBAAmB,IAAgC;CACjE,OAAO,CAAC,mBAAmB,EAAE;AAC/B;;;;;;;;AASA,SAAgB,mBAAmB,IAAgC;CACjE,OAAO,sBAAsB,KAAK,IAAI,OAAO,EAAE;AACjD;;;;;;;;;AAUA,SAAgB,6BAA6B,IAAgC;CAC3E,MAAM,MAAM,IAAI,OAAO;CACvB,OAAO,mBAAmB,EAAE,KAAK,CAAC,4BAA4B,KAAK,GAAG;AACxE;;;;;;;;;;;;;;;AAgBA,SAAS,yBAAyB,KAAsB;CACtD,IAAI,IAAI,SAAS,cAAc,GAAG,OAAO;CACzC,OAAO,QAAQ,cAAc,QAAQ;AACvC;AAEA,SAAS,SAAS,IAAwC;CACxD,IAAI,OAAO,IAAI,QAAQ,YAAY,GAAG,IAAI,WAAW,GAAG,OAAO;CAC/D,OAAO,yBAAyB,GAAG,GAAG,IAAI,KAAK,GAAG;AACpD;;;;;AAMA,eAAsB,uBACpB,IACA,WACA,WACY;CACZ,IAAI,CAAC,WAAW,OAAO,UAAU;CAEjC,MAAM,MAAM,SAAS,EAAE;CACvB,MAAM,SAAS,gBAAgB,SAAS;CACxC,IAAI,QAAQ,IAAI,GAAG,GAAG,OAAO,UAAU;CACvC,MAAM,YACH,OAAO,QAAQ,WAAW,UAAU,IAAI,GAAG,IAAI,aAAa,IAAI,GAAG,MACpE,QAAQ,QAAQ;CAClB,MAAM,YACJ,gBAAgB,oBAAI,IAAI,IAAI,CAAC,GAAI,UAAU,CAAC,GAAI,GAAG,CAAC,GAAG,SAAS;CAClE,MAAM,OAAO,SAAS,KAAK,KAAK,GAAG;CAGnC,MAAM,OAAO,KAAK,YAAY,KAAA,CAAS;CACvC,IAAI,OAAO,QAAQ,UAAU;EAC3B,UAAU,IAAI,KAAK,IAAI;EACvB,KAAU,cAAc;GACtB,IAAI,UAAU,IAAI,GAAG,MAAM,MAAM,UAAU,OAAO,GAAG;EACvD,CAAC;CACH,OACE,aAAa,IAAI,KAAK,IAAI;CAE5B,OAAO;AACT;;;;;;;AAQA,eAAsB,6BACpB,IACA,WACA,WACA,yBAAyB,OACb;CACZ,MAAM,cAAc,GAAG,aAAa,KAAK,EAAE;CAC3C,IAAI,CAAC,aACH,MAAM,IAAI,MAAM,0CAA0C;CAE5D,OAAO,uBAAuB,IAAI,WAAW,YAAY;EACvD,IAAI,UAAU;EACd,IAAI;GACF,OAAO,MAAM,aAAa,UAAU;IAClC,UAAU;IACV,IAAI,CAAC,WAAW,OAAO,UAAU,KAAK;IACtC,MAAM,SAAS,gBAAgB,SAAS;IACxC,OAAO,gBAAgB,oBACrB,IAAI,IAAI,CAAC,GAAI,UAAU,CAAC,GAAI,SAAS,KAAK,CAAC,CAAC,SACtC,UAAU,KAAK,CACvB;GACF,CAAC;EACH,SAAS,OAAO;GAKd,IACE,0BACA,CAAC,WACD,iBAAiB,wBAEjB,OAAO,UAAU,EAAE;GAErB,MAAM;EACR;CACF,CAAC;AACH"}
1
+ {"version":3,"file":"embedded-write-queue.js","names":[],"sources":["../src/embedded-write-queue.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks';\nimport {\n type DatabaseInterface,\n NestedTransactionError,\n} from '@happyvertical/sql';\n\n/**\n * In-process write serialization for embedded database engines (#2360).\n *\n * File-backed SQLite (libsql) allows one writer at a time and the SDK opens a\n * NEW connection for every null-aware upsert attempt (`transaction(\"write\")`),\n * with no busy timeout on either connection. Any other write on the root\n * connection while such a transaction is open — most commonly the change-feed\n * append that follows every model save — collides as `SQLITE_BUSY`, and under\n * sustained concurrency (a `Promise.all` of creates) the two sides livelock\n * through every retry tier. Before the tenant-led conflict target (#2360) the\n * null-aware path was rare (custom conflict keys with NULL values, #1246);\n * with `tenant_id` leading every tenant-scoped default key, EVERY NULL-tenant\n * (global) create takes it, so the collision became the steady state.\n *\n * The queue makes the process a single writer per embedded database for:\n *\n * - a `save()` whose upsert conflict values contain NULL (the only shape the\n * SDK routes through a second-connection transaction), and\n * - the change-feed append (a root-connection write that follows every save).\n * - every model save and delete, so the embedded revision-CAS compare/upsert\n * fallback cannot race an ordinary save, natural-key create, or delete.\n * - a complete root `SmrtObject.withTransaction()` callback, with writes on\n * its bound handle re-entering the same hold instead of self-deadlocking.\n *\n * Ordinary save/delete holds contain database calls only — never an\n * interceptor or hook. Explicit transaction callbacks are caller-controlled,\n * so their bound model writes re-enter the existing hold. PostgreSQL is never\n * serialized here:\n * its null-aware upsert already arbitrates through advisory locks\n * server-side, and cross-process writers exist that an in-process queue could\n * not order anyway (which is also why this queue is a mitigation for embedded\n * engines' in-process writers, not a cross-process guarantee — embedded\n * engines have no cross-process writers in supported deployments).\n */\n\n/** Queue tail per database identity (URL string, or the handle itself). */\nconst urlQueues = new Map<string, Promise<unknown>>();\nconst handleQueues = new WeakMap<object, Promise<unknown>>();\nconst activeQueueKeys = new AsyncLocalStorage<ReadonlySet<string | object>>();\nconst frameworkTransactions = new AsyncLocalStorage<\n ReadonlySet<DatabaseInterface>\n>();\n\n/** Whether this exact handle belongs to a currently active framework transaction. */\nexport function isFrameworkTransactionHandle(db: DatabaseInterface): boolean {\n return frameworkTransactions.getStore()?.has(db) === true;\n}\n\ninterface QueueableDatabase {\n url?: string;\n}\n\n/**\n * Whether `db` is an embedded engine (SQLite/DuckDB/JSON — anything that is\n * not PostgreSQL). Handles without a URL are treated as embedded: serializing\n * them is at worst a throughput cost, while not serializing an embedded\n * handle reintroduces the livelock.\n */\nexport function isEmbeddedDatabase(db: QueueableDatabase): boolean {\n return !isPostgresDatabase(db);\n}\n\n/**\n * Whether `db` is a PostgreSQL handle.\n *\n * The revision compare-and-swap predicate is dialect-specific (#2620): only\n * PostgreSQL needs the millisecond-truncating, timezone-tolerant guard, and\n * only PostgreSQL can evaluate `date_trunc`.\n */\nexport function isPostgresDatabase(db: QueueableDatabase): boolean {\n return /^postgres(?:ql)?:/iu.test(db?.url ?? '');\n}\n\n/**\n * Whether revision compare-and-swap needs the process-local read/upsert path.\n *\n * DuckDB/JSON cannot express the timestamp predicate through the generic\n * update API, and local SQLite has only the process's writers in supported\n * deployments. Remote LibSQL/Turso is different: other processes may write\n * the same database, so it must use one conditional UPDATE at the server.\n */\nexport function usesEmbeddedRevisionFallback(db: QueueableDatabase): boolean {\n const url = db?.url ?? '';\n return isEmbeddedDatabase(db) && !/^(?:https?|libsql):\\/\\//iu.test(url);\n}\n\n/**\n * Whether `url` identifies a SQLite/DuckDB in-memory database that is\n * private to its own connection, as opposed to shared state.\n *\n * Bare `:memory:` and `file::memory:` (no query string) each mint a fresh,\n * connection-private database -- `resolveDuckDBUrl(':memory:')` returns it\n * unchanged, with no per-instance suffix -- so two independent handles that\n * both happen to carry this literal string are NOT the same database and\n * must not share a write-queue identity (#2707). A `cache=shared` query\n * parameter (e.g. `file::memory:?cache=shared`) is the opposite case: it is\n * SQLite's own mechanism for making an in-memory database genuinely shared\n * across connections in the process, so a URL carrying it must keep the\n * ordinary string-keyed (shared) behavior.\n */\nfunction isPerConnectionMemoryUrl(url: string): boolean {\n if (url.includes('cache=shared')) return false;\n return url === ':memory:' || url === 'file::memory:';\n}\n\nfunction queueKey(db: QueueableDatabase): string | object {\n if (typeof db?.url !== 'string' || db.url.length === 0) return db;\n return isPerConnectionMemoryUrl(db.url) ? db : db.url;\n}\n\n/**\n * Run `operation` after every previously queued write on the same database\n * has settled. When `serialize` is false the operation runs immediately.\n */\nexport async function withEmbeddedWriteQueue<T>(\n db: QueueableDatabase,\n serialize: boolean,\n operation: () => Promise<T>,\n): Promise<T> {\n if (!serialize) return operation();\n\n const key = queueKey(db);\n const active = activeQueueKeys.getStore();\n if (active?.has(key)) return operation();\n const previous =\n (typeof key === 'string' ? urlQueues.get(key) : handleQueues.get(key)) ??\n Promise.resolve();\n const run = () =>\n activeQueueKeys.run(new Set([...(active ?? []), key]), operation);\n const next = previous.then(run, run);\n // The stored tail must never reject, or one failed write would poison the\n // queue for every later writer.\n const tail = next.catch(() => undefined);\n if (typeof key === 'string') {\n urlQueues.set(key, tail);\n void tail.finally(() => {\n if (urlQueues.get(key) === tail) urlQueues.delete(key);\n });\n } else {\n handleQueues.set(key, tail);\n }\n return next;\n}\n\n/**\n * Serialize a complete embedded root transaction with ordinary model writes.\n * The transaction handle is registered as a reentrant alias so saves and\n * change-feed appends inside the callback do not queue behind their own root\n * transaction.\n */\nexport async function withEmbeddedWriteTransaction<T>(\n db: DatabaseInterface,\n serialize: boolean,\n operation: (transaction: DatabaseInterface) => Promise<T>,\n reuseUnsupportedNested = false,\n): Promise<T> {\n const transaction = db.transaction?.bind(db);\n if (!transaction) {\n throw new Error('Database transaction support is required');\n }\n return withEmbeddedWriteQueue(db, serialize, async () => {\n let entered = false;\n try {\n return await transaction((bound) => {\n entered = true;\n return frameworkTransactions.run(\n new Set([...(frameworkTransactions.getStore() ?? []), bound]),\n () => {\n if (!serialize) return operation(bound);\n const active = activeQueueKeys.getStore();\n return activeQueueKeys.run(\n new Set([...(active ?? []), queueKey(bound)]),\n () => operation(bound),\n );\n },\n );\n });\n } catch (error) {\n // The SDK explicitly refuses no-savepoint nesting before invoking the\n // callback or touching the enclosing transaction. A helper that needs\n // the caller's atomic scope can use that existing handle directly. Never\n // replay work when the callback ran, even if it threw the same error.\n if (\n reuseUnsupportedNested &&\n !entered &&\n error instanceof NestedTransactionError\n ) {\n return operation(db);\n }\n throw error;\n }\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,IAAM,4BAAY,IAAI,IAA8B;AACpD,IAAM,+BAAe,IAAI,QAAkC;AAC3D,IAAM,kBAAkB,IAAI,kBAAgD;AAC5E,IAAM,wBAAwB,IAAI,kBAEhC;;AAGF,SAAgB,6BAA6B,IAAgC;CAC3E,OAAO,sBAAsB,SAAS,CAAC,EAAE,IAAI,EAAE,MAAM;AACvD;;;;;;;AAYA,SAAgB,mBAAmB,IAAgC;CACjE,OAAO,CAAC,mBAAmB,EAAE;AAC/B;;;;;;;;AASA,SAAgB,mBAAmB,IAAgC;CACjE,OAAO,sBAAsB,KAAK,IAAI,OAAO,EAAE;AACjD;;;;;;;;;AAUA,SAAgB,6BAA6B,IAAgC;CAC3E,MAAM,MAAM,IAAI,OAAO;CACvB,OAAO,mBAAmB,EAAE,KAAK,CAAC,4BAA4B,KAAK,GAAG;AACxE;;;;;;;;;;;;;;;AAgBA,SAAS,yBAAyB,KAAsB;CACtD,IAAI,IAAI,SAAS,cAAc,GAAG,OAAO;CACzC,OAAO,QAAQ,cAAc,QAAQ;AACvC;AAEA,SAAS,SAAS,IAAwC;CACxD,IAAI,OAAO,IAAI,QAAQ,YAAY,GAAG,IAAI,WAAW,GAAG,OAAO;CAC/D,OAAO,yBAAyB,GAAG,GAAG,IAAI,KAAK,GAAG;AACpD;;;;;AAMA,eAAsB,uBACpB,IACA,WACA,WACY;CACZ,IAAI,CAAC,WAAW,OAAO,UAAU;CAEjC,MAAM,MAAM,SAAS,EAAE;CACvB,MAAM,SAAS,gBAAgB,SAAS;CACxC,IAAI,QAAQ,IAAI,GAAG,GAAG,OAAO,UAAU;CACvC,MAAM,YACH,OAAO,QAAQ,WAAW,UAAU,IAAI,GAAG,IAAI,aAAa,IAAI,GAAG,MACpE,QAAQ,QAAQ;CAClB,MAAM,YACJ,gBAAgB,oBAAI,IAAI,IAAI,CAAC,GAAI,UAAU,CAAC,GAAI,GAAG,CAAC,GAAG,SAAS;CAClE,MAAM,OAAO,SAAS,KAAK,KAAK,GAAG;CAGnC,MAAM,OAAO,KAAK,YAAY,KAAA,CAAS;CACvC,IAAI,OAAO,QAAQ,UAAU;EAC3B,UAAU,IAAI,KAAK,IAAI;EACvB,KAAU,cAAc;GACtB,IAAI,UAAU,IAAI,GAAG,MAAM,MAAM,UAAU,OAAO,GAAG;EACvD,CAAC;CACH,OACE,aAAa,IAAI,KAAK,IAAI;CAE5B,OAAO;AACT;;;;;;;AAQA,eAAsB,6BACpB,IACA,WACA,WACA,yBAAyB,OACb;CACZ,MAAM,cAAc,GAAG,aAAa,KAAK,EAAE;CAC3C,IAAI,CAAC,aACH,MAAM,IAAI,MAAM,0CAA0C;CAE5D,OAAO,uBAAuB,IAAI,WAAW,YAAY;EACvD,IAAI,UAAU;EACd,IAAI;GACF,OAAO,MAAM,aAAa,UAAU;IAClC,UAAU;IACV,OAAO,sBAAsB,oBAC3B,IAAI,IAAI,CAAC,GAAI,sBAAsB,SAAS,KAAK,CAAC,GAAI,KAAK,CAAC,SACtD;KACJ,IAAI,CAAC,WAAW,OAAO,UAAU,KAAK;KACtC,MAAM,SAAS,gBAAgB,SAAS;KACxC,OAAO,gBAAgB,oBACrB,IAAI,IAAI,CAAC,GAAI,UAAU,CAAC,GAAI,SAAS,KAAK,CAAC,CAAC,SACtC,UAAU,KAAK,CACvB;IACF,CACF;GACF,CAAC;EACH,SAAS,OAAO;GAKd,IACE,0BACA,CAAC,WACD,iBAAiB,wBAEjB,OAAO,UAAU,EAAE;GAErB,MAAM;EACR;CACF,CAAC;AACH"}
@@ -1,6 +1,6 @@
1
1
  import { ensureChangeFeedTable } from "../change-feed.js";
2
- import { getAuthorizedTenantScopedChangesSince } from "../change-feed-authz.js";
3
2
  import { applyPostgresRuntimeTimeouts } from "../postgres-timeouts.js";
3
+ import { getAuthorizedTenantScopedChangesSince } from "../change-feed-authz.js";
4
4
  import { createLogger } from "@happyvertical/logger";
5
5
  import { getDatabase } from "@happyvertical/sql";
6
6
  //#region src/generators/changes-route.ts
@@ -67,6 +67,8 @@ export interface RuntimeOptions {
67
67
  taskActions?: Record<string, {
68
68
  objectName: string;
69
69
  objectType: string;
70
+ /** Registry identity of the task's class (#3490). */
71
+ registryKey?: string;
70
72
  }>;
71
73
  /**
72
74
  * Lowercased simple names of objects that are `@TenantScoped` (#1554). When
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-runtime-template.d.ts","sourceRoot":"","sources":["../../src/generators/mcp-runtime-template.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"mcp-runtime-template.d.ts","sourceRoot":"","sources":["../../src/generators/mcp-runtime-template.ts"],"names":[],"mappings":"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"}
@@ -44,6 +44,7 @@ function generateRuntimeBootstrap(options = {}) {
44
44
  return tools.map((tool) => {
45
45
  const objectName = toolTargets[tool.name].objectName.toLowerCase();
46
46
  const action = toolTargets[tool.name].action;
47
+ const lookup = toolTargets[tool.name].registryKey ?? toolTargets[tool.name].objectName;
47
48
  switch (action) {
48
49
  case "list": return `${indent}case '${tool.name}': {
49
50
  ${indent} const limit = resolveListBound(args.limit, 'limit', 50, ${MAX_LIST_LIMIT});
@@ -54,7 +55,7 @@ ${indent} // lets successive pages repeat and skip rows on PostgreSQL.
54
55
  ${indent} const orderBy = args.orderBy ?? ${JSON.stringify(listOrderBy[objectName] ?? DEFAULT_LIST_ORDER_BY)};
55
56
  ${indent} const where = args.where ?? {};
56
57
 
57
- ${indent} const collection = await ObjectRegistry.getCollection(${JSON.stringify(toolTargets[tool.name].objectName)}, {
58
+ ${indent} const collection = await ObjectRegistry.getCollection(${JSON.stringify(lookup)}, {
58
59
  ${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },
59
60
  ${indent} ai: aiConfig
60
61
  ${indent} });
@@ -72,7 +73,7 @@ ${indent} if (!args.id && !args.slug) {
72
73
  ${indent} throw new Error('Either id or slug is required');
73
74
  ${indent} }
74
75
 
75
- ${indent} const collection = await ObjectRegistry.getCollection(${JSON.stringify(toolTargets[tool.name].objectName)}, {
76
+ ${indent} const collection = await ObjectRegistry.getCollection(${JSON.stringify(lookup)}, {
76
77
  ${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },
77
78
  ${indent} ai: aiConfig
78
79
  ${indent} });
@@ -87,10 +88,9 @@ ${indent} }
87
88
  ${indent} return successResult(item.toPublicJSON(PUBLIC_JSON_OPTIONS));
88
89
  ${indent}}`;
89
90
  case "create": return `${indent}case '${tool.name}': {
90
- ${indent} const { collection, objectName: targetObjectName } = await resolveCreateTarget(${JSON.stringify(objectName)}, args, aiConfig);
91
+ ${indent} const { collection, objectName: targetObjectName } = await resolveCreateTarget(${JSON.stringify(objectName)}, ${JSON.stringify(lookup)}, args, aiConfig);
91
92
 
92
93
  ${indent} const newItem = await collection.create(applyWritablePolicy(targetObjectName, args));
93
- ${indent} await newItem.save();
94
94
 
95
95
  ${indent} return successResult(newItem.toPublicJSON(PUBLIC_JSON_OPTIONS));
96
96
  ${indent}}`;
@@ -100,7 +100,7 @@ ${indent} if (!id) {
100
100
  ${indent} throw new Error('ID is required for update');
101
101
  ${indent} }
102
102
 
103
- ${indent} const collection = await ObjectRegistry.getCollection(${JSON.stringify(toolTargets[tool.name].objectName)}, {
103
+ ${indent} const collection = await ObjectRegistry.getCollection(${JSON.stringify(lookup)}, {
104
104
  ${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },
105
105
  ${indent} ai: aiConfig
106
106
  ${indent} });
@@ -110,8 +110,10 @@ ${indent} if (!existing) {
110
110
  ${indent} throw new Error('Object not found');
111
111
  ${indent} }
112
112
 
113
- ${indent} Object.assign(existing, applyWritablePolicy(${JSON.stringify(toolTargets[tool.name].objectName)}, updateData));
114
- ${indent} await existing.save();
113
+ ${indent} await collection.withAuditMutation(existing, 'updated', async (bound) => {
114
+ ${indent} Object.assign(bound, applyWritablePolicy(${JSON.stringify(lookup)}, updateData));
115
+ ${indent} await bound.save();
116
+ ${indent} });
115
117
 
116
118
  ${indent} return successResult(existing.toPublicJSON(PUBLIC_JSON_OPTIONS));
117
119
  ${indent}}`;
@@ -120,7 +122,7 @@ ${indent} if (!args.id) {
120
122
  ${indent} throw new Error('ID is required for delete');
121
123
  ${indent} }
122
124
 
123
- ${indent} const collection = await ObjectRegistry.getCollection(${JSON.stringify(toolTargets[tool.name].objectName)}, {
125
+ ${indent} const collection = await ObjectRegistry.getCollection(${JSON.stringify(lookup)}, {
124
126
  ${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },
125
127
  ${indent} ai: aiConfig
126
128
  ${indent} });
@@ -130,7 +132,7 @@ ${indent} if (!toDelete) {
130
132
  ${indent} throw new Error('Object not found');
131
133
  ${indent} }
132
134
 
133
- ${indent} await toDelete.delete();
135
+ ${indent} await collection.delete(args.id);
134
136
 
135
137
  ${indent} return successResult({ success: true, message: 'Object deleted successfully' });
136
138
  ${indent}}`;
@@ -145,7 +147,7 @@ ${indent} if (actionMeta.scope === 'collection' && id) {
145
147
  ${indent} throw new Error(${JSON.stringify(`Custom action ${action} is collection-scoped and does not accept an ID`)});
146
148
  ${indent} }
147
149
 
148
- ${indent} const collection = await ObjectRegistry.getCollection(${JSON.stringify(toolTargets[tool.name].objectName)}, {
150
+ ${indent} const collection = await ObjectRegistry.getCollection(${JSON.stringify(lookup)}, {
149
151
  ${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },
150
152
  ${indent} ai: aiConfig
151
153
  ${indent} });
@@ -153,7 +155,7 @@ ${indent} });
153
155
  ${indent} const target = actionMeta.scope === 'item'
154
156
  ${indent} ? await collection.get(id)
155
157
  ${indent} : actionMeta.isStatic
156
- ${indent} ? ObjectRegistry.getClass(${JSON.stringify(toolTargets[tool.name].objectName)})?.constructor
158
+ ${indent} ? ObjectRegistry.getClass(${JSON.stringify(lookup)})?.constructor
157
159
  ${indent} : collection;
158
160
  ${indent} if (!target) {
159
161
  ${indent} throw new Error(actionMeta.scope === 'item' ? 'Object not found' : 'Custom action target not found');
@@ -227,7 +229,7 @@ const DEBUG = ${debug};
227
229
  // Static tool definitions (generated at build time)
228
230
  const TOOLS = ${toolsCode};
229
231
  const TOOL_LIST_CACHE_HINT = ${JSON.stringify(toolListCacheHint)};
230
- const TOOL_TARGETS: Record<string, { objectName: string; action: string }> = ${JSON.stringify(toolTargets)};
232
+ const TOOL_TARGETS: Record<string, { objectName: string; action: string; registryKey?: string }> = ${JSON.stringify(toolTargets)};
231
233
  const CUSTOM_ACTIONS = ${JSON.stringify(customActions)};
232
234
  const TASK_ACTIONS = ${JSON.stringify(taskActions)};
233
235
  const STI_TARGETS: Record<string, Record<string, string>> = ${JSON.stringify(stiTargets)};
@@ -299,10 +301,10 @@ function applyWritablePolicy(objectName: string, data: any): Record<string, any>
299
301
  }
300
302
 
301
303
  /** Resolve an advertised STI discriminator to its registered subtype collection. */
302
- async function resolveCreateTarget(baseObjectName: string, args: Record<string, any>, aiConfig: any) {
304
+ async function resolveCreateTarget(stiKey: string, baseObjectName: string, args: Record<string, any>, aiConfig: any) {
303
305
  let objectName = baseObjectName;
304
306
  const discriminator = args._meta_type;
305
- const targets = STI_TARGETS[baseObjectName];
307
+ const targets = STI_TARGETS[stiKey];
306
308
  if (typeof discriminator === 'string' && targets) {
307
309
  const target = targets[discriminator];
308
310
  if (!target) throw new Error('Unknown STI discriminator: ' + discriminator);
@@ -440,9 +442,9 @@ function jsonRpcError(id: any, code: number, message: string, data?: any) {
440
442
  async function getTaskRuntime() {
441
443
  if (!taskRuntime) {
442
444
  taskRuntime = (async () => {
443
- const firstAction = Object.values(TASK_ACTIONS)[0] as { objectName: string } | undefined;
445
+ const firstAction = Object.values(TASK_ACTIONS)[0] as { objectName: string; registryKey?: string } | undefined;
444
446
  if (!firstAction) throw new Error('No task-enabled MCP action is configured');
445
- const collection = await ObjectRegistry.getCollection(firstAction.objectName, {
447
+ const collection = await ObjectRegistry.getCollection(firstAction.registryKey ?? firstAction.objectName, {
446
448
  persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },
447
449
  });
448
450
  const db = collection.db;
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-runtime-template.js","names":[],"sources":["../../src/generators/mcp-runtime-template.ts"],"sourcesContent":["/**\n * Runtime bootstrap template for generated MCP servers\n *\n * This template provides stdio transport integration for SMRT-generated MCP servers.\n * It handles:\n * - Server initialization with @modelcontextprotocol/server v2\n * - Tool registration from MCPGenerator\n * - Stdio transport connection\n * - Error handling and logging\n * - Graceful shutdown\n */\n\nimport {\n DEFAULT_LIST_LIMIT,\n DEFAULT_LIST_ORDER_BY,\n MAX_LIST_LIMIT,\n} from '../query-bounds.js';\nimport type { CustomActionScope } from './custom-action.js';\nimport type { MCPConfig, MCPContext } from './mcp.js';\nimport type { McpToolTarget } from './mcp-tool-name.js';\n\n/**\n * Helper function to capitalize first letter\n */\nfunction capitalize(str: string): string {\n return str.charAt(0).toUpperCase() + str.slice(1);\n}\n\nexport interface RuntimeOptions {\n /** Server name (defaults to package name) */\n name?: string;\n\n /** Server version (defaults to package version) */\n version?: string;\n\n /** Server description */\n description?: string;\n\n /** MCP generator configuration */\n config?: MCPConfig;\n\n /** MCP context (database, AI client, etc.) */\n context?: MCPContext;\n\n /** Enable debug logging */\n debug?: boolean;\n\n /** Static tool definitions (generated at build time) */\n tools?: Array<{\n name: string;\n description: string;\n inputSchema: Record<string, unknown>;\n outputSchema?: Record<string, unknown>;\n annotations?: {\n readOnlyHint: boolean;\n destructiveHint: boolean;\n idempotentHint: boolean;\n openWorldHint: boolean;\n };\n title?: string;\n icons?: Array<{\n src: string;\n mimeType?: string;\n sizes?: string[];\n theme?: 'light' | 'dark';\n }>;\n _meta?: Record<string, unknown>;\n }>;\n /** Explicit original execution targets for generated protocol aliases. */\n toolTargets?: Record<string, McpToolTarget>;\n /** Cache hint emitted for deploy-static tools/list results. */\n toolListCacheHint?: {\n ttlMs: number;\n cacheScope: 'private' | 'public';\n };\n /** Internal invocation metadata; never exposed by the MCP tools/list result. */\n customActions?: Record<\n string,\n {\n scope: CustomActionScope;\n isStatic: boolean;\n /** Declared method name; tool IDs are lowercased protocol aliases. */\n methodName?: string;\n parameterNames?: string[];\n optionsParameter?: boolean;\n legacyOptions: boolean;\n }\n >;\n /** Task-enabled item custom actions. Never emitted in tools/list. */\n taskActions?: Record<\n string,\n {\n objectName: string;\n objectType: string;\n }\n >;\n\n /**\n * Lowercased simple names of objects that are `@TenantScoped` (#1554). When\n * non-empty, the generated server imports the tenancy fail-closed gate and\n * wraps tenant-scoped tool calls so a stdio invocation cannot read across all\n * tenants. The tenant is sourced from `SMRT_MCP_TENANT_ID` /\n * `SMRT_MCP_ALLOW_CROSS_TENANT` env vars (this server has no auth principal).\n */\n tenantScopedObjects?: string[];\n\n /**\n * Build-time approved STI discriminators, keyed by the lowercased MCP object\n * prefix. The generated runtime uses this instead of searching an initially\n * empty registry, then loads the approved qualified type through the public\n * collection API.\n */\n stiTargets?: Record<string, Record<string, string>>;\n\n /**\n * Default list ordering per lowercased MCP object prefix (#2367).\n *\n * Baked in at generation time because the tiebreak follows the model's\n * declared primary key, which the emitted server cannot resolve on its own.\n * Prefixes absent from this map fall back to {@link DEFAULT_LIST_ORDER_BY}.\n */\n listOrderBy?: Record<string, readonly string[]>;\n}\n\n/**\n * Generate runtime bootstrap code for MCP server\n *\n * @param options - Runtime configuration options\n * @returns TypeScript code for server entry point\n */\nexport function generateRuntimeBootstrap(options: RuntimeOptions = {}): string {\n const {\n name = 'smrt-mcp-server',\n version = '1.0.0',\n description = 'Auto-generated MCP server from SMRT objects',\n debug = false,\n tools = [],\n toolTargets: explicitToolTargets,\n customActions = {},\n taskActions = {},\n tenantScopedObjects = [],\n stiTargets = {},\n listOrderBy = {},\n toolListCacheHint = { ttlMs: 86_400_000, cacheScope: 'private' },\n } = options;\n\n // Direct template callers retain their legacy object_action inputs. The\n // owning generator always supplies explicit targets, including valid aliases.\n const toolTargets =\n explicitToolTargets ??\n Object.fromEntries(\n tools.map((tool) => {\n const separator = tool.name.indexOf('_');\n return [\n tool.name,\n {\n objectName: capitalize(tool.name.slice(0, separator)),\n action: tool.name.slice(separator + 1),\n },\n ];\n }),\n );\n for (const tool of tools) {\n if (!Object.hasOwn(toolTargets, tool.name)) {\n throw new Error(`Missing MCP tool target: ${tool.name}`);\n }\n }\n\n // Generate static tool array as TypeScript code\n const toolsCode = tools.length > 0 ? JSON.stringify(tools, null, 2) : '[]';\n const hasTaskActions = Object.keys(taskActions).length > 0;\n\n // Fail-closed tenant context (#1554): only wire the tenancy gate when at\n // least one exposed object is tenant-scoped, so apps without tenancy never\n // get a dangling import.\n const tenantScopedSet = Array.from(\n new Set(tenantScopedObjects.map((n) => n.toLowerCase())),\n );\n const hasTenantScoped = tenantScopedSet.length > 0;\n\n // Generate static switch cases using shared helper\n const generateSwitchCases = (indent: string) => {\n return tools\n .map((tool) => {\n const objectName = toolTargets[tool.name].objectName.toLowerCase();\n const action = toolTargets[tool.name].action;\n\n switch (action) {\n case 'list':\n return `${indent}case '${tool.name}': {\n${indent} const limit = resolveListBound(args.limit, 'limit', ${DEFAULT_LIST_LIMIT}, ${MAX_LIST_LIMIT});\n${indent} const offset = resolveListBound(args.offset, 'offset', 0);\n${indent} // #2367: the tool schema advertises \\`orderBy\\`, so honour it; and page\n${indent} // deterministically when it is omitted — LIMIT/OFFSET with no ORDER BY\n${indent} // lets successive pages repeat and skip rows on PostgreSQL.\n${indent} const orderBy = args.orderBy ?? ${JSON.stringify(\n listOrderBy[objectName] ?? DEFAULT_LIST_ORDER_BY,\n )};\n${indent} const where = args.where ?? {};\n\n${indent} const collection = await ObjectRegistry.getCollection(${JSON.stringify(toolTargets[tool.name].objectName)}, {\n${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },\n${indent} ai: aiConfig\n${indent} });\n\n${indent} const items = await collection.list({ where, limit, offset, orderBy });\n${indent} const itemsPublic = items.map((item) => item.toPublicJSON(PUBLIC_JSON_OPTIONS));\n${indent} const structuredContent = {\n${indent} data: itemsPublic,\n${indent} meta: { total: await collection.count({ where }), limit, offset, count: items.length },\n${indent} };\n${indent} return successResult(structuredContent, JSON.stringify(itemsPublic));\n${indent}}`;\n\n case 'get':\n return `${indent}case '${tool.name}': {\n${indent} if (!args.id && !args.slug) {\n${indent} throw new Error('Either id or slug is required');\n${indent} }\n\n${indent} const collection = await ObjectRegistry.getCollection(${JSON.stringify(toolTargets[tool.name].objectName)}, {\n${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },\n${indent} ai: aiConfig\n${indent} });\n\n${indent} const filter = args.id || args.slug;\n${indent} const item = await collection.get(filter);\n\n${indent} if (!item) {\n${indent} throw new Error('Object not found');\n${indent} }\n\n${indent} return successResult(item.toPublicJSON(PUBLIC_JSON_OPTIONS));\n${indent}}`;\n\n case 'create':\n return `${indent}case '${tool.name}': {\n${indent} const { collection, objectName: targetObjectName } = await resolveCreateTarget(${JSON.stringify(objectName)}, args, aiConfig);\n\n${indent} const newItem = await collection.create(applyWritablePolicy(targetObjectName, args));\n${indent} await newItem.save();\n\n${indent} return successResult(newItem.toPublicJSON(PUBLIC_JSON_OPTIONS));\n${indent}}`;\n\n case 'update':\n return `${indent}case '${tool.name}': {\n${indent} const { id, ...updateData } = args;\n${indent} if (!id) {\n${indent} throw new Error('ID is required for update');\n${indent} }\n\n${indent} const collection = await ObjectRegistry.getCollection(${JSON.stringify(toolTargets[tool.name].objectName)}, {\n${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },\n${indent} ai: aiConfig\n${indent} });\n\n${indent} const existing = await collection.get(id);\n${indent} if (!existing) {\n${indent} throw new Error('Object not found');\n${indent} }\n\n${indent} Object.assign(existing, applyWritablePolicy(${JSON.stringify(toolTargets[tool.name].objectName)}, updateData));\n${indent} await existing.save();\n\n${indent} return successResult(existing.toPublicJSON(PUBLIC_JSON_OPTIONS));\n${indent}}`;\n\n case 'delete':\n return `${indent}case '${tool.name}': {\n${indent} if (!args.id) {\n${indent} throw new Error('ID is required for delete');\n${indent} }\n\n${indent} const collection = await ObjectRegistry.getCollection(${JSON.stringify(toolTargets[tool.name].objectName)}, {\n${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },\n${indent} ai: aiConfig\n${indent} });\n\n${indent} const toDelete = await collection.get(args.id);\n${indent} if (!toDelete) {\n${indent} throw new Error('Object not found');\n${indent} }\n\n${indent} await toDelete.delete();\n\n${indent} return successResult({ success: true, message: 'Object deleted successfully' });\n${indent}}`;\n\n default:\n // Custom action. Its descriptor is deliberately kept separate\n // from TOOLS so MCP clients receive only protocol-defined fields.\n return `${indent}case '${tool.name}': {\n${indent} const actionMeta = CUSTOM_ACTIONS['${tool.name}'] || { scope: 'item', isStatic: false, legacyOptions: true };\n${indent} const { id, options, ...directArgs } = args;\n\n${indent} if (actionMeta.scope === 'item' && !id) {\n${indent} throw new Error(${JSON.stringify(`ID is required for custom action ${action}`)});\n${indent} }\n${indent} if (actionMeta.scope === 'collection' && id) {\n${indent} throw new Error(${JSON.stringify(`Custom action ${action} is collection-scoped and does not accept an ID`)});\n${indent} }\n\n${indent} const collection = await ObjectRegistry.getCollection(${JSON.stringify(toolTargets[tool.name].objectName)}, {\n${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },\n${indent} ai: aiConfig\n${indent} });\n\n${indent} const target = actionMeta.scope === 'item'\n${indent} ? await collection.get(id)\n${indent} : actionMeta.isStatic\n${indent} ? ObjectRegistry.getClass(${JSON.stringify(toolTargets[tool.name].objectName)})?.constructor\n${indent} : collection;\n${indent} if (!target) {\n${indent} throw new Error(actionMeta.scope === 'item' ? 'Object not found' : 'Custom action target not found');\n${indent} }\n${indent} const actionMethod = target[actionMeta.methodName || ${JSON.stringify(action)}];\n${indent} if (typeof actionMethod !== 'function') {\n${indent} throw new Error(${JSON.stringify(`Method ${action} not found on custom action target`)});\n${indent} }\n\n${indent} const methodArgs = actionMeta.legacyOptions\n${indent} ? [Object.keys(options ?? {}).length > 0 ? options : directArgs]\n${indent} : actionMeta.optionsParameter\n${indent} ? [options]\n${indent} : (actionMeta.parameterNames || []).map((parameterName) => args[\n${indent} parameterName === 'id'\n${indent} ? 'actionId'\n${indent} : parameterName\n${indent} ]);\n${indent} const result = await actionMethod.call(target, ...methodArgs);\n${indent} const failure = normalizeCustomActionFailure(result);\n${indent} if (failure) {\n${indent} return errorResult(\n${indent} { error: failure },\n${indent} JSON.stringify({ error: failure }),\n${indent} { [SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY]: failure },\n${indent} );\n${indent} }\n\n${indent} const publicResult = toPublicResult(result);\n${indent} return successResult({ data: publicResult }, JSON.stringify(publicResult));\n${indent}}`;\n }\n })\n .join('\\n\\n');\n };\n\n const switchCases = generateSwitchCases(' ');\n\n return `#!/usr/bin/env node\n/**\n * Auto-generated MCP Server\n * Generated by @smrt/core MCPGenerator\n *\n * This server exposes SMRT objects as MCP tools for AI integration.\n *\n * SECURITY (#1540): tool responses exclude @field({ sensitive }) fields and\n * create/update bodies are mass-assignment guarded. This stdio server has NO\n * per-call authentication principal — its trust boundary is the host process /\n * MCP client that launches it. Run it only in a trusted context, or front it\n * with an authenticated gateway. Do not expose it directly to untrusted callers.\n */\n\nimport {\n type CallToolRequest,\n type ListToolsRequest,\n Server,\n ${hasTaskActions ? 'specTypeSchemas,' : ''}\n} from '@modelcontextprotocol/server';\nimport { ${hasTaskActions ? 'StdioServerTransport, ' : ''}serveStdio } from '@modelcontextprotocol/server/stdio';\nimport { existsSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { ObjectRegistry } from '@happyvertical/smrt-core';\nimport { normalizeCustomActionFailure, SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY } from '@happyvertical/smrt-core';\nimport { loadConfig } from '@happyvertical/smrt-config';\n${hasTaskActions ? \"import { McpTaskStore, TaskRunner } from '@happyvertical/smrt-jobs';\\n\" : ''}\n${hasTenantScoped ? \"import { enableTenancy, runTenantScopedEntryPoint } from '@happyvertical/smrt-tenancy';\\n\" : ''}\n// Server configuration\nconst SERVER_NAME = ${JSON.stringify(name)};\nconst SERVER_VERSION = ${JSON.stringify(version)};\nconst SERVER_DESCRIPTION = ${JSON.stringify(description)};\nconst DEBUG = ${debug};\n\n// Static tool definitions (generated at build time)\nconst TOOLS = ${toolsCode};\nconst TOOL_LIST_CACHE_HINT = ${JSON.stringify(toolListCacheHint)};\nconst TOOL_TARGETS: Record<string, { objectName: string; action: string }> = ${JSON.stringify(toolTargets)};\nconst CUSTOM_ACTIONS = ${JSON.stringify(customActions)};\nconst TASK_ACTIONS = ${JSON.stringify(taskActions)};\nconst STI_TARGETS: Record<string, Record<string, string>> = ${JSON.stringify(stiTargets)};\n${\n hasTenantScoped\n ? `\n// Fail-closed tenant context (#1554): tenant-scoped objects must run inside a\n// tenant. This stdio server has no auth principal, so the tenant is taken from\n// the environment; without it (and with tenancy enabled) tenant-scoped tools\n// throw rather than reading across all tenants.\nconst TENANT_SCOPED = new Set(${JSON.stringify(tenantScopedSet)});\nconst MCP_TENANT_ID = process.env.SMRT_MCP_TENANT_ID || undefined;\nconst MCP_ALLOW_CROSS_TENANT = process.env.SMRT_MCP_ALLOW_CROSS_TENANT === 'true';\n`\n : ''\n}\nconst PUBLIC_JSON_OPTIONS = {\n permissions: (process.env.SMRT_MCP_PERMISSIONS || '')\n .split(',')\n .map((permission) => permission.trim())\n .filter(Boolean),\n};\n\n/**\n * Page-bound guard (#2367). MCP tool arguments are untyped JSON, so \\`limit\\`\n * arrives as whatever the client sent: \\`\"abc\"\\` used to become \\`LIMIT NaN\\`\n * (a driver error, not a tool error), and nothing capped the value, so one\n * \\`limit: 100000000\\` call was a full table scan. Rejects anything that is not\n * a non-negative integer and clamps the rest to \\`max\\`.\n */\nfunction resolveListBound(raw: any, name: string, fallback: number, max?: number): number {\n if (raw === undefined || raw === null || raw === '') {\n return max === undefined ? fallback : Math.min(fallback, max);\n }\n const value = typeof raw === 'string' && /^\\\\d+$/.test(raw.trim()) ? Number(raw.trim()) : raw;\n if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {\n throw new Error(\"Invalid \" + name + \": expected a non-negative integer, got \" + JSON.stringify(raw));\n }\n return max === undefined ? value : Math.min(value, max);\n}\n\n/**\n * Mass-assignment guard (#1540): strip framework/server-managed and\n * \\`@field({ readonly: true })\\` fields from create/update bodies, intersecting\n * with the optional \\`@smrt({ api: { writable: [...] } })\\` allowlist.\n */\nfunction applyWritablePolicy(objectName: string, data: any): Record<string, any> {\n if (!data || typeof data !== 'object') return {};\n const serverManaged = new Set([\n 'id', 'tenantId', 'tenant_id',\n 'createdAt', 'created_at', 'updatedAt', 'updated_at',\n ]);\n const readonly = new Set<string>();\n let writable: string[] | null = null;\n const apiConfig = ObjectRegistry.getConfig(objectName)?.api as any;\n if (apiConfig && typeof apiConfig === 'object' && Array.isArray(apiConfig.writable)) {\n writable = apiConfig.writable;\n }\n for (const [name, def] of ObjectRegistry.getFields(objectName)) {\n if (def && ((def as any).readonly === true || (def as any)._meta?.readonly === true)) {\n readonly.add(name);\n }\n }\n const result: Record<string, any> = {};\n for (const [key, value] of Object.entries(data)) {\n if (key.startsWith('_')) continue;\n if (serverManaged.has(key)) continue;\n if (readonly.has(key)) continue;\n if (writable && !writable.includes(key)) continue;\n result[key] = value;\n }\n return result;\n}\n\n/** Resolve an advertised STI discriminator to its registered subtype collection. */\nasync function resolveCreateTarget(baseObjectName: string, args: Record<string, any>, aiConfig: any) {\n let objectName = baseObjectName;\n const discriminator = args._meta_type;\n const targets = STI_TARGETS[baseObjectName];\n if (typeof discriminator === 'string' && targets) {\n const target = targets[discriminator];\n if (!target) throw new Error('Unknown STI discriminator: ' + discriminator);\n objectName = target;\n }\n const collection = await ObjectRegistry.getCollection(objectName, {\n persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },\n ai: aiConfig,\n });\n return { collection, objectName };\n}\n\n/**\n * Sensitive-field-safe serialization for custom-action results (#1540).\n * Recurses through arrays and plain objects so nested SmrtObjects are stripped\n * too; non-plain instances (Date, etc.) and primitives pass through. Cycle-safe.\n */\nfunction toPublicResult(value: any, seen: WeakSet<object> = new WeakSet()): any {\n if (value === null || typeof value !== 'object') return value;\n if (typeof value.toPublicJSON === 'function') return value.toPublicJSON(PUBLIC_JSON_OPTIONS);\n if (Array.isArray(value)) {\n if (seen.has(value)) return value;\n seen.add(value);\n return value.map((entry: any) => toPublicResult(entry, seen));\n }\n const proto = Object.getPrototypeOf(value);\n if (proto !== Object.prototype && proto !== null) return value;\n if (seen.has(value)) return value;\n seen.add(value);\n const out: Record<string, any> = {};\n for (const [key, entry] of Object.entries(value)) {\n out[key] = toPublicResult(entry, seen);\n }\n return out;\n}\n\nfunction successResult(structuredContent: any, text = JSON.stringify(structuredContent)) {\n return {\n content: [{ type: 'text', text }],\n structuredContent,\n };\n}\n\nfunction errorResult(structuredContent: any, text: string, _meta?: Record<string, any>) {\n return {\n content: [{ type: 'text', text }],\n isError: true,\n structuredContent,\n ...(_meta ? { _meta } : {}),\n };\n}\n\n${\n hasTaskActions\n ? `\n// The bundled SDK validates tools/call responses against its older result\n// codec, which does not yet know CreateTaskResult. Intercept the extension at\n// the transport boundary, then pass every non-task message untouched to the\n// SDK server. This keeps ordinary MCP behaviour and version negotiation owned\n// by the SDK while making the extension available today.\nconst MCP_TASKS_EXTENSION = 'io.modelcontextprotocol/tasks';\nlet taskRuntime: Promise<{ store: McpTaskStore; runner: TaskRunner }> | undefined;\n\nfunction clientSupportsTasks(message: any): boolean {\n return message?.params?._meta?.['io.modelcontextprotocol/clientCapabilities']\n ?.extensions?.[MCP_TASKS_EXTENSION] !== undefined;\n}\n\n/** Validate the 2026 request envelope before task dispatch mutates a job. */\nfunction taskEnvelopeError(message: any): { code: number; message: string; data?: any } | undefined {\n const meta = message?.params?._meta;\n if (!meta || typeof meta !== 'object' || Array.isArray(meta)) {\n return {\n code: -32602,\n message: 'Request is missing the required _meta envelope for protocol revision 2026-07-28',\n };\n }\n const protocolVersion = meta['io.modelcontextprotocol/protocolVersion'];\n if (protocolVersion !== '2026-07-28') {\n return typeof protocolVersion === 'string'\n ? {\n code: -32022,\n message: 'Unsupported protocol version: ' + protocolVersion,\n data: { supported: ['2026-07-28'], requested: protocolVersion },\n }\n : {\n code: -32602,\n message: 'Invalid _meta envelope for protocol revision 2026-07-28: io.modelcontextprotocol/protocolVersion must be a string',\n };\n }\n if (!specTypeSchemas.ClientCapabilities.safeParse(\n meta['io.modelcontextprotocol/clientCapabilities'],\n ).success) {\n return {\n code: -32602,\n message: 'Invalid _meta envelope for protocol revision 2026-07-28: io.modelcontextprotocol/clientCapabilities is invalid',\n };\n }\n if (\n meta['io.modelcontextprotocol/clientInfo'] !== undefined &&\n !specTypeSchemas.Implementation.safeParse(\n meta['io.modelcontextprotocol/clientInfo'],\n ).success\n ) {\n return {\n code: -32602,\n message: 'Invalid _meta envelope for protocol revision 2026-07-28: io.modelcontextprotocol/clientInfo is invalid',\n };\n }\n if (\n meta['io.modelcontextprotocol/logLevel'] !== undefined &&\n !specTypeSchemas.LoggingLevel.safeParse(\n meta['io.modelcontextprotocol/logLevel'],\n ).success\n ) {\n return {\n code: -32602,\n message: 'Invalid _meta envelope for protocol revision 2026-07-28: io.modelcontextprotocol/logLevel is invalid',\n };\n }\n if (\n meta.progressToken !== undefined &&\n !specTypeSchemas.ProgressToken.safeParse(meta.progressToken).success\n ) {\n return {\n code: -32602,\n message: 'Invalid _meta envelope for protocol revision 2026-07-28: progressToken is invalid',\n };\n }\n return undefined;\n}\n\nfunction jsonRpcError(id: any, code: number, message: string, data?: any) {\n return { jsonrpc: '2.0', id, error: { code, message, ...(data === undefined ? {} : { data }) } };\n}\n\nasync function getTaskRuntime() {\n if (!taskRuntime) {\n taskRuntime = (async () => {\n const firstAction = Object.values(TASK_ACTIONS)[0] as { objectName: string } | undefined;\n if (!firstAction) throw new Error('No task-enabled MCP action is configured');\n const collection = await ObjectRegistry.getCollection(firstAction.objectName, {\n persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },\n });\n const db = collection.db;\n const store = await McpTaskStore.create(db, { ownerId: process.env.SMRT_MCP_TENANT_ID || null, tenantId: ${hasTenantScoped ? 'MCP_TENANT_ID ?? null' : 'null'} });\n const runner = new TaskRunner({ queues: ['mcp-tasks'] });\n await runner.initialize(db);\n await runner.start();\n return { store, runner };\n })();\n }\n return taskRuntime;\n}\n\nfunction taskInvocationArgs(actionMeta: any, args: Record<string, any>): any[] {\n const { id: _id, options, ...directArgs } = args;\n if (actionMeta.legacyOptions) {\n return [Object.keys(options ?? {}).length > 0 ? options : directArgs];\n }\n if (actionMeta.optionsParameter) return [options];\n const parameterNames = actionMeta.parameterNames || [];\n const invocationParameterNames = parameterNames.at(-1) === 'context'\n ? parameterNames.slice(0, -1)\n : parameterNames;\n return invocationParameterNames.map((parameterName: string) =>\n args[parameterName === 'id' ? 'actionId' : parameterName],\n );\n}\n\nasync function handleTaskExtensionMessage(message: any): Promise<any | null> {\n if (!message || typeof message !== 'object' || message.id === undefined) return null;\n const method = message.method;\n const params = message.params ?? {};\n const action = method === 'tools/call' ? TASK_ACTIONS[params.name] : undefined;\n const isTaskMethod = action || ['tasks/get', 'tasks/update', 'tasks/cancel'].includes(method);\n if (!isTaskMethod) return null;\n // A task-enabled tool retains its normal synchronous behaviour until its\n // client explicitly opts into Tasks. Do not impose the task envelope on\n // legacy calls that will be passed untouched to the SDK.\n if (!clientSupportsTasks(message) && method === 'tools/call') return null;\n const envelopeError = taskEnvelopeError(message);\n if (envelopeError) {\n return jsonRpcError(message.id, envelopeError.code, envelopeError.message, envelopeError.data);\n }\n if (!clientSupportsTasks(message)) {\n // A task-only method cannot fall back to a legacy response shape. A task\n // tool can still run normally when the client did not opt in, so let the\n // SDK handle that direct tools/call path.\n if (method === 'tools/call') return null;\n return jsonRpcError(message.id, -32021, 'Missing required client capability', {\n requiredCapabilities: { extensions: { [MCP_TASKS_EXTENSION]: {} } },\n });\n }\n try {\n const { store } = await getTaskRuntime();\n if (action) {\n if (typeof params.arguments?.id !== 'string') {\n return jsonRpcError(message.id, -32602, 'Task-enabled custom actions require an id');\n }\n const actionMeta = CUSTOM_ACTIONS[params.name];\n const task = await store.createTask({\n objectType: action.objectType,\n objectId: params.arguments.id,\n method: actionMeta.methodName || TOOL_TARGETS[params.name]?.action,\n invocationArgs: taskInvocationArgs(actionMeta, params.arguments),\n tenantId: ${hasTenantScoped ? 'MCP_TENANT_ID ?? null' : 'null'},\n });\n return { jsonrpc: '2.0', id: message.id, result: { resultType: 'task', content: [], structuredContent: {}, ...task } };\n }\n if (typeof params.taskId !== 'string') {\n return jsonRpcError(message.id, -32602, 'taskId is required');\n }\n if (method === 'tasks/get') {\n const task = await store.getTask(params.taskId);\n return { jsonrpc: '2.0', id: message.id, result: { resultType: 'complete', ...task } };\n }\n if (method === 'tasks/update') {\n await store.updateTask(\n params.taskId,\n params.inputResponses && typeof params.inputResponses === 'object'\n ? params.inputResponses\n : {},\n );\n return { jsonrpc: '2.0', id: message.id, result: { resultType: 'complete' } };\n }\n await store.cancelTask(params.taskId);\n return { jsonrpc: '2.0', id: message.id, result: { resultType: 'complete' } };\n } catch (error) {\n const messageText = error instanceof Error ? error.message : 'Task operation failed';\n return jsonRpcError(message.id, -32602, messageText);\n }\n}\n\nclass McpTaskExtensionTransport {\n onclose?: () => void;\n onerror?: (error: Error) => void;\n onmessage?: (message: any) => void;\n\n // An explicit field, not a parameter property: a \\`.ts\\` target has to stay\n // erasable-syntax-only so Node's type stripping can run it (#2279).\n wire: StdioServerTransport;\n\n constructor(wire: StdioServerTransport) {\n this.wire = wire;\n }\n\n async start() {\n this.wire.onclose = () => this.onclose?.();\n this.wire.onerror = (error) => this.onerror?.(error);\n this.wire.onmessage = (message) => {\n void (async () => {\n const response = await handleTaskExtensionMessage(message);\n if (response) await this.wire.send(response);\n else this.onmessage?.(message);\n })().catch((error) => this.onerror?.(error instanceof Error ? error : new Error(String(error))));\n };\n await this.wire.start();\n }\n\n close() { return this.wire.close(); }\n send(message: any) { return this.wire.send(message); }\n}\n`\n : ''\n}\n\n/**\n * Main server startup function\n */\nexport async function createServer(): Promise<Server> {\n if (DEBUG) {\n console.error(\\`[MCP] Starting server: \\${SERVER_NAME} v\\${SERVER_VERSION}\\`);\n }\n${\n hasTenantScoped\n ? `\n // Fail-closed tenant context (#1554): install the tenancy interceptor so\n // tenant-scoped tools are actually filtered, and so the entry-point gate\n // throws (rather than passing through) when no tenant is supplied. Without\n // this, a tenant set via SMRT_MCP_TENANT_ID would only set async context\n // with no interceptor to enforce it.\n enableTenancy();\n`\n : ''\n}\n // Register the application package manifest before resolving generated\n // object names. Generated servers are commonly run from the application\n // package itself, which is not a node_modules dependency of its process.\n const localManifestPaths = [\n resolve(process.cwd(), 'dist', 'manifest.json'),\n resolve(process.cwd(), '.smrt', 'manifest.json'),\n ].filter(existsSync);\n if (localManifestPaths.length > 0) {\n ObjectRegistry.loadAllManifests({ manifestPaths: localManifestPaths });\n }\n\n // A manifest supplies schemas and tool metadata, while an executable\n // custom action also needs the application's actual class constructor.\n // Consumer builds generate this registration module; load it after the\n // manifest so decorators enrich the already-known metadata.\n const localRegisterPath = process.env.SMRT_MCP_REGISTER_PATH\n || resolve(process.cwd(), '.smrt', 'register.js');\n if (existsSync(localRegisterPath)) {\n await import(pathToFileURL(localRegisterPath).href);\n }\n\n // Load configuration from environment and .smrt.config files\n const appConfig = await loadConfig();\n const aiConfig = appConfig?.ai || {};\n\n if (DEBUG) {\n console.error(\\`[MCP] Loaded \\${TOOLS.length} static tools\\`);\n console.error(\\`[MCP] Available tools:\\`, TOOLS.map(t => t.name).join(', '));\n }\n\n // Create MCP server\n const server = new Server(\n {\n name: SERVER_NAME,\n version: SERVER_VERSION,\n },\n {\n capabilities: {\n tools: {},\n ${hasTaskActions ? \"extensions: { 'io.modelcontextprotocol/tasks': {} },\" : ''}\n },\n cacheHints: {\n 'tools/list': TOOL_LIST_CACHE_HINT,\n },\n }\n );\n\n // Register ListTools handler\n server.setRequestHandler('tools/list', async (_request: ListToolsRequest) => {\n if (DEBUG) {\n console.error(\\`[MCP] ListTools request received\\`);\n }\n\n return {\n tools: [...TOOLS].sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0),\n };\n });\n\n // Register CallTool handler\n server.setRequestHandler('tools/call', async (request: CallToolRequest) => {\n const { name: toolName, arguments: args = {} } = request.params;\n\n if (DEBUG) {\n console.error(\\`[MCP] CallTool request: \\${toolName}\\`);\n console.error(\\`[MCP] Arguments:\\`, JSON.stringify(args, null, 2));\n }\n\n try {\n // Static switch statement for tool execution\n const runToolBody = async () => {\n switch (toolName) {\n${switchCases}\n\n default:\n throw new Error(\\`Unknown tool: \\${toolName}\\`);\n }\n };\n${\n hasTenantScoped\n ? `\n // Fail-closed tenant context for tenant-scoped tools (#1554).\n const toolObject = TOOL_TARGETS[toolName]?.objectName;\n const result =\n toolObject && TENANT_SCOPED.has(toolObject.toLowerCase())\n ? await runTenantScopedEntryPoint(\n { tenantScoped: true, tenantId: MCP_TENANT_ID, allowCrossTenant: MCP_ALLOW_CROSS_TENANT, surface: 'MCP' },\n runToolBody,\n )\n : await runToolBody();`\n : `\n const result = await runToolBody();`\n}\n\n if (DEBUG) {\n console.error(\\`[MCP] Tool executed successfully: \\${toolName}\\`);\n }\n\n return result;\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : 'Unknown error';\n console.error(\\`[MCP] Tool execution failed: \\${toolName}\\`, error);\n\n return errorResult(\n { error: { message: errorMessage } },\n \\`Error executing tool \\${toolName}: \\${errorMessage}\\`,\n );\n }\n });\n\n return server;\n}\n\nasync function main() {\n try {\n // Initialize the registry before accepting an intercepted task call. The\n // SDK would normally invoke this factory for the first regular message,\n // but task calls can be the first message on a stdio connection.\n const server = await createServer();\n const transport = ${hasTaskActions ? 'new McpTaskExtensionTransport(new StdioServerTransport())' : 'undefined'};\n const handle = serveStdio(() => server, {\n ...(transport ? { transport } : {}),\n onerror: (error) => console.error('[MCP] Protocol error:', error),\n });\n const shutdown = async () => {\n if (DEBUG) console.error('[MCP] Shutting down gracefully');\n ${hasTaskActions ? 'if (taskRuntime) {\\n const { runner } = await taskRuntime;\\n await runner.stop();\\n }' : ''}\n await handle.close();\n process.exit(0);\n };\n process.on('SIGINT', shutdown);\n process.on('SIGTERM', shutdown);\n } catch (error) {\n console.error('[MCP] Fatal error during server startup:', error);\n process.exit(1);\n }\n}\n\n// Start only when executed, so adapters and tests may import the factory.\nif (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {\n main().catch((error) => {\n console.error('[MCP] Unhandled error:', error);\n process.exit(1);\n });\n}\n`;\n}\n\n/**\n * Generate package.json script for running MCP server\n *\n * @param serverPath - Path to generated server file (relative to package root)\n * @returns Script command for package.json\n */\nexport function generateMCPScript(\n serverPath: string = 'dist/mcp-server.js',\n): string {\n return `node ${serverPath}`;\n}\n\n/**\n * Generate Claude Desktop configuration example\n *\n * @param serverName - Name for the MCP server\n * @param serverPath - Absolute path to server file\n * @returns Configuration object for claude_desktop_config.json\n */\nexport function generateClaudeConfig(\n serverName: string,\n serverPath: string,\n): object {\n return {\n mcpServers: {\n [serverName]: {\n command: 'node',\n args: [serverPath],\n },\n },\n };\n}\n\n/**\n * Generate README documentation for MCP server setup\n *\n * @param serverName - Name of the MCP server\n * @param serverPath - Path to the server file\n * @returns Markdown documentation\n */\nexport function generateMCPDocumentation(\n serverName: string,\n serverPath: string,\n): string {\n return `# MCP Server Setup\n\nThis project includes an auto-generated MCP (Model Context Protocol) server that exposes SMRT objects as tools for AI integration.\n\n## Quick Start\n\n### 1. Build the MCP Server\n\n\\`\\`\\`bash\nnpm run build\n\\`\\`\\`\n\nThis generates the MCP server at: \\`${serverPath}\\`\n\n### 2. Configure Claude Desktop\n\nAdd the following to your Claude Desktop configuration file:\n\n**macOS**: \\`~/.config/Claude/claude_desktop_config.json\\`\n**Windows**: \\`%APPDATA%\\\\Claude\\\\claude_desktop_config.json\\`\n\n\\`\\`\\`json\n{\n \"mcpServers\": {\n \"${serverName}\": {\n \"command\": \"node\",\n \"args\": [\"/absolute/path/to/${serverPath}\"]\n }\n }\n}\n\\`\\`\\`\n\nReplace \\`/absolute/path/to/\\` with the actual absolute path to your project directory.\n\n### 3. Restart Claude Desktop\n\nClose and reopen Claude Desktop to load the new MCP server.\n\n### 4. Test the Integration\n\nIn Claude Code, you can now use the auto-generated tools. For example:\n\n- \\`list_products\\` - List all products\n- \\`get_product\\` - Get a specific product by ID\n- \\`create_product\\` - Create a new product\n- And more...\n\n## Environment Variables\n\nThe MCP server supports optional environment variables:\n\n- \\`DATABASE_URL\\` - Database connection string\n\n**AI Provider Configuration (in priority order):**\n1. **\\`ai\\` block in \\`smrt.config.ts\\`** (declare once; keep the secret in the environment):\n - \\`ai: { provider: 'openai', model: 'gpt-4o', apiKeyEnv: 'OPENAI_API_KEY' }\\`\n\n2. **Generic environment configuration** (supports any provider):\n - \\`SMRT_AI_PROVIDER\\` - Provider name (e.g., 'openai', 'anthropic', 'claude-cli', 'gemini')\n - \\`SMRT_AI_API_KEY\\` - API key for the provider\n - \\`SMRT_AI_MODEL\\` - Model to use (optional)\n - \\`SMRT_AI_BASE_URL\\` - Endpoint override (optional)\n\n3. **Provider-specific key fallbacks** (used when the \\`ai\\` block selects the provider, and only for that provider):\n - \\`OPENAI_API_KEY\\`, \\`ANTHROPIC_API_KEY\\`, \\`GEMINI_API_KEY\\`\n - \\`CLAUDE_API_KEY\\` + \\`CLAUDE_MODEL\\` - Claude CLI provider (defaults to 'sonnet'); read by \\`@happyvertical/ai\\`\n\n**Examples:**\n\\`\\`\\`bash\n# Using generic configuration (recommended)\nexport SMRT_AI_PROVIDER=claude-cli\nexport SMRT_AI_MODEL=sonnet\n\n# Using provider-specific configuration\nexport CLAUDE_API_KEY=your-key\nexport CLAUDE_MODEL=sonnet\n\n# Using OpenAI\nexport OPENAI_API_KEY=your-openai-key\n\\`\\`\\`\n\n## Troubleshooting\n\n### Server Not Appearing in Claude\n\n1. Check that the path in \\`claude_desktop_config.json\\` is absolute\n2. Verify the server file exists at the specified path\n3. Check Claude Desktop logs for errors\n\n### Tools Not Working\n\n1. Ensure your database is accessible (if using one)\n2. Check that SMRT objects are properly decorated with \\`@smrt()\\`\n3. Look for errors in the MCP server output\n\n### Debug Mode\n\nTo enable debug logging, set the \\`DEBUG\\` constant to \\`true\\` in the generated server file.\n\n## Generated Tools\n\nThe following tools are automatically generated from your SMRT objects:\n\n- **CRUD Operations**: \\`list_\\`, \\`get_\\`, \\`create_\\`, \\`update_\\`, \\`delete_\\` for each object type\n- **Custom Actions**: Any custom methods included in the \\`@smrt()\\` decorator configuration\n\nSee the SMRT object definitions for the complete list of available tools and their parameters.\n`;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"mcp-runtime-template.js","names":[],"sources":["../../src/generators/mcp-runtime-template.ts"],"sourcesContent":["/**\n * Runtime bootstrap template for generated MCP servers\n *\n * This template provides stdio transport integration for SMRT-generated MCP servers.\n * It handles:\n * - Server initialization with @modelcontextprotocol/server v2\n * - Tool registration from MCPGenerator\n * - Stdio transport connection\n * - Error handling and logging\n * - Graceful shutdown\n */\n\nimport {\n DEFAULT_LIST_LIMIT,\n DEFAULT_LIST_ORDER_BY,\n MAX_LIST_LIMIT,\n} from '../query-bounds.js';\nimport type { CustomActionScope } from './custom-action.js';\nimport type { MCPConfig, MCPContext } from './mcp.js';\nimport type { McpToolTarget } from './mcp-tool-name.js';\n\n/**\n * Helper function to capitalize first letter\n */\nfunction capitalize(str: string): string {\n return str.charAt(0).toUpperCase() + str.slice(1);\n}\n\nexport interface RuntimeOptions {\n /** Server name (defaults to package name) */\n name?: string;\n\n /** Server version (defaults to package version) */\n version?: string;\n\n /** Server description */\n description?: string;\n\n /** MCP generator configuration */\n config?: MCPConfig;\n\n /** MCP context (database, AI client, etc.) */\n context?: MCPContext;\n\n /** Enable debug logging */\n debug?: boolean;\n\n /** Static tool definitions (generated at build time) */\n tools?: Array<{\n name: string;\n description: string;\n inputSchema: Record<string, unknown>;\n outputSchema?: Record<string, unknown>;\n annotations?: {\n readOnlyHint: boolean;\n destructiveHint: boolean;\n idempotentHint: boolean;\n openWorldHint: boolean;\n };\n title?: string;\n icons?: Array<{\n src: string;\n mimeType?: string;\n sizes?: string[];\n theme?: 'light' | 'dark';\n }>;\n _meta?: Record<string, unknown>;\n }>;\n /** Explicit original execution targets for generated protocol aliases. */\n toolTargets?: Record<string, McpToolTarget>;\n /** Cache hint emitted for deploy-static tools/list results. */\n toolListCacheHint?: {\n ttlMs: number;\n cacheScope: 'private' | 'public';\n };\n /** Internal invocation metadata; never exposed by the MCP tools/list result. */\n customActions?: Record<\n string,\n {\n scope: CustomActionScope;\n isStatic: boolean;\n /** Declared method name; tool IDs are lowercased protocol aliases. */\n methodName?: string;\n parameterNames?: string[];\n optionsParameter?: boolean;\n legacyOptions: boolean;\n }\n >;\n /** Task-enabled item custom actions. Never emitted in tools/list. */\n taskActions?: Record<\n string,\n {\n objectName: string;\n objectType: string;\n /** Registry identity of the task's class (#3490). */\n registryKey?: string;\n }\n >;\n\n /**\n * Lowercased simple names of objects that are `@TenantScoped` (#1554). When\n * non-empty, the generated server imports the tenancy fail-closed gate and\n * wraps tenant-scoped tool calls so a stdio invocation cannot read across all\n * tenants. The tenant is sourced from `SMRT_MCP_TENANT_ID` /\n * `SMRT_MCP_ALLOW_CROSS_TENANT` env vars (this server has no auth principal).\n */\n tenantScopedObjects?: string[];\n\n /**\n * Build-time approved STI discriminators, keyed by the lowercased MCP object\n * prefix. The generated runtime uses this instead of searching an initially\n * empty registry, then loads the approved qualified type through the public\n * collection API.\n */\n stiTargets?: Record<string, Record<string, string>>;\n\n /**\n * Default list ordering per lowercased MCP object prefix (#2367).\n *\n * Baked in at generation time because the tiebreak follows the model's\n * declared primary key, which the emitted server cannot resolve on its own.\n * Prefixes absent from this map fall back to {@link DEFAULT_LIST_ORDER_BY}.\n */\n listOrderBy?: Record<string, readonly string[]>;\n}\n\n/**\n * Generate runtime bootstrap code for MCP server\n *\n * @param options - Runtime configuration options\n * @returns TypeScript code for server entry point\n */\nexport function generateRuntimeBootstrap(options: RuntimeOptions = {}): string {\n const {\n name = 'smrt-mcp-server',\n version = '1.0.0',\n description = 'Auto-generated MCP server from SMRT objects',\n debug = false,\n tools = [],\n toolTargets: explicitToolTargets,\n customActions = {},\n taskActions = {},\n tenantScopedObjects = [],\n stiTargets = {},\n listOrderBy = {},\n toolListCacheHint = { ttlMs: 86_400_000, cacheScope: 'private' },\n } = options;\n\n // Direct template callers retain their legacy object_action inputs. The\n // owning generator always supplies explicit targets, including valid aliases.\n const toolTargets: Record<string, McpToolTarget> =\n explicitToolTargets ??\n Object.fromEntries(\n tools.map((tool) => {\n const separator = tool.name.indexOf('_');\n return [\n tool.name,\n {\n objectName: capitalize(tool.name.slice(0, separator)),\n action: tool.name.slice(separator + 1),\n },\n ];\n }),\n );\n for (const tool of tools) {\n if (!Object.hasOwn(toolTargets, tool.name)) {\n throw new Error(`Missing MCP tool target: ${tool.name}`);\n }\n }\n\n // Generate static tool array as TypeScript code\n const toolsCode = tools.length > 0 ? JSON.stringify(tools, null, 2) : '[]';\n const hasTaskActions = Object.keys(taskActions).length > 0;\n\n // Fail-closed tenant context (#1554): only wire the tenancy gate when at\n // least one exposed object is tenant-scoped, so apps without tenancy never\n // get a dangling import.\n const tenantScopedSet = Array.from(\n new Set(tenantScopedObjects.map((n) => n.toLowerCase())),\n );\n const hasTenantScoped = tenantScopedSet.length > 0;\n\n // Generate static switch cases using shared helper\n const generateSwitchCases = (indent: string) => {\n return tools\n .map((tool) => {\n const objectName = toolTargets[tool.name].objectName.toLowerCase();\n const action = toolTargets[tool.name].action;\n // Registry identity for every emitted lookup (#3490).\n const lookup =\n toolTargets[tool.name].registryKey ??\n toolTargets[tool.name].objectName;\n\n switch (action) {\n case 'list':\n return `${indent}case '${tool.name}': {\n${indent} const limit = resolveListBound(args.limit, 'limit', ${DEFAULT_LIST_LIMIT}, ${MAX_LIST_LIMIT});\n${indent} const offset = resolveListBound(args.offset, 'offset', 0);\n${indent} // #2367: the tool schema advertises \\`orderBy\\`, so honour it; and page\n${indent} // deterministically when it is omitted — LIMIT/OFFSET with no ORDER BY\n${indent} // lets successive pages repeat and skip rows on PostgreSQL.\n${indent} const orderBy = args.orderBy ?? ${JSON.stringify(\n listOrderBy[objectName] ?? DEFAULT_LIST_ORDER_BY,\n )};\n${indent} const where = args.where ?? {};\n\n${indent} const collection = await ObjectRegistry.getCollection(${JSON.stringify(lookup)}, {\n${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },\n${indent} ai: aiConfig\n${indent} });\n\n${indent} const items = await collection.list({ where, limit, offset, orderBy });\n${indent} const itemsPublic = items.map((item) => item.toPublicJSON(PUBLIC_JSON_OPTIONS));\n${indent} const structuredContent = {\n${indent} data: itemsPublic,\n${indent} meta: { total: await collection.count({ where }), limit, offset, count: items.length },\n${indent} };\n${indent} return successResult(structuredContent, JSON.stringify(itemsPublic));\n${indent}}`;\n\n case 'get':\n return `${indent}case '${tool.name}': {\n${indent} if (!args.id && !args.slug) {\n${indent} throw new Error('Either id or slug is required');\n${indent} }\n\n${indent} const collection = await ObjectRegistry.getCollection(${JSON.stringify(lookup)}, {\n${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },\n${indent} ai: aiConfig\n${indent} });\n\n${indent} const filter = args.id || args.slug;\n${indent} const item = await collection.get(filter);\n\n${indent} if (!item) {\n${indent} throw new Error('Object not found');\n${indent} }\n\n${indent} return successResult(item.toPublicJSON(PUBLIC_JSON_OPTIONS));\n${indent}}`;\n\n case 'create':\n return `${indent}case '${tool.name}': {\n${indent} const { collection, objectName: targetObjectName } = await resolveCreateTarget(${JSON.stringify(objectName)}, ${JSON.stringify(lookup)}, args, aiConfig);\n\n${indent} const newItem = await collection.create(applyWritablePolicy(targetObjectName, args));\n\n${indent} return successResult(newItem.toPublicJSON(PUBLIC_JSON_OPTIONS));\n${indent}}`;\n\n case 'update':\n return `${indent}case '${tool.name}': {\n${indent} const { id, ...updateData } = args;\n${indent} if (!id) {\n${indent} throw new Error('ID is required for update');\n${indent} }\n\n${indent} const collection = await ObjectRegistry.getCollection(${JSON.stringify(lookup)}, {\n${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },\n${indent} ai: aiConfig\n${indent} });\n\n${indent} const existing = await collection.get(id);\n${indent} if (!existing) {\n${indent} throw new Error('Object not found');\n${indent} }\n\n${indent} await collection.withAuditMutation(existing, 'updated', async (bound) => {\n${indent} Object.assign(bound, applyWritablePolicy(${JSON.stringify(lookup)}, updateData));\n${indent} await bound.save();\n${indent} });\n\n${indent} return successResult(existing.toPublicJSON(PUBLIC_JSON_OPTIONS));\n${indent}}`;\n\n case 'delete':\n return `${indent}case '${tool.name}': {\n${indent} if (!args.id) {\n${indent} throw new Error('ID is required for delete');\n${indent} }\n\n${indent} const collection = await ObjectRegistry.getCollection(${JSON.stringify(lookup)}, {\n${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },\n${indent} ai: aiConfig\n${indent} });\n\n${indent} const toDelete = await collection.get(args.id);\n${indent} if (!toDelete) {\n${indent} throw new Error('Object not found');\n${indent} }\n\n${indent} await collection.delete(args.id);\n\n${indent} return successResult({ success: true, message: 'Object deleted successfully' });\n${indent}}`;\n\n default:\n // Custom action. Its descriptor is deliberately kept separate\n // from TOOLS so MCP clients receive only protocol-defined fields.\n return `${indent}case '${tool.name}': {\n${indent} const actionMeta = CUSTOM_ACTIONS['${tool.name}'] || { scope: 'item', isStatic: false, legacyOptions: true };\n${indent} const { id, options, ...directArgs } = args;\n\n${indent} if (actionMeta.scope === 'item' && !id) {\n${indent} throw new Error(${JSON.stringify(`ID is required for custom action ${action}`)});\n${indent} }\n${indent} if (actionMeta.scope === 'collection' && id) {\n${indent} throw new Error(${JSON.stringify(`Custom action ${action} is collection-scoped and does not accept an ID`)});\n${indent} }\n\n${indent} const collection = await ObjectRegistry.getCollection(${JSON.stringify(lookup)}, {\n${indent} persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },\n${indent} ai: aiConfig\n${indent} });\n\n${indent} const target = actionMeta.scope === 'item'\n${indent} ? await collection.get(id)\n${indent} : actionMeta.isStatic\n${indent} ? ObjectRegistry.getClass(${JSON.stringify(lookup)})?.constructor\n${indent} : collection;\n${indent} if (!target) {\n${indent} throw new Error(actionMeta.scope === 'item' ? 'Object not found' : 'Custom action target not found');\n${indent} }\n${indent} const actionMethod = target[actionMeta.methodName || ${JSON.stringify(action)}];\n${indent} if (typeof actionMethod !== 'function') {\n${indent} throw new Error(${JSON.stringify(`Method ${action} not found on custom action target`)});\n${indent} }\n\n${indent} const methodArgs = actionMeta.legacyOptions\n${indent} ? [Object.keys(options ?? {}).length > 0 ? options : directArgs]\n${indent} : actionMeta.optionsParameter\n${indent} ? [options]\n${indent} : (actionMeta.parameterNames || []).map((parameterName) => args[\n${indent} parameterName === 'id'\n${indent} ? 'actionId'\n${indent} : parameterName\n${indent} ]);\n${indent} const result = await actionMethod.call(target, ...methodArgs);\n${indent} const failure = normalizeCustomActionFailure(result);\n${indent} if (failure) {\n${indent} return errorResult(\n${indent} { error: failure },\n${indent} JSON.stringify({ error: failure }),\n${indent} { [SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY]: failure },\n${indent} );\n${indent} }\n\n${indent} const publicResult = toPublicResult(result);\n${indent} return successResult({ data: publicResult }, JSON.stringify(publicResult));\n${indent}}`;\n }\n })\n .join('\\n\\n');\n };\n\n const switchCases = generateSwitchCases(' ');\n\n return `#!/usr/bin/env node\n/**\n * Auto-generated MCP Server\n * Generated by @smrt/core MCPGenerator\n *\n * This server exposes SMRT objects as MCP tools for AI integration.\n *\n * SECURITY (#1540): tool responses exclude @field({ sensitive }) fields and\n * create/update bodies are mass-assignment guarded. This stdio server has NO\n * per-call authentication principal — its trust boundary is the host process /\n * MCP client that launches it. Run it only in a trusted context, or front it\n * with an authenticated gateway. Do not expose it directly to untrusted callers.\n */\n\nimport {\n type CallToolRequest,\n type ListToolsRequest,\n Server,\n ${hasTaskActions ? 'specTypeSchemas,' : ''}\n} from '@modelcontextprotocol/server';\nimport { ${hasTaskActions ? 'StdioServerTransport, ' : ''}serveStdio } from '@modelcontextprotocol/server/stdio';\nimport { existsSync } from 'node:fs';\nimport { resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { ObjectRegistry } from '@happyvertical/smrt-core';\nimport { normalizeCustomActionFailure, SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY } from '@happyvertical/smrt-core';\nimport { loadConfig } from '@happyvertical/smrt-config';\n${hasTaskActions ? \"import { McpTaskStore, TaskRunner } from '@happyvertical/smrt-jobs';\\n\" : ''}\n${hasTenantScoped ? \"import { enableTenancy, runTenantScopedEntryPoint } from '@happyvertical/smrt-tenancy';\\n\" : ''}\n// Server configuration\nconst SERVER_NAME = ${JSON.stringify(name)};\nconst SERVER_VERSION = ${JSON.stringify(version)};\nconst SERVER_DESCRIPTION = ${JSON.stringify(description)};\nconst DEBUG = ${debug};\n\n// Static tool definitions (generated at build time)\nconst TOOLS = ${toolsCode};\nconst TOOL_LIST_CACHE_HINT = ${JSON.stringify(toolListCacheHint)};\nconst TOOL_TARGETS: Record<string, { objectName: string; action: string; registryKey?: string }> = ${JSON.stringify(toolTargets)};\nconst CUSTOM_ACTIONS = ${JSON.stringify(customActions)};\nconst TASK_ACTIONS = ${JSON.stringify(taskActions)};\nconst STI_TARGETS: Record<string, Record<string, string>> = ${JSON.stringify(stiTargets)};\n${\n hasTenantScoped\n ? `\n// Fail-closed tenant context (#1554): tenant-scoped objects must run inside a\n// tenant. This stdio server has no auth principal, so the tenant is taken from\n// the environment; without it (and with tenancy enabled) tenant-scoped tools\n// throw rather than reading across all tenants.\nconst TENANT_SCOPED = new Set(${JSON.stringify(tenantScopedSet)});\nconst MCP_TENANT_ID = process.env.SMRT_MCP_TENANT_ID || undefined;\nconst MCP_ALLOW_CROSS_TENANT = process.env.SMRT_MCP_ALLOW_CROSS_TENANT === 'true';\n`\n : ''\n}\nconst PUBLIC_JSON_OPTIONS = {\n permissions: (process.env.SMRT_MCP_PERMISSIONS || '')\n .split(',')\n .map((permission) => permission.trim())\n .filter(Boolean),\n};\n\n/**\n * Page-bound guard (#2367). MCP tool arguments are untyped JSON, so \\`limit\\`\n * arrives as whatever the client sent: \\`\"abc\"\\` used to become \\`LIMIT NaN\\`\n * (a driver error, not a tool error), and nothing capped the value, so one\n * \\`limit: 100000000\\` call was a full table scan. Rejects anything that is not\n * a non-negative integer and clamps the rest to \\`max\\`.\n */\nfunction resolveListBound(raw: any, name: string, fallback: number, max?: number): number {\n if (raw === undefined || raw === null || raw === '') {\n return max === undefined ? fallback : Math.min(fallback, max);\n }\n const value = typeof raw === 'string' && /^\\\\d+$/.test(raw.trim()) ? Number(raw.trim()) : raw;\n if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {\n throw new Error(\"Invalid \" + name + \": expected a non-negative integer, got \" + JSON.stringify(raw));\n }\n return max === undefined ? value : Math.min(value, max);\n}\n\n/**\n * Mass-assignment guard (#1540): strip framework/server-managed and\n * \\`@field({ readonly: true })\\` fields from create/update bodies, intersecting\n * with the optional \\`@smrt({ api: { writable: [...] } })\\` allowlist.\n */\nfunction applyWritablePolicy(objectName: string, data: any): Record<string, any> {\n if (!data || typeof data !== 'object') return {};\n const serverManaged = new Set([\n 'id', 'tenantId', 'tenant_id',\n 'createdAt', 'created_at', 'updatedAt', 'updated_at',\n ]);\n const readonly = new Set<string>();\n let writable: string[] | null = null;\n const apiConfig = ObjectRegistry.getConfig(objectName)?.api as any;\n if (apiConfig && typeof apiConfig === 'object' && Array.isArray(apiConfig.writable)) {\n writable = apiConfig.writable;\n }\n for (const [name, def] of ObjectRegistry.getFields(objectName)) {\n if (def && ((def as any).readonly === true || (def as any)._meta?.readonly === true)) {\n readonly.add(name);\n }\n }\n const result: Record<string, any> = {};\n for (const [key, value] of Object.entries(data)) {\n if (key.startsWith('_')) continue;\n if (serverManaged.has(key)) continue;\n if (readonly.has(key)) continue;\n if (writable && !writable.includes(key)) continue;\n result[key] = value;\n }\n return result;\n}\n\n/** Resolve an advertised STI discriminator to its registered subtype collection. */\nasync function resolveCreateTarget(stiKey: string, baseObjectName: string, args: Record<string, any>, aiConfig: any) {\n let objectName = baseObjectName;\n const discriminator = args._meta_type;\n const targets = STI_TARGETS[stiKey];\n if (typeof discriminator === 'string' && targets) {\n const target = targets[discriminator];\n if (!target) throw new Error('Unknown STI discriminator: ' + discriminator);\n objectName = target;\n }\n const collection = await ObjectRegistry.getCollection(objectName, {\n persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },\n ai: aiConfig,\n });\n return { collection, objectName };\n}\n\n/**\n * Sensitive-field-safe serialization for custom-action results (#1540).\n * Recurses through arrays and plain objects so nested SmrtObjects are stripped\n * too; non-plain instances (Date, etc.) and primitives pass through. Cycle-safe.\n */\nfunction toPublicResult(value: any, seen: WeakSet<object> = new WeakSet()): any {\n if (value === null || typeof value !== 'object') return value;\n if (typeof value.toPublicJSON === 'function') return value.toPublicJSON(PUBLIC_JSON_OPTIONS);\n if (Array.isArray(value)) {\n if (seen.has(value)) return value;\n seen.add(value);\n return value.map((entry: any) => toPublicResult(entry, seen));\n }\n const proto = Object.getPrototypeOf(value);\n if (proto !== Object.prototype && proto !== null) return value;\n if (seen.has(value)) return value;\n seen.add(value);\n const out: Record<string, any> = {};\n for (const [key, entry] of Object.entries(value)) {\n out[key] = toPublicResult(entry, seen);\n }\n return out;\n}\n\nfunction successResult(structuredContent: any, text = JSON.stringify(structuredContent)) {\n return {\n content: [{ type: 'text', text }],\n structuredContent,\n };\n}\n\nfunction errorResult(structuredContent: any, text: string, _meta?: Record<string, any>) {\n return {\n content: [{ type: 'text', text }],\n isError: true,\n structuredContent,\n ...(_meta ? { _meta } : {}),\n };\n}\n\n${\n hasTaskActions\n ? `\n// The bundled SDK validates tools/call responses against its older result\n// codec, which does not yet know CreateTaskResult. Intercept the extension at\n// the transport boundary, then pass every non-task message untouched to the\n// SDK server. This keeps ordinary MCP behaviour and version negotiation owned\n// by the SDK while making the extension available today.\nconst MCP_TASKS_EXTENSION = 'io.modelcontextprotocol/tasks';\nlet taskRuntime: Promise<{ store: McpTaskStore; runner: TaskRunner }> | undefined;\n\nfunction clientSupportsTasks(message: any): boolean {\n return message?.params?._meta?.['io.modelcontextprotocol/clientCapabilities']\n ?.extensions?.[MCP_TASKS_EXTENSION] !== undefined;\n}\n\n/** Validate the 2026 request envelope before task dispatch mutates a job. */\nfunction taskEnvelopeError(message: any): { code: number; message: string; data?: any } | undefined {\n const meta = message?.params?._meta;\n if (!meta || typeof meta !== 'object' || Array.isArray(meta)) {\n return {\n code: -32602,\n message: 'Request is missing the required _meta envelope for protocol revision 2026-07-28',\n };\n }\n const protocolVersion = meta['io.modelcontextprotocol/protocolVersion'];\n if (protocolVersion !== '2026-07-28') {\n return typeof protocolVersion === 'string'\n ? {\n code: -32022,\n message: 'Unsupported protocol version: ' + protocolVersion,\n data: { supported: ['2026-07-28'], requested: protocolVersion },\n }\n : {\n code: -32602,\n message: 'Invalid _meta envelope for protocol revision 2026-07-28: io.modelcontextprotocol/protocolVersion must be a string',\n };\n }\n if (!specTypeSchemas.ClientCapabilities.safeParse(\n meta['io.modelcontextprotocol/clientCapabilities'],\n ).success) {\n return {\n code: -32602,\n message: 'Invalid _meta envelope for protocol revision 2026-07-28: io.modelcontextprotocol/clientCapabilities is invalid',\n };\n }\n if (\n meta['io.modelcontextprotocol/clientInfo'] !== undefined &&\n !specTypeSchemas.Implementation.safeParse(\n meta['io.modelcontextprotocol/clientInfo'],\n ).success\n ) {\n return {\n code: -32602,\n message: 'Invalid _meta envelope for protocol revision 2026-07-28: io.modelcontextprotocol/clientInfo is invalid',\n };\n }\n if (\n meta['io.modelcontextprotocol/logLevel'] !== undefined &&\n !specTypeSchemas.LoggingLevel.safeParse(\n meta['io.modelcontextprotocol/logLevel'],\n ).success\n ) {\n return {\n code: -32602,\n message: 'Invalid _meta envelope for protocol revision 2026-07-28: io.modelcontextprotocol/logLevel is invalid',\n };\n }\n if (\n meta.progressToken !== undefined &&\n !specTypeSchemas.ProgressToken.safeParse(meta.progressToken).success\n ) {\n return {\n code: -32602,\n message: 'Invalid _meta envelope for protocol revision 2026-07-28: progressToken is invalid',\n };\n }\n return undefined;\n}\n\nfunction jsonRpcError(id: any, code: number, message: string, data?: any) {\n return { jsonrpc: '2.0', id, error: { code, message, ...(data === undefined ? {} : { data }) } };\n}\n\nasync function getTaskRuntime() {\n if (!taskRuntime) {\n taskRuntime = (async () => {\n const firstAction = Object.values(TASK_ACTIONS)[0] as { objectName: string; registryKey?: string } | undefined;\n if (!firstAction) throw new Error('No task-enabled MCP action is configured');\n const collection = await ObjectRegistry.getCollection(firstAction.registryKey ?? firstAction.objectName, {\n persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },\n });\n const db = collection.db;\n const store = await McpTaskStore.create(db, { ownerId: process.env.SMRT_MCP_TENANT_ID || null, tenantId: ${hasTenantScoped ? 'MCP_TENANT_ID ?? null' : 'null'} });\n const runner = new TaskRunner({ queues: ['mcp-tasks'] });\n await runner.initialize(db);\n await runner.start();\n return { store, runner };\n })();\n }\n return taskRuntime;\n}\n\nfunction taskInvocationArgs(actionMeta: any, args: Record<string, any>): any[] {\n const { id: _id, options, ...directArgs } = args;\n if (actionMeta.legacyOptions) {\n return [Object.keys(options ?? {}).length > 0 ? options : directArgs];\n }\n if (actionMeta.optionsParameter) return [options];\n const parameterNames = actionMeta.parameterNames || [];\n const invocationParameterNames = parameterNames.at(-1) === 'context'\n ? parameterNames.slice(0, -1)\n : parameterNames;\n return invocationParameterNames.map((parameterName: string) =>\n args[parameterName === 'id' ? 'actionId' : parameterName],\n );\n}\n\nasync function handleTaskExtensionMessage(message: any): Promise<any | null> {\n if (!message || typeof message !== 'object' || message.id === undefined) return null;\n const method = message.method;\n const params = message.params ?? {};\n const action = method === 'tools/call' ? TASK_ACTIONS[params.name] : undefined;\n const isTaskMethod = action || ['tasks/get', 'tasks/update', 'tasks/cancel'].includes(method);\n if (!isTaskMethod) return null;\n // A task-enabled tool retains its normal synchronous behaviour until its\n // client explicitly opts into Tasks. Do not impose the task envelope on\n // legacy calls that will be passed untouched to the SDK.\n if (!clientSupportsTasks(message) && method === 'tools/call') return null;\n const envelopeError = taskEnvelopeError(message);\n if (envelopeError) {\n return jsonRpcError(message.id, envelopeError.code, envelopeError.message, envelopeError.data);\n }\n if (!clientSupportsTasks(message)) {\n // A task-only method cannot fall back to a legacy response shape. A task\n // tool can still run normally when the client did not opt in, so let the\n // SDK handle that direct tools/call path.\n if (method === 'tools/call') return null;\n return jsonRpcError(message.id, -32021, 'Missing required client capability', {\n requiredCapabilities: { extensions: { [MCP_TASKS_EXTENSION]: {} } },\n });\n }\n try {\n const { store } = await getTaskRuntime();\n if (action) {\n if (typeof params.arguments?.id !== 'string') {\n return jsonRpcError(message.id, -32602, 'Task-enabled custom actions require an id');\n }\n const actionMeta = CUSTOM_ACTIONS[params.name];\n const task = await store.createTask({\n objectType: action.objectType,\n objectId: params.arguments.id,\n method: actionMeta.methodName || TOOL_TARGETS[params.name]?.action,\n invocationArgs: taskInvocationArgs(actionMeta, params.arguments),\n tenantId: ${hasTenantScoped ? 'MCP_TENANT_ID ?? null' : 'null'},\n });\n return { jsonrpc: '2.0', id: message.id, result: { resultType: 'task', content: [], structuredContent: {}, ...task } };\n }\n if (typeof params.taskId !== 'string') {\n return jsonRpcError(message.id, -32602, 'taskId is required');\n }\n if (method === 'tasks/get') {\n const task = await store.getTask(params.taskId);\n return { jsonrpc: '2.0', id: message.id, result: { resultType: 'complete', ...task } };\n }\n if (method === 'tasks/update') {\n await store.updateTask(\n params.taskId,\n params.inputResponses && typeof params.inputResponses === 'object'\n ? params.inputResponses\n : {},\n );\n return { jsonrpc: '2.0', id: message.id, result: { resultType: 'complete' } };\n }\n await store.cancelTask(params.taskId);\n return { jsonrpc: '2.0', id: message.id, result: { resultType: 'complete' } };\n } catch (error) {\n const messageText = error instanceof Error ? error.message : 'Task operation failed';\n return jsonRpcError(message.id, -32602, messageText);\n }\n}\n\nclass McpTaskExtensionTransport {\n onclose?: () => void;\n onerror?: (error: Error) => void;\n onmessage?: (message: any) => void;\n\n // An explicit field, not a parameter property: a \\`.ts\\` target has to stay\n // erasable-syntax-only so Node's type stripping can run it (#2279).\n wire: StdioServerTransport;\n\n constructor(wire: StdioServerTransport) {\n this.wire = wire;\n }\n\n async start() {\n this.wire.onclose = () => this.onclose?.();\n this.wire.onerror = (error) => this.onerror?.(error);\n this.wire.onmessage = (message) => {\n void (async () => {\n const response = await handleTaskExtensionMessage(message);\n if (response) await this.wire.send(response);\n else this.onmessage?.(message);\n })().catch((error) => this.onerror?.(error instanceof Error ? error : new Error(String(error))));\n };\n await this.wire.start();\n }\n\n close() { return this.wire.close(); }\n send(message: any) { return this.wire.send(message); }\n}\n`\n : ''\n}\n\n/**\n * Main server startup function\n */\nexport async function createServer(): Promise<Server> {\n if (DEBUG) {\n console.error(\\`[MCP] Starting server: \\${SERVER_NAME} v\\${SERVER_VERSION}\\`);\n }\n${\n hasTenantScoped\n ? `\n // Fail-closed tenant context (#1554): install the tenancy interceptor so\n // tenant-scoped tools are actually filtered, and so the entry-point gate\n // throws (rather than passing through) when no tenant is supplied. Without\n // this, a tenant set via SMRT_MCP_TENANT_ID would only set async context\n // with no interceptor to enforce it.\n enableTenancy();\n`\n : ''\n}\n // Register the application package manifest before resolving generated\n // object names. Generated servers are commonly run from the application\n // package itself, which is not a node_modules dependency of its process.\n const localManifestPaths = [\n resolve(process.cwd(), 'dist', 'manifest.json'),\n resolve(process.cwd(), '.smrt', 'manifest.json'),\n ].filter(existsSync);\n if (localManifestPaths.length > 0) {\n ObjectRegistry.loadAllManifests({ manifestPaths: localManifestPaths });\n }\n\n // A manifest supplies schemas and tool metadata, while an executable\n // custom action also needs the application's actual class constructor.\n // Consumer builds generate this registration module; load it after the\n // manifest so decorators enrich the already-known metadata.\n const localRegisterPath = process.env.SMRT_MCP_REGISTER_PATH\n || resolve(process.cwd(), '.smrt', 'register.js');\n if (existsSync(localRegisterPath)) {\n await import(pathToFileURL(localRegisterPath).href);\n }\n\n // Load configuration from environment and .smrt.config files\n const appConfig = await loadConfig();\n const aiConfig = appConfig?.ai || {};\n\n if (DEBUG) {\n console.error(\\`[MCP] Loaded \\${TOOLS.length} static tools\\`);\n console.error(\\`[MCP] Available tools:\\`, TOOLS.map(t => t.name).join(', '));\n }\n\n // Create MCP server\n const server = new Server(\n {\n name: SERVER_NAME,\n version: SERVER_VERSION,\n },\n {\n capabilities: {\n tools: {},\n ${hasTaskActions ? \"extensions: { 'io.modelcontextprotocol/tasks': {} },\" : ''}\n },\n cacheHints: {\n 'tools/list': TOOL_LIST_CACHE_HINT,\n },\n }\n );\n\n // Register ListTools handler\n server.setRequestHandler('tools/list', async (_request: ListToolsRequest) => {\n if (DEBUG) {\n console.error(\\`[MCP] ListTools request received\\`);\n }\n\n return {\n tools: [...TOOLS].sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0),\n };\n });\n\n // Register CallTool handler\n server.setRequestHandler('tools/call', async (request: CallToolRequest) => {\n const { name: toolName, arguments: args = {} } = request.params;\n\n if (DEBUG) {\n console.error(\\`[MCP] CallTool request: \\${toolName}\\`);\n console.error(\\`[MCP] Arguments:\\`, JSON.stringify(args, null, 2));\n }\n\n try {\n // Static switch statement for tool execution\n const runToolBody = async () => {\n switch (toolName) {\n${switchCases}\n\n default:\n throw new Error(\\`Unknown tool: \\${toolName}\\`);\n }\n };\n${\n hasTenantScoped\n ? `\n // Fail-closed tenant context for tenant-scoped tools (#1554).\n const toolObject = TOOL_TARGETS[toolName]?.objectName;\n const result =\n toolObject && TENANT_SCOPED.has(toolObject.toLowerCase())\n ? await runTenantScopedEntryPoint(\n { tenantScoped: true, tenantId: MCP_TENANT_ID, allowCrossTenant: MCP_ALLOW_CROSS_TENANT, surface: 'MCP' },\n runToolBody,\n )\n : await runToolBody();`\n : `\n const result = await runToolBody();`\n}\n\n if (DEBUG) {\n console.error(\\`[MCP] Tool executed successfully: \\${toolName}\\`);\n }\n\n return result;\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : 'Unknown error';\n console.error(\\`[MCP] Tool execution failed: \\${toolName}\\`, error);\n\n return errorResult(\n { error: { message: errorMessage } },\n \\`Error executing tool \\${toolName}: \\${errorMessage}\\`,\n );\n }\n });\n\n return server;\n}\n\nasync function main() {\n try {\n // Initialize the registry before accepting an intercepted task call. The\n // SDK would normally invoke this factory for the first regular message,\n // but task calls can be the first message on a stdio connection.\n const server = await createServer();\n const transport = ${hasTaskActions ? 'new McpTaskExtensionTransport(new StdioServerTransport())' : 'undefined'};\n const handle = serveStdio(() => server, {\n ...(transport ? { transport } : {}),\n onerror: (error) => console.error('[MCP] Protocol error:', error),\n });\n const shutdown = async () => {\n if (DEBUG) console.error('[MCP] Shutting down gracefully');\n ${hasTaskActions ? 'if (taskRuntime) {\\n const { runner } = await taskRuntime;\\n await runner.stop();\\n }' : ''}\n await handle.close();\n process.exit(0);\n };\n process.on('SIGINT', shutdown);\n process.on('SIGTERM', shutdown);\n } catch (error) {\n console.error('[MCP] Fatal error during server startup:', error);\n process.exit(1);\n }\n}\n\n// Start only when executed, so adapters and tests may import the factory.\nif (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {\n main().catch((error) => {\n console.error('[MCP] Unhandled error:', error);\n process.exit(1);\n });\n}\n`;\n}\n\n/**\n * Generate package.json script for running MCP server\n *\n * @param serverPath - Path to generated server file (relative to package root)\n * @returns Script command for package.json\n */\nexport function generateMCPScript(\n serverPath: string = 'dist/mcp-server.js',\n): string {\n return `node ${serverPath}`;\n}\n\n/**\n * Generate Claude Desktop configuration example\n *\n * @param serverName - Name for the MCP server\n * @param serverPath - Absolute path to server file\n * @returns Configuration object for claude_desktop_config.json\n */\nexport function generateClaudeConfig(\n serverName: string,\n serverPath: string,\n): object {\n return {\n mcpServers: {\n [serverName]: {\n command: 'node',\n args: [serverPath],\n },\n },\n };\n}\n\n/**\n * Generate README documentation for MCP server setup\n *\n * @param serverName - Name of the MCP server\n * @param serverPath - Path to the server file\n * @returns Markdown documentation\n */\nexport function generateMCPDocumentation(\n serverName: string,\n serverPath: string,\n): string {\n return `# MCP Server Setup\n\nThis project includes an auto-generated MCP (Model Context Protocol) server that exposes SMRT objects as tools for AI integration.\n\n## Quick Start\n\n### 1. Build the MCP Server\n\n\\`\\`\\`bash\nnpm run build\n\\`\\`\\`\n\nThis generates the MCP server at: \\`${serverPath}\\`\n\n### 2. Configure Claude Desktop\n\nAdd the following to your Claude Desktop configuration file:\n\n**macOS**: \\`~/.config/Claude/claude_desktop_config.json\\`\n**Windows**: \\`%APPDATA%\\\\Claude\\\\claude_desktop_config.json\\`\n\n\\`\\`\\`json\n{\n \"mcpServers\": {\n \"${serverName}\": {\n \"command\": \"node\",\n \"args\": [\"/absolute/path/to/${serverPath}\"]\n }\n }\n}\n\\`\\`\\`\n\nReplace \\`/absolute/path/to/\\` with the actual absolute path to your project directory.\n\n### 3. Restart Claude Desktop\n\nClose and reopen Claude Desktop to load the new MCP server.\n\n### 4. Test the Integration\n\nIn Claude Code, you can now use the auto-generated tools. For example:\n\n- \\`list_products\\` - List all products\n- \\`get_product\\` - Get a specific product by ID\n- \\`create_product\\` - Create a new product\n- And more...\n\n## Environment Variables\n\nThe MCP server supports optional environment variables:\n\n- \\`DATABASE_URL\\` - Database connection string\n\n**AI Provider Configuration (in priority order):**\n1. **\\`ai\\` block in \\`smrt.config.ts\\`** (declare once; keep the secret in the environment):\n - \\`ai: { provider: 'openai', model: 'gpt-4o', apiKeyEnv: 'OPENAI_API_KEY' }\\`\n\n2. **Generic environment configuration** (supports any provider):\n - \\`SMRT_AI_PROVIDER\\` - Provider name (e.g., 'openai', 'anthropic', 'claude-cli', 'gemini')\n - \\`SMRT_AI_API_KEY\\` - API key for the provider\n - \\`SMRT_AI_MODEL\\` - Model to use (optional)\n - \\`SMRT_AI_BASE_URL\\` - Endpoint override (optional)\n\n3. **Provider-specific key fallbacks** (used when the \\`ai\\` block selects the provider, and only for that provider):\n - \\`OPENAI_API_KEY\\`, \\`ANTHROPIC_API_KEY\\`, \\`GEMINI_API_KEY\\`\n - \\`CLAUDE_API_KEY\\` + \\`CLAUDE_MODEL\\` - Claude CLI provider (defaults to 'sonnet'); read by \\`@happyvertical/ai\\`\n\n**Examples:**\n\\`\\`\\`bash\n# Using generic configuration (recommended)\nexport SMRT_AI_PROVIDER=claude-cli\nexport SMRT_AI_MODEL=sonnet\n\n# Using provider-specific configuration\nexport CLAUDE_API_KEY=your-key\nexport CLAUDE_MODEL=sonnet\n\n# Using OpenAI\nexport OPENAI_API_KEY=your-openai-key\n\\`\\`\\`\n\n## Troubleshooting\n\n### Server Not Appearing in Claude\n\n1. Check that the path in \\`claude_desktop_config.json\\` is absolute\n2. Verify the server file exists at the specified path\n3. Check Claude Desktop logs for errors\n\n### Tools Not Working\n\n1. Ensure your database is accessible (if using one)\n2. Check that SMRT objects are properly decorated with \\`@smrt()\\`\n3. Look for errors in the MCP server output\n\n### Debug Mode\n\nTo enable debug logging, set the \\`DEBUG\\` constant to \\`true\\` in the generated server file.\n\n## Generated Tools\n\nThe following tools are automatically generated from your SMRT objects:\n\n- **CRUD Operations**: \\`list_\\`, \\`get_\\`, \\`create_\\`, \\`update_\\`, \\`delete_\\` for each object type\n- **Custom Actions**: Any custom methods included in the \\`@smrt()\\` decorator configuration\n\nSee the SMRT object definitions for the complete list of available tools and their parameters.\n`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAwBA,SAAS,WAAW,KAAqB;CACvC,OAAO,IAAI,OAAO,CAAC,CAAC,CAAC,YAAY,IAAI,IAAI,MAAM,CAAC;AAClD;;;;;;;AA0GA,SAAgB,yBAAyB,UAA0B,CAAC,GAAW;CAC7E,MAAM,EACJ,OAAO,mBACP,UAAU,SACV,cAAc,+CACd,QAAQ,OACR,QAAQ,CAAC,GACT,aAAa,qBACb,gBAAgB,CAAC,GACjB,cAAc,CAAC,GACf,sBAAsB,CAAC,GACvB,aAAa,CAAC,GACd,cAAc,CAAC,GACf,oBAAoB;EAAE,OAAO;EAAY,YAAY;CAAU,MAC7D;CAIJ,MAAM,cACJ,uBACA,OAAO,YACL,MAAM,KAAK,SAAS;EAClB,MAAM,YAAY,KAAK,KAAK,QAAQ,GAAG;EACvC,OAAO,CACL,KAAK,MACL;GACE,YAAY,WAAW,KAAK,KAAK,MAAM,GAAG,SAAS,CAAC;GACpD,QAAQ,KAAK,KAAK,MAAM,YAAY,CAAC;EACvC,CACF;CACF,CAAC,CACH;CACF,KAAK,MAAM,QAAQ,OACjB,IAAI,CAAC,OAAO,OAAO,aAAa,KAAK,IAAI,GACvC,MAAM,IAAI,MAAM,4BAA4B,KAAK,MAAM;CAK3D,MAAM,YAAY,MAAM,SAAS,IAAI,KAAK,UAAU,OAAO,MAAM,CAAC,IAAI;CACtE,MAAM,iBAAiB,OAAO,KAAK,WAAW,CAAC,CAAC,SAAS;CAKzD,MAAM,kBAAkB,MAAM,KAC5B,IAAI,IAAI,oBAAoB,KAAK,MAAM,EAAE,YAAY,CAAC,CAAC,CACzD;CACA,MAAM,kBAAkB,gBAAgB,SAAS;CAGjD,MAAM,uBAAuB,WAAmB;EAC9C,OAAO,MACJ,KAAK,SAAS;GACb,MAAM,aAAa,YAAY,KAAK,KAAK,CAAC,WAAW,YAAY;GACjE,MAAM,SAAS,YAAY,KAAK,KAAK,CAAC;GAEtC,MAAM,SACJ,YAAY,KAAK,KAAK,CAAC,eACvB,YAAY,KAAK,KAAK,CAAC;GAEzB,QAAQ,QAAR;IACE,KAAK,QACH,OAAO,GAAG,OAAO,QAAQ,KAAK,KAAK;EAC7C,OAAO,4DAA+E,eAAe;EACrG,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO,oCAAoC,KAAK,UACpC,YAAY,eAAe,qBAC7B,EAAE;EACZ,OAAO;;EAEP,OAAO,0DAA0D,KAAK,UAAU,MAAM,EAAE;EACxF,OAAO;EACP,OAAO;EACP,OAAO;;EAEP,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;IAEC,KAAK,OACH,OAAO,GAAG,OAAO,QAAQ,KAAK,KAAK;EAC7C,OAAO;EACP,OAAO;EACP,OAAO;;EAEP,OAAO,0DAA0D,KAAK,UAAU,MAAM,EAAE;EACxF,OAAO;EACP,OAAO;EACP,OAAO;;EAEP,OAAO;EACP,OAAO;;EAEP,OAAO;EACP,OAAO;EACP,OAAO;;EAEP,OAAO;EACP,OAAO;IAEC,KAAK,UACH,OAAO,GAAG,OAAO,QAAQ,KAAK,KAAK;EAC7C,OAAO,mFAAmF,KAAK,UAAU,UAAU,EAAE,IAAI,KAAK,UAAU,MAAM,EAAE;;EAEhJ,OAAO;;EAEP,OAAO;EACP,OAAO;IAEC,KAAK,UACH,OAAO,GAAG,OAAO,QAAQ,KAAK,KAAK;EAC7C,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;;EAEP,OAAO,0DAA0D,KAAK,UAAU,MAAM,EAAE;EACxF,OAAO;EACP,OAAO;EACP,OAAO;;EAEP,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;;EAEP,OAAO;EACP,OAAO,+CAA+C,KAAK,UAAU,MAAM,EAAE;EAC7E,OAAO;EACP,OAAO;;EAEP,OAAO;EACP,OAAO;IAEC,KAAK,UACH,OAAO,GAAG,OAAO,QAAQ,KAAK,KAAK;EAC7C,OAAO;EACP,OAAO;EACP,OAAO;;EAEP,OAAO,0DAA0D,KAAK,UAAU,MAAM,EAAE;EACxF,OAAO;EACP,OAAO;EACP,OAAO;;EAEP,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;;EAEP,OAAO;;EAEP,OAAO;EACP,OAAO;IAEC,SAGE,OAAO,GAAG,OAAO,QAAQ,KAAK,KAAK;EAC7C,OAAO,uCAAuC,KAAK,KAAK;EACxD,OAAO;;EAEP,OAAO;EACP,OAAO,sBAAsB,KAAK,UAAU,oCAAoC,QAAQ,EAAE;EAC1F,OAAO;EACP,OAAO;EACP,OAAO,sBAAsB,KAAK,UAAU,iBAAiB,OAAO,gDAAgD,EAAE;EACtH,OAAO;;EAEP,OAAO,0DAA0D,KAAK,UAAU,MAAM,EAAE;EACxF,OAAO;EACP,OAAO;EACP,OAAO;;EAEP,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO,kCAAkC,KAAK,UAAU,MAAM,EAAE;EAChE,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO,yDAAyD,KAAK,UAAU,MAAM,EAAE;EACvF,OAAO;EACP,OAAO,sBAAsB,KAAK,UAAU,UAAU,OAAO,mCAAmC,EAAE;EAClG,OAAO;;EAEP,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;;EAEP,OAAO;EACP,OAAO;EACP,OAAO;GACD;EACF,CAAC,CAAC,CACD,KAAK,MAAM;CAChB;CAEA,MAAM,cAAc,oBAAoB,YAAY;CAEpD,OAAO;;;;;;;;;;;;;;;;;;IAkBL,iBAAiB,qBAAqB,GAAG;;WAElC,iBAAiB,2BAA2B,GAAG;;;;;;;EAOxD,iBAAiB,2EAA2E,GAAG;EAC/F,kBAAkB,8FAA8F,GAAG;;sBAE/F,KAAK,UAAU,IAAI,EAAE;yBAClB,KAAK,UAAU,OAAO,EAAE;6BACpB,KAAK,UAAU,WAAW,EAAE;gBACzC,MAAM;;;gBAGN,UAAU;+BACK,KAAK,UAAU,iBAAiB,EAAE;qGACoC,KAAK,UAAU,WAAW,EAAE;yBACxG,KAAK,UAAU,aAAa,EAAE;uBAChC,KAAK,UAAU,WAAW,EAAE;8DACW,KAAK,UAAU,UAAU,EAAE;EAEvF,kBACI;;;;;gCAK0B,KAAK,UAAU,eAAe,EAAE;;;IAI1D,GACL;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAqHC,iBACI;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iHA2F2G,kBAAkB,0BAA0B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oBA6DhJ,kBAAkB,0BAA0B,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA0DjE,GACL;;;;;;;;;EAUC,kBACI;;;;;;;IAQA,GACL;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;YAwCW,iBAAiB,yDAAyD,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgCvF,YAAY;;;;;;EAOZ,kBACI;;;;;;;;;sCAUA;6CAEL;;;;;;;;;;;;;;;;;;;;;;;;;;;wBA2BuB,iBAAiB,8DAA8D,YAAY;;;;;;;QAO3G,iBAAiB,6GAA6G,GAAG;;;;;;;;;;;;;;;;;;;;AAoBzI;;;;;;;AAQA,SAAgB,kBACd,aAAqB,sBACb;CACR,OAAO,QAAQ;AACjB;;;;;;;;AASA,SAAgB,qBACd,YACA,YACQ;CACR,OAAO,EACL,YAAY,GACT,aAAa;EACZ,SAAS;EACT,MAAM,CAAC,UAAU;CACnB,EACF,EACF;AACF;;;;;;;;AASA,SAAgB,yBACd,YACA,YACQ;CACR,OAAO;;;;;;;;;;;;sCAY6B,WAAW;;;;;;;;;;;;OAY1C,WAAW;;oCAEkB,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkF/C"}
@@ -4,6 +4,12 @@ export interface McpToolTarget {
4
4
  action: string;
5
5
  /** Original generated name before protocol canonicalization. */
6
6
  originalName?: string;
7
+ /**
8
+ * Registry key (qualified identity) of the generating class (#3490).
9
+ * `objectName` is the wire/display name; every registry lookup — in-process
10
+ * and in generated runtimes — goes through this key when present.
11
+ */
12
+ registryKey?: string;
7
13
  }
8
14
  /**
9
15
  * Preserve valid aliases and deterministically name previously invalid ones.
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-tool-name.d.ts","sourceRoot":"","sources":["../../src/generators/mcp-tool-name.ts"],"names":[],"mappings":"AAEA,yEAAyE;AACzE,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,aAAa,CAAA;CAAE,EAAE,GAC1D,MAAM,EAAE,CAwBV"}
1
+ {"version":3,"file":"mcp-tool-name.d.ts","sourceRoot":"","sources":["../../src/generators/mcp-tool-name.ts"],"names":[],"mappings":"AAEA,yEAAyE;AACzE,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,SAAS;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,aAAa,CAAA;CAAE,EAAE,GAC1D,MAAM,EAAE,CAwBV"}
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-tool-name.js","names":[],"sources":["../../src/generators/mcp-tool-name.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\n\n/** Internal execution identity; never inferred from a protocol alias. */\nexport interface McpToolTarget {\n objectName: string;\n action: string;\n /** Original generated name before protocol canonicalization. */\n originalName?: string;\n}\n\n/**\n * Preserve valid aliases and deterministically name previously invalid ones.\n * Reserve all valid names before assigning hashes so registry order cannot\n * cause an invalid alias to steal a valid published name. Collisions fail closed.\n */\nexport function canonicalMcpToolNames(\n entries: readonly { name: string; target: McpToolTarget }[],\n): string[] {\n const valid = /^[A-Za-z0-9_.-]{1,64}$/;\n const occupied = new Set<string>();\n const rawNames = new Set<string>();\n for (const { name } of entries) {\n if (rawNames.has(name)) throw new Error(`Duplicate MCP tool name: ${name}`);\n rawNames.add(name);\n if (valid.test(name)) occupied.add(name);\n }\n return entries.map(({ name, target }) => {\n if (valid.test(name)) return name;\n const identity = JSON.stringify([target.objectName, target.action, name]);\n const prefix = name.replace(/[^A-Za-z0-9_.-]/g, '_').slice(0, 31);\n const digest = createHash('sha256')\n .update(identity)\n .digest('hex')\n .slice(0, 32);\n const canonical = `${prefix}_${digest}`;\n if (occupied.has(canonical)) {\n throw new Error(`MCP tool name collision: ${canonical}`);\n }\n occupied.add(canonical);\n return canonical;\n });\n}\n"],"mappings":";;;;;;;AAeA,SAAgB,sBACd,SACU;CACV,MAAM,QAAQ;CACd,MAAM,2BAAW,IAAI,IAAY;CACjC,MAAM,2BAAW,IAAI,IAAY;CACjC,KAAK,MAAM,EAAE,UAAU,SAAS;EAC9B,IAAI,SAAS,IAAI,IAAI,GAAG,MAAM,IAAI,MAAM,4BAA4B,MAAM;EAC1E,SAAS,IAAI,IAAI;EACjB,IAAI,MAAM,KAAK,IAAI,GAAG,SAAS,IAAI,IAAI;CACzC;CACA,OAAO,QAAQ,KAAK,EAAE,MAAM,aAAa;EACvC,IAAI,MAAM,KAAK,IAAI,GAAG,OAAO;EAC7B,MAAM,WAAW,KAAK,UAAU;GAAC,OAAO;GAAY,OAAO;GAAQ;EAAI,CAAC;EAMxE,MAAM,YAAY,GALH,KAAK,QAAQ,oBAAoB,GAAG,CAAC,CAAC,MAAM,GAAG,EAKzC,EAAO,GAJb,WAAW,QAAQ,CAAC,CAChC,OAAO,QAAQ,CAAC,CAChB,OAAO,KAAK,CAAC,CACb,MAAM,GAAG,EACmB;EAC/B,IAAI,SAAS,IAAI,SAAS,GACxB,MAAM,IAAI,MAAM,4BAA4B,WAAW;EAEzD,SAAS,IAAI,SAAS;EACtB,OAAO;CACT,CAAC;AACH"}
1
+ {"version":3,"file":"mcp-tool-name.js","names":[],"sources":["../../src/generators/mcp-tool-name.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\n\n/** Internal execution identity; never inferred from a protocol alias. */\nexport interface McpToolTarget {\n objectName: string;\n action: string;\n /** Original generated name before protocol canonicalization. */\n originalName?: string;\n /**\n * Registry key (qualified identity) of the generating class (#3490).\n * `objectName` is the wire/display name; every registry lookup — in-process\n * and in generated runtimes — goes through this key when present.\n */\n registryKey?: string;\n}\n\n/**\n * Preserve valid aliases and deterministically name previously invalid ones.\n * Reserve all valid names before assigning hashes so registry order cannot\n * cause an invalid alias to steal a valid published name. Collisions fail closed.\n */\nexport function canonicalMcpToolNames(\n entries: readonly { name: string; target: McpToolTarget }[],\n): string[] {\n const valid = /^[A-Za-z0-9_.-]{1,64}$/;\n const occupied = new Set<string>();\n const rawNames = new Set<string>();\n for (const { name } of entries) {\n if (rawNames.has(name)) throw new Error(`Duplicate MCP tool name: ${name}`);\n rawNames.add(name);\n if (valid.test(name)) occupied.add(name);\n }\n return entries.map(({ name, target }) => {\n if (valid.test(name)) return name;\n const identity = JSON.stringify([target.objectName, target.action, name]);\n const prefix = name.replace(/[^A-Za-z0-9_.-]/g, '_').slice(0, 31);\n const digest = createHash('sha256')\n .update(identity)\n .digest('hex')\n .slice(0, 32);\n const canonical = `${prefix}_${digest}`;\n if (occupied.has(canonical)) {\n throw new Error(`MCP tool name collision: ${canonical}`);\n }\n occupied.add(canonical);\n return canonical;\n });\n}\n"],"mappings":";;;;;;;AAqBA,SAAgB,sBACd,SACU;CACV,MAAM,QAAQ;CACd,MAAM,2BAAW,IAAI,IAAY;CACjC,MAAM,2BAAW,IAAI,IAAY;CACjC,KAAK,MAAM,EAAE,UAAU,SAAS;EAC9B,IAAI,SAAS,IAAI,IAAI,GAAG,MAAM,IAAI,MAAM,4BAA4B,MAAM;EAC1E,SAAS,IAAI,IAAI;EACjB,IAAI,MAAM,KAAK,IAAI,GAAG,SAAS,IAAI,IAAI;CACzC;CACA,OAAO,QAAQ,KAAK,EAAE,MAAM,aAAa;EACvC,IAAI,MAAM,KAAK,IAAI,GAAG,OAAO;EAC7B,MAAM,WAAW,KAAK,UAAU;GAAC,OAAO;GAAY,OAAO;GAAQ;EAAI,CAAC;EAMxE,MAAM,YAAY,GALH,KAAK,QAAQ,oBAAoB,GAAG,CAAC,CAAC,MAAM,GAAG,EAKzC,EAAO,GAJb,WAAW,QAAQ,CAAC,CAChC,OAAO,QAAQ,CAAC,CAChB,OAAO,KAAK,CAAC,CACb,MAAM,GAAG,EACmB;EAC/B,IAAI,SAAS,IAAI,SAAS,GACxB,MAAM,IAAI,MAAM,4BAA4B,WAAW;EAEzD,SAAS,IAAI,SAAS;EACtB,OAAO;CACT,CAAC;AACH"}
@@ -31,6 +31,15 @@ export interface MCPConfig {
31
31
  name: string;
32
32
  version: string;
33
33
  };
34
+ /**
35
+ * Generate tools only for these registered classes: simple names
36
+ * (case-insensitive) or qualified `@scope/pkg:Class` names. Omitted, every
37
+ * registered class is a candidate. A host that publishes an allow-list
38
+ * (`@happyvertical/smrt-app-mcp`) passes it here so that classes it never
39
+ * lists — and any name collision among them — cannot affect its catalog
40
+ * or calls (#3490). Duplicate tool names within the scope still fail.
41
+ */
42
+ classNames?: readonly string[];
34
43
  }
35
44
  export declare const MCP_STABLE_CATALOG_TTL_MS = 86400000;
36
45
  export interface MCPToolListCacheOptions {
@@ -150,6 +159,12 @@ export interface MCPToolIdentity {
150
159
  objectName: string;
151
160
  action: string;
152
161
  originalName: string;
162
+ /**
163
+ * Registry identity of the generating class (its qualified name, or its
164
+ * registry key when it has no package), so a composer can tell same-named
165
+ * classes of different packages apart (#3490).
166
+ */
167
+ qualifiedName?: string;
153
168
  }
154
169
  /** Return a copied, canonical tool sequence for byte-stable tools/list output. */
155
170
  export declare function sortMCPTools<T extends Pick<MCPTool, 'name'>>(tools: T[]): T[];
@@ -205,7 +220,17 @@ export declare class MCPGenerator {
205
220
  */
206
221
  generateTools(): Promise<MCPTool[]>;
207
222
  /**
208
- * Generate tools for a specific object
223
+ * The registered classes `MCPConfig.classNames` selects, or undefined when
224
+ * unscoped. A qualified name selects exactly that registration; a simple
225
+ * name must match one class only, so the scope never picks one of several
226
+ * same-named classes on its own (#3490).
227
+ */
228
+ private resolveClassScope;
229
+ /** The registry key of the class a generated tool belongs to. */
230
+ private toolClass;
231
+ /**
232
+ * Generate tools for a specific object. `objectName` names the tools;
233
+ * `registryKey` (the class's registry key) resolves everything else.
209
234
  */
210
235
  private generateObjectTools;
211
236
  private buildCustomActionTool;
@@ -231,6 +256,14 @@ export declare class MCPGenerator {
231
256
  private buildOutputSchema;
232
257
  private buildPublicItemSchema;
233
258
  private buildFieldSchemaProperties;
259
+ /**
260
+ * The STI subtypes a create may select, each bound to its exact registry
261
+ * entry (`name` is its registry key). A subtype is the base itself or a
262
+ * class whose constructor descends from the base's, or — for manifest-only
263
+ * entries — whose inheritance chain names the base's qualified identity.
264
+ * Never resolved by simple name: two packages may declare same-named
265
+ * subtypes of one base (#3490).
266
+ */
234
267
  private getStiVariants;
235
268
  /**
236
269
  * Handle MCP tool calls