---
title: "Custom Tools"
description: "Give agents custom tools inside the sandbox using MCP servers or skills."
sidebarTitle: "Custom Tools"
icon: "wrench"
---

There are two ways to give agents custom tools that run inside the sandbox:

| | MCP Server | Skill |
|---|---|---|
| **How it works** | Sandbox Agent spawns your MCP server process and routes tool calls to it via stdio | A markdown file that instructs the agent to run your script with `node` (or any command) |
| **Tool discovery** | Agent sees tools automatically via MCP protocol | Agent reads instructions from the skill file |
| **Best for** | Structured tools with typed inputs/outputs | Lightweight scripts with natural-language instructions |
| **Requires** | `@modelcontextprotocol/sdk` dependency | Just a markdown file and a script |

Both approaches execute code inside the sandbox, so your tools have full access to the sandbox filesystem, network, and installed system tools.

## Option A: Tools via MCP

<Steps>
  <Step title="Write your MCP server">
    Create an MCP server that exposes tools using `@modelcontextprotocol/sdk` with `StdioServerTransport`. This server will run inside the sandbox.

    ```ts src/mcp-server.ts
    import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
    import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
    import { z } from "zod";

    const server = new McpServer({
      name: "rand",
      version: "1.0.0",
    });

    server.tool(
      "random_number",
      "Generate a random integer between min and max (inclusive)",
      {
        min: z.number().describe("Minimum value"),
        max: z.number().describe("Maximum value"),
      },
      async ({ min, max }) => ({
        content: [{ type: "text", text: String(Math.floor(Math.random() * (max - min + 1)) + min) }],
      }),
    );

    const transport = new StdioServerTransport();
    await server.connect(transport);
    ```

    This is a simple example. Your MCP server runs inside the sandbox, so you can execute any code you'd like: query databases, call internal APIs, run shell commands, or interact with any service available in the container.
  </Step>

  <Step title="Package the MCP server">
    Bundle into a single JS file so it can be uploaded and executed without a `node_modules` folder.

    ```bash
    npx esbuild src/mcp-server.ts --bundle --format=cjs --platform=node --target=node18 --minify --outfile=dist/mcp-server.cjs
    ```

    This creates `dist/mcp-server.cjs` ready to upload.
  </Step>

  <Step title="Create sandbox and upload MCP server">
    Start your sandbox, then write the bundled file into it.

    <CodeGroup>
    ```ts TypeScript
    import { SandboxAgent } from "sandbox-agent";
    import fs from "node:fs";

    const client = await SandboxAgent.connect({
      baseUrl: "http://127.0.0.1:2468",
      token: process.env.SANDBOX_TOKEN,
    });

    const content = await fs.promises.readFile("./dist/mcp-server.cjs");
    await client.writeFsFile(
      { path: "/opt/mcp/custom-tools/mcp-server.cjs" },
      content,
    );
    ```

    ```bash cURL
    curl -X PUT "http://127.0.0.1:2468/v1/fs/file?path=/opt/mcp/custom-tools/mcp-server.cjs" \
      -H "Authorization: Bearer $SANDBOX_TOKEN" \
      --data-binary @./dist/mcp-server.cjs
    ```
    </CodeGroup>
  </Step>

  <Step title="Create a session">
    Point an MCP server config at the bundled JS file. When the session starts, Sandbox Agent spawns the MCP server process and routes tool calls to it.

    <CodeGroup>
    ```ts TypeScript
    await client.createSession("custom-tools", {
      agent: "claude",
      mcp: {
        customTools: {
          type: "local",
          command: ["node", "/opt/mcp/custom-tools/mcp-server.cjs"],
        },
      },
    });
    ```

    ```bash cURL
    curl -X POST "http://127.0.0.1:2468/v1/sessions/custom-tools" \
      -H "Authorization: Bearer $SANDBOX_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "agent": "claude",
        "mcp": {
          "customTools": {
            "type": "local",
            "command": ["node", "/opt/mcp/custom-tools/mcp-server.cjs"]
          }
        }
      }'
    ```
    </CodeGroup>
  </Step>
</Steps>

## Option B: Tools via Skills

Skills are markdown files that instruct the agent how to use a script. Upload the script and a skill file, then point the session at the skill directory.

<Steps>
  <Step title="Write your script">
    Write a script that the agent will execute. This runs inside the sandbox just like an MCP server, but the agent invokes it directly via its shell tool.

    ```ts src/random-number.ts
    const min = Number(process.argv[2]);
    const max = Number(process.argv[3]);

    if (Number.isNaN(min) || Number.isNaN(max)) {
      console.error("Usage: random-number <min> <max>");
      process.exit(1);
    }

    console.log(Math.floor(Math.random() * (max - min + 1)) + min);
    ```
  </Step>

  <Step title="Write a skill file">
    Create a `SKILL.md` that tells the agent what the script does and how to run it. The frontmatter `name` and `description` fields are required. See [Skill authoring best practices](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices) for tips on writing effective skills.

    ```md SKILL.md
    ---
    name: random-number
    description: Generate a random integer between min and max (inclusive). Use when the user asks for a random number.
    ---

    To generate a random number, run:

    ```bash
    node /opt/skills/random-number/random-number.cjs <min> <max>
    ```

    This prints a single random integer between min and max (inclusive).
  </Step>

  <Step title="Package the script">
    Bundle the script just like an MCP server so it has no dependencies at runtime.

    ```bash
    npx esbuild src/random-number.ts --bundle --format=cjs --platform=node --target=node18 --minify --outfile=dist/random-number.cjs
    ```
  </Step>

  <Step title="Create sandbox and upload files">
    Upload both the bundled script and the skill file.

    <CodeGroup>
    ```ts TypeScript
    import { SandboxAgent } from "sandbox-agent";
    import fs from "node:fs";

    const client = await SandboxAgent.connect({
      baseUrl: "http://127.0.0.1:2468",
      token: process.env.SANDBOX_TOKEN,
    });

    const script = await fs.promises.readFile("./dist/random-number.cjs");
    await client.writeFsFile(
      { path: "/opt/skills/random-number/random-number.cjs" },
      script,
    );

    const skill = await fs.promises.readFile("./SKILL.md");
    await client.writeFsFile(
      { path: "/opt/skills/random-number/SKILL.md" },
      skill,
    );
    ```

    ```bash cURL
    curl -X PUT "http://127.0.0.1:2468/v1/fs/file?path=/opt/skills/random-number/random-number.cjs" \
      -H "Authorization: Bearer $SANDBOX_TOKEN" \
      --data-binary @./dist/random-number.cjs

    curl -X PUT "http://127.0.0.1:2468/v1/fs/file?path=/opt/skills/random-number/SKILL.md" \
      -H "Authorization: Bearer $SANDBOX_TOKEN" \
      --data-binary @./SKILL.md
    ```
    </CodeGroup>
  </Step>

  <Step title="Create a session">
    Point the session at the skill directory. The agent reads `SKILL.md` and learns how to use your script.

    <CodeGroup>
    ```ts TypeScript
    await client.createSession("custom-tools", {
      agent: "claude",
      skills: {
        sources: [
          { type: "local", source: "/opt/skills/random-number" },
        ],
      },
    });
    ```

    ```bash cURL
    curl -X POST "http://127.0.0.1:2468/v1/sessions/custom-tools" \
      -H "Authorization: Bearer $SANDBOX_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{
        "agent": "claude",
        "skills": {
          "sources": [
            { "type": "local", "source": "/opt/skills/random-number" }
          ]
        }
      }'
    ```
    </CodeGroup>
  </Step>
</Steps>

## Notes

- The sandbox image must include a Node.js runtime that can execute the bundled files.
