Skip to main content
Glama
cggazca

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).