Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions sandbox-agent/docs/content/docs/mcp-config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,15 @@ await sdk.deleteMcpConfig({
| `enabled` | enable/disable server |
| `timeoutMs` | timeout override |

## Parallel Search MCP example

The [Parallel Search MCP example](https://github.com/rivet-dev/agents/tree/main/sandbox-agent/examples/mcp-parallel)
stores an optional remote HTTP configuration through the SDK, reloads it, and
runs `web_search` and `web_fetch` using those connection settings. The
[anonymous endpoint](https://docs.parallel.ai/integrations/mcp/search-mcp) is free
for light use without a Parallel API key, with lower rate limits than authenticated
access. The example needs no model credentials and does not start an agent session.

## Custom MCP servers

To bundle and upload your own MCP server into the sandbox, see [Custom Tools](/sandbox-agent/docs/custom-tools).
49 changes: 49 additions & 0 deletions sandbox-agent/examples/mcp-parallel/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Parallel Search MCP configuration and smoke test

Store a named Parallel Search MCP configuration through the Sandbox Agent SDK,
reload it, and check `web_search` and `web_fetch` over Streamable HTTP. The
[anonymous endpoint](https://docs.parallel.ai/integrations/mcp/search-mcp)
is free for light use without a Parallel API key and has lower rate limits than
authenticated access. This example needs no model credentials, starts no agent
session, and does not verify agent tool dispatch.

## Run

Install Node.js 20+, pnpm 9+, and
[Sandbox Agent](https://sandboxagent.dev/docs/quickstart). From `sandbox-agent/`
in this checkout, install and build the SDK and example dependencies:

```sh
pnpm --filter @sandbox-agent/example-mcp-parallel... install --frozen-lockfile
SKIP_OPENAPI_GEN=1 pnpm --filter sandbox-agent build
```

Start a local server in another terminal:

```sh
sandbox-agent server --no-token --host 127.0.0.1 --port 2468
```

Run the example from `sandbox-agent/`, specifying an absolute directory on the
server for its saved configuration:

```sh
pnpm --filter @sandbox-agent/example-mcp-parallel start http://127.0.0.1:2468 /tmp/parallel-mcp-example
```

For a server that requires a token, set `SANDBOX_TOKEN`. The example stores only
the `parallel-search` entry, reuses an identical entry, and refuses to overwrite
a different one. Other MCP entries and agent defaults remain unchanged. It
prints search excerpts and fetched content from Rivet's documentation, then
disconnects. Network errors and rate limits cause a nonzero exit.

The entry remains in the server directory's `.sandbox-agent/config/mcp.json`.
The smoke test uses the SDK's `getMcpConfig` result as its connection settings;
storing an entry alone does not attach it to a running agent session. For session
setup, see the existing [MCP example](../mcp/src/index.ts).

To remove the example entry from the local server:

```sh
curl --fail -X DELETE 'http://127.0.0.1:2468/v1/config/mcp?directory=/tmp/parallel-mcp-example&mcpName=parallel-search'
```
18 changes: 18 additions & 0 deletions sandbox-agent/examples/mcp-parallel/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "@sandbox-agent/example-mcp-parallel",
"private": true,
"type": "module",
"scripts": {
"start": "tsx src/index.ts",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.27.1",
"sandbox-agent": "workspace:*"
},
"devDependencies": {
"@types/node": "latest",
"tsx": "latest",
"typescript": "latest"
}
}
79 changes: 79 additions & 0 deletions sandbox-agent/examples/mcp-parallel/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { randomUUID } from "node:crypto";
import { isDeepStrictEqual } from "node:util";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
import { type McpServerConfig, SandboxAgent, SandboxAgentError } from "sandbox-agent";

const [baseUrl, directory] = process.argv.slice(2);
if (!baseUrl || !directory) {
throw new Error("Usage: pnpm start <sandbox-agent-url> <absolute-directory-on-server>");
}

const query = { directory, mcpName: "parallel-search" };
const config: McpServerConfig = {
type: "remote",
url: "https://search.parallel.ai/mcp",
transport: "http",
headers: { "User-Agent": "rivet-dev/agents-mcp-parallel-example" },
timeoutMs: 60_000,
};
const sdk = await SandboxAgent.connect({ baseUrl, token: process.env.SANDBOX_TOKEN });
const mcp = new Client({ name: "rivet-dev/agents-mcp-parallel-example", version: "1.0.0" });

try {
let existing: McpServerConfig | undefined;
try {
existing = await sdk.getMcpConfig(query);
} catch (error) {
if (!(error instanceof SandboxAgentError) || error.status !== 404) throw error;
}
if (existing && !isDeepStrictEqual(existing, config)) {
throw new Error("A different parallel-search config already exists. Choose another directory or remove that entry first.");
}
if (!existing) await sdk.setMcpConfig(query, config);

// Use the stored configuration, rather than connecting with an unrelated preset.
const stored = await sdk.getMcpConfig(query);
if (stored.type !== "remote" || !isDeepStrictEqual(stored, config)) {
throw new Error("The stored Parallel configuration does not match the anonymous HTTP preset.");
}
console.log(`Loaded ${query.mcpName} for ${directory}.`);
await mcp.connect(
new StreamableHTTPClientTransport(new URL(stored.url), {
requestInit: { headers: stored.headers ?? undefined },
}),
);

const tools = await mcp.listTools();
for (const name of ["web_search", "web_fetch"]) {
if (!tools.tools.some((tool) => tool.name === name)) throw new Error(`Missing MCP tool: ${name}`);
}
const sessionId = randomUUID();
const calls = [
{
name: "web_search",
arguments: {
objective: "Find the official Rivet documentation for durable actors.",
search_queries: ["Rivet durable actors documentation"],
session_id: sessionId,
},
},
{
name: "web_fetch",
arguments: {
urls: ["https://rivet.dev/docs"],
objective: "Explain what Rivet actors do.",
session_id: sessionId,
},
},
];
for (const call of calls) {
const result = await mcp.callTool(call, undefined, { timeout: stored.timeoutMs ?? undefined });
if (result.isError) throw new Error(`${call.name} failed: ${JSON.stringify(result.content)}`);
console.log(`${call.name}:`);
console.log(JSON.stringify(result.content, null, 2));
}
} finally {
await mcp.close();
await sdk.dispose();
}
14 changes: 14 additions & 0 deletions sandbox-agent/examples/mcp-parallel/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM"],
"module": "ESNext",
"moduleResolution": "Bundler",
"noEmit": true,
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true,
"types": ["node"]
},
"include": ["src/**/*"]
}
19 changes: 19 additions & 0 deletions sandbox-agent/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.