mcp-document-chat
README.md
# MCP Document Chat
A command-line chat client for Claude that reaches its capabilities through a
[Model Context Protocol](https://modelcontextprotocol.io) server rather than
hard-coded functions. The server exposes a document store over all three MCP
primitives — tools, resources and prompts — and the CLI wires them into a
tool-use loop, `@`-mention context injection and `/`-slash commands.
Built with [FastMCP](https://gofastmcp.com) and the Anthropic Python SDK.
```
> /summarize deposition.md
> Tell me about @report.pdf and @plan.md
> Reformat @spec.txt as markdown
```
## Why this exists
Most "chat with an LLM" samples bolt tools directly onto the model call. This
one puts an MCP server in between, which is the part worth practising: the
server is a separate process that could be swapped, reused by any MCP host
(Claude Desktop, an IDE, another agent), and tested on its own.
## Attribution
This started from the MCP project in Anthropic's
[Building with the Claude API](https://anthropic.skilljar.com/claude-with-the-anthropic-api)
course. To keep the record straight:
- **Provided by the course** — the CLI shell (`core/`, `main.py`), packaging and
the original README. The first commit in this repository is that scaffold,
unmodified, so `git log` shows exactly what was given.
- **My work** — the MCP server and client (`mcp_server.py`, `mcp_client.py`),
which the exercise leaves as stubs, plus everything after: the FastMCP
migration, the bug fixes described below, the test suite and the tooling.
## Architecture
```
main.py wires config, clients and the CLI together
core/cli.py prompt-toolkit REPL, completion for / and @
core/cli_chat.py expands @mentions, dispatches /commands
core/chat.py the tool-use loop
core/claude.py Anthropic Messages API wrapper
core/tools.py collects MCP tools, executes tool_use blocks
mcp_client.py async MCP client over stdio
|
| stdio (JSON-RPC)
v
mcp_server.py FastMCP server over the document store
```
The server exposes:
| Primitive | Name | Purpose |
| --- | --- | --- |
| Tool | `read_doc_contents` | Return a document's contents |
| Tool | `edit_doc` | Exact-match string replacement in a document |
| Resource | `docs://documents` | The document index |
| Resource | `docs://documents/{doc_id}` | A single document |
| Prompt | `format` | Rewrite a document as markdown |
| Prompt | `summarize` | Summarize a document in three sentences |
Resources back the `@`-mention autocompletion, prompts back the `/`-commands,
and tools are what Claude calls during the loop.
## Setup
Requires Python 3.10+ and an Anthropic API key.
```bash
git clone <this-repo>
cd mcp-document-chat
cp .env.example .env # then add your ANTHROPIC_API_KEY
uv sync # or: pip install -e .
uv run main.py # or: python main.py
```
`.env` holds `ANTHROPIC_API_KEY`, `CLAUDE_MODEL` and `USE_UV`; see
`.env.example`. `.env` is gitignored — do not commit a real key.
## Usage
| Input | Effect |
| --- | --- |
| `plain text` | Ordinary chat turn; Claude may call the MCP tools |
| `@doc_id` | Inlines that document's contents as context |
| `/format <doc_id>` | Runs the `format` prompt from the server |
| `/summarize <doc_id>` | Runs the `summarize` prompt from the server |
Tab completes both document ids and command names. Ctrl+C or Ctrl+D exits.
## Development
```bash
uv sync --all-groups
uv run pytest # 27 tests, no network or API key needed
uv run ruff check .
uv run ruff format .
```
The server tests drive it through FastMCP's in-memory client transport, so the
real MCP request path is exercised without spawning a subprocess.
## Notes on the implementation
Beyond completing the stubs, working through this surfaced a handful of defects
in the scaffold, each fixed in its own commit with a regression test:
- **Tool errors crashed the session.** The handler for a failed tool call read
a variable that only gets bound by the call that had just raised, so every
transport failure surfaced as `UnboundLocalError` instead of being reported
back to Claude.
- **A bare `/format` killed the REPL.** The command parser indexed the argument
without checking it was there, and the loop only caught `KeyboardInterrupt`.
- **Document completion never fired.** The completer treated resource ids as
dicts when they are strings, so its guard was silently false for every
document — and would have raised `TypeError` for any id containing "id".
- **The `format` prompt was malformed.** An unclosed `<document_id` tag, an
instruction that trailed off mid-sentence, and a reference to an
`edit_document` tool that is registered as `edit_doc`.
- **`/summarize` was documented but never implemented**, so the first command a
new user copied out of the README failed.
The server was also migrated from FastMCP 1.x (the snapshot vendored into the
MCP Python SDK as `mcp.server.fastmcp`) to the standalone `fastmcp` package,
including the move from `Field()` defaults to
`Annotated[str, Field(description=...)]`, which the FastMCP docs prefer.
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues