|
| 1 | +import { env, SELF } from "cloudflare:test"; |
| 2 | +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; |
| 3 | +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; |
| 4 | +import { afterEach, describe, expect, it } from "vitest"; |
| 5 | + |
| 6 | +let client: Client | undefined; |
| 7 | + |
| 8 | +const authorizedFetch: typeof fetch = (input, init = {}) => { |
| 9 | + const headers = new Headers(init.headers); |
| 10 | + headers.set("authorization", "Bearer test-token"); |
| 11 | + return SELF.fetch(input, { ...init, headers }); |
| 12 | +}; |
| 13 | + |
| 14 | +afterEach(async () => { |
| 15 | + await client?.close(); |
| 16 | + client = undefined; |
| 17 | +}); |
| 18 | + |
| 19 | +describe("Computer Code Mode MCP", () => { |
| 20 | + it("serves setup and health routes without authentication", async () => { |
| 21 | + const home = await SELF.fetch("https://example.test/"); |
| 22 | + expect(home.status).toBe(200); |
| 23 | + expect(await home.text()).toContain("https://example.test/mcp"); |
| 24 | + |
| 25 | + const health = await SELF.fetch("https://example.test/health"); |
| 26 | + expect(health.status).toBe(200); |
| 27 | + expect(await health.text()).toBe("ok\n"); |
| 28 | + }); |
| 29 | + |
| 30 | + it("requires the configured bearer token", async () => { |
| 31 | + const missing = await SELF.fetch("https://example.test/mcp", { method: "POST" }); |
| 32 | + expect(missing.status).toBe(401); |
| 33 | + expect(missing.headers.get("www-authenticate")).toBe("Bearer"); |
| 34 | + |
| 35 | + const wrong = await SELF.fetch("https://example.test/mcp", { |
| 36 | + method: "POST", |
| 37 | + headers: { authorization: "Bearer test-tokem" }, |
| 38 | + }); |
| 39 | + expect(wrong.status).toBe(401); |
| 40 | + |
| 41 | + const get = await authorizedFetch("https://example.test/mcp"); |
| 42 | + expect(get.status).toBe(405); |
| 43 | + expect(get.headers.get("allow")).toBe("POST"); |
| 44 | + |
| 45 | + const { COMPUTER_MCP } = env as unknown as { |
| 46 | + COMPUTER_MCP: DurableObjectNamespace; |
| 47 | + }; |
| 48 | + const id = COMPUTER_MCP.idFromName("direct-auth-test"); |
| 49 | + const direct = await COMPUTER_MCP.get(id).fetch("https://example.test/mcp", { |
| 50 | + method: "POST", |
| 51 | + }); |
| 52 | + expect(direct.status).toBe(401); |
| 53 | + }); |
| 54 | + |
| 55 | + it("exposes durable Computer tools through one Code Mode tool", async () => { |
| 56 | + client = new Client({ name: "computer-mcp-test", version: "1.0.0" }); |
| 57 | + const transport = new StreamableHTTPClientTransport(new URL("https://example.test/mcp"), { |
| 58 | + fetch: authorizedFetch, |
| 59 | + }); |
| 60 | + await client.connect(transport); |
| 61 | + |
| 62 | + const listed = await client.listTools(); |
| 63 | + expect(listed.tools.map((tool) => tool.name)).toEqual(["code"]); |
| 64 | + const description = listed.tools[0]?.description; |
| 65 | + expect(description).toContain("codemode.read"); |
| 66 | + expect(description).toContain('"worker-shell"'); |
| 67 | + expect(description).toContain("no ambient outbound network"); |
| 68 | + expect(description).toContain("HTTPS URLs"); |
| 69 | + expect(description).toContain("Cannot run npm"); |
| 70 | + expect(description).toContain('"container-shell"'); |
| 71 | + expect(description).toContain("Full Debian Linux"); |
| 72 | + expect(description).toContain("Cold starts more slowly"); |
| 73 | + |
| 74 | + const result = await client.callTool({ |
| 75 | + name: "code", |
| 76 | + arguments: { |
| 77 | + code: `async () => { |
| 78 | + await codemode.write({ path: "/workspace/message.txt", content: "hello" }); |
| 79 | + await codemode.edit({ |
| 80 | + path: "/workspace/message.txt", |
| 81 | + edits: [{ oldText: "hello", newText: "hello from Code Mode" }] |
| 82 | + }); |
| 83 | + const file = await codemode.read({ path: "/workspace/message.txt" }); |
| 84 | + const listing = await codemode.ls({ path: "/workspace" }); |
| 85 | + const shell = await codemode.exec({ command: "pwd" }); |
| 86 | + return { |
| 87 | + content: file.content, |
| 88 | + listed: listing.entries.some((entry) => entry.name === "message.txt"), |
| 89 | + backend: shell.backend, |
| 90 | + cwd: shell.stdout.trim() |
| 91 | + }; |
| 92 | + }`, |
| 93 | + }, |
| 94 | + }); |
| 95 | + |
| 96 | + expect(result.isError, JSON.stringify(result)).not.toBe(true); |
| 97 | + expect(readTextResult(result)).toEqual({ |
| 98 | + content: "hello from Code Mode", |
| 99 | + listed: true, |
| 100 | + backend: "worker-shell", |
| 101 | + cwd: "/workspace", |
| 102 | + }); |
| 103 | + |
| 104 | + const persisted = await client.callTool({ |
| 105 | + name: "code", |
| 106 | + arguments: { |
| 107 | + code: `async () => { |
| 108 | + const file = await codemode.read({ path: "/workspace/message.txt" }); |
| 109 | + return file.content; |
| 110 | + }`, |
| 111 | + }, |
| 112 | + }); |
| 113 | + expect(readTextResult(persisted)).toBe("hello from Code Mode"); |
| 114 | + |
| 115 | + const outbound = await client.callTool({ |
| 116 | + name: "code", |
| 117 | + arguments: { |
| 118 | + code: `async () => { |
| 119 | + const response = await fetch("https://example.com"); |
| 120 | + return response.status; |
| 121 | + }`, |
| 122 | + }, |
| 123 | + }); |
| 124 | + expect(outbound.isError).toBe(true); |
| 125 | + }); |
| 126 | +}); |
| 127 | + |
| 128 | +function readTextResult(result: Awaited<ReturnType<Client["callTool"]>>) { |
| 129 | + const content = result.content as Array<{ type: string; text?: string }>; |
| 130 | + const text = content.find((item) => item.type === "text"); |
| 131 | + if (!text?.text) throw new Error("Expected a text MCP result."); |
| 132 | + try { |
| 133 | + return JSON.parse(text.text) as unknown; |
| 134 | + } catch { |
| 135 | + return text.text; |
| 136 | + } |
| 137 | +} |
0 commit comments