通过 MCP 驱动代理文档会话
这是一次针对 NextPDF Connect Model Context Protocol (MCP) 服务器的完整代理会话,逐条消息呈现:initialize、tools/list、六次构建一页项目简报的 tools/call
调用,以及为最终文件写入把关的人工介入(HITL)往返。下面每一条 JSON-RPC 消息都是从一个运行中的 bin/nextpdf-mcp 进程逐字捕获的(仅核心层工具),随后仅做了两处脱敏:一次性确认令牌显示为
confirm_<single-use-hex>,本机的系统临时目录被缩短为 C:\Temp。标识符、schema、位置和字节数都与服务器实际发送的完全一致。
composer require nextpdf/server在你的 MCP 宿主中绑定 stdio 传输层——以 Claude Desktop 为例(宿主会从其自身目录启动该命令,因此请使用绝对路径;与 REST 传输层不同,stdio 传输层无需 API key):
{ "mcpServers": { "nextpdf": { "command": "php", "args": ["/absolute/path/to/your/project/vendor/bin/nextpdf-mcp"] } }}服务器在 stdin/stdout 上使用以换行分隔的 JSON-RPC 2.0 通信,并将协议输出与诊断信息严格分离:启动和审计日志行发往 stderr,绝不发往 stdout。
概念概览
标题为“概念概览”的章节MCP 文档会话是有状态的。create_pdf 会在服务器的内存存储中打开一个文档,并返回一个 document_id;此后的每次调用都以该标识符为目标。内容工具(set_font、add_text、
add_table)会以“谨慎”风险等级立即执行,并带有审计日志;preview_layout 是“安全”读取;而带有 file_path 的 output_pdf 属于“需要核准”——它不会在首次调用时执行。相反,服务器会返回一个带有一次性令牌的挑战码,代理将挑战码转交给人工,只有携带
_confirmation_token 的重新调用才会执行写入。留在存储中的文档会在配置的存活时间(默认 30 分钟)后过期。
同样的工具调用也通过 REST 和 gRPC 驱动工具引擎——这些传输层共享同一个执行器——因此除 stdio 分帧外,这里的一切都同样适用。要了解同一引擎在 HTTP 层面的表现,请参见 通过 REST 端到端渲染一张发票。
API 接口
标题为“API 接口”的章节| 工具 | 在本会话中的作用 | 风险等级 |
|---|---|---|
create_pdf | 打开文档,获取 document_id | 谨慎 |
set_font | 选择标题字体,然后是正文字体 | 谨慎 |
add_text | 标题行,然后是引言段落 | 谨慎 |
add_table | 负责人/截止日期清单表格 | 谨慎 |
preview_layout | 在输出前读取布局状态 | 安全 |
output_pdf(file 模式) | 写入 PDF——受闸门管控 | 需要核准 |
这里捕获的部署共注册了 20 个工具(13 个 Core、6 个 Pro、1 个 Enterprise——数量见下方 initialize 响应);本会话仅使用核心工具,因此在纯开源安装环境下也能原样运行。正式记录的目录是你自己服务器的
tools/list 响应,风险阶梯则定义于 HITL 风险等级参考。
会话逐条消息详解
标题为“会话逐条消息详解”的章节1. 初始化连接
标题为“1. 初始化连接”的章节客户端打开会话并声明其协议版本:
{ "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"}2. 发现工具
标题为“2. 发现工具”的章节{ "jsonrpc": "2.0", "id": 2, "method": "tools/list"}完整响应会列出全部 20 个已注册工具及其完整的输入 schema。这里将其精简为开启和收尾本会话的两个工具——省略的 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 的 schema:file_path 是可选的,并且注解中带有 openWorldHint: true——该工具可以触及会话之外的世界,这正是 file 模式受闸门管控的原因。
3. 打开文档
标题为“3. 打开文档”的章节{ "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,并将它贯穿到之后的每一次调用。
4. 添加标题
标题为“4. 添加标题”的章节设置 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 } } }}5. 添加正文段落
标题为“5. 添加正文段落”的章节为引言文本切回常规 11 磅字体;width: 0 会选择全宽多单元格(multi-cell)布局:
{ "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 } } }}6. 添加清单表格
标题为“6. 添加清单表格”的章节{ "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,因此代理始终知道下一个元素落在哪里。
7. 在请求核准前预览
标题为“7. 在请求核准前预览”的章节preview_layout 是一个“安全”的只读调用——行为良好的代理会在请求人工核准写入之前,先检查自己构建出来的内容:
{ "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 } }}文件未被写入。由于 file 模式属于“需要核准”,服务器转而返回一个确认挑战码:
{ "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. 人工核准——携带令牌重新调用”的章节代理将挑战码文本转交给人工。核准后,它会带着相同的参数外加 _confirmation_token 再次调用 output_pdf:
{ "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.pdfPDF Version: 2.0File is not encryptedFile is not linearizedNo syntax or stream encoding errors found; the file may still containerrors that qpdf cannot detect用 qpdf 自己的话说,这是一次结构检查——而非一致性判定。
边界情况与陷阱
标题为“边界情况与陷阱”的章节- **重新调用必须重复相同的参数。**确认令牌绑定于工具名称,加上其签发时所针对的那组参数的规范化摘要。若重新调用时改动了任何内容——哪怕只是翻转
destroy——都不会消耗该令牌;服务器会转而返回一个全新的挑战码。请原样重复参数,并只额外添加_confirmation_token。 - **令牌是一次性的,并会过期。**挑战码会说明过期时间(300 秒)。过期或被消耗后,下一次受闸门管控的调用会获得一个新的挑战码;请转交这个新的挑战码。
- **文件输出会落在一个白名单目录内。**对于位于其配置的临时目录之外的
file_path,服务器会以Output path rejected by security policy拒绝。默认的白名单根目录是系统临时目录下的nextpdf-mcp;运维人员可通过nextpdf-mcp.yaml中的temp_dir设置来更改它。 - **base64 模式不受闸门管控。**不带
file_path的output_pdf会以“审查”等级将 PDF 作为 base64 返回,不产生任何文件系统副作用——关于该边界的深入说明,请参见 要求对文件输出进行人工审批。 - **挑战码是一个结果,而非错误。**挑战码消息带有
isError: false;待核准状态是一次工作流暂停。不要循环重试,也绝不要伪造令牌。 - **通知不会得到回复。**在
notifications/initialized之后,不要阻塞等待响应行。
本会话端到端都在内存中进行:在捕获的这次运行中,内容调用在毫秒级返回,而挂钟时间主要由人工核准的往返所主导——而这正是闸门的意义所在。文档存储默认会为每个会话保留 30 分钟的空闲存活时间(最多 50 个文档),因此缓慢的核准不会丢失已构建的文档——但被弃置的文档会被回收。
安全说明
标题为“安全说明”的章节- **将确认令牌视为一次性机密。**把挑战码文本转交给人工;不要记录该令牌,也不要将其持久化。本页正是出于这个原因才对捕获到的令牌做了脱敏处理。
- **审计轨迹位于 stderr 上。**每一次“谨慎”及以上等级的执行都会通过 PSR-3 记入审计日志(工具、风险、参数、结果),并对敏感参数做脱敏处理。诊断信息绝不会混入协议流。
- **路径白名单是文件系统边界。**请将
temp_dir指向一个专用于 Connect 输出的目录;不要将其放宽到通用位置。 - 风险等级只能向上调整。
nextpdf-mcp.yaml中的运维覆盖可以提升某个工具的风险等级,但绝不能将output_pdf降到“需要核准”以下。
一致性
标题为“一致性”的章节本教程不作任何规范性标准声明。它记录的是 MCP stdio 传输层(JSON-RPC 2.0,协议版本 2025-06-18,即捕获的 initialize 交互中协商出的版本)以及服务器的风险与确认契约。上文的
qpdf --check 步骤仅确认写入文件的结构完整性;对某项标准的一致性由独立的验证器判定,而非由生产软件自行断言。
另请参阅
标题为“另请参阅”的章节- 要求对文件输出进行人工审批——深入讲解确认闸门,包括拒绝路径。
- 通过 REST 端到端渲染一张发票——同一个工具引擎在 HTTP 上的表现,附捕获的通信记录。
- 生成你的第一个 PDF——最小的 Connect 会话。
- Connect recipe 惯例——每个 Connect 教程都遵循的契约。
- HITL 风险等级——正式的风险阶梯与政策解析。
- 工具目录——正式记录的工具目录。