/** * `POST /api/app-document` (#2056) — the authenticated seam the browser hands a * wrapped MCP App document back through so the backend can serve it from a * dedicated origin. * * The route owns validation and the two "can't host it" answers; the listener * that actually serves the bytes is `clients/web/server/app-origin-controller` * and is tested separately. */ import { describe, it, expect, vi } from "vitest"; import { createRemoteApp } from "@inspector/core/mcp/remote/node/server.js"; function post(body: unknown): Request { return new Request("http://test/api/app-document", { method: "POST", headers: { "Content-Type": "application/json" }, body: typeof body === "string" ? body : JSON.stringify(body), }); } function appWith( publishAppDocument?: (doc: { html: string; csp?: string; }) => { url: string } | null, ) { return createRemoteApp({ dangerouslyOmitAuth: true, allowedOrigins: ["http://127.0.0.1:6274"], initialConfig: { defaultEnvironment: {} }, publishAppDocument, }).app; } describe("createRemoteApp POST /api/app-document", () => { it("publishes the document and returns the URL", async () => { const publish = vi .fn<(doc: { html: string; csp?: string }) => { url: string } | null>() .mockReturnValue({ url: "http://localhost:6276/app-document/abc" }); const res = await appWith(publish).request( post({ html: "

hi

", csp: "default-src 'none'" }), ); expect(res.status).toBe(200); await expect(res.json()).resolves.toEqual({ url: "http://localhost:6276/app-document/abc", }); expect(publish).toHaveBeenCalledWith({ html: "

hi

", csp: "default-src 'none'", }); }); it("accepts a document with no csp", async () => { const publish = vi .fn<(doc: { html: string; csp?: string }) => { url: string } | null>() .mockReturnValue({ url: "http://localhost:6276/app-document/abc" }); const res = await appWith(publish).request(post({ html: "

hi

" })); expect(res.status).toBe(200); expect(publish).toHaveBeenCalledWith({ html: "

hi

", csp: undefined, }); }); it("answers 503 when the backend supplies no publisher", async () => { // An embedder with no app-origin listener at all — the browser reads this // as "no dedicated origin available" and renders the app the default way. const res = await appWith().request(post({ html: "

hi

" })); expect(res.status).toBe(503); }); it("answers 503 when the publisher declines (listener never bound)", async () => { const res = await appWith(() => null).request( post({ html: "

hi

" }), ); expect(res.status).toBe(503); }); it("rejects a malformed body", async () => { const res = await appWith(() => ({ url: "http://x/y" })).request( post("{not json"), ); expect(res.status).toBe(400); }); it.each([ ["missing html", {}], ["empty html", { html: "" }], ["non-string html", { html: 42 }], ["non-string csp", { html: "

x

", csp: 42 }], ])("rejects %s with 400", async (_label, body) => { const publish = vi .fn<(doc: { html: string; csp?: string }) => { url: string } | null>() .mockReturnValue({ url: "http://x/y" }); const res = await appWith(publish).request(post(body)); expect(res.status).toBe(400); expect(publish).not.toHaveBeenCalled(); }); it("rejects an oversized document with 413 rather than pinning it in memory", async () => { const publish = vi .fn<(doc: { html: string; csp?: string }) => { url: string } | null>() .mockReturnValue({ url: "http://x/y" }); const res = await appWith(publish).request( post({ html: "x".repeat(8 * 1024 * 1024 + 1) }), ); expect(res.status).toBe(413); expect(publish).not.toHaveBeenCalled(); }); it("requires the auth token like every other /api route", async () => { const { app } = createRemoteApp({ authToken: "s3cret", allowedOrigins: ["http://127.0.0.1:6274"], initialConfig: { defaultEnvironment: {} }, publishAppDocument: () => ({ url: "http://x/y" }), }); const unauthorized = await app.request(post({ html: "

x

" })); expect(unauthorized.status).toBe(401); const authorized = await app.request( new Request("http://test/api/app-document", { method: "POST", headers: { "Content-Type": "application/json", "x-mcp-remote-auth": "Bearer s3cret", }, body: JSON.stringify({ html: "

x

" }), }), ); expect(authorized.status).toBe(200); }); });