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 :
{
"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), 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 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).
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 :
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"create_schedule","arguments":{"name":"Vitrine agents"}}}
{"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 :
{"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"}]}]}}}
{"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 — c'est le tool que le prompt plan_project recommande.
5. Lire un planning
Par un tool — la vue complète, en Markdown lisible :
{"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 :
{"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) :
{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"set_row_member","arguments":{"schedule":"Refonte","row":"2.1","member":"anne@x.test"}}}
{"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 :
{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"list_absences","arguments":{"schedule":"Refonte"}}}
{"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) :
{"jsonrpc":"2.0","id":8,"method":"tools/call","params":{"name":"get_schedule_risks","arguments":{"schedule":"Refonte"}}}
{"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.
{"jsonrpc":"2.0","id":9,"method":"tools/call","params":{"name":"list_my_actions","arguments":{}}}
{"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 :
{"jsonrpc":"2.0","id":10,"method":"tools/call","params":{"name":"get_action_history","arguments":{"schedule":"Suivi agents","action":"Rediger le plan"}}}
{"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 :
{"jsonrpc":"2.0","id":11,"method":"tools/call","params":{"name":"update_action","arguments":{"schedule":"Suivi agents","action":"Rediger le plan","period":"Tests"}}}
{"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 :
{"jsonrpc":"2.0","id":12,"method":"tools/call","params":{"name":"get_action_alerts","arguments":{"schedule":"Suivi agents"}}}
{"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 :
{"jsonrpc":"2.0","id":13,"method":"tools/call","params":{"name":"get_kanban_status","arguments":{"schedule":"Suivi agents"}}}
{"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.
{"jsonrpc":"2.0","id":14,"method":"resources/read","params":{"uri":"palmaplan://me/actions"}}
{"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, générée depuis ce même serveur — chaque tool, ses paramètres, obligatoires ou non.
- Les jetons et les portées — ce que
readetwritepermettent, et ce qu'aucun jeton ne permet. - La vue embarquée — pour montrer le planning plutôt que le décrire.