@try-works/dsh-model-as-tool 0.1.0 → 0.1.2

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 (79) hide show
  1. package/README.md +51 -0
  2. package/bin/dsh-tool-mode-mcp-stdio.mjs +80 -0
  3. package/catalog/desert-ant-models.json +77 -13
  4. package/lib/adapters/index.d.ts +19 -6
  5. package/lib/adapters/index.d.ts.map +1 -1
  6. package/lib/adapters/index.js +28 -13
  7. package/lib/adapters/tongue.d.ts +11 -3
  8. package/lib/adapters/tongue.d.ts.map +1 -1
  9. package/lib/adapters/tongue.js +12 -4
  10. package/lib/adapters/types.d.ts +11 -0
  11. package/lib/adapters/types.d.ts.map +1 -1
  12. package/lib/adapters/voz.d.ts +7 -1
  13. package/lib/adapters/voz.d.ts.map +1 -1
  14. package/lib/adapters/voz.js +16 -4
  15. package/lib/asset-acquisition.d.ts +137 -0
  16. package/lib/asset-acquisition.d.ts.map +1 -0
  17. package/lib/asset-acquisition.js +161 -0
  18. package/lib/authority.d.ts +153 -0
  19. package/lib/authority.d.ts.map +1 -0
  20. package/lib/authority.js +194 -0
  21. package/lib/bindings.d.ts +115 -0
  22. package/lib/bindings.d.ts.map +1 -0
  23. package/lib/bindings.js +239 -0
  24. package/lib/catalog.d.ts.map +1 -1
  25. package/lib/catalog.js +57 -0
  26. package/lib/contract-payload.d.ts +47 -0
  27. package/lib/contract-payload.d.ts.map +1 -0
  28. package/lib/contract-payload.js +137 -0
  29. package/lib/deployment-coordinator.d.ts +213 -0
  30. package/lib/deployment-coordinator.d.ts.map +1 -0
  31. package/lib/deployment-coordinator.js +274 -0
  32. package/lib/harness-budgets.d.ts +158 -0
  33. package/lib/harness-budgets.d.ts.map +1 -0
  34. package/lib/harness-budgets.js +152 -0
  35. package/lib/index.d.ts +41 -0
  36. package/lib/index.d.ts.map +1 -1
  37. package/lib/index.js +424 -16
  38. package/lib/licensing.d.ts +221 -0
  39. package/lib/licensing.d.ts.map +1 -0
  40. package/lib/licensing.js +188 -0
  41. package/lib/mcp/host-binding.d.ts +91 -0
  42. package/lib/mcp/host-binding.d.ts.map +1 -0
  43. package/lib/mcp/host-binding.js +133 -0
  44. package/lib/mcp/http.d.ts +130 -0
  45. package/lib/mcp/http.d.ts.map +1 -0
  46. package/lib/mcp/http.js +274 -0
  47. package/lib/mcp/protocol.d.ts +261 -0
  48. package/lib/mcp/protocol.d.ts.map +1 -0
  49. package/lib/mcp/protocol.js +387 -0
  50. package/lib/mcp/server.d.ts +115 -0
  51. package/lib/mcp/server.d.ts.map +1 -0
  52. package/lib/mcp/server.js +200 -0
  53. package/lib/mcp/stdio.d.ts +80 -0
  54. package/lib/mcp/stdio.d.ts.map +1 -0
  55. package/lib/mcp/stdio.js +176 -0
  56. package/lib/mcp/subscriptions.d.ts +135 -0
  57. package/lib/mcp/subscriptions.d.ts.map +1 -0
  58. package/lib/mcp/subscriptions.js +207 -0
  59. package/lib/provider-bridge.d.ts +69 -0
  60. package/lib/provider-bridge.d.ts.map +1 -0
  61. package/lib/provider-bridge.js +218 -0
  62. package/lib/publication.d.ts +160 -0
  63. package/lib/publication.d.ts.map +1 -0
  64. package/lib/publication.js +173 -0
  65. package/lib/receipt.d.ts +208 -0
  66. package/lib/receipt.d.ts.map +1 -0
  67. package/lib/receipt.js +150 -0
  68. package/lib/registrar.d.ts +13 -0
  69. package/lib/registrar.d.ts.map +1 -1
  70. package/lib/registrar.js +20 -3
  71. package/lib/resource-governance.d.ts +204 -0
  72. package/lib/resource-governance.d.ts.map +1 -0
  73. package/lib/resource-governance.js +213 -0
  74. package/lib/server-lifecycle.d.ts +175 -0
  75. package/lib/server-lifecycle.d.ts.map +1 -0
  76. package/lib/server-lifecycle.js +269 -0
  77. package/lib/types.d.ts +21 -0
  78. package/lib/types.d.ts.map +1 -1
  79. package/package.json +9 -2
@@ -0,0 +1,200 @@
1
+ /**
2
+ * MCP method dispatch (MCP-05, MCP-06, MCP-08, MCP-09, MCP-13).
3
+ *
4
+ * The server is STATELESS by construction, which is what P26-01 requires: a
5
+ * `tools/list` or `tools/call` must be processable as the first application
6
+ * request. There is no session table here to require one, so the requirement is
7
+ * satisfied by the shape of the code rather than by remembering to skip a check.
8
+ *
9
+ * Dispatch resolves ONE authorized definition and calls it through the host's
10
+ * execution path (MCP-08), rather than reaching into a registry by name — the
11
+ * difference matters because the host's path applies the grant, the receipt and
12
+ * the result policy, while a direct lookup would bypass all three.
13
+ *
14
+ * @module @try-works/dsh-model-as-tool/mcp/server
15
+ */
16
+ import { assertHeadersMatch, cacheableResultFields, implementedCapabilities, SUPPORTED_PROTOCOL_VERSIONS, ERROR_CODES, errorResponse, negotiateVersion, parseRequestMetadata, successResult, toolErrorResult, unsupportedRequiredCapabilities, WireError, } from "./protocol.js";
17
+ /**
18
+ * Project a definition into the MCP tool shape.
19
+ *
20
+ * `outputSchema` is taken from the resolved definition rather than from DSH's
21
+ * model-facing `schemas()` projection, which intentionally omits `output`: a
22
+ * schema-only export built from that projection would be missing the very field
23
+ * MCP-09 requires.
24
+ * @param definition - the resolved definition.
25
+ * @returns the MCP tool descriptor.
26
+ */
27
+ export function toMcpTool(definition) {
28
+ return {
29
+ name: definition.name,
30
+ ...definition.title === undefined ? {} : { title: definition.title },
31
+ description: definition.description,
32
+ inputSchema: definition.inputSchema,
33
+ outputSchema: definition.outputSchema,
34
+ };
35
+ }
36
+ /**
37
+ * Dispatch one request.
38
+ *
39
+ * Stateless: nothing is retained between calls, so a valid request works as the
40
+ * first one.
41
+ * @param request - the parsed request.
42
+ * @param host - the host surface.
43
+ * @param options - headers and the caller's abort signal.
44
+ * @returns the response and the HTTP status.
45
+ */
46
+ export async function dispatch(request, host, options = {}) {
47
+ const id = request.id ?? null;
48
+ try {
49
+ if (options.checkHeaders === true && options.headers !== undefined) {
50
+ assertHeadersMatch(options.headers, request);
51
+ }
52
+ const metadata = parseRequestMetadata(request.params);
53
+ negotiateVersion(metadata);
54
+ // Capabilities the CLIENT requires. A requirement this server cannot meet is
55
+ // -32021 rather than a silent partial service.
56
+ const required = requiredCapabilities(metadata.clientCapabilities);
57
+ const unsupported = unsupportedRequiredCapabilities(required);
58
+ if (unsupported.length > 0) {
59
+ throw new WireError(ERROR_CODES.unsupportedClientCapability, `the client requires unsupported capability: ${unsupported.join(', ')}`, { unsupported });
60
+ }
61
+ switch (request.method) {
62
+ case 'server/discover':
63
+ return {
64
+ response: { jsonrpc: '2.0', id, result: successResult(discoverResult(), host.serverInfo) },
65
+ httpStatus: 200,
66
+ };
67
+ case 'tools/list':
68
+ return {
69
+ response: {
70
+ jsonrpc: '2.0',
71
+ id,
72
+ result: successResult({
73
+ // Deterministic ordering (MCP-06): the caller sees a stable list, so
74
+ // a client diffing two listings does not see spurious changes.
75
+ tools: [...host.listTools()]
76
+ .sort((left, right) => left.name.localeCompare(right.name))
77
+ .map(toMcpTool),
78
+ // Required by the schema's CacheableResult; omitting them makes the
79
+ // result non-conformant even though the tool list is correct.
80
+ ...cacheableResultFields(),
81
+ }, host.serverInfo),
82
+ },
83
+ httpStatus: 200,
84
+ };
85
+ case 'subscriptions/listen': {
86
+ // P26-09. The acknowledgement is returned WITH the result rather than after
87
+ // it, so the client cannot observe an event before being told what the server
88
+ // agreed to send.
89
+ //
90
+ // A host with no registry reports the method as unsupported rather than
91
+ // accepting a subscription it cannot serve: an acknowledged subscription that
92
+ // never fires is indistinguishable from a broken server.
93
+ if (options.subscriptions === undefined) {
94
+ throw new WireError(ERROR_CODES.methodNotFound, 'this server does not offer catalog subscriptions');
95
+ }
96
+ const subscription = options.subscriptions.listen(request, options.clientId ?? 'unknown');
97
+ return {
98
+ response: { jsonrpc: '2.0', id, result: successResult(subscription.result, host.serverInfo) },
99
+ httpStatus: 200,
100
+ followUp: [subscription.acknowledgement],
101
+ };
102
+ }
103
+ case 'tools/call':
104
+ return await callTool(request, id, host, options.signal ?? new AbortController().signal);
105
+ default:
106
+ throw new WireError(ERROR_CODES.methodNotFound, `unknown method ${JSON.stringify(request.method)}`);
107
+ }
108
+ }
109
+ catch (error) {
110
+ const wire = error instanceof WireError
111
+ ? error
112
+ : new WireError(ERROR_CODES.internalError, error instanceof Error ? error.message : String(error));
113
+ // An infrastructure failure must never be turned into a successful domain
114
+ // payload (MCP-10), so it leaves as a JSON-RPC error.
115
+ return {
116
+ response: errorResponse(id, wire),
117
+ // Header problems are a transport-level rejection as well as a JSON-RPC error.
118
+ httpStatus: wire.code === ERROR_CODES.headerMismatch || wire.code === ERROR_CODES.invalidParams ? 400 : 200,
119
+ };
120
+ }
121
+ }
122
+ /** Handle `tools/call`. */
123
+ async function callTool(request, id, host, signal) {
124
+ const name = request.params?.name;
125
+ if (typeof name !== 'string' || name.length === 0) {
126
+ throw new WireError(ERROR_CODES.invalidParams, 'tools/call requires params.name');
127
+ }
128
+ const args = request.params?.arguments;
129
+ if (args !== undefined && (typeof args !== 'object' || args === null || Array.isArray(args))) {
130
+ throw new WireError(ERROR_CODES.invalidParams, 'tools/call arguments must be an object');
131
+ }
132
+ const definition = host.listTools().find(tool => tool.name === name);
133
+ if (definition === undefined) {
134
+ // An unknown name is invalid params, not a tool error: nothing was invoked, so
135
+ // reporting a domain failure would be a fabrication.
136
+ throw new WireError(ERROR_CODES.invalidParams, `unknown tool ${JSON.stringify(name)}`);
137
+ }
138
+ // MCP-13: never re-export an imported MCP tool, or a server could delegate back
139
+ // into itself through a loop that no caller can see.
140
+ if (definition.importedFromMcp) {
141
+ throw new WireError(ERROR_CODES.invalidParams, `tool ${JSON.stringify(name)} was imported from MCP and is not re-exported`);
142
+ }
143
+ const outcome = await host.callTool(name, (args ?? {}), signal);
144
+ const result = outcome.isError === true
145
+ // An application failure is a tool-error RESULT (P26-04), so the protocol
146
+ // exchange still succeeded.
147
+ ? toolErrorResult(outcome.text)
148
+ : {
149
+ content: [{ type: 'text', text: outcome.text }],
150
+ // The finalized, post-policy canonical value (MCP-10).
151
+ structuredContent: outcome.structured,
152
+ };
153
+ return {
154
+ response: { jsonrpc: '2.0', id, result: successResult(result, host.serverInfo) },
155
+ httpStatus: 200,
156
+ };
157
+ }
158
+ /** The `server/discover` result, in the shape the pinned schema requires. */
159
+ function discoverResult() {
160
+ return {
161
+ // `supportedVersions` is the field the schema defines; there is no
162
+ // `protocolVersion` on a DiscoverResult, which is the mistake pinning the schema
163
+ // caught. A client learns the versions the server speaks, not one it picked.
164
+ supportedVersions: [...SUPPORTED_PROTOCOL_VERSIONS],
165
+ capabilities: serverCapabilities(),
166
+ ...cacheableResultFields(),
167
+ };
168
+ }
169
+ /**
170
+ * The capabilities this server advertises.
171
+ *
172
+ * Derived from the feature matrix rather than restated, so the matrix a client
173
+ * reads and the capabilities it is offered cannot disagree. P26-12: a feature is
174
+ * advertised only when it is genuinely implemented.
175
+ * @returns the `ServerCapabilities` object.
176
+ */
177
+ function serverCapabilities() {
178
+ const implemented = new Set(implementedCapabilities());
179
+ return {
180
+ ...implemented.has('tools') ? { tools: {} } : {},
181
+ ...implemented.has('prompts') ? { prompts: {} } : {},
182
+ ...implemented.has('resources/subscribe') ? { resources: {} } : {},
183
+ ...implemented.has('logging') ? { logging: {} } : {},
184
+ };
185
+ }
186
+ /**
187
+ * Read the capabilities a client declared as REQUIRED.
188
+ *
189
+ * A client may declare capabilities it merely supports; only the required ones can
190
+ * make a request unserviceable.
191
+ * @param capabilities - the parsed capability object.
192
+ * @returns the required capability names.
193
+ */
194
+ function requiredCapabilities(capabilities) {
195
+ const required = capabilities.required;
196
+ if (!Array.isArray(required))
197
+ return [];
198
+ return required.filter((name) => typeof name === 'string');
199
+ }
200
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The stdio bridge (MCP-03, P26-05, P26-06, P26-08).
3
+ *
4
+ * `P26-05` is explicit that the bridge IS AN HTTP CLIENT, not a second protocol
5
+ * implementation: it must generate the modern per-request headers correctly and
6
+ * talk to the same endpoint a network client uses. So there is no second
7
+ * interpretation of the protocol here to drift out of step with the first.
8
+ *
9
+ * What P26-08 requires, and how each is met:
10
+ *
11
+ * - **Stdout is exclusively protocol traffic; diagnostics go to stderr.** The two
12
+ * sinks are separate parameters, so a diagnostic cannot reach stdout by being
13
+ * written to "the output" — there is no single output to confuse.
14
+ * - **Request IDs and per-request metadata are preserved.** The body is forwarded
15
+ * verbatim; the bridge regenerates only the HEADERS, which is the one thing the
16
+ * transport requires it to derive.
17
+ * - **One stdio process is not one conversation.** Nothing is retained between
18
+ * requests except in-flight cancellation state, which is keyed by request id and
19
+ * removed when the request settles.
20
+ * - **EOF shuts down bridge-owned requests without killing the host.** Closing
21
+ * aborts what this bridge has in flight; the host is a separate process.
22
+ * - **Cancellation is per request.** `notifications/cancelled` aborts exactly the
23
+ * referenced request, so cancelling one call cannot cancel another sharing the
24
+ * worker.
25
+ *
26
+ * @module @try-works/dsh-model-as-tool/mcp/stdio
27
+ */
28
+ import { type WireRequest } from './protocol.ts';
29
+ /** Where the bridge writes. Two sinks, deliberately not one. */
30
+ export interface BridgeIo {
31
+ /** Protocol traffic. Newline-delimited JSON and nothing else. */
32
+ protocol(line: string): void;
33
+ /** Diagnostics. Never protocol traffic. */
34
+ diagnostic(line: string): void;
35
+ }
36
+ /** How the bridge reaches the endpoint. */
37
+ export interface BridgeEndpoint {
38
+ /** The URL to POST to. */
39
+ readonly url: string;
40
+ /** The bearer token, from the operator's configuration and never from a client. */
41
+ readonly token: string;
42
+ /**
43
+ * The request function. Injected so a test can drive the bridge without a socket
44
+ * and without a real host.
45
+ */
46
+ readonly fetch: (url: string, init: {
47
+ method: string;
48
+ headers: Record<string, string>;
49
+ body: string;
50
+ signal: AbortSignal;
51
+ }) => Promise<{
52
+ status: number;
53
+ headers: Record<string, string>;
54
+ body: string;
55
+ }>;
56
+ }
57
+ /** A running bridge. */
58
+ export interface StdioBridge {
59
+ /**
60
+ * Handle one line of stdin.
61
+ * @param line - the raw line, with or without its newline.
62
+ * @returns the protocol line to emit, or undefined when none is due.
63
+ */
64
+ handleLine(line: string): Promise<string | undefined>;
65
+ /** Abort every request this bridge owns, as EOF does. */
66
+ close(): void;
67
+ /** How many requests this bridge currently owns in flight. */
68
+ readonly inFlight: number;
69
+ }
70
+ /** Build the headers for one request (P26-05). */
71
+ export declare function headersFor(request: WireRequest, token: string): Record<string, string>;
72
+ /**
73
+ * Create a bridge to a running MCP endpoint.
74
+ *
75
+ * @param endpoint - the endpoint and its injected request function.
76
+ * @param io - the protocol and diagnostic sinks.
77
+ * @returns the bridge.
78
+ */
79
+ export declare function createStdioBridge(endpoint: BridgeEndpoint, io: BridgeIo): StdioBridge;
80
+ //# sourceMappingURL=stdio.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"stdio.d.ts","sourceRoot":"","sources":["../../src/mcp/stdio.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAoB,KAAK,WAAW,EAAE,MAAM,eAAe,CAAA;AAElE,gEAAgE;AAChE,MAAM,WAAW,QAAQ;IACvB,iEAAiE;IACjE,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IAC5B,2CAA2C;IAC3C,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B;AAED,2CAA2C;AAC3C,MAAM,WAAW,cAAc;IAC7B,0BAA0B;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,mFAAmF;IACnF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,CACd,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,WAAW,CAAA;KAAE,KACzF,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAChF;AAED,wBAAwB;AACxB,MAAM,WAAW,WAAW;IAC1B;;;;OAIG;IACH,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAA;IACrD,yDAAyD;IACzD,KAAK,IAAI,IAAI,CAAA;IACb,8DAA8D;IAC9D,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAC1B;AAED,kDAAkD;AAClD,wBAAgB,UAAU,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAmBtF;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,cAAc,EAAE,EAAE,EAAE,QAAQ,GAAG,WAAW,CAyGrF"}
@@ -0,0 +1,176 @@
1
+ /**
2
+ * The stdio bridge (MCP-03, P26-05, P26-06, P26-08).
3
+ *
4
+ * `P26-05` is explicit that the bridge IS AN HTTP CLIENT, not a second protocol
5
+ * implementation: it must generate the modern per-request headers correctly and
6
+ * talk to the same endpoint a network client uses. So there is no second
7
+ * interpretation of the protocol here to drift out of step with the first.
8
+ *
9
+ * What P26-08 requires, and how each is met:
10
+ *
11
+ * - **Stdout is exclusively protocol traffic; diagnostics go to stderr.** The two
12
+ * sinks are separate parameters, so a diagnostic cannot reach stdout by being
13
+ * written to "the output" — there is no single output to confuse.
14
+ * - **Request IDs and per-request metadata are preserved.** The body is forwarded
15
+ * verbatim; the bridge regenerates only the HEADERS, which is the one thing the
16
+ * transport requires it to derive.
17
+ * - **One stdio process is not one conversation.** Nothing is retained between
18
+ * requests except in-flight cancellation state, which is keyed by request id and
19
+ * removed when the request settles.
20
+ * - **EOF shuts down bridge-owned requests without killing the host.** Closing
21
+ * aborts what this bridge has in flight; the host is a separate process.
22
+ * - **Cancellation is per request.** `notifications/cancelled` aborts exactly the
23
+ * referenced request, so cancelling one call cannot cancel another sharing the
24
+ * worker.
25
+ *
26
+ * @module @try-works/dsh-model-as-tool/mcp/stdio
27
+ */
28
+ import { PROTOCOL_VERSION } from "./protocol.js";
29
+ /** Build the headers for one request (P26-05). */
30
+ export function headersFor(request, token) {
31
+ // The version comes from the BODY, not from a constant, so a request declaring an
32
+ // unsupported version is refused by the endpoint's header check rather than being
33
+ // silently relabelled by the bridge.
34
+ const bodyVersion = request.params?._meta?.['io.modelcontextprotocol/protocolVersion'];
35
+ const headers = {
36
+ 'content-type': 'application/json',
37
+ accept: 'application/json',
38
+ authorization: `Bearer ${token}`,
39
+ 'mcp-protocol-version': typeof bodyVersion === 'string' ? bodyVersion : PROTOCOL_VERSION,
40
+ 'mcp-method': request.method,
41
+ };
42
+ // `Mcp-Name` is required for a named call and must match the body, so it is
43
+ // derived from the body rather than guessed.
44
+ if (request.method === 'tools/call' && typeof request.params?.name === 'string') {
45
+ headers['mcp-name'] = request.params.name;
46
+ }
47
+ return headers;
48
+ }
49
+ /**
50
+ * Create a bridge to a running MCP endpoint.
51
+ *
52
+ * @param endpoint - the endpoint and its injected request function.
53
+ * @param io - the protocol and diagnostic sinks.
54
+ * @returns the bridge.
55
+ */
56
+ export function createStdioBridge(endpoint, io) {
57
+ /** In-flight requests, keyed by request id, so cancellation is per request. */
58
+ const inFlight = new Map();
59
+ return {
60
+ get inFlight() {
61
+ return inFlight.size;
62
+ },
63
+ async handleLine(line) {
64
+ const trimmed = line.trim();
65
+ // A blank line is a keep-alive, not a malformed frame: skipping it is correct
66
+ // and reporting it would spam a diagnostic channel for no reason.
67
+ if (trimmed.length === 0)
68
+ return undefined;
69
+ let request;
70
+ try {
71
+ request = JSON.parse(trimmed);
72
+ }
73
+ catch {
74
+ // A malformed frame is answered on the PROTOCOL channel, because the client
75
+ // is waiting for a response and a diagnostic would not reach it.
76
+ return frame({
77
+ jsonrpc: '2.0',
78
+ id: null,
79
+ error: { code: -32700, message: 'the frame is not valid JSON' },
80
+ });
81
+ }
82
+ // P26-06: a cancellation names the request it cancels and nothing else.
83
+ if (request.method === 'notifications/cancelled') {
84
+ const target = request.params?.requestId;
85
+ const key = target === undefined ? undefined : String(target);
86
+ if (key !== undefined) {
87
+ const controller = inFlight.get(key);
88
+ if (controller === undefined) {
89
+ // Reported, not ignored: a cancellation for an unknown request usually
90
+ // means the client and bridge disagree about what is in flight.
91
+ io.diagnostic(`cancellation for unknown request ${key}`);
92
+ }
93
+ else {
94
+ controller.abort(new Error('cancelled by the client'));
95
+ inFlight.delete(key);
96
+ io.diagnostic(`cancelled request ${key}`);
97
+ }
98
+ }
99
+ // A notification is acknowledged with nothing at all.
100
+ return undefined;
101
+ }
102
+ const controller = new AbortController();
103
+ const key = request.id === undefined || request.id === null ? undefined : String(request.id);
104
+ if (key !== undefined)
105
+ inFlight.set(key, controller);
106
+ try {
107
+ const response = await endpoint.fetch(endpoint.url, {
108
+ method: 'POST',
109
+ headers: headersFor(request, endpoint.token),
110
+ // The body is forwarded VERBATIM: ids and per-request metadata survive
111
+ // exactly, because the bridge regenerates only the headers.
112
+ body: trimmed,
113
+ signal: controller.signal,
114
+ });
115
+ if (response.status === 202 || response.body.length === 0) {
116
+ // A notification has no response to forward.
117
+ return undefined;
118
+ }
119
+ if (response.status >= 400 && !looksLikeJsonRpc(response.body)) {
120
+ // A transport-level failure has no JSON-RPC body to forward, so it is
121
+ // reported as a diagnostic AND as a protocol error: a client waiting for a
122
+ // response must not be left waiting because the failure was transport.
123
+ io.diagnostic(`the endpoint returned HTTP ${response.status}`);
124
+ return frame({
125
+ jsonrpc: '2.0',
126
+ id: request.id ?? null,
127
+ error: { code: -32603, message: `the endpoint returned HTTP ${response.status}` },
128
+ });
129
+ }
130
+ return frame(JSON.parse(response.body));
131
+ }
132
+ catch (error) {
133
+ if (controller.signal.aborted) {
134
+ // Cancellation is not a failure to report: the client asked for it.
135
+ io.diagnostic(`request ${key ?? '(unknown)'} ended by cancellation`);
136
+ return undefined;
137
+ }
138
+ const message = error instanceof Error ? error.message : String(error);
139
+ io.diagnostic(`the request to the endpoint failed: ${message}`);
140
+ return frame({
141
+ jsonrpc: '2.0',
142
+ id: request.id ?? null,
143
+ error: { code: -32603, message: `the bridge could not reach the endpoint: ${message}` },
144
+ });
145
+ }
146
+ finally {
147
+ if (key !== undefined)
148
+ inFlight.delete(key);
149
+ }
150
+ },
151
+ close() {
152
+ // P26-08: EOF shuts down BRIDGE-OWNED requests. The host is a separate
153
+ // process and is not touched here.
154
+ for (const [key, controller] of inFlight) {
155
+ controller.abort(new Error('the bridge is closing'));
156
+ io.diagnostic(`aborted in-flight request ${key} on close`);
157
+ }
158
+ inFlight.clear();
159
+ },
160
+ };
161
+ }
162
+ /** Serialize one protocol frame. */
163
+ function frame(message) {
164
+ return JSON.stringify(message);
165
+ }
166
+ /** Whether a body is a JSON-RPC message rather than a transport error page. */
167
+ function looksLikeJsonRpc(body) {
168
+ try {
169
+ const parsed = JSON.parse(body);
170
+ return parsed.jsonrpc === '2.0';
171
+ }
172
+ catch {
173
+ return false;
174
+ }
175
+ }
176
+ //# sourceMappingURL=stdio.js.map
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Catalog subscriptions (P26-09, P26-10).
3
+ *
4
+ * The rules that make this more than an event emitter, each of which a naive
5
+ * implementation gets wrong:
6
+ *
7
+ * - **Acknowledge first, and acknowledge only what is SUPPORTED.** The ack carries
8
+ * the filter the server will actually honour, not the one the client asked for. A
9
+ * client that requested four notification types and is sent back one is being told
10
+ * the truth; echoing all four and then sending one is a silent lie the client
11
+ * cannot detect until it waits forever.
12
+ * - **No event before the acknowledgement.** So the ack is emitted synchronously as
13
+ * part of establishing the subscription, and the subscription only becomes
14
+ * eligible for real events afterwards.
15
+ * - **Every event carries the subscription id** in the revision's notification
16
+ * metadata, because a client with several subscriptions cannot otherwise tell
17
+ * which one an event belongs to.
18
+ * - **Filter per caller.** Each subscription belongs to one client and sees only its
19
+ * own catalog. One client's private package changes are never delivered to
20
+ * another, which is the leakage the requirement names.
21
+ * - **Teardown is announced.** Revoking a grant sends `notifications/cancelled`
22
+ * referencing the subscription request BEFORE the stream closes, so a client can
23
+ * distinguish "your access ended" from "the connection dropped".
24
+ * - **Progress does not travel here** (P26-10). Progress belongs on the response
25
+ * stream of the call that produced it; routing it onto the catalog stream would
26
+ * deliver one call's progress to every subscriber.
27
+ *
28
+ * @module @try-works/dsh-model-as-tool/mcp/subscriptions
29
+ */
30
+ import type { WireRequest } from './protocol.ts';
31
+ /** The notification types the revision defines for catalog changes. */
32
+ export type SubscriptionFilterKey = 'toolsListChanged' | 'promptsListChanged' | 'resourcesListChanged' | 'resourceSubscriptions';
33
+ /** The filter a client requested, or the subset the server acknowledged. */
34
+ export type SubscriptionFilter = Partial<Record<SubscriptionFilterKey, boolean>> & {
35
+ readonly resourceSubscriptions?: readonly string[];
36
+ };
37
+ /**
38
+ * The notification types this server actually emits.
39
+ *
40
+ * Only tool-catalog changes exist, because only tools are published. Declaring the
41
+ * others unsupported is what makes the acknowledgement honest.
42
+ */
43
+ export declare const SUPPORTED_FILTERS: readonly SubscriptionFilterKey[];
44
+ /** The official metadata key carrying the subscription's request id. */
45
+ export declare const SUBSCRIPTION_ID_KEY = "io.modelcontextprotocol/subscriptionId";
46
+ /** One active subscription. */
47
+ export interface Subscription {
48
+ /** The id of the `subscriptions/listen` request that established it. */
49
+ readonly subscriptionId: string;
50
+ /** The client it belongs to. Every event is filtered through this. */
51
+ readonly clientId: string;
52
+ /** The filter the server ACKNOWLEDGED, which is the supported subset. */
53
+ readonly acknowledged: SubscriptionFilter;
54
+ /**
55
+ * Whether the acknowledgement has been emitted.
56
+ *
57
+ * Nothing is published before it is true, which is how P26-09 requirement that no
58
+ * event precede the acknowledgement is enforced rather than remembered.
59
+ */
60
+ announced: boolean;
61
+ }
62
+ /** A message the transport should deliver to one client. */
63
+ export interface SubscriptionMessage {
64
+ /** The client it is for. */
65
+ readonly clientId: string;
66
+ /** The notification to send. */
67
+ readonly notification: Record<string, unknown>;
68
+ }
69
+ /**
70
+ * Narrow a requested filter to what this server supports.
71
+ *
72
+ * P26-09 says to acknowledge only supported requested filters. A requested type
73
+ * that is not supported is DROPPED rather than acknowledged-and-ignored: an
74
+ * acknowledged type that never fires is indistinguishable from a broken server.
75
+ * @param requested - the filter the client asked for.
76
+ * @returns the acknowledged filter.
77
+ */
78
+ export declare function acknowledgeFilter(requested: SubscriptionFilter): SubscriptionFilter;
79
+ /** Manages the active catalog subscriptions. */
80
+ export declare class SubscriptionRegistry {
81
+ #private;
82
+ /** Active subscriptions, for diagnostics. */
83
+ get active(): readonly Subscription[];
84
+ /**
85
+ * Establish a subscription.
86
+ *
87
+ * The acknowledgement notification is returned as part of the result and is
88
+ * marked sent before anything can be published, so no event can precede it.
89
+ * @param request - the `subscriptions/listen` request.
90
+ * @param clientId - the authenticated client, which the subscription is bound to.
91
+ * @returns the result envelope, the acknowledgement, and the subscription id.
92
+ */
93
+ listen(request: WireRequest, clientId: string): {
94
+ readonly result: Record<string, unknown>;
95
+ readonly acknowledgement: Record<string, unknown>;
96
+ readonly subscriptionId: string;
97
+ };
98
+ /**
99
+ * Publish a tool-catalog change.
100
+ *
101
+ * @param generation - the catalog generation that changed, carried so a client
102
+ * can tell a real change from a duplicate delivery.
103
+ * @returns one message per eligible subscriber, and nothing for anyone else.
104
+ */
105
+ publishToolListChanged(generation: number): readonly SubscriptionMessage[];
106
+ /**
107
+ * Revoke every subscription belonging to a client, announcing each first.
108
+ *
109
+ * P26-09 requires the teardown notification BEFORE the stream closes, so the
110
+ * client learns its access ended rather than inferring it from a dropped socket.
111
+ * @param clientId - the client whose grant was revoked.
112
+ * @returns the teardown messages, already ordered cancellation-first.
113
+ */
114
+ revoke(clientId: string): readonly SubscriptionMessage[];
115
+ /**
116
+ * Close one subscription at the client's request.
117
+ * @param subscriptionId - the subscription to close.
118
+ * @param clientId - the caller, which must own it.
119
+ * @returns whether it was closed.
120
+ */
121
+ close(subscriptionId: string, clientId: string): boolean;
122
+ /** Every subscription belonging to one client. */
123
+ forClient(clientId: string): readonly Subscription[];
124
+ }
125
+ /**
126
+ * Build the `notifications/cancelled` a client sends to stop a subscription.
127
+ *
128
+ * `CancelledNotificationParams` requires only `requestId`, so the notification
129
+ * carries exactly that: adding a `_meta` with an undefined version would put a
130
+ * meaningless member on the wire.
131
+ * @param subscriptionId - the subscription to cancel.
132
+ * @returns the notification.
133
+ */
134
+ export declare function cancelSubscription(subscriptionId: string): Record<string, unknown>;
135
+ //# sourceMappingURL=subscriptions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subscriptions.d.ts","sourceRoot":"","sources":["../../src/mcp/subscriptions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAEhD,uEAAuE;AACvE,MAAM,MAAM,qBAAqB,GAC7B,kBAAkB,GAClB,oBAAoB,GACpB,sBAAsB,GACtB,uBAAuB,CAAA;AAE3B,4EAA4E;AAC5E,MAAM,MAAM,kBAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,qBAAqB,EAAE,OAAO,CAAC,CAAC,GAAG;IACjF,QAAQ,CAAC,qBAAqB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CACnD,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,EAAE,SAAS,qBAAqB,EAAyB,CAAA;AAEvF,wEAAwE;AACxE,eAAO,MAAM,mBAAmB,2CAA2C,CAAA;AAE3E,+BAA+B;AAC/B,MAAM,WAAW,YAAY;IAC3B,wEAAwE;IACxE,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;IAC/B,sEAAsE;IACtE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,yEAAyE;IACzE,QAAQ,CAAC,YAAY,EAAE,kBAAkB,CAAA;IACzC;;;;;OAKG;IACH,SAAS,EAAE,OAAO,CAAA;CACnB;AAED,4DAA4D;AAC5D,MAAM,WAAW,mBAAmB;IAClC,4BAA4B;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,gCAAgC;IAChC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC/C;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,kBAAkB,GAAG,kBAAkB,CAQnF;AAED,gDAAgD;AAChD,qBAAa,oBAAoB;;IAK/B,6CAA6C;IAC7C,IAAI,MAAM,IAAI,SAAS,YAAY,EAAE,CAEpC;IAED;;;;;;;;OAQG;IACH,MAAM,CAAC,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,GAAG;QAC9C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;QACxC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;QACjD,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;KAChC;IA+BD;;;;;;OAMG;IACH,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,mBAAmB,EAAE;IA0B1E;;;;;;;OAOG;IACH,MAAM,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,mBAAmB,EAAE;IAyBxD;;;;;OAKG;IACH,KAAK,CAAC,cAAc,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO;IASxD,kDAAkD;IAClD,SAAS,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,YAAY,EAAE;CAGrD;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAMlF"}