Aller au contenu
getnextpdf.com

Piloter une session documentaire d’agent via MCP

Voici une session d’agent complète sur le serveur Model Context Protocol (MCP) de NextPDF Connect, message par message : initialize, tools/list, six invocations de tools/call qui construisent une note de projet d’une page, et l’aller-retour avec supervision humaine (HITL) qui conditionne l’écriture finale du fichier. Chaque message JSON-RPC ci-dessous a été capturé mot pour mot depuis un processus bin/nextpdf-mcp en fonctionnement (outils de niveau Core uniquement), puis expurgé de deux manières précises : le jeton de confirmation à usage unique est affiché sous la forme confirm_<single-use-hex>, et le répertoire temporaire système de la machine est raccourci en C:\Temp. Les identifiants, schémas, positions et nombres d’octets correspondent exactement à ce que le serveur a envoyé.

Fenêtre de terminal
composer require nextpdf/server

Relie le transport stdio à ton hôte MCP — pour Claude Desktop (les hôtes lancent la commande depuis leur propre répertoire, il faut donc utiliser un chemin absolu ; le transport stdio ne nécessite aucune clé d’API, contrairement au transport REST) :

{
"mcpServers": {
"nextpdf": {
"command": "php",
"args": ["/absolute/path/to/your/project/vendor/bin/nextpdf-mcp"]
}
}
}

Le serveur parle le JSON-RPC 2.0 délimité par des sauts de ligne sur stdin/stdout et sépare strictement la sortie du protocole des diagnostics : les lignes de démarrage et d’audit vont vers stderr, jamais vers stdout.

Une session de document MCP conserve un état. create_pdf ouvre un document dans le magasin en mémoire du serveur et renvoie un document_id ; chaque appel ultérieur cible cet identifiant. Les outils de contenu (set_font, add_text, add_table) s’exécutent immédiatement au niveau de risque Prudence avec journalisation d’audit ; preview_layout est une lecture de niveau Sûr ; et output_pdf avec un file_path relève d’Approbation requise — il ne s’exécute pas au premier appel. À la place, le serveur renvoie un défi assorti d’un jeton à usage unique, l’agent transmet le défi à l’humain, et seul un rappel portant _confirmation_token exécute l’écriture. Les documents laissés dans le magasin expirent après la durée de vie configurée (30 minutes par défaut).

Les mêmes appels d’outils pilotent le moteur d’outils via REST et gRPC — les transports partagent un seul exécuteur — de sorte que tout ce qui figure ici, hormis le tramage stdio, reste valable. Voir Générer une facture de bout en bout via REST pour le même moteur sur la surface HTTP.

OutilRôle dans cette sessionNiveau de risque
create_pdfOuvrir le document, obtenir le document_idPrudence
set_fontChoisir la police du titre, puis celle du corpsPrudence
add_textLigne de titre, puis paragraphe d’introductionPrudence
add_tableTableau de la liste de contrôle (responsable/échéance)Prudence
preview_layoutLire l’état de la mise en page avant la sortieSûr
output_pdf (mode fichier)Écrire le PDF — soumis à la barrièreApprobation requise

Le déploiement capturé ici a enregistré 20 outils (13 Core, 6 Pro, 1 Enterprise — ces nombres apparaissent dans la réponse initialize ci-dessous) ; cette session utilise uniquement des outils Core, elle fonctionne donc sans modification sur une installation purement open source. Le catalogue faisant foi est la réponse tools/list de ton propre serveur, et l’échelle de risque est définie dans la référence des niveaux de risque HITL.

Le client ouvre la session et indique sa version de protocole :

{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-06-18",
"capabilities": {},
"clientInfo": {
"name": "planning-agent",
"version": "1.0.0"
}
}
}

Le serveur confirme la version de protocole et déclare ses capacités, y compris le nombre d’outils par niveau et l’activation de la barrière HITL :

{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-06-18",
"capabilities": {
"tools": {
"listChanged": false
},
"nextpdf": {
"tiers": {
"core": 13,
"pro": 6,
"enterprise": 1
},
"tool_count": 20,
"risk_model_version": 1,
"hitl_enabled": true
}
},
"serverInfo": {
"name": "NextPDF Connect",
"version": "1.0.0"
}
}
}

Le client accuse réception par une notification (les notifications ne portent pas d’id et ne reçoivent aucune réponse) :

{
"jsonrpc": "2.0",
"method": "notifications/initialized"
}
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list"
}

La réponse complète liste les 20 outils enregistrés avec leurs schémas d’entrée intégraux. Elle est présentée ici réduite aux deux outils qui ouvrent et ferment cette session — les 18 entrées omises ont la même forme :

{
"jsonrpc": "2.0",
"id": 2,
"result": {
"tools": [
{
"name": "create_pdf",
"description": "Create a new PDF document and return a document_id for subsequent operations",
"inputSchema": {
"type": "object",
"properties": {
"page_size": {
"type": "string",
"description": "Page size name (e.g. \"A4\", \"Letter\", \"Legal\", \"A3\")",
"default": "A4"
},
"orientation": {
"type": "string",
"enum": [
"portrait",
"landscape"
],
"description": "Page orientation",
"default": "portrait"
},
"title": {
"type": "string",
"description": "Document title metadata"
},
"author": {
"type": "string",
"description": "Document author metadata"
}
},
"required": []
},
"annotations": {
"destructiveHint": false,
"idempotentHint": false
}
},
{
"name": "output_pdf",
"description": "Finalize the PDF and output to file or return as base64",
"inputSchema": {
"type": "object",
"properties": {
"document_id": {
"type": "string",
"description": "The document_id returned by create_pdf"
},
"file_path": {
"type": "string",
"description": "Absolute file path to save the PDF. If omitted, returns base64-encoded PDF data."
},
"destroy": {
"type": "boolean",
"description": "Whether to remove the document from the store after output",
"default": true
}
},
"required": [
"document_id"
]
},
"annotations": {
"destructiveHint": false,
"openWorldHint": true
}
}
]
}
}

Remarque le schéma d’output_pdf : file_path est facultatif, et les annotations portent openWorldHint: true — l’outil peut agir sur le monde extérieur à la session, ce qui explique précisément pourquoi le mode fichier est soumis à la barrière.

{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "create_pdf",
"arguments": {
"page_size": "A4",
"orientation": "portrait",
"title": "Project kickoff brief",
"author": "Planning agent"
}
}
}
{
"jsonrpc": "2.0",
"id": 3,
"result": {
"content": [
{
"type": "text",
"text": "{\"document_id\":\"doc_3b9f435efa0f32d1da7a131d\",\"page_count\":1,\"page_size\":\"A4\",\"orientation\":\"portrait\"}"
}
],
"structuredContent": {
"document_id": "doc_3b9f435efa0f32d1da7a131d",
"page_count": 1,
"page_size": "A4",
"orientation": "portrait"
}
}
}

Chaque résultat d’outil arrive en double dans un même message : un bloc de texte content lisible par un humain, et un structuredContent lisible par une machine. Lis structuredContent.document_id et réutilise-le dans chaque appel suivant.

Définis une police grasse de 16 points, puis place le titre :

{
"jsonrpc": "2.0",
"id": 4,
"method": "tools/call",
"params": {
"name": "set_font",
"arguments": {
"document_id": "doc_3b9f435efa0f32d1da7a131d",
"family": "helvetica",
"style": "B",
"size": 16
}
}
}
{
"jsonrpc": "2.0",
"id": 4,
"result": {
"content": [
{
"type": "text",
"text": "Font set to helvetica B 16pt on document doc_3b9f435efa0f32d1da7a131d."
}
]
}
}
{
"jsonrpc": "2.0",
"id": 5,
"method": "tools/call",
"params": {
"name": "add_text",
"arguments": {
"document_id": "doc_3b9f435efa0f32d1da7a131d",
"text": "Project kickoff brief"
}
}
}
{
"jsonrpc": "2.0",
"id": 5,
"result": {
"content": [
{
"type": "text",
"text": "{\"document_id\":\"doc_3b9f435efa0f32d1da7a131d\",\"position\":{\"x\":10,\"y\":16,\"page\":0}}"
}
],
"structuredContent": {
"document_id": "doc_3b9f435efa0f32d1da7a131d",
"position": {
"x": 10,
"y": 16,
"page": 0
}
}
}
}

Retour à une police normale de 11 points pour le texte d’introduction ; width: 0 sélectionne une disposition multicellulaire pleine largeur :

{
"jsonrpc": "2.0",
"id": 6,
"method": "tools/call",
"params": {
"name": "set_font",
"arguments": {
"document_id": "doc_3b9f435efa0f32d1da7a131d",
"family": "helvetica",
"style": "",
"size": 11
}
}
}
{
"jsonrpc": "2.0",
"id": 6,
"result": {
"content": [
{
"type": "text",
"text": "Font set to helvetica 11pt on document doc_3b9f435efa0f32d1da7a131d."
}
]
}
}
{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "add_text",
"arguments": {
"document_id": "doc_3b9f435efa0f32d1da7a131d",
"text": "Prepared by the planning agent for the 14 July kickoff. Scope, owners, and the first-week checklist are tabled below.",
"width": 0,
"line_height": 6
}
}
}
{
"jsonrpc": "2.0",
"id": 7,
"result": {
"content": [
{
"type": "text",
"text": "{\"document_id\":\"doc_3b9f435efa0f32d1da7a131d\",\"position\":{\"x\":10,\"y\":29.75,\"page\":0}}"
}
],
"structuredContent": {
"document_id": "doc_3b9f435efa0f32d1da7a131d",
"position": {
"x": 10,
"y": 29.75,
"page": 0
}
}
}
}
{
"jsonrpc": "2.0",
"id": 8,
"method": "tools/call",
"params": {
"name": "add_table",
"arguments": {
"document_id": "doc_3b9f435efa0f32d1da7a131d",
"html": "<table><tr><th>Work item</th><th>Owner</th><th>Due</th></tr><tr><td>Repository bootstrap</td><td>Devon</td><td>2026-07-15</td></tr><tr><td>CI pipeline</td><td>Ana</td><td>2026-07-17</td></tr><tr><td>Staging deploy</td><td>Priya</td><td>2026-07-21</td></tr></table>"
}
}
}
{
"jsonrpc": "2.0",
"id": 8,
"result": {
"content": [
{
"type": "text",
"text": "{\"document_id\":\"doc_3b9f435efa0f32d1da7a131d\",\"position\":{\"x\":10,\"y\":84.75,\"page\":0}}"
}
],
"structuredContent": {
"document_id": "doc_3b9f435efa0f32d1da7a131d",
"position": {
"x": 10,
"y": 84.75,
"page": 0
}
}
}
}

Chaque appel de contenu renvoie la position actualisée du curseur, de sorte que l’agent sait toujours où atterrira l’élément suivant.

7. Prévisualiser avant de demander l’approbation

Section intitulée « 7. Prévisualiser avant de demander l’approbation »

preview_layout est un appel Sûr, en lecture seule — un agent consciencieux vérifie ce qu’il a construit avant de demander à un humain d’approuver une écriture :

{
"jsonrpc": "2.0",
"id": 9,
"method": "tools/call",
"params": {
"name": "preview_layout",
"arguments": {
"document_id": "doc_3b9f435efa0f32d1da7a131d"
}
}
}
{
"jsonrpc": "2.0",
"id": 9,
"result": {
"content": [
{
"type": "text",
"text": "{\"document_id\":\"doc_3b9f435efa0f32d1da7a131d\",\"total_pages\":1,\"current_page\":0,\"page_dimensions\":{\"width\":595.276,\"height\":841.89},\"margins\":{\"top\":10,\"right\":10,\"bottom\":10,\"left\":10},\"cursor_position\":{\"x\":10,\"y\":84.75}}"
}
],
"structuredContent": {
"document_id": "doc_3b9f435efa0f32d1da7a131d",
"total_pages": 1,
"current_page": 0,
"page_dimensions": {
"width": 595.276,
"height": 841.89
},
"margins": {
"top": 10,
"right": 10,
"bottom": 10,
"left": 10
},
"cursor_position": {
"x": 10,
"y": 84.75
}
}
}
}

8. Demander l’écriture du fichier — la barrière répond en premier

Section intitulée « 8. Demander l’écriture du fichier — la barrière répond en premier »

L’agent demande à output_pdf d’écrire la note finalisée sur le disque, en gardant le document actif (destroy: false) au cas où l’humain refuserait et où il faudrait se rabattre sur une sortie en base64 :

{
"jsonrpc": "2.0",
"id": 10,
"method": "tools/call",
"params": {
"name": "output_pdf",
"arguments": {
"document_id": "doc_3b9f435efa0f32d1da7a131d",
"file_path": "C:\\Temp\\nextpdf-mcp\\kickoff-brief.pdf",
"destroy": false
}
}
}

Le fichier n’est pas écrit. Comme le mode fichier relève d’Approbation requise, le serveur répond plutôt par un défi de confirmation :

{
"jsonrpc": "2.0",
"id": 10,
"result": {
"content": [
{
"type": "text",
"text": "⚠️ CONFIRMATION REQUIRED\n\nOperation: output_pdf\nDescription: Finalize the PDF and output to file or return as base64\n\nTo proceed, call output_pdf again with parameter _confirmation_token: \"confirm_<single-use-hex>\"\nExpires in 300 seconds."
}
],
"isError": false
}
}

L’agent transmet le texte du défi à l’humain. Après approbation, il rappelle output_pdf avec les mêmes arguments, plus _confirmation_token :

{
"jsonrpc": "2.0",
"id": 11,
"method": "tools/call",
"params": {
"name": "output_pdf",
"arguments": {
"document_id": "doc_3b9f435efa0f32d1da7a131d",
"file_path": "C:\\Temp\\nextpdf-mcp\\kickoff-brief.pdf",
"destroy": false,
"_confirmation_token": "confirm_<single-use-hex>"
}
}
}

Le jeton est consommé, l’écriture s’exécute, et le résultat rend compte du fichier écrit :

{
"jsonrpc": "2.0",
"id": 11,
"result": {
"content": [
{
"type": "text",
"text": "{\"document_id\":\"doc_3b9f435efa0f32d1da7a131d\",\"file_path\":\"C:\\\\Temp\\\\nextpdf-mcp\\\\kickoff-brief.pdf\",\"file_size\":3612,\"page_count\":1,\"destroyed\":false}"
}
],
"structuredContent": {
"document_id": "doc_3b9f435efa0f32d1da7a131d",
"file_path": "C:\\Temp\\nextpdf-mcp\\kickoff-brief.pdf",
"file_size": 3612,
"page_count": 1,
"destroyed": false
}
}
}

La session a écrit kickoff-brief.pdf (3 612 octets, une page, correspondant à structuredContent.file_size et page_count). Sortie capturée de qpdf --check pour ce fichier précis :

checking kickoff-brief.pdf
PDF Version: 2.0
File is not encrypted
File is not linearized
No syntax or stream encoding errors found; the file may still contain
errors that qpdf cannot detect

Il s’agit d’une vérification structurelle, selon les propres termes de qpdf — pas d’un verdict de conformité.

  • Le rappel doit répéter les mêmes arguments. Le jeton de confirmation est lié au nom de l’outil ainsi qu’à un condensé canonique des arguments pour lesquels il a été émis. Un rappel avec le moindre changement — même en inversant destroy — ne consomme pas le jeton ; le serveur répond plutôt par un nouveau défi. Répète exactement les arguments et ajoute uniquement _confirmation_token.
  • Le jeton est à usage unique et expire. Le défi indique le délai d’expiration (300 secondes). Après expiration ou consommation, le prochain appel soumis à la barrière reçoit un nouveau défi ; transmets ce dernier.
  • La sortie de fichier atterrit dans un répertoire de la liste d’autorisation. Le serveur rejette tout file_path situé hors de son répertoire temporaire configuré, en renvoyant Output path rejected by security policy. La racine par défaut de la liste d’autorisation est nextpdf-mcp sous le répertoire temporaire système ; les opérateurs la modifient via le paramètre temp_dir dans nextpdf-mcp.yaml.
  • Le mode base64 n’est pas soumis à la barrière. output_pdf sans file_path renvoie le PDF en base64 au niveau Revue, sans effet de bord sur le système de fichiers — voir Exiger une approbation humaine pour la sortie de fichiers pour cette frontière en détail.
  • Un défi est un résultat, pas une erreur. Le message de défi arrive avec isError: false ; une approbation en attente est une pause du flux de travail. Ne réessaie pas en boucle, et ne fabrique jamais de jeton.
  • Les notifications ne reçoivent aucune réponse. Après notifications/initialized, ne bloque pas en attendant une ligne de réponse.

La session est intégralement en mémoire : les appels de contenu ont répondu en quelques millisecondes lors de l’exécution capturée, et la durée totale est dominée par l’aller-retour d’approbation humaine, ce qui est tout l’intérêt de la barrière. Le magasin de documents conserve une session pendant 30 minutes d’inactivité par défaut (50 documents au maximum), de sorte qu’une approbation lente ne perd pas le document construit — mais un document abandonné est récupéré.

  • Traite le jeton de confirmation comme un secret à usage unique. Transmets le texte du défi à l’humain ; ne journalise pas le jeton et ne le conserve pas. Cette page expurge le jeton capturé précisément pour cette raison.
  • La piste d’audit est sur stderr. Chaque exécution de niveau Prudence ou supérieur est journalisée pour l’audit (outil, risque, arguments, résultat) via PSR-3, avec les paramètres sensibles expurgés. Les diagnostics ne se mêlent jamais au flux du protocole.
  • La liste d’autorisation de chemins est la frontière du système de fichiers. Fais pointer temp_dir vers un répertoire dédié à la sortie de Connect ; ne l’élargis pas à un emplacement à usage général.
  • Les niveaux de risque ne peuvent qu’augmenter. Une substitution d’opérateur dans nextpdf-mcp.yaml peut relever le niveau de risque d’un outil mais ne peut jamais abaisser output_pdf en dessous d’Approbation requise.

Cette recette ne formule aucune revendication normative de conformité à une norme. Elle documente le transport stdio de MCP (JSON-RPC 2.0, version de protocole 2025-06-18 telle que négociée dans l’échange initialize capturé) ainsi que le contrat de risque et de confirmation du serveur. L’étape qpdf --check ci-dessus confirme uniquement l’intégrité structurelle du fichier écrit ; la conformité à une norme est déterminée par un validateur indépendant, et non affirmée par le logiciel producteur.