@modelcontextprotocol/server 2.0.0-alpha.3 → 2.0.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/{ajvProvider-Birb50r-.mjs → ajvProvider-BQMcjynJ.mjs} +952 -154
- package/dist/ajvProvider-BQMcjynJ.mjs.map +1 -0
- package/dist/{ajvProvider-DZ_siXcF.d.mts → ajvProvider-Dzgk80kq.d.mts} +58 -11
- package/dist/ajvProvider-Dzgk80kq.d.mts.map +1 -0
- package/dist/{cfWorkerProvider-BrJKpSFH.mjs → cfWorkerProvider-BDC2rVl3.mjs} +21 -5
- package/dist/cfWorkerProvider-BDC2rVl3.mjs.map +1 -0
- package/dist/{cfWorkerProvider-DUhk5Ewx.d.mts → cfWorkerProvider-DmvjVsvQ.d.mts} +13 -6
- package/dist/cfWorkerProvider-DmvjVsvQ.d.mts.map +1 -0
- package/dist/{transport-DMKhEchd.d.mts → createMcpHandler-Du3hjXvf.d.mts} +5283 -1559
- package/dist/createMcpHandler-Du3hjXvf.d.mts.map +1 -0
- package/dist/index.d.mts +167 -2015
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1238 -1281
- package/dist/index.mjs.map +1 -1
- package/dist/mcp-JttQJlI9.mjs +9998 -0
- package/dist/mcp-JttQJlI9.mjs.map +1 -0
- package/dist/shimsNode.d.mts +1 -1
- package/dist/shimsNode.mjs +1 -1
- package/dist/shimsWorkerd.d.mts +1 -1
- package/dist/shimsWorkerd.mjs +1 -1
- package/dist/stdio.d.mts +61 -3
- package/dist/stdio.d.mts.map +1 -1
- package/dist/stdio.mjs +457 -2
- package/dist/stdio.mjs.map +1 -1
- package/dist/types-DBYdVs-n.d.mts +1099 -0
- package/dist/types-DBYdVs-n.d.mts.map +1 -0
- package/dist/validators/ajv.d.mts +1 -1
- package/dist/validators/ajv.mjs +1 -1
- package/dist/validators/cfWorker.d.mts +1 -1
- package/dist/validators/cfWorker.mjs +1 -1
- package/package.json +3 -6
- package/dist/ajvProvider-Birb50r-.mjs.map +0 -1
- package/dist/ajvProvider-DZ_siXcF.d.mts.map +0 -1
- package/dist/cfWorkerProvider-BrJKpSFH.mjs.map +0 -1
- package/dist/cfWorkerProvider-DUhk5Ewx.d.mts.map +0 -1
- package/dist/src-Pa1iAvsj.mjs +0 -3386
- package/dist/src-Pa1iAvsj.mjs.map +0 -1
- package/dist/transport-DMKhEchd.d.mts.map +0 -1
- package/dist/types-R2RTIcjk.d.mts +0 -66
- package/dist/types-R2RTIcjk.d.mts.map +0 -1
package/dist/shimsNode.d.mts
CHANGED
package/dist/shimsNode.mjs
CHANGED
package/dist/shimsWorkerd.d.mts
CHANGED
package/dist/shimsWorkerd.mjs
CHANGED
package/dist/stdio.d.mts
CHANGED
|
@@ -1,8 +1,66 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { cr as JSONRPCMessage, o as McpServerFactory, rt as Transport } from "./createMcpHandler-Du3hjXvf.mjs";
|
|
2
2
|
import { Readable, Writable } from "node:stream";
|
|
3
3
|
|
|
4
|
-
//#region src/server/
|
|
4
|
+
//#region src/server/serveStdio.d.ts
|
|
5
5
|
|
|
6
|
+
/** Options for {@linkcode serveStdio}. */
|
|
7
|
+
interface ServeStdioOptions {
|
|
8
|
+
/**
|
|
9
|
+
* How a 2025-era opening (an `initialize` request, or any claim-less
|
|
10
|
+
* message) is handled:
|
|
11
|
+
*
|
|
12
|
+
* - `'serve'` (default) — the connection is pinned to a 2025-era instance
|
|
13
|
+
* from the same factory and served exactly as a hand-wired stdio server
|
|
14
|
+
* serves it today.
|
|
15
|
+
* - `'reject'` — the opening request is answered with the
|
|
16
|
+
* unsupported-protocol-version error naming the supported modern
|
|
17
|
+
* revisions (claim-less notifications are dropped); the connection
|
|
18
|
+
* stays open for a modern opening.
|
|
19
|
+
*/
|
|
20
|
+
legacy?: 'serve' | 'reject';
|
|
21
|
+
/**
|
|
22
|
+
* Bring your own transport (for example a `StdioServerTransport`
|
|
23
|
+
* constructed over a Unix domain socket or TCP stream, per the stdio
|
|
24
|
+
* binding's custom-transport guidance). Defaults to a
|
|
25
|
+
* {@linkcode StdioServerTransport} over the current process's stdio. The
|
|
26
|
+
* entry owns the transport: it starts it, receives every inbound message,
|
|
27
|
+
* and closes it when the connection ends.
|
|
28
|
+
*/
|
|
29
|
+
transport?: Transport;
|
|
30
|
+
/** Callback for out-of-band errors (reporting only; it never alters what is written to the wire). */
|
|
31
|
+
onerror?: (error: Error) => void;
|
|
32
|
+
/**
|
|
33
|
+
* Reject a new `subscriptions/listen` with `-32603` 'Subscription limit
|
|
34
|
+
* reached' (in-band, before the ack) when this many subscriptions are
|
|
35
|
+
* already open on this connection.
|
|
36
|
+
* @default 1024
|
|
37
|
+
*/
|
|
38
|
+
maxSubscriptions?: number;
|
|
39
|
+
}
|
|
40
|
+
/** The handle returned by {@linkcode serveStdio}. */
|
|
41
|
+
interface StdioServerHandle {
|
|
42
|
+
/** Tears the connection down: closes the pinned instance (if any) and the underlying transport. */
|
|
43
|
+
close(): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Serves MCP over stdio from a server factory, owning the era decision for
|
|
47
|
+
* the connection: the opening exchange selects the era, ONE instance from the
|
|
48
|
+
* factory is pinned for the connection lifetime, and everything after passes
|
|
49
|
+
* straight through to it. See the module documentation for the opening rules.
|
|
50
|
+
*
|
|
51
|
+
* ```ts
|
|
52
|
+
* import { serveStdio } from '@modelcontextprotocol/server/stdio';
|
|
53
|
+
*
|
|
54
|
+
* serveStdio(() => {
|
|
55
|
+
* const server = new McpServer({ name: 'my-server', version: '1.0.0' }, { capabilities: { tools: {} } });
|
|
56
|
+
* // register tools/resources/prompts once — the same factory serves both eras
|
|
57
|
+
* return server;
|
|
58
|
+
* });
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
declare function serveStdio(factory: McpServerFactory, options?: ServeStdioOptions): StdioServerHandle;
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region src/server/stdio.d.ts
|
|
6
64
|
/**
|
|
7
65
|
* Server transport for stdio: this communicates with an MCP client by reading from the current process' `stdin` and writing to `stdout`.
|
|
8
66
|
*
|
|
@@ -45,5 +103,5 @@ declare class StdioServerTransport implements Transport {
|
|
|
45
103
|
send(message: JSONRPCMessage): Promise<void>;
|
|
46
104
|
}
|
|
47
105
|
//#endregion
|
|
48
|
-
export { StdioServerTransport };
|
|
106
|
+
export { type ServeStdioOptions, type StdioServerHandle, StdioServerTransport, serveStdio };
|
|
49
107
|
//# sourceMappingURL=stdio.d.mts.map
|
package/dist/stdio.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stdio.d.mts","names":[],"sources":["../src/server/stdio.ts"],"sourcesContent":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"stdio.d.mts","names":[],"sources":["../src/server/serveStdio.ts","../src/server/stdio.ts"],"sourcesContent":[],"mappings":";;;;;;UAoFiB,iBAAA;;;;;;;;;;;;;;;;;;;;;;cAsBD;;oBAEM;;;;;;;;;;UAWL,iBAAA;;WAEJ;;;;;;;;;;;;;;;;;;iBA6PG,UAAA,UAAoB,4BAA2B,oBAAyB;;;;;AAlSxF;AAmCA;AA+PA;;;;;;;;ACpWa,cAAA,oBAAA,YAAgC,SAAX,CAAA;EAMV,QAAA,MAAA;EACC,QAAA,OAAA;EAeH,QAAA,WAAA;EACI,QAAA,QAAA;EAGJ,QAAA,OAAA;EASC,WAAA,CAAA,MAAA,CAAA,EA7BC,QA6BD,EAAA,OAAA,CAAA,EA5BE,QA4BF,EAAA,QAAA,EAAA;IAGM;;;;;;IAtCyB,aAAA,CAAA,EAAA,MAAA;;;oBAsBhC;wBACI;mBAGJ;oBASC;0BAGM;;;;WAUV;;WA4BA;gBAwBD,iBAAiB"}
|
package/dist/stdio.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as ReadBuffer,
|
|
1
|
+
import { $ as validateEnvelopeMeta, H as isJSONRPCErrorResponse, K as isJSONRPCResultResponse, M as modernOnlyStrictRejection, Q as requestMetaOf, S as setNegotiatedProtocolVersion, U as isJSONRPCNotification, W as isJSONRPCRequest, X as envelopeClaimVersion, Z as hasEnvelopeClaim, at as ProtocolErrorCode, c as StdioListenRouter, g as ReadBuffer, i as installModernOnlyHandlers, k as carriesValidModernEnvelopeClaim, ot as SUPPORTED_MODERN_PROTOCOL_VERSIONS, rt as UnsupportedProtocolVersionError, s as DEFAULT_MAX_SUBSCRIPTIONS, t as McpServer, y as serializeMessage } from "./mcp-JttQJlI9.mjs";
|
|
2
2
|
import { process } from "@modelcontextprotocol/server/_shims";
|
|
3
3
|
|
|
4
4
|
//#region src/server/stdio.ts
|
|
@@ -102,5 +102,460 @@ var StdioServerTransport = class {
|
|
|
102
102
|
};
|
|
103
103
|
|
|
104
104
|
//#endregion
|
|
105
|
-
|
|
105
|
+
//#region src/server/serveStdio.ts
|
|
106
|
+
/**
|
|
107
|
+
* How long the probe-discard path waits for the probe instance to answer the
|
|
108
|
+
* requests it was delivered before closing it. The wait normally settles as
|
|
109
|
+
* soon as the DiscoverResult is handed to the wire (or immediately, when a
|
|
110
|
+
* delivered cancellation already settled the probe); the bound is a backstop
|
|
111
|
+
* so no edge can ever hold the connection's inbound pump indefinitely behind
|
|
112
|
+
* the discard.
|
|
113
|
+
*/
|
|
114
|
+
const DISCARD_ANSWER_TIMEOUT_MS = 3e3;
|
|
115
|
+
/**
|
|
116
|
+
* The transport a pinned instance is connected to: a thin channel that writes
|
|
117
|
+
* through to the entry-owned wire transport and receives the messages the
|
|
118
|
+
* entry forwards. The wire transport itself is never handed to an instance —
|
|
119
|
+
* that is what lets the entry discard an optimistic probe instance (close the
|
|
120
|
+
* channel) without tearing down the connection.
|
|
121
|
+
*/
|
|
122
|
+
var StdioConnectionChannel = class {
|
|
123
|
+
onclose;
|
|
124
|
+
onerror;
|
|
125
|
+
onmessage;
|
|
126
|
+
_closed = false;
|
|
127
|
+
/** Request ids the entry delivered to the instance that the instance has not yet answered. */
|
|
128
|
+
_pendingRequests = /* @__PURE__ */ new Set();
|
|
129
|
+
_drainWaiters = [];
|
|
130
|
+
constructor(_wire, _onInstanceClose, _outboundIntercept) {
|
|
131
|
+
this._wire = _wire;
|
|
132
|
+
this._onInstanceClose = _onInstanceClose;
|
|
133
|
+
this._outboundIntercept = _outboundIntercept;
|
|
134
|
+
}
|
|
135
|
+
async start() {}
|
|
136
|
+
async send(message, options) {
|
|
137
|
+
if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) {
|
|
138
|
+
const { id } = message;
|
|
139
|
+
if (id !== void 0) this._settle(id);
|
|
140
|
+
}
|
|
141
|
+
if (this._closed) return;
|
|
142
|
+
if (this._outboundIntercept?.(message) === "handled") return;
|
|
143
|
+
return this._wire.send(message, options);
|
|
144
|
+
}
|
|
145
|
+
setProtocolVersion = (version) => {
|
|
146
|
+
this._wire.setProtocolVersion?.(version);
|
|
147
|
+
};
|
|
148
|
+
/** Forwards one inbound message to the connected instance. */
|
|
149
|
+
deliver(message, extra) {
|
|
150
|
+
if (this._closed) return;
|
|
151
|
+
if (isJSONRPCRequest(message)) this._pendingRequests.add(message.id);
|
|
152
|
+
else if (isJSONRPCNotification(message) && message.method === "notifications/cancelled") {
|
|
153
|
+
const cancelledId = message.params?.requestId;
|
|
154
|
+
if (cancelledId !== void 0) this._settle(cancelledId);
|
|
155
|
+
}
|
|
156
|
+
this.onmessage?.(message, extra);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Resolves once every request delivered to the instance has been answered
|
|
160
|
+
* through {@linkcode send}, settled by a delivered cancellation, or the
|
|
161
|
+
* channel has been closed and nothing further can be answered. The wait is
|
|
162
|
+
* bounded by `timeoutMs` as a backstop so no edge can hold the caller
|
|
163
|
+
* indefinitely; resolves `false` only when the bound elapsed with requests
|
|
164
|
+
* still unanswered. Used by the probe-discard path so a probe request the
|
|
165
|
+
* entry accepted is never silently dropped.
|
|
166
|
+
*/
|
|
167
|
+
async whenRequestsAnswered(timeoutMs) {
|
|
168
|
+
if (this._closed || this._pendingRequests.size === 0) return true;
|
|
169
|
+
return await new Promise((resolve) => {
|
|
170
|
+
const waiter = () => {
|
|
171
|
+
clearTimeout(timer);
|
|
172
|
+
resolve(true);
|
|
173
|
+
};
|
|
174
|
+
const timer = setTimeout(() => {
|
|
175
|
+
this._drainWaiters = this._drainWaiters.filter((pending) => pending !== waiter);
|
|
176
|
+
resolve(false);
|
|
177
|
+
}, timeoutMs);
|
|
178
|
+
this._drainWaiters.push(waiter);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
async close() {
|
|
182
|
+
if (this._closed) return;
|
|
183
|
+
this._closed = true;
|
|
184
|
+
this._pendingRequests.clear();
|
|
185
|
+
this._releaseDrainWaiters();
|
|
186
|
+
try {
|
|
187
|
+
this._onInstanceClose();
|
|
188
|
+
} finally {
|
|
189
|
+
this.onclose?.();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
_settle(id) {
|
|
193
|
+
this._pendingRequests.delete(id);
|
|
194
|
+
if (this._pendingRequests.size === 0) this._releaseDrainWaiters();
|
|
195
|
+
}
|
|
196
|
+
_releaseDrainWaiters() {
|
|
197
|
+
const waiters = this._drainWaiters;
|
|
198
|
+
this._drainWaiters = [];
|
|
199
|
+
for (const waiter of waiters) waiter();
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
/**
|
|
203
|
+
* Classifies one message of the opening exchange with the same body-primary
|
|
204
|
+
* rules the HTTP entry applies per request: `initialize` is the legacy
|
|
205
|
+
* handshake unless it carries a valid modern envelope claim; a present claim
|
|
206
|
+
* is validated (never silently ignored); a claim-less message is 2025-era
|
|
207
|
+
* traffic. There is no header layer on stdio, so the body is the only signal.
|
|
208
|
+
*/
|
|
209
|
+
function classifyOpeningMessage(message) {
|
|
210
|
+
const params = message.params;
|
|
211
|
+
if (message.method === "initialize" && !carriesValidModernEnvelopeClaim(params)) {
|
|
212
|
+
const requestedVersion = params !== null && typeof params === "object" && typeof params.protocolVersion === "string" ? params.protocolVersion : void 0;
|
|
213
|
+
return {
|
|
214
|
+
kind: "legacy",
|
|
215
|
+
reason: "initialize",
|
|
216
|
+
...requestedVersion !== void 0 && { requestedVersion }
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
if (!hasEnvelopeClaim(params)) return {
|
|
220
|
+
kind: "legacy",
|
|
221
|
+
reason: "no-claim"
|
|
222
|
+
};
|
|
223
|
+
const meta = requestMetaOf(params);
|
|
224
|
+
const firstIssue = (meta === void 0 ? [] : validateEnvelopeMeta(meta))[0];
|
|
225
|
+
if (firstIssue !== void 0) return {
|
|
226
|
+
kind: "invalid-envelope",
|
|
227
|
+
issue: firstIssue
|
|
228
|
+
};
|
|
229
|
+
const claimedVersion = envelopeClaimVersion(params);
|
|
230
|
+
if (claimedVersion === void 0 || !SUPPORTED_MODERN_PROTOCOL_VERSIONS.includes(claimedVersion)) return {
|
|
231
|
+
kind: "unsupported-revision",
|
|
232
|
+
requested: claimedVersion ?? "unknown"
|
|
233
|
+
};
|
|
234
|
+
return {
|
|
235
|
+
kind: "modern",
|
|
236
|
+
revision: claimedVersion,
|
|
237
|
+
classification: {
|
|
238
|
+
era: "modern",
|
|
239
|
+
revision: claimedVersion
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Serves MCP over stdio from a server factory, owning the era decision for
|
|
245
|
+
* the connection: the opening exchange selects the era, ONE instance from the
|
|
246
|
+
* factory is pinned for the connection lifetime, and everything after passes
|
|
247
|
+
* straight through to it. See the module documentation for the opening rules.
|
|
248
|
+
*
|
|
249
|
+
* ```ts
|
|
250
|
+
* import { serveStdio } from '@modelcontextprotocol/server/stdio';
|
|
251
|
+
*
|
|
252
|
+
* serveStdio(() => {
|
|
253
|
+
* const server = new McpServer({ name: 'my-server', version: '1.0.0' }, { capabilities: { tools: {} } });
|
|
254
|
+
* // register tools/resources/prompts once — the same factory serves both eras
|
|
255
|
+
* return server;
|
|
256
|
+
* });
|
|
257
|
+
* ```
|
|
258
|
+
*/
|
|
259
|
+
function serveStdio(factory, options = {}) {
|
|
260
|
+
const legacyMode = options.legacy ?? "serve";
|
|
261
|
+
const wire = options.transport ?? new StdioServerTransport();
|
|
262
|
+
let state = { phase: "opening" };
|
|
263
|
+
/** Channel currently being discarded (its close must not tear the connection down). */
|
|
264
|
+
let discarding;
|
|
265
|
+
let closing = false;
|
|
266
|
+
/**
|
|
267
|
+
* Whether the connection has been torn down (`handle.close()` or the wire
|
|
268
|
+
* closing). The opening arms re-check this after every await: a close can
|
|
269
|
+
* race factory construction, and the continuation must neither resurrect
|
|
270
|
+
* the connection state nor keep a late-resolved instance around.
|
|
271
|
+
*/
|
|
272
|
+
const isTornDown = () => closing || state.phase === "closed";
|
|
273
|
+
const reportError = (error) => {
|
|
274
|
+
try {
|
|
275
|
+
options.onerror?.(error);
|
|
276
|
+
} catch {}
|
|
277
|
+
};
|
|
278
|
+
const writeErrorResponse = (id, code, message, data) => wire.send({
|
|
279
|
+
jsonrpc: "2.0",
|
|
280
|
+
id,
|
|
281
|
+
error: {
|
|
282
|
+
code,
|
|
283
|
+
message,
|
|
284
|
+
...data !== void 0 && { data }
|
|
285
|
+
}
|
|
286
|
+
}).catch((error) => reportError(toError(error)));
|
|
287
|
+
/**
|
|
288
|
+
* Entry-handled `subscriptions/listen` for this connection: holds the
|
|
289
|
+
* active subscriptions, serves inbound listen / cancelled-of-listen
|
|
290
|
+
* before the pinned instance is consulted, and rewrites the instance's
|
|
291
|
+
* outbound change notifications onto the active subscriptions. Only
|
|
292
|
+
* consulted on a modern-pinned connection — on a legacy connection
|
|
293
|
+
* change notifications pass straight through (the 2025 unsolicited
|
|
294
|
+
* delivery model is unchanged).
|
|
295
|
+
*/
|
|
296
|
+
const listenRouter = new StdioListenRouter(options.maxSubscriptions ?? DEFAULT_MAX_SUBSCRIPTIONS);
|
|
297
|
+
/** Outbound intercept installed on a modern instance's channel. */
|
|
298
|
+
const modernOutboundIntercept = (message) => {
|
|
299
|
+
if (!isJSONRPCNotification(message)) return void 0;
|
|
300
|
+
const routed = listenRouter.routeOutbound(message);
|
|
301
|
+
if (routed === "passthrough") return void 0;
|
|
302
|
+
for (const stamped of routed) wire.send({
|
|
303
|
+
jsonrpc: "2.0",
|
|
304
|
+
...stamped
|
|
305
|
+
}).catch((error) => reportError(toError(error)));
|
|
306
|
+
return "handled";
|
|
307
|
+
};
|
|
308
|
+
/**
|
|
309
|
+
* Entry-handled inbound listen routing for a modern-pinned connection.
|
|
310
|
+
* Returns `true` when the message was served at the entry and must NOT
|
|
311
|
+
* be delivered to the pinned instance.
|
|
312
|
+
*/
|
|
313
|
+
const tryServeListen = async (message) => {
|
|
314
|
+
if (isJSONRPCRequest(message) && message.method === "subscriptions/listen") {
|
|
315
|
+
const meta = requestMetaOf(message.params);
|
|
316
|
+
const issue = hasEnvelopeClaim(message.params) ? (meta === void 0 ? [] : validateEnvelopeMeta(meta))[0] : {
|
|
317
|
+
key: "_meta",
|
|
318
|
+
problem: "the per-request envelope is required on protocol revision 2026-07-28"
|
|
319
|
+
};
|
|
320
|
+
const claimedVersion = envelopeClaimVersion(message.params);
|
|
321
|
+
let reply;
|
|
322
|
+
if (issue !== void 0) reply = {
|
|
323
|
+
jsonrpc: "2.0",
|
|
324
|
+
id: message.id,
|
|
325
|
+
error: {
|
|
326
|
+
code: -32602,
|
|
327
|
+
message: `Invalid _meta envelope: ${issue.key}: ${issue.problem}`
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
else if (claimedVersion === void 0 || !SUPPORTED_MODERN_PROTOCOL_VERSIONS.includes(claimedVersion)) {
|
|
331
|
+
const error = new UnsupportedProtocolVersionError({
|
|
332
|
+
supported: [...SUPPORTED_MODERN_PROTOCOL_VERSIONS],
|
|
333
|
+
requested: claimedVersion ?? "unknown"
|
|
334
|
+
});
|
|
335
|
+
reply = {
|
|
336
|
+
jsonrpc: "2.0",
|
|
337
|
+
id: message.id,
|
|
338
|
+
error: {
|
|
339
|
+
code: error.code,
|
|
340
|
+
message: error.message,
|
|
341
|
+
data: error.data
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
} else reply = listenRouter.serve(message);
|
|
345
|
+
await wire.send("error" in reply ? reply : {
|
|
346
|
+
jsonrpc: "2.0",
|
|
347
|
+
method: reply.method,
|
|
348
|
+
params: reply.params
|
|
349
|
+
}).catch((error) => reportError(toError(error)));
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
if (isJSONRPCNotification(message) && message.method === "notifications/cancelled") {
|
|
353
|
+
const cancelledId = message.params?.requestId;
|
|
354
|
+
if (cancelledId !== void 0 && listenRouter.cancel(cancelledId)) return true;
|
|
355
|
+
}
|
|
356
|
+
return false;
|
|
357
|
+
};
|
|
358
|
+
/** Answers a 2025-era request the entry will not serve (the modern-only rejection cells). */
|
|
359
|
+
const answerLegacyRejection = (request, reason, requestedVersion) => {
|
|
360
|
+
const rejection = modernOnlyStrictRejection({
|
|
361
|
+
kind: "legacy",
|
|
362
|
+
reason,
|
|
363
|
+
...requestedVersion !== void 0 && { requestedVersion }
|
|
364
|
+
}, SUPPORTED_MODERN_PROTOCOL_VERSIONS);
|
|
365
|
+
if (rejection === void 0) return Promise.resolve();
|
|
366
|
+
reportError(/* @__PURE__ */ new Error(`Rejected 2025-era request on a modern-only stdio connection (${rejection.cell}): ${rejection.message}`));
|
|
367
|
+
return writeErrorResponse(request.id, rejection.code, rejection.message, rejection.data);
|
|
368
|
+
};
|
|
369
|
+
const onInstanceClosed = (channel) => {
|
|
370
|
+
if (closing || channel === discarding) return;
|
|
371
|
+
closeAll();
|
|
372
|
+
};
|
|
373
|
+
const connectInstance = async (era, revision) => {
|
|
374
|
+
const product = await factory({ era });
|
|
375
|
+
const server = product instanceof McpServer ? product.server : product;
|
|
376
|
+
if (era === "modern") {
|
|
377
|
+
setNegotiatedProtocolVersion(server, revision);
|
|
378
|
+
installModernOnlyHandlers(server, SUPPORTED_MODERN_PROTOCOL_VERSIONS);
|
|
379
|
+
listenRouter.setServerCapabilities(server.getCapabilities());
|
|
380
|
+
}
|
|
381
|
+
const channel = new StdioConnectionChannel(wire, () => onInstanceClosed(channel), era === "modern" ? modernOutboundIntercept : void 0);
|
|
382
|
+
await product.connect(channel);
|
|
383
|
+
return {
|
|
384
|
+
product,
|
|
385
|
+
channel
|
|
386
|
+
};
|
|
387
|
+
};
|
|
388
|
+
/** Closes an instance whose factory resolved only after the connection was torn down. */
|
|
389
|
+
const disposeLateInstance = (instance) => instance.product.close().catch((error) => reportError(toError(error)));
|
|
390
|
+
const discardProbeInstance = async (instance) => {
|
|
391
|
+
discarding = instance.channel;
|
|
392
|
+
try {
|
|
393
|
+
if (!await instance.channel.whenRequestsAnswered(DISCARD_ANSWER_TIMEOUT_MS)) reportError(/* @__PURE__ */ new Error(`Discarded the probe instance with requests still unanswered after ${DISCARD_ANSWER_TIMEOUT_MS}ms; continuing with the fallback`));
|
|
394
|
+
await instance.product.close();
|
|
395
|
+
} catch (error) {
|
|
396
|
+
reportError(toError(error));
|
|
397
|
+
} finally {
|
|
398
|
+
discarding = void 0;
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
const processMessage = async (message) => {
|
|
402
|
+
if (state.phase === "closed") return;
|
|
403
|
+
if (state.phase === "pinned") {
|
|
404
|
+
if (state.era === "modern" && isJSONRPCRequest(message) && message.method === "initialize" && !carriesValidModernEnvelopeClaim(message.params)) {
|
|
405
|
+
await answerLegacyRejection(message, "initialize", message.params !== null && typeof message.params === "object" && typeof message.params.protocolVersion === "string" ? message.params.protocolVersion : void 0);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
if (state.era === "modern" && await tryServeListen(message)) return;
|
|
409
|
+
state.instance.channel.deliver(message);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (!isJSONRPCRequest(message) && !isJSONRPCNotification(message)) {
|
|
413
|
+
reportError(/* @__PURE__ */ new Error("Discarded a JSON-RPC response received before the connection negotiated an era"));
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const opening = classifyOpeningMessage(message);
|
|
417
|
+
switch (opening.kind) {
|
|
418
|
+
case "invalid-envelope": {
|
|
419
|
+
const detail = `Invalid _meta envelope for protocol revision 2026-07-28: ${opening.issue.key}: ${opening.issue.problem}`;
|
|
420
|
+
if (isJSONRPCRequest(message)) await writeErrorResponse(message.id, ProtocolErrorCode.InvalidParams, detail, { envelope: opening.issue });
|
|
421
|
+
else reportError(/* @__PURE__ */ new Error(`Discarded a notification with a malformed envelope: ${detail}`));
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
case "unsupported-revision":
|
|
425
|
+
if (isJSONRPCRequest(message)) {
|
|
426
|
+
const error = new UnsupportedProtocolVersionError({
|
|
427
|
+
supported: [...SUPPORTED_MODERN_PROTOCOL_VERSIONS],
|
|
428
|
+
requested: opening.requested
|
|
429
|
+
});
|
|
430
|
+
reportError(error);
|
|
431
|
+
await writeErrorResponse(message.id, error.code, error.message, error.data);
|
|
432
|
+
} else reportError(/* @__PURE__ */ new Error(`Discarded a notification claiming unsupported protocol revision ${opening.requested}`));
|
|
433
|
+
return;
|
|
434
|
+
case "modern":
|
|
435
|
+
if (isJSONRPCRequest(message) && message.method === "server/discover") {
|
|
436
|
+
if (state.phase === "probe") {
|
|
437
|
+
state.instance.channel.deliver(message, { classification: opening.classification });
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
const instance = await connectInstance("modern", opening.revision);
|
|
441
|
+
if (isTornDown()) {
|
|
442
|
+
await disposeLateInstance(instance);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
state = {
|
|
446
|
+
phase: "probe",
|
|
447
|
+
instance
|
|
448
|
+
};
|
|
449
|
+
instance.channel.deliver(message, { classification: opening.classification });
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if (state.phase === "probe") {
|
|
453
|
+
if (isJSONRPCNotification(message)) {
|
|
454
|
+
state.instance.channel.deliver(message, { classification: opening.classification });
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
state = {
|
|
458
|
+
phase: "pinned",
|
|
459
|
+
era: "modern",
|
|
460
|
+
instance: state.instance
|
|
461
|
+
};
|
|
462
|
+
} else {
|
|
463
|
+
const instance = await connectInstance("modern", opening.revision);
|
|
464
|
+
if (isTornDown()) {
|
|
465
|
+
await disposeLateInstance(instance);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
state = {
|
|
469
|
+
phase: "pinned",
|
|
470
|
+
era: "modern",
|
|
471
|
+
instance
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
if (await tryServeListen(message)) return;
|
|
475
|
+
state.instance.channel.deliver(message, { classification: opening.classification });
|
|
476
|
+
return;
|
|
477
|
+
case "legacy": {
|
|
478
|
+
if (legacyMode === "reject") {
|
|
479
|
+
if (isJSONRPCRequest(message)) await answerLegacyRejection(message, opening.reason, opening.requestedVersion);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
if (state.phase === "probe") {
|
|
483
|
+
await discardProbeInstance(state.instance);
|
|
484
|
+
if (isTornDown()) return;
|
|
485
|
+
state = { phase: "opening" };
|
|
486
|
+
}
|
|
487
|
+
const instance = await connectInstance("legacy");
|
|
488
|
+
if (isTornDown()) {
|
|
489
|
+
await disposeLateInstance(instance);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
state = {
|
|
493
|
+
phase: "pinned",
|
|
494
|
+
era: "legacy",
|
|
495
|
+
instance
|
|
496
|
+
};
|
|
497
|
+
state.instance.channel.deliver(message);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
const queue = [];
|
|
503
|
+
let pumping = false;
|
|
504
|
+
const pump = async () => {
|
|
505
|
+
if (pumping) return;
|
|
506
|
+
pumping = true;
|
|
507
|
+
try {
|
|
508
|
+
while (queue.length > 0) {
|
|
509
|
+
const message = queue.shift();
|
|
510
|
+
try {
|
|
511
|
+
await processMessage(message);
|
|
512
|
+
} catch (error) {
|
|
513
|
+
if (isJSONRPCRequest(message)) await writeErrorResponse(message.id, ProtocolErrorCode.InternalError, "Internal server error");
|
|
514
|
+
reportError(toError(error));
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
} finally {
|
|
518
|
+
pumping = false;
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
const closeAll = async () => {
|
|
522
|
+
if (closing || state.phase === "closed") return;
|
|
523
|
+
closing = true;
|
|
524
|
+
const current = state;
|
|
525
|
+
state = { phase: "closed" };
|
|
526
|
+
for (const result of listenRouter.teardownAll()) await wire.send(result).catch((error) => reportError(toError(error)));
|
|
527
|
+
if (current.phase === "probe" || current.phase === "pinned") await current.instance.product.close().catch((error) => reportError(toError(error)));
|
|
528
|
+
await wire.close().catch((error) => reportError(toError(error)));
|
|
529
|
+
};
|
|
530
|
+
wire.onmessage = (message) => {
|
|
531
|
+
queue.push(message);
|
|
532
|
+
pump();
|
|
533
|
+
};
|
|
534
|
+
wire.onerror = (error) => {
|
|
535
|
+
reportError(error);
|
|
536
|
+
if (state.phase === "probe" || state.phase === "pinned") state.instance.channel.onerror?.(error);
|
|
537
|
+
};
|
|
538
|
+
wire.onclose = () => {
|
|
539
|
+
if (closing || state.phase === "closed") return;
|
|
540
|
+
closing = true;
|
|
541
|
+
const current = state;
|
|
542
|
+
state = { phase: "closed" };
|
|
543
|
+
if (current.phase === "probe" || current.phase === "pinned") current.instance.product.close().catch((error) => reportError(toError(error)));
|
|
544
|
+
};
|
|
545
|
+
const started = wire.start().catch((error) => {
|
|
546
|
+
reportError(toError(error));
|
|
547
|
+
throw error;
|
|
548
|
+
});
|
|
549
|
+
started.catch(() => {});
|
|
550
|
+
return { close: async () => {
|
|
551
|
+
await started.catch(() => {});
|
|
552
|
+
await closeAll();
|
|
553
|
+
} };
|
|
554
|
+
}
|
|
555
|
+
function toError(value) {
|
|
556
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
//#endregion
|
|
560
|
+
export { StdioServerTransport, serveStdio };
|
|
106
561
|
//# sourceMappingURL=stdio.mjs.map
|