local-data-mcp
An MCP server that exposes a local SQLite database to LLM clients (Claude Desktop, Claude Code, MCP Inspector, …) as structured, read-only tools.
Every tool returns a Pydantic model, so clients get both an outputSchema up front and typed
structuredContent in each result (plus a JSON text fallback for older clients).
Quick start
uv sync
uv run local-data-mcp --seed # creates data/store.db with sample store data
uv run pytest # 16 tests, incl. a real stdio round-trip
uv run local-data-mcp # serve over stdio
Point it at your own database with --db path/to/file.db or LOCAL_DATA_DB=.... The generic tools
(list_tables, describe_table, run_query) work on any SQLite file. The domain tools assume the
sample schema, so replace them with ones that fit your data (see "Adapting" below).
What it exposes
| Kind | Name | Purpose |
|---|---|---|
| tool | list_tables |
Tables + row counts |
| tool | describe_table |
Columns, FKs, row count, 3 sample rows |
| tool | run_query |
Ad-hoc read-only SQL (SELECT/WITH/EXPLAIN), row-limited |
| tool | search_products |
Filter by name, category, price range, stock |
| tool | get_customer |
Customer by id/email + lifetime value + recent orders with items |
| tool | sales_summary |
Revenue/units/orders grouped by month, category or product, with a date range |
| resource | schema://database |
Full DDL |
| prompt | analyze_sales |
Canned analysis workflow using the tools above |
All tools carry readOnlyHint / idempotentHint annotations.
Safety model
- Connections open the file with
mode=ro, so SQLite itself refuses writes. run_queryalso installs a SQLite authorizer that denies everything except SELECT/READ/FUNCTION/RECURSIVE. That covers cases likeWITH x AS (...) DELETE ...,ATTACHand pragmas.- Only one statement is allowed per call, and results are capped at
limit(max 1000), with atruncatedflag. - Table names in
describe_tableare checked againstsqlite_masterand never interpolated from raw input. - Bad input becomes a tool error (
isError: true) that the model can read and recover from, not a crash.
Connect a client
Claude Code
claude mcp add local-data -- uv --directory /ABSOLUTE/PATH/local-data-mcp run local-data-mcp
Claude Desktop (claude_desktop_config.json)
{
"mcpServers": {
"local-data": {
"command": "uv",
"args": ["--directory", "/ABSOLUTE/PATH/local-data-mcp", "run", "local-data-mcp"],
"env": { "LOCAL_DATA_DB": "/ABSOLUTE/PATH/local-data-mcp/data/store.db" }
}
}
}
MCP Inspector (interactive debugging)
npx @modelcontextprotocol/inspector uv run local-data-mcp
Over HTTP: uv run local-data-mcp --transport streamable-http serves at http://127.0.0.1:8000/mcp.
In stdio mode, stdout carries the protocol. Never
print()to stdout in server code; log to stderr.
Layout
src/local_data_mcp/
__init__.py CLI entry point (--db, --seed, --transport)
server.py MCPServer: tools, resource, prompt
models.py Pydantic output models -> outputSchema / structuredContent
db.py read-only SQLite access + authorizer
seed.py deterministic sample database
tests/ in-memory client tests + stdio end-to-end test
Adapting to your own data
- Swap
Databaseindb.pyfor your source (CSV viacsv/DuckDB, a JSON folder, Postgres, …). - Model each answer shape in
models.py. - Add one
@mcp.tool()per question the model should be able to answer. Give each a clear docstring (it becomes the tool description) andAnnotated[..., Field(description=...)]parameters (they become the input schema).