@try-works/dsh-model-as-tool 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -0
- package/bin/dsh-tool-mode-mcp-stdio.mjs +80 -0
- package/catalog/desert-ant-models.json +77 -13
- package/lib/adapters/index.d.ts +19 -6
- package/lib/adapters/index.d.ts.map +1 -1
- package/lib/adapters/index.js +28 -13
- package/lib/adapters/tongue.d.ts +11 -3
- package/lib/adapters/tongue.d.ts.map +1 -1
- package/lib/adapters/tongue.js +12 -4
- package/lib/adapters/types.d.ts +11 -0
- package/lib/adapters/types.d.ts.map +1 -1
- package/lib/adapters/voz.d.ts +7 -1
- package/lib/adapters/voz.d.ts.map +1 -1
- package/lib/adapters/voz.js +16 -4
- package/lib/asset-acquisition.d.ts +137 -0
- package/lib/asset-acquisition.d.ts.map +1 -0
- package/lib/asset-acquisition.js +161 -0
- package/lib/authority.d.ts +153 -0
- package/lib/authority.d.ts.map +1 -0
- package/lib/authority.js +194 -0
- package/lib/bindings.d.ts +115 -0
- package/lib/bindings.d.ts.map +1 -0
- package/lib/bindings.js +239 -0
- package/lib/catalog.d.ts.map +1 -1
- package/lib/catalog.js +57 -0
- package/lib/contract-payload.d.ts +47 -0
- package/lib/contract-payload.d.ts.map +1 -0
- package/lib/contract-payload.js +137 -0
- package/lib/deployment-coordinator.d.ts +213 -0
- package/lib/deployment-coordinator.d.ts.map +1 -0
- package/lib/deployment-coordinator.js +274 -0
- package/lib/harness-budgets.d.ts +158 -0
- package/lib/harness-budgets.d.ts.map +1 -0
- package/lib/harness-budgets.js +152 -0
- package/lib/index.d.ts +41 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +424 -16
- package/lib/licensing.d.ts +221 -0
- package/lib/licensing.d.ts.map +1 -0
- package/lib/licensing.js +188 -0
- package/lib/mcp/host-binding.d.ts +91 -0
- package/lib/mcp/host-binding.d.ts.map +1 -0
- package/lib/mcp/host-binding.js +133 -0
- package/lib/mcp/http.d.ts +130 -0
- package/lib/mcp/http.d.ts.map +1 -0
- package/lib/mcp/http.js +274 -0
- package/lib/mcp/protocol.d.ts +261 -0
- package/lib/mcp/protocol.d.ts.map +1 -0
- package/lib/mcp/protocol.js +387 -0
- package/lib/mcp/server.d.ts +115 -0
- package/lib/mcp/server.d.ts.map +1 -0
- package/lib/mcp/server.js +200 -0
- package/lib/mcp/stdio.d.ts +80 -0
- package/lib/mcp/stdio.d.ts.map +1 -0
- package/lib/mcp/stdio.js +176 -0
- package/lib/mcp/subscriptions.d.ts +135 -0
- package/lib/mcp/subscriptions.d.ts.map +1 -0
- package/lib/mcp/subscriptions.js +207 -0
- package/lib/provider-bridge.d.ts +69 -0
- package/lib/provider-bridge.d.ts.map +1 -0
- package/lib/provider-bridge.js +218 -0
- package/lib/publication.d.ts +160 -0
- package/lib/publication.d.ts.map +1 -0
- package/lib/publication.js +173 -0
- package/lib/receipt.d.ts +208 -0
- package/lib/receipt.d.ts.map +1 -0
- package/lib/receipt.js +150 -0
- package/lib/registrar.d.ts +13 -0
- package/lib/registrar.d.ts.map +1 -1
- package/lib/registrar.js +20 -3
- package/lib/resource-governance.d.ts +204 -0
- package/lib/resource-governance.d.ts.map +1 -0
- package/lib/resource-governance.js +213 -0
- package/lib/server-lifecycle.d.ts +175 -0
- package/lib/server-lifecycle.d.ts.map +1 -0
- package/lib/server-lifecycle.js +269 -0
- package/lib/types.d.ts +21 -0
- package/lib/types.d.ts.map +1 -1
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/lang-detect.d.ts +16 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/lang-detect.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/lang-detect.js +85 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/stt-transcribe.d.ts +20 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/stt-transcribe.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/contracts/stt-transcribe.js +112 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/dsh.d.ts +82 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/dsh.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/dsh.js +260 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/index.d.ts +23 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/index.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/index.js +24 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/meta.d.ts +104 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/meta.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/meta.js +55 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/registry.d.ts +81 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/registry.d.ts.map +1 -0
- package/node_modules/@try-works/tool-mode-contracts/lib/registry.js +163 -0
- package/node_modules/@try-works/tool-mode-contracts/package.json +35 -0
- package/package.json +17 -5
|
@@ -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"}
|
package/lib/mcp/stdio.js
ADDED
|
@@ -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"}
|