Salta ai contenuti
getnextpdf.com

Gestire una sessione documentale di un agente tramite MCP

Questa è una sessione completa di un agente con il server Model Context Protocol (MCP) di NextPDF Connect, messaggio per messaggio: initialize, tools/list, sei invocazioni tools/call che costruiscono un brief di progetto di una pagina e il round trip con intervento umano (HITL) che sottopone al gate la scrittura finale del file. Ogni messaggio JSON-RPC riportato di seguito è stato catturato alla lettera da un processo bin/nextpdf-mcp reale (soli strumenti di livello core), quindi sanificato in esattamente due modi: il token di conferma monouso è mostrato come confirm_<single-use-hex> e la directory temporanea di sistema della macchina è abbreviata in C:\Temp. Identificatori, schemi, posizioni e conteggi di byte sono esattamente quelli inviati dal server.

Terminal window
composer require nextpdf/server

Configurare il trasporto stdio nel proprio host MCP — per Claude Desktop (gli host lanciano il comando dalla propria directory, quindi usare un percorso assoluto; il trasporto stdio non richiede alcuna API key, a differenza del trasporto REST):

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

Il server parla JSON-RPC 2.0 delimitato da newline su stdin/stdout e mantiene l’output di protocollo rigorosamente separato dalla diagnostica: le righe di avvio e di audit vanno su stderr, mai su stdout.

Una sessione documentale MCP è stateful. create_pdf apre un documento nello store in memoria del server e restituisce un document_id; ogni chiamata successiva punta a quell’identificatore. Gli strumenti di contenuto (set_font, add_text, add_table) vengono eseguiti immediatamente al livello di rischio Cautela con registrazione di audit; preview_layout è una lettura Sicura; e output_pdf con un file_path è Approvazione richiesta — non viene eseguito alla prima chiamata. Il server restituisce invece una sfida con un token monouso, l’agente inoltra la sfida alla persona e solo un nuovo richiamo che trasporta _confirmation_token esegue la scrittura. I documenti lasciati nello store scadono dopo il time to live configurato (30 minuti per impostazione predefinita).

Le stesse chiamate agli strumenti guidano il motore degli strumenti su REST e gRPC — i trasporti condividono un unico executor — quindi tutto quanto riportato qui, tranne il framing stdio, vale anche per essi. Vedere Eseguire il rendering di una fattura end-to-end tramite REST per lo stesso motore sulla superficie HTTP.

StrumentoRuolo in questa sessioneLivello di rischio
create_pdfApre il documento, ottiene document_idCautela
set_fontSeleziona il carattere dell’intestazione, poi del corpoCautela
add_textRiga del titolo, poi paragrafo introduttivoCautela
add_tableTabella checklist responsabile/scadenzaCautela
preview_layoutLegge lo stato del layout prima dell’outputSicuro
output_pdf (modalità file)Scrive il PDF — con gateApprovazione richiesta

Il deployment catturato qui ha registrato 20 strumenti (13 core, 6 Pro, 1 Enterprise — i conteggi compaiono nella risposta initialize più avanti); questa sessione usa solo strumenti core, quindi viene eseguita senza modifiche su un’installazione esclusivamente open source. Il catalogo di riferimento è la risposta tools/list del proprio server e la scala dei rischi è definita nel riferimento ai livelli di rischio HITL.

Il client apre la sessione e dichiara la propria versione di protocollo:

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

Il server conferma la versione di protocollo e dichiara le proprie capacità, inclusi i conteggi degli strumenti per livello e il fatto che il gating HITL è abilitato:

{
"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"
}
}
}

Il client conferma con una notifica (le notifiche non trasportano alcun id e non ricevono risposta):

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

La risposta completa elenca tutti e 20 gli strumenti registrati con i loro schemi di input completi. Qui è mostrata abbreviata ai due strumenti che aprono e chiudono questa sessione — le 18 voci omesse hanno la stessa forma:

{
"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
}
}
]
}
}

Si noti lo schema di output_pdf: file_path è opzionale e le annotazioni portano openWorldHint: true — lo strumento può agire sul mondo esterno alla sessione, che è esattamente il motivo per cui la modalità file è sottoposta al gate.

{
"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"
}
}
}

Ogni risultato di uno strumento arriva due volte in un unico messaggio: un blocco di testo content leggibile dall’uomo e structuredContent leggibile dalla macchina. Leggere structuredContent.document_id e propagarlo attraverso ogni chiamata successiva.

Impostare un carattere in grassetto da 16 punti, quindi collocare il titolo:

{
"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
}
}
}
}

Tornare a un carattere normale da 11 punti per il testo introduttivo; width: 0 seleziona un layout multi-cella a piena larghezza:

{
"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
}
}
}
}

Ogni chiamata di contenuto restituisce la position aggiornata del cursore, così l’agente sa sempre dove verrà collocato l’elemento successivo.

preview_layout è una chiamata Sicura e di sola lettura — un agente che si comporta correttamente verifica ciò che ha costruito prima di chiedere a una persona di approvare una scrittura:

{
"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. Richiedere la scrittura del file — il gate risponde per primo

Sezione intitolata “8. Richiedere la scrittura del file — il gate risponde per primo”

L’agente chiede a output_pdf di scrivere su disco il brief completato, mantenendo il documento in vita (destroy: false) nel caso in cui la persona rifiuti e sia necessario ripiegare sull’output in 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
}
}
}

Il file non viene scritto. Poiché la modalità file è Approvazione richiesta, il server risponde invece con una sfida di conferma:

{
"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’agente inoltra il testo della sfida alla persona. In caso di approvazione, chiama nuovamente output_pdf con gli stessi argomenti più _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>"
}
}
}

Il token viene consumato, la scrittura viene eseguita e il risultato riporta il file scritto:

{
"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 sessione ha scritto kickoff-brief.pdf (3.612 byte, una pagina, corrispondenti a structuredContent.file_size e page_count). Output di qpdf --check catturato per quel file esatto:

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

Si tratta di un controllo strutturale, per usare le parole di qpdf — non di una determinazione di conformità.

  • Il nuovo richiamo deve ripetere gli stessi argomenti. Il token di conferma è associato al nome dello strumento più un digest canonico degli argomenti per cui è stato emesso. Richiamare con qualcosa di modificato — anche solo invertendo destroy — non consuma il token; il server risponde invece con una nuova sfida. Ripetere esattamente gli argomenti e aggiungere solo _confirmation_token.
  • Il token è monouso e scade. La sfida indica la scadenza (300 secondi). Dopo la scadenza o il consumo, la successiva chiamata sottoposta al gate riceve una nuova sfida; inoltrare la nuova.
  • L’output dei file finisce all’interno di una directory in allow-list. Il server rifiuta un file_path esterno alla directory temporanea configurata con Output path rejected by security policy. La radice di allow-list predefinita è nextpdf-mcp sotto la directory temporanea di sistema; gli operatori la modificano con l’impostazione temp_dir in nextpdf-mcp.yaml.
  • La modalità base64 non è sottoposta al gate. output_pdf senza file_path restituisce il PDF come base64 al livello Revisione, senza alcun effetto collaterale sul filesystem — vedere Richiedere l’approvazione umana per l’output dei file per quel confine in dettaglio.
  • Una sfida è un risultato, non un errore. Il messaggio di sfida arriva con isError: false; un’approvazione in sospeso è una pausa nel flusso di lavoro. Non riprovare in ciclo e non fabbricare mai un token.
  • Le notifiche non ricevono risposta. Dopo notifications/initialized, non bloccarsi in attesa di una riga di risposta.

La sessione si svolge interamente in memoria: le chiamate di contenuto hanno risposto in millisecondi nell’esecuzione catturata e il tempo complessivo è dominato dal round trip di approvazione umana, che è lo scopo del gate. Lo store dei documenti mantiene una sessione per 30 minuti di inattività per impostazione predefinita (massimo 50 documenti), quindi un’approvazione lenta non perde il documento costruito — ma uno abbandonato viene rimosso.

  • Trattare il token di conferma come un segreto monouso. Inoltrare il testo della sfida alla persona; non registrare il token nei log né conservarlo. Questa pagina oscura il token catturato esattamente per questo motivo.
  • La traccia di audit è su stderr. Ogni esecuzione di livello Cautela o superiore viene registrata nell’audit (strumento, rischio, argomenti, esito) tramite PSR-3, con i parametri sensibili oscurati. La diagnostica non si mescola mai nel flusso di protocollo.
  • L’allow-list dei percorsi è il confine del filesystem. Puntare temp_dir a una directory dedicata all’output di Connect; non ampliarla a una posizione generica.
  • I livelli di rischio possono solo aumentare. Un override dell’operatore in nextpdf-mcp.yaml può innalzare il livello di rischio di uno strumento ma non può mai abbassare output_pdf sotto Approvazione richiesta.

Questa ricetta non formula alcuna dichiarazione normativa sugli standard. Documenta il trasporto stdio di MCP (JSON-RPC 2.0, versione di protocollo 2025-06-18 come negoziata nello scambio initialize catturato) e il contratto di rischio e conferma del server. Il passaggio qpdf --check qui sopra conferma solo l’integrità strutturale del file scritto; la conformità a uno standard è determinata da un validatore indipendente, non asserita dal software che lo produce.