# UNDISK MCP — Documentation

> The undo-first, versioned file workspace for AI agents.
> Live at [mcp.undisk.app](https://mcp.undisk.app)

---

## What is UNDISK?

UNDISK MCP is a remote file workspace for AI agents. Every file mutation an agent makes over [MCP](https://spec.modelcontextprotocol.io) creates an immutable version, so any single write can be surgically undone in under 50ms without affecting other files.

No VM rollbacks. No "restore everything." Just precise, per-file undo with a tamper-evident audit trail.

---

## Quick Start

1. **Sign up** at [mcp.undisk.app/signup](https://mcp.undisk.app/signup)
2. **Get your API key** (shown once after signup at [/keys](https://mcp.undisk.app/keys))
3. **Connect your MCP client:**

### GitHub Copilot / CLI

```
Server Name: undisk
Server Type: http
URL: https://mcp.undisk.app/v1/mcp
HTTP Headers: {"x-api-key":"sk_live_YOUR_KEY_HERE","Authorization":"Bearer sk_live_YOUR_KEY_HERE"}
Tools: *
```

Both `x-api-key` and `Authorization` included for maximum client compatibility.

### GitHub Copilot Cloud Agent (Coding Agent)

For repository-level MCP in GitHub Copilot cloud agent, add `.github/copilot/mcp.json`:

```json
{
  "mcpServers": {
    "undisk": {
      "type": "http",
      "url": "https://mcp.undisk.app/v1/mcp",
      "headers": {
        "x-api-key": "$COPILOT_MCP_UNDISK_API_KEY"
      },
      "tools": ["*"]
    }
  }
}
```

**Important:** Secrets must use the `COPILOT_MCP_` prefix. Go to your repo **Settings → Environments → copilot → Environment secrets** and add `COPILOT_MCP_UNDISK_API_KEY` with your `sk_live_...` key. Only secrets with this prefix are available to MCP configurations.

Optionally add `.github/workflows/copilot-setup-steps.yml` to preinstall dependencies in the agent environment.

### Claude Code

```bash
claude mcp add --transport http undisk https://mcp.undisk.app/v1/mcp
claude mcp update undisk --header "Authorization: Bearer YOUR_KEY"
```

### Cursor / Windsurf / VS Code

```json
{
  "mcpServers": {
    "undisk": {
      "url": "https://mcp.undisk.app/v1/mcp",
      "headers": {
        "Authorization": "Bearer sk_live_YOUR_KEY_HERE"
      }
    }
  }
}
```

### WebSocket (fastest — 4ms reads)

```
wss://mcp.undisk.app/ws?token=sk_live_YOUR_KEY_HERE
```

### Direct HTTP

```
URL:   https://mcp.undisk.app/v1/mcp
Auth:  Authorization: Bearer sk_live_YOUR_KEY_HERE
```

---

## MCP Tools

16 tools exposed via the Model Context Protocol. Every write is versioned.

### File Operations

| Tool | Description |
|------|-------------|
| `read_file` | Read file contents by path. Returns UTF-8 text or base64 for binary with a mimeType hint, plus metadata. |
| `write_file` | Write UTF-8 text or base64-encoded binary to a file. Creates an immutable version automatically. |
| `create_file` | Create a new file (fails if path already exists). Use write_file for upsert behavior. |
| `start_upload` | Start a staged upload for a large binary file. Then send chunks with append_upload_chunk, finalize with complete_upload. |
| `append_upload_chunk` | Append one ordered base64 chunk to a staged upload session. |
| `complete_upload` | Assemble uploaded chunks and commit them as a normal immutable file version. |
| `cancel_upload` | Discard a staged upload session and delete all buffered chunks. |
| `delete_file` | Soft-delete a file. Versioned and restorable via restore_version. |
| `move_file` | Move or rename a file. Both old and new paths are version-tracked. |
| `list_files` | List files in a directory. Supports recursive listing. |
| `search_files` | Search file contents by plain text pattern. Returns matching file metadata (path, name, size). Supports regex, case-insensitive, and content snippets. |

### Version Control

| Tool | Description |
|------|-------------|
| `list_versions` | Show version history for a file: timestamp, agent, content hash, size. |
| `restore_version` | Restore a file to any prior version. Creates a new version with restored content. |
| `get_diff` | Structured diff between two versions: changes with type (added/removed/modified), line ranges, and before/after content. |

### Workspace Admin

| Tool | Description |
|------|-------------|
| `get_policy` | Read workspace access rules and limits. |
| `set_policy` | Configure path ACLs, size limits, extension rules, rate caps. |

---

## Tool Reference

Detailed parameter reference for all 16 tools. Parameters marked ✓ are required.

#### `read_file`

Read a file's current content and metadata from the Undisk workspace. Text files return UTF-8 content directly. Binary files return base64 content with encoding='base64' and a mimeType field. Size in bytes, SHA-256 content hash, last-modified timestamp, and current version number are always included. Each result includes a `browser_url` — a permanent link the human user can open in their browser to view the file. Use list_files to discover available paths first.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `path` | string | ✓ | File path within the workspace (e.g., 'docs/readme.md') |

---

#### `write_file`

Write content to a file in the Undisk workspace. Automatically creates an immutable version, so the previous content is preserved and can be restored anytime via restore_version. Creates the file if it doesn't exist, or updates it if it does (upsert). Use create_file instead when you need a strict 'file must not exist' guard. Supports UTF-8 text by default, or raw binary bytes when content is base64-encoded with encoding='base64'. Returns the new version metadata including version ID and content hash.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `path` | string | ✓ | File path within the workspace (e.g., 'src/config.json') |
| `content` | string | ✓ | File content. Use plain UTF-8 text by default, or base64-encoded bytes when encoding='base64'. |
| `encoding` | string |  | Content encoding. Defaults to 'utf-8'. Use 'base64' for binary files. Values: `utf-8`, `base64`. |

---

#### `create_file`

Create a new file in the Undisk workspace. Fails if a file already exists at the given path. Use create_file when you need a strict 'file must not exist' guard; use write_file for upsert behavior. Supports UTF-8 text by default, or raw binary bytes when content is base64-encoded with encoding='base64'. The creation is recorded as a new version with a full audit trail. Returns version metadata including version ID and content hash.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `path` | string | ✓ | File path for the new file (e.g., 'reports/q1-summary.md') |
| `content` | string | ✓ | Initial file content. Use plain UTF-8 text by default, or base64-encoded bytes when encoding='base64'. |
| `encoding` | string |  | Content encoding. Defaults to 'utf-8'. Use 'base64' for binary files. Values: `utf-8`, `base64`. |

---

#### `start_upload`

Start a staged binary upload for a local file that is too large to fit comfortably in one MCP tool call. Workflow: call start_upload first to get an upload ID, then send ordered chunks via append_upload_chunk, and finalize with complete_upload (or discard with cancel_upload). Returns an upload ID plus recommended chunk sizes.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `path` | string | ✓ | Destination file path within the workspace (for example 'music/track.wav'). |
| `mode` | string |  | Use 'write' to create-or-update, or 'create' to fail if the path already exists. Values: `write`, `create`. |
| `expected_bytes` | number |  | Optional final file size in raw bytes. If provided, complete_upload verifies it exactly. |
| `mime_type` | string |  | Optional MIME type hint for the upload session (for example 'audio/wav'). |

---

#### `append_upload_chunk`

Append one ordered base64 chunk to an upload session started with start_upload. Use small chunks rather than one huge base64 string so local binary files do not overflow the model context window.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `upload_id` | string | ✓ | Upload ID returned by start_upload. |
| `part_number` | number | ✓ | Zero-based chunk index. Send chunks in strict order: 0, 1, 2, ... |
| `content` | string | ✓ | Base64-encoded chunk bytes. Keep each chunk small; use the size guidance from start_upload. |

---

#### `complete_upload`

Finalize a staged upload after all chunks have been sent. Undisk assembles the uploaded bytes and commits them as a normal immutable file version via write or create mode.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `upload_id` | string | ✓ | Upload ID returned by start_upload. |

---

#### `cancel_upload`

Cancel a staged upload and delete all buffered chunks without creating a workspace version. Use this if the local file changed or the upload should be abandoned.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `upload_id` | string | ✓ | Upload ID returned by start_upload. |

---

#### `delete_file`

Soft-delete a file by recording a deletion tombstone as a new version. The file disappears from list_files but all prior versions are preserved. Use restore_version to bring it back at any time. Nothing is permanently destroyed.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `path` | string | ✓ | Path of the file to delete (e.g., 'temp/draft.txt') |

---

#### `move_file`

Move or rename a file within the Undisk workspace. Both the source and destination paths are tracked in the version history, preserving the full audit trail across renames. The move is recorded as a new version on both paths.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `from_path` | string | ✓ | Current file path (e.g., 'drafts/proposal.md') |
| `to_path` | string | ✓ | New file path (e.g., 'final/proposal.md') |

---

#### `list_files`

List files and directories in the Undisk workspace. Returns metadata for each entry including path, size, and last-modified timestamp. Each result includes a `browser_url` — a permanent link the human user can open in their browser to view the file. Use recursive mode to get the full workspace tree. Start here to discover what files are available before reading or writing.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `path` | string |  | Directory path to list (default: '/' for workspace root) |
| `recursive` | boolean |  | If true, includes all files in subdirectories recursively |

---

#### `search_files`

Search across all file contents in the Undisk workspace. By default uses case-sensitive substring matching. Supports regex mode and case-insensitive search. Returns matching file metadata (path, name, size, lastModified). Each result includes a `browser_url` — a permanent link the human user can open in their browser to view the file. Use this to find files when you don't know the exact path. Results are capped at 100 matches. Set `context_lines` to include matching line snippets in results.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `pattern` | string | ✓ | Text pattern to search for across file contents. Interpreted as regex when `regex` is true. |
| `path` | string |  | Directory scope to search within (omit to search entire workspace) |
| `regex` | boolean |  | When true, treat `pattern` as a regular expression. Default: false. |
| `case_sensitive` | boolean |  | When false, match case-insensitively. Default: true. |
| `context_lines` | number |  | Number of lines of context to include around each match (0–10). When > 0, results include a `matches` array with line numbers and content snippets. Default: 0 (no snippets). |

---

#### `list_versions`

Get the complete version history for a file. Every write, delete, move, and restore is recorded as an immutable version. Each entry includes: version ID, timestamp, acting agent, content hash, and size. Use this to find a version ID before calling restore_version or get_diff. Supports pagination via limit/offset.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `path` | string | ✓ | File path to retrieve version history for |
| `limit` | number |  | Maximum number of versions to return (default: 50) |
| `offset` | number |  | Number of versions to skip for pagination |

---

#### `restore_version`

Undo any file change by restoring to a prior version. This is the core undo operation. Call list_versions first to find the version ID, then restore to instantly revert. A new version is created with the restored content, so the restore itself is versioned and reversible.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `path` | string | ✓ | Path of the file to restore |
| `version_id` | string | ✓ | Target version ID to restore to (from list_versions output) |

---

#### `get_diff`

Compare two versions of a file and get a structured line-by-line diff. Use list_versions to find version IDs, then diff any two points in the file's history. Returns an array of changes, each with type ('added', 'removed', or 'modified'), lineRange (start/end), and before/after content strings. Useful for reviewing what changed between writes or before deciding whether to restore.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `path` | string | ✓ | Path of the file to diff |
| `from_version` | string | ✓ | Starting version ID (the 'before' state) |
| `to_version` | string | ✓ | Ending version ID (the 'after' state) |

---

#### `get_policy`

Get the current workspace policy. Returns path ACLs, size limits, extension rules, and rate limit rules configured for this workspace.

No parameters.

---

#### `set_policy`

Set the workspace policy. Replaces the entire policy with the provided configuration. Requires workspace owner access. Policy includes path ACLs, size limits, extension rules, and rate limits.

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `pathAcls` | object[] |  | Path-based access control rules. Each rule has a glob pattern, permission (read/read-write/none), and optional agentId scope. Each item: `pattern` (string, required), `permission` (string: read | read-write | none, required), `agentId` (string, optional). |
| `sizeLimits` | object[] |  | File size limit rules. Each rule has maxBytes. Each item: `maxBytes` (number, required). |
| `extensionRules` | object[] |  | File extension rules. Each rule has allowed and/or denied arrays of extensions (include the dot, e.g. '.txt'). Each item: `allowed` (string[], optional), `denied` (string[], optional). |
| `rateLimits` | object[] |  | Rate limiting rules. Each rule has maxOps, windowSeconds, and scope (agent/workspace). Each item: `maxOps` (number, required), `windowSeconds` (number, required), `scope` (string: agent | workspace, required). |
| `secretScanning` | object |  | Secret scanning config. Enabled by default when omitted. Fields: `enabled` (boolean, required), `block` (boolean, optional), `allowPatterns` (string[], optional). |

---

## Authentication

All MCP connections require an API key. Get your key at [/keys](https://mcp.undisk.app/keys) after signing up.

```
x-api-key: sk_live_...
Authorization: Bearer sk_live_...
```

API keys are shown once at creation and stored as SHA-256 hashes. If you lose your key, regenerate at [/keys](https://mcp.undisk.app/keys) — this revokes all previous keys.

> **Cache note:** API key validations are cached for up to 5 minutes (KV TTL). If you revoke or regenerate a key, the old key may remain valid for up to 5 minutes before the cache expires.

---

## How Versioning Works

Every file mutation (write, delete, move) creates an **immutable version**. Versions are content-addressed via SHA-256, so identical content deduplicates automatically.

1. Agent calls `write_file` with new content
2. UNDISK hashes the content (SHA-256), stores it, and creates a version entry
3. Version entry records: timestamp, agent identity, content hash, file path, size
4. Previous version is **never modified or deleted** (within retention period)
5. Call `restore_version` with any version ID to undo — creates a *new* version with old content

---

## Binary File Support

UNDISK handles binary files natively over MCP. Write binary content as base64 with `encoding: "base64"`; read responses return base64 plus a `mimeType` hint.

```
write_file({ path: "music/track.wav", content: "<base64>", encoding: "base64" })
read_file({ path: "music/track.wav" })
→ { content: "<base64>", encoding: "base64", mimeType: "audio/wav", size: 4200000, ... }
```

MCP request-body cap is 16 MiB. For larger binaries, use staged uploads:

```
start_upload({ path: "music/track.wav", mode: "create", expected_bytes: 4200000 })
append_upload_chunk({ upload_id: "upl_...", part_number: 0, content: "<base64 chunk>" })
append_upload_chunk({ upload_id: "upl_...", part_number: 1, content: "<base64 chunk>" })
complete_upload({ upload_id: "upl_..." })
```

Maximum file size via staged upload: **256 MB**.

---

## The Undo Moment

```
# Agent writes a bad file
→ write_file("/config.yml", bad_content)
  version: ver_a1b2c3

# See what happened
→ list_versions("/config.yml")
  ver_a1b2c3  2026-04-06 11:20  agent_claude  4.1 KB
  ver_x9y8z7  2026-04-06 11:15  agent_claude  3.8 KB  ← good

# Restore the good version
→ restore_version("ver_x9y8z7")
  restored in 8ms. new version: ver_d4e5f6
```

Restore is non-destructive: it creates a new version with old content. No data is ever lost within the retention window.

---

## Policy Engine

Control what agents can do with path-based ACLs, file size limits, rate caps, and anomaly alerts.

```json
{
  "path_acls": [
    { "path": "/production/**", "agents": "*", "permissions": ["read"] },
    { "path": "/drafts/**", "agents": "*", "permissions": ["read", "write", "delete"] },
    { "path": "/secrets/**", "agents": "*", "permissions": [] }
  ],
  "limits": { "max_file_size_mb": 10, "max_ops_per_minute": 1000 },
  "alerts": [{ "condition": "delete_count_per_hour > 50", "action": "block_and_notify" }]
}
```

Permission denials return explanatory errors: the agent is told *which policy* blocked the action and why.

> **Path convention:** MCP tools use paths without a leading slash (e.g., `docs/readme.md`). The policy engine normalizes both file paths and ACL patterns, so `/sandbox/**` and `sandbox/**` are treated identically. The web file browser uses URL paths with a leading slash, but this is handled automatically — agents and policy authors don't need to worry about the difference.

---

## Secret Detection

Every `write_file` and `create_file` call is scanned for 20+ secret patterns before content reaches storage. Matched secrets are blocked by default; the full secret never persists.

```json
{ "secretScanning": { "enabled": true, "block": true, "allowPatterns": [] } }
```

Built-in patterns: AWS keys, GitHub PATs, Stripe keys, OpenAI/Anthropic keys, Slack tokens, GCP/Azure credentials, private keys, JWTs, database URLs, and generic secret assignments.

---

## Audit Trail

Every operation produces a tamper-evident audit entry:

```json
{
  "timestamp": "2026-04-06T10:00:00.000Z",
  "workspace_id": "ws_abc123",
  "agent_id": "agent_claude_prod",
  "human_principal": "user@company.com",
  "operation": "write_file",
  "file_path": "/regulatory/q2-report.md",
  "version_id": "ver_a1b2c3",
  "content_hash": "sha256:e3b0c44298fc1c...",
  "content_size_bytes": 4096,
  "policy_evaluation": { "rules_checked": ["max_file_size", "path_acl"], "result": "ALLOW" }
}
```

Export audit logs as NDJSON with integrity verification hashes. See [Compliance](/docs/compliance) for retention policies.

---

## Transports

| Transport | p50 Read | p50 Write | Best For |
|-----------|----------|-----------|----------|
| **WebSocket** ⚡ | 4 ms | 19 ms | Persistent agent sessions, latency-sensitive |
| **Streamable HTTP** | 37 ms | 91 ms | Universal — all MCP clients |
| **Edge RPC** | <1 ms | <5 ms | Internal — direct DO binding |
| **stdio Proxy** | 37 ms | 91 ms | Local CLI tools via stdin/stdout |

**Why WebSocket is 9× faster:** persistent connection eliminates per-request TLS handshake, auth parsing, and HTTP framing. Batch mode: N requests in one frame at cost of one round-trip.

---

## Architecture

- **Smart Placement** — Worker colocated next to your LLM, not at user edge
- **One Durable Object per workspace** — SQLite for metadata, R2 for content
- **Content-addressable storage** — SHA-256 deduplication; files ≤128 KB inline in SQLite
- **WebSocket Hibernation** — 4ms reads, zero idle billing, JSON-RPC batch support

---

## Pricing

| Plan | Price | Workspaces | Storage | Ops/Day | Retention |
|------|-------|-----------|---------|---------|-----------|
| Free | $0 | 1 | 100 MB | 1,000 | 30 days |
| Pro | $29/mo | 5 | 10 GB | 50,000 | 180 days |
| Team | $99/mo | 25 | 100 GB | 500,000 | 365 days |
| Enterprise | Custom | Unlimited | Custom | Unlimited | Up to 10 years |

Free tier retention (30 days) does **not** meet EU AI Act Article 26(6) minimum of 6 months. Upgrade to Pro or above.

---

## Error Responses

| Code | Meaning |
|------|---------|
| `PERMISSION_DENIED` | Write/delete blocked by a policy ACL. Includes the policy name and rule. |
| `STORAGE_LIMIT` | Workspace storage cap reached. Includes current usage and upgrade URL. |
| `RATE_LIMITED` | Daily or per-minute op cap exceeded. Includes `retry_after_seconds`. |
| `FILE_TOO_LARGE` | File exceeds max size (10 MB Free/Pro, 100 MB Team; platform limit 256 MB). |
| `VERSION_EXPIRED` | Requested version purged per retention policy. |
| `VERSION_NOT_FOUND` | Invalid version ID for the specified file. |
| `WORKSPACE_NOT_FOUND` | Workspace ID doesn't match any active workspace. |
| `INTERNAL_ERROR` | Server error. All committed writes are safe. |

---

## Limits

| Limit | Free | Pro | Team | Enterprise |
|-------|------|-----|------|------------|
| Workspaces | 1 | 5 | 25 | Unlimited |
| Total storage | 100 MB | 10 GB | 100 GB | Custom |
| Operations/day | 1,000 | 50,000 | 500,000 | Unlimited |
| Max file size | 10 MB | 10 MB | 100 MB | Custom (256 MB platform limit) |
| Version retention | 30 days | 180 days | 365 days | Up to 10 years |
| Concurrent agents | 3 | 25 | 100 | Unlimited |

---

## Endpoints

| Endpoint | Protocol | Description |
|----------|----------|-------------|
| `https://mcp.undisk.app/v1/mcp` | Streamable HTTP | Primary MCP endpoint |
| `https://mcp.undisk.app/mcp` | Streamable HTTP | Alias |
| `wss://mcp.undisk.app/ws` | WebSocket | Fastest (4ms reads) |
| `wss://mcp.undisk.app/v1/ws` | WebSocket | Alias |
| `GET https://mcp.undisk.app/health` | HTTP | Health check |

---

## OAuth Scopes

| Scope | Grants Access To |
|-------|-----------------|
| `files:read` | `read_file`, `list_files`, `search_files` |
| `files:write` | `write_file`, `create_file`, `delete_file`, `move_file`, `start_upload`, `append_upload_chunk`, `complete_upload`, `cancel_upload` |
| `versions:read` | `list_versions`, `get_diff` |
| `versions:write` | `restore_version` |
| `policy:read` | `get_policy` |
| `policy:write` | `set_policy` |

---

*[Compliance Documentation](https://mcp.undisk.app/docs/compliance) · [Privacy Policy](https://mcp.undisk.app/privacy) · [Terms](https://mcp.undisk.app/terms)*
