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로 축약됩니다. 식별자, 스키마, 위치,
바이트 수는 서버가 보낸 그대로입니다.
composer require nextpdf/serverMCP 호스트에서 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로 인보이스를 처음부터 끝까지 렌더링하기를 참고하세요.
API 표면
섹션 제목: “API 표면”| 도구 | 이 세션에서의 역할 | 위험 수준 |
|---|---|---|
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 위험 등급 레퍼런스에 정의되어 있습니다.
세션, 메시지 단위로
섹션 제목: “세션, 메시지 단위로”1. 연결 초기화
섹션 제목: “1. 연결 초기화”클라이언트가 세션을 열고 프로토콜 버전을 명시합니다:
{ "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"}2. 도구 탐색
섹션 제목: “2. 도구 탐색”{ "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 모드가 게이트됩니다.
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은 전체 너비
멀티셀 레이아웃을 선택합니다:
{ "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은 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_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는 파일 시스템 부작용 없이 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
단계는 쓰인 파일의 구조적 무결성만 확인합니다. 표준에 대한 적합성은 생성
소프트웨어가 주장하는 것이 아니라 독립적인 검증기가 판정합니다.
함께 보기
섹션 제목: “함께 보기”- 파일 출력에 사람의 승인 요구하기 — 거부 경로를 포함한 확인 게이트의 상세 내용.
- REST로 인보이스를 처음부터 끝까지 렌더링하기 — HTTP를 통한 동일한 도구 엔진, 캡처한 와이어 트랜스크립트 포함.
- 첫 PDF 생성하기 — 가장 작은 Connect 세션.
- Connect 레시피 규약 — 모든 Connect 레시피가 따르는 계약.
- HITL 위험 등급 — 기준이 되는 위험 사다리와 정책 해석.
- 도구 카탈로그 — 기준이 되는 도구 카탈로그.