# 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 |
```