Archive MCP Server
by Surajp1602
README.md
# Archive MCP Server
An MCP server that exposes the **Enterprise Data Archival & Records Management
System**'s records and retention logic to any MCP client — Claude Code, Claude
Desktop, Cursor, or your own client — over stdio.
Instead of clicking through the React dashboard to answer *"what can we archive
in Finance?"*, you ask the model, and it calls these tools.
## Tools
| Tool | What it does |
|---|---|
| `search_records` | Find records by employee, department, or document type |
| `get_record` | Fetch one record with its retention verdict |
| `archival_candidates` | Active records past their retention period, most overdue first |
| `department_summary` | Active vs archived counts per department |
| `retention_forecast` | Month-by-month projection of what becomes archivable next |
| `audit_history` | What the scheduled archival job did, and when |
## Resources
| URI | Contents |
|---|---|
| `policy://retention` | Retention period, in years, per document type |
## Requirements
Python 3.10+ and MCP SDK **2.x**. The v2 SDK renamed `FastMCP` to `MCPServer`
and moved it to `mcp.server.mcpserver`; this code targets v2. Data access is
SQLAlchemy 2.x, with `psycopg2` for PostgreSQL.
## Setup
```bash
python -m venv .venv
source .venv/bin/activate # macOS/Linux
.venv\Scripts\activate # Windows
python -m pip install -r requirements.txt
python seed_db.py # builds the local demo database
python server.py --selftest # sanity check, no MCP client needed
```
Then verify it over a real MCP session:
```bash
python verify_mcp.py
```
## Choosing a database
The server reads `DATABASE_URL` (from the environment, or from a `.env` file —
see `.env.example`):
| `DATABASE_URL` | Backend |
|---|---|
| unset | `sqlite:///archive.db`, the local demo database built by `seed_db.py` |
| set | the real archive database, e.g. `postgresql://user:pw@host/db?sslmode=require` |
`archive.db` holds synthetic records, so the server — and `--selftest` — run for
anyone who clones this repo without credentials. It is not a different codebase:
`seed_db.py` builds the **same five-table schema** the production database uses
(`active_records`, `archived_records`, `retention_policy`, `audit_logs`,
`documents`), so every query in `server.py` runs unchanged against either one.
**Never commit a real `DATABASE_URL`.** `.env` is gitignored; `.env.example` is
the committed template.
## Connecting to Claude Code
From the project directory:
```bash
claude mcp add --scope project archive-system -- /absolute/path/to/.venv/bin/python /absolute/path/to/server.py
claude mcp list
```
`--scope project` writes a committable `.mcp.json` at the project root, so anyone
who clones the repo gets the server. Start `claude`, approve the project server
when prompted, and check `/mcp` — `archive-system` should show **Connected** with
6 tools. Then ask:
> Which IT department records are overdue for archival?
If it fails to start, run `claude --debug=mcp` and read the log under
`~/.claude/debug/`.
## Connecting to Claude Desktop
Add this to `claude_desktop_config.json`:
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
```json
{
"mcpServers": {
"archive-system": {
"command": "D:\\Python\\project\\archive-mcp\\.venv\\Scripts\\python.exe",
"args": ["D:\\Python\\project\\archive-mcp\\server.py"]
}
}
}
```
Point `command` at the venv's Python, not bare `python` — the host does not
inherit your shell's PATH or your activated virtualenv. On Windows both paths
need doubled backslashes.
Restart from the tray icon — **Quit**, not the window close button — or the app
keeps running with the old config.
Note for the Microsoft Store (MSIX) build on Windows: its config is not under
`%APPDATA%` but under the package's own directory,
`%LOCALAPPDATA%\Packages\Claude_<id>\LocalCache\Roaming\Claude\`. It launches
local stdio servers normally. Don't try to confirm that from `logs\mcp.log` —
that file can sit empty and untouched while everything works. Check for the
process instead; the server runs as a child of Claude Desktop:
```powershell
Get-CimInstance Win32_Process -Filter "Name like '%python%'" |
Where-Object { $_.CommandLine -like "*archive-mcp*" }
```
## Design notes
- **stdio transport**, because the client launches the server as a subprocess on
the same machine. An HTTP transport would make sense if the server ran
remotely and served several clients.
- **One seam for storage.** `_connect()` returns a SQLAlchemy `Engine` and is the
only place that knows what the database is. Queries use named bind parameters
(`:department`), which are dialect-neutral, so SQLite and PostgreSQL share one
query set rather than two.
- **`pool_pre_ping=True`**, because a serverless PostgreSQL (Neon and friends)
suspends idle compute and an MCP server sits idle between questions. Without
it, the first question after a quiet spell fails on a stale pooled connection.
- **Eligibility is computed in Python, not SQL.** PostgreSQL `INTERVAL`
arithmetic has no SQLite equivalent, and keeping the comparison in one place
keeps the two backends honest. At a few thousand active rows the cost is not
worth optimising away.
- **Age is measured from `joining_date`.** `created_at` is the bulk-load
timestamp and is identical for every row, so retention computed from it would
find nothing eligible, ever. `joining_date` is an employee-level date standing
in for a document date — the schema carries no document date, which is a real
gap worth closing upstream.
- **Archival state is a table, not a flag.** A record lives in `active_records`
or in `archived_records`, and ids are stable across the move, so `get_record`
checks both. The `status` column is *employment* status and is unrelated.
- **Tools are annotated read-only.** Each carries
`ToolAnnotations(read_only_hint=True, destructive_hint=False)`, so a client can
tell a safe call from a state-changing one before it runs.
- **Tools are read-only in fact, too.** Archiving is destructive and
policy-governed; `archival_candidates` deliberately reports what *could* be
archived and leaves the decision to the existing scheduled job. Exposing a
destructive tool to a model is a choice that needs a confirmation path first.
- **Docstrings are the API.** The model picks tools from the docstring and type
hints, so the valid departments and document types are enumerated there. A
stale enum is worse than none: the model passes a plausible-looking value like
`Legal`, gets an empty result, and reports that there is nothing to archive.
- **One definition of "eligible", used in both directions.** `_verdict` ages a
record against its retention period; `_eligible_on` inverts it to give the date
a record crosses that period, which is what `retention_forecast` buckets by.
They must agree exactly, or a record can appear as *upcoming* in the forecast
and *overdue* in `archival_candidates` on the same day. Writing the inverse the
obvious way (`joining + timedelta(days=years * 365.25)`) breaks this, because
`date + timedelta` keeps only whole days and silently drops the `.75`.
- **Output is formatted text, not raw JSON dumps**, so the model can quote it
back to a user without reformatting.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues