@happyvertical/smrt-core 0.38.2 → 0.38.4

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 (130) hide show
  1. package/AGENTS.md +11 -2
  2. package/dist/__tests__/fixtures/advisor-test-classes.d.ts +9 -0
  3. package/dist/__tests__/fixtures/advisor-test-classes.d.ts.map +1 -1
  4. package/dist/change-feed.d.ts +19 -10
  5. package/dist/change-feed.d.ts.map +1 -1
  6. package/dist/change-feed.js +43 -13
  7. package/dist/change-feed.js.map +1 -1
  8. package/dist/change-signals.d.ts +83 -0
  9. package/dist/change-signals.d.ts.map +1 -0
  10. package/dist/change-signals.js +272 -0
  11. package/dist/change-signals.js.map +1 -0
  12. package/dist/collection-cache.d.ts +21 -0
  13. package/dist/collection-cache.d.ts.map +1 -1
  14. package/dist/collection-cache.js +0 -0
  15. package/dist/collection-cache.js.map +1 -1
  16. package/dist/config.d.ts +1 -1
  17. package/dist/config.d.ts.map +1 -1
  18. package/dist/config.js.map +1 -1
  19. package/dist/decorators/index.d.ts +8 -0
  20. package/dist/decorators/index.d.ts.map +1 -1
  21. package/dist/decorators/index.js.map +1 -1
  22. package/dist/errors.js +2 -1
  23. package/dist/errors.js.map +1 -1
  24. package/dist/generators/changes-route.d.ts +15 -27
  25. package/dist/generators/changes-route.d.ts.map +1 -1
  26. package/dist/generators/changes-route.js +17 -7
  27. package/dist/generators/changes-route.js.map +1 -1
  28. package/dist/generators/cli.d.ts +3 -0
  29. package/dist/generators/cli.d.ts.map +1 -1
  30. package/dist/generators/cli.js +7 -4
  31. package/dist/generators/cli.js.map +1 -1
  32. package/dist/generators/conditional-get.d.ts +17 -6
  33. package/dist/generators/conditional-get.d.ts.map +1 -1
  34. package/dist/generators/conditional-get.js +19 -13
  35. package/dist/generators/conditional-get.js.map +1 -1
  36. package/dist/generators/events-route.d.ts +72 -0
  37. package/dist/generators/events-route.d.ts.map +1 -0
  38. package/dist/generators/events-route.js +234 -0
  39. package/dist/generators/events-route.js.map +1 -0
  40. package/dist/generators/index.d.ts +1 -0
  41. package/dist/generators/index.d.ts.map +1 -1
  42. package/dist/generators/index.js +2 -1
  43. package/dist/generators/mcp-runtime-template.d.ts.map +1 -1
  44. package/dist/generators/mcp-runtime-template.js +11 -5
  45. package/dist/generators/mcp-runtime-template.js.map +1 -1
  46. package/dist/generators/mcp.d.ts +3 -0
  47. package/dist/generators/mcp.d.ts.map +1 -1
  48. package/dist/generators/mcp.js +18 -9
  49. package/dist/generators/mcp.js.map +1 -1
  50. package/dist/generators/rest.d.ts +7 -1
  51. package/dist/generators/rest.d.ts.map +1 -1
  52. package/dist/generators/rest.js +77 -13
  53. package/dist/generators/rest.js.map +1 -1
  54. package/dist/generators.js +2 -1
  55. package/dist/index.d.ts +1 -0
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +3 -1
  58. package/dist/manifest/generator.js.map +1 -1
  59. package/dist/manifest/manifest-loader.js.map +1 -1
  60. package/dist/manifest/static-manifest.d.ts.map +1 -1
  61. package/dist/manifest/static-manifest.js +7 -3
  62. package/dist/manifest/static-manifest.js.map +1 -1
  63. package/dist/manifest/store.js +1 -1
  64. package/dist/manifest/store.js.map +1 -1
  65. package/dist/manifest/test-manifest-stub.d.ts.map +1 -1
  66. package/dist/manifest/test-manifest-stub.js +1171 -90
  67. package/dist/manifest/test-manifest-stub.js.map +1 -1
  68. package/dist/manifest.json +9 -3
  69. package/dist/mcp-advisor/tools/preview-api-endpoints.d.ts +1 -0
  70. package/dist/mcp-advisor/tools/preview-api-endpoints.d.ts.map +1 -1
  71. package/dist/mcp-advisor/types.d.ts +4 -0
  72. package/dist/mcp-advisor/types.d.ts.map +1 -1
  73. package/dist/migrations/tracker.js +5 -4
  74. package/dist/migrations/tracker.js.map +1 -1
  75. package/dist/migrations/types.d.ts +1 -1
  76. package/dist/migrations/types.d.ts.map +1 -1
  77. package/dist/object.d.ts +17 -8
  78. package/dist/object.d.ts.map +1 -1
  79. package/dist/object.js +42 -26
  80. package/dist/object.js.map +1 -1
  81. package/dist/registry/class-registration.d.ts.map +1 -1
  82. package/dist/registry/class-registration.js +12 -0
  83. package/dist/registry/class-registration.js.map +1 -1
  84. package/dist/registry/shared-state.d.ts +2 -1
  85. package/dist/registry/shared-state.d.ts.map +1 -1
  86. package/dist/registry/shared-state.js.map +1 -1
  87. package/dist/registry/types.d.ts.map +1 -1
  88. package/dist/registry.d.ts.map +1 -1
  89. package/dist/registry.js +1 -1
  90. package/dist/registry.js.map +1 -1
  91. package/dist/runtime/client.d.ts.map +1 -1
  92. package/dist/runtime/client.js.map +1 -1
  93. package/dist/scanner/manifest-generator.js.map +1 -1
  94. package/dist/scanner/types.d.ts +7 -0
  95. package/dist/scanner/types.d.ts.map +1 -1
  96. package/dist/schema/ddl/base-strategy.js +1 -1
  97. package/dist/schema/ddl/base-strategy.js.map +1 -1
  98. package/dist/scripts/migrate-datetime-to-timestamp.ts +1 -1
  99. package/dist/signals/sanitizer.d.ts.map +1 -1
  100. package/dist/signals/sanitizer.js.map +1 -1
  101. package/dist/smrt-knowledge.json +8 -8
  102. package/dist/testing/index.d.ts +1 -0
  103. package/dist/testing/index.d.ts.map +1 -1
  104. package/dist/testing/index.js +2 -1
  105. package/dist/testing/notifications-stub.d.ts +24 -0
  106. package/dist/testing/notifications-stub.d.ts.map +1 -0
  107. package/dist/testing/notifications-stub.js +62 -0
  108. package/dist/testing/notifications-stub.js.map +1 -0
  109. package/dist/testing.js +2 -1
  110. package/dist/tools/tool-generator.js +1 -1
  111. package/dist/tools/tool-generator.js.map +1 -1
  112. package/dist/vite-plugin/changes-route.d.ts +14 -0
  113. package/dist/vite-plugin/changes-route.d.ts.map +1 -1
  114. package/dist/vite-plugin/changes-route.js +2 -2
  115. package/dist/vite-plugin/changes-route.js.map +1 -1
  116. package/dist/vite-plugin/events-route.d.ts +9 -0
  117. package/dist/vite-plugin/events-route.d.ts.map +1 -0
  118. package/dist/vite-plugin/events-route.js +155 -0
  119. package/dist/vite-plugin/events-route.js.map +1 -0
  120. package/dist/vite-plugin/index.d.ts +8 -0
  121. package/dist/vite-plugin/index.d.ts.map +1 -1
  122. package/dist/vite-plugin/index.js +8 -2
  123. package/dist/vite-plugin/index.js.map +1 -1
  124. package/dist/vite-plugin/sveltekit-generator.d.ts +8 -0
  125. package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
  126. package/dist/vite-plugin/sveltekit-generator.js +239 -38
  127. package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
  128. package/dist/vite-plugin/web-collections.js +1 -1
  129. package/dist/vite-plugin/web-collections.js.map +1 -1
  130. package/package.json +12 -12
@@ -0,0 +1,234 @@
1
+ import { subscribeToChangeSignals } from "../change-signals.js";
2
+ import { resolveDispatchTenantScope } from "../dispatch/tenant-resolver.js";
3
+ import { ensureChangeFeedTable, getChangesSince } from "../change-feed.js";
4
+ import { resolveChangesDb } from "./changes-route.js";
5
+ import { createLogger } from "@happyvertical/logger";
6
+ //#region src/generators/events-route.ts
7
+ /**
8
+ * Generated `_events` SSE route — live change signals (issue #1763, SERVER
9
+ * half; parent PRD #1755).
10
+ *
11
+ * Handles `GET {basePath}/_events` in the REST generator: an auth-guarded,
12
+ * tenant-scoped Server-Sent-Events stream of coarse change signals ({table,
13
+ * operation, rowId, tenantId} + a `seq` cursor in the SSE `id:` field). It is
14
+ * the push companion to the pull-based `_changes` route (#1758): a subscriber
15
+ * reacts to a signal by re-reading through the authorized collection routes,
16
+ * so **no row payload ever crosses this channel** — authorization stays
17
+ * entirely on the read path.
18
+ *
19
+ * The stream lifecycle (subscribe, catch-up replay, heartbeat, teardown) lives
20
+ * in {@link buildChangeEventStream} so it is written and tested once; both the
21
+ * REST generator here and the generated SvelteKit route import it. `rest.ts`
22
+ * only registers the path.
23
+ *
24
+ * Contract:
25
+ * - **Fail-closed auth** (identical to `_changes`, #1540 posture): no
26
+ * `authMiddleware` configured → 401; the middleware may return a Response to
27
+ * short-circuit (e.g. 403). The feed spans every table, so per-model
28
+ * `api: { public }` opt-outs deliberately do not apply.
29
+ * - **Tenant scope is captured ONCE at connection open** — signal delivery
30
+ * happens from a different async context (the writer's afterSave, possibly
31
+ * another request or replica) with no tenant ALS active, so the filter must
32
+ * be the value resolved at subscribe time, not re-resolved per signal.
33
+ * - **Same-origin only** for this slice: `rest.ts` does NOT wrap `_events` in
34
+ * CORS headers. `EventSource` cannot set request headers and credentialed
35
+ * cross-origin SSE needs `Access-Control-Allow-Credentials` the CORS helper
36
+ * does not emit; cross-origin SSE is a deliberate follow-up.
37
+ */
38
+ var logger = createLogger({ level: "info" });
39
+ /**
40
+ * Pseudo object name passed to the auth middleware for the events route, so
41
+ * middlewares can recognize and specially authorize it (mirrors `_changes`).
42
+ */
43
+ var EVENTS_ROUTE_OBJECT_NAME = "_events";
44
+ /** Default heartbeat interval (ms). Overridable via stream options. */
45
+ var DEFAULT_EVENTS_HEARTBEAT_MS = 15e3;
46
+ var encoder = new TextEncoder();
47
+ /**
48
+ * Whether a signal is visible to a captured tenant scope. Exact same rule as
49
+ * `getChangesSince`'s tenantId filter, run **synchronously server-side** inside
50
+ * the enqueue callback before any byte hits the wire:
51
+ * - not enforced → visible.
52
+ * - enforced, no active tenant (`tenantId === null`) → only global signals.
53
+ * - enforced, tenant `T` → `T`'s signals plus global signals.
54
+ */
55
+ function signalVisibleToTenant(sig, scope) {
56
+ if (!scope.enforced) return true;
57
+ if (scope.tenantId === null) return sig.tenantId === null;
58
+ return sig.tenantId === scope.tenantId || sig.tenantId === null;
59
+ }
60
+ /**
61
+ * SSE frame for a change signal. The `data` JSON is EXACTLY
62
+ * `{table, operation, rowId, tenantId}` — the `seq` lives only in the `id:`
63
+ * field (the EventSource `Last-Event-ID` a client echoes to resume).
64
+ */
65
+ function encodeSseEvent(sig) {
66
+ const data = JSON.stringify({
67
+ table: sig.table,
68
+ operation: sig.operation,
69
+ rowId: sig.rowId,
70
+ tenantId: sig.tenantId
71
+ });
72
+ return encoder.encode(`id: ${sig.seq}\nevent: change\ndata: ${data}\n\n`);
73
+ }
74
+ /** SSE resync frame. The id advances EventSource past an unservable cursor. */
75
+ function encodeSseResyncEvent(cursor) {
76
+ return encoder.encode(`id: ${cursor}\nevent: resync\ndata: {}\n\n`);
77
+ }
78
+ /** SSE comment line (used for heartbeats — ignored by EventSource). */
79
+ function encodeSseComment(text) {
80
+ return encoder.encode(`: ${text}\n\n`);
81
+ }
82
+ /**
83
+ * Build the SSE body stream for an `_events` connection.
84
+ *
85
+ * `start(controller)`:
86
+ * a. **Subscribe FIRST**, before catch-up. Subscribing before the catch-up
87
+ * read closes the gap window: a write landing between subscribe and the
88
+ * catch-up read is delivered twice (once live, once in the replay) — which
89
+ * is safe, since the client dedupes by the SSE `id:`/seq.
90
+ * b. Write the `retry:` reconnection hint.
91
+ * c. If a cursor was supplied, replay changes after it (paging until
92
+ * exhausted); on `resyncRequired`, emit `event: resync` at the server's
93
+ * fresh horizon.
94
+ * d. Start the heartbeat interval.
95
+ *
96
+ * `cancel()` tears down on disconnect: clears the heartbeat and unsubscribes,
97
+ * so a dropped client never leaks its subscription (which would pin the dead
98
+ * controller and keep the cross-replica listener refcount above 0).
99
+ */
100
+ function buildChangeEventStream(db, options) {
101
+ const { cursor, tenantScope } = options;
102
+ const heartbeatMs = options.heartbeatMs ?? 15e3;
103
+ let unsubscribe = null;
104
+ let heartbeat = null;
105
+ let closed = false;
106
+ const teardown = () => {
107
+ if (closed) return;
108
+ closed = true;
109
+ if (heartbeat) {
110
+ clearInterval(heartbeat);
111
+ heartbeat = null;
112
+ }
113
+ if (unsubscribe) {
114
+ unsubscribe();
115
+ unsubscribe = null;
116
+ }
117
+ };
118
+ return new ReadableStream({
119
+ async start(controller) {
120
+ unsubscribe = subscribeToChangeSignals(db, (sig) => {
121
+ if (closed) return;
122
+ if (!signalVisibleToTenant(sig, tenantScope)) return;
123
+ try {
124
+ controller.enqueue(encodeSseEvent(sig));
125
+ } catch {
126
+ teardown();
127
+ }
128
+ });
129
+ controller.enqueue(encoder.encode("retry: 3000\n\n"));
130
+ if (cursor != null) try {
131
+ const catchupTenantId = tenantScope.enforced ? tenantScope.tenantId : void 0;
132
+ let since = cursor;
133
+ for (;;) {
134
+ const page = await getChangesSince(db, {
135
+ since,
136
+ tenantId: catchupTenantId
137
+ });
138
+ if (page.resyncRequired) {
139
+ const resyncCursor = typeof page.resyncCursor === "number" && Number.isFinite(page.resyncCursor) && page.resyncCursor >= 0 ? page.resyncCursor : since;
140
+ controller.enqueue(encodeSseResyncEvent(resyncCursor));
141
+ break;
142
+ }
143
+ for (const change of page.changes) controller.enqueue(encodeSseEvent({
144
+ table: change.table,
145
+ operation: change.operation,
146
+ rowId: change.rowId,
147
+ tenantId: change.tenantId,
148
+ seq: change.seq
149
+ }));
150
+ if (closed) break;
151
+ if (page.cursor === since || page.changes.length === 0) break;
152
+ since = page.cursor;
153
+ while (!closed && controller.desiredSize !== null && controller.desiredSize <= 0) await new Promise((resolve) => setTimeout(resolve, 5));
154
+ }
155
+ } catch (error) {
156
+ logger.warn("_events: cursor catch-up failed", { error: error instanceof Error ? error.message : String(error) });
157
+ }
158
+ heartbeat = setInterval(() => {
159
+ if (closed) return;
160
+ try {
161
+ controller.enqueue(encodeSseComment("heartbeat"));
162
+ } catch {
163
+ teardown();
164
+ }
165
+ }, heartbeatMs);
166
+ heartbeat.unref?.();
167
+ },
168
+ cancel() {
169
+ teardown();
170
+ }
171
+ });
172
+ }
173
+ /**
174
+ * Handle a request against the generated `_events` route.
175
+ *
176
+ * Returns 405 for non-GET; 401 when no auth middleware is configured
177
+ * (fail-closed) or the middleware rejects; 503 when the generator has no
178
+ * database; otherwise a 200 `text/event-stream` response whose body is the
179
+ * live signal stream (built by {@link buildChangeEventStream}).
180
+ */
181
+ async function handleEventsRoute(req, options) {
182
+ if (req.method !== "GET") return new Response(JSON.stringify({ error: "Method not allowed" }), {
183
+ status: 405,
184
+ headers: { "Content-Type": "application/json" }
185
+ });
186
+ if (!options.authMiddleware) return new Response(JSON.stringify({ error: "Authentication required" }), {
187
+ status: 401,
188
+ headers: { "Content-Type": "application/json" }
189
+ });
190
+ const authResult = await options.authMiddleware(EVENTS_ROUTE_OBJECT_NAME, req.method.toLowerCase())(req);
191
+ if (authResult instanceof Response) return authResult;
192
+ if (options.db == null) return new Response(JSON.stringify({ error: "Live events unavailable: no database configured for the API generator" }), {
193
+ status: 503,
194
+ headers: { "Content-Type": "application/json" }
195
+ });
196
+ const db = await resolveChangesDb(options.db);
197
+ await ensureChangeFeedTable(db);
198
+ const cursor = parseCursor(authResult);
199
+ const tenantScope = resolveDispatchTenantScope();
200
+ return new Response(buildChangeEventStream(db, {
201
+ cursor,
202
+ tenantScope
203
+ }), {
204
+ status: 200,
205
+ headers: {
206
+ "Content-Type": "text/event-stream",
207
+ "Cache-Control": "no-cache, no-transform",
208
+ Connection: "keep-alive",
209
+ "X-Accel-Buffering": "no"
210
+ }
211
+ });
212
+ }
213
+ /**
214
+ * Resolve the catch-up cursor for a request: `Last-Event-ID` header first
215
+ * (what an auto-reconnecting EventSource sends), then `?since=`. Returns a
216
+ * non-negative integer, or `null` for live-forward only.
217
+ */
218
+ function parseCursor(req) {
219
+ const lastEventId = req.headers.get("Last-Event-ID");
220
+ if (lastEventId !== null && lastEventId.trim() !== "") {
221
+ const n = Number(lastEventId);
222
+ if (Number.isFinite(n) && n >= 0) return Math.floor(n);
223
+ }
224
+ const since = new URL(req.url).searchParams.get("since");
225
+ if (since !== null && since.trim() !== "") {
226
+ const n = Number(since);
227
+ if (Number.isFinite(n) && n >= 0) return Math.floor(n);
228
+ }
229
+ return null;
230
+ }
231
+ //#endregion
232
+ export { DEFAULT_EVENTS_HEARTBEAT_MS, EVENTS_ROUTE_OBJECT_NAME, buildChangeEventStream, handleEventsRoute, signalVisibleToTenant };
233
+
234
+ //# sourceMappingURL=events-route.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events-route.js","names":[],"sources":["../../src/generators/events-route.ts"],"sourcesContent":["/**\n * Generated `_events` SSE route — live change signals (issue #1763, SERVER\n * half; parent PRD #1755).\n *\n * Handles `GET {basePath}/_events` in the REST generator: an auth-guarded,\n * tenant-scoped Server-Sent-Events stream of coarse change signals ({table,\n * operation, rowId, tenantId} + a `seq` cursor in the SSE `id:` field). It is\n * the push companion to the pull-based `_changes` route (#1758): a subscriber\n * reacts to a signal by re-reading through the authorized collection routes,\n * so **no row payload ever crosses this channel** — authorization stays\n * entirely on the read path.\n *\n * The stream lifecycle (subscribe, catch-up replay, heartbeat, teardown) lives\n * in {@link buildChangeEventStream} so it is written and tested once; both the\n * REST generator here and the generated SvelteKit route import it. `rest.ts`\n * only registers the path.\n *\n * Contract:\n * - **Fail-closed auth** (identical to `_changes`, #1540 posture): no\n * `authMiddleware` configured → 401; the middleware may return a Response to\n * short-circuit (e.g. 403). The feed spans every table, so per-model\n * `api: { public }` opt-outs deliberately do not apply.\n * - **Tenant scope is captured ONCE at connection open** — signal delivery\n * happens from a different async context (the writer's afterSave, possibly\n * another request or replica) with no tenant ALS active, so the filter must\n * be the value resolved at subscribe time, not re-resolved per signal.\n * - **Same-origin only** for this slice: `rest.ts` does NOT wrap `_events` in\n * CORS headers. `EventSource` cannot set request headers and credentialed\n * cross-origin SSE needs `Access-Control-Allow-Credentials` the CORS helper\n * does not emit; cross-origin SSE is a deliberate follow-up.\n */\n\nimport { createLogger } from '@happyvertical/logger';\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { ensureChangeFeedTable, getChangesSince } from '../change-feed.js';\nimport {\n type ChangeSignal,\n subscribeToChangeSignals,\n} from '../change-signals.js';\nimport {\n type DispatchTenantScope,\n resolveDispatchTenantScope,\n} from '../dispatch/tenant-resolver.js';\nimport {\n type ChangesAuthMiddleware,\n resolveChangesDb,\n} from './changes-route.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/** Options for the `_events` route handler. */\nexport interface EventsRouteOptions {\n /** The generator's configured auth middleware, if any. */\n authMiddleware?: ChangesAuthMiddleware;\n /** The generator's `APIContext.db` (instance, config object, or URL string). */\n db?: unknown;\n}\n\n/**\n * Pseudo object name passed to the auth middleware for the events route, so\n * middlewares can recognize and specially authorize it (mirrors `_changes`).\n */\nexport const EVENTS_ROUTE_OBJECT_NAME = '_events';\n\n/** Default heartbeat interval (ms). Overridable via stream options. */\nexport const DEFAULT_EVENTS_HEARTBEAT_MS = 15000;\n\nconst encoder = new TextEncoder();\n\n/** Options for {@link buildChangeEventStream}. */\nexport interface ChangeEventStreamOptions {\n /**\n * Catch-up cursor. When a non-negative number, changes after it are replayed\n * before going live; `null` means live-forward only (no catch-up).\n */\n cursor: number | null;\n /**\n * Tenant scope captured at connection open. Delivery filters against this\n * fixed value — it must NOT be re-resolved per signal (delivery runs outside\n * any tenant ALS context).\n */\n tenantScope: DispatchTenantScope;\n /** Heartbeat interval (ms). Defaults to {@link DEFAULT_EVENTS_HEARTBEAT_MS}. */\n heartbeatMs?: number;\n}\n\n/**\n * Whether a signal is visible to a captured tenant scope. Exact same rule as\n * `getChangesSince`'s tenantId filter, run **synchronously server-side** inside\n * the enqueue callback before any byte hits the wire:\n * - not enforced → visible.\n * - enforced, no active tenant (`tenantId === null`) → only global signals.\n * - enforced, tenant `T` → `T`'s signals plus global signals.\n */\nexport function signalVisibleToTenant(\n sig: ChangeSignal,\n scope: DispatchTenantScope,\n): boolean {\n if (!scope.enforced) return true;\n if (scope.tenantId === null) return sig.tenantId === null;\n return sig.tenantId === scope.tenantId || sig.tenantId === null;\n}\n\n/**\n * SSE frame for a change signal. The `data` JSON is EXACTLY\n * `{table, operation, rowId, tenantId}` — the `seq` lives only in the `id:`\n * field (the EventSource `Last-Event-ID` a client echoes to resume).\n */\nfunction encodeSseEvent(sig: ChangeSignal): Uint8Array {\n const data = JSON.stringify({\n table: sig.table,\n operation: sig.operation,\n rowId: sig.rowId,\n tenantId: sig.tenantId,\n });\n return encoder.encode(`id: ${sig.seq}\\nevent: change\\ndata: ${data}\\n\\n`);\n}\n\n/** SSE resync frame. The id advances EventSource past an unservable cursor. */\nfunction encodeSseResyncEvent(cursor: number): Uint8Array {\n return encoder.encode(`id: ${cursor}\\nevent: resync\\ndata: {}\\n\\n`);\n}\n\n/** SSE comment line (used for heartbeats — ignored by EventSource). */\nfunction encodeSseComment(text: string): Uint8Array {\n return encoder.encode(`: ${text}\\n\\n`);\n}\n\n/**\n * Build the SSE body stream for an `_events` connection.\n *\n * `start(controller)`:\n * a. **Subscribe FIRST**, before catch-up. Subscribing before the catch-up\n * read closes the gap window: a write landing between subscribe and the\n * catch-up read is delivered twice (once live, once in the replay) — which\n * is safe, since the client dedupes by the SSE `id:`/seq.\n * b. Write the `retry:` reconnection hint.\n * c. If a cursor was supplied, replay changes after it (paging until\n * exhausted); on `resyncRequired`, emit `event: resync` at the server's\n * fresh horizon.\n * d. Start the heartbeat interval.\n *\n * `cancel()` tears down on disconnect: clears the heartbeat and unsubscribes,\n * so a dropped client never leaks its subscription (which would pin the dead\n * controller and keep the cross-replica listener refcount above 0).\n */\nexport function buildChangeEventStream(\n db: DatabaseInterface,\n options: ChangeEventStreamOptions,\n): ReadableStream<Uint8Array> {\n const { cursor, tenantScope } = options;\n const heartbeatMs = options.heartbeatMs ?? DEFAULT_EVENTS_HEARTBEAT_MS;\n\n let unsubscribe: (() => void) | null = null;\n let heartbeat: ReturnType<typeof setInterval> | null = null;\n let closed = false;\n\n const teardown = () => {\n if (closed) return;\n closed = true;\n if (heartbeat) {\n clearInterval(heartbeat);\n heartbeat = null;\n }\n if (unsubscribe) {\n unsubscribe();\n unsubscribe = null;\n }\n };\n\n return new ReadableStream<Uint8Array>({\n async start(controller) {\n // (a) Subscribe FIRST, before catch-up — closes the subscribe/catch-up\n // gap window (a write in between is delivered twice; the client dedupes\n // by seq). The tenant filter uses the scope captured at open, never a\n // per-signal re-resolution.\n unsubscribe = subscribeToChangeSignals(db, (sig) => {\n if (closed) return;\n if (!signalVisibleToTenant(sig, tenantScope)) return;\n try {\n controller.enqueue(encodeSseEvent(sig));\n } catch {\n // Controller already closed (client gone before cancel fired) —\n // tear down so we stop trying to write to a dead controller.\n teardown();\n }\n });\n\n // (b) Reconnection hint.\n controller.enqueue(encoder.encode('retry: 3000\\n\\n'));\n\n // (c) Catch-up replay from the cursor, if one was supplied.\n if (cursor != null) {\n try {\n // Catch-up MUST filter by the scope captured at connection open, not\n // re-resolve the tenant via ALS at call time. start() happens to run\n // in-request today, but relying on that is fragile — and it must match\n // the live-signal filter exactly (signalVisibleToTenant): when\n // enforced, `scope.tenantId` (a tenant id → that tenant + global; null\n // → global only); when not enforced, undefined → no tenant filter.\n const catchupTenantId = tenantScope.enforced\n ? tenantScope.tenantId\n : undefined;\n let since = cursor;\n // Page until exhausted (cursor stops advancing / resync).\n for (;;) {\n const page = await getChangesSince(db, {\n since,\n tenantId: catchupTenantId,\n });\n if (page.resyncRequired) {\n const resyncCursor =\n typeof page.resyncCursor === 'number' &&\n Number.isFinite(page.resyncCursor) &&\n page.resyncCursor >= 0\n ? page.resyncCursor\n : since;\n controller.enqueue(encodeSseResyncEvent(resyncCursor));\n break;\n }\n for (const change of page.changes) {\n controller.enqueue(\n encodeSseEvent({\n table: change.table,\n operation: change.operation,\n rowId: change.rowId,\n tenantId: change.tenantId,\n seq: change.seq,\n }),\n );\n }\n if (closed) break;\n if (page.cursor === since || page.changes.length === 0) {\n break;\n }\n since = page.cursor;\n // NOTE: catch-up enqueues per-page without a hard cap. It is\n // bounded — an over-old cursor hits `resyncRequired` and stops — but\n // a large retention window replayed to a slow client could spike\n // memory. Honor the controller's backpressure signal cheaply: when\n // the internal queue is full (`desiredSize <= 0`), yield between\n // pages so the consumer drains first. Bounded by `closed` (set on\n // cancel/disconnect), so it can't spin on a client that never reads.\n while (\n !closed &&\n controller.desiredSize !== null &&\n controller.desiredSize <= 0\n ) {\n await new Promise((resolve) => setTimeout(resolve, 5));\n }\n }\n } catch (error) {\n logger.warn('_events: cursor catch-up failed', {\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n\n // (d) Heartbeat keeps intermediaries from idling the connection out.\n heartbeat = setInterval(() => {\n if (closed) return;\n try {\n controller.enqueue(encodeSseComment('heartbeat'));\n } catch {\n teardown();\n }\n }, heartbeatMs);\n // Do not keep the event loop alive solely for heartbeats.\n (heartbeat as { unref?: () => void }).unref?.();\n },\n cancel() {\n // Client disconnected (abort) — release the subscription + heartbeat.\n teardown();\n },\n });\n}\n\n/**\n * Handle a request against the generated `_events` route.\n *\n * Returns 405 for non-GET; 401 when no auth middleware is configured\n * (fail-closed) or the middleware rejects; 503 when the generator has no\n * database; otherwise a 200 `text/event-stream` response whose body is the\n * live signal stream (built by {@link buildChangeEventStream}).\n */\nexport async function handleEventsRoute(\n req: Request,\n options: EventsRouteOptions,\n): Promise<Response> {\n if (req.method !== 'GET') {\n return new Response(JSON.stringify({ error: 'Method not allowed' }), {\n status: 405,\n headers: { 'Content-Type': 'application/json' },\n });\n }\n\n // Fail-closed (#1540): the signal stream spans every table, so it is never\n // public — an auth middleware must be configured and must pass.\n if (!options.authMiddleware) {\n return new Response(JSON.stringify({ error: 'Authentication required' }), {\n status: 401,\n headers: { 'Content-Type': 'application/json' },\n });\n }\n const authCheck = options.authMiddleware(\n EVENTS_ROUTE_OBJECT_NAME,\n req.method.toLowerCase(),\n );\n const authResult = await authCheck(req);\n if (authResult instanceof Response) {\n return authResult;\n }\n\n if (options.db == null) {\n return new Response(\n JSON.stringify({\n error:\n 'Live events unavailable: no database configured for the API generator',\n }),\n { status: 503, headers: { 'Content-Type': 'application/json' } },\n );\n }\n\n const db = await resolveChangesDb(options.db);\n // A raw handle passed straight to the generator may not have gone through\n // framework init; the feed table backs cursor catch-up.\n await ensureChangeFeedTable(db);\n\n // Cursor: Last-Event-ID (reconnection) takes precedence over ?since=.\n // Default = live-forward only (no catch-up).\n const cursor = parseCursor(authResult);\n\n // Capture the tenant scope ONCE at connection open — delivery runs outside\n // any tenant ALS context and must filter against this fixed value.\n const tenantScope = resolveDispatchTenantScope();\n\n return new Response(buildChangeEventStream(db, { cursor, tenantScope }), {\n status: 200,\n headers: {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n },\n });\n}\n\n/**\n * Resolve the catch-up cursor for a request: `Last-Event-ID` header first\n * (what an auto-reconnecting EventSource sends), then `?since=`. Returns a\n * non-negative integer, or `null` for live-forward only.\n */\nfunction parseCursor(req: Request): number | null {\n const lastEventId = req.headers.get('Last-Event-ID');\n if (lastEventId !== null && lastEventId.trim() !== '') {\n const n = Number(lastEventId);\n if (Number.isFinite(n) && n >= 0) return Math.floor(n);\n }\n\n const since = new URL(req.url).searchParams.get('since');\n if (since !== null && since.trim() !== '') {\n const n = Number(since);\n if (Number.isFinite(n) && n >= 0) return Math.floor(n);\n }\n\n return null;\n}\n"],"mappings":"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"}
@@ -4,6 +4,7 @@
4
4
  export type { CLIConfig, CLIContext } from './cli';
5
5
  export { CLIGenerator, getCLIHandler, setupCLI } from './cli';
6
6
  export { canonicalReadRepresentation, computeBodyEtag, computeTableVersionEtag, conditionalJsonResponse, ifNoneMatchHasConcreteMatch, ifNoneMatchSatisfied, PRIVATE_READ_CACHE_CONTROL, type ReadCacheControlOptions, resolveReadCacheControl, resolveTenantEtagDiscriminator, versionConditionalResponse, warnIfSharedCacheNeutralized, } from './conditional-get';
7
+ export { buildChangeEventStream, type ChangeEventStreamOptions, DEFAULT_EVENTS_HEARTBEAT_MS, signalVisibleToTenant, } from './events-route';
7
8
  export type { MCPConfig, MCPContext, MCPRequest, MCPResponse, MCPTool, } from './mcp';
8
9
  export { MCPGenerator } from './mcp';
9
10
  export type { APIConfig, APIContext, RestServerConfig } from './rest';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/generators/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AAEnD,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAG9D,OAAO,EACL,2BAA2B,EAC3B,eAAe,EACf,uBAAuB,EACvB,uBAAuB,EACvB,2BAA2B,EAC3B,oBAAoB,EACpB,0BAA0B,EAC1B,KAAK,uBAAuB,EAC5B,uBAAuB,EACvB,8BAA8B,EAC9B,0BAA0B,EAC1B,4BAA4B,GAC7B,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EACV,SAAS,EACT,UAAU,EACV,UAAU,EACV,WAAW,EACX,OAAO,GACR,MAAM,OAAO,CAAC;AAEf,OAAO,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AACrC,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAEtE,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAC;AACzE,YAAY,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE/C,OAAO,EACL,mBAAmB,EACnB,cAAc,GACf,MAAM,WAAW,CAAC;AAEnB,OAAO,EACL,iBAAiB,EACjB,yBAAyB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,GACvB,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/generators/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AAEnD,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAG9D,OAAO,EACL,2BAA2B,EAC3B,eAAe,EACf,uBAAuB,EACvB,uBAAuB,EACvB,2BAA2B,EAC3B,oBAAoB,EACpB,0BAA0B,EAC1B,KAAK,uBAAuB,EAC5B,uBAAuB,EACvB,8BAA8B,EAC9B,0BAA0B,EAC1B,4BAA4B,GAC7B,MAAM,mBAAmB,CAAC;AAK3B,OAAO,EACL,sBAAsB,EACtB,KAAK,wBAAwB,EAC7B,2BAA2B,EAC3B,qBAAqB,GACtB,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,SAAS,EACT,UAAU,EACV,UAAU,EACV,WAAW,EACX,OAAO,GACR,MAAM,OAAO,CAAC;AAEf,OAAO,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AACrC,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAEtE,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,QAAQ,CAAC;AACzE,YAAY,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE/C,OAAO,EACL,mBAAmB,EACnB,cAAc,GACf,MAAM,WAAW,CAAC;AAEnB,OAAO,EACL,iBAAiB,EACjB,yBAAyB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,GACvB,MAAM,eAAe,CAAC"}
@@ -1,7 +1,8 @@
1
1
  import { runWithTenantGate, setTenantEntryPointRunner } from "./tenant-gate.js";
2
2
  import { CLIGenerator, getCLIHandler, setupCLI } from "./cli.js";
3
3
  import { PRIVATE_READ_CACHE_CONTROL, canonicalReadRepresentation, computeBodyEtag, computeTableVersionEtag, conditionalJsonResponse, ifNoneMatchHasConcreteMatch, ifNoneMatchSatisfied, resolveReadCacheControl, resolveTenantEtagDiscriminator, versionConditionalResponse, warnIfSharedCacheNeutralized } from "./conditional-get.js";
4
+ import { DEFAULT_EVENTS_HEARTBEAT_MS, buildChangeEventStream, signalVisibleToTenant } from "./events-route.js";
4
5
  import { MCPGenerator } from "./mcp.js";
5
6
  import { APIGenerator, createRestServer, startRestServer } from "./rest.js";
6
7
  import { generateOpenAPISpec, setupSwaggerUI } from "./swagger.js";
7
- export { APIGenerator, CLIGenerator, MCPGenerator, PRIVATE_READ_CACHE_CONTROL, canonicalReadRepresentation, computeBodyEtag, computeTableVersionEtag, conditionalJsonResponse, createRestServer, generateOpenAPISpec, getCLIHandler, ifNoneMatchHasConcreteMatch, ifNoneMatchSatisfied, resolveReadCacheControl, resolveTenantEtagDiscriminator, runWithTenantGate, setTenantEntryPointRunner, setupCLI, setupSwaggerUI, startRestServer, versionConditionalResponse, warnIfSharedCacheNeutralized };
8
+ export { APIGenerator, CLIGenerator, DEFAULT_EVENTS_HEARTBEAT_MS, MCPGenerator, PRIVATE_READ_CACHE_CONTROL, buildChangeEventStream, canonicalReadRepresentation, computeBodyEtag, computeTableVersionEtag, conditionalJsonResponse, createRestServer, generateOpenAPISpec, getCLIHandler, ifNoneMatchHasConcreteMatch, ifNoneMatchSatisfied, resolveReadCacheControl, resolveTenantEtagDiscriminator, runWithTenantGate, setTenantEntryPointRunner, setupCLI, setupSwaggerUI, signalVisibleToTenant, startRestServer, versionConditionalResponse, warnIfSharedCacheNeutralized };
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-runtime-template.d.ts","sourceRoot":"","sources":["../../src/generators/mcp-runtime-template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAStD,MAAM,WAAW,cAAc;IAC7B,6CAA6C;IAC7C,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,yBAAyB;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,kCAAkC;IAClC,MAAM,CAAC,EAAE,SAAS,CAAC;IAEnB,8CAA8C;IAC9C,OAAO,CAAC,EAAE,UAAU,CAAC;IAErB,2BAA2B;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB,wDAAwD;IACxD,KAAK,CAAC,EAAE,KAAK,CAAC;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACtC,CAAC,CAAC;IAEH;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;CAChC;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,GAAE,cAAmB,GAAG,MAAM,CAyZ7E;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,GAAE,MAA6B,GACxC,MAAM,CAER;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,MAAM,CASR;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,MAAM,CA0GR"}
1
+ {"version":3,"file":"mcp-runtime-template.d.ts","sourceRoot":"","sources":["../../src/generators/mcp-runtime-template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAStD,MAAM,WAAW,cAAc;IAC7B,6CAA6C;IAC7C,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,yBAAyB;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,kCAAkC;IAClC,MAAM,CAAC,EAAE,SAAS,CAAC;IAEnB,8CAA8C;IAC9C,OAAO,CAAC,EAAE,UAAU,CAAC;IAErB,2BAA2B;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB,wDAAwD;IACxD,KAAK,CAAC,EAAE,KAAK,CAAC;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACtC,CAAC,CAAC;IAEH;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;CAChC;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,GAAE,cAAmB,GAAG,MAAM,CA+Z7E;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,GAAE,MAA6B,GACxC,MAAM,CAER;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,MAAM,CASR;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,MAAM,CA0GR"}
@@ -31,7 +31,7 @@ ${indent} ai: aiConfig
31
31
  ${indent} });
32
32
 
33
33
  ${indent} const items = await collection.list({ where, limit, offset });
34
- ${indent} const itemsPublic = items.map((item) => item.toPublicJSON());
34
+ ${indent} const itemsPublic = items.map((item) => item.toPublicJSON(PUBLIC_JSON_OPTIONS));
35
35
  ${indent} return { content: [{ type: 'text', text: JSON.stringify(itemsPublic) }] };
36
36
  ${indent}}`;
37
37
  case "get": return `${indent}case '${tool.name}': {
@@ -51,7 +51,7 @@ ${indent} if (!item) {
51
51
  ${indent} throw new Error('Object not found');
52
52
  ${indent} }
53
53
 
54
- ${indent} return { content: [{ type: 'text', text: JSON.stringify(item.toPublicJSON()) }] };
54
+ ${indent} return { content: [{ type: 'text', text: JSON.stringify(item.toPublicJSON(PUBLIC_JSON_OPTIONS)) }] };
55
55
  ${indent}}`;
56
56
  case "create": return `${indent}case '${tool.name}': {
57
57
  ${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {
@@ -62,7 +62,7 @@ ${indent} });
62
62
  ${indent} const newItem = await collection.create(applyWritablePolicy('${capitalize(objectName)}', args));
63
63
  ${indent} await newItem.save();
64
64
 
65
- ${indent} return { content: [{ type: 'text', text: JSON.stringify(newItem.toPublicJSON()) }] };
65
+ ${indent} return { content: [{ type: 'text', text: JSON.stringify(newItem.toPublicJSON(PUBLIC_JSON_OPTIONS)) }] };
66
66
  ${indent}}`;
67
67
  case "update": return `${indent}case '${tool.name}': {
68
68
  ${indent} const { id, ...updateData } = args;
@@ -83,7 +83,7 @@ ${indent} }
83
83
  ${indent} Object.assign(existing, applyWritablePolicy('${capitalize(objectName)}', updateData));
84
84
  ${indent} await existing.save();
85
85
 
86
- ${indent} return { content: [{ type: 'text', text: JSON.stringify(existing.toPublicJSON()) }] };
86
+ ${indent} return { content: [{ type: 'text', text: JSON.stringify(existing.toPublicJSON(PUBLIC_JSON_OPTIONS)) }] };
87
87
  ${indent}}`;
88
88
  case "delete": return `${indent}case '${tool.name}': {
89
89
  ${indent} if (!args.id) {
@@ -176,6 +176,12 @@ const TENANT_SCOPED = new Set(${JSON.stringify(tenantScopedSet)});
176
176
  const MCP_TENANT_ID = process.env.SMRT_MCP_TENANT_ID || undefined;
177
177
  const MCP_ALLOW_CROSS_TENANT = process.env.SMRT_MCP_ALLOW_CROSS_TENANT === 'true';
178
178
  ` : ""}
179
+ const PUBLIC_JSON_OPTIONS = {
180
+ permissions: (process.env.SMRT_MCP_PERMISSIONS || '')
181
+ .split(',')
182
+ .map((permission) => permission.trim())
183
+ .filter(Boolean),
184
+ };
179
185
 
180
186
  /**
181
187
  * Mass-assignment guard (#1540): strip framework/server-managed and
@@ -217,7 +223,7 @@ function applyWritablePolicy(objectName: string, data: any): Record<string, any>
217
223
  */
218
224
  function toPublicResult(value: any, seen: WeakSet<object> = new WeakSet()): any {
219
225
  if (value === null || typeof value !== 'object') return value;
220
- if (typeof value.toPublicJSON === 'function') return value.toPublicJSON();
226
+ if (typeof value.toPublicJSON === 'function') return value.toPublicJSON(PUBLIC_JSON_OPTIONS);
221
227
  if (Array.isArray(value)) {
222
228
  if (seen.has(value)) return value;
223
229
  seen.add(value);
@@ -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/sdk\n * - Tool registration from MCPGenerator\n * - Stdio transport connection\n * - Error handling and logging\n * - Graceful shutdown\n */\n\nimport type { MCPConfig, MCPContext } from './mcp.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 }>;\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/**\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 tenantScopedObjects = [],\n } = options;\n\n // Generate static tool array as TypeScript code\n const toolsCode = tools.length > 0 ? JSON.stringify(tools, null, 2) : '[]';\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, action] = tool.name.split('_');\n\n switch (action) {\n case 'list':\n return `${indent}case '${tool.name}': {\n${indent} const limit = args.limit ?? 50;\n${indent} const offset = args.offset ?? 0;\n${indent} const where = args.where ?? {};\n\n${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {\n${indent} persistence: { type: 'sql', url: process.env.DATABASE_URL || ':memory:' },\n${indent} ai: aiConfig\n${indent} });\n\n${indent} const items = await collection.list({ where, limit, offset });\n${indent} const itemsPublic = items.map((item) => item.toPublicJSON());\n${indent} return { content: [{ type: 'text', text: 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('${capitalize(objectName)}', {\n${indent} persistence: { type: 'sql', 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 { content: [{ type: 'text', text: JSON.stringify(item.toPublicJSON()) }] };\n${indent}}`;\n\n case 'create':\n return `${indent}case '${tool.name}': {\n${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {\n${indent} persistence: { type: 'sql', url: process.env.DATABASE_URL || ':memory:' },\n${indent} ai: aiConfig\n${indent} });\n\n${indent} const newItem = await collection.create(applyWritablePolicy('${capitalize(objectName)}', args));\n${indent} await newItem.save();\n\n${indent} return { content: [{ type: 'text', text: JSON.stringify(newItem.toPublicJSON()) }] };\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('${capitalize(objectName)}', {\n${indent} persistence: { type: 'sql', 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('${capitalize(objectName)}', updateData));\n${indent} await existing.save();\n\n${indent} return { content: [{ type: 'text', text: JSON.stringify(existing.toPublicJSON()) }] };\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('${capitalize(objectName)}', {\n${indent} persistence: { type: 'sql', 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 { content: [{ type: 'text', text: JSON.stringify({ success: true, message: 'Object deleted successfully' }) }] };\n${indent}}`;\n\n default:\n // Custom action\n return `${indent}case '${tool.name}': {\n${indent} const { id, options = {}, ...directArgs } = args;\n\n${indent} if (!id) {\n${indent} throw new Error('ID is required for custom action ${action}');\n${indent} }\n\n${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {\n${indent} persistence: { type: 'sql', url: process.env.DATABASE_URL || ':memory:' },\n${indent} ai: aiConfig\n${indent} });\n\n${indent} const object = await collection.get(id);\n${indent} if (!object) {\n${indent} throw new Error('Object not found');\n${indent} }\n\n${indent} if (typeof object['${action}'] !== 'function') {\n${indent} throw new Error('Method ${action} not found on object');\n${indent} }\n\n${indent} const methodArgs = Object.keys(options).length > 0 ? options : directArgs;\n${indent} const result = await object['${action}'](methodArgs);\n\n${indent} return { content: [{ type: 'text', text: JSON.stringify(toPublicResult(result)) }] };\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 { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport {\n CallToolRequestSchema,\n ListToolsRequestSchema,\n type CallToolRequest,\n type ListToolsRequest,\n} from '@modelcontextprotocol/sdk/types.js';\nimport { ObjectRegistry } from '@happyvertical/smrt-core';\nimport { config } from '@happyvertical/smrt-config';\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};\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}\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/**\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();\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\n/**\n * Main server startup function\n */\nasync function main() {\n try {\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 // Load configuration from environment and .smrt.config files\n const appConfig = await config.load();\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 },\n }\n );\n\n // Register ListTools handler\n server.setRequestHandler(ListToolsRequestSchema, async (_request: ListToolsRequest) => {\n if (DEBUG) {\n console.error(\\`[MCP] ListTools request received\\`);\n }\n\n return {\n tools: TOOLS,\n };\n });\n\n // Register CallTool handler\n server.setRequestHandler(CallToolRequestSchema, 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] = toolName.split('_');\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 {\n content: [\n {\n type: 'text',\n text: \\`Error executing tool \\${toolName}: \\${errorMessage}\\`,\n },\n ],\n isError: true,\n };\n }\n });\n\n // Setup stdio transport\n const transport = new StdioServerTransport();\n\n // Connect server to transport\n await server.connect(transport);\n\n if (DEBUG) {\n console.error(\\`[MCP] Server connected via stdio transport\\`);\n console.error(\\`[MCP] Ready to receive requests\\`);\n }\n\n // Handle graceful shutdown\n process.on('SIGINT', async () => {\n if (DEBUG) {\n console.error(\\`[MCP] Received SIGINT, shutting down gracefully\\`);\n }\n await server.close();\n process.exit(0);\n });\n\n process.on('SIGTERM', async () => {\n if (DEBUG) {\n console.error(\\`[MCP] Received SIGTERM, shutting down gracefully\\`);\n }\n await server.close();\n process.exit(0);\n });\n } catch (error) {\n console.error('[MCP] Fatal error during server startup:', error);\n process.exit(1);\n }\n}\n\n// Start the server\nmain().catch((error) => {\n console.error('[MCP] Unhandled error:', error);\n process.exit(1);\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. **Generic 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\n2. **Provider-specific fallbacks**:\n - \\`OPENAI_API_KEY\\` - OpenAI API key (auto-detects provider as 'openai')\n - \\`ANTHROPIC_API_KEY\\` - Anthropic API key (auto-detects provider as 'anthropic')\n - \\`CLAUDE_API_KEY\\` + \\`CLAUDE_MODEL\\` - Claude CLI provider (defaults to 'sonnet')\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/sdk\n * - Tool registration from MCPGenerator\n * - Stdio transport connection\n * - Error handling and logging\n * - Graceful shutdown\n */\n\nimport type { MCPConfig, MCPContext } from './mcp.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 }>;\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/**\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 tenantScopedObjects = [],\n } = options;\n\n // Generate static tool array as TypeScript code\n const toolsCode = tools.length > 0 ? JSON.stringify(tools, null, 2) : '[]';\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, action] = tool.name.split('_');\n\n switch (action) {\n case 'list':\n return `${indent}case '${tool.name}': {\n${indent} const limit = args.limit ?? 50;\n${indent} const offset = args.offset ?? 0;\n${indent} const where = args.where ?? {};\n\n${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {\n${indent} persistence: { type: 'sql', url: process.env.DATABASE_URL || ':memory:' },\n${indent} ai: aiConfig\n${indent} });\n\n${indent} const items = await collection.list({ where, limit, offset });\n${indent} const itemsPublic = items.map((item) => item.toPublicJSON(PUBLIC_JSON_OPTIONS));\n${indent} return { content: [{ type: 'text', text: 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('${capitalize(objectName)}', {\n${indent} persistence: { type: 'sql', 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 { content: [{ type: 'text', text: JSON.stringify(item.toPublicJSON(PUBLIC_JSON_OPTIONS)) }] };\n${indent}}`;\n\n case 'create':\n return `${indent}case '${tool.name}': {\n${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {\n${indent} persistence: { type: 'sql', url: process.env.DATABASE_URL || ':memory:' },\n${indent} ai: aiConfig\n${indent} });\n\n${indent} const newItem = await collection.create(applyWritablePolicy('${capitalize(objectName)}', args));\n${indent} await newItem.save();\n\n${indent} return { content: [{ type: 'text', text: JSON.stringify(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('${capitalize(objectName)}', {\n${indent} persistence: { type: 'sql', 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('${capitalize(objectName)}', updateData));\n${indent} await existing.save();\n\n${indent} return { content: [{ type: 'text', text: JSON.stringify(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('${capitalize(objectName)}', {\n${indent} persistence: { type: 'sql', 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 { content: [{ type: 'text', text: JSON.stringify({ success: true, message: 'Object deleted successfully' }) }] };\n${indent}}`;\n\n default:\n // Custom action\n return `${indent}case '${tool.name}': {\n${indent} const { id, options = {}, ...directArgs } = args;\n\n${indent} if (!id) {\n${indent} throw new Error('ID is required for custom action ${action}');\n${indent} }\n\n${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {\n${indent} persistence: { type: 'sql', url: process.env.DATABASE_URL || ':memory:' },\n${indent} ai: aiConfig\n${indent} });\n\n${indent} const object = await collection.get(id);\n${indent} if (!object) {\n${indent} throw new Error('Object not found');\n${indent} }\n\n${indent} if (typeof object['${action}'] !== 'function') {\n${indent} throw new Error('Method ${action} not found on object');\n${indent} }\n\n${indent} const methodArgs = Object.keys(options).length > 0 ? options : directArgs;\n${indent} const result = await object['${action}'](methodArgs);\n\n${indent} return { content: [{ type: 'text', text: JSON.stringify(toPublicResult(result)) }] };\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 { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\nimport {\n CallToolRequestSchema,\n ListToolsRequestSchema,\n type CallToolRequest,\n type ListToolsRequest,\n} from '@modelcontextprotocol/sdk/types.js';\nimport { ObjectRegistry } from '@happyvertical/smrt-core';\nimport { config } from '@happyvertical/smrt-config';\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};\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 * 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/**\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\n/**\n * Main server startup function\n */\nasync function main() {\n try {\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 // Load configuration from environment and .smrt.config files\n const appConfig = await config.load();\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 },\n }\n );\n\n // Register ListTools handler\n server.setRequestHandler(ListToolsRequestSchema, async (_request: ListToolsRequest) => {\n if (DEBUG) {\n console.error(\\`[MCP] ListTools request received\\`);\n }\n\n return {\n tools: TOOLS,\n };\n });\n\n // Register CallTool handler\n server.setRequestHandler(CallToolRequestSchema, 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] = toolName.split('_');\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 {\n content: [\n {\n type: 'text',\n text: \\`Error executing tool \\${toolName}: \\${errorMessage}\\`,\n },\n ],\n isError: true,\n };\n }\n });\n\n // Setup stdio transport\n const transport = new StdioServerTransport();\n\n // Connect server to transport\n await server.connect(transport);\n\n if (DEBUG) {\n console.error(\\`[MCP] Server connected via stdio transport\\`);\n console.error(\\`[MCP] Ready to receive requests\\`);\n }\n\n // Handle graceful shutdown\n process.on('SIGINT', async () => {\n if (DEBUG) {\n console.error(\\`[MCP] Received SIGINT, shutting down gracefully\\`);\n }\n await server.close();\n process.exit(0);\n });\n\n process.on('SIGTERM', async () => {\n if (DEBUG) {\n console.error(\\`[MCP] Received SIGTERM, shutting down gracefully\\`);\n }\n await server.close();\n process.exit(0);\n });\n } catch (error) {\n console.error('[MCP] Fatal error during server startup:', error);\n process.exit(1);\n }\n}\n\n// Start the server\nmain().catch((error) => {\n console.error('[MCP] Unhandled error:', error);\n process.exit(1);\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. **Generic 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\n2. **Provider-specific fallbacks**:\n - \\`OPENAI_API_KEY\\` - OpenAI API key (auto-detects provider as 'openai')\n - \\`ANTHROPIC_API_KEY\\` - Anthropic API key (auto-detects provider as 'anthropic')\n - \\`CLAUDE_API_KEY\\` + \\`CLAUDE_MODEL\\` - Claude CLI provider (defaults to 'sonnet')\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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"}
@@ -30,6 +30,8 @@ export interface MCPContext {
30
30
  id: string;
31
31
  roles?: string[];
32
32
  };
33
+ /** Resolved permission slugs held by the caller. */
34
+ permissions?: Iterable<string>;
33
35
  /**
34
36
  * Tenant the calling principal is scoped to (#1554). When set, tenant-scoped
35
37
  * tools run inside this tenant's context. Hosts that authenticate a principal
@@ -114,6 +116,7 @@ export declare class MCPGenerator {
114
116
  * primitives pass through unchanged; a cycle guard prevents infinite loops.
115
117
  */
116
118
  private toPublicData;
119
+ private getPublicJsonOptions;
117
120
  /**
118
121
  * Mass-assignment guard (#1540): strip framework/server-managed and
119
122
  * `@field({ readonly: true })` fields from a create/update body, and — when an
@@ -1 +1 @@
1
- {"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../../src/generators/mcp.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../../src/generators/mcp.ts"],"names":[],"mappings":"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"}
@@ -353,23 +353,26 @@ var MCPGenerator = class {
353
353
  * would otherwise call its `toJSON()`. Non-plain instances (Date, etc.) and
354
354
  * primitives pass through unchanged; a cycle guard prevents infinite loops.
355
355
  */
356
- toPublicData(value, seen = /* @__PURE__ */ new WeakSet()) {
356
+ toPublicData(value, seen = /* @__PURE__ */ new WeakSet(), options = this.getPublicJsonOptions()) {
357
357
  if (value === null || typeof value !== "object") return value;
358
358
  const publicSource = value;
359
- if (typeof publicSource.toPublicJSON === "function") return publicSource.toPublicJSON();
359
+ if (typeof publicSource.toPublicJSON === "function") return publicSource.toPublicJSON(options);
360
360
  if (Array.isArray(value)) {
361
361
  if (seen.has(value)) return value;
362
362
  seen.add(value);
363
- return value.map((entry) => this.toPublicData(entry, seen));
363
+ return value.map((entry) => this.toPublicData(entry, seen, options));
364
364
  }
365
365
  const proto = Object.getPrototypeOf(value);
366
366
  if (proto !== Object.prototype && proto !== null) return value;
367
367
  if (seen.has(value)) return value;
368
368
  seen.add(value);
369
369
  const out = {};
370
- for (const [key, entry] of Object.entries(value)) out[key] = this.toPublicData(entry, seen);
370
+ for (const [key, entry] of Object.entries(value)) out[key] = this.toPublicData(entry, seen, options);
371
371
  return out;
372
372
  }
373
+ getPublicJsonOptions() {
374
+ return { permissions: this.context.permissions };
375
+ }
373
376
  /**
374
377
  * Mass-assignment guard (#1540): strip framework/server-managed and
375
378
  * `@field({ readonly: true })` fields from a create/update body, and — when an
@@ -718,7 +721,7 @@ ${indent} ai: aiConfig
718
721
  ${indent} });
719
722
 
720
723
  ${indent} const items = await collection.list({ where, limit, offset });
721
- ${indent} const itemsPublic = items.map((item) => item.toPublicJSON());
724
+ ${indent} const itemsPublic = items.map((item) => item.toPublicJSON(PUBLIC_JSON_OPTIONS));
722
725
  ${indent} return { content: [{ type: 'text', text: JSON.stringify(itemsPublic) }] };
723
726
  ${indent}}`;
724
727
  case "get": return `${indent}case '${tool.name}': {
@@ -738,7 +741,7 @@ ${indent} if (!item) {
738
741
  ${indent} throw new Error('Object not found');
739
742
  ${indent} }
740
743
 
741
- ${indent} return { content: [{ type: 'text', text: JSON.stringify(item.toPublicJSON()) }] };
744
+ ${indent} return { content: [{ type: 'text', text: JSON.stringify(item.toPublicJSON(PUBLIC_JSON_OPTIONS)) }] };
742
745
  ${indent}}`;
743
746
  case "create": return `${indent}case '${tool.name}': {
744
747
  ${indent} const collection = await ObjectRegistry.getCollection('${capitalize(objectName)}', {
@@ -749,7 +752,7 @@ ${indent} });
749
752
  ${indent} const newItem = await collection.create(applyWritablePolicy('${capitalize(objectName)}', args));
750
753
  ${indent} await newItem.save();
751
754
 
752
- ${indent} return { content: [{ type: 'text', text: JSON.stringify(newItem.toPublicJSON()) }] };
755
+ ${indent} return { content: [{ type: 'text', text: JSON.stringify(newItem.toPublicJSON(PUBLIC_JSON_OPTIONS)) }] };
753
756
  ${indent}}`;
754
757
  case "update": return `${indent}case '${tool.name}': {
755
758
  ${indent} const { id, ...updateData } = args;
@@ -770,7 +773,7 @@ ${indent} }
770
773
  ${indent} Object.assign(existing, applyWritablePolicy('${capitalize(objectName)}', updateData));
771
774
  ${indent} await existing.save();
772
775
 
773
- ${indent} return { content: [{ type: 'text', text: JSON.stringify(existing.toPublicJSON()) }] };
776
+ ${indent} return { content: [{ type: 'text', text: JSON.stringify(existing.toPublicJSON(PUBLIC_JSON_OPTIONS)) }] };
774
777
  ${indent}}`;
775
778
  case "delete": return `${indent}case '${tool.name}': {
776
779
  ${indent} if (!args.id) {
@@ -851,6 +854,12 @@ const TENANT_SCOPED = new Set(${JSON.stringify(tenantScopedSet)});
851
854
  const MCP_TENANT_ID = process.env.SMRT_MCP_TENANT_ID || undefined;
852
855
  const MCP_ALLOW_CROSS_TENANT = process.env.SMRT_MCP_ALLOW_CROSS_TENANT === 'true';
853
856
  ` : ""}
857
+ const PUBLIC_JSON_OPTIONS = {
858
+ permissions: (process.env.SMRT_MCP_PERMISSIONS || '')
859
+ .split(',')
860
+ .map((permission) => permission.trim())
861
+ .filter(Boolean),
862
+ };
854
863
 
855
864
  /**
856
865
  * Mass-assignment guard (#1540): strip framework/server-managed and
@@ -892,7 +901,7 @@ function applyWritablePolicy(objectName: string, data: any): Record<string, any>
892
901
  */
893
902
  function toPublicResult(value: any, seen: WeakSet<object> = new WeakSet()): any {
894
903
  if (value === null || typeof value !== 'object') return value;
895
- if (typeof value.toPublicJSON === 'function') return value.toPublicJSON();
904
+ if (typeof value.toPublicJSON === 'function') return value.toPublicJSON(PUBLIC_JSON_OPTIONS);
896
905
  if (Array.isArray(value)) {
897
906
  if (seen.has(value)) return value;
898
907
  seen.add(value);