Przejdź do głównej zawartości
getnextpdf.com

Sterowanie sesją dokumentu agenta przez MCP

To jedna kompletna sesja agenta z serwerem Model Context Protocol (MCP) NextPDF Connect, komunikat po komunikacie: initialize, tools/list, sześć wywołań tools/call, które budują jednostronicowy brief projektu, oraz wymiana w obie strony z udziałem człowieka (HITL), która bramkuje końcowy zapis pliku. Każdy komunikat JSON-RPC poniżej został zarejestrowany dosłownie z działającego procesu bin/nextpdf-mcp (tylko narzędzia poziomu Core), a następnie oczyszczony na dokładnie dwa sposoby: jednorazowy token potwierdzenia jest pokazany jako confirm_<single-use-hex>, a systemowy katalog tymczasowy maszyny skrócono do C:\Temp. Identyfikatory, schematy, pozycje i liczby bajtów są dokładnie tym, co wysłał serwer.

Okno terminala
composer require nextpdf/server

Powiąż transport stdio w hoście MCP — na przykładzie Claude Desktop (hosty uruchamiają polecenie z własnego katalogu, więc użyj ścieżki bezwzględnej; transport stdio nie wymaga klucza API, w przeciwieństwie do transportu REST):

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

Serwer komunikuje się w JSON-RPC 2.0 rozdzielanym znakami nowej linii na stdin/stdout i ściśle oddziela wyjście protokołu od diagnostyki: wiersze startowe i audytowe trafiają na stderr, nigdy na stdout.

Sesja dokumentu MCP jest stanowa. create_pdf otwiera dokument w przechowywanym w pamięci magazynie serwera i zwraca document_id; każde późniejsze wywołanie odwołuje się do tego identyfikatora. Narzędzia treści (set_font, add_text, add_table) wykonują się natychmiast na poziomie ryzyka Caution z rejestrowaniem audytowym; preview_layout to odczyt Safe; a output_pdf z file_path ma poziom Approval Required — nie uruchamia się przy pierwszym wywołaniu. Zamiast tego serwer zwraca wyzwanie z jednorazowym tokenem, agent przekazuje wyzwanie człowiekowi, a zapis wykonuje dopiero ponowne wywołanie zawierające _confirmation_token. Dokumenty pozostawione w magazynie wygasają po skonfigurowanym czasie życia (domyślnie 30 minut).

Te same wywołania narzędzi napędzają silnik narzędzi przez REST i gRPC — transporty współdzielą jeden executor — więc wszystko tutaj poza ramkowaniem stdio ma zastosowanie także tam. Zobacz Renderowanie faktury od początku do końca przez REST, aby poznać ten sam silnik na powierzchni HTTP.

NarzędzieRola w tej sesjiPoziom ryzyka
create_pdfOtwarcie dokumentu, uzyskanie document_idCaution
set_fontWybór kroju nagłówka, potem tekstu głównegoCaution
add_textWiersz tytułu, potem akapit wprowadzającyCaution
add_tableTabela kontrolna właściciel/terminCaution
preview_layoutOdczyt stanu układu przed wyjściemSafe
output_pdf (tryb plikowy)Zapis pliku PDF — z bramąApproval Required

Wdrożenie zarejestrowane tutaj zgłosiło 20 narzędzi (13 Core, 6 Pro, 1 Enterprise — liczby te pojawiają się w odpowiedzi initialize poniżej); ta sesja korzysta tylko z narzędzi Core, więc działa bez zmian w instalacji zawierającej wyłącznie open source. Katalogiem wiążącym jest odpowiedź tools/list twojego własnego serwera, a drabinę ryzyka definiuje dokumentacja poziomów ryzyka HITL.

Klient otwiera sesję i podaje swoją wersję protokołu:

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

Serwer potwierdza wersję protokołu i deklaruje swoje możliwości, w tym liczby narzędzi w podziale na poziomy oraz włączenie bramkowania 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"
}
}
}

Klient potwierdza powiadomieniem (powiadomienia nie zawierają id i nie otrzymują odpowiedzi):

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

Pełna odpowiedź wymienia wszystkie 20 zarejestrowanych narzędzi z ich kompletnymi schematami wejściowymi. Pokazano ją tutaj w skróconej formie, ograniczonej do dwóch narzędzi, które otwierają i zamykają tę sesję — pominięte 18 pozycji ma ten sam kształt:

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

Zwróć uwagę na schemat output_pdf: file_path jest opcjonalne, a adnotacje zawierają openWorldHint: true — narzędzie może oddziaływać na świat poza sesją, i właśnie dlatego tryb plikowy jest objęty bramą.

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

Każdy wynik narzędzia przychodzi dwukrotnie w jednym komunikacie: jako czytelny dla człowieka blok tekstu content oraz jako czytelny maszynowo structuredContent. Odczytaj structuredContent.document_id i przekazuj go w każdym kolejnym wywołaniu.

Ustaw pogrubiony krój 16-punktowy, a następnie umieść tytuł:

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

Powrót do zwykłego kroju 11-punktowego dla tekstu wprowadzającego; width: 0 wybiera pełnoszerokościowy układ wielokomórkowy:

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

Każde wywołanie treści zwraca zaktualizowaną position kursora, więc agent zawsze wie, gdzie trafi następny element.

preview_layout to wywołanie Safe, tylko do odczytu — dobrze zachowujący się agent sprawdza to, co zbudował, zanim poprosi człowieka o zatwierdzenie zapisu:

{
"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. Żądanie zapisu pliku — brama odpowiada jako pierwsza

Dział zatytułowany „8. Żądanie zapisu pliku — brama odpowiada jako pierwsza”

Agent prosi output_pdf o zapisanie gotowego briefu na dysku, utrzymując dokument przy życiu (destroy: false) na wypadek, gdyby człowiek odrzucił żądanie i konieczne było przejście awaryjne na wyjście 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
}
}
}

Plik nie zostaje zapisany. Ponieważ tryb plikowy ma poziom Approval Required, serwer odpowiada zamiast tego wyzwaniem potwierdzenia:

{
"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. Człowiek zatwierdza — ponowne wywołanie z tokenem

Dział zatytułowany „9. Człowiek zatwierdza — ponowne wywołanie z tokenem”

Agent przekazuje tekst wyzwania człowiekowi. Po zatwierdzeniu wywołuje output_pdf ponownie z tymi samymi argumentami oraz _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>"
}
}
}

Token zostaje skonsumowany, zapis się wykonuje, a wynik zgłasza zapisany plik:

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

Sesja zapisała kickoff-brief.pdf (3612 bajtów, jedna strona, zgodnie z structuredContent.file_size i page_count). Zarejestrowane wyjście qpdf --check dla dokładnie tego pliku:

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

To sprawdzenie strukturalne, wyrażone słowami samego qpdf — a nie orzeczenie o zgodności.

  • Ponowne wywołanie musi powtórzyć te same argumenty. Token potwierdzenia jest powiązany z nazwą narzędzia oraz kanonicznym skrótem argumentów, dla których został wydany. Ponowne wywołanie z jakąkolwiek zmianą — nawet przełączeniem destroy — nie konsumuje tokenu; serwer odpowiada zamiast tego nowym wyzwaniem. Powtórz argumenty dokładnie i dodaj tylko _confirmation_token.
  • Token jest jednorazowy i wygasa. Wyzwanie podaje czas wygaśnięcia (300 sekund). Po wygaśnięciu lub skonsumowaniu następne wywołanie objęte bramą otrzymuje nowe wyzwanie; przekaż to nowe.
  • Wyjście plikowe trafia do katalogu z listy dozwolonych. Serwer odrzuca file_path spoza skonfigurowanego katalogu tymczasowego komunikatem Output path rejected by security policy. Domyślnym korzeniem listy dozwolonych jest nextpdf-mcp w systemowym katalogu tymczasowym; operatorzy zmieniają go ustawieniem temp_dir w nextpdf-mcp.yaml.
  • Tryb base64 nie jest objęty bramą. output_pdf bez file_path zwraca plik PDF jako base64 na poziomie Review, bez efektu ubocznego w systemie plików — zobacz Wymaganie zatwierdzenia przez człowieka dla wyjścia plikowego, aby dogłębnie poznać tę granicę.
  • Wyzwanie jest wynikiem, nie błędem. Komunikat wyzwania przychodzi z isError: false; oczekujące zatwierdzenie to pauza w przepływie pracy. Nie ponawiaj w pętli i nigdy nie fabrykuj tokenu.
  • Powiadomienia nie otrzymują odpowiedzi. Po notifications/initialized nie blokuj się w oczekiwaniu na wiersz odpowiedzi.

Sesja działa w całości w pamięci: wywołania treści zwracały wynik w milisekundach w zarejestrowanym przebiegu, a czas zegarowy jest zdominowany przez wymianę w obie strony przy zatwierdzaniu przez człowieka, co jest właśnie sensem bramy. Magazyn dokumentów przechowuje sesję domyślnie przez 30 minut bezczynności (maksymalnie 50 dokumentów), więc powolne zatwierdzenie nie powoduje utraty zbudowanego dokumentu — ale porzucony zostaje odzyskany.

  • Traktuj token potwierdzenia jako jednorazowy sekret. Przekaż tekst wyzwania człowiekowi; nie zapisuj tokenu w logach ani go nie utrwalaj. Ta strona ukrywa zarejestrowany token dokładnie z tego powodu.
  • Ślad audytowy trafia na stderr. Każde wykonanie na poziomie Caution lub wyższym jest rejestrowane w audycie (narzędzie, ryzyko, argumenty, wynik) przez PSR-3, z ukryciem wrażliwych parametrów. Diagnostyka nigdy nie miesza się ze strumieniem protokołu.
  • Lista dozwolonych ścieżek jest granicą systemu plików. Skieruj temp_dir na katalog przeznaczony na wyjście Connect; nie rozszerzaj go na lokalizację ogólnego przeznaczenia.
  • Poziomy ryzyka można podnosić tylko w górę. Nadpisanie przez operatora w nextpdf-mcp.yaml może podnieść poziom ryzyka narzędzia, ale nigdy nie może obniżyć output_pdf poniżej Approval Required.

Ten przepis nie formułuje żadnego normatywnego twierdzenia o zgodności ze standardami. Dokumentuje transport stdio MCP (JSON-RPC 2.0, wersja protokołu 2025-06-18 wynegocjowana w zarejestrowanej wymianie initialize) oraz kontrakt ryzyka i potwierdzenia serwera. Krok qpdf --check powyżej potwierdza wyłącznie integralność strukturalną zapisanego pliku; zgodność ze standardem orzeka niezależny walidator, a nie oprogramowanie, które go wytwarza.