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.
- pdf_agent_kit-0.1.0/PKG-INFO +106 -0
- pdf_agent_kit-0.1.0/README.md +95 -0
- pdf_agent_kit-0.1.0/pyproject.toml +32 -0
- pdf_agent_kit-0.1.0/setup.cfg +4 -0
- pdf_agent_kit-0.1.0/src/pdf_agent_kit/__init__.py +9 -0
- pdf_agent_kit-0.1.0/src/pdf_agent_kit/__main__.py +11 -0
- pdf_agent_kit-0.1.0/src/pdf_agent_kit/cli.py +100 -0
- pdf_agent_kit-0.1.0/src/pdf_agent_kit/client.py +112 -0
- pdf_agent_kit-0.1.0/src/pdf_agent_kit/config.py +44 -0
- pdf_agent_kit-0.1.0/src/pdf_agent_kit/server.py +77 -0
- pdf_agent_kit-0.1.0/src/pdf_agent_kit/setup_cmd.py +112 -0
- pdf_agent_kit-0.1.0/src/pdf_agent_kit.egg-info/PKG-INFO +106 -0
- pdf_agent_kit-0.1.0/src/pdf_agent_kit.egg-info/SOURCES.txt +18 -0
- pdf_agent_kit-0.1.0/src/pdf_agent_kit.egg-info/dependency_links.txt +1 -0
- pdf_agent_kit-0.1.0/src/pdf_agent_kit.egg-info/entry_points.txt +3 -0
- pdf_agent_kit-0.1.0/src/pdf_agent_kit.egg-info/requires.txt +5 -0
- pdf_agent_kit-0.1.0/src/pdf_agent_kit.egg-info/top_level.txt +1 -0
- pdf_agent_kit-0.1.0/tests/test_cli.py +46 -0
- pdf_agent_kit-0.1.0/tests/test_server.py +61 -0
- pdf_agent_kit-0.1.0/tests/test_setup_cmd.py +28 -0
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -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
|