跳到內容
getnextpdf.com

透過 MCP 驅動代理人文件工作階段

這是針對 NextPDF Connect Model Context Protocol(MCP)伺服器的一次完整代理人工作階段,逐則訊息呈現:initializetools/list、六次建構一頁式專案簡報的 tools/call 呼叫,以及為最終檔案寫入把關的人工介入(HITL) 往返。下方每一則 JSON-RPC 訊息都是從一個實際運作的 bin/nextpdf-mcp 程序(僅 core 等級工具)逐字擷取,接著僅以兩種方式加以淨化:一次性確認權杖以 confirm_<single-use-hex> 表示,而機器的系統暫存目錄縮寫為 C:\Temp。識別碼、schema、位置與位元組數皆與伺服器實際送出的內容完全一致。

Terminal window
composer require nextpdf/server

在您的 MCP host 中綁定 stdio 傳輸層——以 Claude Desktop 為例(host 會從自己的目錄啟動指令,因此請使用絕對路徑;與 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_fontadd_textadd_table)以「注意」(Caution)風險等級立即執行並記錄稽核;preview_layout 是「安全」(Safe)讀取;而帶有 file_pathoutput_pdf 屬於「需要核准」(Approval Required)——它不會在第一次呼叫時執行。伺服器會改為回傳一個帶有一次性權杖的挑戰碼,代理人將挑戰碼轉達給人類,只有攜帶 _confirmation_token 重新呼叫才會執行寫入。留在儲存區的文件會在設定的存活時間後過期(預設為 30 分鐘)。

相同的工具呼叫也能透過 REST 與 gRPC 驅動工具引擎——這些傳輸層共用同一個 executor——因此此處除了 stdio 框架之外的一切都可沿用。若要在 HTTP 介面上使用相同引擎,請參閱 透過 REST 端對端產生發票

工具在本工作階段中的角色風險等級
create_pdf開啟文件,取得 document_id注意
set_font選擇標題字體,再選內文字體注意
add_text標題行,再加入前言段落注意
add_table負責人/到期日檢查清單表格注意
preview_layout輸出前讀取版面狀態安全
output_pdf(檔案模式)寫入 PDF——受閘門管控需要核准

此處擷取的部署註冊了 20 個工具(13 個 core、6 個 Pro、1 個 Enterprise——這些數量出現在下方的 initialize 回應中);本工作階段僅使用 core 工具,因此在僅含開源版本的安裝上也能原封不動地執行。權威的目錄以您自己伺服器的 tools/list 回覆為準,而風險階梯定義於 HITL 風險等級參考

client 開啟工作階段並宣告其協定版本:

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

client 以一則通知確認(通知不帶 id,也不會收到回應):

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

完整回覆會列出全部 20 個已註冊工具及其完整的 input 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 是選用的,且 annotations 帶有 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 會選擇全寬的 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
}
}
}
}
{
"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 是「安全」的唯讀呼叫——行為良好的代理人會在請求人類核准寫入之前,先檢查自己建構的成果:

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

代理人請求 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
}
}
}

檔案不會被寫入。由於檔案模式屬於「需要核准」,伺服器會改以確認挑戰碼回應:

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

代理人將挑戰碼文字轉達給人類。核准後,它以相同的引數再加上 _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_sizepage_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 秒)。過期或被消耗後,下一次受閘門管控的呼叫會取得新的挑戰碼;請轉達新的那一個。
  • 檔案輸出會落在允許清單內的目錄。 伺服器會以 Output path rejected by security policy 拒絕位於其設定暫存目錄之外的 file_path。預設的允許清單根目錄為系統暫存目錄下的 nextpdf-mcp; 操作者可透過 nextpdf-mcp.yaml 中的 temp_dir 設定加以變更。
  • base64 模式不受閘門管控。 不含 file_pathoutput_pdf 會在 「審查」(Review)等級以 base64 回傳 PDF,且不產生任何檔案系統副作用——如需深入了解該邊界,請參閱 為檔案輸出要求人類核准
  • 挑戰碼是結果,而非錯誤。 挑戰碼訊息會帶著 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 步驟僅確認已寫入檔案的結構完整性;對標準的合規性由獨立驗證器判定,而非由產生軟體所主張。