Sharebox MCP Server
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 + SQLAlchemy + SQLite.
Table of Contents
Related MCP server: Trust Chain MCP
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:
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.
Every mutation writes an
audit_logsrow 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:
projects
Column | Type | Notes |
| Integer | primary key |
| String | indexed, required |
| String | |
| DateTime(tz) |
|
cases
Column | Type | Notes |
| Integer | primary key |
| Integer | FK to |
| String | indexed, required |
| String | defaults to |
| DateTime(tz) |
file_uploads
Column | Type | Notes |
| Integer | primary key |
| Integer | FK to |
| String | required |
| Integer | bytes |
| DateTime(tz) |
audit_logs
Column | Type | Notes |
| Integer | primary key |
| String | e.g. |
| String | e.g. |
| Integer | primary key of the mutated row |
| String | JSON blob of the relevant fields |
| 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. 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.
{
"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.
[
{
"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.
{
"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
descriptionparameter is accepted by the tool but is not currently persisted: thecasestable has no description column. Add one tomodels.pyand thread it throughcrud.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.
{
"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).
cd CustomMCPServer
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txtDependencies (requirements.txt): fastmcp, fastapi, sqlalchemy, pydantic, uvicorn.
Initialize the database
python run.py --init-dbThis 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
python run.pyWith 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:
fastmcp run app/mcp_server.py:mcpConnecting 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):
{
"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 is a self-contained end-to-end check of the CRUD and audit
layers. It bypasses MCP and drives crud.py directly.
python test_client.pyWarning: the script begins with
Base.metadata.drop_all()— it destroys the contents ofsharebox.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
└── .gitignoreDesign notes
Audit logging is a caller-committed hook. log_activity() calls db.add() but deliberately
does not commit — the comment in 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:
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 transactionadd_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:
@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
(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 |
| You skipped |
Summary always reports "Project not found" | Two different |
| Python older than 3.10. Upgrade, or rewrite |
Test passes but the server sees no data |
|
This server cannot be deployed
Maintenance
Related MCP Connectors
Tamper-evident proof creation and verification for AI agents via MCP, A2A, and REST.
- kanonikOAuthai.kanonik
Governance runtime for compliance: verified, human-approved writes to a tamper-evident record.
Find governed AI capabilities and verify signed receipts. Read-only, no account.
Register every AI agent, log every action, prove it. EU AI Act compliance built in.
Related MCP Servers
- AlicenseAqualityCmaintenanceEnables AI agents to sign decisions with post-quantum cryptographic proofs and maintain secure audit trails for compliance. It provides tools for stamping events, verifying chain integrity, and exporting audit data across industries like finance and healthcare.431 npmMIT
- AlicenseBqualityBmaintenanceManages trust chains and attestations with built-in EU AI Act compliance.57 npm38 PyPIMIT
- FlicenseNot gradedqualityDmaintenanceProvides versioned file storage for AI agents with immutable writes, auditing, and rollback capabilities.1-
- AlicenseNot gradedqualityCmaintenanceProvides an immutable, tamper-evident audit trail for AI agents, enabling event logging with cryptographic chaining, search, verification, and statistics.2MIT