skybridge 0.0.0-dev.80b6796 → 0.0.0-dev.80df5a4
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 +24 -15
- package/dist/cli/detect-port.d.ts +18 -0
- package/dist/cli/detect-port.js +61 -0
- package/dist/cli/detect-port.js.map +1 -0
- package/dist/cli/header.js +1 -1
- package/dist/cli/header.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.d.ts +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-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 -6
- package/dist/cli/use-nodemon.js +20 -17
- package/dist/cli/use-nodemon.js.map +1 -1
- 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 -1
- package/dist/commands/dev.js +50 -8
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/start.d.ts +3 -1
- package/dist/commands/start.js +30 -9
- package/dist/commands/start.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 +15 -0
- package/dist/server/express.js +103 -0
- package/dist/server/express.js.map +1 -0
- package/dist/server/express.test.d.ts +1 -0
- package/dist/server/express.test.js +348 -0
- package/dist/server/express.test.js.map +1 -0
- package/dist/server/index.d.ts +5 -3
- package/dist/server/index.js +3 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/inferUtilityTypes.d.ts +6 -6
- 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.d.ts +124 -0
- package/dist/server/middleware.js +93 -0
- package/dist/server/middleware.js.map +1 -0
- package/dist/server/middleware.test-d.d.ts +1 -0
- package/dist/server/middleware.test-d.js +75 -0
- package/dist/server/middleware.test-d.js.map +1 -0
- package/dist/server/middleware.test.d.ts +1 -0
- package/dist/server/middleware.test.js +493 -0
- package/dist/server/middleware.test.js.map +1 -0
- package/dist/server/server.d.ts +149 -63
- package/dist/server/server.js +383 -76
- package/dist/server/server.js.map +1 -1
- package/dist/server/templateHelper.d.ts +5 -8
- 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 -6
- package/dist/web/bridges/apps-sdk/adaptor.js +46 -17
- package/dist/web/bridges/apps-sdk/adaptor.js.map +1 -1
- package/dist/web/bridges/apps-sdk/bridge.d.ts +1 -1
- package/dist/web/bridges/apps-sdk/index.d.ts +1 -1
- package/dist/web/bridges/apps-sdk/index.js.map +1 -1
- package/dist/web/bridges/apps-sdk/types.d.ts +24 -13
- package/dist/web/bridges/apps-sdk/types.js.map +1 -1
- package/dist/web/bridges/mcp-app/adaptor.d.ts +20 -8
- package/dist/web/bridges/mcp-app/adaptor.js +141 -58
- package/dist/web/bridges/mcp-app/adaptor.js.map +1 -1
- package/dist/web/bridges/mcp-app/bridge.d.ts +13 -30
- package/dist/web/bridges/mcp-app/bridge.js +43 -196
- package/dist/web/bridges/mcp-app/bridge.js.map +1 -1
- package/dist/web/bridges/mcp-app/use-mcp-app-context.d.ts +5 -3
- package/dist/web/bridges/mcp-app/use-mcp-app-context.js +2 -2
- 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 +1 -41
- package/dist/web/bridges/mcp-app/use-mcp-app-context.test.js.map +1 -1
- package/dist/web/bridges/types.d.ts +22 -11
- package/dist/web/components/modal-provider.js +3 -5
- 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 +23 -20
- 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 +33 -30
- 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/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 +2 -2
- package/dist/web/hooks/index.js +1 -1
- package/dist/web/hooks/index.js.map +1 -1
- package/dist/web/hooks/test/utils.js +4 -0
- package/dist/web/hooks/test/utils.js.map +1 -1
- package/dist/web/hooks/use-display-mode.d.ts +3 -3
- package/dist/web/hooks/use-display-mode.js.map +1 -1
- package/dist/web/hooks/use-display-mode.test-d.d.ts +1 -0
- package/dist/web/hooks/use-display-mode.test-d.js +8 -0
- package/dist/web/hooks/use-display-mode.test-d.js.map +1 -0
- 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 +27 -3
- package/dist/web/hooks/use-files.test.js.map +1 -1
- package/dist/web/hooks/use-layout.test.js +3 -3
- package/dist/web/hooks/use-layout.test.js.map +1 -1
- package/dist/web/hooks/use-open-external.d.ts +3 -1
- package/dist/web/hooks/use-open-external.js +1 -1
- package/dist/web/hooks/use-open-external.js.map +1 -1
- package/dist/web/hooks/use-open-external.test.js +26 -11
- package/dist/web/hooks/use-open-external.test.js.map +1 -1
- 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 +5 -1
- package/dist/web/hooks/use-request-modal.test.js.map +1 -1
- package/dist/web/hooks/use-set-open-in-app-url.test.js +5 -11
- package/dist/web/hooks/use-set-open-in-app-url.test.js.map +1 -1
- package/dist/web/hooks/use-tool-info.test.js +1 -1
- package/dist/web/hooks/use-tool-info.test.js.map +1 -1
- package/dist/web/hooks/use-user.js +18 -2
- package/dist/web/hooks/use-user.js.map +1 -1
- package/dist/web/hooks/use-user.test.js +29 -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/plugin.d.ts +4 -1
- package/dist/web/plugin/plugin.js +135 -18
- 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/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/package.json +41 -30
- package/tsconfig.base.json +33 -0
- package/dist/server/templates/development.hbs +0 -67
- package/dist/server/templates/production.hbs +0 -6
- package/dist/server/widgetsDevServer.d.ts +0 -12
- package/dist/server/widgetsDevServer.js +0 -57
- package/dist/server/widgetsDevServer.js.map +0 -1
- package/dist/test/widget.test.js +0 -255
- 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 -61
- 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/{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
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { AudioContent, EmbeddedResource, ImageContent, ResourceLink, TextContent } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
type ContentAnnotations = {
|
|
3
|
+
audience?: ("user" | "assistant")[];
|
|
4
|
+
priority?: number;
|
|
5
|
+
lastModified?: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function text(value: string, annotations?: ContentAnnotations): TextContent;
|
|
8
|
+
export declare function image(data: string | Uint8Array, mimeType: string, annotations?: ContentAnnotations): ImageContent;
|
|
9
|
+
export declare function audio(data: string | Uint8Array, mimeType: string, annotations?: ContentAnnotations): AudioContent;
|
|
10
|
+
export declare function embeddedResource(resource: {
|
|
11
|
+
uri: string;
|
|
12
|
+
mimeType?: string;
|
|
13
|
+
text: string;
|
|
14
|
+
} | {
|
|
15
|
+
uri: string;
|
|
16
|
+
mimeType?: string;
|
|
17
|
+
blob: string;
|
|
18
|
+
}, annotations?: ContentAnnotations): EmbeddedResource;
|
|
19
|
+
export declare function resourceLink(link: {
|
|
20
|
+
uri: string;
|
|
21
|
+
name: string;
|
|
22
|
+
title?: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
mimeType?: string;
|
|
25
|
+
size?: number;
|
|
26
|
+
}, annotations?: ContentAnnotations): ResourceLink;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns a base64-encoded string.
|
|
3
|
+
* - `Uint8Array` input is encoded via `Buffer.toString("base64")`.
|
|
4
|
+
* - `string` input is assumed to be **already base64-encoded** and is returned
|
|
5
|
+
* as-is. Passing raw/unencoded string bytes will produce invalid MCP content.
|
|
6
|
+
*/
|
|
7
|
+
function toBase64(data) {
|
|
8
|
+
if (typeof data === "string") {
|
|
9
|
+
return data;
|
|
10
|
+
}
|
|
11
|
+
return Buffer.from(data).toString("base64");
|
|
12
|
+
}
|
|
13
|
+
export function text(value, annotations) {
|
|
14
|
+
return { type: "text", text: value, ...(annotations && { annotations }) };
|
|
15
|
+
}
|
|
16
|
+
export function image(data, mimeType, annotations) {
|
|
17
|
+
return {
|
|
18
|
+
type: "image",
|
|
19
|
+
data: toBase64(data),
|
|
20
|
+
mimeType,
|
|
21
|
+
...(annotations && { annotations }),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function audio(data, mimeType, annotations) {
|
|
25
|
+
return {
|
|
26
|
+
type: "audio",
|
|
27
|
+
data: toBase64(data),
|
|
28
|
+
mimeType,
|
|
29
|
+
...(annotations && { annotations }),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
export function embeddedResource(resource, annotations) {
|
|
33
|
+
return {
|
|
34
|
+
type: "resource",
|
|
35
|
+
resource,
|
|
36
|
+
...(annotations && { annotations }),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
export function resourceLink(link, annotations) {
|
|
40
|
+
return {
|
|
41
|
+
type: "resource_link",
|
|
42
|
+
...link,
|
|
43
|
+
...(annotations && { annotations }),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=content-helpers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-helpers.js","sourceRoot":"","sources":["../../src/server/content-helpers.ts"],"names":[],"mappings":"AAcA;;;;;GAKG;AACH,SAAS,QAAQ,CAAC,IAAyB;IACzC,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;AAC9C,CAAC;AAED,MAAM,UAAU,IAAI,CAClB,KAAa,EACb,WAAgC;IAEhC,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,CAAC,WAAW,IAAI,EAAE,WAAW,EAAE,CAAC,EAAE,CAAC;AAC5E,CAAC;AAED,MAAM,UAAU,KAAK,CACnB,IAAyB,EACzB,QAAgB,EAChB,WAAgC;IAEhC,OAAO;QACL,IAAI,EAAE,OAAO;QACb,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC;QACpB,QAAQ;QACR,GAAG,CAAC,WAAW,IAAI,EAAE,WAAW,EAAE,CAAC;KACpC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,KAAK,CACnB,IAAyB,EACzB,QAAgB,EAChB,WAAgC;IAEhC,OAAO;QACL,IAAI,EAAE,OAAO;QACb,IAAI,EAAE,QAAQ,CAAC,IAAI,CAAC;QACpB,QAAQ;QACR,GAAG,CAAC,WAAW,IAAI,EAAE,WAAW,EAAE,CAAC;KACpC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,gBAAgB,CAC9B,QAEoD,EACpD,WAAgC;IAEhC,OAAO;QACL,IAAI,EAAE,UAAU;QAChB,QAAQ;QACR,GAAG,CAAC,WAAW,IAAI,EAAE,WAAW,EAAE,CAAC;KACpC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,YAAY,CAC1B,IAOC,EACD,WAAgC;IAEhC,OAAO;QACL,IAAI,EAAE,eAAe;QACrB,GAAG,IAAI;QACP,GAAG,CAAC,WAAW,IAAI,EAAE,WAAW,EAAE,CAAC;KACpC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { audio, embeddedResource, image, resourceLink, text, } from "./content-helpers.js";
|
|
3
|
+
describe("text", () => {
|
|
4
|
+
it("returns a TextContent without annotations when none given", () => {
|
|
5
|
+
expect(text("hello")).toEqual({ type: "text", text: "hello" });
|
|
6
|
+
});
|
|
7
|
+
it("includes annotations when provided", () => {
|
|
8
|
+
expect(text("hi", { priority: 1 })).toEqual({
|
|
9
|
+
type: "text",
|
|
10
|
+
text: "hi",
|
|
11
|
+
annotations: { priority: 1 },
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
describe("image", () => {
|
|
16
|
+
it("base64-encodes Uint8Array data", () => {
|
|
17
|
+
const bytes = new Uint8Array([104, 105]);
|
|
18
|
+
expect(image(bytes, "image/png")).toEqual({
|
|
19
|
+
type: "image",
|
|
20
|
+
data: "aGk=",
|
|
21
|
+
mimeType: "image/png",
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
it("passes string data through unchanged (caller is responsible for base64)", () => {
|
|
25
|
+
expect(image("YWxyZWFkeS1iNjQ=", "image/png")).toEqual({
|
|
26
|
+
type: "image",
|
|
27
|
+
data: "YWxyZWFkeS1iNjQ=",
|
|
28
|
+
mimeType: "image/png",
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
describe("audio", () => {
|
|
33
|
+
it("base64-encodes Uint8Array data", () => {
|
|
34
|
+
const bytes = new Uint8Array([104, 105]);
|
|
35
|
+
expect(audio(bytes, "audio/mpeg")).toMatchObject({
|
|
36
|
+
type: "audio",
|
|
37
|
+
data: "aGk=",
|
|
38
|
+
mimeType: "audio/mpeg",
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe("embeddedResource", () => {
|
|
43
|
+
it("wraps a text resource with a type tag", () => {
|
|
44
|
+
expect(embeddedResource({
|
|
45
|
+
uri: "file:///a.txt",
|
|
46
|
+
mimeType: "text/plain",
|
|
47
|
+
text: "x",
|
|
48
|
+
})).toEqual({
|
|
49
|
+
type: "resource",
|
|
50
|
+
resource: { uri: "file:///a.txt", mimeType: "text/plain", text: "x" },
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
it("wraps a blob resource with a type tag", () => {
|
|
54
|
+
expect(embeddedResource({ uri: "file:///a.bin", blob: "YmFy" })).toEqual({
|
|
55
|
+
type: "resource",
|
|
56
|
+
resource: { uri: "file:///a.bin", blob: "YmFy" },
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
describe("resourceLink", () => {
|
|
61
|
+
it("spreads link fields alongside the type tag", () => {
|
|
62
|
+
expect(resourceLink({ uri: "file:///a", name: "a", title: "A" })).toEqual({
|
|
63
|
+
type: "resource_link",
|
|
64
|
+
uri: "file:///a",
|
|
65
|
+
name: "a",
|
|
66
|
+
title: "A",
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
//# sourceMappingURL=content-helpers.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-helpers.test.js","sourceRoot":"","sources":["../../src/server/content-helpers.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type http from "node:http";
|
|
2
|
+
import express from "express";
|
|
3
|
+
import type { McpServer } from "./server.js";
|
|
4
|
+
export declare function createApp({ mcpServer, httpServer, customMiddleware, errorMiddleware, }: {
|
|
5
|
+
mcpServer: McpServer;
|
|
6
|
+
httpServer: http.Server;
|
|
7
|
+
customMiddleware?: {
|
|
8
|
+
path?: string;
|
|
9
|
+
handlers: express.RequestHandler[];
|
|
10
|
+
}[];
|
|
11
|
+
errorMiddleware?: {
|
|
12
|
+
path?: string;
|
|
13
|
+
handlers: express.ErrorRequestHandler[];
|
|
14
|
+
}[];
|
|
15
|
+
}): Promise<express.Express>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
3
|
+
import cors from "cors";
|
|
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
|
+
}
|
|
15
|
+
function applyMiddlewares(app, middlewares) {
|
|
16
|
+
for (const middleware of middlewares) {
|
|
17
|
+
if (middleware.path) {
|
|
18
|
+
app.use(middleware.path, ...middleware.handlers);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
app.use(...middleware.handlers);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
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, customMiddleware = [], errorMiddleware = [], }) {
|
|
36
|
+
const app = express();
|
|
37
|
+
app.use(express.json());
|
|
38
|
+
applyMiddlewares(app, customMiddleware);
|
|
39
|
+
// Read `process.env.NODE_ENV` inline: wrangler/esbuild only substitute the literal expression,
|
|
40
|
+
// so a local const would defeat dead-code elimination of the dev-only imports below.
|
|
41
|
+
if (process.env.NODE_ENV !== "production") {
|
|
42
|
+
const { devtoolsStaticServer } = await import("@skybridge/devtools");
|
|
43
|
+
app.use(await devtoolsStaticServer());
|
|
44
|
+
const { viewsDevServer } = await import("./viewsDevServer.js");
|
|
45
|
+
app.use(await viewsDevServer(httpServer));
|
|
46
|
+
const controlPort = parseControlPort(process.env.__TUNNEL_CONTROL_PORT);
|
|
47
|
+
if (controlPort !== null) {
|
|
48
|
+
const { createTunnelProxyRouter } = await import("./tunnel-proxy-router.js");
|
|
49
|
+
app.use(createTunnelProxyRouter(controlPort));
|
|
50
|
+
}
|
|
51
|
+
else if (process.env.__TUNNEL_CONTROL_PORT !== undefined) {
|
|
52
|
+
console.warn(`Ignoring invalid __TUNNEL_CONTROL_PORT=${process.env.__TUNNEL_CONTROL_PORT}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
const assetsPath = path.join(process.cwd(), "dist", "assets");
|
|
57
|
+
app.use("/assets", cors());
|
|
58
|
+
app.use("/assets", express.static(assetsPath));
|
|
59
|
+
}
|
|
60
|
+
app.use("/mcp", mcpMiddleware(mcpServer));
|
|
61
|
+
applyMiddlewares(app, errorMiddleware);
|
|
62
|
+
app.use("/mcp", defaultErrorHandler);
|
|
63
|
+
return app;
|
|
64
|
+
}
|
|
65
|
+
const mcpMiddleware = (server) => {
|
|
66
|
+
return async (req, res, next) => {
|
|
67
|
+
if (req.method !== "POST") {
|
|
68
|
+
res.writeHead(405).end(JSON.stringify({
|
|
69
|
+
jsonrpc: "2.0",
|
|
70
|
+
error: {
|
|
71
|
+
code: -32000,
|
|
72
|
+
message: "Method not allowed.",
|
|
73
|
+
},
|
|
74
|
+
id: null,
|
|
75
|
+
}));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const transport = new StreamableHTTPServerTransport({
|
|
80
|
+
sessionIdGenerator: undefined,
|
|
81
|
+
// Respond with a single JSON body instead of SSE. Skybridge's stateless
|
|
82
|
+
// transport never streams server-initiated messages, so SSE adds no
|
|
83
|
+
// capability — and on workerd specifically, `cloudflare:node`'s http
|
|
84
|
+
// bridge silently drops chunked writes that happen after the request
|
|
85
|
+
// handler awaits, which manifests as a 200 with empty body for any
|
|
86
|
+
// async tools/call.
|
|
87
|
+
enableJsonResponse: true,
|
|
88
|
+
});
|
|
89
|
+
res.on("close", () => {
|
|
90
|
+
transport.close();
|
|
91
|
+
});
|
|
92
|
+
await server.connectStatelessTransport(transport);
|
|
93
|
+
// Express strips the mount path from req.url (e.g. "/mcp" becomes "/").
|
|
94
|
+
// Restore it so the SDK builds the correct requestInfo.url.
|
|
95
|
+
req.url = req.originalUrl;
|
|
96
|
+
await transport.handleRequest(req, res, req.body);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
next(error);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
//# sourceMappingURL=express.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"express.js","sourceRoot":"","sources":["../../src/server/express.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { McpServer } from "./server.js";
|
|
4
|
+
vi.mock("@skybridge/devtools", () => ({
|
|
5
|
+
devtoolsStaticServer: () => ((_req, _res, next) => next()),
|
|
6
|
+
}));
|
|
7
|
+
vi.mock("./viewsDevServer.js", () => ({
|
|
8
|
+
viewsDevServer: (_httpServer) => ((_req, _res, next) => next()),
|
|
9
|
+
}));
|
|
10
|
+
const fakeServer = {};
|
|
11
|
+
async function listen(app) {
|
|
12
|
+
const server = http.createServer(app);
|
|
13
|
+
await new Promise((resolve) => server.listen(0, resolve));
|
|
14
|
+
const port = server.address().port;
|
|
15
|
+
return { port, server };
|
|
16
|
+
}
|
|
17
|
+
let openServer;
|
|
18
|
+
afterEach(() => openServer?.close());
|
|
19
|
+
async function postMcp(port) {
|
|
20
|
+
return fetch(`http://localhost:${port}/mcp`, {
|
|
21
|
+
method: "POST",
|
|
22
|
+
headers: { "Content-Type": "application/json" },
|
|
23
|
+
body: JSON.stringify({ jsonrpc: "2.0", method: "initialize", id: 1 }),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
async function postApi(port) {
|
|
27
|
+
return fetch(`http://localhost:${port}/api/test`, { method: "POST" });
|
|
28
|
+
}
|
|
29
|
+
describe("createApp", () => {
|
|
30
|
+
it("runs global custom middleware before the /mcp handler", async () => {
|
|
31
|
+
const { createApp } = await import("./express.js");
|
|
32
|
+
const calls = [];
|
|
33
|
+
const mw = (_req, _res, next) => {
|
|
34
|
+
calls.push("custom");
|
|
35
|
+
next();
|
|
36
|
+
};
|
|
37
|
+
const httpServer = http.createServer();
|
|
38
|
+
const app = await createApp({
|
|
39
|
+
mcpServer: fakeServer,
|
|
40
|
+
httpServer,
|
|
41
|
+
customMiddleware: [{ handlers: [mw] }],
|
|
42
|
+
});
|
|
43
|
+
const { port, server } = await listen(app);
|
|
44
|
+
openServer = server;
|
|
45
|
+
await postMcp(port);
|
|
46
|
+
expect(calls).toEqual(["custom"]);
|
|
47
|
+
});
|
|
48
|
+
it("runs path-scoped middleware on /mcp", async () => {
|
|
49
|
+
const { createApp } = await import("./express.js");
|
|
50
|
+
const calls = [];
|
|
51
|
+
const mw = (_req, _res, next) => {
|
|
52
|
+
calls.push("auth");
|
|
53
|
+
next();
|
|
54
|
+
};
|
|
55
|
+
const httpServer = http.createServer();
|
|
56
|
+
const app = await createApp({
|
|
57
|
+
mcpServer: fakeServer,
|
|
58
|
+
httpServer,
|
|
59
|
+
customMiddleware: [{ path: "/mcp", handlers: [mw] }],
|
|
60
|
+
});
|
|
61
|
+
const { port, server } = await listen(app);
|
|
62
|
+
openServer = server;
|
|
63
|
+
await postMcp(port);
|
|
64
|
+
expect(calls).toEqual(["auth"]);
|
|
65
|
+
});
|
|
66
|
+
it("allows middleware to short-circuit with 401", async () => {
|
|
67
|
+
const { createApp } = await import("./express.js");
|
|
68
|
+
const calls = [];
|
|
69
|
+
const reject = (_req, res) => {
|
|
70
|
+
calls.push("reject");
|
|
71
|
+
res.status(401).json({ error: "Unauthorized" });
|
|
72
|
+
};
|
|
73
|
+
const httpServer = http.createServer();
|
|
74
|
+
const app = await createApp({
|
|
75
|
+
mcpServer: fakeServer,
|
|
76
|
+
httpServer,
|
|
77
|
+
customMiddleware: [{ path: "/mcp", handlers: [reject] }],
|
|
78
|
+
});
|
|
79
|
+
const { port, server } = await listen(app);
|
|
80
|
+
openServer = server;
|
|
81
|
+
const res = await postMcp(port);
|
|
82
|
+
expect(calls).toEqual(["reject"]);
|
|
83
|
+
expect(res.status).toBe(401);
|
|
84
|
+
expect(await res.json()).toEqual({ error: "Unauthorized" });
|
|
85
|
+
});
|
|
86
|
+
it("runs multiple global middleware in registration order", async () => {
|
|
87
|
+
const { createApp } = await import("./express.js");
|
|
88
|
+
const calls = [];
|
|
89
|
+
const mwA = (_req, _res, next) => {
|
|
90
|
+
calls.push("A");
|
|
91
|
+
next();
|
|
92
|
+
};
|
|
93
|
+
const mwB = (_req, _res, next) => {
|
|
94
|
+
calls.push("B");
|
|
95
|
+
next();
|
|
96
|
+
};
|
|
97
|
+
const httpServer = http.createServer();
|
|
98
|
+
const app = await createApp({
|
|
99
|
+
mcpServer: fakeServer,
|
|
100
|
+
httpServer,
|
|
101
|
+
customMiddleware: [{ handlers: [mwA] }, { handlers: [mwB] }],
|
|
102
|
+
});
|
|
103
|
+
const { port, server } = await listen(app);
|
|
104
|
+
openServer = server;
|
|
105
|
+
await postMcp(port);
|
|
106
|
+
expect(calls).toEqual(["A", "B"]);
|
|
107
|
+
});
|
|
108
|
+
it("path-scoped middleware does not run on non-matching paths", async () => {
|
|
109
|
+
const { createApp } = await import("./express.js");
|
|
110
|
+
const calls = [];
|
|
111
|
+
const apiMw = (_req, _res, next) => {
|
|
112
|
+
calls.push("api");
|
|
113
|
+
next();
|
|
114
|
+
};
|
|
115
|
+
const httpServer = http.createServer();
|
|
116
|
+
const app = await createApp({
|
|
117
|
+
mcpServer: fakeServer,
|
|
118
|
+
httpServer,
|
|
119
|
+
customMiddleware: [{ path: "/api", handlers: [apiMw] }],
|
|
120
|
+
});
|
|
121
|
+
const { port, server } = await listen(app);
|
|
122
|
+
openServer = server;
|
|
123
|
+
// Hit /mcp — the /api middleware should NOT fire
|
|
124
|
+
await postMcp(port);
|
|
125
|
+
expect(calls).toEqual([]);
|
|
126
|
+
});
|
|
127
|
+
it("supports Express Router via custom middleware", async () => {
|
|
128
|
+
const { createApp } = await import("./express.js");
|
|
129
|
+
const { Router } = await import("express");
|
|
130
|
+
const router = Router();
|
|
131
|
+
router.get("/health", (_req, res) => {
|
|
132
|
+
res.json({ status: "ok" });
|
|
133
|
+
});
|
|
134
|
+
const httpServer = http.createServer();
|
|
135
|
+
const app = await createApp({
|
|
136
|
+
mcpServer: fakeServer,
|
|
137
|
+
httpServer,
|
|
138
|
+
customMiddleware: [{ handlers: [router] }],
|
|
139
|
+
});
|
|
140
|
+
const { port, server } = await listen(app);
|
|
141
|
+
openServer = server;
|
|
142
|
+
const res = await fetch(`http://localhost:${port}/health`);
|
|
143
|
+
expect(res.status).toBe(200);
|
|
144
|
+
expect(await res.json()).toEqual({ status: "ok" });
|
|
145
|
+
});
|
|
146
|
+
it("supports path-prefixed Router", async () => {
|
|
147
|
+
const { createApp } = await import("./express.js");
|
|
148
|
+
const { Router } = await import("express");
|
|
149
|
+
const router = Router();
|
|
150
|
+
router.get("/data", (_req, res) => {
|
|
151
|
+
res.json({ value: 42 });
|
|
152
|
+
});
|
|
153
|
+
const httpServer = http.createServer();
|
|
154
|
+
const app = await createApp({
|
|
155
|
+
mcpServer: fakeServer,
|
|
156
|
+
httpServer,
|
|
157
|
+
customMiddleware: [
|
|
158
|
+
{ path: "/api", handlers: [router] },
|
|
159
|
+
],
|
|
160
|
+
});
|
|
161
|
+
const { port, server } = await listen(app);
|
|
162
|
+
openServer = server;
|
|
163
|
+
const res = await fetch(`http://localhost:${port}/api/data`);
|
|
164
|
+
expect(res.status).toBe(200);
|
|
165
|
+
expect(await res.json()).toEqual({ value: 42 });
|
|
166
|
+
});
|
|
167
|
+
it("server survives middleware errors without crashing", async () => {
|
|
168
|
+
const { createApp } = await import("./express.js");
|
|
169
|
+
const throwing = () => {
|
|
170
|
+
throw new Error("boom");
|
|
171
|
+
};
|
|
172
|
+
const httpServer = http.createServer();
|
|
173
|
+
const app = await createApp({
|
|
174
|
+
mcpServer: fakeServer,
|
|
175
|
+
httpServer,
|
|
176
|
+
customMiddleware: [{ path: "/explode", handlers: [throwing] }],
|
|
177
|
+
});
|
|
178
|
+
const { port, server } = await listen(app);
|
|
179
|
+
openServer = server;
|
|
180
|
+
const res = await fetch(`http://localhost:${port}/explode`);
|
|
181
|
+
expect(res.status).toBe(500);
|
|
182
|
+
// Server process did not crash — it still accepts connections
|
|
183
|
+
const followUp = await fetch(`http://localhost:${port}/explode`);
|
|
184
|
+
expect(followUp.status).toBe(500);
|
|
185
|
+
});
|
|
186
|
+
it("returns 500 JSON-RPC error when the MCP handler throws and no error middleware is registered", async () => {
|
|
187
|
+
const { createApp } = await import("./express.js");
|
|
188
|
+
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => { });
|
|
189
|
+
const httpServer = http.createServer();
|
|
190
|
+
const app = await createApp({ mcpServer: fakeServer, httpServer });
|
|
191
|
+
const { port, server } = await listen(app);
|
|
192
|
+
openServer = server;
|
|
193
|
+
const res = await postMcp(port);
|
|
194
|
+
expect(res.status).toBe(500);
|
|
195
|
+
expect(await res.json()).toEqual({
|
|
196
|
+
jsonrpc: "2.0",
|
|
197
|
+
error: { code: -32603, message: "Internal server error" },
|
|
198
|
+
id: null,
|
|
199
|
+
});
|
|
200
|
+
expect(consoleSpy).toHaveBeenCalledWith("Error handling MCP request:", expect.any(Error));
|
|
201
|
+
consoleSpy.mockRestore();
|
|
202
|
+
});
|
|
203
|
+
it("invokes a custom error handler when the MCP handler throws", async () => {
|
|
204
|
+
const { createApp } = await import("./express.js");
|
|
205
|
+
const calls = [];
|
|
206
|
+
const errorHandler = (_err, _req, res, _next) => {
|
|
207
|
+
calls.push("error-handler");
|
|
208
|
+
res.status(503).json({ custom: true });
|
|
209
|
+
};
|
|
210
|
+
const httpServer = http.createServer();
|
|
211
|
+
const app = await createApp({
|
|
212
|
+
mcpServer: fakeServer,
|
|
213
|
+
httpServer,
|
|
214
|
+
errorMiddleware: [{ handlers: [errorHandler] }],
|
|
215
|
+
});
|
|
216
|
+
const { port, server } = await listen(app);
|
|
217
|
+
openServer = server;
|
|
218
|
+
const res = await postMcp(port);
|
|
219
|
+
expect(calls).toEqual(["error-handler"]);
|
|
220
|
+
expect(res.status).toBe(503);
|
|
221
|
+
expect(await res.json()).toEqual({ custom: true });
|
|
222
|
+
});
|
|
223
|
+
it("invokes a path-scoped error handler only for matching routes", async () => {
|
|
224
|
+
const { createApp } = await import("./express.js");
|
|
225
|
+
const calls = [];
|
|
226
|
+
const mcpErrorHandler = (_err, _req, res, _next) => {
|
|
227
|
+
calls.push("mcp-error-handler");
|
|
228
|
+
res.status(503).json({ from: "mcp-error-handler" });
|
|
229
|
+
};
|
|
230
|
+
const throwingApiRoute = (_req, _res, next) => {
|
|
231
|
+
next(new Error("api error"));
|
|
232
|
+
};
|
|
233
|
+
const httpServer = http.createServer();
|
|
234
|
+
const app = await createApp({
|
|
235
|
+
mcpServer: fakeServer,
|
|
236
|
+
httpServer,
|
|
237
|
+
customMiddleware: [{ path: "/api/test", handlers: [throwingApiRoute] }],
|
|
238
|
+
errorMiddleware: [{ path: "/mcp", handlers: [mcpErrorHandler] }],
|
|
239
|
+
});
|
|
240
|
+
const { port, server } = await listen(app);
|
|
241
|
+
openServer = server;
|
|
242
|
+
const mcpRes = await postMcp(port);
|
|
243
|
+
expect(calls).toEqual(["mcp-error-handler"]);
|
|
244
|
+
expect(mcpRes.status).toBe(503);
|
|
245
|
+
expect(await mcpRes.json()).toEqual({ from: "mcp-error-handler" });
|
|
246
|
+
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => { });
|
|
247
|
+
const apiRes = await postApi(port);
|
|
248
|
+
expect(calls).toEqual(["mcp-error-handler"]);
|
|
249
|
+
expect(apiRes.status).toBe(500);
|
|
250
|
+
consoleSpy.mockRestore();
|
|
251
|
+
});
|
|
252
|
+
it("handles concurrent /mcp requests without 'Already connected to a transport'", async () => {
|
|
253
|
+
const { createApp } = await import("./express.js");
|
|
254
|
+
const mcpServer = new McpServer({
|
|
255
|
+
name: "concurrent-test",
|
|
256
|
+
version: "0.0.0",
|
|
257
|
+
});
|
|
258
|
+
// Slow tool: keeps the underlying transport bound long enough to overlap
|
|
259
|
+
// with concurrent requests, exposing the shared-McpServer race.
|
|
260
|
+
mcpServer.registerTool({ name: "slow", description: "slow" }, async () => {
|
|
261
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
262
|
+
return { content: [{ type: "text", text: "done" }] };
|
|
263
|
+
});
|
|
264
|
+
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => { });
|
|
265
|
+
const httpServer = http.createServer();
|
|
266
|
+
const app = await createApp({ mcpServer, httpServer });
|
|
267
|
+
const { port, server } = await listen(app);
|
|
268
|
+
openServer = server;
|
|
269
|
+
const callBody = (id) => JSON.stringify({
|
|
270
|
+
jsonrpc: "2.0",
|
|
271
|
+
method: "tools/call",
|
|
272
|
+
id,
|
|
273
|
+
params: { name: "slow", arguments: {} },
|
|
274
|
+
});
|
|
275
|
+
const N = 10;
|
|
276
|
+
const responses = await Promise.all(Array.from({ length: N }, (_, i) => fetch(`http://localhost:${port}/mcp`, {
|
|
277
|
+
method: "POST",
|
|
278
|
+
headers: {
|
|
279
|
+
"Content-Type": "application/json",
|
|
280
|
+
Accept: "application/json, text/event-stream",
|
|
281
|
+
},
|
|
282
|
+
body: callBody(i + 1),
|
|
283
|
+
})));
|
|
284
|
+
expect(responses.map((r) => r.status)).toEqual(Array(N).fill(200));
|
|
285
|
+
expect(consoleSpy).not.toHaveBeenCalledWith("Error handling MCP request:", expect.any(Error));
|
|
286
|
+
consoleSpy.mockRestore();
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
describe("createApp tunnel routes", () => {
|
|
290
|
+
it("proxies POST /__skybridge/tunnel to the cli control server in dev mode", async () => {
|
|
291
|
+
// Stand up a fake control listener that returns a known JSON body.
|
|
292
|
+
const control = http.createServer((_req, res) => {
|
|
293
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
294
|
+
res.end('{"status":"idle"}');
|
|
295
|
+
});
|
|
296
|
+
await new Promise((resolve) => control.listen(0, "127.0.0.1", resolve));
|
|
297
|
+
const controlAddr = control.address();
|
|
298
|
+
if (typeof controlAddr === "string" || controlAddr === null) {
|
|
299
|
+
control.close();
|
|
300
|
+
throw new Error("control server has no address");
|
|
301
|
+
}
|
|
302
|
+
const controlPort = controlAddr.port;
|
|
303
|
+
const prev = process.env.__TUNNEL_CONTROL_PORT;
|
|
304
|
+
process.env.__TUNNEL_CONTROL_PORT = String(controlPort);
|
|
305
|
+
try {
|
|
306
|
+
const { createApp } = await import("./express.js");
|
|
307
|
+
const httpServer = http.createServer();
|
|
308
|
+
const app = await createApp({ mcpServer: fakeServer, httpServer });
|
|
309
|
+
const { port, server } = await listen(app);
|
|
310
|
+
openServer = server;
|
|
311
|
+
const res = await fetch(`http://localhost:${port}/__skybridge/tunnel`, {
|
|
312
|
+
method: "POST",
|
|
313
|
+
});
|
|
314
|
+
expect(res.status).toBe(200);
|
|
315
|
+
expect(await res.json()).toEqual({ status: "idle" });
|
|
316
|
+
}
|
|
317
|
+
finally {
|
|
318
|
+
if (prev === undefined) {
|
|
319
|
+
delete process.env.__TUNNEL_CONTROL_PORT;
|
|
320
|
+
}
|
|
321
|
+
else {
|
|
322
|
+
process.env.__TUNNEL_CONTROL_PORT = prev;
|
|
323
|
+
}
|
|
324
|
+
await new Promise((resolve) => control.close(() => resolve()));
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
it("does not expose /__skybridge/tunnel in production mode", async () => {
|
|
328
|
+
const prevEnv = process.env.NODE_ENV;
|
|
329
|
+
process.env.NODE_ENV = "production";
|
|
330
|
+
try {
|
|
331
|
+
vi.resetModules();
|
|
332
|
+
const { createApp } = await import("./express.js");
|
|
333
|
+
const httpServer = http.createServer();
|
|
334
|
+
const app = await createApp({ mcpServer: fakeServer, httpServer });
|
|
335
|
+
const { port, server } = await listen(app);
|
|
336
|
+
openServer = server;
|
|
337
|
+
const res = await fetch(`http://localhost:${port}/__skybridge/tunnel`, {
|
|
338
|
+
method: "POST",
|
|
339
|
+
});
|
|
340
|
+
expect(res.status).toBe(404);
|
|
341
|
+
}
|
|
342
|
+
finally {
|
|
343
|
+
process.env.NODE_ENV = prevEnv;
|
|
344
|
+
vi.resetModules();
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
//# sourceMappingURL=express.test.js.map
|