pdf-agent-kit 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.4
2
+ Name: pdf-agent-kit
3
+ Version: 0.1.0
4
+ Summary: Local MCP + CLI thin adapter for pdf_editor_api.
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: fastmcp>=2.0.0
8
+ Requires-Dist: httpx>=0.28.0
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest<9,>=8.0; extra == "dev"
11
+
12
+ # pdf-agent-kit
13
+
14
+ `pdf-agent-kit` is a local adapter package for the pdf-agent-kit cloud API.
15
+
16
+ - MCP entrypoint: `pdf-agent-kit` (stdio, one tool: `extract_pdf_to_json`)
17
+ - CLI entrypoint: `pdf-edit`
18
+ - Python import: `from pdf_agent_kit import extract`
19
+
20
+ The package is thin by design: it reads a local PDF file, attaches `PDF_EDITOR_API_KEY`, and forwards to the cloud API.
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pip install pdf-agent-kit
26
+ ```
27
+
28
+ or:
29
+
30
+ ```bash
31
+ uvx pdf-agent-kit
32
+ ```
33
+
34
+ ## Environment
35
+
36
+ ```bash
37
+ export PDF_EDITOR_API_KEY="sk_live_..."
38
+ ```
39
+
40
+ If missing, commands fail with:
41
+
42
+ ```text
43
+ PDF_EDITOR_API_KEY not set. Get your key at https://pdfagentkit.com/dashboard
44
+ ```
45
+
46
+ ## MCP Tool
47
+
48
+ Tool name: `extract_pdf_to_json`
49
+
50
+ Parameters:
51
+
52
+ - `file_path` (required)
53
+ - `pages` (optional, default `all`)
54
+ - `filename` (optional override)
55
+
56
+ Returns API JSON: metadata + per-page text + usage stats.
57
+
58
+ ## CLI
59
+
60
+ ```bash
61
+ pdf-edit extract /path/to/file.pdf
62
+ pdf-edit extract /path/to/file.pdf --pages "1-5"
63
+ pdf-edit status
64
+ pdf-edit setup
65
+ ```
66
+
67
+ CLI contract:
68
+
69
+ - success JSON goes to stdout
70
+ - errors go to stderr
71
+ - exit code `0` success, `1` user error, `2` server/network error
72
+
73
+ ## Setup
74
+
75
+ `pdf-edit setup`:
76
+
77
+ 1. Reads `PDF_EDITOR_API_KEY` (or prompts once if missing)
78
+ 2. Verifies key with `/v1/account/status`
79
+ 3. Patches MCP config for:
80
+ - Claude Desktop
81
+ - Cursor
82
+ - VS Code
83
+ 4. Tries to add Claude Code integration via `claude mcp add-json`
84
+
85
+ Injected server entry:
86
+
87
+ ```json
88
+ {
89
+ "mcpServers": {
90
+ "pdf-agent-kit": {
91
+ "command": "uvx",
92
+ "args": ["pdf-agent-kit"],
93
+ "env": {
94
+ "PDF_EDITOR_API_KEY": "sk_live_..."
95
+ }
96
+ }
97
+ }
98
+ }
99
+ ```
100
+
101
+ ## Development
102
+
103
+ ```bash
104
+ pip install -e .[dev]
105
+ pytest -q
106
+ ```
@@ -0,0 +1,95 @@
1
+ # pdf-agent-kit
2
+
3
+ `pdf-agent-kit` is a local adapter package for the pdf-agent-kit cloud API.
4
+
5
+ - MCP entrypoint: `pdf-agent-kit` (stdio, one tool: `extract_pdf_to_json`)
6
+ - CLI entrypoint: `pdf-edit`
7
+ - Python import: `from pdf_agent_kit import extract`
8
+
9
+ The package is thin by design: it reads a local PDF file, attaches `PDF_EDITOR_API_KEY`, and forwards to the cloud API.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install pdf-agent-kit
15
+ ```
16
+
17
+ or:
18
+
19
+ ```bash
20
+ uvx pdf-agent-kit
21
+ ```
22
+
23
+ ## Environment
24
+
25
+ ```bash
26
+ export PDF_EDITOR_API_KEY="sk_live_..."
27
+ ```
28
+
29
+ If missing, commands fail with:
30
+
31
+ ```text
32
+ PDF_EDITOR_API_KEY not set. Get your key at https://pdfagentkit.com/dashboard
33
+ ```
34
+
35
+ ## MCP Tool
36
+
37
+ Tool name: `extract_pdf_to_json`
38
+
39
+ Parameters:
40
+
41
+ - `file_path` (required)
42
+ - `pages` (optional, default `all`)
43
+ - `filename` (optional override)
44
+
45
+ Returns API JSON: metadata + per-page text + usage stats.
46
+
47
+ ## CLI
48
+
49
+ ```bash
50
+ pdf-edit extract /path/to/file.pdf
51
+ pdf-edit extract /path/to/file.pdf --pages "1-5"
52
+ pdf-edit status
53
+ pdf-edit setup
54
+ ```
55
+
56
+ CLI contract:
57
+
58
+ - success JSON goes to stdout
59
+ - errors go to stderr
60
+ - exit code `0` success, `1` user error, `2` server/network error
61
+
62
+ ## Setup
63
+
64
+ `pdf-edit setup`:
65
+
66
+ 1. Reads `PDF_EDITOR_API_KEY` (or prompts once if missing)
67
+ 2. Verifies key with `/v1/account/status`
68
+ 3. Patches MCP config for:
69
+ - Claude Desktop
70
+ - Cursor
71
+ - VS Code
72
+ 4. Tries to add Claude Code integration via `claude mcp add-json`
73
+
74
+ Injected server entry:
75
+
76
+ ```json
77
+ {
78
+ "mcpServers": {
79
+ "pdf-agent-kit": {
80
+ "command": "uvx",
81
+ "args": ["pdf-agent-kit"],
82
+ "env": {
83
+ "PDF_EDITOR_API_KEY": "sk_live_..."
84
+ }
85
+ }
86
+ }
87
+ }
88
+ ```
89
+
90
+ ## Development
91
+
92
+ ```bash
93
+ pip install -e .[dev]
94
+ pytest -q
95
+ ```
@@ -0,0 +1,32 @@
1
+ [project]
2
+ name = "pdf-agent-kit"
3
+ version = "0.1.0"
4
+ description = "Local MCP + CLI thin adapter for pdf_editor_api."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ dependencies = [
8
+ "fastmcp>=2.0.0",
9
+ "httpx>=0.28.0",
10
+ ]
11
+
12
+ [project.optional-dependencies]
13
+ dev = [
14
+ "pytest>=8.0,<9",
15
+ ]
16
+
17
+ [project.scripts]
18
+ pdf-agent-kit = "pdf_agent_kit.__main__:main"
19
+ pdf-edit = "pdf_agent_kit.cli:main"
20
+
21
+ [build-system]
22
+ requires = ["setuptools>=75", "wheel"]
23
+ build-backend = "setuptools.build_meta"
24
+
25
+ [tool.setuptools.package-dir]
26
+ "" = "src"
27
+
28
+ [tool.setuptools.packages.find]
29
+ where = ["src"]
30
+
31
+ [tool.pytest.ini_options]
32
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,9 @@
1
+ """pdf-agent-kit package."""
2
+
3
+ __all__ = ["__version__"]
4
+
5
+ __version__ = "0.1.0"
6
+
7
+ from pdf_agent_kit.server import extract
8
+
9
+ __all__.append("extract")
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from pdf_agent_kit.server import main as run_stdio_server
4
+
5
+
6
+ def main() -> None:
7
+ run_stdio_server()
8
+
9
+
10
+ if __name__ == "__main__":
11
+ main()
@@ -0,0 +1,100 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import asyncio
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+
9
+ from pdf_agent_kit.client import PdfAgentKitClient, PdfAgentKitError
10
+ from pdf_agent_kit.config import APP_NAME
11
+ from pdf_agent_kit.server import _read_pdf_file
12
+ from pdf_agent_kit.setup_cmd import prompt_for_api_key_if_missing, run_setup
13
+
14
+ EXIT_USER_ERROR = 1
15
+ EXIT_SERVER_ERROR = 2
16
+
17
+
18
+ def build_parser() -> argparse.ArgumentParser:
19
+ parser = argparse.ArgumentParser(prog="pdf-edit", description="CLI adapter for pdf-agent-kit.")
20
+ parser.add_argument("--version", action="version", version=APP_NAME)
21
+ subparsers = parser.add_subparsers(dest="command", required=True)
22
+
23
+ extract_parser = subparsers.add_parser("extract", help="Extract PDF into JSON.")
24
+ extract_parser.add_argument("file", help="Path to PDF file")
25
+ extract_parser.add_argument("--pages", default="all", help="Pages filter, e.g. 1-5 or 1,3,7")
26
+ extract_parser.add_argument("--filename", help="Optional filename override sent to API")
27
+
28
+ subparsers.add_parser("status", help="Validate API key and show remaining credits.")
29
+ subparsers.add_parser("setup", help="Auto-configure MCP clients.")
30
+ return parser
31
+
32
+
33
+ def run_extract(args: argparse.Namespace) -> int:
34
+ file_path = str(Path(args.file).expanduser())
35
+ try:
36
+ api_key = PdfAgentKitClient.require_api_key()
37
+ inferred_filename, content = _read_pdf_file(file_path)
38
+ payload = asyncio.run(
39
+ PdfAgentKitClient.from_env().extract_pdf(
40
+ api_key=api_key,
41
+ filename=(args.filename or inferred_filename).strip() or inferred_filename,
42
+ content=content,
43
+ pages=args.pages,
44
+ )
45
+ )
46
+ except PdfAgentKitError as exc:
47
+ print(str(exc), file=os.sys.stderr)
48
+ return EXIT_SERVER_ERROR if exc.is_server_error else EXIT_USER_ERROR
49
+
50
+ print(json.dumps(payload, ensure_ascii=False))
51
+ return 0
52
+
53
+
54
+ def run_status() -> int:
55
+ try:
56
+ api_key = PdfAgentKitClient.require_api_key()
57
+ payload = asyncio.run(PdfAgentKitClient.from_env().account_status(api_key=api_key))
58
+ except PdfAgentKitError as exc:
59
+ print(str(exc), file=os.sys.stderr)
60
+ return EXIT_SERVER_ERROR if exc.is_server_error else EXIT_USER_ERROR
61
+
62
+ print(json.dumps(payload, ensure_ascii=False))
63
+ return 0
64
+
65
+
66
+ def run_setup_command() -> int:
67
+ try:
68
+ api_key = prompt_for_api_key_if_missing()
69
+ payload = asyncio.run(PdfAgentKitClient.from_env().account_status(api_key=api_key))
70
+ if not payload.get("success"):
71
+ print("Invalid API key. Generate a new one at https://pdfagentkit.com/dashboard", file=os.sys.stderr)
72
+ return EXIT_USER_ERROR
73
+ result = run_setup(api_key)
74
+ except ValueError as exc:
75
+ print(str(exc), file=os.sys.stderr)
76
+ return EXIT_USER_ERROR
77
+ except PdfAgentKitError as exc:
78
+ print(str(exc), file=os.sys.stderr)
79
+ return EXIT_SERVER_ERROR if exc.is_server_error else EXIT_USER_ERROR
80
+
81
+ output = {
82
+ "success": True,
83
+ "configured": result.configured_targets,
84
+ "warnings": result.warnings,
85
+ }
86
+ print(json.dumps(output, ensure_ascii=False))
87
+ return 0
88
+
89
+
90
+ def main(argv: list[str] | None = None) -> int:
91
+ parser = build_parser()
92
+ args = parser.parse_args(argv)
93
+ if args.command == "extract":
94
+ return run_extract(args)
95
+ if args.command == "status":
96
+ return run_status()
97
+ if args.command == "setup":
98
+ return run_setup_command()
99
+ parser.print_help()
100
+ return EXIT_USER_ERROR
@@ -0,0 +1,112 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+ from typing import Any
6
+
7
+ import httpx
8
+
9
+ from pdf_agent_kit.config import API_KEY_ENV_VAR, DASHBOARD_URL, DEFAULT_API_BASE_URL
10
+
11
+ EXTRACT_ENDPOINT = "/v1/extract/json"
12
+ STATUS_ENDPOINT = "/v1/account/status"
13
+
14
+
15
+ class PdfAgentKitError(Exception):
16
+ def __init__(self, message: str, *, is_server_error: bool = False) -> None:
17
+ super().__init__(message)
18
+ self.is_server_error = is_server_error
19
+
20
+
21
+ @dataclass(slots=True)
22
+ class PdfAgentKitClient:
23
+ base_url: str = DEFAULT_API_BASE_URL
24
+ timeout_seconds: float = 120.0
25
+
26
+ @classmethod
27
+ def from_env(cls) -> "PdfAgentKitClient":
28
+ return cls(base_url=os.getenv("PDF_EDITOR_API_BASE_URL", DEFAULT_API_BASE_URL).rstrip("/"))
29
+
30
+ @staticmethod
31
+ def require_api_key() -> str:
32
+ value = os.getenv(API_KEY_ENV_VAR, "").strip()
33
+ if not value:
34
+ raise PdfAgentKitError(f"{API_KEY_ENV_VAR} not set. Get your key at {DASHBOARD_URL}")
35
+ return value
36
+
37
+ async def extract_pdf(self, *, api_key: str, filename: str, content: bytes, pages: str = "all") -> dict[str, Any]:
38
+ timeout = httpx.Timeout(self.timeout_seconds, connect=10.0)
39
+ headers = {"Authorization": f"Bearer {api_key}"}
40
+ files = {"pdf_file": (filename, content, "application/pdf")}
41
+ data = {"pages": pages}
42
+
43
+ try:
44
+ async with httpx.AsyncClient(base_url=self.base_url, timeout=timeout) as client:
45
+ response = await client.post(EXTRACT_ENDPOINT, headers=headers, files=files, data=data)
46
+ except httpx.TimeoutException as exc:
47
+ raise PdfAgentKitError("Server error. Try again in a moment.", is_server_error=True) from exc
48
+ except httpx.RequestError as exc:
49
+ raise PdfAgentKitError("Cannot reach PDF processing server. Check internet connection.", is_server_error=True) from exc
50
+
51
+ return self._handle_response(response)
52
+
53
+ async def account_status(self, *, api_key: str) -> dict[str, Any]:
54
+ timeout = httpx.Timeout(self.timeout_seconds, connect=10.0)
55
+ headers = {"Authorization": f"Bearer {api_key}"}
56
+ try:
57
+ async with httpx.AsyncClient(base_url=self.base_url, timeout=timeout) as client:
58
+ response = await client.get(STATUS_ENDPOINT, headers=headers)
59
+ except httpx.TimeoutException as exc:
60
+ raise PdfAgentKitError("Server error. Try again in a moment.", is_server_error=True) from exc
61
+ except httpx.RequestError as exc:
62
+ raise PdfAgentKitError("Cannot reach PDF processing server. Check internet connection.", is_server_error=True) from exc
63
+
64
+ return self._handle_response(response)
65
+
66
+ def _handle_response(self, response: httpx.Response) -> dict[str, Any]:
67
+ if response.status_code == 401:
68
+ raise PdfAgentKitError(f"Invalid or expired API key. Generate a new one at {DASHBOARD_URL}")
69
+ if response.status_code == 402:
70
+ message = _extract_error_message(response) or "Insufficient credits. Top up at https://pdfagentkit.com/dashboard"
71
+ raise PdfAgentKitError(message)
72
+ if response.status_code == 429:
73
+ raise PdfAgentKitError("Rate limit exceeded. Retry shortly.")
74
+ if response.status_code >= 500:
75
+ raise PdfAgentKitError("Server error. Try again in a moment.", is_server_error=True)
76
+ if response.status_code >= 400:
77
+ message = _extract_error_message(response) or f"Request failed ({response.status_code})."
78
+ raise PdfAgentKitError(message)
79
+
80
+ try:
81
+ payload = response.json()
82
+ except ValueError as exc:
83
+ raise PdfAgentKitError("Server error. Try again in a moment.", is_server_error=True) from exc
84
+
85
+ if not isinstance(payload, dict):
86
+ raise PdfAgentKitError("Server error. Try again in a moment.", is_server_error=True)
87
+ return payload
88
+
89
+
90
+ def _extract_error_message(response: httpx.Response) -> str | None:
91
+ try:
92
+ payload = response.json()
93
+ except ValueError:
94
+ return response.text.strip() or None
95
+
96
+ if isinstance(payload, dict):
97
+ detail = payload.get("detail")
98
+ if isinstance(detail, dict):
99
+ error = detail.get("error")
100
+ if isinstance(error, dict):
101
+ message = error.get("message")
102
+ if isinstance(message, str) and message.strip():
103
+ return message.strip()
104
+ error = payload.get("error")
105
+ if isinstance(error, dict):
106
+ message = error.get("message")
107
+ if isinstance(message, str) and message.strip():
108
+ return message.strip()
109
+ message = payload.get("message")
110
+ if isinstance(message, str) and message.strip():
111
+ return message.strip()
112
+ return None
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import platform
5
+ from pathlib import Path
6
+
7
+ APP_NAME = "pdf-agent-kit"
8
+ CLAUDE_SERVER_NAME = "pdf-agent-kit"
9
+ DEFAULT_API_BASE_URL = "https://pdf-editor-api-production.up.railway.app"
10
+ API_KEY_ENV_VAR = "PDF_EDITOR_API_KEY"
11
+ DASHBOARD_URL = "https://pdfagentkit.com/dashboard"
12
+
13
+
14
+ def _platform_config_root() -> Path:
15
+ if platform.system() == "Darwin":
16
+ return (Path.home() / "Library" / "Application Support").resolve()
17
+ if os.name == "nt":
18
+ return Path(os.getenv("APPDATA", str(Path.home() / "AppData" / "Roaming"))).resolve()
19
+ return Path(os.getenv("XDG_CONFIG_HOME", str(Path.home() / ".config"))).resolve()
20
+
21
+
22
+ def get_claude_config_path() -> Path:
23
+ if platform.system() == "Darwin":
24
+ return (Path.home() / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json").resolve()
25
+ if os.name == "nt":
26
+ return (
27
+ Path(os.getenv("APPDATA", str(Path.home() / "AppData" / "Roaming")))
28
+ / "Claude"
29
+ / "claude_desktop_config.json"
30
+ ).resolve()
31
+
32
+ return (Path.home() / ".config" / "Claude" / "claude_desktop_config.json").resolve()
33
+
34
+
35
+ def get_cursor_config_path() -> Path:
36
+ if os.name == "nt":
37
+ return (_platform_config_root() / "Cursor" / "mcp.json").resolve()
38
+ return (Path.home() / ".cursor" / "mcp.json").resolve()
39
+
40
+
41
+ def get_vscode_config_path() -> Path:
42
+ if os.name == "nt":
43
+ return (_platform_config_root() / "Code" / "User" / "mcp.json").resolve()
44
+ return (Path.home() / ".vscode" / "mcp.json").resolve()
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from fastmcp import FastMCP
9
+
10
+ from pdf_agent_kit.client import PdfAgentKitClient, PdfAgentKitError
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ mcp = FastMCP(
15
+ name="pdf-agent-kit",
16
+ instructions=(
17
+ "Extract structured text from a PDF file. Returns JSON with per-page text content, document metadata "
18
+ "(page count, title, author, scanned detection), and usage stats. Use for invoice processing, contract "
19
+ "analysis, financial statements, resume parsing, and report ingestion. Each page uses 1 credit. Use "
20
+ "the pages parameter (for example '1-5' or '1,3,7') to extract only what you need. Requires "
21
+ "PDF_EDITOR_API_KEY environment variable."
22
+ ),
23
+ )
24
+
25
+
26
+ def _read_pdf_file(file_path: str) -> tuple[str, bytes]:
27
+ if not file_path.strip():
28
+ raise PdfAgentKitError("File path is required.")
29
+
30
+ path = Path(file_path).expanduser()
31
+ if not path.exists():
32
+ raise PdfAgentKitError(f"File not found: {path}")
33
+ if not path.is_file():
34
+ raise PdfAgentKitError(f"File not found: {path}")
35
+
36
+ try:
37
+ content = path.read_bytes()
38
+ except OSError as exc:
39
+ raise PdfAgentKitError(f"File not found: {path}") from exc
40
+
41
+ if not content.startswith(b"%PDF"):
42
+ raise PdfAgentKitError(f"Not a valid PDF: {path}")
43
+
44
+ return path.name, content
45
+
46
+
47
+ def _tool_error(message: str) -> dict[str, Any]:
48
+ return {"success": False, "error": message}
49
+
50
+
51
+ @mcp.tool
52
+ async def extract_pdf_to_json(file_path: str, pages: str = "all", filename: str | None = None) -> dict[str, Any]:
53
+ """Proxy a local PDF file to the cloud pdf-editor-api extraction endpoint."""
54
+
55
+ try:
56
+ api_key = PdfAgentKitClient.require_api_key()
57
+ inferred_filename, content = _read_pdf_file(file_path)
58
+ client = PdfAgentKitClient.from_env()
59
+ return await client.extract_pdf(
60
+ api_key=api_key,
61
+ filename=(filename or inferred_filename).strip() or inferred_filename,
62
+ content=content,
63
+ pages=pages,
64
+ )
65
+ except PdfAgentKitError as exc:
66
+ return _tool_error(str(exc))
67
+ except Exception:
68
+ logger.exception("Unexpected error while extracting PDF through MCP.")
69
+ return _tool_error("Server error. Try again in a moment.")
70
+
71
+
72
+ def extract(file_path: str, *, pages: str = "all", filename: str | None = None) -> dict[str, Any]:
73
+ return asyncio.run(extract_pdf_to_json(file_path=file_path, pages=pages, filename=filename))
74
+
75
+
76
+ def main() -> None:
77
+ mcp.run(transport="stdio", show_banner=False)
@@ -0,0 +1,112 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import shutil
5
+ import subprocess
6
+ import sys
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from pdf_agent_kit.config import (
12
+ API_KEY_ENV_VAR,
13
+ APP_NAME,
14
+ CLAUDE_SERVER_NAME,
15
+ get_claude_config_path,
16
+ get_cursor_config_path,
17
+ get_vscode_config_path,
18
+ )
19
+
20
+
21
+ @dataclass(slots=True)
22
+ class SetupResult:
23
+ configured_targets: list[str]
24
+ warnings: list[str]
25
+
26
+
27
+ def build_server_entry(api_key: str) -> dict[str, Any]:
28
+ return {
29
+ "command": "uvx",
30
+ "args": [APP_NAME],
31
+ "env": {API_KEY_ENV_VAR: api_key},
32
+ }
33
+
34
+
35
+ def merge_mcp_config(existing: dict[str, Any] | None, server_name: str, entry: dict[str, Any]) -> dict[str, Any]:
36
+ payload: dict[str, Any] = dict(existing or {})
37
+ mcp_servers = payload.get("mcpServers")
38
+ if not isinstance(mcp_servers, dict):
39
+ mcp_servers = {}
40
+ else:
41
+ mcp_servers = dict(mcp_servers)
42
+ mcp_servers[server_name] = entry
43
+ payload["mcpServers"] = mcp_servers
44
+ return payload
45
+
46
+
47
+ def run_setup(api_key: str) -> SetupResult:
48
+ warnings: list[str] = []
49
+ configured: list[str] = []
50
+
51
+ entry = build_server_entry(api_key)
52
+ targets = {
53
+ "Claude Desktop": get_claude_config_path(),
54
+ "Cursor": get_cursor_config_path(),
55
+ "VS Code": get_vscode_config_path(),
56
+ }
57
+
58
+ for label, path in targets.items():
59
+ try:
60
+ write_mcp_config(path, CLAUDE_SERVER_NAME, entry)
61
+ configured.append(f"{label}: {path}")
62
+ except Exception as exc: # noqa: BLE001
63
+ warnings.append(f"{label}: failed to update {path} ({exc})")
64
+
65
+ if shutil.which("claude"):
66
+ try:
67
+ payload = json.dumps(entry)
68
+ subprocess.run(
69
+ ["claude", "mcp", "add-json", CLAUDE_SERVER_NAME, payload],
70
+ check=True,
71
+ stdout=subprocess.PIPE,
72
+ stderr=subprocess.PIPE,
73
+ text=True,
74
+ )
75
+ configured.append("Claude Code: added via `claude mcp add-json`")
76
+ except subprocess.CalledProcessError as exc:
77
+ warnings.append(f"Claude Code: failed to add server ({exc.stderr.strip() or exc.stdout.strip()})")
78
+ else:
79
+ warnings.append("Claude Code: `claude` CLI not found, skipped.")
80
+
81
+ return SetupResult(configured_targets=configured, warnings=warnings)
82
+
83
+
84
+ def prompt_for_api_key_if_missing() -> str:
85
+ import os
86
+
87
+ api_key = os.getenv(API_KEY_ENV_VAR, "").strip()
88
+ if api_key:
89
+ return api_key
90
+
91
+ sys.stdout.write(f"{API_KEY_ENV_VAR} is not set. Enter your API key: ")
92
+ sys.stdout.flush()
93
+ value = sys.stdin.readline().strip()
94
+ if not value:
95
+ raise ValueError(f"{API_KEY_ENV_VAR} not set. Get your key at https://pdfagentkit.com/dashboard")
96
+ return value
97
+
98
+
99
+ def write_mcp_config(path: Path, server_name: str, entry: dict[str, Any]) -> None:
100
+ existing = load_json(path)
101
+ merged = merge_mcp_config(existing, server_name, entry)
102
+ path.parent.mkdir(parents=True, exist_ok=True)
103
+ path.write_text(json.dumps(merged, indent=2), encoding="utf-8")
104
+
105
+
106
+ def load_json(path: Path) -> dict[str, Any] | None:
107
+ if not path.exists():
108
+ return None
109
+ payload = json.loads(path.read_text(encoding="utf-8"))
110
+ if not isinstance(payload, dict):
111
+ raise ValueError(f"{path} must contain a JSON object.")
112
+ return payload
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.4
2
+ Name: pdf-agent-kit
3
+ Version: 0.1.0
4
+ Summary: Local MCP + CLI thin adapter for pdf_editor_api.
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: fastmcp>=2.0.0
8
+ Requires-Dist: httpx>=0.28.0
9
+ Provides-Extra: dev
10
+ Requires-Dist: pytest<9,>=8.0; extra == "dev"
11
+
12
+ # pdf-agent-kit
13
+
14
+ `pdf-agent-kit` is a local adapter package for the pdf-agent-kit cloud API.
15
+
16
+ - MCP entrypoint: `pdf-agent-kit` (stdio, one tool: `extract_pdf_to_json`)
17
+ - CLI entrypoint: `pdf-edit`
18
+ - Python import: `from pdf_agent_kit import extract`
19
+
20
+ The package is thin by design: it reads a local PDF file, attaches `PDF_EDITOR_API_KEY`, and forwards to the cloud API.
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pip install pdf-agent-kit
26
+ ```
27
+
28
+ or:
29
+
30
+ ```bash
31
+ uvx pdf-agent-kit
32
+ ```
33
+
34
+ ## Environment
35
+
36
+ ```bash
37
+ export PDF_EDITOR_API_KEY="sk_live_..."
38
+ ```
39
+
40
+ If missing, commands fail with:
41
+
42
+ ```text
43
+ PDF_EDITOR_API_KEY not set. Get your key at https://pdfagentkit.com/dashboard
44
+ ```
45
+
46
+ ## MCP Tool
47
+
48
+ Tool name: `extract_pdf_to_json`
49
+
50
+ Parameters:
51
+
52
+ - `file_path` (required)
53
+ - `pages` (optional, default `all`)
54
+ - `filename` (optional override)
55
+
56
+ Returns API JSON: metadata + per-page text + usage stats.
57
+
58
+ ## CLI
59
+
60
+ ```bash
61
+ pdf-edit extract /path/to/file.pdf
62
+ pdf-edit extract /path/to/file.pdf --pages "1-5"
63
+ pdf-edit status
64
+ pdf-edit setup
65
+ ```
66
+
67
+ CLI contract:
68
+
69
+ - success JSON goes to stdout
70
+ - errors go to stderr
71
+ - exit code `0` success, `1` user error, `2` server/network error
72
+
73
+ ## Setup
74
+
75
+ `pdf-edit setup`:
76
+
77
+ 1. Reads `PDF_EDITOR_API_KEY` (or prompts once if missing)
78
+ 2. Verifies key with `/v1/account/status`
79
+ 3. Patches MCP config for:
80
+ - Claude Desktop
81
+ - Cursor
82
+ - VS Code
83
+ 4. Tries to add Claude Code integration via `claude mcp add-json`
84
+
85
+ Injected server entry:
86
+
87
+ ```json
88
+ {
89
+ "mcpServers": {
90
+ "pdf-agent-kit": {
91
+ "command": "uvx",
92
+ "args": ["pdf-agent-kit"],
93
+ "env": {
94
+ "PDF_EDITOR_API_KEY": "sk_live_..."
95
+ }
96
+ }
97
+ }
98
+ }
99
+ ```
100
+
101
+ ## Development
102
+
103
+ ```bash
104
+ pip install -e .[dev]
105
+ pytest -q
106
+ ```
@@ -0,0 +1,18 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/pdf_agent_kit/__init__.py
4
+ src/pdf_agent_kit/__main__.py
5
+ src/pdf_agent_kit/cli.py
6
+ src/pdf_agent_kit/client.py
7
+ src/pdf_agent_kit/config.py
8
+ src/pdf_agent_kit/server.py
9
+ src/pdf_agent_kit/setup_cmd.py
10
+ src/pdf_agent_kit.egg-info/PKG-INFO
11
+ src/pdf_agent_kit.egg-info/SOURCES.txt
12
+ src/pdf_agent_kit.egg-info/dependency_links.txt
13
+ src/pdf_agent_kit.egg-info/entry_points.txt
14
+ src/pdf_agent_kit.egg-info/requires.txt
15
+ src/pdf_agent_kit.egg-info/top_level.txt
16
+ tests/test_cli.py
17
+ tests/test_server.py
18
+ tests/test_setup_cmd.py
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ pdf-agent-kit = pdf_agent_kit.__main__:main
3
+ pdf-edit = pdf_agent_kit.cli:main
@@ -0,0 +1,5 @@
1
+ fastmcp>=2.0.0
2
+ httpx>=0.28.0
3
+
4
+ [dev]
5
+ pytest<9,>=8.0
@@ -0,0 +1 @@
1
+ pdf_agent_kit
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from pdf_agent_kit import cli
6
+ from pdf_agent_kit.client import PdfAgentKitError
7
+
8
+
9
+ def test_cli_extract_success(monkeypatch, tmp_path: Path, capsys) -> None:
10
+ pdf_path = tmp_path / "sample.pdf"
11
+ pdf_path.write_bytes(b"%PDF-1.7\nhello")
12
+
13
+ monkeypatch.setattr("pdf_agent_kit.cli.PdfAgentKitClient.require_api_key", lambda: "sk_live_123")
14
+ monkeypatch.setattr("pdf_agent_kit.cli._read_pdf_file", lambda path: ("sample.pdf", b"%PDF-1.7\nhello"))
15
+
16
+ async def fake_extract_pdf(*, api_key: str, filename: str, content: bytes, pages: str):
17
+ return {"success": True, "data": {"metadata": {"page_count": 1}}}
18
+
19
+ class FakeClient:
20
+ async def extract_pdf(self, **kwargs):
21
+ return await fake_extract_pdf(**kwargs)
22
+
23
+ monkeypatch.setattr("pdf_agent_kit.cli.PdfAgentKitClient.from_env", lambda: FakeClient())
24
+
25
+ code = cli.main(["extract", str(pdf_path)])
26
+ captured = capsys.readouterr()
27
+ assert code == 0
28
+ assert "\"success\": true" in captured.out.lower()
29
+
30
+
31
+ def test_cli_extract_server_error_returns_exit_2(monkeypatch, tmp_path: Path, capsys) -> None:
32
+ pdf_path = tmp_path / "sample.pdf"
33
+ pdf_path.write_bytes(b"%PDF-1.7\nhello")
34
+ monkeypatch.setattr("pdf_agent_kit.cli.PdfAgentKitClient.require_api_key", lambda: "sk_live_123")
35
+ monkeypatch.setattr("pdf_agent_kit.cli._read_pdf_file", lambda path: ("sample.pdf", b"%PDF-1.7\nhello"))
36
+
37
+ class FakeClient:
38
+ async def extract_pdf(self, **kwargs):
39
+ raise PdfAgentKitError("Server error. Try again in a moment.", is_server_error=True)
40
+
41
+ monkeypatch.setattr("pdf_agent_kit.cli.PdfAgentKitClient.from_env", lambda: FakeClient())
42
+
43
+ code = cli.main(["extract", str(pdf_path)])
44
+ captured = capsys.readouterr()
45
+ assert code == 2
46
+ assert "Server error. Try again in a moment." in captured.err
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from pathlib import Path
5
+
6
+ from pdf_agent_kit.client import PdfAgentKitClient
7
+ from pdf_agent_kit.server import extract_pdf_to_json
8
+
9
+
10
+ def test_extract_pdf_to_json_reads_local_file_and_returns_payload(monkeypatch, tmp_path: Path) -> None:
11
+ pdf_path = tmp_path / "sample.pdf"
12
+ pdf_path.write_bytes(b"%PDF-1.7\nhello")
13
+ monkeypatch.setenv("PDF_EDITOR_API_KEY", "sk_live_123")
14
+
15
+ recorded: dict[str, object] = {}
16
+
17
+ async def fake_extract(
18
+ self: PdfAgentKitClient,
19
+ *,
20
+ api_key: str,
21
+ filename: str,
22
+ content: bytes,
23
+ pages: str = "all",
24
+ ) -> dict:
25
+ recorded["api_key"] = api_key
26
+ recorded["filename"] = filename
27
+ recorded["content"] = content
28
+ recorded["pages"] = pages
29
+ return {"success": True, "data": {"metadata": {"page_count": 1}}}
30
+
31
+ monkeypatch.setattr(PdfAgentKitClient, "extract_pdf", fake_extract)
32
+
33
+ result = asyncio.run(extract_pdf_to_json(file_path=str(pdf_path), pages="1"))
34
+
35
+ assert result["success"] is True
36
+ assert recorded["api_key"] == "sk_live_123"
37
+ assert recorded["filename"] == "sample.pdf"
38
+ assert recorded["content"] == b"%PDF-1.7\nhello"
39
+ assert recorded["pages"] == "1"
40
+
41
+
42
+ def test_extract_pdf_to_json_requires_api_key(monkeypatch, tmp_path: Path) -> None:
43
+ pdf_path = tmp_path / "sample.pdf"
44
+ pdf_path.write_bytes(b"%PDF-1.7\nhello")
45
+ monkeypatch.delenv("PDF_EDITOR_API_KEY", raising=False)
46
+
47
+ result = asyncio.run(extract_pdf_to_json(file_path=str(pdf_path)))
48
+
49
+ assert result["success"] is False
50
+ assert result["error"] == "PDF_EDITOR_API_KEY not set. Get your key at https://pdfagentkit.com/dashboard"
51
+
52
+
53
+ def test_extract_pdf_to_json_rejects_non_pdf_content(monkeypatch, tmp_path: Path) -> None:
54
+ pdf_path = tmp_path / "bad.pdf"
55
+ pdf_path.write_bytes(b"not-a-pdf")
56
+ monkeypatch.setenv("PDF_EDITOR_API_KEY", "sk_live_123")
57
+
58
+ result = asyncio.run(extract_pdf_to_json(file_path=str(pdf_path)))
59
+
60
+ assert result["success"] is False
61
+ assert "Not a valid PDF" in result["error"]
@@ -0,0 +1,28 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from pdf_agent_kit.setup_cmd import build_server_entry, merge_mcp_config, write_mcp_config
6
+
7
+
8
+ def test_merge_mcp_config_preserves_existing_fields(tmp_path: Path) -> None:
9
+ entry = build_server_entry("sk_live_abc")
10
+ merged = merge_mcp_config(
11
+ {"preferences": {"mode": "task"}, "mcpServers": {"existing": {"command": "node"}}},
12
+ "pdf-agent-kit",
13
+ entry,
14
+ )
15
+
16
+ assert merged["preferences"] == {"mode": "task"}
17
+ assert merged["mcpServers"]["existing"]["command"] == "node"
18
+ assert merged["mcpServers"]["pdf-agent-kit"]["command"] == "uvx"
19
+ assert merged["mcpServers"]["pdf-agent-kit"]["args"] == ["pdf-agent-kit"]
20
+
21
+
22
+ def test_write_mcp_config_creates_file(tmp_path: Path) -> None:
23
+ path = tmp_path / "claude_desktop_config.json"
24
+ write_mcp_config(path, "pdf-agent-kit", build_server_entry("sk_live_abc"))
25
+
26
+ content = path.read_text(encoding="utf-8")
27
+ assert "\"pdf-agent-kit\"" in content
28
+ assert "\"PDF_EDITOR_API_KEY\"" in content