skybridge 0.0.0-dev.8e6ba30 → 0.0.0-dev.8e70ea4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/README.md +15 -11
  2. package/dist/cli/header.js +1 -1
  3. package/dist/cli/header.js.map +1 -1
  4. package/dist/cli/tunnel-control-server.d.ts +9 -0
  5. package/dist/cli/tunnel-control-server.js +31 -0
  6. package/dist/cli/tunnel-control-server.js.map +1 -0
  7. package/dist/cli/tunnel-control-server.test.js +39 -0
  8. package/dist/cli/tunnel-control-server.test.js.map +1 -0
  9. package/dist/cli/tunnel-handler.d.ts +3 -0
  10. package/dist/cli/tunnel-handler.js +48 -0
  11. package/dist/cli/tunnel-handler.js.map +1 -0
  12. package/dist/cli/tunnel-handler.test.js +105 -0
  13. package/dist/cli/tunnel-handler.test.js.map +1 -0
  14. package/dist/cli/tunnel.d.ts +57 -0
  15. package/dist/cli/tunnel.js +154 -0
  16. package/dist/cli/tunnel.js.map +1 -0
  17. package/dist/cli/tunnel.test.js +190 -0
  18. package/dist/cli/tunnel.test.js.map +1 -0
  19. package/dist/cli/types.d.ts +5 -0
  20. package/dist/cli/types.js +2 -0
  21. package/dist/cli/types.js.map +1 -0
  22. package/dist/cli/use-messages.d.ts +3 -0
  23. package/dist/cli/use-messages.js +11 -0
  24. package/dist/cli/use-messages.js.map +1 -0
  25. package/dist/cli/use-nodemon.d.ts +2 -7
  26. package/dist/cli/use-nodemon.js +8 -20
  27. package/dist/cli/use-nodemon.js.map +1 -1
  28. package/dist/cli/use-open-browser.d.ts +1 -0
  29. package/dist/cli/use-open-browser.js +44 -0
  30. package/dist/cli/use-open-browser.js.map +1 -0
  31. package/dist/cli/use-tunnel.d.ts +14 -0
  32. package/dist/cli/use-tunnel.js +131 -0
  33. package/dist/cli/use-tunnel.js.map +1 -0
  34. package/dist/cli/use-typescript-check.d.ts +1 -0
  35. package/dist/cli/use-typescript-check.js +41 -6
  36. package/dist/cli/use-typescript-check.js.map +1 -1
  37. package/dist/commands/build.js +63 -7
  38. package/dist/commands/build.js.map +1 -1
  39. package/dist/commands/dev.d.ts +3 -0
  40. package/dist/commands/dev.js +47 -3
  41. package/dist/commands/dev.js.map +1 -1
  42. package/dist/commands/start.js +7 -10
  43. package/dist/commands/start.js.map +1 -1
  44. package/dist/server/asset-base-url-transform-plugin.d.ts +5 -6
  45. package/dist/server/asset-base-url-transform-plugin.js +9 -10
  46. package/dist/server/asset-base-url-transform-plugin.js.map +1 -1
  47. package/dist/server/asset-base-url-transform-plugin.test.js +41 -13
  48. package/dist/server/asset-base-url-transform-plugin.test.js.map +1 -1
  49. package/dist/server/content-helpers.d.ts +27 -0
  50. package/dist/server/content-helpers.js +46 -0
  51. package/dist/server/content-helpers.js.map +1 -0
  52. package/dist/server/content-helpers.test.d.ts +1 -0
  53. package/dist/server/content-helpers.test.js +70 -0
  54. package/dist/server/content-helpers.test.js.map +1 -0
  55. package/dist/server/express.d.ts +4 -4
  56. package/dist/server/express.js +51 -22
  57. package/dist/server/express.js.map +1 -1
  58. package/dist/server/express.test.js +311 -61
  59. package/dist/server/express.test.js.map +1 -1
  60. package/dist/server/index.d.ts +4 -3
  61. package/dist/server/index.js +3 -2
  62. package/dist/server/index.js.map +1 -1
  63. package/dist/server/inferUtilityTypes.d.ts +6 -6
  64. package/dist/server/metric.d.ts +14 -0
  65. package/dist/server/metric.js +62 -0
  66. package/dist/server/metric.js.map +1 -0
  67. package/dist/server/middleware.test.js +12 -9
  68. package/dist/server/middleware.test.js.map +1 -1
  69. package/dist/server/server.d.ts +125 -76
  70. package/dist/server/server.js +272 -79
  71. package/dist/server/server.js.map +1 -1
  72. package/dist/server/templateHelper.d.ts +5 -7
  73. package/dist/server/templateHelper.js +3 -22
  74. package/dist/server/templateHelper.js.map +1 -1
  75. package/dist/server/templates.generated.d.ts +4 -0
  76. package/dist/server/templates.generated.js +47 -0
  77. package/dist/server/templates.generated.js.map +1 -0
  78. package/dist/server/tunnel-proxy-router.d.ts +7 -0
  79. package/dist/server/tunnel-proxy-router.js +110 -0
  80. package/dist/server/tunnel-proxy-router.js.map +1 -0
  81. package/dist/server/tunnel-proxy-router.test.d.ts +1 -0
  82. package/dist/server/tunnel-proxy-router.test.js +229 -0
  83. package/dist/server/tunnel-proxy-router.test.js.map +1 -0
  84. package/dist/server/viewsDevServer.d.ts +14 -0
  85. package/dist/server/viewsDevServer.js +45 -0
  86. package/dist/server/viewsDevServer.js.map +1 -0
  87. package/dist/test/utils.d.ts +13 -21
  88. package/dist/test/utils.js +42 -37
  89. package/dist/test/utils.js.map +1 -1
  90. package/dist/test/view.test.d.ts +1 -0
  91. package/dist/test/view.test.js +523 -0
  92. package/dist/test/view.test.js.map +1 -0
  93. package/dist/version.d.ts +1 -0
  94. package/dist/version.js +3 -0
  95. package/dist/version.js.map +1 -0
  96. package/dist/web/bridges/apps-sdk/adaptor.d.ts +5 -3
  97. package/dist/web/bridges/apps-sdk/adaptor.js +32 -14
  98. package/dist/web/bridges/apps-sdk/adaptor.js.map +1 -1
  99. package/dist/web/bridges/apps-sdk/types.d.ts +10 -5
  100. package/dist/web/bridges/apps-sdk/types.js.map +1 -1
  101. package/dist/web/bridges/mcp-app/adaptor.d.ts +15 -5
  102. package/dist/web/bridges/mcp-app/adaptor.js +106 -22
  103. package/dist/web/bridges/mcp-app/adaptor.js.map +1 -1
  104. package/dist/web/bridges/types.d.ts +15 -8
  105. package/dist/web/components/modal-provider.js +2 -2
  106. package/dist/web/components/modal-provider.js.map +1 -1
  107. package/dist/web/create-store.js +17 -3
  108. package/dist/web/create-store.js.map +1 -1
  109. package/dist/web/create-store.test.js +14 -16
  110. package/dist/web/create-store.test.js.map +1 -1
  111. package/dist/web/data-llm.d.ts +1 -1
  112. package/dist/web/data-llm.js +3 -3
  113. package/dist/web/data-llm.js.map +1 -1
  114. package/dist/web/data-llm.test.js +22 -22
  115. package/dist/web/data-llm.test.js.map +1 -1
  116. package/dist/web/generate-helpers.d.ts +20 -18
  117. package/dist/web/generate-helpers.js +20 -18
  118. package/dist/web/generate-helpers.js.map +1 -1
  119. package/dist/web/generate-helpers.test-d.js +26 -26
  120. package/dist/web/generate-helpers.test-d.js.map +1 -1
  121. package/dist/web/helpers/state.d.ts +2 -2
  122. package/dist/web/helpers/state.js +11 -11
  123. package/dist/web/helpers/state.js.map +1 -1
  124. package/dist/web/helpers/state.test.js +9 -9
  125. package/dist/web/helpers/state.test.js.map +1 -1
  126. package/dist/web/hooks/index.d.ts +1 -1
  127. package/dist/web/hooks/index.js +1 -1
  128. package/dist/web/hooks/index.js.map +1 -1
  129. package/dist/web/hooks/use-files.d.ts +2 -1
  130. package/dist/web/hooks/use-files.js +1 -0
  131. package/dist/web/hooks/use-files.js.map +1 -1
  132. package/dist/web/hooks/use-files.test.js +22 -2
  133. package/dist/web/hooks/use-files.test.js.map +1 -1
  134. package/dist/web/hooks/use-request-modal.d.ts +1 -1
  135. package/dist/web/hooks/use-request-modal.js +4 -4
  136. package/dist/web/hooks/use-request-modal.js.map +1 -1
  137. package/dist/web/hooks/use-request-modal.test.js +5 -1
  138. package/dist/web/hooks/use-request-modal.test.js.map +1 -1
  139. package/dist/web/hooks/use-user.js +18 -2
  140. package/dist/web/hooks/use-user.js.map +1 -1
  141. package/dist/web/hooks/use-user.test.js +28 -0
  142. package/dist/web/hooks/use-user.test.js.map +1 -1
  143. package/dist/web/hooks/use-view-state.d.ts +4 -0
  144. package/dist/web/hooks/use-view-state.js +32 -0
  145. package/dist/web/hooks/use-view-state.js.map +1 -0
  146. package/dist/web/hooks/use-view-state.test.d.ts +1 -0
  147. package/dist/web/hooks/use-view-state.test.js +177 -0
  148. package/dist/web/hooks/use-view-state.test.js.map +1 -0
  149. package/dist/web/index.d.ts +1 -2
  150. package/dist/web/index.js +1 -2
  151. package/dist/web/index.js.map +1 -1
  152. package/dist/web/mount-view.d.ts +1 -0
  153. package/dist/web/{mount-widget.js → mount-view.js} +2 -2
  154. package/dist/web/mount-view.js.map +1 -0
  155. package/dist/web/plugin/plugin.d.ts +4 -1
  156. package/dist/web/plugin/plugin.js +127 -25
  157. package/dist/web/plugin/plugin.js.map +1 -1
  158. package/dist/web/plugin/scan-views.d.ts +16 -0
  159. package/dist/web/plugin/scan-views.js +88 -0
  160. package/dist/web/plugin/scan-views.js.map +1 -0
  161. package/dist/web/plugin/scan-views.test.d.ts +1 -0
  162. package/dist/web/plugin/scan-views.test.js +99 -0
  163. package/dist/web/plugin/scan-views.test.js.map +1 -0
  164. package/dist/web/plugin/transform-data-llm.js +1 -1
  165. package/dist/web/plugin/transform-data-llm.js.map +1 -1
  166. package/dist/web/plugin/validate-view.d.ts +1 -0
  167. package/dist/web/plugin/validate-view.js +9 -0
  168. package/dist/web/plugin/validate-view.js.map +1 -0
  169. package/dist/web/plugin/validate-view.test.d.ts +1 -0
  170. package/dist/web/plugin/validate-view.test.js +24 -0
  171. package/dist/web/plugin/validate-view.test.js.map +1 -0
  172. package/package.json +26 -16
  173. package/tsconfig.base.json +2 -0
  174. package/dist/server/templates/development.hbs +0 -12
  175. package/dist/server/templates/production.hbs +0 -6
  176. package/dist/server/widgetsDevServer.d.ts +0 -13
  177. package/dist/server/widgetsDevServer.js +0 -57
  178. package/dist/server/widgetsDevServer.js.map +0 -1
  179. package/dist/test/widget.test.js +0 -263
  180. package/dist/test/widget.test.js.map +0 -1
  181. package/dist/web/hooks/use-widget-state.d.ts +0 -4
  182. package/dist/web/hooks/use-widget-state.js +0 -32
  183. package/dist/web/hooks/use-widget-state.js.map +0 -1
  184. package/dist/web/hooks/use-widget-state.test.js +0 -64
  185. package/dist/web/hooks/use-widget-state.test.js.map +0 -1
  186. package/dist/web/mount-widget.d.ts +0 -1
  187. package/dist/web/mount-widget.js.map +0 -1
  188. package/dist/web/plugin/validate-widget.d.ts +0 -5
  189. package/dist/web/plugin/validate-widget.js +0 -27
  190. package/dist/web/plugin/validate-widget.js.map +0 -1
  191. package/dist/web/plugin/validate-widget.test.js +0 -42
  192. package/dist/web/plugin/validate-widget.test.js.map +0 -1
  193. /package/dist/{test/widget.test.d.ts → cli/tunnel-control-server.test.d.ts} +0 -0
  194. /package/dist/{web/hooks/use-widget-state.test.d.ts → cli/tunnel-handler.test.d.ts} +0 -0
  195. /package/dist/{web/plugin/validate-widget.test.d.ts → cli/tunnel.test.d.ts} +0 -0
@@ -1,11 +1,11 @@
1
1
  import type http from "node:http";
2
2
  import express from "express";
3
- import type { McpServer } from "./server";
4
- export declare function createApp({ mcpServer, httpServer, customMiddleware, }: {
3
+ import type { McpServer } from "./server.js";
4
+ export declare function createApp({ mcpServer, httpServer, errorMiddleware, }: {
5
5
  mcpServer: McpServer;
6
6
  httpServer: http.Server;
7
- customMiddleware?: {
7
+ errorMiddleware?: {
8
8
  path?: string;
9
- handlers: express.RequestHandler[];
9
+ handlers: express.ErrorRequestHandler[];
10
10
  }[];
11
11
  }): Promise<express.Express>;
@@ -2,11 +2,18 @@ import path from "node:path";
2
2
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
3
3
  import cors from "cors";
4
4
  import express from "express";
5
- export async function createApp({ mcpServer, httpServer, customMiddleware = [], }) {
6
- const app = express();
7
- app.use(express.json());
8
- const env = process.env.NODE_ENV || "development";
9
- for (const middleware of customMiddleware) {
5
+ function parseControlPort(raw) {
6
+ if (raw === undefined) {
7
+ return null;
8
+ }
9
+ const n = Number.parseInt(raw, 10);
10
+ if (!Number.isFinite(n) || n <= 0 || n >= 65536) {
11
+ return null;
12
+ }
13
+ return n;
14
+ }
15
+ function applyMiddlewares(app, middlewares) {
16
+ for (const middleware of middlewares) {
10
17
  if (middleware.path) {
11
18
  app.use(middleware.path, ...middleware.handlers);
12
19
  }
@@ -14,22 +21,47 @@ export async function createApp({ mcpServer, httpServer, customMiddleware = [],
14
21
  app.use(...middleware.handlers);
15
22
  }
16
23
  }
17
- if (env !== "production") {
24
+ }
25
+ function defaultErrorHandler(err, _req, res, _next) {
26
+ console.error("Error handling MCP request:", err);
27
+ if (!res.headersSent) {
28
+ res.status(500).json({
29
+ jsonrpc: "2.0",
30
+ error: { code: -32603, message: "Internal server error" },
31
+ id: null,
32
+ });
33
+ }
34
+ }
35
+ export async function createApp({ mcpServer, httpServer, errorMiddleware = [], }) {
36
+ const app = mcpServer.express;
37
+ // Read `process.env.NODE_ENV` inline: wrangler/esbuild only substitute the literal expression,
38
+ // so a local const would defeat dead-code elimination of the dev-only imports below.
39
+ if (process.env.NODE_ENV !== "production") {
18
40
  const { devtoolsStaticServer } = await import("@skybridge/devtools");
19
41
  app.use(await devtoolsStaticServer());
20
- const { widgetsDevServer } = await import("./widgetsDevServer.js");
21
- app.use(await widgetsDevServer(httpServer));
42
+ const { viewsDevServer } = await import("./viewsDevServer.js");
43
+ app.use(await viewsDevServer(httpServer));
44
+ const controlPort = parseControlPort(process.env.__TUNNEL_CONTROL_PORT);
45
+ if (controlPort !== null) {
46
+ const { createTunnelProxyRouter } = await import("./tunnel-proxy-router.js");
47
+ app.use(createTunnelProxyRouter(controlPort));
48
+ }
49
+ else if (process.env.__TUNNEL_CONTROL_PORT !== undefined) {
50
+ console.warn(`Ignoring invalid __TUNNEL_CONTROL_PORT=${process.env.__TUNNEL_CONTROL_PORT}`);
51
+ }
22
52
  }
23
- if (env === "production") {
53
+ else {
24
54
  const assetsPath = path.join(process.cwd(), "dist", "assets");
25
55
  app.use("/assets", cors());
26
56
  app.use("/assets", express.static(assetsPath));
27
57
  }
28
58
  app.use("/mcp", mcpMiddleware(mcpServer));
59
+ applyMiddlewares(app, errorMiddleware);
60
+ app.use("/mcp", defaultErrorHandler);
29
61
  return app;
30
62
  }
31
63
  const mcpMiddleware = (server) => {
32
- return async (req, res, _next) => {
64
+ return async (req, res, next) => {
33
65
  if (req.method !== "POST") {
34
66
  res.writeHead(405).end(JSON.stringify({
35
67
  jsonrpc: "2.0",
@@ -44,28 +76,25 @@ const mcpMiddleware = (server) => {
44
76
  try {
45
77
  const transport = new StreamableHTTPServerTransport({
46
78
  sessionIdGenerator: undefined,
79
+ // Respond with a single JSON body instead of SSE. Skybridge's stateless
80
+ // transport never streams server-initiated messages, so SSE adds no
81
+ // capability — and on workerd specifically, `cloudflare:node`'s http
82
+ // bridge silently drops chunked writes that happen after the request
83
+ // handler awaits, which manifests as a 200 with empty body for any
84
+ // async tools/call.
85
+ enableJsonResponse: true,
47
86
  });
48
87
  res.on("close", () => {
49
88
  transport.close();
50
89
  });
51
- await server.connect(transport);
90
+ await server.connectStatelessTransport(transport);
52
91
  // Express strips the mount path from req.url (e.g. "/mcp" becomes "/").
53
92
  // Restore it so the SDK builds the correct requestInfo.url.
54
93
  req.url = req.originalUrl;
55
94
  await transport.handleRequest(req, res, req.body);
56
95
  }
57
96
  catch (error) {
58
- console.error("Error handling MCP request:", error);
59
- if (!res.headersSent) {
60
- res.status(500).json({
61
- jsonrpc: "2.0",
62
- error: {
63
- code: -32603,
64
- message: "Internal server error",
65
- },
66
- id: null,
67
- });
68
- }
97
+ next(error);
69
98
  }
70
99
  };
71
100
  };
@@ -1 +1 @@
1
- {"version":3,"file":"express.js","sourceRoot":"","sources":["../../src/server/express.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"express.js","sourceRoot":"","sources":["../../src/server/express.ts"],"names":[],"mappings":"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"}
@@ -1,12 +1,12 @@
1
1
  import http from "node:http";
2
2
  import { afterEach, describe, expect, it, vi } from "vitest";
3
+ import { McpServer } from "./server.js";
3
4
  vi.mock("@skybridge/devtools", () => ({
4
5
  devtoolsStaticServer: () => ((_req, _res, next) => next()),
5
6
  }));
6
- vi.mock("./widgetsDevServer.js", () => ({
7
- widgetsDevServer: (_httpServer) => ((_req, _res, next) => next()),
7
+ vi.mock("./viewsDevServer.js", () => ({
8
+ viewsDevServer: (_httpServer) => ((_req, _res, next) => next()),
8
9
  }));
9
- const fakeServer = {};
10
10
  async function listen(app) {
11
11
  const server = http.createServer(app);
12
12
  await new Promise((resolve) => server.listen(0, resolve));
@@ -22,6 +22,97 @@ async function postMcp(port) {
22
22
  body: JSON.stringify({ jsonrpc: "2.0", method: "initialize", id: 1 }),
23
23
  });
24
24
  }
25
+ async function postApi(port) {
26
+ return fetch(`http://localhost:${port}/api/test`, { method: "POST" });
27
+ }
28
+ describe("McpServer.express", () => {
29
+ it("exposes a ready Express app immediately after construction", () => {
30
+ const server = new McpServer({ name: "t", version: "0.0.0" });
31
+ expect(server.express).toBeDefined();
32
+ expect(typeof server.express.use).toBe("function");
33
+ expect(typeof server.express.get).toBe("function");
34
+ });
35
+ it("server.express.get registers a route reachable alongside /mcp", async () => {
36
+ const { createApp } = await import("./express.js");
37
+ const server = new McpServer({ name: "t", version: "0.0.0" });
38
+ server.express.get("/health", (_req, res) => {
39
+ res.json({ status: "ok" });
40
+ });
41
+ const httpServer = http.createServer();
42
+ await createApp({ mcpServer: server, httpServer });
43
+ const { port, server: listening } = await listen(server.express);
44
+ openServer = listening;
45
+ const health = await fetch(`http://localhost:${port}/health`);
46
+ expect(health.status).toBe(200);
47
+ expect(await health.json()).toEqual({ status: "ok" });
48
+ // /mcp still works (POST returns 200/4xx, not 404)
49
+ const mcp = await postMcp(port);
50
+ expect(mcp.status).not.toBe(404);
51
+ });
52
+ it("server.use and server.express.use produce the same registration order", async () => {
53
+ const { createApp } = await import("./express.js");
54
+ const callsA = [];
55
+ const callsB = [];
56
+ const buildServer = () => new McpServer({ name: "t", version: "0.0.0" });
57
+ const sA = buildServer();
58
+ sA.use((_req, _res, next) => {
59
+ callsA.push("first");
60
+ next();
61
+ });
62
+ sA.express.use((_req, _res, next) => {
63
+ callsA.push("second");
64
+ next();
65
+ });
66
+ const sB = buildServer();
67
+ sB.express.use((_req, _res, next) => {
68
+ callsB.push("first");
69
+ next();
70
+ });
71
+ sB.use((_req, _res, next) => {
72
+ callsB.push("second");
73
+ next();
74
+ });
75
+ for (const s of [sA, sB]) {
76
+ s.express.get("/probe", (_req, res) => res.json({ ok: true }));
77
+ const httpServer = http.createServer();
78
+ await createApp({ mcpServer: s, httpServer });
79
+ const { port, server: listening } = await listen(s.express);
80
+ openServer = listening;
81
+ await fetch(`http://localhost:${port}/probe`);
82
+ listening.close();
83
+ }
84
+ expect(callsA).toEqual(["first", "second"]);
85
+ expect(callsB).toEqual(["first", "second"]);
86
+ });
87
+ it("useOnError still wraps thrown /mcp errors after the route is mounted", async () => {
88
+ const { createApp } = await import("./express.js");
89
+ const server = new McpServer({ name: "t", version: "0.0.0" });
90
+ // Register the error handler BEFORE createApp — useOnError should still
91
+ // apply it after /mcp, so /mcp errors hit it.
92
+ const seen = [];
93
+ server.useOnError((_err, _req, res, _next) => {
94
+ seen.push("useOnError");
95
+ res.status(503).json({ from: "useOnError" });
96
+ });
97
+ // Force the /mcp handler to throw so the error pipeline runs.
98
+ vi.spyOn(server, "connectStatelessTransport").mockRejectedValue(new Error("boom"));
99
+ const httpServer = http.createServer();
100
+ await createApp({
101
+ mcpServer: server,
102
+ httpServer,
103
+ // Mirror what run() does: forward the McpServer's useOnError handlers
104
+ // to createApp so they get applied after /mcp.
105
+ // biome-ignore lint/complexity/useLiteralKeys: test mirrors run() internals to verify useOnError ordering
106
+ errorMiddleware: server["customErrorMiddleware"],
107
+ });
108
+ const { port, server: listening } = await listen(server.express);
109
+ openServer = listening;
110
+ const res = await postMcp(port);
111
+ expect(seen).toEqual(["useOnError"]);
112
+ expect(res.status).toBe(503);
113
+ expect(await res.json()).toEqual({ from: "useOnError" });
114
+ });
115
+ });
25
116
  describe("createApp", () => {
26
117
  it("runs global custom middleware before the /mcp handler", async () => {
27
118
  const { createApp } = await import("./express.js");
@@ -30,14 +121,12 @@ describe("createApp", () => {
30
121
  calls.push("custom");
31
122
  next();
32
123
  };
124
+ const server = new McpServer({ name: "t", version: "0.0.0" });
125
+ server.use(mw);
33
126
  const httpServer = http.createServer();
34
- const app = await createApp({
35
- mcpServer: fakeServer,
36
- httpServer,
37
- customMiddleware: [{ handlers: [mw] }],
38
- });
39
- const { port, server } = await listen(app);
40
- openServer = server;
127
+ const app = await createApp({ mcpServer: server, httpServer });
128
+ const { port, server: httpListening } = await listen(app);
129
+ openServer = httpListening;
41
130
  await postMcp(port);
42
131
  expect(calls).toEqual(["custom"]);
43
132
  });
@@ -48,31 +137,30 @@ describe("createApp", () => {
48
137
  calls.push("auth");
49
138
  next();
50
139
  };
140
+ const server = new McpServer({ name: "t", version: "0.0.0" });
141
+ server.use("/mcp", mw);
51
142
  const httpServer = http.createServer();
52
- const app = await createApp({
53
- mcpServer: fakeServer,
54
- httpServer,
55
- customMiddleware: [{ path: "/mcp", handlers: [mw] }],
56
- });
57
- const { port, server } = await listen(app);
58
- openServer = server;
143
+ const app = await createApp({ mcpServer: server, httpServer });
144
+ const { port, server: httpListening } = await listen(app);
145
+ openServer = httpListening;
59
146
  await postMcp(port);
60
147
  expect(calls).toEqual(["auth"]);
61
148
  });
62
149
  it("allows middleware to short-circuit with 401", async () => {
63
150
  const { createApp } = await import("./express.js");
151
+ const calls = [];
64
152
  const reject = (_req, res) => {
153
+ calls.push("reject");
65
154
  res.status(401).json({ error: "Unauthorized" });
66
155
  };
156
+ const server = new McpServer({ name: "t", version: "0.0.0" });
157
+ server.use("/mcp", reject);
67
158
  const httpServer = http.createServer();
68
- const app = await createApp({
69
- mcpServer: fakeServer,
70
- httpServer,
71
- customMiddleware: [{ path: "/mcp", handlers: [reject] }],
72
- });
73
- const { port, server } = await listen(app);
74
- openServer = server;
159
+ const app = await createApp({ mcpServer: server, httpServer });
160
+ const { port, server: httpListening } = await listen(app);
161
+ openServer = httpListening;
75
162
  const res = await postMcp(port);
163
+ expect(calls).toEqual(["reject"]);
76
164
  expect(res.status).toBe(401);
77
165
  expect(await res.json()).toEqual({ error: "Unauthorized" });
78
166
  });
@@ -87,14 +175,13 @@ describe("createApp", () => {
87
175
  calls.push("B");
88
176
  next();
89
177
  };
178
+ const server = new McpServer({ name: "t", version: "0.0.0" });
179
+ server.use(mwA);
180
+ server.use(mwB);
90
181
  const httpServer = http.createServer();
91
- const app = await createApp({
92
- mcpServer: fakeServer,
93
- httpServer,
94
- customMiddleware: [{ handlers: [mwA] }, { handlers: [mwB] }],
95
- });
96
- const { port, server } = await listen(app);
97
- openServer = server;
182
+ const app = await createApp({ mcpServer: server, httpServer });
183
+ const { port, server: httpListening } = await listen(app);
184
+ openServer = httpListening;
98
185
  await postMcp(port);
99
186
  expect(calls).toEqual(["A", "B"]);
100
187
  });
@@ -105,14 +192,12 @@ describe("createApp", () => {
105
192
  calls.push("api");
106
193
  next();
107
194
  };
195
+ const server = new McpServer({ name: "t", version: "0.0.0" });
196
+ server.use("/api", apiMw);
108
197
  const httpServer = http.createServer();
109
- const app = await createApp({
110
- mcpServer: fakeServer,
111
- httpServer,
112
- customMiddleware: [{ path: "/api", handlers: [apiMw] }],
113
- });
114
- const { port, server } = await listen(app);
115
- openServer = server;
198
+ const app = await createApp({ mcpServer: server, httpServer });
199
+ const { port, server: httpListening } = await listen(app);
200
+ openServer = httpListening;
116
201
  // Hit /mcp — the /api middleware should NOT fire
117
202
  await postMcp(port);
118
203
  expect(calls).toEqual([]);
@@ -124,14 +209,12 @@ describe("createApp", () => {
124
209
  router.get("/health", (_req, res) => {
125
210
  res.json({ status: "ok" });
126
211
  });
212
+ const server = new McpServer({ name: "t", version: "0.0.0" });
213
+ server.use(router);
127
214
  const httpServer = http.createServer();
128
- const app = await createApp({
129
- mcpServer: fakeServer,
130
- httpServer,
131
- customMiddleware: [{ handlers: [router] }],
132
- });
133
- const { port, server } = await listen(app);
134
- openServer = server;
215
+ const app = await createApp({ mcpServer: server, httpServer });
216
+ const { port, server: httpListening } = await listen(app);
217
+ openServer = httpListening;
135
218
  const res = await fetch(`http://localhost:${port}/health`);
136
219
  expect(res.status).toBe(200);
137
220
  expect(await res.json()).toEqual({ status: "ok" });
@@ -143,16 +226,12 @@ describe("createApp", () => {
143
226
  router.get("/data", (_req, res) => {
144
227
  res.json({ value: 42 });
145
228
  });
229
+ const server = new McpServer({ name: "t", version: "0.0.0" });
230
+ server.use("/api", router);
146
231
  const httpServer = http.createServer();
147
- const app = await createApp({
148
- mcpServer: fakeServer,
149
- httpServer,
150
- customMiddleware: [
151
- { path: "/api", handlers: [router] },
152
- ],
153
- });
154
- const { port, server } = await listen(app);
155
- openServer = server;
232
+ const app = await createApp({ mcpServer: server, httpServer });
233
+ const { port, server: httpListening } = await listen(app);
234
+ openServer = httpListening;
156
235
  const res = await fetch(`http://localhost:${port}/api/data`);
157
236
  expect(res.status).toBe(200);
158
237
  expect(await res.json()).toEqual({ value: 42 });
@@ -162,19 +241,190 @@ describe("createApp", () => {
162
241
  const throwing = () => {
163
242
  throw new Error("boom");
164
243
  };
244
+ const server = new McpServer({ name: "t", version: "0.0.0" });
245
+ server.use("/explode", throwing);
165
246
  const httpServer = http.createServer();
166
- const app = await createApp({
167
- mcpServer: fakeServer,
168
- httpServer,
169
- customMiddleware: [{ path: "/explode", handlers: [throwing] }],
170
- });
171
- const { port, server } = await listen(app);
172
- openServer = server;
247
+ const app = await createApp({ mcpServer: server, httpServer });
248
+ const { port, server: httpListening } = await listen(app);
249
+ openServer = httpListening;
173
250
  const res = await fetch(`http://localhost:${port}/explode`);
174
251
  expect(res.status).toBe(500);
175
252
  // Server process did not crash — it still accepts connections
176
253
  const followUp = await fetch(`http://localhost:${port}/explode`);
177
254
  expect(followUp.status).toBe(500);
178
255
  });
256
+ it("returns 500 JSON-RPC error when the MCP handler throws and no error middleware is registered", async () => {
257
+ const { createApp } = await import("./express.js");
258
+ const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => { });
259
+ const mcpServer = new McpServer({ name: "t", version: "0.0.0" });
260
+ // Force the express-level error path: make connectStatelessTransport
261
+ // reject so the request handler hits its try/catch and calls next(error),
262
+ // which lands in the default /mcp error handler.
263
+ vi.spyOn(mcpServer, "connectStatelessTransport").mockRejectedValue(new Error("boom"));
264
+ const httpServer = http.createServer();
265
+ const app = await createApp({ mcpServer, httpServer });
266
+ const { port, server: httpListening } = await listen(app);
267
+ openServer = httpListening;
268
+ const res = await postMcp(port);
269
+ expect(res.status).toBe(500);
270
+ expect(await res.json()).toEqual({
271
+ jsonrpc: "2.0",
272
+ error: { code: -32603, message: "Internal server error" },
273
+ id: null,
274
+ });
275
+ expect(consoleSpy).toHaveBeenCalledWith("Error handling MCP request:", expect.any(Error));
276
+ consoleSpy.mockRestore();
277
+ });
278
+ it("invokes a custom error handler when the MCP handler throws", async () => {
279
+ const { createApp } = await import("./express.js");
280
+ const calls = [];
281
+ const errorHandler = (_err, _req, res, _next) => {
282
+ calls.push("error-handler");
283
+ res.status(503).json({ custom: true });
284
+ };
285
+ const mcpServer = new McpServer({ name: "t", version: "0.0.0" });
286
+ vi.spyOn(mcpServer, "connectStatelessTransport").mockRejectedValue(new Error("boom"));
287
+ const httpServer = http.createServer();
288
+ const app = await createApp({
289
+ mcpServer,
290
+ httpServer,
291
+ errorMiddleware: [{ handlers: [errorHandler] }],
292
+ });
293
+ const { port, server: httpListening } = await listen(app);
294
+ openServer = httpListening;
295
+ const res = await postMcp(port);
296
+ expect(calls).toEqual(["error-handler"]);
297
+ expect(res.status).toBe(503);
298
+ expect(await res.json()).toEqual({ custom: true });
299
+ });
300
+ it("invokes a path-scoped error handler only for matching routes", async () => {
301
+ const { createApp } = await import("./express.js");
302
+ const calls = [];
303
+ const mcpErrorHandler = (_err, _req, res, _next) => {
304
+ calls.push("mcp-error-handler");
305
+ res.status(503).json({ from: "mcp-error-handler" });
306
+ };
307
+ const throwingApiRoute = (_req, _res, next) => {
308
+ next(new Error("api error"));
309
+ };
310
+ const mcpServer = new McpServer({ name: "t", version: "0.0.0" });
311
+ vi.spyOn(mcpServer, "connectStatelessTransport").mockRejectedValue(new Error("boom"));
312
+ mcpServer.use("/api/test", throwingApiRoute);
313
+ const httpServer = http.createServer();
314
+ const app = await createApp({
315
+ mcpServer,
316
+ httpServer,
317
+ errorMiddleware: [{ path: "/mcp", handlers: [mcpErrorHandler] }],
318
+ });
319
+ const { port, server: httpListening } = await listen(app);
320
+ openServer = httpListening;
321
+ const mcpRes = await postMcp(port);
322
+ expect(calls).toEqual(["mcp-error-handler"]);
323
+ expect(mcpRes.status).toBe(503);
324
+ expect(await mcpRes.json()).toEqual({ from: "mcp-error-handler" });
325
+ const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => { });
326
+ const apiRes = await postApi(port);
327
+ expect(calls).toEqual(["mcp-error-handler"]);
328
+ expect(apiRes.status).toBe(500);
329
+ consoleSpy.mockRestore();
330
+ });
331
+ it("handles concurrent /mcp requests without 'Already connected to a transport'", async () => {
332
+ const { createApp } = await import("./express.js");
333
+ const mcpServer = new McpServer({
334
+ name: "concurrent-test",
335
+ version: "0.0.0",
336
+ });
337
+ // Slow tool: keeps the underlying transport bound long enough to overlap
338
+ // with concurrent requests, exposing the shared-McpServer race.
339
+ mcpServer.registerTool({ name: "slow", description: "slow" }, async () => {
340
+ await new Promise((r) => setTimeout(r, 50));
341
+ return { content: [{ type: "text", text: "done" }] };
342
+ });
343
+ const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => { });
344
+ const httpServer = http.createServer();
345
+ const app = await createApp({ mcpServer, httpServer });
346
+ const { port, server } = await listen(app);
347
+ openServer = server;
348
+ const callBody = (id) => JSON.stringify({
349
+ jsonrpc: "2.0",
350
+ method: "tools/call",
351
+ id,
352
+ params: { name: "slow", arguments: {} },
353
+ });
354
+ const N = 10;
355
+ const responses = await Promise.all(Array.from({ length: N }, (_, i) => fetch(`http://localhost:${port}/mcp`, {
356
+ method: "POST",
357
+ headers: {
358
+ "Content-Type": "application/json",
359
+ Accept: "application/json, text/event-stream",
360
+ },
361
+ body: callBody(i + 1),
362
+ })));
363
+ expect(responses.map((r) => r.status)).toEqual(Array(N).fill(200));
364
+ expect(consoleSpy).not.toHaveBeenCalledWith("Error handling MCP request:", expect.any(Error));
365
+ consoleSpy.mockRestore();
366
+ });
367
+ });
368
+ describe("createApp tunnel routes", () => {
369
+ it("proxies POST /__skybridge/tunnel to the cli control server in dev mode", async () => {
370
+ // Stand up a fake control listener that returns a known JSON body.
371
+ const control = http.createServer((_req, res) => {
372
+ res.writeHead(200, { "Content-Type": "application/json" });
373
+ res.end('{"status":"idle"}');
374
+ });
375
+ await new Promise((resolve) => control.listen(0, "127.0.0.1", resolve));
376
+ const controlAddr = control.address();
377
+ if (typeof controlAddr === "string" || controlAddr === null) {
378
+ control.close();
379
+ throw new Error("control server has no address");
380
+ }
381
+ const controlPort = controlAddr.port;
382
+ const prev = process.env.__TUNNEL_CONTROL_PORT;
383
+ process.env.__TUNNEL_CONTROL_PORT = String(controlPort);
384
+ try {
385
+ const { createApp } = await import("./express.js");
386
+ const mcpServer = new McpServer({ name: "t", version: "0.0.0" });
387
+ const httpServer = http.createServer();
388
+ const app = await createApp({ mcpServer, httpServer });
389
+ const { port, server } = await listen(app);
390
+ openServer = server;
391
+ const res = await fetch(`http://localhost:${port}/__skybridge/tunnel`, {
392
+ method: "POST",
393
+ });
394
+ expect(res.status).toBe(200);
395
+ expect(await res.json()).toEqual({ status: "idle" });
396
+ }
397
+ finally {
398
+ if (prev === undefined) {
399
+ delete process.env.__TUNNEL_CONTROL_PORT;
400
+ }
401
+ else {
402
+ process.env.__TUNNEL_CONTROL_PORT = prev;
403
+ }
404
+ await new Promise((resolve) => control.close(() => resolve()));
405
+ }
406
+ });
407
+ it("does not expose /__skybridge/tunnel in production mode", async () => {
408
+ const prevEnv = process.env.NODE_ENV;
409
+ process.env.NODE_ENV = "production";
410
+ try {
411
+ vi.resetModules();
412
+ const { createApp } = await import("./express.js");
413
+ const { McpServer: ReloadedMcpServer } = await import("./server.js");
414
+ const mcpServer = new ReloadedMcpServer({ name: "t", version: "0.0.0" });
415
+ const httpServer = http.createServer();
416
+ const app = await createApp({ mcpServer, httpServer });
417
+ const { port, server } = await listen(app);
418
+ openServer = server;
419
+ const res = await fetch(`http://localhost:${port}/__skybridge/tunnel`, {
420
+ method: "POST",
421
+ });
422
+ expect(res.status).toBe(404);
423
+ }
424
+ finally {
425
+ process.env.NODE_ENV = prevEnv;
426
+ vi.resetModules();
427
+ }
428
+ });
179
429
  });
180
430
  //# sourceMappingURL=express.test.js.map