docs: describe the MCP surface and how to connect a client
`docs/MCP.md` is the manual a person needs to put this in front of an agent:
the 29 tools with their arguments, the workflow ("generate, then write it in"
in one call, versus paragraph by paragraph), the error contract, and the exact
configuration for the three clients in play — the Harness's
`cordis.patch.yml`, Claude Code's `claude mcp add` and `.mcp.json`, and Codex's
`config.toml` — including the note that a new Harness entry has to sit inside
the `insert:` list or it is silently treated as an override of an entry that
does not exist.
`overview.md` gains the section that belongs in a design document rather than a
manual: why the MCP surface is shaped differently from the REST one, and the
two rules a new tool has to follow — keep the docstring to a line or two,
because it is sent with every request, and register with
`structured_output=False`, because the inferred envelope sends the same JSON
twice and clients disagree about which copy counts.
This commit is contained in:
@@ -22,6 +22,7 @@ touching a single sentence.
|
||||
- FastAPI (Python) — REST API
|
||||
- SQLAlchemy + Alembic — ORM layer and schema migrations
|
||||
- TiDB v8.5.0 — MySQL-compatible distributed SQL database, deployed in k3s
|
||||
- MCP Python SDK — the same domain layer, exposed as MCP tools (see below)
|
||||
- Dependencies managed with `venv` + `requirements.txt`
|
||||
|
||||
**Frontend**
|
||||
@@ -303,6 +304,51 @@ Conventions worth knowing:
|
||||
are accepted on write and de-duplicated.
|
||||
- Paper `status` is one of `draft`, `writing`, `done`.
|
||||
|
||||
## MCP server
|
||||
|
||||
The same domain layer is also served over the **Model Context Protocol**, so an
|
||||
agent — Claude Code, Codex, the DeepSeek Harness — can write a paper without a
|
||||
browser. Full manual: [docs/MCP.md](MCP.md).
|
||||
|
||||
It is a third front door rather than a second implementation. The REST routes,
|
||||
the MCP tools and the seed script all call `app.crud` and all validate through
|
||||
`app.schemas`, so a rule fixed in the CRUD layer is fixed everywhere and a paper
|
||||
written by an agent is indistinguishable from one written by hand. What
|
||||
`app/mcp/` adds is only what a *model* needs and a browser does not:
|
||||
|
||||
| Addition | Why the REST shape is wrong for a model |
|
||||
|---|---|
|
||||
| 29 tools with `paper_` / `paragraph_` / `sentence_` / `template_` / `field_` prefixes | a model picks a tool out of a list by its name, not by reading 29 descriptions |
|
||||
| JSON strings, `None`-free, no pagination envelope on reads | a tool result is paid for in context tokens |
|
||||
| `paragraph_write` addressed by **heading** as well as position | nobody writing "1. Introduction" knows the template places it at `sort = 20` |
|
||||
| `paper_write` / `paper_write_text` | "generate the paper, then put it in" is one intention, not thirty round trips |
|
||||
| `paper_delete` refuses once before it deletes | a cascading delete has no undo in a tool call |
|
||||
| `sentence_search` across papers | the job is consistency — one paper says 洪水损失, the next must not say GUL |
|
||||
|
||||
Two transports are served from one build: `stdio`, which is what a client
|
||||
spawns, and `streamable-http`, which is what a client on another machine
|
||||
connects to (behind a bearer token). The package is split so that only
|
||||
`app/mcp/server.py` knows a transport exists:
|
||||
|
||||
```
|
||||
app/mcp/
|
||||
├── server.py transports, CLI, and the instructions sent at initialize
|
||||
├── support.py sessions, JSON shaping, heading resolution, the one text splitter
|
||||
├── specs.py the shapes a model may send (a paragraph, a sentence, a citation)
|
||||
├── selfcheck.py --check: connect once and print the tool surface
|
||||
└── tools/ papers, paragraphs, sentences, templates, fields — registered
|
||||
by register_all(), each tool three lines around an app.crud call
|
||||
```
|
||||
|
||||
Two decisions are worth knowing before adding a tool. **Results are registered
|
||||
with `structured_output=False`**: inferred from a `-> str` annotation, the SDK
|
||||
publishes a `{"result": …}` envelope and sends the JSON twice, once as
|
||||
`structuredContent` and once as text, and clients that read only one of the two
|
||||
then disagree about what the tool returned. And **the docstring is the tool
|
||||
description verbatim**, sent with every request — so a tool docstring is one or
|
||||
two lines and the reasoning goes in the module docstring, where it costs
|
||||
nothing.
|
||||
|
||||
## Frontend
|
||||
|
||||
### Shell
|
||||
@@ -431,11 +477,14 @@ paper-doc/
|
||||
│ │ ├── core/ # settings and configuration
|
||||
│ │ ├── crud/ # data-access helpers
|
||||
│ │ ├── db/ # engine, session, declarative base
|
||||
│ │ ├── mcp/ # MCP server: tools, specs, transports, self-check
|
||||
│ │ ├── models/ # SQLAlchemy models
|
||||
│ │ └── schemas/ # Pydantic request/response models
|
||||
│ ├── alembic/ # migration environment and revisions
|
||||
│ ├── scripts/seed.py # idempotent seed for the field library + templates
|
||||
│ ├── scripts/smoke_papers.py # end-to-end check of the writing loop
|
||||
│ ├── scripts/mcp_server.py # MCP entry point (stdio / http)
|
||||
│ ├── scripts/smoke_mcp.py # the same loop, spoken over MCP by a real client
|
||||
│ ├── alembic.ini
|
||||
│ ├── requirements.txt
|
||||
│ └── .env.example
|
||||
@@ -520,3 +569,18 @@ exits non-zero on the first failed expectation and cleans up after itself:
|
||||
cd backend
|
||||
.venv/bin/python scripts/smoke_papers.py
|
||||
```
|
||||
|
||||
**Checking the MCP server**
|
||||
|
||||
`scripts/mcp_server.py --check` connects to the database once and prints the
|
||||
tool surface, which is the failure this catches: a client that spawned the
|
||||
server successfully and then sees every call fail. `scripts/smoke_mcp.py`
|
||||
drives the whole writing loop through a real MCP client — the same child
|
||||
process and JSON-RPC over stdin/stdout that Claude Code, Codex and the Harness
|
||||
use — and runs unchanged against `--url` for the HTTP transport:
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
.venv/bin/python scripts/mcp_server.py --check
|
||||
.venv/bin/python scripts/smoke_mcp.py
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user