# Scratchpad MCP — persistent storage for AI agents (`mikepressure/scratchpad-mcp`) Actor

An MCP server that gives AI agents persistent, token-efficient storage. Versioned files, structured diffs, append-only logs, on-demand summaries — all per-agent isolated.

- **URL**: https://apify.com/mikepressure/scratchpad-mcp.md
- **Developed by:** [Mike Pressure](https://apify.com/mikepressure) (community)
- **Categories:** AI, Developer tools, MCP servers
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, NaN bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-usage

## What's an Apify Actor?

Actors are a software tools running on the Apify platform, for all kinds of web data extraction and automation use cases.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

In JavaScript/TypeScript projects, use official [JavaScript/TypeScript client](https://docs.apify.com/api/client/js.md):

```bash
npm install apify-client
```

In Python projects, use official [Python client library](https://docs.apify.com/api/client/python.md):

```bash
pip install apify-client
```

In shell scripts, use [Apify CLI](https://docs.apify.com/cli/docs.md):

````bash
# MacOS / Linux
curl -fsSL https://apify.com/install-cli.sh | bash
# Windows
irm https://apify.com/install-cli.ps1 | iex
```bash

In AI frameworks, you might use the [Apify MCP server](https://docs.apify.com/platform/integrations/mcp.md).

If your project is in a different language, use the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).


# README

## scratchpad-mcp

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Node](https://img.shields.io/badge/node-%3E%3D20-brightgreen.svg)](https://nodejs.org)
[![MCP](https://img.shields.io/badge/MCP-compatible-blue.svg)](https://modelcontextprotocol.io)

Persistent, token-efficient storage for AI agents. An MCP server that stops your
agents from re-reading the same files and re-loading the same context every turn.

```text
agent: "what changed in this file since I last read it?"
server: { diff: [...], current_version: 14 }   ← not the whole file
````

### Why

Agents waste tokens. They re-read files they've already seen, re-summarize
documents they've already processed, and re-discover state they've already
computed. This server gives them a place to put that work and pick it up later
in a way the model can reason about cheaply.

Concretely:

- **Versioned writes** so an agent can store a working document and ask "what
  changed since I last saw this?" — the server returns a structured diff
  instead of the full content.
- **Append-only logs** with cursor-based pagination, so an agent can record
  its own action history and replay it efficiently.
- **On-demand summaries** for long files (>2000 estimated tokens), generated
  by Claude Haiku and cached per file version, so repeat summary calls are
  free.
- **Per-agent namespacing** so one server instance can serve many agents
  without leaking state between them.

### Tools

All tools take `agent_id` as their first argument. Operations are scoped to
that agent — agents cannot read each other's files or logs.

| Tool | What it does |
|---|---|
| `write_file(agent_id, path, content)` | Store content at a path. Auto-versions on every write. Keeps the 10 most recent versions. |
| `read_file(agent_id, path, since_version?)` | Read full content, or a JSON line-diff against a prior version. If `since_version` has been pruned, returns full content with `version_too_old: true`. |
| `append_log(agent_id, path, entry)` | Append one entry to an append-only log. Returns the new entry ID. |
| `read_log(agent_id, path, since_entry?)` | Read log entries with cursor pagination. 100 entries per page, `has_more` flag plus `last_entry_id` cursor. |
| `list_files(agent_id, prefix?)` | List files (metadata only) optionally filtered by path prefix. |
| `delete_file(agent_id, path)` | Delete a file and all its versions and any cached summary. |
| `summarize_file(agent_id, path)` | LLM-summarize a long file (>8000 chars). Cached per version, so repeat calls on an unchanged file cost nothing. |
| `get_usage_stats(agent_id)` | Return total bytes, file count, log count, and total operations for an agent. |

#### Diff format

`read_file` with `since_version` returns a JSON array of chunks:

```json
{
  "diff": [
    { "op": "equal",  "lines": ["line that didn't change"] },
    { "op": "remove", "lines": ["line that was deleted"] },
    { "op": "add",    "lines": ["line that was added"] }
  ]
}
```

Line-level diffing is intentional — it's the format agents handle most
reliably, and it lets the agent reason about *what changed* rather than
re-processing the whole file.

#### Path rules

Paths must match `[a-zA-Z0-9/_.-]+`, max 255 chars, no leading `/`, no `..`
sequences. Errors name the violated rule.

#### Limits

- 1 MB per file write
- 64 KB per log entry
- 10 retained versions per file (older ones pruned automatically)
- 100 log entries per `read_log` page

### Install

Requires Node 20+ and an Anthropic API key (only for `summarize_file`).

```powershell
git clone <this repo>
cd scratchpad-mcp
npm install
npm run build
```

That produces `dist/index.js`, the runnable server.

### Configure with Claude Desktop

Add to `%APPDATA%\Claude\claude_desktop_config.json` (Windows) or
`~/Library/Application Support/Claude/claude_desktop_config.json` (macOS):

```json
{
  "mcpServers": {
    "scratchpad": {
      "command": "node",
      "args": ["C:\\path\\to\\scratchpad-mcp\\dist\\index.js"],
      "env": {
        "ANTHROPIC_API_KEY": "sk-ant-..."
      }
    }
  }
}
```

`ANTHROPIC_API_KEY` is only required if you intend to call `summarize_file`.
The other seven tools work without it.

Optional: set `SCRATCHPAD_DB_PATH` to override the SQLite location. Defaults
to `scratchpad.db` in the project root.

Restart Claude Desktop. The server should appear in the MCP servers list with
8 tools.

### Security model — read this before hosting

`agent_id` is a **plaintext tool parameter**. There is no authentication: a
caller can claim to be any `agent_id`, and the server will trust it. This is
deliberate for V1 and works fine for the intended deployment shape, which is:

- **One-user-per-server-process.** The agent and the SQLite file share a
  trust boundary. Examples: Claude Desktop install, Smithery local install,
  per-user Apify Actor run (Apify spawns a fresh container with a fresh
  database file per run by default).

It is **not safe** for:

- **Multi-tenant standby mode** where one server process serves multiple
  untrusted callers reading and writing the same SQLite file. Anyone can
  pass another caller's `agent_id` and read or overwrite their data.

If you want multi-tenant, derive `agent_id` from the caller's API key in a
wrapper layer (this is the V2 plan) or run one process per tenant.

#### Defense in depth that *is* in place

- All SQL is parameterized — no injection possible via path, agent\_id, or
  prefix.
- Path validation rejects `..`, leading `/`, spaces, and any character
  outside `[a-zA-Z0-9/_.-]`.
- `list_files` prefix matching uses `SUBSTR` equality (not `LIKE`) so the SQL
  wildcards `_` and `%` never apply, and matching is case-sensitive.
- Per-call size caps (1 MB / file, 64 KB / log entry).
- Per-agent quotas (1000 files, 100k log entries, 100 MB total) so a runaway
  agent can't exhaust shared disk on a hosted deploy.
- Errors return only `err.message` — no stack traces, no SQLite paths, no
  API keys.

### Who pays for `summarize_file`?

The caller. Always.

- **Local install (Smithery, Claude Desktop, mcp.so):** the user provides
  their own `ANTHROPIC_API_KEY` in their config. Their machine, their key,
  their bill.
- **Apify hosted:** every Actor run reads `anthropicApiKey` from its per-run
  input. A `.actor/entrypoint.sh` launcher maps that into the env before
  starting the server. Each caller pays Anthropic for their own summaries;
  the Actor publisher only collects Apify's per-call fee.

If you fork this and intend to host it, do **not** hardcode an API key into
the Dockerfile, the Apify Actor environment, or any config that gets shipped
publicly. The other seven tools work without a key, so leaving it unset is a
safe default.

### How storage works

A single SQLite file holds everything:

- `files` — one row per `(agent_id, path)`, tracks the current version.
- `file_versions` — full content per version, capped at 10 most recent per
  file. Pruning happens on every `write_file`.
- `log_entries` — append-only entries, never modified.
- `summaries` — per-file summary cache, invalidated by version mismatch.
- `agent_usage` — per-agent operation counter for `get_usage_stats`.

Versioning stores full content per version (not deltas) because writes need to
be fast and reads need to be unambiguous. Diffs are computed on read by
running the two versions through line-level diffing — the cost is paid by the
caller asking for the diff, not by every writer.

### Roadmap

- \[ ] Apify packaging for pay-per-call billing.
- \[ ] Derive `agent_id` from API key instead of taking it as a parameter.
- \[ ] Postgres backend (the SQLite schema is portable; this is a connection
  swap, not a rewrite).
- \[ ] Per-agent rate limiting.
- \[ ] Structured logging for ops visibility.

### License

MIT — see [LICENSE](./LICENSE).

# Actor input Schema

## `anthropicApiKey` (type: `string`):

Required only if you intend to call summarize\_file. The other seven tools work without it.

## Actor input object example

```json
{}
```

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {};

// Run the Actor and wait for it to finish
const run = await client.actor("mikepressure/scratchpad-mcp").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {}

# Run the Actor and wait for it to finish
run = client.actor("mikepressure/scratchpad-mcp").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{}' |
apify call mikepressure/scratchpad-mcp --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=mikepressure/scratchpad-mcp",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "Scratchpad MCP — persistent storage for AI agents",
        "description": "An MCP server that gives AI agents persistent, token-efficient storage. Versioned files, structured diffs, append-only logs, on-demand summaries — all per-agent isolated.",
        "version": "0.1",
        "x-build-id": "PWpBaHgP8ZKpi7809"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/mikepressure~scratchpad-mcp/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-mikepressure-scratchpad-mcp",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },
        "/acts/mikepressure~scratchpad-mcp/runs": {
            "post": {
                "operationId": "runs-sync-mikepressure-scratchpad-mcp",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor and returns information about the initiated run in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/runsResponseSchema"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/acts/mikepressure~scratchpad-mcp/run-sync": {
            "post": {
                "operationId": "run-sync-mikepressure-scratchpad-mcp",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "inputSchema": {
                "type": "object",
                "properties": {
                    "anthropicApiKey": {
                        "title": "Anthropic API Key",
                        "type": "string",
                        "description": "Required only if you intend to call summarize_file. The other seven tools work without it."
                    }
                }
            },
            "runsResponseSchema": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "actId": {
                                "type": "string"
                            },
                            "userId": {
                                "type": "string"
                            },
                            "startedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "finishedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "status": {
                                "type": "string",
                                "example": "READY"
                            },
                            "meta": {
                                "type": "object",
                                "properties": {
                                    "origin": {
                                        "type": "string",
                                        "example": "API"
                                    },
                                    "userAgent": {
                                        "type": "string"
                                    }
                                }
                            },
                            "stats": {
                                "type": "object",
                                "properties": {
                                    "inputBodyLen": {
                                        "type": "integer",
                                        "example": 2000
                                    },
                                    "rebootCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "restartCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "resurrectCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "computeUnits": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "options": {
                                "type": "object",
                                "properties": {
                                    "build": {
                                        "type": "string",
                                        "example": "latest"
                                    },
                                    "timeoutSecs": {
                                        "type": "integer",
                                        "example": 300
                                    },
                                    "memoryMbytes": {
                                        "type": "integer",
                                        "example": 1024
                                    },
                                    "diskMbytes": {
                                        "type": "integer",
                                        "example": 2048
                                    }
                                }
                            },
                            "buildId": {
                                "type": "string"
                            },
                            "defaultKeyValueStoreId": {
                                "type": "string"
                            },
                            "defaultDatasetId": {
                                "type": "string"
                            },
                            "defaultRequestQueueId": {
                                "type": "string"
                            },
                            "buildNumber": {
                                "type": "string",
                                "example": "1.0.0"
                            },
                            "containerUrl": {
                                "type": "string"
                            },
                            "usage": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "integer",
                                        "example": 1
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "usageTotalUsd": {
                                "type": "number",
                                "example": 0.00005
                            },
                            "usageUsd": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "number",
                                        "example": 0.00005
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
