@happyvertical/smrt-core 0.40.60 → 0.40.62

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +21 -0
  2. package/dist/generated-client-runtime.d.ts +6 -5
  3. package/dist/generated-client-runtime.d.ts.map +1 -1
  4. package/dist/generated-client-runtime.js +49 -14
  5. package/dist/generated-client-runtime.js.map +1 -1
  6. package/dist/generators/index.d.ts +3 -2
  7. package/dist/generators/index.d.ts.map +1 -1
  8. package/dist/generators/index.js +3 -2
  9. package/dist/generators/mcp-runtime-template.d.ts +10 -0
  10. package/dist/generators/mcp-runtime-template.d.ts.map +1 -1
  11. package/dist/generators/mcp-runtime-template.js +238 -4
  12. package/dist/generators/mcp-runtime-template.js.map +1 -1
  13. package/dist/generators/mcp.d.ts +93 -0
  14. package/dist/generators/mcp.d.ts.map +1 -1
  15. package/dist/generators/mcp.js +166 -11
  16. package/dist/generators/mcp.js.map +1 -1
  17. package/dist/generators/rest.d.ts.map +1 -1
  18. package/dist/generators/rest.js +3 -0
  19. package/dist/generators/rest.js.map +1 -1
  20. package/dist/generators/typed-http-error.d.ts +16 -0
  21. package/dist/generators/typed-http-error.d.ts.map +1 -0
  22. package/dist/generators/typed-http-error.js +17 -0
  23. package/dist/generators/typed-http-error.js.map +1 -0
  24. package/dist/generators.js +3 -2
  25. package/dist/index.js +3 -2
  26. package/dist/manifest/static-manifest.js +1 -1
  27. package/dist/manifest/static-manifest.js.map +1 -1
  28. package/dist/manifest/store.js +1 -1
  29. package/dist/manifest.json +1 -1
  30. package/dist/prebuild/index.d.ts.map +1 -1
  31. package/dist/prebuild/index.js +13 -1
  32. package/dist/prebuild/index.js.map +1 -1
  33. package/dist/registry/types.d.ts +23 -0
  34. package/dist/registry/types.d.ts.map +1 -1
  35. package/dist/smrt-knowledge.json +3 -3
  36. package/dist/system/compatibility.d.ts.map +1 -1
  37. package/dist/system/compatibility.js +6 -0
  38. package/dist/system/compatibility.js.map +1 -1
  39. package/dist/vite-plugin/index.d.ts +2 -0
  40. package/dist/vite-plugin/index.d.ts.map +1 -1
  41. package/dist/vite-plugin/index.js +24 -11
  42. package/dist/vite-plugin/index.js.map +1 -1
  43. package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
  44. package/dist/vite-plugin/sveltekit-generator.js +94 -26
  45. package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
  46. package/dist/vite-plugin/web-collections.d.ts +1 -0
  47. package/dist/vite-plugin/web-collections.d.ts.map +1 -1
  48. package/dist/vite-plugin/web-collections.js +17 -2
  49. package/dist/vite-plugin/web-collections.js.map +1 -1
  50. package/package.json +5 -5
  51. package/dist/manifest/test-manifest-loader.d.ts +0 -3
  52. package/dist/manifest/test-manifest-loader.d.ts.map +0 -1
  53. package/dist/manifest/test-manifest-stub.d.ts +0 -4
  54. package/dist/manifest/test-manifest-stub.d.ts.map +0 -1
  55. package/dist/manifest/test-manifest-stub.js +0 -77056
  56. package/dist/manifest/test-manifest-stub.js.map +0 -1
package/README.md CHANGED
@@ -368,6 +368,27 @@ class Product extends SmrtObject { /* ... */ }
368
368
 
369
369
  Generators produce OpenAPI REST endpoints, Commander CLI commands, and MCP server tools respectively. The Vite plugin (`smrtPlugin`) generates virtual modules at dev time for routes, clients, and manifests.
370
370
 
371
+ ### Durable MCP tasks
372
+
373
+ Long-running item actions may opt into the experimental
374
+ `io.modelcontextprotocol/tasks` extension. Tasks are disabled by default; list
375
+ the action names explicitly and mark the action as background-eligible:
376
+
377
+ ```typescript
378
+ @smrt({
379
+ mcp: { include: ['generateReport'], tasks: ['generateReport'] },
380
+ })
381
+ class Report extends SmrtObject {
382
+ @backgroundEligible()
383
+ async generateReport(): Promise<ReportResult> { /* ... */ }
384
+ }
385
+ ```
386
+
387
+ The generated MCP server advertises the extension only when at least one task
388
+ action is enabled. A task-aware client can request the action as a durable job,
389
+ then use `tasks/get`, `tasks/update`, and `tasks/cancel` to observe or control
390
+ it. Generated stdio servers run an `mcp-tasks` worker automatically.
391
+
371
392
  ## Dependencies
372
393
 
373
394
  - `@happyvertical/ai` -- AI client (is/do operations, embeddings)
@@ -9,10 +9,11 @@
9
9
  * failures were therefore invisible to callers — an optimistic-update layer
10
10
  * (e.g. TanStack DB `onInsert`) could never observe the failure and roll back.
11
11
  *
12
- * `SmrtClientError` carries the HTTP `status` and the parsed error `body` (when
13
- * the response was JSON), so callers can branch on status and surface server
14
- * messages. Both helpers are emitted with a `__smrt` prefix so they cannot
15
- * collide with a collection key in the generated module.
12
+ * `SmrtClientError` carries the HTTP `status` and, for non-5xx responses, the
13
+ * parsed error `body`, so callers can branch on status and surface safe server
14
+ * messages. Server-side failures stay opaque. Both helpers are emitted with a
15
+ * `__smrt` prefix so they cannot collide with a collection key in the generated
16
+ * module.
16
17
  *
17
18
  * WIRE-SHAPE POLICY (#1797, ADR 0001): the server returns BARE JSON — a bare
18
19
  * array for list, a bare object for get/create/update — with snake_case field
@@ -22,5 +23,5 @@
22
23
  * (vite-plugin + prebuild) are written to match this shape. This same
23
24
  * snake_case wire feeds the mobile DTO codegen; do not change the wire here.
24
25
  */
25
- export declare const CLIENT_FETCH_RUNTIME = "class SmrtClientError extends Error {\n constructor(message, status, body) {\n super(message);\n this.name = 'SmrtClientError';\n this.status = status;\n this.body = body;\n }\n}\n\nasync function __smrtParseBody(response) {\n const contentType = response.headers.get('content-type') || '';\n if (contentType.includes('application/json')) {\n try {\n return await response.json();\n } catch {\n return undefined;\n }\n }\n try {\n return await response.text();\n } catch {\n return undefined;\n }\n}\n\n// Rejects on !response.ok with a typed SmrtClientError; otherwise resolves the\n// parsed JSON body exactly as the server sent it (bare array/object, snake_case\n// fields).\nasync function __smrtFetchJson(url, init) {\n const response = await fetch(url, init);\n const body = await __smrtParseBody(response);\n if (!response.ok) {\n const detail =\n body && typeof body === 'object' && body.error ? ': ' + body.error : '';\n throw new SmrtClientError(\n 'Request failed with status ' + response.status + detail,\n response.status,\n body,\n );\n }\n return body;\n}\n\n// DELETE variant: rejects on !response.ok (so a failed delete is observable),\n// otherwise resolves true.\nasync function __smrtFetchOk(url, init) {\n const response = await fetch(url, init);\n if (!response.ok) {\n const body = await __smrtParseBody(response);\n const detail =\n body && typeof body === 'object' && body.error ? ': ' + body.error : '';\n throw new SmrtClientError(\n 'Request failed with status ' + response.status + detail,\n response.status,\n body,\n );\n }\n return true;\n}";
26
+ export declare const CLIENT_FETCH_RUNTIME = "class SmrtClientError extends Error {\n constructor(message, status, body, code) {\n super(message);\n this.name = 'SmrtClientError';\n this.status = status;\n if (body !== undefined) this.body = body;\n if (code) this.code = code;\n }\n}\n\nasync function __smrtParseBody(response) {\n const contentType = response.headers.get('content-type') || '';\n if (contentType.includes('application/json')) {\n try {\n return await response.json();\n } catch {\n return undefined;\n }\n }\n try {\n return await response.text();\n } catch {\n return undefined;\n }\n}\n\n// Generated REST routes retain the legacy { error: string } form while custom\n// action failures use { error: { ok: false, code, message, status? } }. Keep\n// the nested message human-readable and retain its machine-readable code.\nfunction __smrtErrorDetail(body) {\n if (!body || typeof body !== 'object') return {};\n const error = body.error;\n if (typeof error === 'string') return { message: error };\n if (error && typeof error === 'object') {\n return {\n message: typeof error.message === 'string' ? error.message : undefined,\n code: typeof error.code === 'string' ? error.code : undefined,\n };\n }\n return typeof body.message === 'string' ? { message: body.message } : {};\n}\n\n// Rejects on !response.ok with a typed SmrtClientError; otherwise resolves the\n// parsed JSON body exactly as the server sent it (bare array/object, snake_case\n// fields).\nasync function __smrtFetchJson(url, init) {\n const response = await fetch(url, init);\n if (!response.ok) {\n // Server-side failures may carry stack traces or upstream payloads. Do not\n // parse, interpolate, or expose them to browser callers.\n if (response.status >= 500) {\n throw new SmrtClientError(\n 'Request failed with status ' + response.status,\n response.status,\n );\n }\n const body = await __smrtParseBody(response);\n const detail = __smrtErrorDetail(body);\n throw new SmrtClientError(\n 'Request failed with status ' + response.status +\n (detail.message ? ': ' + detail.message : ''),\n response.status,\n body,\n detail.code,\n );\n }\n return __smrtParseBody(response);\n}\n\n// DELETE variant: rejects on !response.ok (so a failed delete is observable),\n// otherwise resolves true.\nasync function __smrtFetchOk(url, init) {\n const response = await fetch(url, init);\n if (!response.ok) {\n // Keep 5xx failures opaque for the same reason as __smrtFetchJson.\n if (response.status >= 500) {\n throw new SmrtClientError(\n 'Request failed with status ' + response.status,\n response.status,\n );\n }\n const body = await __smrtParseBody(response);\n const detail = __smrtErrorDetail(body);\n throw new SmrtClientError(\n 'Request failed with status ' + response.status +\n (detail.message ? ': ' + detail.message : ''),\n response.status,\n body,\n detail.code,\n );\n }\n return true;\n}";
26
27
  //# sourceMappingURL=generated-client-runtime.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"generated-client-runtime.d.ts","sourceRoot":"","sources":["../src/generated-client-runtime.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,oBAAoB,4pDA0D/B,CAAC"}
1
+ {"version":3,"file":"generated-client-runtime.d.ts","sourceRoot":"","sources":["../src/generated-client-runtime.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,oBAAoB,sgGA4F/B,CAAC"}
@@ -10,10 +10,11 @@
10
10
  * failures were therefore invisible to callers — an optimistic-update layer
11
11
  * (e.g. TanStack DB `onInsert`) could never observe the failure and roll back.
12
12
  *
13
- * `SmrtClientError` carries the HTTP `status` and the parsed error `body` (when
14
- * the response was JSON), so callers can branch on status and surface server
15
- * messages. Both helpers are emitted with a `__smrt` prefix so they cannot
16
- * collide with a collection key in the generated module.
13
+ * `SmrtClientError` carries the HTTP `status` and, for non-5xx responses, the
14
+ * parsed error `body`, so callers can branch on status and surface safe server
15
+ * messages. Server-side failures stay opaque. Both helpers are emitted with a
16
+ * `__smrt` prefix so they cannot collide with a collection key in the generated
17
+ * module.
17
18
  *
18
19
  * WIRE-SHAPE POLICY (#1797, ADR 0001): the server returns BARE JSON — a bare
19
20
  * array for list, a bare object for get/create/update — with snake_case field
@@ -24,11 +25,12 @@
24
25
  * snake_case wire feeds the mobile DTO codegen; do not change the wire here.
25
26
  */
26
27
  var CLIENT_FETCH_RUNTIME = `class SmrtClientError extends Error {
27
- constructor(message, status, body) {
28
+ constructor(message, status, body, code) {
28
29
  super(message);
29
30
  this.name = 'SmrtClientError';
30
31
  this.status = status;
31
- this.body = body;
32
+ if (body !== undefined) this.body = body;
33
+ if (code) this.code = code;
32
34
  }
33
35
  }
34
36
 
@@ -48,22 +50,47 @@ async function __smrtParseBody(response) {
48
50
  }
49
51
  }
50
52
 
53
+ // Generated REST routes retain the legacy { error: string } form while custom
54
+ // action failures use { error: { ok: false, code, message, status? } }. Keep
55
+ // the nested message human-readable and retain its machine-readable code.
56
+ function __smrtErrorDetail(body) {
57
+ if (!body || typeof body !== 'object') return {};
58
+ const error = body.error;
59
+ if (typeof error === 'string') return { message: error };
60
+ if (error && typeof error === 'object') {
61
+ return {
62
+ message: typeof error.message === 'string' ? error.message : undefined,
63
+ code: typeof error.code === 'string' ? error.code : undefined,
64
+ };
65
+ }
66
+ return typeof body.message === 'string' ? { message: body.message } : {};
67
+ }
68
+
51
69
  // Rejects on !response.ok with a typed SmrtClientError; otherwise resolves the
52
70
  // parsed JSON body exactly as the server sent it (bare array/object, snake_case
53
71
  // fields).
54
72
  async function __smrtFetchJson(url, init) {
55
73
  const response = await fetch(url, init);
56
- const body = await __smrtParseBody(response);
57
74
  if (!response.ok) {
58
- const detail =
59
- body && typeof body === 'object' && body.error ? ': ' + body.error : '';
75
+ // Server-side failures may carry stack traces or upstream payloads. Do not
76
+ // parse, interpolate, or expose them to browser callers.
77
+ if (response.status >= 500) {
78
+ throw new SmrtClientError(
79
+ 'Request failed with status ' + response.status,
80
+ response.status,
81
+ );
82
+ }
83
+ const body = await __smrtParseBody(response);
84
+ const detail = __smrtErrorDetail(body);
60
85
  throw new SmrtClientError(
61
- 'Request failed with status ' + response.status + detail,
86
+ 'Request failed with status ' + response.status +
87
+ (detail.message ? ': ' + detail.message : ''),
62
88
  response.status,
63
89
  body,
90
+ detail.code,
64
91
  );
65
92
  }
66
- return body;
93
+ return __smrtParseBody(response);
67
94
  }
68
95
 
69
96
  // DELETE variant: rejects on !response.ok (so a failed delete is observable),
@@ -71,13 +98,21 @@ async function __smrtFetchJson(url, init) {
71
98
  async function __smrtFetchOk(url, init) {
72
99
  const response = await fetch(url, init);
73
100
  if (!response.ok) {
101
+ // Keep 5xx failures opaque for the same reason as __smrtFetchJson.
102
+ if (response.status >= 500) {
103
+ throw new SmrtClientError(
104
+ 'Request failed with status ' + response.status,
105
+ response.status,
106
+ );
107
+ }
74
108
  const body = await __smrtParseBody(response);
75
- const detail =
76
- body && typeof body === 'object' && body.error ? ': ' + body.error : '';
109
+ const detail = __smrtErrorDetail(body);
77
110
  throw new SmrtClientError(
78
- 'Request failed with status ' + response.status + detail,
111
+ 'Request failed with status ' + response.status +
112
+ (detail.message ? ': ' + detail.message : ''),
79
113
  response.status,
80
114
  body,
115
+ detail.code,
81
116
  );
82
117
  }
83
118
  return true;
@@ -1 +1 @@
1
- {"version":3,"file":"generated-client-runtime.js","names":[],"sources":["../src/generated-client-runtime.ts"],"sourcesContent":["/**\n * Shared runtime source injected into every generated API client\n * (`smrtPlugin`'s `@happyvertical/smrt-virt-client` and `smrtConsumer`'s\n * `@smrt/client` fallback).\n *\n * The generated fetchers MUST reject on `!response.ok` (#1796): before this,\n * every fetcher resolved `r.json()` regardless of HTTP status, so a 500 (or any\n * error response) resolved successfully with the error payload. Mutation\n * failures were therefore invisible to callers — an optimistic-update layer\n * (e.g. TanStack DB `onInsert`) could never observe the failure and roll back.\n *\n * `SmrtClientError` carries the HTTP `status` and the parsed error `body` (when\n * the response was JSON), so callers can branch on status and surface server\n * messages. Both helpers are emitted with a `__smrt` prefix so they cannot\n * collide with a collection key in the generated module.\n *\n * WIRE-SHAPE POLICY (#1797, ADR 0001): the server returns BARE JSON — a bare\n * array for list, a bare object for get/create/update — with snake_case field\n * names (`created_at`, `updated_at`) exactly as `SmrtObject.toJSON()` emits\n * them. These helpers pass that JSON through unchanged; they do NOT wrap it in\n * an envelope or camelCase the keys. The generated `.d.ts` declarations\n * (vite-plugin + prebuild) are written to match this shape. This same\n * snake_case wire feeds the mobile DTO codegen; do not change the wire here.\n */\nexport const CLIENT_FETCH_RUNTIME = `class SmrtClientError extends Error {\n constructor(message, status, body) {\n super(message);\n this.name = 'SmrtClientError';\n this.status = status;\n this.body = body;\n }\n}\n\nasync function __smrtParseBody(response) {\n const contentType = response.headers.get('content-type') || '';\n if (contentType.includes('application/json')) {\n try {\n return await response.json();\n } catch {\n return undefined;\n }\n }\n try {\n return await response.text();\n } catch {\n return undefined;\n }\n}\n\n// Rejects on !response.ok with a typed SmrtClientError; otherwise resolves the\n// parsed JSON body exactly as the server sent it (bare array/object, snake_case\n// fields).\nasync function __smrtFetchJson(url, init) {\n const response = await fetch(url, init);\n const body = await __smrtParseBody(response);\n if (!response.ok) {\n const detail =\n body && typeof body === 'object' && body.error ? ': ' + body.error : '';\n throw new SmrtClientError(\n 'Request failed with status ' + response.status + detail,\n response.status,\n body,\n );\n }\n return body;\n}\n\n// DELETE variant: rejects on !response.ok (so a failed delete is observable),\n// otherwise resolves true.\nasync function __smrtFetchOk(url, init) {\n const response = await fetch(url, init);\n if (!response.ok) {\n const body = await __smrtParseBody(response);\n const detail =\n body && typeof body === 'object' && body.error ? ': ' + body.error : '';\n throw new SmrtClientError(\n 'Request failed with status ' + response.status + detail,\n response.status,\n body,\n );\n }\n return true;\n}`;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAwBA,IAAa,uBAAuB"}
1
+ {"version":3,"file":"generated-client-runtime.js","names":[],"sources":["../src/generated-client-runtime.ts"],"sourcesContent":["/**\n * Shared runtime source injected into every generated API client\n * (`smrtPlugin`'s `@happyvertical/smrt-virt-client` and `smrtConsumer`'s\n * `@smrt/client` fallback).\n *\n * The generated fetchers MUST reject on `!response.ok` (#1796): before this,\n * every fetcher resolved `r.json()` regardless of HTTP status, so a 500 (or any\n * error response) resolved successfully with the error payload. Mutation\n * failures were therefore invisible to callers — an optimistic-update layer\n * (e.g. TanStack DB `onInsert`) could never observe the failure and roll back.\n *\n * `SmrtClientError` carries the HTTP `status` and, for non-5xx responses, the\n * parsed error `body`, so callers can branch on status and surface safe server\n * messages. Server-side failures stay opaque. Both helpers are emitted with a\n * `__smrt` prefix so they cannot collide with a collection key in the generated\n * module.\n *\n * WIRE-SHAPE POLICY (#1797, ADR 0001): the server returns BARE JSON — a bare\n * array for list, a bare object for get/create/update — with snake_case field\n * names (`created_at`, `updated_at`) exactly as `SmrtObject.toJSON()` emits\n * them. These helpers pass that JSON through unchanged; they do NOT wrap it in\n * an envelope or camelCase the keys. The generated `.d.ts` declarations\n * (vite-plugin + prebuild) are written to match this shape. This same\n * snake_case wire feeds the mobile DTO codegen; do not change the wire here.\n */\nexport const CLIENT_FETCH_RUNTIME = `class SmrtClientError extends Error {\n constructor(message, status, body, code) {\n super(message);\n this.name = 'SmrtClientError';\n this.status = status;\n if (body !== undefined) this.body = body;\n if (code) this.code = code;\n }\n}\n\nasync function __smrtParseBody(response) {\n const contentType = response.headers.get('content-type') || '';\n if (contentType.includes('application/json')) {\n try {\n return await response.json();\n } catch {\n return undefined;\n }\n }\n try {\n return await response.text();\n } catch {\n return undefined;\n }\n}\n\n// Generated REST routes retain the legacy { error: string } form while custom\n// action failures use { error: { ok: false, code, message, status? } }. Keep\n// the nested message human-readable and retain its machine-readable code.\nfunction __smrtErrorDetail(body) {\n if (!body || typeof body !== 'object') return {};\n const error = body.error;\n if (typeof error === 'string') return { message: error };\n if (error && typeof error === 'object') {\n return {\n message: typeof error.message === 'string' ? error.message : undefined,\n code: typeof error.code === 'string' ? error.code : undefined,\n };\n }\n return typeof body.message === 'string' ? { message: body.message } : {};\n}\n\n// Rejects on !response.ok with a typed SmrtClientError; otherwise resolves the\n// parsed JSON body exactly as the server sent it (bare array/object, snake_case\n// fields).\nasync function __smrtFetchJson(url, init) {\n const response = await fetch(url, init);\n if (!response.ok) {\n // Server-side failures may carry stack traces or upstream payloads. Do not\n // parse, interpolate, or expose them to browser callers.\n if (response.status >= 500) {\n throw new SmrtClientError(\n 'Request failed with status ' + response.status,\n response.status,\n );\n }\n const body = await __smrtParseBody(response);\n const detail = __smrtErrorDetail(body);\n throw new SmrtClientError(\n 'Request failed with status ' + response.status +\n (detail.message ? ': ' + detail.message : ''),\n response.status,\n body,\n detail.code,\n );\n }\n return __smrtParseBody(response);\n}\n\n// DELETE variant: rejects on !response.ok (so a failed delete is observable),\n// otherwise resolves true.\nasync function __smrtFetchOk(url, init) {\n const response = await fetch(url, init);\n if (!response.ok) {\n // Keep 5xx failures opaque for the same reason as __smrtFetchJson.\n if (response.status >= 500) {\n throw new SmrtClientError(\n 'Request failed with status ' + response.status,\n response.status,\n );\n }\n const body = await __smrtParseBody(response);\n const detail = __smrtErrorDetail(body);\n throw new SmrtClientError(\n 'Request failed with status ' + response.status +\n (detail.message ? ': ' + detail.message : ''),\n response.status,\n body,\n detail.code,\n );\n }\n return true;\n}`;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,IAAa,uBAAuB"}
@@ -6,11 +6,12 @@ export { CLIGenerator, getCLIHandler, setupCLI } from './cli';
6
6
  export { canonicalReadRepresentation, computeBodyEtag, computeTableVersionEtag, conditionalJsonResponse, ifNoneMatchHasConcreteMatch, ifNoneMatchSatisfied, PRIVATE_READ_CACHE_CONTROL, type ReadCacheControlOptions, resolveReadCacheControl, resolveTenantEtagDiscriminator, versionConditionalResponse, warnIfSharedCacheNeutralized, } from './conditional-get';
7
7
  export { buildCustomActionInputSchema, buildCustomActionInvocationArgs, type CustomActionFailure, type CustomActionMetadata, type CustomActionScope, customActionParameterInputName, normalizeCustomActionFailure, type ResolveCustomActionMetadataOptions, resolveCustomActionMetadata, SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY, } from './custom-action';
8
8
  export { buildChangeEventStream, type ChangeEventStreamOptions, changeEventSubscribersAtCapacity, DEFAULT_EVENTS_HEARTBEAT_MS, DEFAULT_EVENTS_MAX_SUBSCRIBERS, DEFAULT_EVENTS_RETRY_AFTER_SECONDS, eventStreamCapacityExceededResponse, normalizeEventsMaxSubscribers, signalVisibleToTenant, tryReserveChangeEventSubscriberSlot, } from './events-route';
9
- export type { MCPConfig, MCPContext, MCPRequest, MCPResponse, MCPTool, } from './mcp';
10
- export { MCPGenerator } from './mcp';
9
+ export type { MCPConfig, MCPContext, MCPRequest, MCPResponse, MCPTool, MCPToolListCacheHint, MCPToolListCacheOptions, } from './mcp';
10
+ export { MCP_STABLE_CATALOG_TTL_MS, MCPGenerator, resolveMCPToolListCacheHint, sortMCPTools, } from './mcp';
11
11
  export type { APIConfig, APIContext, RestServerConfig } from './rest';
12
12
  export { APIGenerator, computeRuntimeWebManifestHash, createRestServer, startRestServer, } from './rest';
13
13
  export type { OpenAPIConfig } from './swagger';
14
14
  export { generateOpenAPISpec, setupSwaggerUI, } from './swagger';
15
15
  export { runWithTenantGate, setTenantEntryPointRunner, type TenantEntryPointRunner, type TenantGateOptions, } from './tenant-gate';
16
+ export { normalizeTypedHttpError, type TypedHttpFailure, } from './typed-http-error';
16
17
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/generators/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AAEnD,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAG9D,OAAO,EACL,2BAA2B,EAC3B,eAAe,EACf,uBAAuB,EACvB,uBAAuB,EACvB,2BAA2B,EAC3B,oBAAoB,EACpB,0BAA0B,EAC1B,KAAK,uBAAuB,EAC5B,uBAAuB,EACvB,8BAA8B,EAC9B,0BAA0B,EAC1B,4BAA4B,GAC7B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,4BAA4B,EAC5B,+BAA+B,EAC/B,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EACtB,8BAA8B,EAC9B,4BAA4B,EAC5B,KAAK,kCAAkC,EACvC,2BAA2B,EAC3B,qCAAqC,GACtC,MAAM,iBAAiB,CAAC;AAKzB,OAAO,EACL,sBAAsB,EACtB,KAAK,wBAAwB,EAC7B,gCAAgC,EAChC,2BAA2B,EAC3B,8BAA8B,EAC9B,kCAAkC,EAClC,mCAAmC,EACnC,6BAA6B,EAC7B,qBAAqB,EACrB,mCAAmC,GACpC,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,SAAS,EACT,UAAU,EACV,UAAU,EACV,WAAW,EACX,OAAO,GACR,MAAM,OAAO,CAAC;AAEf,OAAO,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AACrC,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAEtE,OAAO,EACL,YAAY,EACZ,6BAA6B,EAC7B,gBAAgB,EAChB,eAAe,GAChB,MAAM,QAAQ,CAAC;AAChB,YAAY,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE/C,OAAO,EACL,mBAAmB,EACnB,cAAc,GACf,MAAM,WAAW,CAAC;AAEnB,OAAO,EACL,iBAAiB,EACjB,yBAAyB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,GACvB,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/generators/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,OAAO,CAAC;AAEnD,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAG9D,OAAO,EACL,2BAA2B,EAC3B,eAAe,EACf,uBAAuB,EACvB,uBAAuB,EACvB,2BAA2B,EAC3B,oBAAoB,EACpB,0BAA0B,EAC1B,KAAK,uBAAuB,EAC5B,uBAAuB,EACvB,8BAA8B,EAC9B,0BAA0B,EAC1B,4BAA4B,GAC7B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,4BAA4B,EAC5B,+BAA+B,EAC/B,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,EACtB,8BAA8B,EAC9B,4BAA4B,EAC5B,KAAK,kCAAkC,EACvC,2BAA2B,EAC3B,qCAAqC,GACtC,MAAM,iBAAiB,CAAC;AAKzB,OAAO,EACL,sBAAsB,EACtB,KAAK,wBAAwB,EAC7B,gCAAgC,EAChC,2BAA2B,EAC3B,8BAA8B,EAC9B,kCAAkC,EAClC,mCAAmC,EACnC,6BAA6B,EAC7B,qBAAqB,EACrB,mCAAmC,GACpC,MAAM,gBAAgB,CAAC;AACxB,YAAY,EACV,SAAS,EACT,UAAU,EACV,UAAU,EACV,WAAW,EACX,OAAO,EACP,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,OAAO,CAAC;AAEf,OAAO,EACL,yBAAyB,EACzB,YAAY,EACZ,2BAA2B,EAC3B,YAAY,GACb,MAAM,OAAO,CAAC;AACf,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,QAAQ,CAAC;AAEtE,OAAO,EACL,YAAY,EACZ,6BAA6B,EAC7B,gBAAgB,EAChB,eAAe,GAChB,MAAM,QAAQ,CAAC;AAChB,YAAY,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAE/C,OAAO,EACL,mBAAmB,EACnB,cAAc,GACf,MAAM,WAAW,CAAC;AAEnB,OAAO,EACL,iBAAiB,EACjB,yBAAyB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,GACvB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,uBAAuB,EACvB,KAAK,gBAAgB,GACtB,MAAM,oBAAoB,CAAC"}
@@ -3,7 +3,8 @@ import { runWithTenantGate, setTenantEntryPointRunner } from "./tenant-gate.js";
3
3
  import { CLIGenerator, getCLIHandler, setupCLI } from "./cli.js";
4
4
  import { PRIVATE_READ_CACHE_CONTROL, canonicalReadRepresentation, computeBodyEtag, computeTableVersionEtag, conditionalJsonResponse, ifNoneMatchHasConcreteMatch, ifNoneMatchSatisfied, resolveReadCacheControl, resolveTenantEtagDiscriminator, versionConditionalResponse, warnIfSharedCacheNeutralized } from "./conditional-get.js";
5
5
  import { DEFAULT_EVENTS_HEARTBEAT_MS, DEFAULT_EVENTS_MAX_SUBSCRIBERS, DEFAULT_EVENTS_RETRY_AFTER_SECONDS, buildChangeEventStream, changeEventSubscribersAtCapacity, eventStreamCapacityExceededResponse, normalizeEventsMaxSubscribers, signalVisibleToTenant, tryReserveChangeEventSubscriberSlot } from "./events-route.js";
6
- import { MCPGenerator } from "./mcp.js";
6
+ import { MCPGenerator, MCP_STABLE_CATALOG_TTL_MS, resolveMCPToolListCacheHint, sortMCPTools } from "./mcp.js";
7
+ import { normalizeTypedHttpError } from "./typed-http-error.js";
7
8
  import { APIGenerator, computeRuntimeWebManifestHash, createRestServer, startRestServer } from "./rest.js";
8
9
  import { generateOpenAPISpec, setupSwaggerUI } from "./swagger.js";
9
- export { APIGenerator, CLIGenerator, DEFAULT_EVENTS_HEARTBEAT_MS, DEFAULT_EVENTS_MAX_SUBSCRIBERS, DEFAULT_EVENTS_RETRY_AFTER_SECONDS, MCPGenerator, PRIVATE_READ_CACHE_CONTROL, SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY, buildChangeEventStream, buildCustomActionInputSchema, buildCustomActionInvocationArgs, canonicalReadRepresentation, changeEventSubscribersAtCapacity, computeBodyEtag, computeRuntimeWebManifestHash, computeTableVersionEtag, conditionalJsonResponse, createRestServer, customActionParameterInputName, eventStreamCapacityExceededResponse, generateOpenAPISpec, getCLIHandler, ifNoneMatchHasConcreteMatch, ifNoneMatchSatisfied, normalizeCustomActionFailure, normalizeEventsMaxSubscribers, resolveCustomActionMetadata, resolveReadCacheControl, resolveTenantEtagDiscriminator, runWithTenantGate, setTenantEntryPointRunner, setupCLI, setupSwaggerUI, signalVisibleToTenant, startRestServer, tryReserveChangeEventSubscriberSlot, versionConditionalResponse, warnIfSharedCacheNeutralized };
10
+ export { APIGenerator, CLIGenerator, DEFAULT_EVENTS_HEARTBEAT_MS, DEFAULT_EVENTS_MAX_SUBSCRIBERS, DEFAULT_EVENTS_RETRY_AFTER_SECONDS, MCPGenerator, MCP_STABLE_CATALOG_TTL_MS, PRIVATE_READ_CACHE_CONTROL, SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY, buildChangeEventStream, buildCustomActionInputSchema, buildCustomActionInvocationArgs, canonicalReadRepresentation, changeEventSubscribersAtCapacity, computeBodyEtag, computeRuntimeWebManifestHash, computeTableVersionEtag, conditionalJsonResponse, createRestServer, customActionParameterInputName, eventStreamCapacityExceededResponse, generateOpenAPISpec, getCLIHandler, ifNoneMatchHasConcreteMatch, ifNoneMatchSatisfied, normalizeCustomActionFailure, normalizeEventsMaxSubscribers, normalizeTypedHttpError, resolveCustomActionMetadata, resolveMCPToolListCacheHint, resolveReadCacheControl, resolveTenantEtagDiscriminator, runWithTenantGate, setTenantEntryPointRunner, setupCLI, setupSwaggerUI, signalVisibleToTenant, sortMCPTools, startRestServer, tryReserveChangeEventSubscriberSlot, versionConditionalResponse, warnIfSharedCacheNeutralized };
@@ -20,6 +20,11 @@ export interface RuntimeOptions {
20
20
  inputSchema: Record<string, unknown>;
21
21
  outputSchema?: Record<string, unknown>;
22
22
  }>;
23
+ /** Cache hint emitted for deploy-static tools/list results. */
24
+ toolListCacheHint?: {
25
+ ttlMs: number;
26
+ cacheScope: 'private' | 'public';
27
+ };
23
28
  /** Internal invocation metadata; never exposed by the MCP tools/list result. */
24
29
  customActions?: Record<string, {
25
30
  scope: CustomActionScope;
@@ -30,6 +35,11 @@ export interface RuntimeOptions {
30
35
  optionsParameter?: boolean;
31
36
  legacyOptions: boolean;
32
37
  }>;
38
+ /** Task-enabled item custom actions. Never emitted in tools/list. */
39
+ taskActions?: Record<string, {
40
+ objectName: string;
41
+ objectType: string;
42
+ }>;
33
43
  /**
34
44
  * Lowercased simple names of objects that are `@TenantScoped` (#1554). When
35
45
  * non-empty, the generated server imports the tenancy fail-closed gate and
@@ -1 +1 @@
1
- {"version":3,"file":"mcp-runtime-template.d.ts","sourceRoot":"","sources":["../../src/generators/mcp-runtime-template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAStD,MAAM,WAAW,cAAc;IAC7B,6CAA6C;IAC7C,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,yBAAyB;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,kCAAkC;IAClC,MAAM,CAAC,EAAE,SAAS,CAAC;IAEnB,8CAA8C;IAC9C,OAAO,CAAC,EAAE,UAAU,CAAC;IAErB,2BAA2B;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB,wDAAwD;IACxD,KAAK,CAAC,EAAE,KAAK,CAAC;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACrC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACxC,CAAC,CAAC;IACH,gFAAgF;IAChF,aAAa,CAAC,EAAE,MAAM,CACpB,MAAM,EACN;QACE,KAAK,EAAE,iBAAiB,CAAC;QACzB,QAAQ,EAAE,OAAO,CAAC;QAClB,sEAAsE;QACtE,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;QAC1B,gBAAgB,CAAC,EAAE,OAAO,CAAC;QAC3B,aAAa,EAAE,OAAO,CAAC;KACxB,CACF,CAAC;IAEF;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAE/B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACrD;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,GAAE,cAAmB,GAAG,MAAM,CA8d7E;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,GAAE,MAA6B,GACxC,MAAM,CAER;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,MAAM,CASR;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,MAAM,CA0GR"}
1
+ {"version":3,"file":"mcp-runtime-template.d.ts","sourceRoot":"","sources":["../../src/generators/mcp-runtime-template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAC5D,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAStD,MAAM,WAAW,cAAc;IAC7B,6CAA6C;IAC7C,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,mDAAmD;IACnD,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB,yBAAyB;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,kCAAkC;IAClC,MAAM,CAAC,EAAE,SAAS,CAAC;IAEnB,8CAA8C;IAC9C,OAAO,CAAC,EAAE,UAAU,CAAC;IAErB,2BAA2B;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB,wDAAwD;IACxD,KAAK,CAAC,EAAE,KAAK,CAAC;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACrC,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACxC,CAAC,CAAC;IACH,+DAA+D;IAC/D,iBAAiB,CAAC,EAAE;QAClB,KAAK,EAAE,MAAM,CAAC;QACd,UAAU,EAAE,SAAS,GAAG,QAAQ,CAAC;KAClC,CAAC;IACF,gFAAgF;IAChF,aAAa,CAAC,EAAE,MAAM,CACpB,MAAM,EACN;QACE,KAAK,EAAE,iBAAiB,CAAC;QACzB,QAAQ,EAAE,OAAO,CAAC;QAClB,sEAAsE;QACtE,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;QAC1B,gBAAgB,CAAC,EAAE,OAAO,CAAC;QAC3B,aAAa,EAAE,OAAO,CAAC;KACxB,CACF,CAAC;IACF,qEAAqE;IACrE,WAAW,CAAC,EAAE,MAAM,CAClB,MAAM,EACN;QACE,UAAU,EAAE,MAAM,CAAC;QACnB,UAAU,EAAE,MAAM,CAAC;KACpB,CACF,CAAC;IAEF;;;;;;OAMG;IACH,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAE/B;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CACrD;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,GAAE,cAAmB,GAAG,MAAM,CA2sB7E;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,UAAU,GAAE,MAA6B,GACxC,MAAM,CAER;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,MAAM,CASR;AAED;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,GACjB,MAAM,CA0GR"}
@@ -12,8 +12,12 @@ function capitalize(str) {
12
12
  * @returns TypeScript code for server entry point
13
13
  */
14
14
  function generateRuntimeBootstrap(options = {}) {
15
- const { name = "smrt-mcp-server", version = "1.0.0", description = "Auto-generated MCP server from SMRT objects", debug = false, tools = [], customActions = {}, tenantScopedObjects = [], stiTargets = {} } = options;
15
+ const { name = "smrt-mcp-server", version = "1.0.0", description = "Auto-generated MCP server from SMRT objects", debug = false, tools = [], customActions = {}, taskActions = {}, tenantScopedObjects = [], stiTargets = {}, toolListCacheHint = {
16
+ ttlMs: 864e5,
17
+ cacheScope: "private"
18
+ } } = options;
16
19
  const toolsCode = tools.length > 0 ? JSON.stringify(tools, null, 2) : "[]";
20
+ const hasTaskActions = Object.keys(taskActions).length > 0;
17
21
  const tenantScopedSet = Array.from(new Set(tenantScopedObjects.map((n) => n.toLowerCase())));
18
22
  const hasTenantScoped = tenantScopedSet.length > 0;
19
23
  const generateSwitchCases = (indent) => {
@@ -180,14 +184,16 @@ import {
180
184
  type CallToolRequest,
181
185
  type ListToolsRequest,
182
186
  Server,
187
+ ${hasTaskActions ? "specTypeSchemas," : ""}
183
188
  } from '@modelcontextprotocol/server';
184
- import { serveStdio } from '@modelcontextprotocol/server/stdio';
189
+ import { ${hasTaskActions ? "StdioServerTransport, " : ""}serveStdio } from '@modelcontextprotocol/server/stdio';
185
190
  import { existsSync } from 'node:fs';
186
191
  import { resolve } from 'node:path';
187
192
  import { pathToFileURL } from 'node:url';
188
193
  import { ObjectRegistry } from '@happyvertical/smrt-core';
189
194
  import { normalizeCustomActionFailure, SMRT_CUSTOM_ACTION_ERROR_METADATA_KEY } from '@happyvertical/smrt-core';
190
195
  import { loadConfig } from '@happyvertical/smrt-config';
196
+ ${hasTaskActions ? "import { McpTaskStore, TaskRunner } from '@happyvertical/smrt-jobs';\n" : ""}
191
197
  ${hasTenantScoped ? "import { enableTenancy, runTenantScopedEntryPoint } from '@happyvertical/smrt-tenancy';\n" : ""}
192
198
  // Server configuration
193
199
  const SERVER_NAME = ${JSON.stringify(name)};
@@ -197,7 +203,9 @@ const DEBUG = ${debug};
197
203
 
198
204
  // Static tool definitions (generated at build time)
199
205
  const TOOLS = ${toolsCode};
206
+ const TOOL_LIST_CACHE_HINT = ${JSON.stringify(toolListCacheHint)};
200
207
  const CUSTOM_ACTIONS = ${JSON.stringify(customActions)};
208
+ const TASK_ACTIONS = ${JSON.stringify(taskActions)};
201
209
  const STI_TARGETS: Record<string, Record<string, string>> = ${JSON.stringify(stiTargets)};
202
210
  ${hasTenantScoped ? `
203
211
  // Fail-closed tenant context (#1554): tenant-scoped objects must run inside a
@@ -305,6 +313,211 @@ function errorResult(structuredContent: any, text: string, _meta?: Record<string
305
313
  };
306
314
  }
307
315
 
316
+ ${hasTaskActions ? `
317
+ // The bundled SDK validates tools/call responses against its older result
318
+ // codec, which does not yet know CreateTaskResult. Intercept the extension at
319
+ // the transport boundary, then pass every non-task message untouched to the
320
+ // SDK server. This keeps ordinary MCP behaviour and version negotiation owned
321
+ // by the SDK while making the extension available today.
322
+ const MCP_TASKS_EXTENSION = 'io.modelcontextprotocol/tasks';
323
+ let taskRuntime: Promise<{ store: McpTaskStore; runner: TaskRunner }> | undefined;
324
+
325
+ function clientSupportsTasks(message: any): boolean {
326
+ return message?.params?._meta?.['io.modelcontextprotocol/clientCapabilities']
327
+ ?.extensions?.[MCP_TASKS_EXTENSION] !== undefined;
328
+ }
329
+
330
+ /** Validate the 2026 request envelope before task dispatch mutates a job. */
331
+ function taskEnvelopeError(message: any): { code: number; message: string; data?: any } | undefined {
332
+ const meta = message?.params?._meta;
333
+ if (!meta || typeof meta !== 'object' || Array.isArray(meta)) {
334
+ return {
335
+ code: -32602,
336
+ message: 'Request is missing the required _meta envelope for protocol revision 2026-07-28',
337
+ };
338
+ }
339
+ const protocolVersion = meta['io.modelcontextprotocol/protocolVersion'];
340
+ if (protocolVersion !== '2026-07-28') {
341
+ return typeof protocolVersion === 'string'
342
+ ? {
343
+ code: -32022,
344
+ message: 'Unsupported protocol version: ' + protocolVersion,
345
+ data: { supported: ['2026-07-28'], requested: protocolVersion },
346
+ }
347
+ : {
348
+ code: -32602,
349
+ message: 'Invalid _meta envelope for protocol revision 2026-07-28: io.modelcontextprotocol/protocolVersion must be a string',
350
+ };
351
+ }
352
+ if (!specTypeSchemas.ClientCapabilities.safeParse(
353
+ meta['io.modelcontextprotocol/clientCapabilities'],
354
+ ).success) {
355
+ return {
356
+ code: -32602,
357
+ message: 'Invalid _meta envelope for protocol revision 2026-07-28: io.modelcontextprotocol/clientCapabilities is invalid',
358
+ };
359
+ }
360
+ if (
361
+ meta['io.modelcontextprotocol/clientInfo'] !== undefined &&
362
+ !specTypeSchemas.Implementation.safeParse(
363
+ meta['io.modelcontextprotocol/clientInfo'],
364
+ ).success
365
+ ) {
366
+ return {
367
+ code: -32602,
368
+ message: 'Invalid _meta envelope for protocol revision 2026-07-28: io.modelcontextprotocol/clientInfo is invalid',
369
+ };
370
+ }
371
+ if (
372
+ meta['io.modelcontextprotocol/logLevel'] !== undefined &&
373
+ !specTypeSchemas.LoggingLevel.safeParse(
374
+ meta['io.modelcontextprotocol/logLevel'],
375
+ ).success
376
+ ) {
377
+ return {
378
+ code: -32602,
379
+ message: 'Invalid _meta envelope for protocol revision 2026-07-28: io.modelcontextprotocol/logLevel is invalid',
380
+ };
381
+ }
382
+ if (
383
+ meta.progressToken !== undefined &&
384
+ !specTypeSchemas.ProgressToken.safeParse(meta.progressToken).success
385
+ ) {
386
+ return {
387
+ code: -32602,
388
+ message: 'Invalid _meta envelope for protocol revision 2026-07-28: progressToken is invalid',
389
+ };
390
+ }
391
+ return undefined;
392
+ }
393
+
394
+ function jsonRpcError(id: any, code: number, message: string, data?: any) {
395
+ return { jsonrpc: '2.0', id, error: { code, message, ...(data === undefined ? {} : { data }) } };
396
+ }
397
+
398
+ async function getTaskRuntime() {
399
+ if (!taskRuntime) {
400
+ taskRuntime = (async () => {
401
+ const firstAction = Object.values(TASK_ACTIONS)[0] as { objectName: string } | undefined;
402
+ if (!firstAction) throw new Error('No task-enabled MCP action is configured');
403
+ const collection = await ObjectRegistry.getCollection(firstAction.objectName, {
404
+ persistence: { type: process.env.DATABASE_TYPE || 'sqlite', url: process.env.DATABASE_URL || ':memory:' },
405
+ });
406
+ const db = collection.db;
407
+ const store = await McpTaskStore.create(db, { ownerId: process.env.SMRT_MCP_TENANT_ID || null });
408
+ const runner = new TaskRunner({ queues: ['mcp-tasks'] });
409
+ await runner.initialize(db);
410
+ await runner.start();
411
+ return { store, runner };
412
+ })();
413
+ }
414
+ return taskRuntime;
415
+ }
416
+
417
+ function taskInvocationArgs(actionMeta: any, args: Record<string, any>): any[] {
418
+ const { id: _id, options, ...directArgs } = args;
419
+ if (actionMeta.legacyOptions) {
420
+ return [Object.keys(options ?? {}).length > 0 ? options : directArgs];
421
+ }
422
+ if (actionMeta.optionsParameter) return [options];
423
+ const parameterNames = actionMeta.parameterNames || [];
424
+ const invocationParameterNames = parameterNames.at(-1) === 'context'
425
+ ? parameterNames.slice(0, -1)
426
+ : parameterNames;
427
+ return invocationParameterNames.map((parameterName: string) =>
428
+ args[parameterName === 'id' ? 'actionId' : parameterName],
429
+ );
430
+ }
431
+
432
+ async function handleTaskExtensionMessage(message: any): Promise<any | null> {
433
+ if (!message || typeof message !== 'object' || message.id === undefined) return null;
434
+ const method = message.method;
435
+ const params = message.params ?? {};
436
+ const action = method === 'tools/call' ? TASK_ACTIONS[params.name] : undefined;
437
+ const isTaskMethod = action || ['tasks/get', 'tasks/update', 'tasks/cancel'].includes(method);
438
+ if (!isTaskMethod) return null;
439
+ // A task-enabled tool retains its normal synchronous behaviour until its
440
+ // client explicitly opts into Tasks. Do not impose the task envelope on
441
+ // legacy calls that will be passed untouched to the SDK.
442
+ if (!clientSupportsTasks(message) && method === 'tools/call') return null;
443
+ const envelopeError = taskEnvelopeError(message);
444
+ if (envelopeError) {
445
+ return jsonRpcError(message.id, envelopeError.code, envelopeError.message, envelopeError.data);
446
+ }
447
+ if (!clientSupportsTasks(message)) {
448
+ // A task-only method cannot fall back to a legacy response shape. A task
449
+ // tool can still run normally when the client did not opt in, so let the
450
+ // SDK handle that direct tools/call path.
451
+ if (method === 'tools/call') return null;
452
+ return jsonRpcError(message.id, -32021, 'Missing required client capability', {
453
+ requiredCapabilities: { extensions: { [MCP_TASKS_EXTENSION]: {} } },
454
+ });
455
+ }
456
+ try {
457
+ const { store } = await getTaskRuntime();
458
+ if (action) {
459
+ if (typeof params.arguments?.id !== 'string') {
460
+ return jsonRpcError(message.id, -32602, 'Task-enabled custom actions require an id');
461
+ }
462
+ const actionMeta = CUSTOM_ACTIONS[params.name];
463
+ const task = await store.createTask({
464
+ objectType: action.objectType,
465
+ objectId: params.arguments.id,
466
+ method: actionMeta.methodName || params.name.slice(params.name.indexOf('_') + 1),
467
+ invocationArgs: taskInvocationArgs(actionMeta, params.arguments),
468
+ tenantId: ${hasTenantScoped ? "MCP_TENANT_ID ?? null" : "null"},
469
+ });
470
+ return { jsonrpc: '2.0', id: message.id, result: { resultType: 'task', content: [], structuredContent: {}, ...task } };
471
+ }
472
+ if (typeof params.taskId !== 'string') {
473
+ return jsonRpcError(message.id, -32602, 'taskId is required');
474
+ }
475
+ if (method === 'tasks/get') {
476
+ const task = await store.getTask(params.taskId);
477
+ return { jsonrpc: '2.0', id: message.id, result: { resultType: 'complete', ...task } };
478
+ }
479
+ if (method === 'tasks/update') {
480
+ await store.updateTask(
481
+ params.taskId,
482
+ params.inputResponses && typeof params.inputResponses === 'object'
483
+ ? params.inputResponses
484
+ : {},
485
+ );
486
+ return { jsonrpc: '2.0', id: message.id, result: { resultType: 'complete' } };
487
+ }
488
+ await store.cancelTask(params.taskId);
489
+ return { jsonrpc: '2.0', id: message.id, result: { resultType: 'complete' } };
490
+ } catch (error) {
491
+ const messageText = error instanceof Error ? error.message : 'Task operation failed';
492
+ return jsonRpcError(message.id, -32602, messageText);
493
+ }
494
+ }
495
+
496
+ class McpTaskExtensionTransport {
497
+ onclose?: () => void;
498
+ onerror?: (error: Error) => void;
499
+ onmessage?: (message: any) => void;
500
+
501
+ constructor(private readonly wire: StdioServerTransport) {}
502
+
503
+ async start() {
504
+ this.wire.onclose = () => this.onclose?.();
505
+ this.wire.onerror = (error) => this.onerror?.(error);
506
+ this.wire.onmessage = (message) => {
507
+ void (async () => {
508
+ const response = await handleTaskExtensionMessage(message);
509
+ if (response) await this.wire.send(response);
510
+ else this.onmessage?.(message);
511
+ })().catch((error) => this.onerror?.(error instanceof Error ? error : new Error(String(error))));
512
+ };
513
+ await this.wire.start();
514
+ }
515
+
516
+ close() { return this.wire.close(); }
517
+ send(message: any) { return this.wire.send(message); }
518
+ }
519
+ ` : ""}
520
+
308
521
  /**
309
522
  * Main server startup function
310
523
  */
@@ -331,6 +544,16 @@ ${hasTenantScoped ? `
331
544
  ObjectRegistry.loadAllManifests({ manifestPaths: localManifestPaths });
332
545
  }
333
546
 
547
+ // A manifest supplies schemas and tool metadata, while an executable
548
+ // custom action also needs the application's actual class constructor.
549
+ // Consumer builds generate this registration module; load it after the
550
+ // manifest so decorators enrich the already-known metadata.
551
+ const localRegisterPath = process.env.SMRT_MCP_REGISTER_PATH
552
+ || resolve(process.cwd(), '.smrt', 'register.js');
553
+ if (existsSync(localRegisterPath)) {
554
+ await import(pathToFileURL(localRegisterPath).href);
555
+ }
556
+
334
557
  // Load configuration from environment and .smrt.config files
335
558
  const appConfig = await loadConfig();
336
559
  const aiConfig = appConfig?.ai || {};
@@ -349,6 +572,10 @@ ${hasTenantScoped ? `
349
572
  {
350
573
  capabilities: {
351
574
  tools: {},
575
+ ${hasTaskActions ? "extensions: { 'io.modelcontextprotocol/tasks': {} }," : ""}
576
+ },
577
+ cacheHints: {
578
+ 'tools/list': TOOL_LIST_CACHE_HINT,
352
579
  },
353
580
  }
354
581
  );
@@ -360,7 +587,7 @@ ${hasTenantScoped ? `
360
587
  }
361
588
 
362
589
  return {
363
- tools: TOOLS,
590
+ tools: [...TOOLS].sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0),
364
591
  };
365
592
  });
366
593
 
@@ -416,11 +643,18 @@ ${hasTenantScoped ? `
416
643
 
417
644
  async function main() {
418
645
  try {
419
- const handle = serveStdio(() => createServer(), {
646
+ // Initialize the registry before accepting an intercepted task call. The
647
+ // SDK would normally invoke this factory for the first regular message,
648
+ // but task calls can be the first message on a stdio connection.
649
+ const server = await createServer();
650
+ const transport = ${hasTaskActions ? "new McpTaskExtensionTransport(new StdioServerTransport())" : "undefined"};
651
+ const handle = serveStdio(() => server, {
652
+ ...(transport ? { transport } : {}),
420
653
  onerror: (error) => console.error('[MCP] Protocol error:', error),
421
654
  });
422
655
  const shutdown = async () => {
423
656
  if (DEBUG) console.error('[MCP] Shutting down gracefully');
657
+ ${hasTaskActions ? "if (taskRuntime) {\n const { runner } = await taskRuntime;\n await runner.stop();\n }" : ""}
424
658
  await handle.close();
425
659
  process.exit(0);
426
660
  };