Перейти к содержимому
getnextpdf.com

Управление сессией документа агента через MCP

Это одна полная сессия агента с сервером NextPDF Connect Model Context Protocol (MCP), сообщение за сообщением: initialize, tools/list, шесть вызовов tools/call, которые формируют одностраничный бриф проекта, и цикл human-in-the-loop (HITL), который контролирует финальную запись файла. Каждое сообщение JSON-RPC ниже дословно записано из работающего процесса bin/nextpdf-mcp (только инструменты уровня Core), затем очищено ровно двумя способами: одноразовый токен подтверждения показан как confirm_<single-use-hex>, а системный временный каталог машины сокращён до C:\Temp. Идентификаторы, схемы, позиции и количество байтов — ровно те, что отправил сервер.

Окно терминала
composer require nextpdf/server

Пропишите транспорт stdio в вашем MCP-хосте — например, для Claude Desktop (хосты запускают команду из собственного каталога, поэтому используйте абсолютный путь; транспорту stdio, в отличие от транспорта REST, не нужен API-ключ):

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

Сервер общается по протоколу JSON-RPC 2.0 с разделением сообщений переводом строки через stdin/stdout и строго отделяет вывод протокола от диагностики: строки запуска и аудита идут в stderr, никогда в stdout.

Сессия документа MCP сохраняет состояние. create_pdf открывает документ во внутреннем хранилище сервера в памяти и возвращает document_id; каждый последующий вызов обращается к этому идентификатору. Инструменты содержимого (set_font, add_text, add_table) выполняются немедленно на уровне риска Caution с журналированием аудита; preview_layout — это безопасное чтение (Safe); а output_pdf с file_path относится к уровню Approval Required — он не выполняется при первом вызове. Вместо этого сервер возвращает запрос на подтверждение с одноразовым токеном, агент передаёт запрос человеку, и только повторный вызов с _confirmation_token выполняет запись. Документы, оставленные в хранилище, удаляются по истечении настроенного времени жизни (по умолчанию 30 минут).

Те же вызовы инструментов управляют движком инструментов через REST и gRPC — транспорты используют один исполнитель, — поэтому всё здесь, кроме кадрирования stdio, переносится. См. Рендеринг счёта от начала до конца через REST — тот же движок на поверхности HTTP.

ИнструментРоль в этой сессииУровень риска
create_pdfОткрытие документа, получение document_idCaution (осторожность)
set_fontВыбор шрифта заголовка, затем основного текстаCaution (осторожность)
add_textСтрока заголовка, затем вводный абзацCaution (осторожность)
add_tableТаблица-чеклист с ответственными и срокамиCaution (осторожность)
preview_layoutЧтение состояния макета перед выводомSafe (безопасный)
output_pdf (режим файла)Запись PDF — через шлюзApproval Required (требуется одобрение)

В зафиксированном здесь развёртывании зарегистрировано 20 инструментов (13 Core, 6 Pro, 1 Enterprise — их количество приведено в ответе initialize ниже); эта сессия использует только инструменты Core, поэтому она работает без изменений в установке только с открытым исходным кодом. Официальным каталогом является ответ tools/list вашего собственного сервера, а лестница рисков определена в справочнике по уровням риска HITL.

Клиент открывает сессию и указывает свою версию протокола:

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

Сервер подтверждает версию протокола и объявляет свои возможности, включая количество инструментов по уровням и то, что контроль 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"
}
}
}

Клиент подтверждает уведомлением (уведомления не несут id и не получают ответа):

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

Полный ответ перечисляет все 20 зарегистрированных инструментов с их полными входными схемами. Здесь он показан сокращённым до двух инструментов, которые открывают и закрывают эту сессию — 18 опущенных записей имеют такую же форму:

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

Обратите внимание на схему output_pdf: file_path необязателен, а аннотации содержат openWorldHint: true — инструмент может воздействовать на мир за пределами сессии, именно поэтому режим файла проходит через шлюз.

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

Каждый результат работы инструмента приходит дважды в одном сообщении: удобочитаемый текстовый блок content и машиночитаемый structuredContent. Прочитайте structuredContent.document_id и передавайте его через каждый следующий вызов.

Задайте жирный шрифт 16 пунктов, затем разместите заголовок:

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

Возврат к обычному шрифту 11 пунктов для вводного текста; width: 0 выбирает многоячеечную компоновку на всю ширину:

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

Каждый вызов, добавляющий содержимое, возвращает обновлённую position курсора, поэтому агент всегда знает, куда попадёт следующий элемент.

preview_layout — это безопасный вызов только для чтения (Safe): корректно ведущий себя агент проверяет то, что он построил, прежде чем просить человека одобрить запись:

{
"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. Запрос на запись файла — сначала отвечает шлюз

Заголовок раздела «8. Запрос на запись файла — сначала отвечает шлюз»

Агент просит output_pdf записать готовый бриф на диск, сохраняя документ (destroy: false) на случай, если человек отклонит запрос и потребуется вернуться к выводу в 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
}
}
}

Файл не записывается. Поскольку режим файла имеет уровень Approval Required, сервер вместо этого отвечает запросом на подтверждение:

{
"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. Человек одобряет — повторный вызов с токеном

Заголовок раздела «9. Человек одобряет — повторный вызов с токеном»

Агент передаёт текст запроса на подтверждение человеку. После одобрения он снова вызывает output_pdf с теми же аргументами плюс _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>"
}
}
}

Токен расходуется, запись выполняется, и результат сообщает о записанном файле:

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

Сессия записала kickoff-brief.pdf (3 612 байт, одна страница, что совпадает с structuredContent.file_size и page_count). Зафиксированный вывод qpdf --check для этого файла:

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

Это структурная проверка, по собственным словам qpdf, — а не заключение о соответствии.

  • Повторный вызов должен повторять те же аргументы. Токен подтверждения привязан к имени инструмента и каноническому дайджесту аргументов, для которых он был выдан. Повторный вызов с любым изменением — даже при переключении destroy — не расходует токен; вместо этого сервер отвечает новым запросом на подтверждение. Повторите аргументы в точности и добавьте только _confirmation_token.
  • Токен одноразовый и имеет срок действия. Запрос на подтверждение указывает срок действия (300 секунд). После истечения срока или использования следующий вызов через шлюз получает новый запрос на подтверждение; передайте новый.
  • Вывод файла попадает в каталог из списка разрешённых. Сервер отклоняет file_path за пределами настроенного временного каталога с сообщением Output path rejected by security policy. Корневой каталог списка разрешённых по умолчанию — nextpdf-mcp внутри системного временного каталога; операторы меняют его настройкой temp_dir в nextpdf-mcp.yaml.
  • Режим base64 не проходит через шлюз. output_pdf без file_path возвращает PDF в виде base64 на уровне Review, без побочных эффектов в файловой системе — подробнее об этой границе см. Обязательное подтверждение вывода файлов человеком.
  • Запрос на подтверждение — это результат, а не ошибка. Сообщение с запросом приходит с isError: false; ожидание одобрения — это пауза в рабочем процессе. Не повторяйте попытку в цикле и никогда не подделывайте токен.
  • Уведомления не получают ответа. После notifications/initialized не блокируйтесь в ожидании строки ответа.

Сессия целиком выполняется в памяти: в зафиксированном запуске вызовы, добавляющие содержимое, возвращались за миллисекунды, а общее время определяется циклом одобрения человеком — в этом и смысл шлюза. Хранилище документов удерживает сессию 30 минут простоя по умолчанию (максимум 50 документов), поэтому медленное одобрение не теряет построенный документ — но заброшенный документ высвобождается.

  • Относитесь к токену подтверждения как к одноразовому секрету. Передавайте человеку текст запроса на подтверждение; не записывайте токен в журнал и не сохраняйте его. Именно поэтому на этой странице зафиксированный токен скрыт.
  • Журнал аудита идёт в stderr. Каждое выполнение уровня Caution и выше журналируется для аудита (инструмент, риск, аргументы, результат) через PSR-3, с сокрытием чувствительных параметров. Диагностика никогда не смешивается с потоком протокола.
  • Список разрешённых путей — это граница файловой системы. Направьте temp_dir на каталог, выделенный для вывода Connect; не расширяйте его до каталога общего назначения.
  • Уровни риска только повышаются. Переопределение оператором в nextpdf-mcp.yaml может повысить уровень риска инструмента, но никогда не может понизить output_pdf ниже Approval Required.

Этот рецепт не делает никаких нормативных заявлений о соответствии стандартам. Он документирует транспорт MCP stdio (JSON-RPC 2.0, версия протокола 2025-06-18, согласованная в зафиксированном обмене initialize) и контракт сервера по рискам и подтверждению. Шаг qpdf --check выше подтверждает только структурную целостность записанного файла; соответствие стандарту определяется независимым валидатором, а не утверждается производящим программным обеспечением.