convertfilefast-mcp 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,131 @@
1
+ Metadata-Version: 2.4
2
+ Name: convertfilefast-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server for ConvertFileFast — convert 50+ file formats and run PDF/image operations as AI-agent tools.
5
+ Author: ConvertFileFast
6
+ License: MIT
7
+ Keywords: mcp,model-context-protocol,pdf,file-conversion,convert,agents
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: fastmcp>=3.0
11
+ Requires-Dist: httpx>=0.27
12
+
13
+ # ConvertFileFast MCP server
14
+
15
+ An [MCP](https://modelcontextprotocol.io) server that lets AI agents (Claude
16
+ Desktop, Claude Code, Cursor, etc.) convert files and run PDF/image operations
17
+ through the ConvertFileFast API.
18
+
19
+ It is a **separate service** that talks to the ConvertFileFast REST API over
20
+ HTTP — it does not import the backend, so it never affects the API's
21
+ dependencies, and it mirrors how the hosted MCP runs in production.
22
+
23
+ ## Tools
24
+
25
+ | Tool | What it does |
26
+ |------|--------------|
27
+ | `convert_file` | Convert between 40+ format pairs (DOCX→PDF, PDF→CSV, PDF→JPG, HTML→PDF, URL→PDF, PNG→JPG, CSV→JSON, …) |
28
+ | `merge_pdfs` | Merge multiple PDFs into one |
29
+ | `split_pdf` | Extract pages/ranges from a PDF |
30
+ | `compress_pdf` | Reduce PDF size (low/medium/high/maximum) |
31
+ | `rotate_pdf` | Rotate pages (90/180/270°) |
32
+ | `protect_pdf` | Add password protection |
33
+ | `unlock_pdf` | Remove password protection |
34
+ | `resize_image` | Resize an image |
35
+ | `compress_image` | Compress an image (and optionally cap dimensions) |
36
+
37
+ Every tool accepts a public `source_url` (preferred — the agent passes a URL and
38
+ never handles binary) **or** `file_base64`. The result file is written to the
39
+ output directory and the tool returns its path.
40
+
41
+ ## Prerequisites
42
+
43
+ A ConvertFileFast API key (starts with `cff_`). Create one at
44
+ <https://www.convertfilefast.com/signup>.
45
+
46
+ ## Configuration
47
+
48
+ The server reads these environment variables:
49
+
50
+ | Variable | Default | Purpose |
51
+ |----------|---------|---------|
52
+ | `CONVERTFILEFAST_API_KEY` | — | Your `cff_` key (sent as `X-API-Key`) |
53
+ | `CONVERTFILEFAST_API_BASE` | `https://api.convertfilefast.com` | API base URL (set to `http://127.0.0.1:8000` to test against a local backend) |
54
+ | `CONVERTFILEFAST_OUTPUT_DIR` | `~/ConvertFileFast` | Where converted files are saved |
55
+ | `CONVERTFILEFAST_TIMEOUT` | `180` | Per-request timeout (seconds) |
56
+
57
+ ## Install (local, works today)
58
+
59
+ Until the package is published to PyPI, point your MCP client at the local
60
+ virtual environment created for this repo.
61
+
62
+ ### Claude Desktop
63
+
64
+ Edit `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
65
+
66
+ ```json
67
+ {
68
+ "mcpServers": {
69
+ "convertfilefast": {
70
+ "command": "C:\\Projetos\\GitHub-Clones\\conversor-pdf\\mcp\\.venv\\Scripts\\python.exe",
71
+ "args": ["C:\\Projetos\\GitHub-Clones\\conversor-pdf\\mcp\\server.py"],
72
+ "env": {
73
+ "CONVERTFILEFAST_API_KEY": "cff_REPLACE_WITH_YOUR_KEY"
74
+ }
75
+ }
76
+ }
77
+ }
78
+ ```
79
+
80
+ Restart Claude Desktop, then ask: *"Convert https://example.com/report.docx to PDF."*
81
+
82
+ ### Cursor
83
+
84
+ Add the same block to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (per
85
+ project).
86
+
87
+ ### Claude Code
88
+
89
+ ```bash
90
+ claude mcp add convertfilefast \
91
+ --env CONVERTFILEFAST_API_KEY=cff_REPLACE_WITH_YOUR_KEY \
92
+ -- "C:\Projetos\GitHub-Clones\conversor-pdf\mcp\.venv\Scripts\python.exe" \
93
+ "C:\Projetos\GitHub-Clones\conversor-pdf\mcp\server.py"
94
+ ```
95
+
96
+ > **Testing against the local backend:** add
97
+ > `"CONVERTFILEFAST_API_BASE": "http://127.0.0.1:8000"` to the `env` block and
98
+ > start the backend first (`uvicorn app.main:app` from `api/`).
99
+
100
+ ## Future: published install
101
+
102
+ Once published to PyPI, the portable, machine-independent config will be:
103
+
104
+ ```json
105
+ {
106
+ "mcpServers": {
107
+ "convertfilefast": {
108
+ "command": "uvx",
109
+ "args": ["convertfilefast-mcp"],
110
+ "env": { "CONVERTFILEFAST_API_KEY": "cff_..." }
111
+ }
112
+ }
113
+ }
114
+ ```
115
+
116
+ ## Development
117
+
118
+ ```bash
119
+ # from this directory
120
+ uv venv .venv --python 3.12
121
+ uv pip install --python .venv/Scripts/python.exe fastmcp httpx
122
+
123
+ # run (stdio)
124
+ CONVERTFILEFAST_API_KEY=cff_... .venv/Scripts/python.exe server.py
125
+ ```
126
+
127
+ Inspect the tools with the MCP Inspector:
128
+
129
+ ```bash
130
+ npx @modelcontextprotocol/inspector .venv/Scripts/python.exe server.py
131
+ ```
@@ -0,0 +1,119 @@
1
+ # ConvertFileFast MCP server
2
+
3
+ An [MCP](https://modelcontextprotocol.io) server that lets AI agents (Claude
4
+ Desktop, Claude Code, Cursor, etc.) convert files and run PDF/image operations
5
+ through the ConvertFileFast API.
6
+
7
+ It is a **separate service** that talks to the ConvertFileFast REST API over
8
+ HTTP — it does not import the backend, so it never affects the API's
9
+ dependencies, and it mirrors how the hosted MCP runs in production.
10
+
11
+ ## Tools
12
+
13
+ | Tool | What it does |
14
+ |------|--------------|
15
+ | `convert_file` | Convert between 40+ format pairs (DOCX→PDF, PDF→CSV, PDF→JPG, HTML→PDF, URL→PDF, PNG→JPG, CSV→JSON, …) |
16
+ | `merge_pdfs` | Merge multiple PDFs into one |
17
+ | `split_pdf` | Extract pages/ranges from a PDF |
18
+ | `compress_pdf` | Reduce PDF size (low/medium/high/maximum) |
19
+ | `rotate_pdf` | Rotate pages (90/180/270°) |
20
+ | `protect_pdf` | Add password protection |
21
+ | `unlock_pdf` | Remove password protection |
22
+ | `resize_image` | Resize an image |
23
+ | `compress_image` | Compress an image (and optionally cap dimensions) |
24
+
25
+ Every tool accepts a public `source_url` (preferred — the agent passes a URL and
26
+ never handles binary) **or** `file_base64`. The result file is written to the
27
+ output directory and the tool returns its path.
28
+
29
+ ## Prerequisites
30
+
31
+ A ConvertFileFast API key (starts with `cff_`). Create one at
32
+ <https://www.convertfilefast.com/signup>.
33
+
34
+ ## Configuration
35
+
36
+ The server reads these environment variables:
37
+
38
+ | Variable | Default | Purpose |
39
+ |----------|---------|---------|
40
+ | `CONVERTFILEFAST_API_KEY` | — | Your `cff_` key (sent as `X-API-Key`) |
41
+ | `CONVERTFILEFAST_API_BASE` | `https://api.convertfilefast.com` | API base URL (set to `http://127.0.0.1:8000` to test against a local backend) |
42
+ | `CONVERTFILEFAST_OUTPUT_DIR` | `~/ConvertFileFast` | Where converted files are saved |
43
+ | `CONVERTFILEFAST_TIMEOUT` | `180` | Per-request timeout (seconds) |
44
+
45
+ ## Install (local, works today)
46
+
47
+ Until the package is published to PyPI, point your MCP client at the local
48
+ virtual environment created for this repo.
49
+
50
+ ### Claude Desktop
51
+
52
+ Edit `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
53
+
54
+ ```json
55
+ {
56
+ "mcpServers": {
57
+ "convertfilefast": {
58
+ "command": "C:\\Projetos\\GitHub-Clones\\conversor-pdf\\mcp\\.venv\\Scripts\\python.exe",
59
+ "args": ["C:\\Projetos\\GitHub-Clones\\conversor-pdf\\mcp\\server.py"],
60
+ "env": {
61
+ "CONVERTFILEFAST_API_KEY": "cff_REPLACE_WITH_YOUR_KEY"
62
+ }
63
+ }
64
+ }
65
+ }
66
+ ```
67
+
68
+ Restart Claude Desktop, then ask: *"Convert https://example.com/report.docx to PDF."*
69
+
70
+ ### Cursor
71
+
72
+ Add the same block to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (per
73
+ project).
74
+
75
+ ### Claude Code
76
+
77
+ ```bash
78
+ claude mcp add convertfilefast \
79
+ --env CONVERTFILEFAST_API_KEY=cff_REPLACE_WITH_YOUR_KEY \
80
+ -- "C:\Projetos\GitHub-Clones\conversor-pdf\mcp\.venv\Scripts\python.exe" \
81
+ "C:\Projetos\GitHub-Clones\conversor-pdf\mcp\server.py"
82
+ ```
83
+
84
+ > **Testing against the local backend:** add
85
+ > `"CONVERTFILEFAST_API_BASE": "http://127.0.0.1:8000"` to the `env` block and
86
+ > start the backend first (`uvicorn app.main:app` from `api/`).
87
+
88
+ ## Future: published install
89
+
90
+ Once published to PyPI, the portable, machine-independent config will be:
91
+
92
+ ```json
93
+ {
94
+ "mcpServers": {
95
+ "convertfilefast": {
96
+ "command": "uvx",
97
+ "args": ["convertfilefast-mcp"],
98
+ "env": { "CONVERTFILEFAST_API_KEY": "cff_..." }
99
+ }
100
+ }
101
+ }
102
+ ```
103
+
104
+ ## Development
105
+
106
+ ```bash
107
+ # from this directory
108
+ uv venv .venv --python 3.12
109
+ uv pip install --python .venv/Scripts/python.exe fastmcp httpx
110
+
111
+ # run (stdio)
112
+ CONVERTFILEFAST_API_KEY=cff_... .venv/Scripts/python.exe server.py
113
+ ```
114
+
115
+ Inspect the tools with the MCP Inspector:
116
+
117
+ ```bash
118
+ npx @modelcontextprotocol/inspector .venv/Scripts/python.exe server.py
119
+ ```
@@ -0,0 +1,131 @@
1
+ Metadata-Version: 2.4
2
+ Name: convertfilefast-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server for ConvertFileFast — convert 50+ file formats and run PDF/image operations as AI-agent tools.
5
+ Author: ConvertFileFast
6
+ License: MIT
7
+ Keywords: mcp,model-context-protocol,pdf,file-conversion,convert,agents
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: fastmcp>=3.0
11
+ Requires-Dist: httpx>=0.27
12
+
13
+ # ConvertFileFast MCP server
14
+
15
+ An [MCP](https://modelcontextprotocol.io) server that lets AI agents (Claude
16
+ Desktop, Claude Code, Cursor, etc.) convert files and run PDF/image operations
17
+ through the ConvertFileFast API.
18
+
19
+ It is a **separate service** that talks to the ConvertFileFast REST API over
20
+ HTTP — it does not import the backend, so it never affects the API's
21
+ dependencies, and it mirrors how the hosted MCP runs in production.
22
+
23
+ ## Tools
24
+
25
+ | Tool | What it does |
26
+ |------|--------------|
27
+ | `convert_file` | Convert between 40+ format pairs (DOCX→PDF, PDF→CSV, PDF→JPG, HTML→PDF, URL→PDF, PNG→JPG, CSV→JSON, …) |
28
+ | `merge_pdfs` | Merge multiple PDFs into one |
29
+ | `split_pdf` | Extract pages/ranges from a PDF |
30
+ | `compress_pdf` | Reduce PDF size (low/medium/high/maximum) |
31
+ | `rotate_pdf` | Rotate pages (90/180/270°) |
32
+ | `protect_pdf` | Add password protection |
33
+ | `unlock_pdf` | Remove password protection |
34
+ | `resize_image` | Resize an image |
35
+ | `compress_image` | Compress an image (and optionally cap dimensions) |
36
+
37
+ Every tool accepts a public `source_url` (preferred — the agent passes a URL and
38
+ never handles binary) **or** `file_base64`. The result file is written to the
39
+ output directory and the tool returns its path.
40
+
41
+ ## Prerequisites
42
+
43
+ A ConvertFileFast API key (starts with `cff_`). Create one at
44
+ <https://www.convertfilefast.com/signup>.
45
+
46
+ ## Configuration
47
+
48
+ The server reads these environment variables:
49
+
50
+ | Variable | Default | Purpose |
51
+ |----------|---------|---------|
52
+ | `CONVERTFILEFAST_API_KEY` | — | Your `cff_` key (sent as `X-API-Key`) |
53
+ | `CONVERTFILEFAST_API_BASE` | `https://api.convertfilefast.com` | API base URL (set to `http://127.0.0.1:8000` to test against a local backend) |
54
+ | `CONVERTFILEFAST_OUTPUT_DIR` | `~/ConvertFileFast` | Where converted files are saved |
55
+ | `CONVERTFILEFAST_TIMEOUT` | `180` | Per-request timeout (seconds) |
56
+
57
+ ## Install (local, works today)
58
+
59
+ Until the package is published to PyPI, point your MCP client at the local
60
+ virtual environment created for this repo.
61
+
62
+ ### Claude Desktop
63
+
64
+ Edit `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
65
+
66
+ ```json
67
+ {
68
+ "mcpServers": {
69
+ "convertfilefast": {
70
+ "command": "C:\\Projetos\\GitHub-Clones\\conversor-pdf\\mcp\\.venv\\Scripts\\python.exe",
71
+ "args": ["C:\\Projetos\\GitHub-Clones\\conversor-pdf\\mcp\\server.py"],
72
+ "env": {
73
+ "CONVERTFILEFAST_API_KEY": "cff_REPLACE_WITH_YOUR_KEY"
74
+ }
75
+ }
76
+ }
77
+ }
78
+ ```
79
+
80
+ Restart Claude Desktop, then ask: *"Convert https://example.com/report.docx to PDF."*
81
+
82
+ ### Cursor
83
+
84
+ Add the same block to `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (per
85
+ project).
86
+
87
+ ### Claude Code
88
+
89
+ ```bash
90
+ claude mcp add convertfilefast \
91
+ --env CONVERTFILEFAST_API_KEY=cff_REPLACE_WITH_YOUR_KEY \
92
+ -- "C:\Projetos\GitHub-Clones\conversor-pdf\mcp\.venv\Scripts\python.exe" \
93
+ "C:\Projetos\GitHub-Clones\conversor-pdf\mcp\server.py"
94
+ ```
95
+
96
+ > **Testing against the local backend:** add
97
+ > `"CONVERTFILEFAST_API_BASE": "http://127.0.0.1:8000"` to the `env` block and
98
+ > start the backend first (`uvicorn app.main:app` from `api/`).
99
+
100
+ ## Future: published install
101
+
102
+ Once published to PyPI, the portable, machine-independent config will be:
103
+
104
+ ```json
105
+ {
106
+ "mcpServers": {
107
+ "convertfilefast": {
108
+ "command": "uvx",
109
+ "args": ["convertfilefast-mcp"],
110
+ "env": { "CONVERTFILEFAST_API_KEY": "cff_..." }
111
+ }
112
+ }
113
+ }
114
+ ```
115
+
116
+ ## Development
117
+
118
+ ```bash
119
+ # from this directory
120
+ uv venv .venv --python 3.12
121
+ uv pip install --python .venv/Scripts/python.exe fastmcp httpx
122
+
123
+ # run (stdio)
124
+ CONVERTFILEFAST_API_KEY=cff_... .venv/Scripts/python.exe server.py
125
+ ```
126
+
127
+ Inspect the tools with the MCP Inspector:
128
+
129
+ ```bash
130
+ npx @modelcontextprotocol/inspector .venv/Scripts/python.exe server.py
131
+ ```
@@ -0,0 +1,9 @@
1
+ README.md
2
+ pyproject.toml
3
+ server.py
4
+ convertfilefast_mcp.egg-info/PKG-INFO
5
+ convertfilefast_mcp.egg-info/SOURCES.txt
6
+ convertfilefast_mcp.egg-info/dependency_links.txt
7
+ convertfilefast_mcp.egg-info/entry_points.txt
8
+ convertfilefast_mcp.egg-info/requires.txt
9
+ convertfilefast_mcp.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ convertfilefast-mcp = server:main
@@ -0,0 +1,2 @@
1
+ fastmcp>=3.0
2
+ httpx>=0.27
@@ -0,0 +1,23 @@
1
+ [project]
2
+ name = "convertfilefast-mcp"
3
+ version = "0.1.0"
4
+ description = "MCP server for ConvertFileFast — convert 50+ file formats and run PDF/image operations as AI-agent tools."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = { text = "MIT" }
8
+ authors = [{ name = "ConvertFileFast" }]
9
+ keywords = ["mcp", "model-context-protocol", "pdf", "file-conversion", "convert", "agents"]
10
+ dependencies = [
11
+ "fastmcp>=3.0",
12
+ "httpx>=0.27",
13
+ ]
14
+
15
+ [project.scripts]
16
+ convertfilefast-mcp = "server:main"
17
+
18
+ [build-system]
19
+ requires = ["setuptools>=68"]
20
+ build-backend = "setuptools.build_meta"
21
+
22
+ [tool.setuptools]
23
+ py-modules = ["server"]
@@ -0,0 +1,355 @@
1
+ """ConvertFileFast MCP server.
2
+
3
+ Exposes file conversion as tools for AI agents (Claude, Cursor, ChatGPT, ...).
4
+
5
+ This is a *separate service* from the REST backend: it talks to the
6
+ ConvertFileFast HTTP API and never imports the backend (keeps the backend's
7
+ dependencies untouched and matches how the hosted MCP runs in production).
8
+
9
+ Auth: the caller's ConvertFileFast API key (prefix ``cff_``), taken from the
10
+ ``CONVERTFILEFAST_API_KEY`` environment variable and sent as ``X-API-Key``.
11
+
12
+ Configuration (env vars):
13
+ CONVERTFILEFAST_API_BASE API base URL (default: production API)
14
+ CONVERTFILEFAST_API_KEY the cff_ key used for requests
15
+ CONVERTFILEFAST_OUTPUT_DIR where converted files are written (default ~/ConvertFileFast)
16
+ CONVERTFILEFAST_TIMEOUT per-request timeout in seconds (default 180)
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import base64
22
+ import os
23
+ import re
24
+ from pathlib import Path
25
+ from typing import Annotated, Literal, Optional
26
+ from urllib.parse import urlparse
27
+
28
+ import httpx
29
+ from fastmcp import FastMCP
30
+ from pydantic import Field
31
+
32
+ API_BASE = os.environ.get("CONVERTFILEFAST_API_BASE", "https://api.convertfilefast.com").rstrip("/")
33
+ API_KEY = os.environ.get("CONVERTFILEFAST_API_KEY", "")
34
+ OUTPUT_DIR = Path(os.environ.get("CONVERTFILEFAST_OUTPUT_DIR", str(Path.home() / "ConvertFileFast")))
35
+ HTTP_TIMEOUT = float(os.environ.get("CONVERTFILEFAST_TIMEOUT", "180"))
36
+
37
+ mcp = FastMCP(
38
+ "ConvertFileFast",
39
+ instructions=(
40
+ "Convert files between 50+ formats and run PDF/image operations "
41
+ "(merge, split, compress, rotate, protect, unlock, resize). Prefer "
42
+ "passing a public URL via `source_url`; otherwise pass base64. The "
43
+ "result file is saved locally and the tool returns its path."
44
+ ),
45
+ )
46
+
47
+ # --- Conversion matrix (mirrors the backend's /v2/convert/{from}-to-{to}) ---
48
+ _DOC_TO_PDF = ["docx", "doc", "xlsx", "pptx", "ppt", "rtf", "odt", "txt", "csv"]
49
+ _WEB_TO_PDF = ["html", "markdown", "url"]
50
+ _IMG_TO_PDF = ["jpg", "png", "heic", "webp", "avif", "svg", "bmp", "tiff"]
51
+ _PDF_TARGETS = ["docx", "txt", "csv", "xlsx", "jpg", "png"]
52
+ _IMG_TO_IMG = [
53
+ "jpg-to-png", "png-to-jpg", "jpg-to-webp", "png-to-webp",
54
+ "heic-to-jpg", "heic-to-png", "webp-to-jpg", "webp-to-png",
55
+ "avif-to-jpg", "avif-to-png", "svg-to-png", "svg-to-jpg",
56
+ "bmp-to-jpg", "bmp-to-png", "tiff-to-jpg", "tiff-to-png",
57
+ ]
58
+ _DATA = ["csv-to-json", "json-to-csv", "csv-to-xlsx", "xlsx-to-csv"]
59
+
60
+ VALID_SLUGS: set[str] = set(_IMG_TO_IMG) | set(_DATA)
61
+ for _s in _DOC_TO_PDF + _WEB_TO_PDF + _IMG_TO_PDF:
62
+ VALID_SLUGS.add(f"{_s}-to-pdf")
63
+ for _t in _PDF_TARGETS:
64
+ VALID_SLUGS.add(f"pdf-to-{_t}")
65
+
66
+ _EXT_ALIAS = {"jpeg": "jpg", "tif": "tiff", "md": "markdown", "htm": "html", "text": "txt"}
67
+
68
+ TargetFormat = Literal["pdf", "docx", "xlsx", "csv", "txt", "json", "jpg", "png", "webp"]
69
+
70
+
71
+ # --------------------------------------------------------------------------- #
72
+ # Helpers
73
+ # --------------------------------------------------------------------------- #
74
+ def _client() -> httpx.Client:
75
+ headers = {"User-Agent": "convertfilefast-mcp"}
76
+ if API_KEY:
77
+ headers["X-API-Key"] = API_KEY
78
+ return httpx.Client(base_url=API_BASE, headers=headers, timeout=HTTP_TIMEOUT)
79
+
80
+
81
+ def _formval(v: object) -> str:
82
+ if isinstance(v, bool):
83
+ return "true" if v else "false"
84
+ return str(v)
85
+
86
+
87
+ def _filename_from_response(resp: httpx.Response, fallback: str) -> str:
88
+ cd = resp.headers.get("content-disposition", "")
89
+ m = re.search(r"filename\*?=(?:UTF-8'')?\"?([^\";]+)\"?", cd)
90
+ return os.path.basename(m.group(1)) if m else fallback
91
+
92
+
93
+ def _deliver(content: bytes, out_name: str) -> dict:
94
+ OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
95
+ out_path = OUTPUT_DIR / out_name
96
+ out_path.write_bytes(content)
97
+ return {"status": "success", "output_path": str(out_path), "filename": out_name, "bytes": len(content)}
98
+
99
+
100
+ def _error(resp: httpx.Response) -> dict:
101
+ try:
102
+ detail = resp.json().get("detail", resp.text[:300])
103
+ except Exception:
104
+ detail = resp.text[:300]
105
+ hint = None
106
+ if resp.status_code in (401, 403):
107
+ hint = "Set a valid CONVERTFILEFAST_API_KEY (cff_...)."
108
+ elif resp.status_code == 429:
109
+ hint = "Rate or credit limit reached. Add credits or slow down."
110
+ return {"status": "error", "http_status": resp.status_code, "detail": detail, "hint": hint}
111
+
112
+
113
+ def _send(
114
+ endpoint: str,
115
+ *,
116
+ default_out: str,
117
+ source_url: Optional[str] = None,
118
+ file_base64: Optional[str] = None,
119
+ filename: Optional[str] = None,
120
+ extra: Optional[dict] = None,
121
+ ) -> dict:
122
+ """POST to a conversion/operation endpoint with url-or-upload input."""
123
+ data: dict[str, str] = {k: _formval(v) for k, v in (extra or {}).items() if v is not None}
124
+ files = None
125
+ if source_url:
126
+ data["url"] = source_url
127
+ elif file_base64:
128
+ try:
129
+ raw = base64.b64decode(file_base64)
130
+ except Exception:
131
+ return {"status": "error", "detail": "file_base64 is not valid base64."}
132
+ files = {"file": (filename or "input", raw)}
133
+ else:
134
+ return {"status": "error", "detail": "Provide either source_url or file_base64."}
135
+
136
+ try:
137
+ with _client() as c:
138
+ resp = c.post(endpoint, data=data, files=files)
139
+ except httpx.RequestError as e:
140
+ return {"status": "error", "detail": f"Request to API failed: {e}"}
141
+
142
+ if resp.status_code != 200:
143
+ return _error(resp)
144
+ return _deliver(resp.content, _filename_from_response(resp, default_out))
145
+
146
+
147
+ def _guess_source_format(source_url: Optional[str], filename: Optional[str], target: str) -> Optional[str]:
148
+ name = filename or (urlparse(source_url).path if source_url else "")
149
+ ext = Path(name).suffix.lower().lstrip(".")
150
+ ext = _EXT_ALIAS.get(ext, ext)
151
+ if ext:
152
+ return ext
153
+ if source_url and target == "pdf":
154
+ return "url" # a URL with no file extension, to PDF, is a webpage
155
+ return None
156
+
157
+
158
+ # --------------------------------------------------------------------------- #
159
+ # Tools
160
+ # --------------------------------------------------------------------------- #
161
+ @mcp.tool(annotations={"idempotentHint": True, "openWorldHint": True})
162
+ def convert_file(
163
+ target_format: Annotated[TargetFormat, Field(description="Desired output format, e.g. 'pdf', 'csv', 'png'.")],
164
+ source_url: Annotated[Optional[str], Field(description="Public URL to the source file or webpage (preferred). For a webpage to PDF, point at the page and use target_format='pdf'.")] = None,
165
+ file_base64: Annotated[Optional[str], Field(description="Base64-encoded source file. Use only when there is no URL.")] = None,
166
+ filename: Annotated[Optional[str], Field(description="Original filename (with extension); used to detect the source format and name the output.")] = None,
167
+ source_format: Annotated[Optional[str], Field(description="Source format token (e.g. 'docx', 'pdf', 'png', 'html', 'url'). Inferred from filename/URL if omitted.")] = None,
168
+ ) -> dict:
169
+ """Convert a document, spreadsheet, presentation, image, PDF, or data file to another format.
170
+
171
+ Supports 40+ conversions, e.g. DOCX->PDF, PDF->CSV, PDF->JPG, HTML->PDF,
172
+ URL(webpage)->PDF, PNG->JPG, CSV->JSON. Pass a public `source_url` (preferred)
173
+ or `file_base64`. Returns the local path of the converted file.
174
+ """
175
+ src = (source_format or _guess_source_format(source_url, filename, target_format) or "").lower()
176
+ src = _EXT_ALIAS.get(src, src)
177
+ if not src:
178
+ return {"status": "error", "detail": "Could not determine the source format. Pass `source_format` (e.g. 'docx') or a `filename` with an extension."}
179
+
180
+ slug = f"{src}-to-{target_format}"
181
+ if slug not in VALID_SLUGS:
182
+ valid_targets = sorted(s.split("-to-")[1] for s in VALID_SLUGS if s.startswith(f"{src}-to-"))
183
+ return {"status": "error", "detail": f"Conversion '{slug}' is not supported.", "supported_targets_for_source": valid_targets or None}
184
+
185
+ stem = Path(filename).stem if filename else "converted"
186
+ return _send(f"/v2/convert/{slug}", default_out=f"{stem}.{target_format}",
187
+ source_url=source_url, file_base64=file_base64, filename=filename)
188
+
189
+
190
+ @mcp.tool(annotations={"idempotentHint": True, "openWorldHint": True})
191
+ def merge_pdfs(
192
+ source_urls: Annotated[Optional[list[str]], Field(description="Public URLs of the PDFs to merge, in order (minimum 2).")] = None,
193
+ files_base64: Annotated[Optional[list[str]], Field(description="Base64-encoded PDFs to merge, in order (minimum 2). Use when there are no URLs.")] = None,
194
+ output_name: Annotated[str, Field(description="Name for the merged PDF.")] = "merged.pdf",
195
+ ) -> dict:
196
+ """Merge multiple PDFs into a single PDF, preserving the given order."""
197
+ blobs: list[tuple[str, bytes]] = []
198
+ if files_base64:
199
+ for i, b in enumerate(files_base64):
200
+ try:
201
+ blobs.append((f"part{i + 1}.pdf", base64.b64decode(b)))
202
+ except Exception:
203
+ return {"status": "error", "detail": f"files_base64[{i}] is not valid base64."}
204
+ elif source_urls:
205
+ try:
206
+ with httpx.Client(timeout=HTTP_TIMEOUT, follow_redirects=True) as fetch:
207
+ for i, u in enumerate(source_urls):
208
+ rr = fetch.get(u)
209
+ if rr.status_code != 200:
210
+ return {"status": "error", "detail": f"Could not fetch {u} (HTTP {rr.status_code})."}
211
+ blobs.append((f"part{i + 1}.pdf", rr.content))
212
+ except httpx.RequestError as e:
213
+ return {"status": "error", "detail": f"Failed to fetch a URL: {e}"}
214
+ else:
215
+ return {"status": "error", "detail": "Provide source_urls or files_base64 (at least 2 PDFs)."}
216
+
217
+ if len(blobs) < 2:
218
+ return {"status": "error", "detail": "Need at least 2 PDFs to merge."}
219
+
220
+ files = [("files", (name, data)) for name, data in blobs]
221
+ try:
222
+ with _client() as c:
223
+ resp = c.post("/v2/pdf/merge", files=files)
224
+ except httpx.RequestError as e:
225
+ return {"status": "error", "detail": f"Request to API failed: {e}"}
226
+ if resp.status_code != 200:
227
+ return _error(resp)
228
+ return _deliver(resp.content, _filename_from_response(resp, output_name))
229
+
230
+
231
+ @mcp.tool(annotations={"idempotentHint": True, "openWorldHint": True})
232
+ def split_pdf(
233
+ pages: Annotated[str, Field(description="Pages/ranges to extract, e.g. '1,3,5-7' or '1-5'.")],
234
+ source_url: Annotated[Optional[str], Field(description="Public URL of the PDF.")] = None,
235
+ file_base64: Annotated[Optional[str], Field(description="Base64-encoded PDF.")] = None,
236
+ ) -> dict:
237
+ """Extract specific pages/ranges from a PDF (returns the resulting file(s))."""
238
+ return _send("/v2/pdf/split", default_out="split.zip", source_url=source_url,
239
+ file_base64=file_base64, extra={"pages": pages})
240
+
241
+
242
+ @mcp.tool(annotations={"idempotentHint": True, "openWorldHint": True})
243
+ def compress_pdf(
244
+ source_url: Annotated[Optional[str], Field(description="Public URL of the PDF.")] = None,
245
+ file_base64: Annotated[Optional[str], Field(description="Base64-encoded PDF.")] = None,
246
+ quality: Annotated[Literal["low", "medium", "high", "maximum"], Field(description="Compression preset (more compression = smaller file).")] = "medium",
247
+ remove_metadata: Annotated[bool, Field(description="Strip PDF metadata to reduce size further.")] = False,
248
+ ) -> dict:
249
+ """Reduce the file size of a PDF."""
250
+ return _send("/v2/pdf/compress", default_out="compressed.pdf", source_url=source_url,
251
+ file_base64=file_base64, extra={"quality": quality, "remove_metadata": remove_metadata})
252
+
253
+
254
+ @mcp.tool(annotations={"idempotentHint": True, "openWorldHint": True})
255
+ def rotate_pdf(
256
+ angle: Annotated[Literal[90, 180, 270], Field(description="Rotation in degrees (clockwise).")],
257
+ source_url: Annotated[Optional[str], Field(description="Public URL of the PDF.")] = None,
258
+ file_base64: Annotated[Optional[str], Field(description="Base64-encoded PDF.")] = None,
259
+ pages: Annotated[Optional[str], Field(description="Pages to rotate, e.g. '1,3,5-7'. Omit to rotate all pages.")] = None,
260
+ ) -> dict:
261
+ """Rotate pages in a PDF."""
262
+ return _send("/v2/pdf/rotate", default_out="rotated.pdf", source_url=source_url,
263
+ file_base64=file_base64, extra={"angle": angle, "pages": pages})
264
+
265
+
266
+ @mcp.tool(annotations={"idempotentHint": True, "openWorldHint": True})
267
+ def protect_pdf(
268
+ password: Annotated[str, Field(description="Password to set on the PDF.")],
269
+ source_url: Annotated[Optional[str], Field(description="Public URL of the PDF.")] = None,
270
+ file_base64: Annotated[Optional[str], Field(description="Base64-encoded PDF.")] = None,
271
+ ) -> dict:
272
+ """Add password protection (encryption) to a PDF."""
273
+ return _send("/v2/pdf/protect", default_out="protected.pdf", source_url=source_url,
274
+ file_base64=file_base64, extra={"password": password})
275
+
276
+
277
+ @mcp.tool(annotations={"idempotentHint": True, "openWorldHint": True})
278
+ def unlock_pdf(
279
+ password: Annotated[str, Field(description="Current password of the PDF.")],
280
+ source_url: Annotated[Optional[str], Field(description="Public URL of the PDF.")] = None,
281
+ file_base64: Annotated[Optional[str], Field(description="Base64-encoded PDF.")] = None,
282
+ ) -> dict:
283
+ """Remove password protection from a PDF (requires the current password)."""
284
+ return _send("/v2/pdf/unlock", default_out="unlocked.pdf", source_url=source_url,
285
+ file_base64=file_base64, extra={"password": password})
286
+
287
+
288
+ @mcp.tool(annotations={"idempotentHint": True, "openWorldHint": True})
289
+ def resize_image(
290
+ source_url: Annotated[Optional[str], Field(description="Public URL of the image.")] = None,
291
+ file_base64: Annotated[Optional[str], Field(description="Base64-encoded image.")] = None,
292
+ width: Annotated[Optional[int], Field(description="Target width in pixels.")] = None,
293
+ height: Annotated[Optional[int], Field(description="Target height in pixels.")] = None,
294
+ fit: Annotated[Literal["contain", "cover", "fill"], Field(description="How to fit within width/height.")] = "contain",
295
+ format: Annotated[Optional[str], Field(description="Output format (jpeg, png, webp, ...). Defaults to source format.")] = None,
296
+ ) -> dict:
297
+ """Resize an image to the given width/height."""
298
+ if width is None and height is None:
299
+ return {"status": "error", "detail": "Provide at least one of width or height."}
300
+ return _send("/v2/image/resize", default_out="resized.png", source_url=source_url,
301
+ file_base64=file_base64, extra={"width": width, "height": height, "fit": fit, "format": format})
302
+
303
+
304
+ @mcp.tool(annotations={"idempotentHint": True, "openWorldHint": True})
305
+ def compress_image(
306
+ source_url: Annotated[Optional[str], Field(description="Public URL of the image.")] = None,
307
+ file_base64: Annotated[Optional[str], Field(description="Base64-encoded image.")] = None,
308
+ quality: Annotated[int, Field(description="Compression quality 1-100 (higher = better quality, larger file).", ge=1, le=100)] = 85,
309
+ format: Annotated[Optional[str], Field(description="Output format (jpeg, png, webp, ...). Defaults to source format.")] = None,
310
+ max_width: Annotated[Optional[int], Field(description="Optionally cap the width in pixels.")] = None,
311
+ max_height: Annotated[Optional[int], Field(description="Optionally cap the height in pixels.")] = None,
312
+ ) -> dict:
313
+ """Compress an image to reduce file size, optionally capping its dimensions."""
314
+ return _send("/v2/image/compress", default_out="compressed.png", source_url=source_url,
315
+ file_base64=file_base64,
316
+ extra={"quality": quality, "format": format, "max_width": max_width, "max_height": max_height})
317
+
318
+
319
+ @mcp.tool(annotations={"idempotentHint": False, "openWorldHint": True})
320
+ def report_issue(
321
+ summary: Annotated[str, Field(description="What went wrong or what's missing. Be specific.")],
322
+ category: Annotated[
323
+ Literal["bug", "missing_conversion", "quality", "other"],
324
+ Field(description="Type of feedback."),
325
+ ] = "other",
326
+ tool: Annotated[Optional[str], Field(description="Which tool/conversion this is about, e.g. 'convert_file docx-to-pdf'.")] = None,
327
+ context: Annotated[Optional[str], Field(description="Extra detail: error message, input format, expected vs actual output.")] = None,
328
+ ) -> dict:
329
+ """Report a bug, missing conversion, or quality issue with ConvertFileFast to the maintainers.
330
+
331
+ Use this when a conversion fails, a format you need isn't supported, or the
332
+ output quality is poor. Your report goes into the team's development backlog.
333
+ """
334
+ data: dict[str, str] = {"summary": summary, "category": category}
335
+ if tool:
336
+ data["tool"] = tool
337
+ if context:
338
+ data["context"] = context
339
+ try:
340
+ with _client() as c:
341
+ resp = c.post("/v2/feedback", data=data)
342
+ except httpx.RequestError as e:
343
+ return {"status": "error", "detail": f"Could not send feedback: {e}"}
344
+ if resp.status_code != 200:
345
+ return _error(resp)
346
+ return {"status": "received", "message": "Thanks — your report was logged for the ConvertFileFast team."}
347
+
348
+
349
+ def main() -> None:
350
+ """Console-script / module entry point. Runs the stdio MCP server."""
351
+ mcp.run() # stdio transport by default
352
+
353
+
354
+ if __name__ == "__main__":
355
+ main()
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+