--- 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 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. 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. Start your sandbox, then write the bundled file into it. ```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 ``` 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. ```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"] } } }' ``` ## 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. 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 "); process.exit(1); } console.log(Math.floor(Math.random() * (max - min + 1)) + min); ``` 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 ``` This prints a single random integer between min and max (inclusive). 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 ``` Upload both the bundled script and the skill file. ```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 ``` Point the session at the skill directory. The agent reads `SKILL.md` and learns how to use your script. ```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" } ] } }' ``` ## Notes - The sandbox image must include a Node.js runtime that can execute the bundled files.