Skip to main content
Glama
JuanCamiloMendoza99

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.