Sharebox MCP Server
by dalvivishal
README.md
# Sharebox MCP Server
A **Model Context Protocol (MCP)** server that exposes a Sharebox-style project / case /
file-management system to any MCP-capable AI client (Claude Desktop, Claude Code, Cursor, or a
custom agent). Every write operation is recorded in an immutable audit trail, so an LLM can
create cases and register file uploads while leaving a complete forensic record.
Built with [FastMCP](https://github.com/jlowin/fastmcp) + SQLAlchemy + SQLite.
---
## Table of Contents
- [Why this exists](#why-this-exists)
- [Architecture](#architecture)
- [Data model](#data-model)
- [MCP tools](#mcp-tools)
- [Installation](#installation)
- [Running the server](#running-the-server)
- [Connecting an MCP client](#connecting-an-mcp-client)
- [Testing](#testing)
- [Project layout](#project-layout)
- [Design notes](#design-notes)
- [Extending the server](#extending-the-server)
- [Troubleshooting](#troubleshooting)
---
## Why this exists
Document and case management systems (legal discovery, insurance claims, compliance
workflows) are exactly the kind of tool an AI assistant should be able to drive — "how many
open cases are on project 7?", "log this file against case 42" — but they are also exactly the
kind of system where an unattributed mutation is unacceptable.
This server addresses both halves:
1. **Read and write tools** are exposed over MCP, so an LLM can query and mutate the system
through a typed, self-describing interface instead of scraping a UI.
2. **Every mutation writes an `audit_logs` row** alongside the data change, capturing the
action, the table, the record ID, and a JSON payload of the details.
---
## Architecture
```
+----------------------+
| MCP Client | Claude Desktop / Claude Code / Cursor / custom agent
| (the LLM host) |
+----------+-----------+
| JSON-RPC over stdio (MCP transport)
v
+----------------------+
| app/mcp_server.py | FastMCP("ShareboxMCPServer")
| @mcp.tool() | 4 registered tools; each opens and closes its own DB session
+----------+-----------+
v
+----------------------+
| app/crud.py | business logic + add_entrust_logs() audit hook
+----------+-----------+
v
+----------------------+
| app/models.py | SQLAlchemy ORM
| app/database.py | engine / SessionLocal / Base
+----------+-----------+
v
sharebox.db (SQLite)
```
The server speaks **stdio** transport by default, which is what desktop MCP clients launch and
pipe into. There is no HTTP surface unless you add one.
---
## Data model
Four tables, defined in [app/models.py](app/models.py):
### `projects`
| Column | Type | Notes |
|---|---|---|
| `id` | Integer | primary key |
| `name` | String | indexed, required |
| `description` | String | |
| `created_at` | DateTime(tz) | `server_default=now()` |
### `cases`
| Column | Type | Notes |
|---|---|---|
| `id` | Integer | primary key |
| `project_id` | Integer | FK to `projects.id`, required |
| `title` | String | indexed, required |
| `status` | String | defaults to `"open"` |
| `created_at` | DateTime(tz) | |
### `file_uploads`
| Column | Type | Notes |
|---|---|---|
| `id` | Integer | primary key |
| `case_id` | Integer | FK to `cases.id`, required |
| `filename` | String | required |
| `file_size` | Integer | bytes |
| `uploaded_at` | DateTime(tz) | |
### `audit_logs`
| Column | Type | Notes |
|---|---|---|
| `id` | Integer | primary key |
| `action` | String | e.g. `"CREATE"` |
| `table_name` | String | e.g. `"cases"` |
| `record_id` | Integer | primary key of the mutated row |
| `details` | String | JSON blob of the relevant fields |
| `timestamp` | DateTime(tz) | |
Relationships: `Project 1-N Case 1-N FileUpload`. `AuditLog` is deliberately **not**
foreign-keyed to anything — it stores `table_name` + `record_id` as loose references, so a log
row survives even if the target row is later removed.
---
## MCP tools
All four are declared in [app/mcp_server.py](app/mcp_server.py). The docstring on each function
is what the LLM sees as the tool description, so it is part of the interface.
### `get_project_summary(project_id: int) -> dict`
Read-only. Returns project metadata plus case counts.
```json
{
"project_id": 1,
"name": "Project Alpha",
"description": "A confidential test project",
"total_cases": 3,
"open_cases": 2
}
```
Returns `{"error": "Project not found"}` if the ID does not exist.
### `list_recent_file_uploads(days: int = 1) -> list[dict]`
Read-only. Returns every upload with `uploaded_at >= now() - days`.
```json
[
{
"id": 1,
"filename": "evidence.pdf",
"case_id": 1,
"file_size": 10240,
"uploaded_at": "2026-09-15 08:14:22"
}
]
```
### `create_new_case(project_id: int, title: str, description: str = "") -> dict`
**Mutating — audited.** Validates that the parent project exists, creates the case with
`status="open"`, and writes a `CREATE` / `cases` audit row.
```json
{
"success": true,
"case_id": 12,
"title": "Case 100",
"message": "Case created and securely audited."
}
```
On a bad `project_id` it returns `{"error": "Project with ID N does not exist."}` rather than
raising — MCP clients handle a structured error better than a stack trace.
> The `description` parameter is accepted by the tool but is not currently persisted: the
> `cases` table has no description column. Add one to `models.py` and thread it through
> `crud.create_case()` if you need it stored.
### `upload_file_metadata(case_id: int, filename: str, file_size: int) -> dict`
**Mutating — audited.** Validates that the parent case exists, records the metadata, and writes
a `CREATE` / `file_uploads` audit row.
```json
{
"success": true,
"file_upload_id": 5,
"message": "File metadata uploaded and securely audited."
}
```
> This tool records **metadata only**. No file bytes cross the MCP boundary. Actual file
> transfer is expected to happen out-of-band; this server tracks that it happened.
### Not exposed as a tool
`crud.create_project()` exists and is audited, but is intentionally **not** registered as an MCP
tool — creating top-level projects is treated as an administrative action reserved for a human.
Register it with `@mcp.tool()` if you want to change that policy.
---
## Installation
Requires **Python 3.10+** (the tool signatures use `list[dict]` builtin generics).
```bash
cd CustomMCPServer
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
```
Dependencies (`requirements.txt`): `fastmcp`, `fastapi`, `sqlalchemy`, `pydantic`, `uvicorn`.
### Initialize the database
```bash
python run.py --init-db
```
This runs `Base.metadata.create_all()` and produces `sharebox.db` in the working directory. It
is idempotent — safe to re-run; it will not drop existing tables.
---
## Running the server
```bash
python run.py
```
With no flags, `run.py` calls `mcp.run()`, which serves MCP over **stdio**. You will see no
output and the process will appear to hang — that is correct. It is waiting for JSON-RPC frames
on stdin. Stop it with `Ctrl+C`.
You can also use the FastMCP CLI:
```bash
fastmcp run app/mcp_server.py:mcp
```
---
## Connecting an MCP client
### Claude Desktop / Claude Code
Add the server to your MCP config (`claude_desktop_config.json`, or via `claude mcp add` in
Claude Code):
```json
{
"mcpServers": {
"sharebox": {
"command": "/absolute/path/to/CustomMCPServer/venv/bin/python",
"args": ["/absolute/path/to/CustomMCPServer/run.py"],
"cwd": "/absolute/path/to/CustomMCPServer"
}
}
}
```
On Windows, use `venv\\Scripts\\python.exe` as the `command`.
**`cwd` matters.** The SQLite URL is the relative path `sqlite:///./sharebox.db`, so the server
resolves the database against whatever directory it was launched from. Point `cwd` at the
project root, or you will silently get a fresh, empty database.
Restart the client. The four tools should appear in its tool list, and you can then ask things
like *"Summarize project 1"* or *"Open a new case on project 1 called Discovery Batch 3."*
---
## Testing
[test_client.py](test_client.py) is a self-contained end-to-end check of the CRUD and audit
layers. It bypasses MCP and drives `crud.py` directly.
```bash
python test_client.py
```
> **Warning:** the script begins with `Base.metadata.drop_all()` — it **destroys** the contents
> of `sharebox.db`. Never run it against a database you care about.
It exercises the full path (create project, create case, upload file metadata, read summary,
list uploads) and then asserts that exactly 3 audit rows were written, exiting with status `1`
if the count is wrong.
Expected tail:
```
6. Verifying Audit Logs...
Total Audit Logs: 3
- Action: CREATE, Table: projects, Record ID: 1, Details: {"name": "Project Alpha"}
- Action: CREATE, Table: cases, Record ID: 1, Details: {"title": "Case 100", "project_id": 1}
- Action: CREATE, Table: file_uploads, Record ID: 1, Details: {...}
SUCCESS: 3 audit logs successfully recorded.
```
---
## Project layout
```
CustomMCPServer/
├── app/
│ ├── __init__.py
│ ├── crud.py # business logic + audit logging
│ ├── database.py # engine, SessionLocal, Base, get_db()
│ ├── mcp_server.py # FastMCP instance + the 4 @mcp.tool() definitions
│ └── models.py # SQLAlchemy ORM models
├── requirements.txt
├── run.py # entrypoint: --init-db, or serve MCP over stdio
├── test_client.py # destructive end-to-end CRUD/audit test
└── .gitignore
```
---
## Design notes
**Audit logging is a caller-committed hook.** `log_activity()` calls `db.add()` but deliberately
does **not** commit — the comment in [app/crud.py](app/crud.py) states the intent: the caller
commits, so the data change and the audit row land atomically.
In the current code the create functions commit the entity first, then log, then commit again.
A crash between those two commits would leave an entity with no audit row. If you need a hard
guarantee, restructure to a single commit:
```python
db.add(db_case)
db.flush() # assigns db_case.id without committing
add_entrust_logs(db, "CREATE", "cases", db_case.id, {...})
db.commit() # one atomic transaction
```
**`add_entrust_logs` is an alias.** It forwards to `log_activity()` verbatim; the alias exists to
satisfy an external naming requirement. Either name works.
**Session per tool call.** Each `@mcp.tool()` opens a `SessionLocal()` and closes it in a
`finally` block. Tools are therefore safe to call concurrently, and no session outlives a single
request.
**`check_same_thread=False`** is set on the SQLite connection because FastMCP may dispatch tool
calls from a thread other than the one that created the engine.
**Tool-level error handling.** Mutating tools catch `ValueError` from the CRUD layer and return
`{"error": "..."}`. This is deliberate: an LLM can read and recover from a returned error
object, whereas a raised exception becomes an opaque protocol-level failure.
**`list_recent_file_uploads` uses naive UTC.** The cutoff is computed with `datetime.utcnow()`
and compared against a timezone-aware column. This works on SQLite, which stores naive
timestamps, but will need an aware cutoff (`datetime.now(timezone.utc)`) if you move to
PostgreSQL.
---
## Extending the server
**Add a tool** — decorate a function in `app/mcp_server.py`:
```python
@mcp.tool()
def close_case(case_id: int) -> dict:
"""Close an open case. This action is strictly logged in the audit trail."""
db = get_db_session()
try:
case = crud.close_case(db, case_id) # add this to crud.py
return {"success": True, "case_id": case.id}
except ValueError as e:
return {"error": str(e)}
finally:
db.close()
```
Write the docstring carefully — it is the only description the LLM gets.
**Audit a new mutation** — call `add_entrust_logs(db, action, table_name, record_id, details)`
inside the CRUD function before the final commit. The `details` dict is JSON-serialized, so keep
it to primitives.
**Switch databases** — change `SQLALCHEMY_DATABASE_URL` in [app/database.py](app/database.py)
(e.g. `postgresql+psycopg://user:pass@host/db`) and drop the SQLite-only `connect_args`.
---
## Troubleshooting
| Symptom | Cause / fix |
|---|---|
| Client lists the server but shows zero tools | The process crashed at import. Run `python run.py` manually and read the traceback — stdio transport hides startup errors in the client UI. |
| `no such table: projects` | You skipped `python run.py --init-db`, or `cwd` points elsewhere so a blank `sharebox.db` was created. |
| Summary always reports "Project not found" | Two different `sharebox.db` files. Confirm the `cwd` in your MCP config matches where you ran `--init-db`. |
| `TypeError: 'type' object is not subscriptable` | Python older than 3.10. Upgrade, or rewrite `list[dict]` as `List[Dict]` from `typing`. |
| Test passes but the server sees no data | `test_client.py` drops and recreates all tables. Re-seed after running it. |
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues