#
tokens: 4337/50000 6/6 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .gitignore
├── images
│   └── wow.gif
├── LICENSE
├── package.json
├── pnpm-lock.yaml
├── README.md
├── src
│   ├── index.ts
│   └── schemas.ts
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
1 | .env
2 | node_modules
3 | .DS_Store
4 | dist
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
 1 | <div id="toc" align="center">
 2 |   <ul style="list-style: none">
 3 |     <summary>
 4 |       <h1><img src="images/wow.gif" alt="Scrapybara" width="24"> Scrapybara MCP <img src="images/wow.gif" alt="Scrapybara" width="24"></h1>
 5 |     </summary>
 6 |   </ul>
 7 | </div>
 8 | 
 9 | <p align="center">
10 |   <a href="https://github.com/scrapybara/scrapybara-playground/blob/main/license"><img alt="MIT License" src="https://img.shields.io/badge/license-MIT-blue" /></a>
11 |   <a href="https://discord.gg/s4bPUVFXqA"><img alt="Discord" src="https://img.shields.io/badge/Discord-Join%20the%20community-6D1CCF.svg?logo=discord" /></a>
12 |   <a href="https://x.com/scrapybara"><img alt="X" src="https://img.shields.io/badge/Twitter-Follow%20us-6D1CCF.svg?logo=X" /></a>
13 | 
14 | A Model Context Protocol server for [Scrapybara](https://scrapybara.com). This server enables MCP clients such as [Claude Desktop](https://claude.ai/download), [Cursor](https://www.cursor.com/), and [Windsurf](https://codeium.com/windsurf) to interact with virtual Ubuntu desktops and take actions such as browsing the web, running code, and more.
15 | 
16 | ## Prerequisites
17 | 
18 | - Node.js 18+
19 | - pnpm
20 | - Scrapybara API key (get one at [scrapybara.com](https://scrapybara.com))
21 | 
22 | ## Installation
23 | 
24 | 1. Clone the repository:
25 | 
26 | ```bash
27 | git clone https://github.com/scrapybara/scrapybara-mcp.git
28 | cd scrapybara-mcp
29 | ```
30 | 
31 | 2. Install dependencies:
32 | 
33 | ```bash
34 | pnpm install
35 | ```
36 | 
37 | 3. Build the project:
38 | 
39 | ```bash
40 | pnpm build
41 | ```
42 | 
43 | 4. Add the following to your MCP client config:
44 | 
45 | ```json
46 | {
47 |   "mcpServers": {
48 |     "scrapybara-mcp": {
49 |       "command": "node",
50 |       "args": ["path/to/scrapybara-mcp/dist/index.js"],
51 |       "env": {
52 |         "SCRAPYBARA_API_KEY": "<YOUR_SCRAPYBARA_API_KEY>",
53 |         "ACT_MODEL": "<YOUR_ACT_MODEL>", // "anthropic" or "openai"
54 |         "AUTH_STATE_ID": "<YOUR_AUTH_STATE_ID>" // Optional, for authenticating the browser
55 |       }
56 |     }
57 |   }
58 | }
59 | ```
60 | 
61 | 5. Restart your MCP client and you're good to go!
62 | 
63 | ## Tools
64 | 
65 | - **start_instance** - Start a Scrapybara Ubuntu instance. Use it as a desktop sandbox to access the web or run code. Always present the stream URL to the user afterwards so they can watch the instance in real time.
66 | - **get_instances** - Get all running Scrapybara instances.
67 | - **stop_instance** - Stop a running Scrapybara instance.
68 | - **bash** - Run a bash command in a Scrapybara instance.
69 | - **act** - Take action on a Scrapybara instance through an agent. The agent can control the instance with mouse/keyboard and bash commands.
70 | 
71 | ## Contributing
72 | 
73 | Scrapybara MCP is a community-driven project. Whether you're submitting an idea, fixing a typo, adding a new tool, or improving an existing one, your contributions are greatly appreciated!
74 | 
75 | Before contributing, read through the existing issues and pull requests to see if someone else is already working on something similar. That way you can avoid duplicating efforts.
76 | 
77 | If there are more tools or features you'd like to see, feel free to suggest them on the [issues page](https://github.com/scrapybara/scrapybara-mcp/issues).
78 | 
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "compilerOptions": {
 3 |     "target": "ES2022",
 4 |     "module": "ESNext",
 5 |     "moduleResolution": "node",
 6 |     "outDir": "./dist",
 7 |     "rootDir": "./src",
 8 |     "strict": true,
 9 |     "esModuleInterop": true,
10 |     "skipLibCheck": true,
11 |     "forceConsistentCasingInFileNames": true,
12 |     "allowImportingTsExtensions": false,
13 |     "noEmit": false
14 |   },
15 |   "include": ["src/**/*.ts"],
16 |   "exclude": ["node_modules"]
17 | }
18 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "name": "scrapybara-mcp",
 3 |   "version": "0.1.0",
 4 |   "description": "MCP server for Scrapybara",
 5 |   "license": "MIT",
 6 |   "author": "Scrapybara",
 7 |   "homepage": "https://scrapybara.com",
 8 |   "bugs": "https://github.com/scrapybara/scrapybara-mcp/issues",
 9 |   "type": "module",
10 |   "bin": {
11 |     "mcp-server-scrapybara": "dist/index.js"
12 |   },
13 |   "files": [
14 |     "dist"
15 |   ],
16 |   "scripts": {
17 |     "build": "tsc && shx chmod +x dist/*.js",
18 |     "watch": "tsc --watch"
19 |   },
20 |   "dependencies": {
21 |     "@modelcontextprotocol/sdk": "^1.7.0",
22 |     "scrapybara": "^2.4.1",
23 |     "zod": "^3.24.2",
24 |     "zod-to-json-schema": "^3.24.4"
25 |   },
26 |   "devDependencies": {
27 |     "@types/node": "^22",
28 |     "shx": "^0.3.4",
29 |     "typescript": "^5.6.2"
30 |   }
31 | }
32 | 
```

--------------------------------------------------------------------------------
/src/schemas.ts:
--------------------------------------------------------------------------------

```typescript
 1 | import { z } from "zod";
 2 | 
 3 | export const CancellationNotificationSchema = z.object({
 4 |   method: z.literal("notifications/cancelled"),
 5 |   params: z.object({
 6 |     requestId: z.string(),
 7 |   }),
 8 | });
 9 | 
10 | export const StartInstanceSchema = z.object({});
11 | 
12 | export const GetInstancesSchema = z.object({});
13 | 
14 | export const StopInstanceSchema = z.object({
15 |   instance_id: z.string().describe("The ID of the instance to stop."),
16 | });
17 | 
18 | export const BashSchema = z.object({
19 |   instance_id: z
20 |     .string()
21 |     .describe("The ID of the instance to run the command on."),
22 |   command: z.string().describe("The command to run in the instance shell."),
23 | });
24 | 
25 | export const ActSchema = z.object({
26 |   instance_id: z.string().describe("The ID of the instance to act on."),
27 |   prompt: z.string().describe(`The prompt to act on.
28 | <EXAMPLES>
29 | - Go to https://ycombinator.com/companies, set batch filter to W25, and extract all company names.
30 | - Find the best way to contact Scrapybara.
31 | - Order a Big Mac from McDonald's on Doordash.
32 | </EXAMPLES>
33 | `),
34 |   schema: z
35 |     .any()
36 |     .optional()
37 |     .describe("Optional schema if you want to extract structured output."),
38 | });
39 | 
```

--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------

```typescript
  1 | #!/usr/bin/env node
  2 | 
  3 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
  4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
  5 | import {
  6 |   CallToolRequestSchema,
  7 |   ListToolsRequestSchema,
  8 |   TextContent,
  9 | } from "@modelcontextprotocol/sdk/types.js";
 10 | import { zodToJsonSchema } from "zod-to-json-schema";
 11 | import { z } from "zod";
 12 | 
 13 | import { ScrapybaraClient, UbuntuInstance, Scrapybara } from "scrapybara";
 14 | import {
 15 |   anthropic,
 16 |   UBUNTU_SYSTEM_PROMPT as ANTHROPIC_UBUNTU_SYSTEM_PROMPT,
 17 | } from "scrapybara/anthropic/index.js";
 18 | import {
 19 |   openai,
 20 |   UBUNTU_SYSTEM_PROMPT as OPENAI_UBUNTU_SYSTEM_PROMPT,
 21 | } from "scrapybara/openai/index.js";
 22 | import { bashTool, computerTool, editTool } from "scrapybara/tools/index.js";
 23 | 
 24 | import {
 25 |   StopInstanceSchema,
 26 |   BashSchema,
 27 |   ActSchema,
 28 |   StartInstanceSchema,
 29 |   GetInstancesSchema,
 30 |   CancellationNotificationSchema,
 31 | } from "./schemas.js";
 32 | 
 33 | let actModel =
 34 |   process.env.ACT_MODEL === "anthropic"
 35 |     ? anthropic()
 36 |     : process.env.ACT_MODEL === "openai"
 37 |     ? openai()
 38 |     : anthropic(); // Default to Anthropic
 39 | 
 40 | let actSystem =
 41 |   process.env.ACT_MODEL === "anthropic"
 42 |     ? ANTHROPIC_UBUNTU_SYSTEM_PROMPT
 43 |     : process.env.ACT_MODEL === "openai"
 44 |     ? OPENAI_UBUNTU_SYSTEM_PROMPT
 45 |     : ANTHROPIC_UBUNTU_SYSTEM_PROMPT; // Default to Anthropic's prompt
 46 | 
 47 | let currentController: AbortController | null = null;
 48 | 
 49 | const server = new Server(
 50 |   {
 51 |     name: "scrapybara-mcp",
 52 |     version: "0.1.0",
 53 |   },
 54 |   {
 55 |     capabilities: {
 56 |       tools: {},
 57 |       notifications: {},
 58 |     },
 59 |   }
 60 | );
 61 | 
 62 | server.setNotificationHandler(CancellationNotificationSchema, async () => {
 63 |   if (currentController) {
 64 |     currentController.abort();
 65 |     currentController = null;
 66 |   }
 67 | });
 68 | 
 69 | server.setRequestHandler(ListToolsRequestSchema, async () => {
 70 |   return {
 71 |     tools: [
 72 |       {
 73 |         name: "start_instance",
 74 |         description:
 75 |           "Start a Scrapybara Ubuntu instance. Use it as a desktop sandbox to access the web or run code. Always present the stream URL to the user afterwards so they can watch the instance in real time.",
 76 |         inputSchema: zodToJsonSchema(StartInstanceSchema),
 77 |       },
 78 |       {
 79 |         name: "get_instances",
 80 |         description: "Get all running Scrapybara instances.",
 81 |         inputSchema: zodToJsonSchema(GetInstancesSchema),
 82 |       },
 83 |       {
 84 |         name: "stop_instance",
 85 |         description: "Stop a running Scrapybara instance.",
 86 |         inputSchema: zodToJsonSchema(StopInstanceSchema),
 87 |       },
 88 |       {
 89 |         name: "bash",
 90 |         description: "Run a bash command in a Scrapybara instance.",
 91 |         inputSchema: zodToJsonSchema(BashSchema),
 92 |       },
 93 |       {
 94 |         name: "act",
 95 |         description:
 96 |           "Take action on a Scrapybara instance through an agent. The agent can control the instance with mouse/keyboard and bash commands.",
 97 |         inputSchema: zodToJsonSchema(ActSchema),
 98 |       },
 99 |     ],
100 |   };
101 | });
102 | 
103 | server.setRequestHandler(CallToolRequestSchema, async (request) => {
104 |   try {
105 |     if (!request.params.arguments) {
106 |       throw new Error("Arguments are required");
107 |     }
108 | 
109 |     currentController = new AbortController();
110 | 
111 |     const client = new ScrapybaraClient({
112 |       apiKey: process.env.SCRAPYBARA_API_KEY,
113 |     });
114 | 
115 |     switch (request.params.name) {
116 |       case "start_instance": {
117 |         const instance = await client.startUbuntu();
118 |         await instance.browser.start({
119 |           abortSignal: currentController.signal,
120 |         });
121 | 
122 |         if (process.env.AUTH_STATE_ID) {
123 |           await instance.browser.authenticate(
124 |             {
125 |               authStateId: process.env.AUTH_STATE_ID,
126 |             },
127 |             { abortSignal: currentController.signal }
128 |           );
129 |         }
130 | 
131 |         const streamUrlResponse = await instance.getStreamUrl({
132 |           abortSignal: currentController.signal,
133 |         });
134 | 
135 |         const streamUrl = streamUrlResponse.streamUrl;
136 |         return {
137 |           content: [
138 |             {
139 |               type: "text",
140 |               text: JSON.stringify({ ...instance, streamUrl }, null, 2),
141 |             } as TextContent,
142 |           ],
143 |         };
144 |       }
145 | 
146 |       case "get_instances": {
147 |         const instances = await client.getInstances({
148 |           abortSignal: currentController.signal,
149 |         });
150 | 
151 |         return {
152 |           content: [
153 |             {
154 |               type: "text",
155 |               text: JSON.stringify(instances, null, 2),
156 |             } as TextContent,
157 |           ],
158 |         };
159 |       }
160 | 
161 |       case "stop_instance": {
162 |         const args = StopInstanceSchema.parse(request.params.arguments);
163 |         const instance = await client.get(args.instance_id, {
164 |           abortSignal: currentController.signal,
165 |         });
166 | 
167 |         const response = await instance.stop({
168 |           abortSignal: currentController.signal,
169 |         });
170 | 
171 |         return {
172 |           content: [
173 |             {
174 |               type: "text",
175 |               text: JSON.stringify(response, null, 2),
176 |             } as TextContent,
177 |           ],
178 |         };
179 |       }
180 | 
181 |       case "bash": {
182 |         const args = BashSchema.parse(request.params.arguments);
183 |         const instance = await client.get(args.instance_id, {
184 |           abortSignal: currentController.signal,
185 |         });
186 | 
187 |         if ("bash" in instance) {
188 |           const response = await instance.bash(
189 |             { command: args.command },
190 |             { abortSignal: currentController.signal }
191 |           );
192 | 
193 |           return {
194 |             content: [
195 |               {
196 |                 type: "text",
197 |                 text: JSON.stringify(response, null, 2),
198 |               } as TextContent,
199 |             ],
200 |           };
201 |         } else {
202 |           throw new Error("Instance does not support bash commands");
203 |         }
204 |       }
205 | 
206 |       case "act": {
207 |         const args = ActSchema.parse(request.params.arguments);
208 |         const instance = await client.get(args.instance_id, {
209 |           abortSignal: currentController.signal,
210 |         });
211 | 
212 |         const tools: Scrapybara.Tool[] = [computerTool(instance)];
213 | 
214 |         if (instance instanceof UbuntuInstance) {
215 |           tools.push(bashTool(instance));
216 |           tools.push(editTool(instance));
217 |         }
218 | 
219 |         const actResponse = await client.act({
220 |           model: actModel,
221 |           tools,
222 |           system: actSystem,
223 |           prompt: args.prompt,
224 |           schema: args.schema,
225 |           requestOptions: {
226 |             abortSignal: currentController.signal,
227 |           },
228 |         });
229 | 
230 |         return {
231 |           content: [
232 |             {
233 |               type: "text",
234 |               text: JSON.stringify(
235 |                 { text: actResponse.text, output: actResponse.output },
236 |                 null,
237 |                 2
238 |               ),
239 |             } as TextContent,
240 |           ],
241 |         };
242 |       }
243 | 
244 |       default:
245 |         throw new Error(`Unknown tool: ${request.params.name}`);
246 |     }
247 |   } catch (error) {
248 |     if (error instanceof z.ZodError) {
249 |       throw new Error(`Invalid input: ${JSON.stringify(error.errors)}`);
250 |     }
251 |     if (error instanceof Error && error.name === "AbortError") {
252 |       return {
253 |         content: [
254 |           {
255 |             type: "text",
256 |             text: JSON.stringify(
257 |               { status: "Operation was cancelled." },
258 |               null,
259 |               2
260 |             ),
261 |           } as TextContent,
262 |         ],
263 |       };
264 |     }
265 |     throw error;
266 |   }
267 | });
268 | 
269 | async function runServer() {
270 |   const transport = new StdioServerTransport();
271 |   await server.connect(transport);
272 | }
273 | 
274 | runServer().catch((error) => {
275 |   const errorMsg = error instanceof Error ? error.message : String(error);
276 |   console.error(errorMsg);
277 | });
278 | 
```