diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e0c3c6ac..a351598e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -143,6 +143,9 @@ jobs: workspace: "@example/computer-container" path: examples/container typegen: "npx wrangler types" + - name: mcp + workspace: "@example/computer-mcp" + path: examples/mcp - name: think workspace: "@cloudflare/example-think" path: examples/think diff --git a/README.md b/README.md index e5763303..5c0c6796 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,9 @@ public surface. Each is a Worker workspace with its own README. - [`examples/worker-javascript`](examples/worker-javascript) — mirrors `worker-shell`, but `exec` evaluates an ECMAScript module in a Dynamic Worker instead of running a shell command. +- [`examples/mcp`](examples/mcp) — a Computer MCP example: + one Code Mode `code` tool backed by a durable workspace, a Worker shell, + and a full Linux container. - [`examples/think`](examples/think) — a [`@cloudflare/think`](https://www.npmjs.com/package/@cloudflare/think) chat agent that uses the workspace as its working directory, reachable from a terminal. diff --git a/examples/mcp/.gitignore b/examples/mcp/.gitignore new file mode 100644 index 00000000..07c6655d --- /dev/null +++ b/examples/mcp/.gitignore @@ -0,0 +1,5 @@ +.dev.vars +.wrangler/ +build/ +node_modules/ +worker-configuration.d.ts diff --git a/examples/mcp/Dockerfile b/examples/mcp/Dockerfile new file mode 100644 index 00000000..ee159c40 --- /dev/null +++ b/examples/mcp/Dockerfile @@ -0,0 +1,17 @@ +FROM ghcr.io/cloudflare/computer-computerd-linux-x64:0.1.1 AS computerd + +FROM node:22-bookworm-slim + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates curl fuse3 git libfuse2 \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=computerd /usr/local/bin/computerd /usr/local/bin/computerd + +ENV PORT=8080 +ENV MOUNT_POINT=/workspace +ENV FUSE_MOUNT=auto + +EXPOSE 8080 +ENTRYPOINT ["/usr/local/bin/computerd"] diff --git a/examples/mcp/README.md b/examples/mcp/README.md new file mode 100644 index 00000000..595a4fa0 --- /dev/null +++ b/examples/mcp/README.md @@ -0,0 +1,161 @@ +# Deploy a Computer MCP + +This example exposes Computer through MCP. It gives an MCP client one durable workspace, a fast Worker shell, and a full Linux container behind a single Code Mode `code` tool. + +## Deploy + +[![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/computer/tree/main/examples/mcp) + +To deploy from a clone instead, start Docker and run: + +```bash +npm install +npm run deploy --workspace @example/computer-mcp +``` + +The endpoint fails closed until you set `MCP_TOKEN`. Generate a random token rather than reusing a password: + +```bash +openssl rand -hex 32 +``` + +After deployment, add the result as an encrypted Worker secret in the Cloudflare dashboard, or set it from a clone of this repository: + +```bash +npx wrangler secret put MCP_TOKEN --config examples/mcp/wrangler.jsonc +``` + +## Connect + +Configure your MCP client to use the remote HTTP endpoint: + +```text +https://.workers.dev/mcp +``` + +Send the token on every MCP request: + +```text +Authorization: Bearer +``` + +For clients that accept MCP server configuration as JSON, the entry typically looks like this: + +```json +{ + "mcpServers": { + "computer": { + "type": "http", + "url": "https://.workers.dev/mcp", + "headers": { + "Authorization": "Bearer " + } + } + } +} +``` + +The exact configuration filename and format depend on the client. Keep the token in the client's secret storage when it provides one rather than committing it to a configuration file. + +The Worker's root URL prints its MCP endpoint and available backends. `GET /health` returns `ok` without authentication. + +## Use it + +Once connected, ask your MCP client to work in the Computer workspace. For example: + +```text +Create /workspace/hello.txt, read it back, and list the workspace files. +``` + +Commands use `worker-shell` by default. Select the container when the task needs a full Linux environment: + +```text +Use container-shell to create a small Node.js project in /workspace, install its dependencies, and run its tests. +``` + +The client sees one public MCP tool named `code`. The model uses that tool to write a small JavaScript function that combines Computer's durable filesystem and command tools: + +```js +async () => { + await codemode.write({ + path: "/workspace/package.json", + content: JSON.stringify({ scripts: { test: "node --test" } }), + }); + + const result = await codemode.exec({ + command: "npm test", + backend: "container-shell", + }); + + return { exitCode: result.exitCode, stdout: result.stdout }; +} +``` + +You do not need to call the underlying Computer tools individually. The `code` tool describes these functions and backends to the model: + +| Function | Purpose | +| --- | --- | +| `codemode.read({ path, offset?, limit? })` | Read a file, optionally one range at a time. | +| `codemode.ls({ path })` | List a directory. | +| `codemode.write({ path, content })` | Create or replace a file. | +| `codemode.edit({ path, edits })` | Apply exact text replacements to a file. | +| `codemode.exec({ command, cwd?, backend?, env? })` | Run a command, using `worker-shell` unless another backend is selected. | + +## How it works + +`@cloudflare/codemode` runs Code Mode orchestration code in an isolated Dynamic Worker with outbound networking disabled. Tool calls return to the Durable Object and operate on its Computer workspace. + +| Backend | Use it for | +| --- | --- | +| `worker-shell` | The fast default for common commands. It has no ambient network access; its built-in Git command supports HTTPS remotes. | +| `container-shell` | Full Debian Linux with Node.js, npm, git, native binaries, and outbound networking. | + +The model can select a backend in `codemode.exec()`. The example does not retry automatically, so backend choice, cost, and failures remain visible. + +The container starts only when `container-shell` is selected. Computer synchronizes `/workspace` between the Durable Object and the container's FUSE mount before and after each command. + +## Run locally + +Local development requires a running Docker daemon for the Linux container. From the repository root: + +```bash +npm install +printf 'MCP_TOKEN=development-token\n' > examples/mcp/.dev.vars +npm run dev --workspace @example/computer-mcp +``` + +The `predev` script builds the workspace packages before Wrangler starts. On the first run, Wrangler also builds the container image. Connect to `http://127.0.0.1:8787/mcp` with the same bearer token. + +## Validate + +```bash +npm run typecheck --workspace @example/computer-mcp +npm test --workspace @example/computer-mcp +``` + +The workerd integration test authenticates a real MCP client, verifies that only `code` is public, runs filesystem and Worker-shell operations, and confirms that files persist across calls. It does not start the Linux container. + +## Debug + +Check the public routes first: + +```bash +curl https://.workers.dev/health +curl https://.workers.dev/ +``` + +Then stream Worker and Durable Object logs: + +```bash +npx wrangler tail --config examples/mcp/wrangler.jsonc +``` + +A `401` means the bearer token is missing or incorrect. A `503` means `MCP_TOKEN` has not been configured. Backend failures are returned in the `codemode.exec()` result with the selected backend name. + +## Security model + +This example is intentionally single-user. Every authenticated request reaches the same Durable Object and workspace. Keep `MCP_TOKEN` private and deploy a separate copy for each trust boundary. + +Code Mode's orchestration Worker and the Worker shell cannot make arbitrary outbound requests. The Worker shell's built-in Git command can use HTTPS remotes. The Linux container has outbound access so package managers and development tools work. + +For a multi-user service, replace the bearer-token check with OAuth, derive the Durable Object name from the authenticated subject, and add per-user execution and storage limits. diff --git a/examples/mcp/package.json b/examples/mcp/package.json new file mode 100644 index 00000000..c407bc48 --- /dev/null +++ b/examples/mcp/package.json @@ -0,0 +1,31 @@ +{ + "name": "@example/computer-mcp", + "version": "0.0.0", + "private": true, + "type": "module", + "description": "Expose a durable Computer workspace through MCP.", + "scripts": { + "build:computer": "npm run build --workspace @cloudflare/computer", + "predev": "npm run build:computer", + "dev": "wrangler dev", + "predeploy": "npm run build:computer", + "deploy": "wrangler deploy", + "test": "vitest run --config vitest.config.ts", + "typecheck": "tsc --noEmit", + "cf-typegen": "wrangler types" + }, + "dependencies": { + "@cloudflare/codemode": "^0.5.0", + "@cloudflare/computer": "*", + "@modelcontextprotocol/sdk": "1.30.0", + "ai": "^7.0.0", + "zod": "^4.4.3" + }, + "devDependencies": { + "@cloudflare/vitest-pool-workers": "^0.16.10", + "@cloudflare/workers-types": "^4.20260616.1", + "typescript": "^6.0.3", + "vitest": "^4.1.7", + "wrangler": "^4.107.1" + } +} diff --git a/examples/mcp/src/index.test.ts b/examples/mcp/src/index.test.ts new file mode 100644 index 00000000..79ec84f7 --- /dev/null +++ b/examples/mcp/src/index.test.ts @@ -0,0 +1,140 @@ +import { env, SELF } from "cloudflare:test"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; +import { afterEach, describe, expect, it } from "vitest"; + +let client: Client | undefined; + +const authorizedFetch: typeof fetch = (input, init = {}) => { + const headers = new Headers(init.headers); + headers.set("authorization", "Bearer test-token"); + return SELF.fetch(input, { ...init, headers }); +}; + +afterEach(async () => { + await client?.close(); + client = undefined; +}); + +describe("Computer Code Mode MCP", () => { + it("serves setup and health routes without authentication", async () => { + const home = await SELF.fetch("https://example.test/"); + expect(home.status).toBe(200); + expect(await home.text()).toContain("https://example.test/mcp"); + + const health = await SELF.fetch("https://example.test/health"); + expect(health.status).toBe(200); + expect(await health.text()).toBe("ok\n"); + }); + + it("requires the configured bearer token", async () => { + const missing = await SELF.fetch("https://example.test/mcp", { method: "POST" }); + expect(missing.status).toBe(401); + expect(missing.headers.get("www-authenticate")).toBe("Bearer"); + + const wrong = await SELF.fetch("https://example.test/mcp", { + method: "POST", + headers: { authorization: "Bearer test-tokem" }, + }); + expect(wrong.status).toBe(401); + + const get = await authorizedFetch("https://example.test/mcp"); + expect(get.status).toBe(405); + expect(get.headers.get("allow")).toBe("POST"); + + const { COMPUTER_MCP } = env as unknown as { + COMPUTER_MCP: DurableObjectNamespace; + }; + const id = COMPUTER_MCP.idFromName("direct-auth-test"); + const direct = await COMPUTER_MCP.get(id).fetch("https://example.test/mcp", { + method: "POST", + }); + expect(direct.status).toBe(401); + }); + + it("exposes durable Computer tools through one Code Mode tool", async () => { + client = new Client({ name: "computer-mcp-test", version: "1.0.0" }); + const transport = new StreamableHTTPClientTransport(new URL("https://example.test/mcp"), { + fetch: authorizedFetch, + }); + await client.connect(transport); + + const listed = await client.listTools(); + expect(listed.tools.map((tool) => tool.name)).toEqual(["code"]); + const description = listed.tools[0]?.description; + expect(description).toContain("codemode.read"); + expect(description).toContain('"worker-shell"'); + expect(description).toContain("no ambient outbound network"); + expect(description).toContain("HTTPS URLs"); + expect(description).toContain("Cannot run npm"); + expect(description).toContain('"container-shell"'); + expect(description).toContain("Full Debian Linux"); + expect(description).toContain("Cold starts more slowly"); + + const result = await client.callTool({ + name: "code", + arguments: { + code: `async () => { + await codemode.write({ path: "/workspace/message.txt", content: "hello" }); + await codemode.edit({ + path: "/workspace/message.txt", + edits: [{ oldText: "hello", newText: "hello from Code Mode" }] + }); + const file = await codemode.read({ path: "/workspace/message.txt" }); + const listing = await codemode.ls({ path: "/workspace" }); + const shell = await codemode.exec({ command: "pwd" }); + const git = await codemode.exec({ command: "git init && git status --short" }); + return { + content: file.content, + listed: listing.entries.some((entry) => entry.name === "message.txt"), + backend: shell.backend, + cwd: shell.stdout.trim(), + gitWorked: git.exitCode === 0 && git.stdout.includes("message.txt") + }; + }`, + }, + }); + + expect(result.isError, JSON.stringify(result)).not.toBe(true); + expect(readTextResult(result)).toEqual({ + content: "hello from Code Mode", + listed: true, + backend: "worker-shell", + cwd: "/workspace", + gitWorked: true, + }); + + const persisted = await client.callTool({ + name: "code", + arguments: { + code: `async () => { + const file = await codemode.read({ path: "/workspace/message.txt" }); + return file.content; + }`, + }, + }); + expect(readTextResult(persisted)).toBe("hello from Code Mode"); + + const outbound = await client.callTool({ + name: "code", + arguments: { + code: `async () => { + const response = await fetch("https://example.com"); + return response.status; + }`, + }, + }); + expect(outbound.isError).toBe(true); + }); +}); + +function readTextResult(result: Awaited>) { + const content = result.content as Array<{ type: string; text?: string }>; + const text = content.find((item) => item.type === "text"); + if (!text?.text) throw new Error("Expected a text MCP result."); + try { + return JSON.parse(text.text) as unknown; + } catch { + return text.text; + } +} diff --git a/examples/mcp/src/index.ts b/examples/mcp/src/index.ts new file mode 100644 index 00000000..e8f56f82 --- /dev/null +++ b/examples/mcp/src/index.ts @@ -0,0 +1,130 @@ +import { DurableObject } from "cloudflare:workers"; +import { + type DurableObjectStorageLike, + getWorkspace, + type WorkspaceOptions, + WorkspaceProxy, + WorkspaceServiceProxy, + withWorkspace, +} from "@cloudflare/computer"; +import { + CloudflareContainerBackend, + withWorkspaceContainer, +} from "@cloudflare/computer/backends/container"; +import { WorkerShellBackend } from "@cloudflare/computer/backends/worker-shell"; +import { createGitClient } from "@cloudflare/computer/git"; +import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"; + +import { createComputerMCPServer } from "./server.js"; + +interface Env { + LOADER: WorkerLoader; + MCP_TOKEN?: string; + COMPUTER_MCP: DurableObjectNamespace; +} + +export { WorkspaceProxy, WorkspaceServiceProxy }; + +const TOKEN_ENCODER = new TextEncoder(); + +class ComputerMCPDurableObject extends DurableObject {} + +class ComputerMCPBase extends withWorkspaceContainer(ComputerMCPDurableObject) { + readonly workerShell = new WorkerShellBackend({ + loader: this.env.LOADER, + workspace: { binding: "COMPUTER_MCP", id: this.ctx.id.toString() }, + ctx: this.ctx, + egress: { mode: "none" }, + }); + + readonly containerShell = new CloudflareContainerBackend({ + container: () => this, + workspace: { binding: "COMPUTER_MCP", id: this.ctx.id.toString() }, + egress: { mode: "direct" }, + }); +} + +function workspaceOptions(self: InstanceType): WorkspaceOptions { + const { ctx } = self as unknown as { ctx: DurableObjectState }; + return { + storage: ctx.storage as unknown as DurableObjectStorageLike, + sessionId: ctx.id.toString(), + git: createGitClient(), + backends: [self.workerShell, self.containerShell], + }; +} + +/** One durable Computer workspace exposed through a Code Mode MCP server. */ +export class ComputerMCP extends withWorkspace(ComputerMCPBase, workspaceOptions) { + override async fetch(request: Request): Promise { + const path = new URL(request.url).pathname; + // computerd reaches this callback through an internal binding. The public + // Worker forwards only /mcp. + if (path === "/ws") return this.containerShell.handleFetch(request); + if (path !== "/mcp") return new Response("not found", { status: 404 }); + + const unauthorized = authorize(request, this.env.MCP_TOKEN); + if (unauthorized) return unauthorized; + if (request.method !== "POST") return methodNotAllowed(); + + const server = await createComputerMCPServer(await getWorkspace(this), this.env.LOADER); + const transport = new WebStandardStreamableHTTPServerTransport(); + await server.connect(transport); + return transport.handleRequest(request); + } +} + +export default { + fetch(request: Request, env: Env): Response | Promise { + const url = new URL(request.url); + if (url.pathname === "/health") return new Response("ok\n"); + if (url.pathname === "/") return home(url.origin); + if (url.pathname !== "/mcp") return new Response("not found", { status: 404 }); + + const unauthorized = authorize(request, env.MCP_TOKEN); + if (unauthorized) return unauthorized; + + const id = env.COMPUTER_MCP.idFromName("computer"); + return env.COMPUTER_MCP.get(id).fetch(request); + }, +} satisfies ExportedHandler; + +function authorize(request: Request, token: string | undefined): Response | undefined { + if (!token) return new Response("MCP_TOKEN is not configured.\n", { status: 503 }); + + const supplied = TOKEN_ENCODER.encode(request.headers.get("authorization") ?? ""); + const expected = TOKEN_ENCODER.encode(`Bearer ${token}`); + if ( + supplied.byteLength === expected.byteLength && + crypto.subtle.timingSafeEqual(supplied, expected) + ) { + return undefined; + } + + return new Response("Unauthorized\n", { + status: 401, + headers: { "www-authenticate": "Bearer" }, + }); +} + +function methodNotAllowed() { + return new Response("The stateless MCP endpoint accepts POST requests only.\n", { + status: 405, + headers: { allow: "POST" }, + }); +} + +function home(origin: string) { + return new Response( + [ + "Computer MCP", + "", + `Endpoint: ${origin}/mcp`, + "Authorization: Bearer ", + "Default backend: worker-shell", + "Full Linux backend: container-shell", + "", + ].join("\n"), + { headers: { "content-type": "text/plain; charset=utf-8" } }, + ); +} diff --git a/examples/mcp/src/server.ts b/examples/mcp/src/server.ts new file mode 100644 index 00000000..090275db --- /dev/null +++ b/examples/mcp/src/server.ts @@ -0,0 +1,126 @@ +import { DynamicWorkerExecutor } from "@cloudflare/codemode"; +import { codeMcpServer } from "@cloudflare/codemode/mcp"; +import type { WorkspaceClient } from "@cloudflare/computer"; +import { createAITools } from "@cloudflare/computer/tools"; +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import type { ToolSet } from "ai"; + +/** Create the one-tool Code Mode MCP server for a Computer workspace. */ +export async function createComputerMCPServer(workspace: WorkspaceClient, loader: WorkerLoader) { + const computer = new McpServer({ name: "computer", version: "1.0.0" }); + const tools = createAITools({ + workspace, + assets: false, + shell: { + backends: { + "worker-shell": { + description: + "just-bash in an isolated Dynamic Worker. Starts quickly, " + + "does not boot a container, and has no ambient outbound network. " + + "Use it for common shell commands, quick file inspection, and " + + "text transformations. Its built-in git command supports clone, " + + "status, diff, and log; clone accepts HTTPS URLs through the " + + "durable workspace. Prefer the dedicated read, write, and edit " + + "tools for file operations. Cannot run npm, Node.js, Python, " + + "package managers, or arbitrary native binaries.", + }, + "container-shell": { + description: + "Full Debian Linux in a Cloudflare Container with Node.js, npm, " + + "git, package management, native binaries, and outbound network. " + + "Use it for dependency installation, builds, tests, or commands " + + "that worker-shell cannot run. Cold starts more slowly because " + + "the container must boot; prefer worker-shell for simple tasks.", + }, + }, + defaultBackend: "worker-shell", + }, + }); + + for (const [name, tool] of Object.entries(tools)) { + registerComputerTool(computer, name, tool); + } + + return codeMcpServer({ + server: computer, + executor: new DynamicWorkerExecutor({ loader, globalOutbound: null }), + }); +} + +interface ToolCallContext { + signal: AbortSignal; +} + +type ToolResult = { + content: Array<{ type: "text"; text: string }>; + isError?: boolean; +}; + +type RegisterTool = ( + name: string, + config: { description?: string; inputSchema: unknown }, + callback: (args: unknown, context: ToolCallContext) => Promise, +) => unknown; + +type ExecuteTool = ( + args: unknown, + options: { + toolCallId: string; + messages: never[]; + abortSignal: AbortSignal; + context: undefined; + }, +) => unknown | PromiseLike; + +function registerComputerTool(server: McpServer, name: string, tool: ToolSet[string]) { + if (!tool.execute) throw new Error(`Computer tool ${name} is not executable.`); + const execute = tool.execute as ExecuteTool; + const registerTool = server.registerTool.bind(server) as RegisterTool; + registerTool( + name, + { + description: typeof tool.description === "string" ? tool.description : undefined, + inputSchema: tool.inputSchema, + }, + async (args, context) => { + try { + const execution = await execute(args, { + toolCallId: `mcp:${name}`, + messages: [], + abortSignal: context.signal, + context: undefined, + }); + const value = await finalToolValue(execution); + return { + content: [{ type: "text", text: JSON.stringify(value) ?? "undefined" }], + }; + } catch (error) { + return { + content: [ + { + type: "text", + text: error instanceof Error ? error.message : String(error), + }, + ], + isError: true, + }; + } + }, + ); +} + +async function finalToolValue(execution: unknown): Promise { + if (!isAsyncIterable(execution)) return execution; + let finalValue: unknown; + for await (const value of execution) finalValue = value; + return finalValue; +} + +function isAsyncIterable(value: unknown): value is AsyncIterable { + return ( + typeof value === "object" && + value !== null && + Symbol.asyncIterator in value && + typeof value[Symbol.asyncIterator] === "function" + ); +} diff --git a/examples/mcp/tsconfig.json b/examples/mcp/tsconfig.json new file mode 100644 index 00000000..6dbb76cf --- /dev/null +++ b/examples/mcp/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022", "ESNext.Disposable"], + "types": [ + "@cloudflare/workers-types", + "@cloudflare/vitest-pool-workers/types", + "vitest/globals" + ], + "strict": true, + "noEmit": true, + "skipLibCheck": true + }, + "include": ["src/**/*.ts"] +} diff --git a/examples/mcp/vitest.config.ts b/examples/mcp/vitest.config.ts new file mode 100644 index 00000000..8ed09668 --- /dev/null +++ b/examples/mcp/vitest.config.ts @@ -0,0 +1,13 @@ +import { cloudflareTest } from "@cloudflare/vitest-pool-workers"; +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + plugins: [ + cloudflareTest({ + wrangler: { configPath: "./wrangler.test.jsonc" }, + }), + ], + test: { + include: ["src/**/*.test.ts"], + }, +}); diff --git a/examples/mcp/wrangler.jsonc b/examples/mcp/wrangler.jsonc new file mode 100644 index 00000000..a627409b --- /dev/null +++ b/examples/mcp/wrangler.jsonc @@ -0,0 +1,23 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "computer-mcp", + "main": "src/index.ts", + "workers_dev": true, + "compatibility_date": "2026-06-17", + "compatibility_flags": ["nodejs_compat", "experimental"], + "worker_loaders": [{ "binding": "LOADER" }], + "containers": [ + { + "class_name": "ComputerMCP", + "image": "./Dockerfile", + "instance_type": "standard-2", + "max_instances": 1, + "rollout_active_grace_period": 0, + "rollout_step_percentage": [100] + } + ], + "durable_objects": { + "bindings": [{ "name": "COMPUTER_MCP", "class_name": "ComputerMCP" }] + }, + "migrations": [{ "tag": "v1", "new_sqlite_classes": ["ComputerMCP"] }] +} diff --git a/examples/mcp/wrangler.test.jsonc b/examples/mcp/wrangler.test.jsonc new file mode 100644 index 00000000..9c997419 --- /dev/null +++ b/examples/mcp/wrangler.test.jsonc @@ -0,0 +1,13 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "computer-mcp-test", + "main": "src/index.ts", + "compatibility_date": "2026-06-17", + "compatibility_flags": ["nodejs_compat", "experimental"], + "vars": { "MCP_TOKEN": "test-token" }, + "worker_loaders": [{ "binding": "LOADER" }], + "durable_objects": { + "bindings": [{ "name": "COMPUTER_MCP", "class_name": "ComputerMCP" }] + }, + "migrations": [{ "tag": "v1", "new_sqlite_classes": ["ComputerMCP"] }] +} diff --git a/package-lock.json b/package-lock.json index 8676f5da..8de9b756 100644 --- a/package-lock.json +++ b/package-lock.json @@ -81,6 +81,31 @@ "dev": true, "license": "MIT OR Apache-2.0" }, + "examples/mcp": { + "name": "@example/computer-mcp", + "version": "0.0.0", + "dependencies": { + "@cloudflare/codemode": "^0.5.0", + "@cloudflare/computer": "*", + "@modelcontextprotocol/sdk": "1.30.0", + "ai": "^7.0.0", + "zod": "^4.4.3" + }, + "devDependencies": { + "@cloudflare/vitest-pool-workers": "^0.16.10", + "@cloudflare/workers-types": "^4.20260616.1", + "typescript": "^6.0.3", + "vitest": "^4.1.7", + "wrangler": "^4.107.1" + } + }, + "examples/mcp/node_modules/@cloudflare/workers-types": { + "version": "4.20260702.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workers-types/-/workers-types-4.20260702.1.tgz", + "integrity": "sha512-mOhf5TUEB1m2vPrxtqoIGfz0fUC9xyxRDx5gWHy5s+OCo6dcV+g7wI1R7gYCMFohhqF/2y2xeKVwMwCJjfn/WA==", + "dev": true, + "license": "MIT OR Apache-2.0" + }, "examples/think": { "name": "@cloudflare/example-think", "version": "0.0.0", @@ -3104,6 +3129,10 @@ "resolved": "examples/container", "link": true }, + "node_modules/@example/computer-mcp": { + "resolved": "examples/mcp", + "link": true + }, "node_modules/@example/computer-tutorial": { "resolved": "examples/tutorial", "link": true