@assistant-ui/mcp-docs-server 0.3.0 → 0.3.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.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { runProxy } from "./proxy.js";
2
2
  //#region src/index.d.ts
3
- declare function runServer(): Promise<void>;
3
+ export declare function runServer(): Promise<void>;
4
4
  //#endregion
5
- export { runProxy, runServer };
5
+ export { runProxy };
6
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;iBAIsB,aAAS"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;wBAIsB,aAAS"}
package/dist/proxy.d.ts CHANGED
@@ -1,9 +1,8 @@
1
1
  //#region src/proxy.d.ts
2
- declare function runProxy({ url, stdin, stdout }?: {
2
+ export declare function runProxy({ url, stdin, stdout }?: {
3
3
  url?: URL;
4
4
  stdin?: NodeJS.ReadableStream;
5
5
  stdout?: NodeJS.WritableStream;
6
6
  }): Promise<void>;
7
7
  //#endregion
8
- export { runProxy };
9
8
  //# sourceMappingURL=proxy.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.d.ts","names":[],"sources":["../src/proxy.ts"],"mappings":";iBAesB,WACpB,KACA,OACA;EAEA,MAAM;EACN,QAAQ,OAAO;EACf,SAAS,OAAO;IACZ"}
1
+ {"version":3,"file":"proxy.d.ts","names":[],"sources":["../src/proxy.ts"],"mappings":";wBAesB,WACpB,KACA,OACA;EAEA,MAAM;EACN,QAAQ,OAAO;EACf,SAAS,OAAO;IACZ"}
package/dist/proxy.js CHANGED
@@ -1,12 +1,14 @@
1
1
  import { StreamableHTTPClientTransport, isInitializeRequest, isJSONRPCRequest, isJSONRPCResultResponse } from "@modelcontextprotocol/client";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
3
+ import { finished } from "node:stream";
3
4
  //#region src/proxy.ts
4
5
  const DEFAULT_URL = "https://www.assistant-ui.com/mcp";
5
6
  const logError = (message, error) => {
6
7
  console.error(`assistant-ui MCP proxy: ${message}`, error);
7
8
  };
8
9
  async function runProxy({ url = new URL(process.env.ASSISTANT_UI_MCP_URL ?? DEFAULT_URL), stdin = process.stdin, stdout = process.stdout } = {}) {
9
- const stdio = new StdioServerTransport(stdin, stdout);
10
+ const input = stdin;
11
+ const stdio = new StdioServerTransport(input, stdout);
10
12
  const http = new StreamableHTTPClientTransport(url);
11
13
  let initializeRequestId;
12
14
  let closing = false;
@@ -21,9 +23,14 @@ async function runProxy({ url = new URL(process.env.ASSISTANT_UI_MCP_URL ?? DEFA
21
23
  logError("failed to close transport", error);
22
24
  }).then(resolveClosed);
23
25
  };
26
+ const logUnlessClosing = (message, error) => {
27
+ if (closing) return;
28
+ logError(message, error);
29
+ };
24
30
  stdio.onmessage = (message) => {
25
- if (isInitializeRequest(message)) initializeRequestId = message.id;
31
+ if (isJSONRPCRequest(message) && isInitializeRequest(message)) initializeRequestId = message.id;
26
32
  http.send(message).catch(async (error) => {
33
+ if (closing) return;
27
34
  logError("failed to forward message to hosted endpoint", error);
28
35
  if (!isJSONRPCRequest(message)) return;
29
36
  await stdio.send({
@@ -34,7 +41,7 @@ async function runProxy({ url = new URL(process.env.ASSISTANT_UI_MCP_URL ?? DEFA
34
41
  message: "Failed to proxy request to the hosted MCP server"
35
42
  }
36
43
  }).catch((sendError) => {
37
- logError("failed to return proxy error response", sendError);
44
+ logUnlessClosing("failed to return proxy error response", sendError);
38
45
  });
39
46
  });
40
47
  };
@@ -44,14 +51,14 @@ async function runProxy({ url = new URL(process.env.ASSISTANT_UI_MCP_URL ?? DEFA
44
51
  if (typeof protocolVersion === "string") http.setProtocolVersion(protocolVersion);
45
52
  }
46
53
  stdio.send(message).catch((error) => {
47
- logError("failed to forward message to stdio", error);
54
+ logUnlessClosing("failed to forward message to stdio", error);
48
55
  });
49
56
  };
50
57
  stdio.onerror = (error) => {
51
- logError("stdio transport error", error);
58
+ logUnlessClosing("stdio transport error", error);
52
59
  };
53
60
  http.onerror = (error) => {
54
- logError("HTTP transport error", error);
61
+ logUnlessClosing("HTTP transport error", error);
55
62
  };
56
63
  stdio.onclose = () => {
57
64
  closeCounterpart(http);
@@ -59,16 +66,24 @@ async function runProxy({ url = new URL(process.env.ASSISTANT_UI_MCP_URL ?? DEFA
59
66
  http.onclose = () => {
60
67
  closeCounterpart(stdio);
61
68
  };
69
+ let releaseInput;
62
70
  try {
63
71
  await http.start();
64
72
  await stdio.start();
73
+ releaseInput = finished(input, { writable: false }, () => {
74
+ stdio.close().catch((error) => {
75
+ logError("failed to close transport", error);
76
+ });
77
+ });
78
+ await closed;
65
79
  } catch (error) {
66
80
  closing = true;
67
81
  await Promise.allSettled([http.close(), stdio.close()]);
68
82
  resolveClosed();
69
83
  throw error;
84
+ } finally {
85
+ releaseInput?.();
70
86
  }
71
- await closed;
72
87
  }
73
88
  //#endregion
74
89
  export { runProxy };
package/dist/proxy.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.js","names":[],"sources":["../src/proxy.ts"],"sourcesContent":["import {\n StreamableHTTPClientTransport,\n isInitializeRequest,\n isJSONRPCRequest,\n isJSONRPCResultResponse,\n} from \"@modelcontextprotocol/client\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/server/stdio\";\nimport type { Readable, Writable } from \"node:stream\";\n\nconst DEFAULT_URL = \"https://www.assistant-ui.com/mcp\";\n\nconst logError = (message: string, error: unknown) => {\n console.error(`assistant-ui MCP proxy: ${message}`, error);\n};\n\nexport async function runProxy({\n url = new URL(process.env.ASSISTANT_UI_MCP_URL ?? DEFAULT_URL),\n stdin = process.stdin,\n stdout = process.stdout,\n}: {\n url?: URL;\n stdin?: NodeJS.ReadableStream;\n stdout?: NodeJS.WritableStream;\n} = {}) {\n const stdio = new StdioServerTransport(stdin as Readable, stdout as Writable);\n const http = new StreamableHTTPClientTransport(url);\n let initializeRequestId: string | number | undefined;\n let closing = false;\n let resolveClosed = () => {};\n const closed = new Promise<void>((resolve) => {\n resolveClosed = resolve;\n });\n\n const closeCounterpart = (counterpart: { close(): Promise<void> }) => {\n if (closing) return;\n closing = true;\n void counterpart\n .close()\n .catch((error: unknown) => {\n logError(\"failed to close transport\", error);\n })\n .then(resolveClosed);\n };\n\n stdio.onmessage = (message) => {\n if (isInitializeRequest(message)) {\n initializeRequestId = message.id;\n }\n\n void http.send(message).catch(async (error: unknown) => {\n logError(\"failed to forward message to hosted endpoint\", error);\n if (!isJSONRPCRequest(message)) return;\n\n await stdio\n .send({\n jsonrpc: \"2.0\",\n id: message.id,\n error: {\n code: -32603,\n message: \"Failed to proxy request to the hosted MCP server\",\n },\n })\n .catch((sendError: unknown) => {\n logError(\"failed to return proxy error response\", sendError);\n });\n });\n };\n\n http.onmessage = (message) => {\n if (\n isJSONRPCResultResponse(message) &&\n message.id === initializeRequestId\n ) {\n const protocolVersion = (\n message.result as { protocolVersion?: unknown } | null\n )?.protocolVersion;\n if (typeof protocolVersion === \"string\") {\n http.setProtocolVersion(protocolVersion);\n }\n }\n\n void stdio.send(message).catch((error: unknown) => {\n logError(\"failed to forward message to stdio\", error);\n });\n };\n\n stdio.onerror = (error) => {\n logError(\"stdio transport error\", error);\n };\n http.onerror = (error) => {\n logError(\"HTTP transport error\", error);\n };\n stdio.onclose = () => {\n closeCounterpart(http);\n };\n http.onclose = () => {\n closeCounterpart(stdio);\n };\n\n try {\n await http.start();\n await stdio.start();\n } catch (error) {\n closing = true;\n await Promise.allSettled([http.close(), stdio.close()]);\n resolveClosed();\n throw error;\n }\n\n await closed;\n}\n"],"mappings":";;;AASA,MAAM,cAAc;AAEpB,MAAM,YAAY,SAAiB,UAAmB;CACpD,QAAQ,MAAM,2BAA2B,WAAW,KAAK;AAC3D;AAEA,eAAsB,SAAS,EAC7B,MAAM,IAAI,IAAI,QAAQ,IAAI,wBAAwB,WAAW,GAC7D,QAAQ,QAAQ,OAChB,SAAS,QAAQ,WAKf,CAAC,GAAG;CACN,MAAM,QAAQ,IAAI,qBAAqB,OAAmB,MAAkB;CAC5E,MAAM,OAAO,IAAI,8BAA8B,GAAG;CAClD,IAAI;CACJ,IAAI,UAAU;CACd,IAAI,sBAAsB,CAAC;CAC3B,MAAM,SAAS,IAAI,SAAe,YAAY;EAC5C,gBAAgB;CAClB,CAAC;CAED,MAAM,oBAAoB,gBAA4C;EACpE,IAAI,SAAS;EACb,UAAU;EACV,YACG,MAAM,CAAC,CACP,OAAO,UAAmB;GACzB,SAAS,6BAA6B,KAAK;EAC7C,CAAC,CAAC,CACD,KAAK,aAAa;CACvB;CAEA,MAAM,aAAa,YAAY;EAC7B,IAAI,oBAAoB,OAAO,GAC7B,sBAAsB,QAAQ;EAGhC,KAAU,KAAK,OAAO,CAAC,CAAC,MAAM,OAAO,UAAmB;GACtD,SAAS,gDAAgD,KAAK;GAC9D,IAAI,CAAC,iBAAiB,OAAO,GAAG;GAEhC,MAAM,MACH,KAAK;IACJ,SAAS;IACT,IAAI,QAAQ;IACZ,OAAO;KACL,MAAM;KACN,SAAS;IACX;GACF,CAAC,CAAC,CACD,OAAO,cAAuB;IAC7B,SAAS,yCAAyC,SAAS;GAC7D,CAAC;EACL,CAAC;CACH;CAEA,KAAK,aAAa,YAAY;EAC5B,IACE,wBAAwB,OAAO,KAC/B,QAAQ,OAAO,qBACf;GACA,MAAM,kBACJ,QAAQ,QACP;GACH,IAAI,OAAO,oBAAoB,UAC7B,KAAK,mBAAmB,eAAe;EAE3C;EAEA,MAAW,KAAK,OAAO,CAAC,CAAC,OAAO,UAAmB;GACjD,SAAS,sCAAsC,KAAK;EACtD,CAAC;CACH;CAEA,MAAM,WAAW,UAAU;EACzB,SAAS,yBAAyB,KAAK;CACzC;CACA,KAAK,WAAW,UAAU;EACxB,SAAS,wBAAwB,KAAK;CACxC;CACA,MAAM,gBAAgB;EACpB,iBAAiB,IAAI;CACvB;CACA,KAAK,gBAAgB;EACnB,iBAAiB,KAAK;CACxB;CAEA,IAAI;EACF,MAAM,KAAK,MAAM;EACjB,MAAM,MAAM,MAAM;CACpB,SAAS,OAAO;EACd,UAAU;EACV,MAAM,QAAQ,WAAW,CAAC,KAAK,MAAM,GAAG,MAAM,MAAM,CAAC,CAAC;EACtD,cAAc;EACd,MAAM;CACR;CAEA,MAAM;AACR"}
1
+ {"version":3,"file":"proxy.js","names":[],"sources":["../src/proxy.ts"],"sourcesContent":["import {\n StreamableHTTPClientTransport,\n isInitializeRequest,\n isJSONRPCRequest,\n isJSONRPCResultResponse,\n} from \"@modelcontextprotocol/client\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/server/stdio\";\nimport { type Readable, type Writable, finished } from \"node:stream\";\n\nconst DEFAULT_URL = \"https://www.assistant-ui.com/mcp\";\n\nconst logError = (message: string, error: unknown) => {\n console.error(`assistant-ui MCP proxy: ${message}`, error);\n};\n\nexport async function runProxy({\n url = new URL(process.env.ASSISTANT_UI_MCP_URL ?? DEFAULT_URL),\n stdin = process.stdin,\n stdout = process.stdout,\n}: {\n url?: URL;\n stdin?: NodeJS.ReadableStream;\n stdout?: NodeJS.WritableStream;\n} = {}) {\n const input = stdin as Readable;\n const stdio = new StdioServerTransport(input, stdout as Writable);\n const http = new StreamableHTTPClientTransport(url);\n let initializeRequestId: string | number | undefined;\n let closing = false;\n let resolveClosed = () => {};\n const closed = new Promise<void>((resolve) => {\n resolveClosed = resolve;\n });\n\n const closeCounterpart = (counterpart: { close(): Promise<void> }) => {\n if (closing) return;\n closing = true;\n void counterpart\n .close()\n .catch((error: unknown) => {\n logError(\"failed to close transport\", error);\n })\n .then(resolveClosed);\n };\n\n const logUnlessClosing = (message: string, error: unknown) => {\n if (closing) return;\n logError(message, error);\n };\n\n stdio.onmessage = (message) => {\n if (isJSONRPCRequest(message) && isInitializeRequest(message)) {\n initializeRequestId = message.id;\n }\n\n void http.send(message).catch(async (error: unknown) => {\n if (closing) return;\n logError(\"failed to forward message to hosted endpoint\", error);\n if (!isJSONRPCRequest(message)) return;\n\n await stdio\n .send({\n jsonrpc: \"2.0\",\n id: message.id,\n error: {\n code: -32603,\n message: \"Failed to proxy request to the hosted MCP server\",\n },\n })\n .catch((sendError: unknown) => {\n logUnlessClosing(\"failed to return proxy error response\", sendError);\n });\n });\n };\n\n http.onmessage = (message) => {\n if (\n isJSONRPCResultResponse(message) &&\n message.id === initializeRequestId\n ) {\n const protocolVersion = (\n message.result as { protocolVersion?: unknown } | null\n )?.protocolVersion;\n if (typeof protocolVersion === \"string\") {\n http.setProtocolVersion(protocolVersion);\n }\n }\n\n void stdio.send(message).catch((error: unknown) => {\n logUnlessClosing(\"failed to forward message to stdio\", error);\n });\n };\n\n stdio.onerror = (error) => {\n logUnlessClosing(\"stdio transport error\", error);\n };\n http.onerror = (error) => {\n logUnlessClosing(\"HTTP transport error\", error);\n };\n stdio.onclose = () => {\n closeCounterpart(http);\n };\n http.onclose = () => {\n closeCounterpart(stdio);\n };\n\n let releaseInput: (() => void) | undefined;\n\n try {\n await http.start();\n await stdio.start();\n releaseInput = finished(input, { writable: false }, () => {\n void stdio.close().catch((error: unknown) => {\n logError(\"failed to close transport\", error);\n });\n });\n await closed;\n } catch (error) {\n closing = true;\n await Promise.allSettled([http.close(), stdio.close()]);\n resolveClosed();\n throw error;\n } finally {\n releaseInput?.();\n }\n}\n"],"mappings":";;;;AASA,MAAM,cAAc;AAEpB,MAAM,YAAY,SAAiB,UAAmB;CACpD,QAAQ,MAAM,2BAA2B,WAAW,KAAK;AAC3D;AAEA,eAAsB,SAAS,EAC7B,MAAM,IAAI,IAAI,QAAQ,IAAI,wBAAwB,WAAW,GAC7D,QAAQ,QAAQ,OAChB,SAAS,QAAQ,WAKf,CAAC,GAAG;CACN,MAAM,QAAQ;CACd,MAAM,QAAQ,IAAI,qBAAqB,OAAO,MAAkB;CAChE,MAAM,OAAO,IAAI,8BAA8B,GAAG;CAClD,IAAI;CACJ,IAAI,UAAU;CACd,IAAI,sBAAsB,CAAC;CAC3B,MAAM,SAAS,IAAI,SAAe,YAAY;EAC5C,gBAAgB;CAClB,CAAC;CAED,MAAM,oBAAoB,gBAA4C;EACpE,IAAI,SAAS;EACb,UAAU;EACV,YACG,MAAM,CAAC,CACP,OAAO,UAAmB;GACzB,SAAS,6BAA6B,KAAK;EAC7C,CAAC,CAAC,CACD,KAAK,aAAa;CACvB;CAEA,MAAM,oBAAoB,SAAiB,UAAmB;EAC5D,IAAI,SAAS;EACb,SAAS,SAAS,KAAK;CACzB;CAEA,MAAM,aAAa,YAAY;EAC7B,IAAI,iBAAiB,OAAO,KAAK,oBAAoB,OAAO,GAC1D,sBAAsB,QAAQ;EAGhC,KAAU,KAAK,OAAO,CAAC,CAAC,MAAM,OAAO,UAAmB;GACtD,IAAI,SAAS;GACb,SAAS,gDAAgD,KAAK;GAC9D,IAAI,CAAC,iBAAiB,OAAO,GAAG;GAEhC,MAAM,MACH,KAAK;IACJ,SAAS;IACT,IAAI,QAAQ;IACZ,OAAO;KACL,MAAM;KACN,SAAS;IACX;GACF,CAAC,CAAC,CACD,OAAO,cAAuB;IAC7B,iBAAiB,yCAAyC,SAAS;GACrE,CAAC;EACL,CAAC;CACH;CAEA,KAAK,aAAa,YAAY;EAC5B,IACE,wBAAwB,OAAO,KAC/B,QAAQ,OAAO,qBACf;GACA,MAAM,kBACJ,QAAQ,QACP;GACH,IAAI,OAAO,oBAAoB,UAC7B,KAAK,mBAAmB,eAAe;EAE3C;EAEA,MAAW,KAAK,OAAO,CAAC,CAAC,OAAO,UAAmB;GACjD,iBAAiB,sCAAsC,KAAK;EAC9D,CAAC;CACH;CAEA,MAAM,WAAW,UAAU;EACzB,iBAAiB,yBAAyB,KAAK;CACjD;CACA,KAAK,WAAW,UAAU;EACxB,iBAAiB,wBAAwB,KAAK;CAChD;CACA,MAAM,gBAAgB;EACpB,iBAAiB,IAAI;CACvB;CACA,KAAK,gBAAgB;EACnB,iBAAiB,KAAK;CACxB;CAEA,IAAI;CAEJ,IAAI;EACF,MAAM,KAAK,MAAM;EACjB,MAAM,MAAM,MAAM;EAClB,eAAe,SAAS,OAAO,EAAE,UAAU,MAAM,SAAS;GACxD,MAAW,MAAM,CAAC,CAAC,OAAO,UAAmB;IAC3C,SAAS,6BAA6B,KAAK;GAC7C,CAAC;EACH,CAAC;EACD,MAAM;CACR,SAAS,OAAO;EACd,UAAU;EACV,MAAM,QAAQ,WAAW,CAAC,KAAK,MAAM,GAAG,MAAM,MAAM,CAAC,CAAC;EACtD,cAAc;EACd,MAAM;CACR,UAAU;EACR,eAAe;CACjB;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assistant-ui/mcp-docs-server",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Stdio proxy for the hosted assistant-ui documentation MCP server",
5
5
  "keywords": [
6
6
  "mcp",
@@ -35,11 +35,11 @@
35
35
  "@modelcontextprotocol/server": "^2.0.0"
36
36
  },
37
37
  "devDependencies": {
38
- "@assistant-ui/x-buildutils": "0.0.25",
38
+ "@assistant-ui/x-buildutils": "0.0.28",
39
39
  "@modelcontextprotocol/core": "^2.0.0",
40
- "@types/node": "^26.4.0",
40
+ "@types/node": "^26.5.1",
41
41
  "tsx": "^4.23.13",
42
- "vitest": "^4.1.11"
42
+ "vitest": "^5.0.1"
43
43
  },
44
44
  "publishConfig": {
45
45
  "access": "public",
@@ -58,6 +58,7 @@
58
58
  "build": "aui-build",
59
59
  "dev": "tsx src/stdio.ts",
60
60
  "test": "vitest run",
61
+ "typecheck": "tsc --noEmit",
61
62
  "test:watch": "vitest"
62
63
  }
63
64
  }
package/src/proxy.test.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { once } from "node:events";
1
3
  import {
2
4
  createServer,
3
5
  type IncomingMessage,
@@ -147,6 +149,139 @@ const createRpcClient = (stdin: PassThrough, stdout: PassThrough) => {
147
149
  };
148
150
 
149
151
  describe("runProxy", () => {
152
+ it.each([
153
+ {
154
+ scenario: "stdin reached EOF before startup",
155
+ createStdin: async () => {
156
+ const stdin = new PassThrough();
157
+ const ended = once(stdin, "end");
158
+ stdin.resume();
159
+ stdin.end();
160
+ await ended;
161
+ return stdin;
162
+ },
163
+ stopInput: (stdin: PassThrough) => stdin.end(),
164
+ },
165
+ {
166
+ scenario: "stdin reaches EOF after startup",
167
+ createStdin: async () => new PassThrough(),
168
+ stopInput: (stdin: PassThrough) => stdin.end(),
169
+ },
170
+ {
171
+ scenario: "stdin ends without emitting close",
172
+ createStdin: async () => new PassThrough({ autoDestroy: false }),
173
+ stopInput: (stdin: PassThrough) => stdin.end(),
174
+ },
175
+ {
176
+ scenario: "stdin is destroyed without EOF",
177
+ createStdin: async () => new PassThrough(),
178
+ stopInput: (stdin: PassThrough) => stdin.destroy(),
179
+ },
180
+ ])("closes when $scenario", async ({ createStdin, stopInput }) => {
181
+ const stdin = await createStdin();
182
+ const stdout = new PassThrough();
183
+ const stderr = vi
184
+ .spyOn(console, "error")
185
+ .mockImplementation(() => undefined);
186
+ const proxy = runProxy({
187
+ url: new URL("https://example.invalid/mcp"),
188
+ stdin,
189
+ stdout,
190
+ });
191
+ let closed = false;
192
+
193
+ try {
194
+ await new Promise<void>((resolve) => setImmediate(resolve));
195
+ stopInput(stdin);
196
+ await expect(
197
+ withTimeout(proxy, "proxy shutdown after stdin closed"),
198
+ ).resolves.toBeUndefined();
199
+ closed = true;
200
+ } finally {
201
+ stdout.destroy(closed ? undefined : new Error("Proxy test cleanup"));
202
+ await proxy;
203
+ stdin.destroy();
204
+ stderr.mockRestore();
205
+ }
206
+ });
207
+
208
+ it("does not log errors caused by clean shutdown", async () => {
209
+ let resolvePendingRequest = () => {};
210
+ const pendingRequest = new Promise<void>((resolve) => {
211
+ resolvePendingRequest = resolve;
212
+ });
213
+ const httpServer = createServer(async (request, response) => {
214
+ if (request.method !== "POST") {
215
+ response.statusCode = 405;
216
+ response.end();
217
+ return;
218
+ }
219
+
220
+ let body = "";
221
+ for await (const chunk of request) body += chunk.toString();
222
+ const message = JSON.parse(body) as { method: string; id?: number };
223
+ if (message.method === "initialize") {
224
+ response.setHeader("content-type", "application/json");
225
+ response.end(
226
+ JSON.stringify({
227
+ jsonrpc: "2.0",
228
+ id: message.id,
229
+ result: {
230
+ protocolVersion: "2025-11-25",
231
+ capabilities: {},
232
+ serverInfo: { name: "proxy-test", version: "1.0.0" },
233
+ },
234
+ }),
235
+ );
236
+ return;
237
+ }
238
+
239
+ response.writeHead(200, {
240
+ "content-type": "text/event-stream",
241
+ "cache-control": "no-cache",
242
+ });
243
+ response.write(": keep-alive\n\n");
244
+ resolvePendingRequest();
245
+ });
246
+ const url = await listen(httpServer);
247
+ const proxyStdin = new PassThrough();
248
+ const proxyStdout = new PassThrough();
249
+ const stderr = vi
250
+ .spyOn(console, "error")
251
+ .mockImplementation(() => undefined);
252
+ const proxy = runProxy({ url, stdin: proxyStdin, stdout: proxyStdout });
253
+ const client = createRpcClient(proxyStdin, proxyStdout);
254
+ let proxyClosed = false;
255
+
256
+ try {
257
+ const initialize = await client.request("initialize", {
258
+ protocolVersion: "2025-11-25",
259
+ capabilities: {},
260
+ clientInfo: { name: "proxy-test-client", version: "1.0.0" },
261
+ });
262
+ expect(initialize.error).toBeUndefined();
263
+
264
+ client.notify("notifications/initialized");
265
+ await withTimeout(pendingRequest, "pending request");
266
+ proxyStdin.end();
267
+ await expect(
268
+ withTimeout(proxy, "proxy shutdown after stdin closed"),
269
+ ).resolves.toBeUndefined();
270
+ proxyClosed = true;
271
+
272
+ expect(stderr).not.toHaveBeenCalled();
273
+ } finally {
274
+ if (!proxyClosed) {
275
+ proxyStdin.destroy();
276
+ await proxy.catch(() => undefined);
277
+ }
278
+ await closeHttpServer(httpServer);
279
+ proxyStdout.destroy();
280
+ proxyStdin.destroy();
281
+ stderr.mockRestore();
282
+ }
283
+ });
284
+
150
285
  it("proxies MCP requests and returns transport failures", async () => {
151
286
  const inputSchema = fromJsonSchema<{ text: string }>({
152
287
  type: "object",
package/src/proxy.ts CHANGED
@@ -5,7 +5,7 @@ import {
5
5
  isJSONRPCResultResponse,
6
6
  } from "@modelcontextprotocol/client";
7
7
  import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
8
- import type { Readable, Writable } from "node:stream";
8
+ import { type Readable, type Writable, finished } from "node:stream";
9
9
 
10
10
  const DEFAULT_URL = "https://www.assistant-ui.com/mcp";
11
11
 
@@ -22,7 +22,8 @@ export async function runProxy({
22
22
  stdin?: NodeJS.ReadableStream;
23
23
  stdout?: NodeJS.WritableStream;
24
24
  } = {}) {
25
- const stdio = new StdioServerTransport(stdin as Readable, stdout as Writable);
25
+ const input = stdin as Readable;
26
+ const stdio = new StdioServerTransport(input, stdout as Writable);
26
27
  const http = new StreamableHTTPClientTransport(url);
27
28
  let initializeRequestId: string | number | undefined;
28
29
  let closing = false;
@@ -42,12 +43,18 @@ export async function runProxy({
42
43
  .then(resolveClosed);
43
44
  };
44
45
 
46
+ const logUnlessClosing = (message: string, error: unknown) => {
47
+ if (closing) return;
48
+ logError(message, error);
49
+ };
50
+
45
51
  stdio.onmessage = (message) => {
46
- if (isInitializeRequest(message)) {
52
+ if (isJSONRPCRequest(message) && isInitializeRequest(message)) {
47
53
  initializeRequestId = message.id;
48
54
  }
49
55
 
50
56
  void http.send(message).catch(async (error: unknown) => {
57
+ if (closing) return;
51
58
  logError("failed to forward message to hosted endpoint", error);
52
59
  if (!isJSONRPCRequest(message)) return;
53
60
 
@@ -61,7 +68,7 @@ export async function runProxy({
61
68
  },
62
69
  })
63
70
  .catch((sendError: unknown) => {
64
- logError("failed to return proxy error response", sendError);
71
+ logUnlessClosing("failed to return proxy error response", sendError);
65
72
  });
66
73
  });
67
74
  };
@@ -80,15 +87,15 @@ export async function runProxy({
80
87
  }
81
88
 
82
89
  void stdio.send(message).catch((error: unknown) => {
83
- logError("failed to forward message to stdio", error);
90
+ logUnlessClosing("failed to forward message to stdio", error);
84
91
  });
85
92
  };
86
93
 
87
94
  stdio.onerror = (error) => {
88
- logError("stdio transport error", error);
95
+ logUnlessClosing("stdio transport error", error);
89
96
  };
90
97
  http.onerror = (error) => {
91
- logError("HTTP transport error", error);
98
+ logUnlessClosing("HTTP transport error", error);
92
99
  };
93
100
  stdio.onclose = () => {
94
101
  closeCounterpart(http);
@@ -97,15 +104,23 @@ export async function runProxy({
97
104
  closeCounterpart(stdio);
98
105
  };
99
106
 
107
+ let releaseInput: (() => void) | undefined;
108
+
100
109
  try {
101
110
  await http.start();
102
111
  await stdio.start();
112
+ releaseInput = finished(input, { writable: false }, () => {
113
+ void stdio.close().catch((error: unknown) => {
114
+ logError("failed to close transport", error);
115
+ });
116
+ });
117
+ await closed;
103
118
  } catch (error) {
104
119
  closing = true;
105
120
  await Promise.allSettled([http.close(), stdio.close()]);
106
121
  resolveClosed();
107
122
  throw error;
123
+ } finally {
124
+ releaseInput?.();
108
125
  }
109
-
110
- await closed;
111
126
  }