Ir al contenido
getnextpdf.com

Dirigir una sesión de documento de agente por MCP

Esta es una sesión de agente completa contra el servidor Model Context Protocol (MCP) de NextPDF Connect, mensaje a mensaje: initialize, tools/list, seis invocaciones de tools/call que construyen un informe de proyecto de una página y el ciclo de ida y vuelta con intervención humana (HITL) que controla la escritura final del archivo. Cada mensaje JSON-RPC que aparece a continuación se capturó literalmente de un proceso bin/nextpdf-mcp en vivo (solo herramientas de nivel Core) y, después, se saneó exactamente de dos maneras: el token de confirmación de un solo uso se muestra como confirm_<single-use-hex>, y el directorio temporal del sistema de la máquina se abrevia a C:\Temp. Los identificadores, los esquemas, las posiciones y los recuentos de bytes son exactamente lo que envió el servidor.

Ventana de terminal
composer require nextpdf/server

Enlazar el transporte stdio en el host MCP; para Claude Desktop (los hosts lanzan el comando desde su propio directorio, así que conviene usar una ruta absoluta; el transporte stdio no necesita clave de API, a diferencia del transporte REST):

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

El servidor habla JSON-RPC 2.0 delimitado por saltos de línea en stdin/stdout y mantiene la salida del protocolo estrictamente separada de los diagnósticos: las líneas de arranque y de auditoría van a stderr, nunca a stdout.

Una sesión de documento MCP mantiene estado. create_pdf abre un documento en el almacén en memoria del servidor y devuelve un document_id; cada llamada posterior apunta a ese identificador. Las herramientas de contenido (set_font, add_text, add_table) se ejecutan de inmediato en el nivel de riesgo Precaución con registro de auditoría; preview_layout es una lectura de nivel Seguro; y output_pdf con un file_path es Aprobación requerida: no se ejecuta en la primera llamada. En su lugar, el servidor devuelve un desafío con un token de un solo uso, el agente transmite el desafío a la persona y solo una nueva llamada que lleve _confirmation_token ejecuta la escritura. Los documentos que quedan en el almacén caducan tras el tiempo de vida configurado (30 minutos de forma predeterminada).

Las mismas llamadas de herramientas impulsan el motor de herramientas sobre REST y gRPC (los transportes comparten un único ejecutor), así que todo lo que hay aquí, salvo el encuadre de stdio, se traslada igual. Véase Renderizar una factura de principio a fin por REST para el mismo motor en la superficie HTTP.

HerramientaFunción en esta sesiónNivel de riesgo
create_pdfAbrir el documento, obtener document_idPrecaución
set_fontSeleccionar la fuente de encabezado y luego la del cuerpoPrecaución
add_textLínea de título y luego párrafo de introducciónPrecaución
add_tableTabla de tareas con responsable y fecha de vencimientoPrecaución
preview_layoutLeer el estado del diseño antes de la salidaSeguro
output_pdf (modo de archivo)Escribir el PDF — con puertaAprobación requerida

El despliegue capturado aquí registró 20 herramientas (13 de Core, 6 de Pro, 1 de Enterprise; los recuentos aparecen en la respuesta de initialize que se muestra más abajo); esta sesión usa únicamente herramientas de Core, por lo que se ejecuta sin cambios en una instalación exclusivamente de código abierto. El catálogo de referencia es la respuesta tools/list de su propio servidor, y la escala de riesgo se define en la referencia de niveles de riesgo HITL.

El cliente abre la sesión e indica su versión de protocolo:

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

El servidor confirma la versión de protocolo y declara sus capacidades, incluidos los recuentos de herramientas por nivel y que el control HITL está habilitado:

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

El cliente confirma con una notificación (las notificaciones no llevan id ni reciben respuesta):

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

La respuesta completa enumera las 20 herramientas registradas con sus esquemas de entrada completos. Aquí se muestra abreviada a las dos herramientas que abren y cierran esta sesión; las 18 entradas omitidas tienen la misma 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
}
}
]
}
}

Observar el esquema de output_pdf: file_path es opcional, y las anotaciones llevan openWorldHint: true: la herramienta puede afectar al mundo fuera de la sesión, que es precisamente por lo que el modo de archivo tiene puerta.

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

Cada resultado de herramienta llega dos veces en un mismo mensaje: un bloque de texto content legible por personas y structuredContent legible por máquinas. Leer structuredContent.document_id y pasarlo por cada una de las llamadas siguientes.

Establecer una fuente en negrita de 16 puntos y luego colocar el título:

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

De vuelta a una fuente normal de 11 puntos para el texto de introducción; width: 0 selecciona un diseño multicelda de ancho completo:

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

Cada llamada de contenido devuelve la position actualizada del cursor, de modo que el agente siempre sabe dónde cae el siguiente elemento.

7. Previsualizar antes de solicitar aprobación

Sección titulada «7. Previsualizar antes de solicitar aprobación»

preview_layout es una llamada de nivel Seguro, de solo lectura: un agente que se comporta correctamente comprueba lo que ha construido antes de pedir a una persona que apruebe una escritura:

{
"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. Solicitar la escritura del archivo: la puerta responde primero

Sección titulada «8. Solicitar la escritura del archivo: la puerta responde primero»

El agente pide a output_pdf que escriba el informe terminado en disco, manteniendo vivo el documento (destroy: false) por si la persona lo rechaza y hay que recurrir a la salida 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
}
}
}

El archivo no se escribe. Como el modo de archivo es Aprobación requerida, el servidor responde en su lugar con un desafío de confirmación:

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

9. La persona aprueba: nueva llamada con el token

Sección titulada «9. La persona aprueba: nueva llamada con el token»

El agente transmite el texto del desafío a la persona. Tras la aprobación, llama de nuevo a output_pdf con los mismos argumentos más _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>"
}
}
}

El token se consume, la escritura se ejecuta y el resultado informa del archivo escrito:

{
"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 sesión escribió kickoff-brief.pdf (3612 bytes, una página, coincidiendo con structuredContent.file_size y page_count). Salida capturada de qpdf --check para ese archivo exacto:

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

Se trata de una comprobación estructural, en palabras del propio qpdf, no de una determinación de conformidad.

  • La nueva llamada debe repetir los mismos argumentos. El token de confirmación está vinculado al nombre de la herramienta más un resumen canónico de los argumentos para los que se emitió. Volver a llamar con cualquier cambio —incluso invirtiendo destroy— no consume el token; el servidor responde en su lugar con un desafío nuevo. Repetir los argumentos exactamente y añadir únicamente _confirmation_token.
  • El token es de un solo uso y caduca. El desafío indica el vencimiento (300 segundos). Tras la caducidad o el consumo, la siguiente llamada con puerta obtiene un nuevo desafío; transmitir el nuevo.
  • La salida de archivos cae dentro de un directorio en lista de permitidos. El servidor rechaza un file_path fuera de su directorio temporal configurado con Output path rejected by security policy. La raíz de la lista de permitidos predeterminada es nextpdf-mcp bajo el directorio temporal del sistema; los operadores la cambian con el ajuste temp_dir en nextpdf-mcp.yaml.
  • El modo base64 no tiene puerta. output_pdf sin file_path devuelve el PDF como base64 en el nivel Revisión, sin efecto secundario en el sistema de archivos; véase Exigir aprobación humana para la salida de archivos para conocer en profundidad ese límite.
  • Un desafío es un resultado, no un error. El mensaje de desafío llega con isError: false; una aprobación pendiente es una pausa del flujo de trabajo. No reintentar en un bucle y nunca fabricar un token.
  • Las notificaciones no obtienen respuesta. Tras notifications/initialized, no bloquear esperando una línea de respuesta.

La sesión es en memoria de principio a fin: en la ejecución capturada, las llamadas de contenido respondieron en milisegundos, y el tiempo total está dominado por el ciclo de ida y vuelta de la aprobación humana, que es el propósito de la puerta. El almacén de documentos conserva una sesión durante 30 minutos de inactividad de forma predeterminada (50 documentos como máximo), de modo que una aprobación lenta no pierde el documento construido, pero uno abandonado se recupera.

  • Tratar el token de confirmación como un secreto de un solo uso. Transmitir el texto del desafío a la persona; no registrar ni conservar el token. Esta página censura el token capturado precisamente por ese motivo.
  • El rastro de auditoría está en stderr. Cada ejecución de nivel Precaución o superior se registra en auditoría (herramienta, riesgo, argumentos, resultado) mediante PSR-3, con los parámetros sensibles censurados. Los diagnósticos nunca se mezclan en el flujo del protocolo.
  • La lista de permitidos de rutas es el límite del sistema de archivos. Apuntar temp_dir a un directorio dedicado a la salida de Connect; no ampliarlo a una ubicación de uso general.
  • Los niveles de riesgo solo pueden subir. Una anulación por parte del operador en nextpdf-mcp.yaml puede elevar el nivel de riesgo de una herramienta, pero nunca puede bajar output_pdf por debajo de Aprobación requerida.

Esta receta no formula ninguna afirmación normativa sobre estándares. Documenta el transporte stdio de MCP (JSON-RPC 2.0, versión de protocolo 2025-06-18 según se negoció en el intercambio initialize capturado) y el contrato de riesgo y confirmación del servidor. El paso qpdf --check anterior confirma únicamente la integridad estructural del archivo escrito; la conformidad con un estándar la determina un validador independiente, no la afirma el software productor.