DocumentMCP
README.md
<div align="center">
# MCP — Serveur documentaire
**Un serveur Model Context Protocol complet, et les preuves qu'il fonctionne.**
Tools · Resources · Prompts · Client · Tests · Guide 24 pages
[](https://www.python.org/)
[](https://modelcontextprotocol.io/)
[](#-tester)
[](docs/MCP-Guide.pdf)
</div>
---
Ce dépôt contient le projet fil rouge du cours
_[Introduction to Model Context Protocol](https://academy.claude.com/courses/introduction-to-model-context-protocol)_
(Claude Academy), reconstruit, corrigé et documenté.
L'objectif n'est pas seulement d'avoir un serveur qui démarre : c'est de **montrer
comment on prouve qu'il fonctionne**, sans modèle, sans clé API, et de documenter les
pièges réels rencontrés en chemin.
> 📘 **Le guide complet (24 pages, PDF)** : [`docs/MCP-Guide.pdf`](docs/MCP-Guide.pdf)
> — architecture, les trois primitives, construction pas à pas, les trois niveaux de
> test, étude de cas illustrée et 14 pièges documentés.
---
## Sommaire
- [Ce que fait le serveur](#-ce-que-fait-le-serveur)
- [Démarrage rapide](#-démarrage-rapide)
- [Tester — les trois niveaux](#-tester--les-trois-niveaux)
- [Les trois primitives expliquées](#-les-trois-primitives-expliquées)
- [Structure du dépôt](#-structure-du-dépôt)
- [Brancher sur Claude Desktop / Claude Code](#-brancher-sur-claude-desktop--claude-code)
- [Pièges connus](#-pièges-connus)
- [Aller plus loin](#-aller-plus-loin)
---
## 🧩 Ce que fait le serveur
Un mini gestionnaire de documents, avec les trois familles de capacités du protocole.
| Élément | Type | Qui décide | Rôle |
|---|---|---|---|
| `read_doc_contents` | **tool** | le modèle | lire un document |
| `edit_document` | **tool** | le modèle | remplacer une chaîne (première occurrence) |
| `search_docs` | **tool** | le modèle | chercher un mot-clé, renvoyer des extraits |
| `docs://documents` | **resource** directe | l'application | liste des ids, en JSON — pour l'autocomplete `@` |
| `docs://documents/{doc_id}` | **resource** templatée | l'application | contenu d'un document, en texte brut |
| `format` | **prompt** | l'utilisateur | instructions de réécriture en markdown |
Les documents sont stockés dans un dictionnaire en mémoire : le serveur repart de son
état initial à chaque redémarrage.
---
## 🚀 Démarrage rapide
### Linux / macOS
```bash
git clone https://github.com/thekiller-dev/mcp-server-course.git
cd mcp-server-course
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
```
Ou avec [uv](https://docs.astral.sh/uv/) :
```bash
uv venv && uv pip install -r requirements.txt
```
### Windows (PowerShell)
```powershell
python -m venv .venv
.venv\Scripts\pip install -r requirements.txt
```
### Fish (Linux)
```fish
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
# ou, pour tout enchaîner d'un coup :
fish setup.fish
```
> ⚠️ **Version du SDK.** Le cours utilise `FastMCP`. Depuis la 2.x, le SDK `mcp` l'a
> renommé `MCPServer` et a changé plusieurs signatures : tout le code ci-dessous casse
> avec `No module named 'mcp.server.fastmcp'`. `requirements.txt` épingle donc
> `mcp[cli]<2`.
---
## 🧪 Tester — les trois niveaux
### Niveau 1 — sans clé API, sans navigateur (3 secondes)
```bash
.venv/bin/python test_server.py
```
Ce script lance le serveur en sous-processus et parle le protocole en stdio, exactement
comme le ferait Claude Desktop. Il vérifie les tools et leurs schémas, appelle un tool,
lit les deux resources, récupère le prompt.
```
✅ connecté à DocumentMCP v1.30.0
TOOLS : read_doc_contents, edit_document, search_docs
RESOURCES : [('docs://documents', 'application/json')]
RESOURCE TEMPLATES : [('docs://documents/{doc_id}', 'text/plain')]
PROMPTS : format
🎉 tous les tests passent
```
### Niveau 2 — la boucle complète, racontée
```bash
.venv/bin/python demo_sans_cle.py
```
Rejoue ce que ferait un modèle, tool call après tool call, en affichant chaque étape :
lecture, édition, vérification de la persistance, **erreur volontaire** (l'exception
revient comme résultat, sans casser la session), lecture des resources, récupération du
prompt.
### Niveau 3 — l'inspecteur
```bash
.venv/bin/mcp dev mcp_server.py # nécessite Node.js (npx)
```
Le terminal affiche une URL du type `http://localhost:6274/?MCP_PROXY_AUTH_TOKEN=…`.
Ouvrez **l'URL complète** — sans le jeton, la connexion est refusée.
À essayer dans l'interface :
| Onglet | Valeur | Ce que ça prouve |
|---|---|---|
| Tools → `read_doc_contents` | `{"document_id": "plan.md"}` | schéma dérivé du code |
| Tools → `edit_document` | old `4. Ship it`, new `4. Ship it to production` | l'état persiste entre deux appels |
| Tools → `edit_document` | old `ce texte n'existe pas` | **une erreur reste une réponse** |
| Resources | `docs://documents` | sérialisation JSON automatique |
| Resource Templates | `docs://documents/{doc_id}` + `report.pdf` | parsing du gabarit d'URI |
| Prompts | `format` + `email.md` | prompt calculé côté serveur |
Panneau de droite, onglet **Protocol** : chaque ligne est un message JSON-RPC réel.
### Niveau 4 — un modèle aux commandes
```bash
export ANTHROPIC_API_KEY="sk-ant-..." # PowerShell : $env:ANTHROPIC_API_KEY = "..."
.venv/bin/python mcp_client.py
```
Trois questions suffisent à valider le découpage :
| Question | Ce qui doit se passer | Primitive |
|---|---|---|
| `Quels documents parlent de revenue ?` | Claude appelle `search_docs` seul | tool |
| `@report.pdf résume les chiffres` | **aucun** tool call, contenu injecté | resource |
| `/format email.md` | instructions récupérées puis envoyées | prompt |
La deuxième ligne est la plus importante : un tool call là où une resource suffit
signale une architecture mal découpée.
---
## 🧠 Les trois primitives expliquées
La différence n'est pas technique, elle tient à **qui décide**.
```
Ça lit des données → resource (l'application décide)
Ça agit, ou ça peut échouer → tool (le modèle décide)
C'est une consigne destinée au modèle → prompt (l'utilisateur décide)
```
### Un tool : le schéma vient du code
```python
@mcp.tool(
name="read_doc_contents",
description="Read the contents of a document and return it as a string.",
)
def read_document(
document_id: str = Field(description="Id of the document to read"),
) -> str:
if document_id not in docs:
raise ValueError(f"Doc with id {document_id} not found")
return docs[document_id]
```
- le type hint `str` devient `"type": "string"` dans le JSON Schema ;
- `Field(description=…)` devient la description du paramètre — c'est du **prompt
engineering**, pas de la décoration ;
- `raise ValueError` devient un **résultat d'erreur** renvoyé au modèle, pas un crash.
Vous n'écrivez jamais de schéma JSON à la main : il ne peut donc pas dériver de
l'implémentation.
### Une resource : directe ou templatée
```python
@mcp.resource("docs://documents", mime_type="application/json")
def list_docs() -> list[str]:
return list(docs.keys())
@mcp.resource("docs://documents/{doc_id}", mime_type="text/plain")
def fetch_doc(doc_id: str) -> str:
if doc_id not in docs:
raise ValueError(f"Doc with id {doc_id} not found")
return docs[doc_id]
```
Le préfixe d'URI est libre : ce n'est pas une URL réseau, c'est un espace de noms. Le SDK
extrait `{doc_id}` et le passe en argument nommé.
### Un prompt : la couche utilisateur
```python
@mcp.prompt(name="format", description="Rewrite the document as clean markdown.")
def format_document(doc_id: str = Field(description="Id of the document")) -> str:
return f"""Please rewrite the following document into clean markdown.
Do not invent information.
<document id="{doc_id}">
{docs.get(doc_id, "Document not found")}
</document>"""
```
---
## 📁 Structure du dépôt
```
mcp-server-course/
├── mcp_server.py # le serveur : 3 tools, 2 resources, 1 prompt
├── mcp_client.py # client + boucle agentique avec Claude (@mention, /format)
├── test_server.py # test de protocole — aucune clé API requise
├── demo_sans_cle.py # la boucle complète, rejouée et commentée
├── requirements.txt # mcp[cli]<2, anthropic
├── pyproject.toml # [tool.uv] package = false (voir « Pièges connus »)
├── setup.fish # installation + test en une commande (fish)
└── docs/
├── MCP-Guide.pdf # le guide de 24 pages
└── screenshots/ # preuves d'exécution (inspecteur, JSON-RPC)
```
---
## 🔌 Brancher sur Claude Desktop / Claude Code
**Claude Desktop** — `claude_desktop_config.json` :
| OS | Chemin |
|---|---|
| Linux | `~/.config/Claude/claude_desktop_config.json` |
| macOS | `~/Library/Application Support/Claude/claude_desktop_config.json` |
| Windows | `%APPDATA%\Claude\claude_desktop_config.json` |
```json
{
"mcpServers": {
"documents": {
"command": "uv",
"args": ["--no-project", "--with", "mcp", "mcp", "run",
"/chemin/absolu/vers/mcp-server-course/mcp_server.py"]
}
}
}
```
Quittez **complètement** l'application puis relancez-la : la configuration n'est lue
qu'au démarrage.
**Claude Code** :
```bash
claude mcp add documents -- python /chemin/absolu/vers/mcp_server.py
```
---
## ⚠️ Pièges connus
Ces erreurs sont documentées parce qu'elles ont réellement été rencontrées.
| Symptôme | Cause | Solution |
|---|---|---|
| `command not found: uv` | binaire dans `~/.local/bin`, absent du PATH | `fish_add_path ~/.local/bin` |
| `syntax error: unexpected end of file` | chemin abîmé par un copier-coller (`[env.fish](http://…)`) | retaper la commande |
| `No module named 'mcp'` | Python du système au lieu du venv | `.venv/bin/python test_server.py` |
| `No module named 'mcp.server.fastmcp'` | SDK 2.x, `FastMCP` → `MCPServer` | `pip install "mcp[cli]<2"` |
| `Unable to determine which files to ship inside the wheel` | `uv run` tente de builder le dossier | `[tool.uv] package = false`, ou `uv run --no-project …` |
| terminal figé après `mcp run` | comportement normal du stdio | `mcp dev` pour un humain, `mcp run` pour un client |
| `npx: command not found` | Node.js absent | Node 20+ via NodeSource, pas le `npm` d'apt |
| `nodejs : Conflicts: npm` | nodejs NodeSource + npm d'apt | ne pas installer `npm` via apt |
| inspecteur « Disconnected » | URL sans le jeton | ouvrir le lien complet `?MCP_PROXY_AUTH_TOKEN=…` |
| le modèle ignore l'outil | description trop vague | dire *quand* l'utiliser, pas seulement ce qu'il fait |
**La règle de tri** : (1) le script de test répond-il ? (2) l'inspecteur exécute-t-il
l'outil à la main ? (3) seulement ensuite, regarder les descriptions. 90 % des problèmes
se règlent à l'étape 1.
---
## 🔭 Aller plus loin
1. **Resource de synthèse** — `stats://documents` renvoyant en JSON le nombre de
documents, le nombre total de mots et le document le plus long.
2. **Tool d'écriture complète** — `create_document(document_id, content)`, avec erreur
explicite si l'identifiant existe déjà.
3. **Transport HTTP** — `mcp.run(transport="streamable-http")`, connexion avec
`mcp.client.streamable_http`, puis sortie du mono-utilisateur local.
4. **Persistance** — remplacer le dictionnaire par des fichiers ou une base. La surface
MCP ne bouge pas : c'est le signe d'une bonne séparation.
---
## 📚 Ressources
- [Introduction to Model Context Protocol](https://academy.claude.com/courses/introduction-to-model-context-protocol) — Claude Academy
- [Spécification MCP](https://modelcontextprotocol.io/specification)
- [SDK Python](https://github.com/modelcontextprotocol/python-sdk)
- [MCP Inspector](https://github.com/modelcontextprotocol/inspector)
---
<div align="center">
<sub>Construit en apprenant · documenté en déboguant · partagé pour que ça serve.</sub>
</div>
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues