---
title: "MCP Protocol"
description: "Baponi MCP server reference. Model Context Protocol 2025-11-25 implementation. Streamable HTTP transport, sandbox_execute and files_download tools, stateless design."
url: https://baponi.ai/docs/api/mcp
lastUpdated: 2026-03-16
---
# MCP Protocol
Baponi is a native Model Context Protocol (MCP) server that exposes sandboxed code execution and file access as tools. Any MCP-compatible client (Claude Desktop, Claude Code, Cursor, Windsurf, or custom agents) can connect with a URL and API key. No SDK installation, no local process management, no Docker containers.

**Endpoint:** `POST https://api.baponi.ai/mcp`
**Protocol version:** `2025-11-25`
**Transport:** Streamable HTTP (JSON-RPC 2.0 over POST, Server-Sent Events over GET)
**Auth:** `Authorization: Bearer sk-us-YOUR_API_KEY`

Baponi's MCP server is fully stateless. There are no session IDs, no reconnection tokens, and no server-side session state. Every request is authenticated independently via your API key. Pod restarts, scaling events, and load balancer changes are invisible to clients.

## Connecting from MCP clients

Configure your MCP client with the Baponi endpoint URL and an API key. Create API keys in the [admin console](https://console.baponi.ai) under **API Keys**.

Add to your `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "baponi": {
      "url": "https://api.baponi.ai/mcp",
      "headers": {
        "Authorization": "Bearer sk-us-YOUR_API_KEY"
      }
    }
  }
}
```

**Config file location:**
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`

After saving, restart Claude Desktop. Baponi tools appear in the tool picker (hammer icon).

```bash
claude mcp add baponi \
  --transport http \
  --url https://api.baponi.ai/mcp \
  --header "Authorization: Bearer sk-us-YOUR_API_KEY"
```

Claude Code stores the configuration automatically. Run `claude mcp list` to verify.

Add to `.cursor/mcp.json` in your project root:

```json
{
  "mcpServers": {
    "baponi": {
      "url": "https://api.baponi.ai/mcp",
      "headers": {
        "Authorization": "Bearer sk-us-YOUR_API_KEY"
      }
    }
  }
}
```

Reload the Cursor window after saving. Baponi tools appear in the MCP tools panel.

Add to your Windsurf MCP configuration file (`~/.windsurf/mcp.json` or project-level):

```json
{
  "mcpServers": {
    "baponi": {
      "url": "https://api.baponi.ai/mcp",
      "headers": {
        "Authorization": "Bearer sk-us-YOUR_API_KEY"
      }
    }
  }
}
```

Baponi uses the standard Streamable HTTP transport. Any MCP client that supports remote HTTP servers can connect:

- **URL:** `https://api.baponi.ai/mcp`
- **Transport:** Streamable HTTP (POST for JSON-RPC, GET for SSE)
- **Auth header:** `Authorization: Bearer sk-us-YOUR_API_KEY`
- **No local process needed.** Baponi runs in the cloud, not as a local stdio server

## Transport details

Baponi implements the MCP Streamable HTTP transport defined in the [MCP specification](https://spec.modelcontextprotocol.io/specification/2025-11-25/basic/transports/#streamable-http):

| HTTP Method | Content-Type | Purpose |
|-------------|-------------|---------|
| `POST` | `application/json` | Send JSON-RPC requests and receive responses |
| `GET` | `text/event-stream` | Open an SSE stream for server-initiated messages |

All requests go to a single endpoint: `https://api.baponi.ai/mcp`. There is no separate SSE URL.

### Stateless design: zero session management

Unlike many MCP servers, Baponi does not use `Mcp-Session-Id` headers or maintain server-side session state. This is intentional:

- **No session IDs or reconnection tokens.** Every request stands alone.
- **No `initialize` requirement.** The server responds to `tools/list` and `tools/call` without prior initialization. Sending `initialize` is accepted and returns capabilities, but it is not a prerequisite.
- **No sticky routing.** Requests can hit any backend pod. Scaling, deploys, and pod restarts never break connections.
- **No session cleanup.** Nothing to expire, nothing to garbage-collect.

This means MCP clients that expect session negotiation still work (the `initialize` handshake succeeds), but clients that skip it and go straight to `tools/list` or `tools/call` also work.

## Methods

### initialize

Negotiate protocol version and discover server capabilities. MCP clients typically send this as the first request. On Baponi, it is accepted but not required. The response is the same regardless of prior state.

**Request:**

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-11-25",
    "capabilities": {},
    "clientInfo": {
      "name": "my-agent",
      "version": "1.0"
    }
  }
}
```

**Response:**

```json
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-11-25",
    "capabilities": {
      "tools": {
        "listChanged": false
      }
    },
    "serverInfo": {
      "name": "baponi",
      "version": "0.1.0"
    },
    "instructions": "Baponi is a sandboxed code execution platform for AI agents. Use sandbox_execute to run Python, Node.js, or Bash code. Use files_download to retrieve files produced by executions."
  }
}
```

**Fields in `result`:**

| Field | Description |
|-------|-------------|
| `protocolVersion` | The MCP spec version the server implements (`2025-11-25`) |
| `capabilities.tools.listChanged` | Whether the server sends `notifications/tools/list_changed`. Always `false`. Tool list changes require a new `tools/list` call. |
| `serverInfo.name` | Server identifier (`baponi`) |
| `serverInfo.version` | Server version |
| `instructions` | Human-readable description of the server's purpose, surfaced to LLMs by MCP clients |

### notifications/initialized

After receiving the `initialize` response, MCP clients send this notification to signal readiness. Baponi accepts and acknowledges it, but takes no action (stateless design).

```json
{
  "jsonrpc": "2.0",
  "method": "notifications/initialized"
}
```

No response is returned (JSON-RPC notifications have no `id`).

### tools/list

Discover the tools available to your API key. The response depends on your organization's configuration. Tools can be enabled or disabled per-organization in the admin console.

**Request:**

```json
{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/list"
}
```

**Response:**

```json
{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "tools": [
      {
        "name": "sandbox_execute",
        "description": "Execute code in a sandboxed environment",
        "inputSchema": {
          "type": "object",
          "properties": {
            "code": {
              "type": "string",
              "description": "Code to execute"
            },
            "language": {
              "type": "string",
              "enum": ["python", "node", "bash"],
              "description": "Programming language. Defaults to the sandbox's configured language if omitted."
            },
            "timeout": {
              "type": "integer",
              "minimum": 1,
              "maximum": 3600,
              "description": "Execution timeout in seconds. Defaults to sandbox configuration."
            },
            "thread_id": {
              "type": "string",
              "description": "Persistent thread ID. Files written to /home/baponi persist across calls with the same thread_id."
            },
            "metadata": {
              "type": "object",
              "description": "Arbitrary key-value metadata attached to the execution record."
            },
            "env_vars": {
              "type": "object",
              "additionalProperties": { "type": "string" },
              "description": "Environment variables injected into the sandbox. Keys must be uppercase letters, digits, and underscores, starting with a letter."
            }
          },
          "required": ["code"]
        },
        "annotations": {
          "readOnlyHint": false,
          "destructiveHint": false,
          "idempotentHint": false
        }
      },
      {
        "name": "files_download",
        "description": "Download file content from a thread or volume",
        "inputSchema": {
          "type": "object",
          "properties": {
            "source": {
              "type": "string",
              "enum": ["thread", "volume"],
              "description": "Where to read the file from."
            },
            "id": {
              "type": "string",
              "description": "The thread_id or volume name."
            },
            "path": {
              "type": "string",
              "description": "File path relative to the storage root."
            }
          },
          "required": ["source", "id", "path"]
        },
        "annotations": {
          "readOnlyHint": true,
          "destructiveHint": false,
          "idempotentHint": true
        }
      }
    ]
  }
}
```

#### Tool annotations

Each tool includes [MCP annotations](https://spec.modelcontextprotocol.io/specification/2025-11-25/server/tools/#annotations) that help clients decide how to present and approve tool calls:

| Annotation | `sandbox_execute` | `files_download` | Meaning |
|------------|-------------------|-------------------|---------|
| `readOnlyHint` | `false` | `true` | Whether the tool only reads data without side effects |
| `destructiveHint` | `false` | `false` | Whether the tool may delete or overwrite external data |
| `idempotentHint` | `false` | `true` | Whether repeated calls with the same arguments produce the same result |

#### Tool visibility

Not all tools appear for every API key:

- **`sandbox_execute`** is always available when code execution is enabled for the organization.
- **`files_download`** appears only when the API key's sandbox has storage configured (a storage connection or volume).
- Tools disabled in the admin console **Tool Configuration** settings are excluded from the `tools/list` response entirely.

### tools/call: sandbox_execute

Execute code in an isolated sandbox. Each execution gets its own multi-layer sandbox with dedicated CPU, RAM, filesystem, and network namespace. Code runs to completion and the result is returned.

**Request:**

```json
{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "sandbox_execute",
    "arguments": {
      "code": "import sys; print(f'Python {sys.version}')",
      "language": "python"
    }
  }
}
```

**Response (success):**

```json
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "## Execution Result\n\n**stdout:**\n```\nPython 3.14.0a1 (main, Oct 15 2025, 08:00:00)\n```\n\n**Exit code:** 0"
      }
    ],
    "isError": false
  }
}
```

**Response (execution error, code ran but returned non-zero exit code):**

```json
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "## Execution Result\n\n**stderr:**\n```\nTraceback (most recent call last):\n  File \"<stdin>\", line 1, in <module>\nNameError: name 'undefined_var' is not defined\n```\n\n**Exit code:** 1"
      }
    ],
    "isError": true
  }
}
```

Execution errors (non-zero exit code, timeout) are returned as MCP tool results with `isError: true`, not as JSON-RPC errors. This lets the LLM see the error output and decide how to recover. JSON-RPC error codes are reserved for protocol-level failures (invalid request, auth failure, server error).

#### sandbox_execute parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `code` | string | Yes | Source code to execute. Max 1 MB. |
| `language` | string | No | `"python"`, `"node"`, or `"bash"`. Defaults to the sandbox's configured default language. |
| `timeout` | integer | No | Execution timeout in seconds (1-3600). Defaults to sandbox configuration. Plan limits apply: Free tier max 60s, Pro max 3600s. |
| `thread_id` | string | No | Associates the execution with a persistent thread. Files written to `/home/baponi` are preserved across executions sharing the same `thread_id`. Omit for ephemeral executions where no files need to persist. |
| `metadata` | object | No | Arbitrary JSON metadata attached to the execution record. Useful for correlating executions with your application's data model. Appears in audit logs and execution history. |
| `env_vars` | object | No | Environment variables injected into the sandbox for this execution. Keys must be uppercase letters, digits, and underscores (starting with a letter). Merged with sandbox-level and API key-level env vars (request takes highest precedence). See [environment variables](/docs/api/execute.md#environment-variables) for limits. |

#### Persistent threads

When you pass a `thread_id`, the sandbox mounts persistent storage at `/home/baponi`. Files written there are preserved and available to subsequent executions with the same `thread_id`. This enables multi-step workflows:

```json
// Step 1: Generate a file
{
  "method": "tools/call",
  "params": {
    "name": "sandbox_execute",
    "arguments": {
      "code": "import json; data = {'results': [1,2,3]}; open('/home/baponi/data.json','w').write(json.dumps(data))",
      "language": "python",
      "thread_id": "analysis-session-42"
    }
  }
}

// Step 2: Read and process the file (same thread_id)
{
  "method": "tools/call",
  "params": {
    "name": "sandbox_execute",
    "arguments": {
      "code": "import json; data = json.load(open('/home/baponi/data.json')); print(sum(data['results']))",
      "language": "python",
      "thread_id": "analysis-session-42"
    }
  }
}
```

Only one execution can use a given `thread_id` at a time. Concurrent requests with the same `thread_id` return a `409 Conflict` error. Use different thread IDs for parallel work.

### tools/call: files_download

Download a file from a thread's persistent storage or a managed volume. Useful for retrieving artifacts produced by code executions: charts, CSVs, processed data, generated images.

**Request:**

```json
{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "tools/call",
  "params": {
    "name": "files_download",
    "arguments": {
      "source": "thread",
      "id": "analysis-session-42",
      "path": "data.json"
    }
  }
}
```

**Response (text file):**

```json
{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\"results\": [1, 2, 3]}"
      }
    ],
    "isError": false
  }
}
```

**Response (image file: PNG, JPG, GIF, WebP):**

```json
{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "content": [
      {
        "type": "image",
        "data": "iVBORw0KGgoAAAANSUhEUgAA...",
        "mimeType": "image/png"
      }
    ],
    "isError": false
  }
}
```

#### files_download parameters

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `source` | string | Yes | `"thread"` for thread persistent storage, `"volume"` for managed volumes. |
| `id` | string | Yes | The `thread_id` (if source is `"thread"`) or volume name (if source is `"volume"`). |
| `path` | string | Yes | File path relative to the storage root. Example: `"output/chart.png"`. |

#### File type handling

| File type | MCP content type | Behavior |
|-----------|-----------------|----------|
| Text files (`.txt`, `.json`, `.csv`, `.py`, `.md`, etc.) | `TextContent` | Returned inline as text |
| Images (`.png`, `.jpg`, `.gif`, `.webp`) | `ImageContent` | Returned base64-encoded with MIME type |
| Files larger than 1 MiB | Error | Returns an error with instructions to use the [REST Files API](/docs/api/files.md) for large file downloads |
| Binary files (non-text, non-image) | Error | Returns an error with instructions to use the REST Files API |

## Error handling

### JSON-RPC error codes

Protocol-level errors use standard JSON-RPC error codes. These indicate the request itself was invalid, not that code execution failed.

```json
{
  "jsonrpc": "2.0",
  "id": 3,
  "error": {
    "code": -32601,
    "message": "Method not found: tools/unknown"
  }
}
```

| Code | Name | When it occurs |
|------|------|----------------|
| `-32700` | Parse error | Request body is not valid JSON |
| `-32600` | Invalid request | Missing required JSON-RPC fields (`jsonrpc`, `method`) |
| `-32601` | Method not found | Unknown method name (e.g., `tools/unknown`) |
| `-32602` | Invalid params | Method parameters fail validation (e.g., missing `code` in `sandbox_execute`) |
| `-32603` | Internal error | Server-side failure. Retry with exponential backoff. |

### Authentication errors

Invalid or missing API keys return HTTP `401 Unauthorized` before JSON-RPC processing:

```
HTTP/1.1 401 Unauthorized
Content-Type: application/json

{"error": "unauthorized", "message": "Invalid or revoked API key."}
```

After 3 failed authentication attempts from the same IP within 60 seconds, subsequent requests are rate-limited with HTTP `429`.

### Execution errors vs. protocol errors

Baponi distinguishes between two kinds of errors:

- **Execution errors** (code ran but failed): Returned as a normal `tools/call` result with `isError: true`. The LLM sees the stderr and exit code, and can decide to fix and retry.
- **Protocol errors** (request was invalid): Returned as a JSON-RPC `error` object. The client should fix the request format.

This separation is intentional. LLMs are effective at recovering from execution errors when they can see the error output. Protocol errors indicate a client bug that needs human attention.

## Request headers

| Header | Required | Description |
|--------|----------|-------------|
| `Authorization` | Yes | `Bearer sk-us-YOUR_API_KEY`. API key created in the [admin console](https://console.baponi.ai). |
| `Content-Type` | Yes (POST) | `application/json` for POST requests. |
| `Accept` | No | `application/json` or `text/event-stream`. Defaults to `application/json`. |
| `Mcp-Protocol-Version` | No | `2025-11-25`. Validated if present, but not required. The server accepts requests without this header. |

## Complete example: end-to-end flow

This example shows a full MCP session with `curl`: initialize, list tools, execute code, and download a file.

**1. Initialize (optional):**

```bash
curl -s -X POST https://api.baponi.ai/mcp \
  -H "Authorization: Bearer sk-us-YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "initialize",
    "params": {
      "protocolVersion": "2025-11-25",
      "capabilities": {},
      "clientInfo": {"name": "curl-example", "version": "1.0"}
    }
  }'
```

**2. List available tools:**

```bash
curl -s -X POST https://api.baponi.ai/mcp \
  -H "Authorization: Bearer sk-us-YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 2,
    "method": "tools/list"
  }'
```

**3. Execute code with a persistent thread:**

```bash
curl -s -X POST https://api.baponi.ai/mcp \
  -H "Authorization: Bearer sk-us-YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 3,
    "method": "tools/call",
    "params": {
      "name": "sandbox_execute",
      "arguments": {
        "code": "import csv\nwith open(\"/home/baponi/report.csv\", \"w\") as f:\n    w = csv.writer(f)\n    w.writerow([\"metric\", \"value\"])\n    w.writerow([\"latency_p50\", \"12ms\"])\n    w.writerow([\"latency_p99\", \"45ms\"])\nprint(\"Report written.\")",
        "language": "python",
        "thread_id": "demo-thread-1"
      }
    }
  }'
```

**4. Download the generated file:**

```bash
curl -s -X POST https://api.baponi.ai/mcp \
  -H "Authorization: Bearer sk-us-YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "id": 4,
    "method": "tools/call",
    "params": {
      "name": "files_download",
      "arguments": {
        "source": "thread",
        "id": "demo-thread-1",
        "path": "report.csv"
      }
    }
  }'
```

## Comparison with the REST API

Baponi exposes the same execution capabilities through both MCP and REST. Choose based on your integration pattern:

| | MCP (`/mcp`) | REST (`/v1/sandbox/execute`) |
|---|---|---|
| **Best for** | MCP-native clients (Claude, Cursor, IDEs) | Direct HTTP integration, custom agents |
| **Protocol** | JSON-RPC 2.0 | Standard REST |
| **Tool discovery** | Built-in via `tools/list` | Read the [API docs](/docs/api/execute.md) |
| **File download** | Inline via `files_download` tool (text and images up to 1 MiB) | Signed URLs via [Files API](/docs/api/files.md) (any size) |
| **Streaming** | SSE via GET | Not available on execute endpoint |
| **Auth** | Same API keys | Same API keys |
| **Sandbox config** | Same, determined by API key | Same, determined by API key |

Both surfaces are backed by the same execution infrastructure. Latency, isolation, and security guarantees are identical.

## Frequently asked questions

### Do I need to run a local MCP server process?

No. Baponi's MCP server runs in the cloud. You configure your MCP client with a URL. No Docker, no `npx`, no local process. This is the Streamable HTTP transport, not the stdio transport.

### What happens if the server pod restarts during my session?

Nothing. Baponi's MCP implementation is stateless. Your next request is routed to any available pod and authenticated independently. There is no session to lose.

### Can I use both MCP and REST in the same application?

Yes. Both use the same API keys, the same sandboxes, and the same thread storage. An execution started via MCP's `sandbox_execute` produces files that are accessible via the REST Files API, and vice versa.

### Why does `tools/list` only show `sandbox_execute`?

The `files_download` tool only appears when your API key's sandbox has storage configured (a storage connection or volume). Configure storage in the [admin console](https://console.baponi.ai) under your sandbox settings.

### Is there a message size limit?

The maximum request body size is 1 MB, matching the code size limit for `sandbox_execute`. File downloads via `files_download` are limited to 1 MiB inline. For larger files, use the [REST Files API](/docs/api/files.md) which provides signed URLs for direct download.

### Does Baponi support MCP resources or prompts?

Baponi currently implements the MCP **tools** capability only. Resources and prompts are not exposed. Tool discovery via `tools/list` is the primary integration surface.