@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.
- package/README.md +21 -0
- package/dist/generated-client-runtime.d.ts +6 -5
- package/dist/generated-client-runtime.d.ts.map +1 -1
- package/dist/generated-client-runtime.js +49 -14
- package/dist/generated-client-runtime.js.map +1 -1
- package/dist/generators/index.d.ts +3 -2
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +3 -2
- package/dist/generators/mcp-runtime-template.d.ts +10 -0
- package/dist/generators/mcp-runtime-template.d.ts.map +1 -1
- package/dist/generators/mcp-runtime-template.js +238 -4
- package/dist/generators/mcp-runtime-template.js.map +1 -1
- package/dist/generators/mcp.d.ts +93 -0
- package/dist/generators/mcp.d.ts.map +1 -1
- package/dist/generators/mcp.js +166 -11
- package/dist/generators/mcp.js.map +1 -1
- package/dist/generators/rest.d.ts.map +1 -1
- package/dist/generators/rest.js +3 -0
- package/dist/generators/rest.js.map +1 -1
- package/dist/generators/typed-http-error.d.ts +16 -0
- package/dist/generators/typed-http-error.d.ts.map +1 -0
- package/dist/generators/typed-http-error.js +17 -0
- package/dist/generators/typed-http-error.js.map +1 -0
- package/dist/generators.js +3 -2
- package/dist/index.js +3 -2
- package/dist/manifest/static-manifest.js +1 -1
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest.json +1 -1
- package/dist/prebuild/index.d.ts.map +1 -1
- package/dist/prebuild/index.js +13 -1
- package/dist/prebuild/index.js.map +1 -1
- package/dist/registry/types.d.ts +23 -0
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +3 -3
- package/dist/system/compatibility.d.ts.map +1 -1
- package/dist/system/compatibility.js +6 -0
- package/dist/system/compatibility.js.map +1 -1
- package/dist/vite-plugin/index.d.ts +2 -0
- package/dist/vite-plugin/index.d.ts.map +1 -1
- package/dist/vite-plugin/index.js +24 -11
- package/dist/vite-plugin/index.js.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +94 -26
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/dist/vite-plugin/web-collections.d.ts +1 -0
- package/dist/vite-plugin/web-collections.d.ts.map +1 -1
- package/dist/vite-plugin/web-collections.js +17 -2
- package/dist/vite-plugin/web-collections.js.map +1 -1
- package/package.json +5 -5
- package/dist/manifest/test-manifest-loader.d.ts +0 -3
- package/dist/manifest/test-manifest-loader.d.ts.map +0 -1
- package/dist/manifest/test-manifest-stub.d.ts +0 -4
- package/dist/manifest/test-manifest-stub.d.ts.map +0 -1
- package/dist/manifest/test-manifest-stub.js +0 -77056
- 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
|
|
13
|
-
*
|
|
14
|
-
* messages. Both helpers are emitted with a
|
|
15
|
-
* collide with a collection key in the generated
|
|
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
|
|
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
|
|
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
|
|
14
|
-
*
|
|
15
|
-
* messages. Both helpers are emitted with a
|
|
16
|
-
* collide with a collection key in the generated
|
|
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
|
-
|
|
59
|
-
|
|
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 +
|
|
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
|
|
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 +
|
|
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
|
|
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,
|
|
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"}
|
package/dist/generators/index.js
CHANGED
|
@@ -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,
|
|
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 = {}
|
|
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
|
-
|
|
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
|
};
|