books-catalog
by cggazca
README.md
# mcp-quickstart — a books-catalog MCP server
A clean, well-documented **Model Context Protocol (MCP)** server, built with
**FastMCP**, that exposes a local SQLite catalog of public-domain books as a set
of well-designed tools. It is meant as a reference for *how to design MCP tools*
an LLM can use reliably: clear names, schema-generating type hints,
docstrings-as-contracts, validated inputs, and actionable error messages.
The data is a small set of public-domain classics (Austen, Verne, Doyle,
Dostoevsky, ...) authored as seed data for this repo — nothing proprietary, and
deliberately a generic domain.
---
## What it exposes
Five tools over a 30-book catalog:
| Tool | Signature | Purpose |
| --- | --- | --- |
| `search_books` | `(query: str, limit: int = 10)` | Substring search over title + author. |
| `get_book` | `(book_id: int)` | Fetch one book by id; clear error if missing. |
| `find_books_by_genre` | `(genre, year_from?, year_to?, limit=20)` | Filter by genre + optional year range. |
| `top_rated_books` | `(limit=5, genre?)` | Highest-rated, optionally within a genre. |
| `catalog_stats` | `()` | Totals, per-genre counts, year range, averages. |
---
## Quickstart
```bash
git clone <this-repo> && cd mcp-quickstart
python -m pip install -r requirements.txt # installs `mcp` and `pytest`
# Smoke test — list the tools and schemas without a live client:
python -m books_mcp list-tools
# Run the test suite (offline, in-memory catalog):
python -m pytest
# Run the server (stdio transport — this is what an MCP client launches):
python -m books_mcp
```
The smoke test output:
```text
books-catalog MCP server exposes 5 tools (catalog: 30 books):
- search_books(query, limit)
Search the catalog by a keyword in the title or author.
- get_book(book_id)
Fetch a single book by its numeric id.
- find_books_by_genre(genre, year_from, year_to, limit)
List books in a genre, optionally filtered by publication-year range.
- top_rated_books(limit, genre)
Return the highest-rated books, optionally within a single genre.
- catalog_stats()
Summarize the whole catalog.
```
Example tool result (`catalog_stats()`):
```json
{
"total_books": 30,
"total_authors": 19,
"genres": { "Adventure": 7, "Fiction": 7, "Gothic": 6, "ScienceFiction": 3,
"Historical": 2, "Mystery": 2, "Romance": 2, "Fantasy": 1 },
"year_range": [1811, 1902],
"avg_pages": 468.4,
"avg_rating": 4.17
}
```
---
## MCP tool-design patterns demonstrated
These are the choices that make tools usable *by a model*, not just callable:
1. **One job per tool, named as a verb-phrase.** `search_books`,
`find_books_by_genre`, `get_book`. A model picks the right tool from the name
alone.
2. **The docstring is the contract.** FastMCP turns each function's signature
into the tool's JSON input schema and its docstring into the description the
model sees. Both are written for a model reader: what it does, every argument,
the return shape, and what raises.
3. **Type hints drive the schema.** `book_id: int`, `genre: str`,
`year_from: int | None = None` become a validated input schema with the right
types and optionality — no hand-written JSON schema.
4. **Validate and bound every input.** Limits are clamped to a hard `MAX_LIMIT`
so a tool call can never trigger an unbounded scan; empty queries and bad
ids are rejected up front.
5. **Errors are actionable.** An unknown genre returns
`Unknown genre 'Cyberpunk'. Available genres: Adventure, Fiction, ...`; a bad
id returns the valid range. The model can correct itself from the message.
6. **Outputs are plain JSON-serializable dicts**, never database row objects —
stable, predictable, and easy for the model to consume.
7. **The data layer is separate from the protocol.** `db.py` is a typed,
independently testable SQLite layer; `server.py` only does validation +
wiring. You can unit-test all the logic without standing up a server.
---
## Connect it to an MCP client
### Claude Desktop
Add the server to your `claude_desktop_config.json`
(macOS: `~/Library/Application Support/Claude/`;
Windows: `%APPDATA%\Claude\`):
```json
{
"mcpServers": {
"books-catalog": {
"command": "python",
"args": ["-m", "books_mcp"],
"cwd": "/absolute/path/to/mcp-quickstart"
}
}
}
```
Restart Claude Desktop; the five tools appear under the server. Then ask
natural-language questions like *"What are the top-rated adventure books before
1880?"* and the model will call `find_books_by_genre` / `top_rated_books`.
### Any MCP client
The server speaks the standard MCP stdio transport, so any compliant client
(the MCP Inspector, a custom client using the `mcp` SDK, etc.) can launch
`python -m books_mcp` and discover the tools via the normal `list_tools` /
`call_tool` handshake.
---
## Storage
By default the server builds an **in-memory** SQLite catalog from the bundled
seed data at startup — zero setup, always fresh, no files left behind. To
materialize a file-backed catalog (e.g. to inspect it with the `sqlite3` CLI or
share it across processes):
```bash
python -m books_mcp seed --db catalog.db
BOOKS_DB_PATH=catalog.db python -m books_mcp # serve from the file
```
---
## Project layout
```
mcp-quickstart/
├── books_mcp/
│ ├── __init__.py
│ ├── __main__.py # serve | list-tools | seed
│ ├── server.py # FastMCP server + the five tools
│ ├── db.py # typed SQLite data-access layer (no MCP)
│ ├── seed.py # build/seed a file-backed catalog
│ └── data/
│ └── books.json # public-domain seed catalog
└── tests/ # pytest suite, all offline
├── test_db.py # data-layer behavior
└── test_tools.py # tool behavior, validation, and registration
```
## License
MIT — see [LICENSE](LICENSE).
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues