콘텐츠로 이동
getnextpdf.com

MCP를 통해 에이전트 문서 세션 구동하기

이 문서는 NextPDF Connect Model Context Protocol(MCP) 서버를 상대로 한 하나의 완전한 에이전트 세션을 메시지 단위로 보여줍니다. initialize, tools/list, 한 페이지 프로젝트 브리프를 작성하는 여섯 번의 tools/call 호출, 그리고 최종 파일 쓰기를 게이트하는 휴먼 인 더 루프(HITL) 왕복입니다. 아래의 모든 JSON-RPC 메시지는 실제로 동작 중인 bin/nextpdf-mcp 프로세스(core 등급 도구만)에서 그대로 캡처한 뒤, 정확히 두 가지 방식으로만 정제했습니다. 일회용 확인 토큰은 confirm_<single-use-hex>로 표시되고, 머신의 시스템 임시 디렉터리는 C:\Temp로 축약됩니다. 식별자, 스키마, 위치, 바이트 수는 서버가 보낸 그대로입니다.

Terminal window
composer require nextpdf/server

MCP 호스트에서 stdio 트랜스포트를 바인딩합니다 — Claude Desktop의 경우(호스트는 자체 디렉터리에서 명령을 실행하므로 절대 경로를 사용하세요. REST 트랜스포트와 달리 stdio 트랜스포트는 API 키가 필요하지 않습니다):

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

서버는 stdin/stdout에서 개행으로 구분된 JSON-RPC 2.0으로 통신하며, 프로토콜 출력을 진단 정보와 엄격히 분리합니다. 시작 및 감사 로그 라인은 stderr로 가며, 절대 stdout으로 가지 않습니다.

MCP 문서 세션은 상태를 가집니다(stateful). create_pdf는 서버의 인메모리 저장소에 문서를 열고 document_id를 반환하며, 이후의 모든 호출은 그 식별자를 대상으로 합니다. 콘텐츠 도구(set_font, add_text, add_table)는 감사 로깅과 함께 Caution 위험 수준에서 즉시 실행됩니다. preview_layout은 Safe 읽기이고, file_path를 지정한 output_pdf는 Approval Required입니다 — 첫 호출에서는 실행되지 않습니다. 대신 서버는 일회용 토큰이 포함된 챌린지를 반환하고, 에이전트는 그 챌린지를 사람에게 전달하며, _confirmation_token을 포함한 재호출만이 쓰기를 실행합니다. 저장소에 남은 문서는 설정된 TTL(time to live, 기본 30분)이 지나면 만료됩니다.

동일한 도구 호출이 REST 및 gRPC를 통해 도구 엔진을 구동합니다 — 트랜스포트는 하나의 실행기를 공유합니다 — 따라서 stdio 프레이밍을 제외한 여기의 모든 내용이 그대로 적용됩니다. HTTP 표면에서 동일한 엔진을 보려면 REST로 인보이스를 처음부터 끝까지 렌더링하기를 참고하세요.

도구이 세션에서의 역할위험 수준
create_pdf문서를 열고 document_id를 얻음Caution
set_font제목용, 그다음 본문용 글꼴 선택Caution
add_text제목 줄, 그다음 도입 단락Caution
add_table담당자/마감일 체크리스트 표Caution
preview_layout출력 전 레이아웃 상태 읽기Safe
output_pdf (file 모드)PDF 쓰기 — 게이트됨Approval Required

여기서 캡처한 배포에는 20개의 도구가 등록되어 있습니다(core 13개, Pro 6개, Enterprise 1개 — 개수는 아래 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 게이트 활성화 여부를 포함한 기능(capabilities)을 선언합니다:

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

클라이언트는 알림(notification)으로 확인 응답을 보냅니다(알림에는 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를 담고 있습니다 — 이 도구는 세션 외부의 세계에 영향을 줄 수 있으며, 바로 그 때문에 file 모드가 게이트됩니다.

{
"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에게 완성된 브리프를 디스크에 쓰도록 요청하면서, 사람이 거부하여 base64 출력으로 되돌아가야 할 경우를 대비해 문서를 살려 둡니다(destroy: false):

{
"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 모드가 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. 사람이 승인 — 토큰과 함께 재호출”

에이전트는 챌린지 텍스트를 사람에게 전달합니다. 승인되면 동일한 인자_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초)을 명시합니다. 만료되거나 소비된 후에는 다음 게이트 호출이 새 챌린지를 받으니, 그 새 챌린지를 전달하세요.
  • 파일 출력은 허용 목록에 등록된 디렉터리 안에 놓입니다. 서버는 설정된 임시 디렉터리 밖의 file_pathOutput path rejected by security policy로 거부합니다. 기본 허용 목록 루트는 시스템 임시 디렉터리 아래의 nextpdf-mcp 이며, 운영자는 nextpdf-mcp.yamltemp_dir 설정으로 이를 변경합니다.
  • base64 모드는 게이트되지 않습니다. file_path 없는 output_pdf는 파일 시스템 부작용 없이 Review 수준에서 PDF를 base64로 반환합니다 — 그 경계에 대한 자세한 내용은 파일 출력에 사람의 승인 요구하기를 참고하세요.
  • 챌린지는 오류가 아니라 결과입니다. 챌린지 메시지는 isError: false와 함께 도착합니다. 승인 대기는 워크플로 일시 정지입니다. 루프에서 재시도하지 말고, 토큰을 절대 조작하지 마세요.
  • 알림에는 응답이 없습니다. notifications/initialized 이후에는 응답 라인을 기다리며 블로킹하지 마세요.

세션은 처음부터 끝까지 인메모리입니다. 캡처한 실행에서 콘텐츠 호출은 밀리초 단위로 반환되었고, 전체 소요 시간(wall time)은 사람 승인 왕복이 지배하는데, 이것이 바로 게이트의 요점입니다. 문서 저장소는 기본적으로 유휴 상태 30분 동안 세션을 유지하므로(최대 50개 문서), 승인이 느려도 작성된 문서를 잃지 않습니다 — 다만 방치된 문서는 회수됩니다.

  • 확인 토큰을 일회용 비밀로 취급하세요. 챌린지 텍스트를 사람에게 전달하되, 토큰을 로그에 남기거나 영속화하지 마세요. 이 페이지가 캡처한 토큰을 가리는 이유가 바로 그것입니다.
  • 감사 추적은 stderr에 있습니다. Caution 이상의 모든 실행은 PSR-3을 통해 감사 로깅되며(도구, 위험, 인자, 결과), 민감한 파라미터는 가려집니다. 진단 정보는 절대 프로토콜 스트림에 섞이지 않습니다.
  • 경로 허용 목록이 파일 시스템 경계입니다. temp_dir를 Connect 출력 전용 디렉터리로 지정하고, 범용 위치로 넓히지 마세요.
  • 위험 수준은 오직 위로만 조정됩니다. nextpdf-mcp.yaml의 운영자 재정의는 도구의 위험 수준을 높일 수 있지만, output_pdf를 Approval Required 아래로는 절대 낮출 수 없습니다.

이 레시피는 어떠한 규범적 표준 주장도 하지 않습니다. 이 문서는 MCP stdio 트랜스포트(JSON-RPC 2.0, 캡처한 initialize 교환에서 협상된 프로토콜 버전 2025-06-18)와 서버의 위험 및 확인 계약을 문서화합니다. 위의 qpdf --check 단계는 쓰인 파일의 구조적 무결성만 확인합니다. 표준에 대한 적합성은 생성 소프트웨어가 주장하는 것이 아니라 독립적인 검증기가 판정합니다.