#
tokens: 3228/50000 6/6 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

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

# Files

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

```
.env
node_modules
.DS_Store
dist
```

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

```markdown
<div id="toc" align="center">
  <ul style="list-style: none">
    <summary>
      <h1><img src="images/wow.gif" alt="Scrapybara" width="24"> Scrapybara MCP <img src="images/wow.gif" alt="Scrapybara" width="24"></h1>
    </summary>
  </ul>
</div>

<p align="center">
  <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>
  <a href="https://discord.gg/s4bPUVFXqA"><img alt="Discord" src="https://img.shields.io/badge/Discord-Join%20the%20community-6D1CCF.svg?logo=discord" /></a>
  <a href="https://x.com/scrapybara"><img alt="X" src="https://img.shields.io/badge/Twitter-Follow%20us-6D1CCF.svg?logo=X" /></a>

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.

## Prerequisites

- Node.js 18+
- pnpm
- Scrapybara API key (get one at [scrapybara.com](https://scrapybara.com))

## Installation

1. Clone the repository:

```bash
git clone https://github.com/scrapybara/scrapybara-mcp.git
cd scrapybara-mcp
```

2. Install dependencies:

```bash
pnpm install
```

3. Build the project:

```bash
pnpm build
```

4. Add the following to your MCP client config:

```json
{
  "mcpServers": {
    "scrapybara-mcp": {
      "command": "node",
      "args": ["path/to/scrapybara-mcp/dist/index.js"],
      "env": {
        "SCRAPYBARA_API_KEY": "<YOUR_SCRAPYBARA_API_KEY>",
        "ACT_MODEL": "<YOUR_ACT_MODEL>", // "anthropic" or "openai"
        "AUTH_STATE_ID": "<YOUR_AUTH_STATE_ID>" // Optional, for authenticating the browser
      }
    }
  }
}
```

5. Restart your MCP client and you're good to go!

## Tools

- **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.
- **get_instances** - Get all running Scrapybara instances.
- **stop_instance** - Stop a running Scrapybara instance.
- **bash** - Run a bash command in a Scrapybara instance.
- **act** - Take action on a Scrapybara instance through an agent. The agent can control the instance with mouse/keyboard and bash commands.

## Contributing

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!

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.

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).

```

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

```json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "node",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "allowImportingTsExtensions": false,
    "noEmit": false
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules"]
}

```

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

```json
{
  "name": "scrapybara-mcp",
  "version": "0.1.0",
  "description": "MCP server for Scrapybara",
  "license": "MIT",
  "author": "Scrapybara",
  "homepage": "https://scrapybara.com",
  "bugs": "https://github.com/scrapybara/scrapybara-mcp/issues",
  "type": "module",
  "bin": {
    "mcp-server-scrapybara": "dist/index.js"
  },
  "files": [
    "dist"
  ],
  "scripts": {
    "build": "tsc && shx chmod +x dist/*.js",
    "watch": "tsc --watch"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^1.7.0",
    "scrapybara": "^2.4.1",
    "zod": "^3.24.2",
    "zod-to-json-schema": "^3.24.4"
  },
  "devDependencies": {
    "@types/node": "^22",
    "shx": "^0.3.4",
    "typescript": "^5.6.2"
  }
}

```

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

```typescript
import { z } from "zod";

export const CancellationNotificationSchema = z.object({
  method: z.literal("notifications/cancelled"),
  params: z.object({
    requestId: z.string(),
  }),
});

export const StartInstanceSchema = z.object({});

export const GetInstancesSchema = z.object({});

export const StopInstanceSchema = z.object({
  instance_id: z.string().describe("The ID of the instance to stop."),
});

export const BashSchema = z.object({
  instance_id: z
    .string()
    .describe("The ID of the instance to run the command on."),
  command: z.string().describe("The command to run in the instance shell."),
});

export const ActSchema = z.object({
  instance_id: z.string().describe("The ID of the instance to act on."),
  prompt: z.string().describe(`The prompt to act on.
<EXAMPLES>
- Go to https://ycombinator.com/companies, set batch filter to W25, and extract all company names.
- Find the best way to contact Scrapybara.
- Order a Big Mac from McDonald's on Doordash.
</EXAMPLES>
`),
  schema: z
    .any()
    .optional()
    .describe("Optional schema if you want to extract structured output."),
});

```

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

```typescript
#!/usr/bin/env node

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
  TextContent,
} from "@modelcontextprotocol/sdk/types.js";
import { zodToJsonSchema } from "zod-to-json-schema";
import { z } from "zod";

import { ScrapybaraClient, UbuntuInstance, Scrapybara } from "scrapybara";
import {
  anthropic,
  UBUNTU_SYSTEM_PROMPT as ANTHROPIC_UBUNTU_SYSTEM_PROMPT,
} from "scrapybara/anthropic/index.js";
import {
  openai,
  UBUNTU_SYSTEM_PROMPT as OPENAI_UBUNTU_SYSTEM_PROMPT,
} from "scrapybara/openai/index.js";
import { bashTool, computerTool, editTool } from "scrapybara/tools/index.js";

import {
  StopInstanceSchema,
  BashSchema,
  ActSchema,
  StartInstanceSchema,
  GetInstancesSchema,
  CancellationNotificationSchema,
} from "./schemas.js";

let actModel =
  process.env.ACT_MODEL === "anthropic"
    ? anthropic()
    : process.env.ACT_MODEL === "openai"
    ? openai()
    : anthropic(); // Default to Anthropic

let actSystem =
  process.env.ACT_MODEL === "anthropic"
    ? ANTHROPIC_UBUNTU_SYSTEM_PROMPT
    : process.env.ACT_MODEL === "openai"
    ? OPENAI_UBUNTU_SYSTEM_PROMPT
    : ANTHROPIC_UBUNTU_SYSTEM_PROMPT; // Default to Anthropic's prompt

let currentController: AbortController | null = null;

const server = new Server(
  {
    name: "scrapybara-mcp",
    version: "0.1.0",
  },
  {
    capabilities: {
      tools: {},
      notifications: {},
    },
  }
);

server.setNotificationHandler(CancellationNotificationSchema, async () => {
  if (currentController) {
    currentController.abort();
    currentController = null;
  }
});

server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "start_instance",
        description:
          "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.",
        inputSchema: zodToJsonSchema(StartInstanceSchema),
      },
      {
        name: "get_instances",
        description: "Get all running Scrapybara instances.",
        inputSchema: zodToJsonSchema(GetInstancesSchema),
      },
      {
        name: "stop_instance",
        description: "Stop a running Scrapybara instance.",
        inputSchema: zodToJsonSchema(StopInstanceSchema),
      },
      {
        name: "bash",
        description: "Run a bash command in a Scrapybara instance.",
        inputSchema: zodToJsonSchema(BashSchema),
      },
      {
        name: "act",
        description:
          "Take action on a Scrapybara instance through an agent. The agent can control the instance with mouse/keyboard and bash commands.",
        inputSchema: zodToJsonSchema(ActSchema),
      },
    ],
  };
});

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  try {
    if (!request.params.arguments) {
      throw new Error("Arguments are required");
    }

    currentController = new AbortController();

    const client = new ScrapybaraClient({
      apiKey: process.env.SCRAPYBARA_API_KEY,
    });

    switch (request.params.name) {
      case "start_instance": {
        const instance = await client.startUbuntu();
        await instance.browser.start({
          abortSignal: currentController.signal,
        });

        if (process.env.AUTH_STATE_ID) {
          await instance.browser.authenticate(
            {
              authStateId: process.env.AUTH_STATE_ID,
            },
            { abortSignal: currentController.signal }
          );
        }

        const streamUrlResponse = await instance.getStreamUrl({
          abortSignal: currentController.signal,
        });

        const streamUrl = streamUrlResponse.streamUrl;
        return {
          content: [
            {
              type: "text",
              text: JSON.stringify({ ...instance, streamUrl }, null, 2),
            } as TextContent,
          ],
        };
      }

      case "get_instances": {
        const instances = await client.getInstances({
          abortSignal: currentController.signal,
        });

        return {
          content: [
            {
              type: "text",
              text: JSON.stringify(instances, null, 2),
            } as TextContent,
          ],
        };
      }

      case "stop_instance": {
        const args = StopInstanceSchema.parse(request.params.arguments);
        const instance = await client.get(args.instance_id, {
          abortSignal: currentController.signal,
        });

        const response = await instance.stop({
          abortSignal: currentController.signal,
        });

        return {
          content: [
            {
              type: "text",
              text: JSON.stringify(response, null, 2),
            } as TextContent,
          ],
        };
      }

      case "bash": {
        const args = BashSchema.parse(request.params.arguments);
        const instance = await client.get(args.instance_id, {
          abortSignal: currentController.signal,
        });

        if ("bash" in instance) {
          const response = await instance.bash(
            { command: args.command },
            { abortSignal: currentController.signal }
          );

          return {
            content: [
              {
                type: "text",
                text: JSON.stringify(response, null, 2),
              } as TextContent,
            ],
          };
        } else {
          throw new Error("Instance does not support bash commands");
        }
      }

      case "act": {
        const args = ActSchema.parse(request.params.arguments);
        const instance = await client.get(args.instance_id, {
          abortSignal: currentController.signal,
        });

        const tools: Scrapybara.Tool[] = [computerTool(instance)];

        if (instance instanceof UbuntuInstance) {
          tools.push(bashTool(instance));
          tools.push(editTool(instance));
        }

        const actResponse = await client.act({
          model: actModel,
          tools,
          system: actSystem,
          prompt: args.prompt,
          schema: args.schema,
          requestOptions: {
            abortSignal: currentController.signal,
          },
        });

        return {
          content: [
            {
              type: "text",
              text: JSON.stringify(
                { text: actResponse.text, output: actResponse.output },
                null,
                2
              ),
            } as TextContent,
          ],
        };
      }

      default:
        throw new Error(`Unknown tool: ${request.params.name}`);
    }
  } catch (error) {
    if (error instanceof z.ZodError) {
      throw new Error(`Invalid input: ${JSON.stringify(error.errors)}`);
    }
    if (error instanceof Error && error.name === "AbortError") {
      return {
        content: [
          {
            type: "text",
            text: JSON.stringify(
              { status: "Operation was cancelled." },
              null,
              2
            ),
          } as TextContent,
        ],
      };
    }
    throw error;
  }
});

async function runServer() {
  const transport = new StdioServerTransport();
  await server.connect(transport);
}

runServer().catch((error) => {
  const errorMsg = error instanceof Error ? error.message : String(error);
  console.error(errorMsg);
});

```