skybridge 0.0.0-dev.eb9ddab → 0.0.0-dev.ebe4332
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 +123 -120
- package/dist/cli/detect-port.js.map +1 -1
- package/dist/cli/header.js +1 -1
- package/dist/cli/header.js.map +1 -1
- package/dist/cli/run-command.js.map +1 -1
- package/dist/cli/telemetry.js.map +1 -1
- package/dist/cli/tunnel-control-server.d.ts +9 -0
- package/dist/cli/tunnel-control-server.js +31 -0
- package/dist/cli/tunnel-control-server.js.map +1 -0
- package/dist/cli/tunnel-control-server.test.js +39 -0
- package/dist/cli/tunnel-control-server.test.js.map +1 -0
- package/dist/cli/tunnel-handler.d.ts +3 -0
- package/dist/cli/tunnel-handler.js +48 -0
- package/dist/cli/tunnel-handler.js.map +1 -0
- package/dist/cli/tunnel-handler.test.js +105 -0
- package/dist/cli/tunnel-handler.test.js.map +1 -0
- package/dist/cli/tunnel.d.ts +57 -0
- package/dist/cli/tunnel.js +154 -0
- package/dist/cli/tunnel.js.map +1 -0
- package/dist/cli/tunnel.test.js +190 -0
- package/dist/cli/tunnel.test.js.map +1 -0
- package/dist/cli/types.d.ts +5 -0
- package/dist/cli/types.js +2 -0
- package/dist/cli/types.js.map +1 -0
- package/dist/cli/use-execute-steps.js.map +1 -1
- package/dist/cli/use-messages.d.ts +3 -0
- package/dist/cli/use-messages.js +11 -0
- package/dist/cli/use-messages.js.map +1 -0
- package/dist/cli/use-nodemon.d.ts +2 -7
- package/dist/cli/use-nodemon.js +18 -21
- package/dist/cli/use-nodemon.js.map +1 -1
- package/dist/cli/use-open-browser.d.ts +1 -0
- package/dist/cli/use-open-browser.js +44 -0
- package/dist/cli/use-open-browser.js.map +1 -0
- package/dist/cli/use-tunnel.d.ts +14 -0
- package/dist/cli/use-tunnel.js +131 -0
- package/dist/cli/use-tunnel.js.map +1 -0
- package/dist/cli/use-typescript-check.d.ts +1 -0
- package/dist/cli/use-typescript-check.js +41 -6
- package/dist/cli/use-typescript-check.js.map +1 -1
- package/dist/commands/build.js +63 -7
- package/dist/commands/build.js.map +1 -1
- package/dist/commands/dev.d.ts +3 -0
- package/dist/commands/dev.js +47 -3
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/start.js +7 -10
- package/dist/commands/start.js.map +1 -1
- package/dist/commands/telemetry/disable.js.map +1 -1
- package/dist/commands/telemetry/enable.js.map +1 -1
- package/dist/commands/telemetry/status.js.map +1 -1
- package/dist/server/asset-base-url-transform-plugin.d.ts +5 -6
- package/dist/server/asset-base-url-transform-plugin.js +9 -10
- package/dist/server/asset-base-url-transform-plugin.js.map +1 -1
- package/dist/server/asset-base-url-transform-plugin.test.js +41 -13
- package/dist/server/asset-base-url-transform-plugin.test.js.map +1 -1
- package/dist/server/content-helpers.d.ts +27 -0
- package/dist/server/content-helpers.js +46 -0
- package/dist/server/content-helpers.js.map +1 -0
- package/dist/server/content-helpers.test.d.ts +1 -0
- package/dist/server/content-helpers.test.js +70 -0
- package/dist/server/content-helpers.test.js.map +1 -0
- package/dist/server/express.d.ts +2 -6
- package/dist/server/express.js +34 -10
- package/dist/server/express.js.map +1 -1
- package/dist/server/express.test.js +249 -71
- package/dist/server/express.test.js.map +1 -1
- package/dist/server/file-ref.d.ts +8 -0
- package/dist/server/file-ref.js +8 -0
- package/dist/server/file-ref.js.map +1 -0
- package/dist/server/index.d.ts +5 -3
- package/dist/server/index.js +4 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/inferUtilityTypes.d.ts +6 -6
- package/dist/server/inferUtilityTypes.js.map +1 -1
- package/dist/server/metric.d.ts +14 -0
- package/dist/server/metric.js +62 -0
- package/dist/server/metric.js.map +1 -0
- package/dist/server/middleware.js.map +1 -1
- package/dist/server/middleware.test-d.js.map +1 -1
- package/dist/server/middleware.test.js +12 -9
- package/dist/server/middleware.test.js.map +1 -1
- package/dist/server/server.d.ts +125 -76
- package/dist/server/server.js +262 -82
- package/dist/server/server.js.map +1 -1
- package/dist/server/templateHelper.d.ts +5 -7
- package/dist/server/templateHelper.js +3 -22
- package/dist/server/templateHelper.js.map +1 -1
- package/dist/server/templates.generated.d.ts +4 -0
- package/dist/server/templates.generated.js +47 -0
- package/dist/server/templates.generated.js.map +1 -0
- package/dist/server/tunnel-proxy-router.d.ts +7 -0
- package/dist/server/tunnel-proxy-router.js +110 -0
- package/dist/server/tunnel-proxy-router.js.map +1 -0
- package/dist/server/tunnel-proxy-router.test.d.ts +1 -0
- package/dist/server/tunnel-proxy-router.test.js +229 -0
- package/dist/server/tunnel-proxy-router.test.js.map +1 -0
- package/dist/server/viewsDevServer.d.ts +14 -0
- package/dist/server/viewsDevServer.js +45 -0
- package/dist/server/viewsDevServer.js.map +1 -0
- package/dist/test/utils.d.ts +13 -21
- package/dist/test/utils.js +42 -37
- package/dist/test/utils.js.map +1 -1
- package/dist/test/view.test.d.ts +1 -0
- package/dist/test/view.test.js +523 -0
- package/dist/test/view.test.js.map +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/dist/version.js.map +1 -0
- package/dist/web/bridges/apps-sdk/adaptor.d.ts +8 -4
- package/dist/web/bridges/apps-sdk/adaptor.js +50 -17
- package/dist/web/bridges/apps-sdk/adaptor.js.map +1 -1
- package/dist/web/bridges/apps-sdk/bridge.js.map +1 -1
- package/dist/web/bridges/apps-sdk/index.js.map +1 -1
- package/dist/web/bridges/apps-sdk/types.d.ts +18 -6
- package/dist/web/bridges/apps-sdk/types.js.map +1 -1
- package/dist/web/bridges/apps-sdk/use-apps-sdk-context.js.map +1 -1
- package/dist/web/bridges/get-adaptor.js.map +1 -1
- package/dist/web/bridges/index.js.map +1 -1
- package/dist/web/bridges/mcp-app/adaptor.d.ts +18 -6
- package/dist/web/bridges/mcp-app/adaptor.js +115 -28
- package/dist/web/bridges/mcp-app/adaptor.js.map +1 -1
- package/dist/web/bridges/mcp-app/bridge.js.map +1 -1
- package/dist/web/bridges/mcp-app/index.js.map +1 -1
- package/dist/web/bridges/mcp-app/types.js.map +1 -1
- package/dist/web/bridges/mcp-app/use-mcp-app-context.js.map +1 -1
- package/dist/web/bridges/mcp-app/use-mcp-app-context.test.js.map +1 -1
- package/dist/web/bridges/types.d.ts +24 -10
- package/dist/web/bridges/types.js.map +1 -1
- package/dist/web/bridges/use-host-context.js.map +1 -1
- package/dist/web/components/modal-provider.js +2 -2
- package/dist/web/components/modal-provider.js.map +1 -1
- package/dist/web/create-store.js +17 -3
- package/dist/web/create-store.js.map +1 -1
- package/dist/web/create-store.test.js +14 -16
- package/dist/web/create-store.test.js.map +1 -1
- package/dist/web/data-llm.d.ts +1 -1
- package/dist/web/data-llm.js +3 -3
- package/dist/web/data-llm.js.map +1 -1
- package/dist/web/data-llm.test.js +22 -22
- package/dist/web/data-llm.test.js.map +1 -1
- package/dist/web/generate-helpers.d.ts +20 -18
- package/dist/web/generate-helpers.js +20 -18
- package/dist/web/generate-helpers.js.map +1 -1
- package/dist/web/generate-helpers.test-d.js +26 -26
- package/dist/web/generate-helpers.test-d.js.map +1 -1
- package/dist/web/generate-helpers.test.js.map +1 -1
- package/dist/web/helpers/state.d.ts +2 -2
- package/dist/web/helpers/state.js +11 -11
- package/dist/web/helpers/state.js.map +1 -1
- package/dist/web/helpers/state.test.js +9 -9
- package/dist/web/helpers/state.test.js.map +1 -1
- package/dist/web/hooks/index.d.ts +3 -1
- package/dist/web/hooks/index.js +3 -1
- package/dist/web/hooks/index.js.map +1 -1
- package/dist/web/hooks/test/utils.js.map +1 -1
- package/dist/web/hooks/use-call-tool.js.map +1 -1
- package/dist/web/hooks/use-call-tool.test-d.js.map +1 -1
- package/dist/web/hooks/use-call-tool.test.js +27 -6
- package/dist/web/hooks/use-call-tool.test.js.map +1 -1
- package/dist/web/hooks/use-display-mode.js.map +1 -1
- package/dist/web/hooks/use-display-mode.test-d.js.map +1 -1
- package/dist/web/hooks/use-display-mode.test.js.map +1 -1
- package/dist/web/hooks/use-files.d.ts +2 -1
- package/dist/web/hooks/use-files.js +1 -0
- package/dist/web/hooks/use-files.js.map +1 -1
- package/dist/web/hooks/use-files.test.js +22 -2
- package/dist/web/hooks/use-files.test.js.map +1 -1
- package/dist/web/hooks/use-layout.js.map +1 -1
- package/dist/web/hooks/use-layout.test.js.map +1 -1
- package/dist/web/hooks/use-open-external.js.map +1 -1
- package/dist/web/hooks/use-open-external.test.js.map +1 -1
- package/dist/web/hooks/use-request-close.d.ts +2 -0
- package/dist/web/hooks/use-request-close.js +8 -0
- package/dist/web/hooks/use-request-close.js.map +1 -0
- package/dist/web/hooks/use-request-close.test.d.ts +1 -0
- package/dist/web/hooks/use-request-close.test.js +52 -0
- package/dist/web/hooks/use-request-close.test.js.map +1 -0
- package/dist/web/hooks/use-request-modal.d.ts +1 -1
- package/dist/web/hooks/use-request-modal.js +4 -4
- package/dist/web/hooks/use-request-modal.js.map +1 -1
- package/dist/web/hooks/use-request-modal.test.js +1 -1
- package/dist/web/hooks/use-request-modal.test.js.map +1 -1
- package/dist/web/hooks/use-request-size.d.ts +3 -0
- package/dist/web/hooks/use-request-size.js +8 -0
- package/dist/web/hooks/use-request-size.js.map +1 -0
- package/dist/web/hooks/use-request-size.test.d.ts +1 -0
- package/dist/web/hooks/use-request-size.test.js +65 -0
- package/dist/web/hooks/use-request-size.test.js.map +1 -0
- package/dist/web/hooks/use-send-follow-up-message.d.ts +2 -1
- package/dist/web/hooks/use-send-follow-up-message.js +2 -2
- package/dist/web/hooks/use-send-follow-up-message.js.map +1 -1
- package/dist/web/hooks/use-set-open-in-app-url.js.map +1 -1
- package/dist/web/hooks/use-set-open-in-app-url.test.js.map +1 -1
- package/dist/web/hooks/use-tool-info.js.map +1 -1
- package/dist/web/hooks/use-tool-info.test-d.js.map +1 -1
- package/dist/web/hooks/use-tool-info.test.js.map +1 -1
- package/dist/web/hooks/use-user.js.map +1 -1
- package/dist/web/hooks/use-user.test.js.map +1 -1
- package/dist/web/hooks/use-view-state.d.ts +4 -0
- package/dist/web/hooks/use-view-state.js +32 -0
- package/dist/web/hooks/use-view-state.js.map +1 -0
- package/dist/web/hooks/use-view-state.test.d.ts +1 -0
- package/dist/web/hooks/use-view-state.test.js +177 -0
- package/dist/web/hooks/use-view-state.test.js.map +1 -0
- package/dist/web/index.d.ts +1 -2
- package/dist/web/index.js +1 -2
- package/dist/web/index.js.map +1 -1
- package/dist/web/mount-view.d.ts +1 -0
- package/dist/web/{mount-widget.js → mount-view.js} +2 -2
- package/dist/web/mount-view.js.map +1 -0
- package/dist/web/plugin/data-llm.test.js.map +1 -1
- package/dist/web/plugin/plugin.d.ts +4 -1
- package/dist/web/plugin/plugin.js +134 -25
- package/dist/web/plugin/plugin.js.map +1 -1
- package/dist/web/plugin/scan-views.d.ts +16 -0
- package/dist/web/plugin/scan-views.js +88 -0
- package/dist/web/plugin/scan-views.js.map +1 -0
- package/dist/web/plugin/scan-views.test.d.ts +1 -0
- package/dist/web/plugin/scan-views.test.js +99 -0
- package/dist/web/plugin/scan-views.test.js.map +1 -0
- package/dist/web/plugin/transform-data-llm.js +1 -1
- package/dist/web/plugin/transform-data-llm.js.map +1 -1
- package/dist/web/plugin/transform-data-llm.test.js.map +1 -1
- package/dist/web/plugin/validate-view.d.ts +1 -0
- package/dist/web/plugin/validate-view.js +9 -0
- package/dist/web/plugin/validate-view.js.map +1 -0
- package/dist/web/plugin/validate-view.test.d.ts +1 -0
- package/dist/web/plugin/validate-view.test.js +24 -0
- package/dist/web/plugin/validate-view.test.js.map +1 -0
- package/dist/web/proxy.js.map +1 -1
- package/dist/web/types.js.map +1 -1
- package/package.json +33 -23
- package/tsconfig.base.json +2 -0
- package/dist/server/templates/development.hbs +0 -12
- package/dist/server/templates/production.hbs +0 -6
- package/dist/server/widgetsDevServer.d.ts +0 -13
- package/dist/server/widgetsDevServer.js +0 -57
- package/dist/server/widgetsDevServer.js.map +0 -1
- package/dist/test/widget.test.js +0 -263
- package/dist/test/widget.test.js.map +0 -1
- package/dist/web/hooks/use-widget-state.d.ts +0 -4
- package/dist/web/hooks/use-widget-state.js +0 -32
- package/dist/web/hooks/use-widget-state.js.map +0 -1
- package/dist/web/hooks/use-widget-state.test.js +0 -64
- package/dist/web/hooks/use-widget-state.test.js.map +0 -1
- package/dist/web/mount-widget.d.ts +0 -1
- package/dist/web/mount-widget.js.map +0 -1
- package/dist/web/plugin/validate-widget.d.ts +0 -5
- package/dist/web/plugin/validate-widget.js +0 -27
- package/dist/web/plugin/validate-widget.js.map +0 -1
- package/dist/web/plugin/validate-widget.test.js +0 -42
- package/dist/web/plugin/validate-widget.test.js.map +0 -1
- /package/dist/{test/widget.test.d.ts → cli/tunnel-control-server.test.d.ts} +0 -0
- /package/dist/{web/hooks/use-widget-state.test.d.ts → cli/tunnel-handler.test.d.ts} +0 -0
- /package/dist/{web/plugin/validate-widget.test.d.ts → cli/tunnel.test.d.ts} +0 -0
package/dist/server/express.js
CHANGED
|
@@ -2,6 +2,16 @@ 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
|
+
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
|
+
}
|
|
5
15
|
function applyMiddlewares(app, middlewares) {
|
|
6
16
|
for (const middleware of middlewares) {
|
|
7
17
|
if (middleware.path) {
|
|
@@ -22,18 +32,25 @@ function defaultErrorHandler(err, _req, res, _next) {
|
|
|
22
32
|
});
|
|
23
33
|
}
|
|
24
34
|
}
|
|
25
|
-
export async function createApp({ mcpServer, httpServer,
|
|
26
|
-
const app = express
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
if (env !== "production") {
|
|
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") {
|
|
31
40
|
const { devtoolsStaticServer } = await import("@skybridge/devtools");
|
|
32
41
|
app.use(await devtoolsStaticServer());
|
|
33
|
-
const {
|
|
34
|
-
app.use(await
|
|
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
|
+
}
|
|
35
52
|
}
|
|
36
|
-
|
|
53
|
+
else {
|
|
37
54
|
const assetsPath = path.join(process.cwd(), "dist", "assets");
|
|
38
55
|
app.use("/assets", cors());
|
|
39
56
|
app.use("/assets", express.static(assetsPath));
|
|
@@ -59,11 +76,18 @@ const mcpMiddleware = (server) => {
|
|
|
59
76
|
try {
|
|
60
77
|
const transport = new StreamableHTTPServerTransport({
|
|
61
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,
|
|
62
86
|
});
|
|
63
87
|
res.on("close", () => {
|
|
64
88
|
transport.close();
|
|
65
89
|
});
|
|
66
|
-
await server.
|
|
90
|
+
await server.connectStatelessTransport(transport);
|
|
67
91
|
// Express strips the mount path from req.url (e.g. "/mcp" becomes "/").
|
|
68
92
|
// Restore it so the SDK builds the correct requestInfo.url.
|
|
69
93
|
req.url = req.originalUrl;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"express.js","sourceRoot":"","sources":["../../src/server/express.ts"],"names":[],"mappings":"AACA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,6BAA6B,EAAE,MAAM,oDAAoD,CAAC;AACnG,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,OAAO,MAAM,SAAS,CAAC;AAG9B,SAAS,gBAAgB,CACvB,GAAoB,EACpB,WAGE;IAEF,KAAK,MAAM,UAAU,IAAI,WAAW,EAAE,CAAC;QACrC,IAAI,UAAU,CAAC,IAAI,EAAE,CAAC;YACpB,GAAG,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,EAAE,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;QACnD,CAAC;aAAM,CAAC;YACN,GAAG,CAAC,GAAG,CAAC,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC;QAClC,CAAC;IACH,CAAC;AACH,CAAC;AAED,SAAS,mBAAmB,CAC1B,GAAY,EACZ,IAAqB,EACrB,GAAqB,EACrB,KAA2B;IAE3B,OAAO,CAAC,KAAK,CAAC,6BAA6B,EAAE,GAAG,CAAC,CAAC;IAClD,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;QACrB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;YACnB,OAAO,EAAE,KAAK;YACd,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,uBAAuB,EAAE;YACzD,EAAE,EAAE,IAAI;SACT,CAAC,CAAC;IACL,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,EAC9B,SAAS,EACT,UAAU,EACV,
|
|
1
|
+
{"version":3,"file":"express.js","sourceRoot":"","sources":["../../src/server/express.ts"],"names":[],"mappings":"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","sourcesContent":["import type http from \"node:http\";\nimport path from \"node:path\";\nimport { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\";\nimport cors from \"cors\";\nimport express from \"express\";\nimport type { McpServer } from \"./server.js\";\n\nfunction parseControlPort(raw: string | undefined): number | null {\n if (raw === undefined) {\n return null;\n }\n const n = Number.parseInt(raw, 10);\n if (!Number.isFinite(n) || n <= 0 || n >= 65536) {\n return null;\n }\n return n;\n}\n\nfunction applyMiddlewares(\n app: express.Express,\n middlewares: Array<{\n path?: string;\n handlers: express.ErrorRequestHandler[];\n }>,\n): void {\n for (const middleware of middlewares) {\n if (middleware.path) {\n app.use(middleware.path, ...middleware.handlers);\n } else {\n app.use(...middleware.handlers);\n }\n }\n}\n\nfunction defaultErrorHandler(\n err: unknown,\n _req: express.Request,\n res: express.Response,\n _next: express.NextFunction,\n) {\n console.error(\"Error handling MCP request:\", err);\n if (!res.headersSent) {\n res.status(500).json({\n jsonrpc: \"2.0\",\n error: { code: -32603, message: \"Internal server error\" },\n id: null,\n });\n }\n}\n\nexport async function createApp({\n mcpServer,\n httpServer,\n errorMiddleware = [],\n}: {\n mcpServer: McpServer;\n httpServer: http.Server;\n errorMiddleware?: {\n path?: string;\n handlers: express.ErrorRequestHandler[];\n }[];\n}): Promise<express.Express> {\n const app = mcpServer.express;\n\n // Read `process.env.NODE_ENV` inline: wrangler/esbuild only substitute the literal expression,\n // so a local const would defeat dead-code elimination of the dev-only imports below.\n if (process.env.NODE_ENV !== \"production\") {\n const { devtoolsStaticServer } = await import(\"@skybridge/devtools\");\n app.use(await devtoolsStaticServer());\n const { viewsDevServer } = await import(\"./viewsDevServer.js\");\n app.use(await viewsDevServer(httpServer));\n\n const controlPort = parseControlPort(process.env.__TUNNEL_CONTROL_PORT);\n if (controlPort !== null) {\n const { createTunnelProxyRouter } = await import(\n \"./tunnel-proxy-router.js\"\n );\n app.use(createTunnelProxyRouter(controlPort));\n } else if (process.env.__TUNNEL_CONTROL_PORT !== undefined) {\n console.warn(\n `Ignoring invalid __TUNNEL_CONTROL_PORT=${process.env.__TUNNEL_CONTROL_PORT}`,\n );\n }\n } else {\n const assetsPath = path.join(process.cwd(), \"dist\", \"assets\");\n\n app.use(\"/assets\", cors());\n app.use(\"/assets\", express.static(assetsPath));\n }\n\n app.use(\"/mcp\", mcpMiddleware(mcpServer));\n\n applyMiddlewares(app, errorMiddleware);\n\n app.use(\"/mcp\", defaultErrorHandler);\n\n return app;\n}\n\nconst mcpMiddleware = (server: McpServer): express.RequestHandler => {\n return async (\n req: express.Request,\n res: express.Response,\n next: express.NextFunction,\n ) => {\n if (req.method !== \"POST\") {\n res.writeHead(405).end(\n JSON.stringify({\n jsonrpc: \"2.0\",\n error: {\n code: -32000,\n message: \"Method not allowed.\",\n },\n id: null,\n }),\n );\n return;\n }\n\n try {\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n // Respond with a single JSON body instead of SSE. Skybridge's stateless\n // transport never streams server-initiated messages, so SSE adds no\n // capability — and on workerd specifically, `cloudflare:node`'s http\n // bridge silently drops chunked writes that happen after the request\n // handler awaits, which manifests as a 200 with empty body for any\n // async tools/call.\n enableJsonResponse: true,\n });\n\n res.on(\"close\", () => {\n transport.close();\n });\n\n await server.connectStatelessTransport(transport);\n // Express strips the mount path from req.url (e.g. \"/mcp\" becomes \"/\").\n // Restore it so the SDK builds the correct requestInfo.url.\n req.url = req.originalUrl;\n await transport.handleRequest(req, res, req.body);\n } catch (error) {\n next(error);\n }\n };\n};\n"]}
|
|
@@ -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("./
|
|
7
|
-
|
|
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));
|
|
@@ -25,6 +25,94 @@ async function postMcp(port) {
|
|
|
25
25
|
async function postApi(port) {
|
|
26
26
|
return fetch(`http://localhost:${port}/api/test`, { method: "POST" });
|
|
27
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
|
+
});
|
|
28
116
|
describe("createApp", () => {
|
|
29
117
|
it("runs global custom middleware before the /mcp handler", async () => {
|
|
30
118
|
const { createApp } = await import("./express.js");
|
|
@@ -33,14 +121,12 @@ describe("createApp", () => {
|
|
|
33
121
|
calls.push("custom");
|
|
34
122
|
next();
|
|
35
123
|
};
|
|
124
|
+
const server = new McpServer({ name: "t", version: "0.0.0" });
|
|
125
|
+
server.use(mw);
|
|
36
126
|
const httpServer = http.createServer();
|
|
37
|
-
const app = await createApp({
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
customMiddleware: [{ handlers: [mw] }],
|
|
41
|
-
});
|
|
42
|
-
const { port, server } = await listen(app);
|
|
43
|
-
openServer = server;
|
|
127
|
+
const app = await createApp({ mcpServer: server, httpServer });
|
|
128
|
+
const { port, server: httpListening } = await listen(app);
|
|
129
|
+
openServer = httpListening;
|
|
44
130
|
await postMcp(port);
|
|
45
131
|
expect(calls).toEqual(["custom"]);
|
|
46
132
|
});
|
|
@@ -51,14 +137,12 @@ describe("createApp", () => {
|
|
|
51
137
|
calls.push("auth");
|
|
52
138
|
next();
|
|
53
139
|
};
|
|
140
|
+
const server = new McpServer({ name: "t", version: "0.0.0" });
|
|
141
|
+
server.use("/mcp", mw);
|
|
54
142
|
const httpServer = http.createServer();
|
|
55
|
-
const app = await createApp({
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
customMiddleware: [{ path: "/mcp", handlers: [mw] }],
|
|
59
|
-
});
|
|
60
|
-
const { port, server } = await listen(app);
|
|
61
|
-
openServer = server;
|
|
143
|
+
const app = await createApp({ mcpServer: server, httpServer });
|
|
144
|
+
const { port, server: httpListening } = await listen(app);
|
|
145
|
+
openServer = httpListening;
|
|
62
146
|
await postMcp(port);
|
|
63
147
|
expect(calls).toEqual(["auth"]);
|
|
64
148
|
});
|
|
@@ -69,14 +153,12 @@ describe("createApp", () => {
|
|
|
69
153
|
calls.push("reject");
|
|
70
154
|
res.status(401).json({ error: "Unauthorized" });
|
|
71
155
|
};
|
|
156
|
+
const server = new McpServer({ name: "t", version: "0.0.0" });
|
|
157
|
+
server.use("/mcp", reject);
|
|
72
158
|
const httpServer = http.createServer();
|
|
73
|
-
const app = await createApp({
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
customMiddleware: [{ path: "/mcp", handlers: [reject] }],
|
|
77
|
-
});
|
|
78
|
-
const { port, server } = await listen(app);
|
|
79
|
-
openServer = server;
|
|
159
|
+
const app = await createApp({ mcpServer: server, httpServer });
|
|
160
|
+
const { port, server: httpListening } = await listen(app);
|
|
161
|
+
openServer = httpListening;
|
|
80
162
|
const res = await postMcp(port);
|
|
81
163
|
expect(calls).toEqual(["reject"]);
|
|
82
164
|
expect(res.status).toBe(401);
|
|
@@ -93,14 +175,13 @@ describe("createApp", () => {
|
|
|
93
175
|
calls.push("B");
|
|
94
176
|
next();
|
|
95
177
|
};
|
|
178
|
+
const server = new McpServer({ name: "t", version: "0.0.0" });
|
|
179
|
+
server.use(mwA);
|
|
180
|
+
server.use(mwB);
|
|
96
181
|
const httpServer = http.createServer();
|
|
97
|
-
const app = await createApp({
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
customMiddleware: [{ handlers: [mwA] }, { handlers: [mwB] }],
|
|
101
|
-
});
|
|
102
|
-
const { port, server } = await listen(app);
|
|
103
|
-
openServer = server;
|
|
182
|
+
const app = await createApp({ mcpServer: server, httpServer });
|
|
183
|
+
const { port, server: httpListening } = await listen(app);
|
|
184
|
+
openServer = httpListening;
|
|
104
185
|
await postMcp(port);
|
|
105
186
|
expect(calls).toEqual(["A", "B"]);
|
|
106
187
|
});
|
|
@@ -111,14 +192,12 @@ describe("createApp", () => {
|
|
|
111
192
|
calls.push("api");
|
|
112
193
|
next();
|
|
113
194
|
};
|
|
195
|
+
const server = new McpServer({ name: "t", version: "0.0.0" });
|
|
196
|
+
server.use("/api", apiMw);
|
|
114
197
|
const httpServer = http.createServer();
|
|
115
|
-
const app = await createApp({
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
customMiddleware: [{ path: "/api", handlers: [apiMw] }],
|
|
119
|
-
});
|
|
120
|
-
const { port, server } = await listen(app);
|
|
121
|
-
openServer = server;
|
|
198
|
+
const app = await createApp({ mcpServer: server, httpServer });
|
|
199
|
+
const { port, server: httpListening } = await listen(app);
|
|
200
|
+
openServer = httpListening;
|
|
122
201
|
// Hit /mcp — the /api middleware should NOT fire
|
|
123
202
|
await postMcp(port);
|
|
124
203
|
expect(calls).toEqual([]);
|
|
@@ -130,14 +209,12 @@ describe("createApp", () => {
|
|
|
130
209
|
router.get("/health", (_req, res) => {
|
|
131
210
|
res.json({ status: "ok" });
|
|
132
211
|
});
|
|
212
|
+
const server = new McpServer({ name: "t", version: "0.0.0" });
|
|
213
|
+
server.use(router);
|
|
133
214
|
const httpServer = http.createServer();
|
|
134
|
-
const app = await createApp({
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
customMiddleware: [{ handlers: [router] }],
|
|
138
|
-
});
|
|
139
|
-
const { port, server } = await listen(app);
|
|
140
|
-
openServer = server;
|
|
215
|
+
const app = await createApp({ mcpServer: server, httpServer });
|
|
216
|
+
const { port, server: httpListening } = await listen(app);
|
|
217
|
+
openServer = httpListening;
|
|
141
218
|
const res = await fetch(`http://localhost:${port}/health`);
|
|
142
219
|
expect(res.status).toBe(200);
|
|
143
220
|
expect(await res.json()).toEqual({ status: "ok" });
|
|
@@ -149,16 +226,12 @@ describe("createApp", () => {
|
|
|
149
226
|
router.get("/data", (_req, res) => {
|
|
150
227
|
res.json({ value: 42 });
|
|
151
228
|
});
|
|
229
|
+
const server = new McpServer({ name: "t", version: "0.0.0" });
|
|
230
|
+
server.use("/api", router);
|
|
152
231
|
const httpServer = http.createServer();
|
|
153
|
-
const app = await createApp({
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
customMiddleware: [
|
|
157
|
-
{ path: "/api", handlers: [router] },
|
|
158
|
-
],
|
|
159
|
-
});
|
|
160
|
-
const { port, server } = await listen(app);
|
|
161
|
-
openServer = server;
|
|
232
|
+
const app = await createApp({ mcpServer: server, httpServer });
|
|
233
|
+
const { port, server: httpListening } = await listen(app);
|
|
234
|
+
openServer = httpListening;
|
|
162
235
|
const res = await fetch(`http://localhost:${port}/api/data`);
|
|
163
236
|
expect(res.status).toBe(200);
|
|
164
237
|
expect(await res.json()).toEqual({ value: 42 });
|
|
@@ -168,14 +241,12 @@ describe("createApp", () => {
|
|
|
168
241
|
const throwing = () => {
|
|
169
242
|
throw new Error("boom");
|
|
170
243
|
};
|
|
244
|
+
const server = new McpServer({ name: "t", version: "0.0.0" });
|
|
245
|
+
server.use("/explode", throwing);
|
|
171
246
|
const httpServer = http.createServer();
|
|
172
|
-
const app = await createApp({
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
customMiddleware: [{ path: "/explode", handlers: [throwing] }],
|
|
176
|
-
});
|
|
177
|
-
const { port, server } = await listen(app);
|
|
178
|
-
openServer = server;
|
|
247
|
+
const app = await createApp({ mcpServer: server, httpServer });
|
|
248
|
+
const { port, server: httpListening } = await listen(app);
|
|
249
|
+
openServer = httpListening;
|
|
179
250
|
const res = await fetch(`http://localhost:${port}/explode`);
|
|
180
251
|
expect(res.status).toBe(500);
|
|
181
252
|
// Server process did not crash — it still accepts connections
|
|
@@ -185,10 +256,15 @@ describe("createApp", () => {
|
|
|
185
256
|
it("returns 500 JSON-RPC error when the MCP handler throws and no error middleware is registered", async () => {
|
|
186
257
|
const { createApp } = await import("./express.js");
|
|
187
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"));
|
|
188
264
|
const httpServer = http.createServer();
|
|
189
|
-
const app = await createApp({ mcpServer
|
|
190
|
-
const { port, server } = await listen(app);
|
|
191
|
-
openServer =
|
|
265
|
+
const app = await createApp({ mcpServer, httpServer });
|
|
266
|
+
const { port, server: httpListening } = await listen(app);
|
|
267
|
+
openServer = httpListening;
|
|
192
268
|
const res = await postMcp(port);
|
|
193
269
|
expect(res.status).toBe(500);
|
|
194
270
|
expect(await res.json()).toEqual({
|
|
@@ -206,14 +282,16 @@ describe("createApp", () => {
|
|
|
206
282
|
calls.push("error-handler");
|
|
207
283
|
res.status(503).json({ custom: true });
|
|
208
284
|
};
|
|
285
|
+
const mcpServer = new McpServer({ name: "t", version: "0.0.0" });
|
|
286
|
+
vi.spyOn(mcpServer, "connectStatelessTransport").mockRejectedValue(new Error("boom"));
|
|
209
287
|
const httpServer = http.createServer();
|
|
210
288
|
const app = await createApp({
|
|
211
|
-
mcpServer
|
|
289
|
+
mcpServer,
|
|
212
290
|
httpServer,
|
|
213
291
|
errorMiddleware: [{ handlers: [errorHandler] }],
|
|
214
292
|
});
|
|
215
|
-
const { port, server } = await listen(app);
|
|
216
|
-
openServer =
|
|
293
|
+
const { port, server: httpListening } = await listen(app);
|
|
294
|
+
openServer = httpListening;
|
|
217
295
|
const res = await postMcp(port);
|
|
218
296
|
expect(calls).toEqual(["error-handler"]);
|
|
219
297
|
expect(res.status).toBe(503);
|
|
@@ -229,15 +307,17 @@ describe("createApp", () => {
|
|
|
229
307
|
const throwingApiRoute = (_req, _res, next) => {
|
|
230
308
|
next(new Error("api error"));
|
|
231
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);
|
|
232
313
|
const httpServer = http.createServer();
|
|
233
314
|
const app = await createApp({
|
|
234
|
-
mcpServer
|
|
315
|
+
mcpServer,
|
|
235
316
|
httpServer,
|
|
236
|
-
customMiddleware: [{ path: "/api/test", handlers: [throwingApiRoute] }],
|
|
237
317
|
errorMiddleware: [{ path: "/mcp", handlers: [mcpErrorHandler] }],
|
|
238
318
|
});
|
|
239
|
-
const { port, server } = await listen(app);
|
|
240
|
-
openServer =
|
|
319
|
+
const { port, server: httpListening } = await listen(app);
|
|
320
|
+
openServer = httpListening;
|
|
241
321
|
const mcpRes = await postMcp(port);
|
|
242
322
|
expect(calls).toEqual(["mcp-error-handler"]);
|
|
243
323
|
expect(mcpRes.status).toBe(503);
|
|
@@ -248,5 +328,103 @@ describe("createApp", () => {
|
|
|
248
328
|
expect(apiRes.status).toBe(500);
|
|
249
329
|
consoleSpy.mockRestore();
|
|
250
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
|
+
});
|
|
251
429
|
});
|
|
252
430
|
//# sourceMappingURL=express.test.js.map
|