# Pour les agents

Cette page s'adresse à un modèle qui lit ce fichier brut, sans rendu — le chemin machine,
sans détour humain. Chaque exemple ci-dessous a été rejoué contre un vrai serveur PalmaPlan
(pas recopié d'une spécification) ; les identifiants et jetons sont ceux de cet essai, à
remplacer par les vôtres.

## 1. Découvrir le serveur

Un appel à `/mcp` sans jeton dit où aller :

```
POST /mcp
Content-Type: application/json
Accept: application/json, text/event-stream

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}
```

```
401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="http://<hôte>/.well-known/oauth-protected-resource"

{"type":"https://palmaplan.dev/errors/UNAUTHORIZED","title":"/mcp n accepte qu un Bearer : un jeton nommé de Réglages › Connexions et agents, absent, invalide ou révoqué","status":401,"code":"UNAUTHORIZED"}
```

`GET` l'adresse portée par `resource_metadata` donne la ressource et son émetteur :

```json
{
  "resource": "http://<hôte>/mcp",
  "authorization_servers": ["http://<hôte>"],
  "scopes_supported": ["read", "write"],
  "bearer_methods_supported": ["header"]
}
```

`GET /.well-known/oauth-authorization-server` sur cet émetteur donne les points de
terminaison OAuth (`authorization_endpoint`, `token_endpoint`, `registration_endpoint`,
`revocation_endpoint`) — utile si vous suivez le chemin 2b ci-dessous.

## 2. Obtenir un Bearer

Deux chemins, selon si un humain est dans la boucle.

**2a. Un jeton déjà donné (le chemin le plus court).** Un humain crée un jeton nommé depuis
*Réglages › Connexions et agents › Accès avancés* (portée `read` ou `write`) et vous le confie — par variable
d'environnement, par exemple. Aucun aller-retour OAuth n'est nécessaire : ce jeton
s'utilise directement comme `Authorization: Bearer pp_…` sur `/mcp`. C'est le chemin du
stdio ([Claude Desktop](claude-desktop.md)), et il reste disponible en HTTP.

**2b. L'enregistrement dynamique + OAuth 2.1 (le chemin autonome).** Vous vous enregistrez
vous-même (`POST /oauth/register`, RFC 7591 : `client_name`, `redirect_uris`), puis
construisez une adresse d'autorisation avec PKCE S256 obligatoire et `resource` égale à
`<hôte>/mcp` (RFC 8707). **Un humain doit ouvrir cette adresse et cliquer « Autoriser »** —
cette étape ne s'automatise pas, par construction : c'est le consentement de la personne
propriétaire des données. Voir [Brancher Claude Code](claude-code.md) pour le
déroulé complet, exactement rejoué. Le code obtenu s'échange contre un jeton nommé
(`POST /oauth/token`).

Dans les deux cas, le résultat est le même : un jeton `pp_…`, avec les portées `read`
et/ou `write` ([détail](jetons-et-portees.md)).

## 3. Ouvrir une session MCP

`initialize`, avec le Bearer :

```
POST /mcp
Content-Type: application/json
Accept: application/json, text/event-stream
Authorization: Bearer pp_…

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"mon-agent","version":"1.0"}}}
```

La réponse est **au format SSE** (`Content-Type: text/event-stream`), même pour un seul
message — attendez-vous à `event: message\ndata: {…}\n\n`, pas à un JSON brut. Elle porte
aussi l'en-tête `Mcp-Session-Id` : **conservez-le et renvoyez-le sur chaque appel suivant.**

```
200 OK
Mcp-Session-Id: 01a0dd94-a733-790d-bb2c-0cc7dad419b3
Content-Type: text/event-stream

event: message
data: {"result":{"protocolVersion":"2025-11-25","capabilities":{"resources":{"listChanged":true,"subscribe":true},"tools":{"listChanged":true},"prompts":{"listChanged":true}},"serverInfo":{"name":"palmaplan","version":"0.27.0"}},"jsonrpc":"2.0","id":1}
```

`serverInfo.version` est la version de l'instance qui répond : un autre numéro n'est pas une
erreur. Cette capture a été prise contre un serveur en `0.15.0` ; c'est le seul champ qu'une
livraison change, et **ce champ-là est retouché à chaque clôture** plutôt que de laisser la
page annoncer une version périmée — à chaque clôture depuis `0.20.0` ; on y lit aujourd'hui
`0.27.0`. Le reste de la capture est brut. Depuis
`0.21.0`, un test le vérifie (`scripts/site-versions.test.ts`) : la garde des chiffres balaie
`docs/` aussi.

## 4. Construire un planning

`tools/call`, avec le même Bearer et le `Mcp-Session-Id` reçu à l'étape 3 :

```json
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"create_schedule","arguments":{"name":"Vitrine agents"}}}
```

```json
{"result":{"content":[{"type":"text","text":"Planning « Vitrine agents » créé.\nid: 01a0dd9c-…"}],"structuredContent":{"schedule":"01a0dd9c-…","seq":0}},"jsonrpc":"2.0","id":2}
```

Un planning neuf naît avec un groupe « Phase 1 » et trois lignes vides — **utilisable
directement par son libellé**, sans relire le planning pour retrouver son identifiant :
partout où PalmaPlan attend un identifiant, un nom convient. Pour poser une ligne et un
bloc :

```json
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"add_rows","arguments":{"schedule":"Vitrine agents","group":"Phase 1","rows":[{"periods":[{"start":"2026-10-05","duration":5,"label":"Cadrage"}]}]}}}
```

```json
{"result":{"content":[{"type":"text","text":"1 ligne ajoutée, 1 bloc.\nids: 01a0dd9c-…"}],"structuredContent":{"dry_run":false,"seq":2,"applied":[…],"effects":[…],"warnings":[]}},"jsonrpc":"2.0","id":3}
```

Pour un planning entier en un seul appel plutôt que ligne par ligne, voir `build_schedule`
dans [la référence générée](reference-tools.md#en-masse) — c'est le tool que le prompt
`plan_project` recommande.

## 5. Lire un planning

Par un tool — la vue complète, en Markdown lisible :

```json
{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"get_schedule","arguments":{"schedule":"Vitrine agents"}}}
```

Ou par une resource — la même donnée, adressable sans appel de tool :

```json
{"jsonrpc":"2.0","id":5,"method":"resources/read","params":{"uri":"palmaplan://schedule/01a0dd9c-…"}}
```

`palmaplan://schedule/{id}/json` rend la même chose en JSON normalisé, pour un traitement
exact plutôt qu'une lecture. `palmaplan://schedules` (sans identifiant) liste tous les
plannings accessibles à ce jeton.

## 6. Les absences et les risques (S22)

Une **absence** appartient à une personne, pas à un planning : posée une fois, elle vaut
dans tous les plannings dont la personne est membre, et elle **ne déplace jamais un bloc**
— elle signale un **risque**. Le motif est l'un de `conges`, `temps_partiel`, `formation`,
`autre` : **jamais un motif médical, jamais un champ libre** — le schéma les refuse (une
donnée de santé est une catégorie particulière, article 9 du RGPD, et un outil de planning
n'a besoin de savoir *que* vous êtes absent, jamais *pourquoi*).

D'abord, relier une ligne à la personne qui y travaille — `set_row_member` accepte « me »,
un nom, un e-mail ou un identifiant (`list_members` les donne) :

```json
{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"set_row_member","arguments":{"schedule":"Refonte","row":"2.1","member":"anne@x.test"}}}
```

```json
{"result":{"content":[{"type":"text","text":"ligne 2.1 reliée à Anne.\nrow: 01a0f9e5-…"}],"structuredContent":{"dry_run":false,"seq":4,"applied":[…],"row":"01a0f9e5-…","member_id":"01a0f9e5-…"}},"jsonrpc":"2.0","id":6}
```

Les absences des membres d'un planning se lisent par `list_absences` — la même visibilité
que l'Agenda de l'écran, par la même route (`GET /schedules/:id/absences`) : un étranger ou
un ancien membre n'y voit rien, même désigné par une ligne ou une action :

```json
{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"list_absences","arguments":{"schedule":"Refonte"}}}
```

```json
{"result":{"content":[{"type":"text","text":"Absences des membres de ce planning :\nAnne :\n  - Congés — le 2026-08-19\n    id: 01a0f9e5-…"}],"structuredContent":{"schedule_id":"01a0f9e5-…","members":[{"user_id":"01a0f9e5-…","name":"Xavier","role":"owner"},{"user_id":"01a0f9e5-…","name":"Anne","role":"editor"}],"absences":[{"id":"01a0f9e5-…","user_id":"01a0f9e5-…","motif":"conges","forme":{"kind":"periode","date_from":"2026-08-19","date_to":"2026-08-19","debut":"matin","fin":"apres_midi"},"created_at":"2026-07-01T00:00:00.000Z","updated_at":"2026-07-01T00:00:00.000Z"}],"linked":[{"user_id":"01a0f9e5-…","current":true,"rows":["01a0f9e5-…"],"actions":[]}]}},"jsonrpc":"2.0","id":7}
```

Sans `schedule`, `list_absences` rend les **vôtres** (`/me/absences`) ; `add_absence` en
pose une — une période (un jour, une demi-journée, ou une plage longue) ou des jours
récurrents (tous les vendredis, un vendredi sur deux…) — et `remove_absence` la retire, par
son identifiant.

Enfin, `get_schedule_risks` dit quels blocs tombent — **le même calcul, la même liste**
que l'écran Risques (`risquesDAbsence`, dans le cœur, qui ne connaît ni les comptes ni la
base) :

```json
{"jsonrpc":"2.0","id":8,"method":"tools/call","params":{"name":"get_schedule_risks","arguments":{"schedule":"Refonte"}}}
```

```json
{"result":{"content":[{"type":"text","text":"1 risque :\n- « Tests » — Anne — 1 j ouvré sur 5 (partiel), du 2026-08-19 au 2026-08-19 (ligne reliée)\n  period: 01a0f9e5-…"}],"structuredContent":{"schedule":"01a0f9e5-…","risks":[{"period_id":"01a0f9e5-…","user_id":"01a0f9e5-…","source":"ligne","jours_ouvres_absents":1,"jours_ouvres_total":5,"partiel":true,"premier_jour":"2026-08-19","dernier_jour":"2026-08-19"}],"lignes_membre_retire":[]}},"jsonrpc":"2.0","id":8}
```

Un bloc est à risque dès qu'**au moins un jour ouvré** de sa durée tombe sur l'absence de
quelqu'un qui y travaille — par la ligne reliée, ou par une action qui lui est confiée
(`add_action { assignee }`). `jours_ouvres_absents` compte en demi-journées (`0.5` pour une
absence d'une seule moitié de jour) ; `partiel` est faux seulement quand l'absence couvre
tout le bloc. Une ligne reliée à quelqu'un qui n'est plus membre du planning ressort à part,
dans `lignes_membre_retire`, sans aucune date — un lien périmé ne fuit jamais une absence.

## 7. Le kanban : ce qui m'attend (S23)

`list_my_actions` rend ce qui vous est confié — **tous plannings confondus** —, regroupé
par planning : pas besoin de connaître un identifiant de planning pour savoir ce qui vous
attend un lundi matin.

```json
{"jsonrpc":"2.0","id":9,"method":"tools/call","params":{"name":"list_my_actions","arguments":{}}}
```

```json
{"result":{"content":[{"type":"text","text":"# Mes actions\n\n1 action sur 1 planning :\n\n## Suivi agents\n- ○ Rediger le plan — Cadrage (01a0fb2a-…)\n\nas_of: 2026-10-02T05:51:20.153Z"}],"structuredContent":{"as_of":"2026-10-02T05:51:20.153Z","schedules":[{"id":"01a0fb2a-…","name":"Suivi agents","actions":[{"action":{"id":"01a0fb2a-…","period_id":"01a0fb2a-…","title":"Rediger le plan","status":"todo","assignee":{"kind":"user","user_id":"01a0fb2a-…"}},"period_label":"Cadrage","change":null}]}],"removed":[],"since_unknown":[]}},"jsonrpc":"2.0","id":9}
```

**`as_of` est la boucle « depuis ma dernière visite » : repassez-le comme `since` au
prochain appel.** Avec `since`, la même réponse porte en plus, par action, `change`
(`"new"`, `"updated"`, ou `null`), et un tableau `removed` — chaque action qui vous était
confiée et qui ne l'est plus, avec sa raison en clair (`reassigned`, `unassigned`,
`deleted`, `access_lost`), jamais une disparition muette.

L'histoire d'une action — qui, quand, et le temps passé dans chaque colonne, lus dans le
journal existant, sans stockage neuf :

```json
{"jsonrpc":"2.0","id":10,"method":"tools/call","params":{"name":"get_action_history","arguments":{"schedule":"Suivi agents","action":"Rediger le plan"}}}
```

```json
{"result":{"content":[{"type":"text","text":"# Histoire de « Rediger le plan »\n\n- 2026-10-02 05:51 · Xavier · créée\n- 2026-10-02 05:51 · Xavier · passée de à faire à en cours\n\nTemps passé : < 1 h à faire, < 1 h en cours, 0 h fait.\nActuellement en cours depuis le 2026-10-02 05:51."}],"structuredContent":{"action_id":"01a0fb2a-…","events":[{"kind":"created","who":{"name":"Xavier"}},{"kind":"status","from":"todo","to":"doing","who":{"name":"Xavier"}}],"columns":{"todo_ms":20,"doing_ms":11,"done_ms":0,"current":"doing","since":"2026-10-02T05:51:20.160Z"}}},"jsonrpc":"2.0","id":10}
```

`update_action` déplace une action vers un autre bloc avec `period` — l'avancement des
deux blocs (celui qu'elle quitte, celui qu'elle rejoint) suit, déduit de leurs actions :

```json
{"jsonrpc":"2.0","id":11,"method":"tools/call","params":{"name":"update_action","arguments":{"schedule":"Suivi agents","action":"Rediger le plan","period":"Tests"}}}
```

```json
{"result":{"content":[{"type":"text","text":"Action « Rediger le plan » modifiée — responsable : vous : « Tests » à 0 % (0/1) ; « Cadrage » n a plus d action : son avancement (0 %) se règle à nouveau à la main.\nid: 01a0fb2a-…"}],"structuredContent":{"applied":[{"type":"action.move","payload":{"id":"01a0fb2a-…","period_id":"01a0fb2a-…"}}],"progress":0}},"jsonrpc":"2.0","id":11}
```

`get_action_alerts` dit ce qui coince : une action non faite dont le bloc est terminé
(`en_retard`), une action « en cours » qui n'a pas bougé depuis `idle_days` jours
(`immobile`, sept par défaut), une action sans responsable dans un bloc déjà commencé
(`sans_responsable`), ou confiée à quelqu'un d'absent pendant son bloc (`absent`) — une
action peut porter plusieurs alertes à la fois. Sans `schedule`, toutes les vôtres, tous
les plannings que vous pouvez lire confondus ; avec, celles d'un planning entier :

```json
{"jsonrpc":"2.0","id":12,"method":"tools/call","params":{"name":"get_action_alerts","arguments":{"schedule":"Suivi agents"}}}
```

```json
{"result":{"content":[{"type":"text","text":"# Ce qui coince\n\n2 alertes :\n- ⏰ « Sans responsable » (Cadrage) — en retard de 4 j ouvrés (bloc terminé le 2026-09-28)\n- ❓ « Sans responsable » (Cadrage) — personne n’en est responsable"}],"structuredContent":{"alerts":[{"sorte":"en_retard","depuis":"2026-09-28","jours":4,"title":"Sans responsable","period_label":"Cadrage"},{"sorte":"sans_responsable","title":"Sans responsable","period_label":"Cadrage"}],"idle_unknown":[]}},"jsonrpc":"2.0","id":12}
```

`get_kanban_status` rend les colonnes, ce qui a bougé cette semaine (qui, de quoi à quoi,
quand), et les mêmes alertes — un seul appel plutôt que les reconstituer soi-même :

```json
{"jsonrpc":"2.0","id":13,"method":"tools/call","params":{"name":"get_kanban_status","arguments":{"schedule":"Suivi agents"}}}
```

```json
{"result":{"content":[{"type":"text","text":"# État du kanban\n\n○ à faire : 1 · ◐ en cours : 1 · ✓ fait : 0\n\n## Cette semaine\n- 2026-10-02 05:51 · Xavier · « Rediger le plan » : à faire → en cours\n\n## Ce qui coince\n- ⏰ « Sans responsable » (Cadrage) — en retard de 4 j ouvrés (bloc terminé le 2026-09-28)\n- ❓ « Sans responsable » (Cadrage) — personne n’en est responsable"}],"structuredContent":{"columns":{"todo":1,"doing":1,"done":0},"moved_this_week":[{"title":"Rediger le plan","from":"todo","to":"doing","who":{"name":"Xavier"}}]}},"jsonrpc":"2.0","id":13}
```

Enfin, `palmaplan://me/actions` est la même vue en resource — adressable sans appel de
tool, et abonnable (`resources/subscribe`) : à l'abonnement, le serveur veille tous les
plannings que vous pouvez lire, et envoie `resources/updated` dès que l'un d'eux voit une
de vos actions changer.

```json
{"jsonrpc":"2.0","id":14,"method":"resources/read","params":{"uri":"palmaplan://me/actions"}}
```

```json
{"result":{"contents":[{"uri":"palmaplan://me/actions","mimeType":"text/markdown","text":"# Mes actions\n\n1 action sur 1 planning :\n\n## Suivi agents\n- ◐ Rediger le plan — Tests (01a0fb2a-…)"}]},"jsonrpc":"2.0","id":14}
```

**Limite à connaître** : la liste des plannings veillés est prise **au moment de
l'abonnement**. Un planning qui vous est partagé après coup n'est suivi qu'après un
réabonnement (`resources/unsubscribe` puis `resources/subscribe` à nouveau) — ni plus tôt,
ni automatiquement.

## Ce qui suit

- [La référence complète des tools, resources et prompts](reference-tools.md), générée
  depuis ce même serveur — chaque tool, ses paramètres, obligatoires ou non.
- [Les jetons et les portées](jetons-et-portees.md) — ce que `read` et `write` permettent,
  et ce qu'aucun jeton ne permet.
- [La vue embarquée](vue-embarquee.md) — pour montrer le planning plutôt que le décrire.
