Ga naar inhoud
getnextpdf.com

Een agentdocumentsessie aansturen via MCP

Dit is één complete agentsessie tegen de NextPDF Connect Model Context Protocol (MCP)-server, bericht voor bericht: initialize, tools/list, zes tools/call-aanroepen die een projectbriefing van één pagina opbouwen, en de human-in-the-loop (HITL)-uitwisseling die de uiteindelijke schrijfactie naar het bestand bewaakt. Elk JSON-RPC-bericht hieronder is letterlijk vastgelegd uit een live bin/nextpdf-mcp-proces (alleen core-tier-tools) en vervolgens op precies twee manieren geschoond: het eenmalige bevestigingstoken wordt getoond als confirm_<single-use-hex>, en de tijdelijke systeemmap van de machine is afgekort tot C:\Temp. Identifiers, schema’s, posities en byte-aantallen zijn exact wat de server heeft verzonden.

Terminal window
composer require nextpdf/server

Bind het stdio-transport in je MCP-host — voor Claude Desktop (hosts starten het commando vanuit hun eigen map, gebruik dus een absoluut pad; het stdio-transport heeft geen API-key nodig, in tegenstelling tot het REST-transport):

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

De server spreekt newline-gescheiden JSON-RPC 2.0 op stdin/stdout en houdt protocoluitvoer strikt gescheiden van diagnostiek: opstart- en auditregels gaan naar stderr, nooit naar stdout.

Een MCP-documentsessie is stateful. create_pdf opent een document in de in-memory store van de server en geeft een document_id terug; elke latere aanroep richt zich op die identifier. Content-tools (set_font, add_text, add_table) worden onmiddellijk uitgevoerd op het risiconiveau Caution, met auditlogging; preview_layout is een Safe leesactie; en output_pdf met een file_path is Approval Required — deze draait niet bij de eerste aanroep. In plaats daarvan geeft de server een challenge met een eenmalig token terug, geeft de agent de challenge door aan de mens, en pas een nieuwe aanroep met _confirmation_token voert de schrijfactie uit. Documenten die in de store achterblijven, verlopen na de geconfigureerde time to live (standaard 30 minuten).

Dezelfde toolaanroepen sturen de tool-engine aan over REST en gRPC — de transports delen één executor — dus alles hier, behalve de stdio-framing, is overdraagbaar. Zie Een factuur end-to-end renderen via REST voor dezelfde engine op het HTTP-oppervlak.

ToolRol in deze sessieRisiconiveau
create_pdfHet document openen, document_id ophalenCaution
set_fontKop-, daarna body-lettertype selecterenCaution
add_textTitelregel, daarna introparagraafCaution
add_tableChecklisttabel met eigenaar/deadlineCaution
preview_layoutLay-outstatus lezen vóór uitvoerSafe
output_pdf (bestandsmodus)De PDF schrijven — gatedApproval Required

De hier vastgelegde deployment registreerde 20 tools (13 core, 6 Pro, 1 Enterprise — de aantallen verschijnen in de initialize-respons hieronder); deze sessie gebruikt alleen core-tools en draait dus ongewijzigd op een installatie met uitsluitend open source. De maatgevende catalogus is de tools/list-respons van je eigen server, en de risicoladder is gedefinieerd in de HITL risk tiers reference.

De client opent de sessie en geeft zijn protocolversie op:

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

De server bevestigt de protocolversie en declareert zijn capabilities, waaronder de tool-aantallen per tier en dat HITL-gating is ingeschakeld:

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

De client bevestigt met een notification (notifications dragen geen id en krijgen geen respons):

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

De volledige respons somt alle 20 geregistreerde tools op met hun complete input-schema’s. Hier wordt deze ingekort tot de twee tools die deze sessie openen en afsluiten — de weggelaten 18 vermeldingen hebben dezelfde vorm:

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

Let op het schema van output_pdf: file_path is optioneel, en de annotations bevatten openWorldHint: true — de tool kan de wereld buiten de sessie raken, precies daarom is de bestandsmodus gated.

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

Elk toolresultaat komt tweemaal binnen in één bericht: een voor mensen leesbaar content-tekstblok, en machineleesbare structuredContent. Lees structuredContent.document_id en geef het door aan elke volgende aanroep.

Stel een vet lettertype van 16 punt in en plaats vervolgens de titel:

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

Terug naar een normaal lettertype van 11 punt voor de introtekst; width: 0 selecteert een multi-cell-lay-out over de volle breedte:

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

Elke content-aanroep geeft de bijgewerkte cursor-position terug, zodat de agent altijd weet waar het volgende element belandt.

7. Bekijk een preview voordat je om goedkeuring vraagt

Sectie met titel “7. Bekijk een preview voordat je om goedkeuring vraagt”

preview_layout is een Safe, alleen-lezen aanroep — een net functionerende agent controleert wat hij heeft gebouwd voordat hij een mens om goedkeuring voor een schrijfactie vraagt:

{
"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. Vraag de schrijfactie aan — de gate antwoordt eerst

Sectie met titel “8. Vraag de schrijfactie aan — de gate antwoordt eerst”

De agent vraagt output_pdf om de voltooide briefing naar schijf te schrijven en houdt het document in leven (destroy: false) voor het geval de mens weigert en er teruggevallen moet worden op base64-uitvoer:

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

Het bestand wordt niet geschreven. Omdat de bestandsmodus Approval Required is, antwoordt de server in plaats daarvan met een bevestigingschallenge:

{
"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. De mens keurt goed — roep opnieuw aan met het token

Sectie met titel “9. De mens keurt goed — roep opnieuw aan met het token”

De agent geeft de challenge-tekst door aan de mens. Bij goedkeuring roept hij output_pdf opnieuw aan met dezelfde argumenten 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>"
}
}
}

Het token wordt verbruikt, de schrijfactie wordt uitgevoerd, en het resultaat rapporteert het geschreven bestand:

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

De sessie schreef kickoff-brief.pdf (3.612 bytes, één pagina, overeenkomend met structuredContent.file_size en page_count). Vastgelegde qpdf --check-uitvoer voor exact dat bestand:

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

Dat is een structurele controle, in qpdf’s eigen woorden — geen conformiteitsbepaling.

  • De nieuwe aanroep moet dezelfde argumenten herhalen. Het bevestigingstoken is gebonden aan de toolnaam plus een canonieke digest van de argumenten waarvoor het is uitgegeven. Opnieuw aanroepen waarbij ook maar iets is gewijzigd — zelfs het omzetten van destroy — verbruikt het token niet; de server antwoordt in plaats daarvan met een nieuwe challenge. Herhaal de argumenten exact en voeg alleen _confirmation_token toe.
  • Het token is eenmalig en verloopt. De challenge vermeldt de vervaltijd (300 seconden). Na verval of verbruik krijgt de volgende gated aanroep een nieuwe challenge; geef de nieuwe door.
  • Bestandsuitvoer belandt binnen een allow-listed map. De server weigert een file_path buiten zijn geconfigureerde tijdelijke map met Output path rejected by security policy. De standaard allow-list-root is nextpdf-mcp onder de tijdelijke systeemmap; operators wijzigen dit met de instelling temp_dir in nextpdf-mcp.yaml.
  • base64-modus is niet gated. output_pdf zonder file_path geeft de PDF als base64 terug op het Review-niveau, zonder neveneffect op het bestandssysteem — zie Menselijke goedkeuring vereisen voor bestandsuitvoer voor die grens in detail.
  • Een challenge is een resultaat, geen fout. Het challenge-bericht komt binnen met isError: false; een openstaande goedkeuring is een pauze in de workflow. Probeer niet in een lus opnieuw, en verzin nooit een token.
  • Notifications krijgen geen antwoord. Blokkeer na notifications/initialized niet in afwachting van een responseregel.

De sessie is end-to-end in-memory: content-aanroepen kwamen in milliseconden terug tijdens de vastgelegde run, en de wandkloktijd wordt gedomineerd door de uitwisseling voor menselijke goedkeuring, wat precies het doel van de gate is. De document-store bewaart een sessie standaard 30 minuten bij inactiviteit (maximaal 50 documenten), zodat een trage goedkeuring het opgebouwde document niet verliest — maar een verlaten document wordt teruggevorderd.

  • Behandel het bevestigingstoken als een eenmalig geheim. Geef de challenge-tekst door aan de mens; log het token niet en bewaar het niet. Deze pagina redigeert het vastgelegde token precies om die reden.
  • De audit trail staat op stderr. Elke uitvoering op Caution-niveau of hoger wordt geaudit (tool, risico, argumenten, uitkomst) via PSR-3, met gevoelige parameters geredigeerd. Diagnostiek vermengt zich nooit met de protocolstream.
  • De pad-allow-list is de grens van het bestandssysteem. Richt temp_dir op een map die specifiek voor Connect-uitvoer is bedoeld; verbreed deze niet naar een algemene locatie.
  • Risiconiveaus kunnen alleen omhoog. Een operator-override in nextpdf-mcp.yaml kan het risiconiveau van een tool verhogen, maar kan output_pdf nooit onder Approval Required brengen.

Dit recept doet geen normatieve claim over standaarden. Het documenteert het MCP-stdio-transport (JSON-RPC 2.0, protocolversie 2025-06-18 zoals onderhandeld in de vastgelegde initialize-uitwisseling) en het risico- en bevestigingscontract van de server. De qpdf --check-stap hierboven bevestigt uitsluitend de structurele integriteit van het geschreven bestand; conformiteit met een standaard wordt bepaald door een onafhankelijke validator, niet beweerd door de producerende software.