file-convert-mcp 0.1.2__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,30 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+
8
+ permissions:
9
+ contents: read
10
+ id-token: write
11
+
12
+ jobs:
13
+ publish:
14
+ name: Build and publish to PyPI
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v4
18
+
19
+ - name: Set up Python
20
+ uses: actions/setup-python@v5
21
+ with:
22
+ python-version: "3.12"
23
+
24
+ - name: Build package
25
+ run: |
26
+ python -m pip install --upgrade build
27
+ python -m build
28
+
29
+ - name: Publish package to PyPI
30
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,7 @@
1
+ .env
2
+ .venv
3
+ uv.lock
4
+ __pycache__/
5
+ .pytest_cache/
6
+ dist/
7
+ *.egg-info/
@@ -0,0 +1,25 @@
1
+ # Generated by https://smithery.ai. See: https://smithery.ai/docs/build/project-config
2
+ FROM python:3.12-alpine
3
+
4
+ # Install runtime and build dependencies
5
+ RUN apk add --no-cache file build-base libffi-dev openssl-dev
6
+
7
+ WORKDIR /app
8
+
9
+ # Copy project files
10
+ COPY pyproject.toml .
11
+ COPY src ./src
12
+ COPY LICENSE README.md ./
13
+
14
+ # Install package
15
+ RUN pip install --no-cache-dir .
16
+
17
+ # Expose port 8000 for HTTP transport
18
+ EXPOSE 8000
19
+
20
+ # Set default environment variables
21
+ ENV TRANSPORT=http
22
+ ENV PORT=8000
23
+
24
+ # Set default command
25
+ CMD ["file-convert-mcp"]
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 convert.guru
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,223 @@
1
+ Metadata-Version: 2.5
2
+ Name: file-convert-mcp
3
+ Version: 0.1.2
4
+ Summary: MCP tool for file conversion and file format detection by Convert.Guru
5
+ Project-URL: Homepage, https://convert.guru
6
+ Project-URL: Repository, https://github.com/convertguru/file-convert-mcp
7
+ License: MIT
8
+ License-File: LICENSE
9
+ Requires-Python: >=3.12
10
+ Requires-Dist: aiohttp
11
+ Requires-Dist: fastmcp<3,>=2.14.7
12
+ Requires-Dist: python-dotenv
13
+ Requires-Dist: python-magic-bin==0.4.14; sys_platform == 'win32'
14
+ Requires-Dist: python-magic; sys_platform != 'win32'
15
+ Requires-Dist: starlette
16
+ Description-Content-Type: text/markdown
17
+
18
+ # File Convert MCP Server
19
+ [![smithery badge](https://smithery.ai/badge/@convertguru/file-convert-mcp)](https://smithery.ai/server/@convertguru/file-convert-mcp)
20
+
21
+ A Model Context Protocol (MCP) server for converting files between various formats.
22
+
23
+ File Convert is an MCP server designed to handle the conversion of a wide array of file formats, including images, office documents, audio, video, text, and data files. It aims to provide seamless transformation into popular formats such as PDF, JPG, MP4, and HTML, among others. This server offers a set of powerful tools to convert diverse file types into easily accessible and widely compatible formats like PDF, JPG, PNG, TXT.
24
+
25
+ ## ✨ Features
26
+
27
+ - **Intelligent File Type Detection:** Employs a combination of AI, TrID, and magic bytes for accurate file type identification.
28
+ - **Versatile File Conversion:** Supports conversion between numerous file types and the following popular formats:
29
+ - **Images:** Handles a vast range of formats, including many legacy ones. Converts to PDF, JPG, PNG.
30
+ - **Office Documents:** Converts to PDF, TXT, HTML.
31
+ - **Audio/Video:** Converts to MP3, MP4.
32
+ - **Databases:** Converts to CSV.
33
+ - **Various Files:** Offers conversion capabilities for other file types as well.
34
+
35
+ ## 🛠️ Requirements
36
+
37
+ 1. **Python:** Version 3.12 or higher is required.
38
+ 2. **uv Tool:** Install the [Python uv tool](https://docs.astral.sh/uv/getting-started/installation/) for efficient dependency management and easy execution of the MCP server.
39
+ 3. **API Key (Development):** As of May 2025, the MCP is in its testing phase, and **no API key is currently required** for development. For future production use, please [contact the Convert.Guru team](https://convert.guru/contact) to obtain an API key.
40
+
41
+ ## 🚀 Usage with Desktop App
42
+
43
+ To integrate this server with your desktop application, add the following configuration to your app's server settings. If necessary, adjust the absolute path to the `uv`/`uvx` executables (or `uv.exe`/`uvx.exe` on Windows).
44
+
45
+ **Using `uvx` (recommended):**
46
+
47
+ ```json
48
+ {
49
+ "mcpServers": {
50
+ "file-convert": {
51
+ "command": "uvx",
52
+ "args": ["--from", "git+https://github.com/convertguru/file-convert-mcp.git", "file-convert-mcp"],
53
+ "env": {
54
+ "CONVERT_GURU_API_KEY": "your_api_key_here"
55
+ }
56
+ }
57
+ }
58
+ }
59
+ ```
60
+
61
+ **If you have cloned the repository locally (adjust paths as needed):**
62
+
63
+ ```json
64
+ {
65
+ "mcpServers": {
66
+ "file-convert": {
67
+ "command": "/home/User/.local/bin/uv",
68
+ "args": ["--directory", "/home/User/file-convert-mcp/src/file_convert_mcp", "run", "server.py"],
69
+ "env": {
70
+ "CONVERT_GURU_API_KEY": "your_api_key_here"
71
+ }
72
+ }
73
+ }
74
+ }
75
+ ```
76
+
77
+ **Using local `uvx`:**
78
+
79
+ ```json
80
+ {
81
+ "mcpServers": {
82
+ "file-convert": {
83
+ "command": "uvx",
84
+ "args": ["--from", "/home/User/file-convert-mcp", "file-convert-mcp"],
85
+ "env": {
86
+ "CONVERT_GURU_API_KEY": "your_api_key_here"
87
+ }
88
+ }
89
+ }
90
+ }
91
+ ```
92
+
93
+ ### Environment Variables
94
+
95
+ The MCP server supports the following environment variables:
96
+
97
+ - **`CONVERT_GURU_API_KEY`**: Your Convert.Guru API key (optional for development)
98
+ - **`TRANSPORT`**: Transport method for the MCP server
99
+ - `"stdio"` (default): Standard input/output transport
100
+ - `"http"`: HTTP transport (streamable) with REST API endpoints
101
+ - **`PORT`**: Port number for HTTP transport (default: 8000, only used when `TRANSPORT=http`)
102
+
103
+ When using HTTP transport, the server provides additional endpoints:
104
+ - `GET /health` - Health check endpoint
105
+ - `GET /tools` - List of available MCP tools
106
+
107
+ Also, see this guide on how to [set up MCP tools in Claude Desktop](https://modelcontextprotocol.io/quickstart/user#for-claude-desktop-users).
108
+
109
+ ### Installing via Smithery
110
+
111
+ To install file-convert-mcp for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@convertguru/file-convert-mcp):
112
+
113
+ ```bash
114
+ npx -y @smithery/cli install @convertguru/file-convert-mcp --client claude
115
+ ```
116
+
117
+ ## 🛠️ Development
118
+
119
+ Get started with local development by following these steps:
120
+
121
+ **1. Clone the repository:**
122
+
123
+ ```bash
124
+ git clone https://github.com/convertguru/file-convert-mcp.git
125
+ ```
126
+
127
+ **2. Fetch and cache Python dependencies (optional, `uvx` handles this automatically):**
128
+
129
+ ```bash
130
+ cd file-convert-mcp
131
+ uv sync
132
+ cd ..
133
+ ```
134
+
135
+ **3. Create .env file with your (optional for now) API key:**
136
+
137
+ ```bash
138
+ echo "CONVERT_GURU_API_KEY=your_api_key_here" > file-convert-mcp/.env
139
+ ```
140
+
141
+ **4. Run the MCP server locally using various `uv` commands:**
142
+
143
+ ```bash
144
+ # Using uvx with .env file from the local directory
145
+ cd file-convert-mcp
146
+ UV_ENV_FILE=.env uvx --from ./file-convert-mcp file-convert-mcp
147
+
148
+ # OR using uv directly to run the server script
149
+ uv run file-convert-mcp/src/file_convert_mcp/server.py
150
+
151
+ # OR specifying the directory for uv
152
+ uv --directory file-convert-mcp/src/file_convert_mcp run server.py
153
+
154
+ # OR navigating into the server directory
155
+ cd file-convert-mcp/src/file_convert_mcp
156
+ uv run server.py
157
+
158
+ # OR using uvx to fetch the core from the GitHub repository + local .env file
159
+ UV_ENV_FILE=.env uvx --from git+https://github.com/convertguru/file-convert-mcp.git file-convert-mcp
160
+
161
+ # OR run with HTTP transport on custom port
162
+ TRANSPORT=http PORT=9000 uv run file-convert-mcp/src/file_convert_mcp/server.py
163
+
164
+ # OR run with stdio transport (default)
165
+ TRANSPORT=stdio uv run file-convert-mcp/src/file_convert_mcp/server.py
166
+ ```
167
+
168
+ **5. Create .env file with your configuration (optional for now):**
169
+
170
+ ```bash
171
+ # Basic configuration
172
+ echo "CONVERT_GURU_API_KEY=your_api_key_here" > file-convert-mcp/.env
173
+
174
+ # OR with transport configuration
175
+ echo "CONVERT_GURU_API_KEY=your_api_key_here" > file-convert-mcp/.env
176
+ echo "TRANSPORT=http" >> file-convert-mcp/.env
177
+ echo "PORT=8000" >> file-convert-mcp/.env
178
+ ```
179
+
180
+ **6. Modify the server logic if needed:**
181
+ Edit the main server file located at `src/file_convert_mcp/server.py.`
182
+
183
+ **7. Clearing the `uv` Cache (if needed):**
184
+ If `uv` has cached an older version of the code in `~/.cache/uv`, you might need to clear the cache. Alternatively, use `uv` with the `-n` or `--no-cache` option to bypass it.
185
+
186
+ ## ⚙️ Available Tools
187
+
188
+ The MCP server provides the following tools:
189
+
190
+ * `detect_file_type`: Analyzes the first 200 bytes of an uploaded file and uses the Convert.Guru API to determine its type.
191
+ * `convert_file`: Converts a given file to a specified output format. The desired file extension should be passed as the `ext_out` parameter.
192
+
193
+ Files larger than 40 MiB are rejected before upload. All result files are saved
194
+ beside the input file. Existing files are preserved by adding numbered suffixes.
195
+ The result contains `converted_file_paths` for all downloaded files and retains
196
+ `converted_file_path` for the first file for compatibility. If a later download
197
+ fails, the error response includes the paths already downloaded.
198
+
199
+ ### Local development and checks (shared user installation)
200
+
201
+ ```powershell
202
+ python -m pip install --user pytest build hatchling editables
203
+ python -m pip install --user --no-build-isolation -e .
204
+ python -m pytest -q
205
+ python -m build --no-isolation
206
+ ```
207
+
208
+ These tests use a local API substitute and do not upload files to Convert.Guru.
209
+ They cover the 40 MiB boundary, multiple results, existing files, partial failure,
210
+ native file detection, and the installed STDIO/HTTP command. Large temporary test
211
+ files are removed automatically. Building creates local artifacts in `dist/`;
212
+ it does not publish them.
213
+
214
+ On Windows, the package uses `python-magic-bin`, which bundles libmagic.
215
+ FastMCP is constrained to the tested 2.x API. If the user Scripts directory is not
216
+ on PATH, launch `file-convert-mcp.exe` using its full path, or run
217
+ `python -m file_convert_mcp.server`.
218
+
219
+ ## 📜 License
220
+
221
+ Visit us [https://convert.guru](https://convert.guru/)
222
+
223
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for complete details.
@@ -0,0 +1,206 @@
1
+ # File Convert MCP Server
2
+ [![smithery badge](https://smithery.ai/badge/@convertguru/file-convert-mcp)](https://smithery.ai/server/@convertguru/file-convert-mcp)
3
+
4
+ A Model Context Protocol (MCP) server for converting files between various formats.
5
+
6
+ File Convert is an MCP server designed to handle the conversion of a wide array of file formats, including images, office documents, audio, video, text, and data files. It aims to provide seamless transformation into popular formats such as PDF, JPG, MP4, and HTML, among others. This server offers a set of powerful tools to convert diverse file types into easily accessible and widely compatible formats like PDF, JPG, PNG, TXT.
7
+
8
+ ## ✨ Features
9
+
10
+ - **Intelligent File Type Detection:** Employs a combination of AI, TrID, and magic bytes for accurate file type identification.
11
+ - **Versatile File Conversion:** Supports conversion between numerous file types and the following popular formats:
12
+ - **Images:** Handles a vast range of formats, including many legacy ones. Converts to PDF, JPG, PNG.
13
+ - **Office Documents:** Converts to PDF, TXT, HTML.
14
+ - **Audio/Video:** Converts to MP3, MP4.
15
+ - **Databases:** Converts to CSV.
16
+ - **Various Files:** Offers conversion capabilities for other file types as well.
17
+
18
+ ## 🛠️ Requirements
19
+
20
+ 1. **Python:** Version 3.12 or higher is required.
21
+ 2. **uv Tool:** Install the [Python uv tool](https://docs.astral.sh/uv/getting-started/installation/) for efficient dependency management and easy execution of the MCP server.
22
+ 3. **API Key (Development):** As of May 2025, the MCP is in its testing phase, and **no API key is currently required** for development. For future production use, please [contact the Convert.Guru team](https://convert.guru/contact) to obtain an API key.
23
+
24
+ ## 🚀 Usage with Desktop App
25
+
26
+ To integrate this server with your desktop application, add the following configuration to your app's server settings. If necessary, adjust the absolute path to the `uv`/`uvx` executables (or `uv.exe`/`uvx.exe` on Windows).
27
+
28
+ **Using `uvx` (recommended):**
29
+
30
+ ```json
31
+ {
32
+ "mcpServers": {
33
+ "file-convert": {
34
+ "command": "uvx",
35
+ "args": ["--from", "git+https://github.com/convertguru/file-convert-mcp.git", "file-convert-mcp"],
36
+ "env": {
37
+ "CONVERT_GURU_API_KEY": "your_api_key_here"
38
+ }
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ **If you have cloned the repository locally (adjust paths as needed):**
45
+
46
+ ```json
47
+ {
48
+ "mcpServers": {
49
+ "file-convert": {
50
+ "command": "/home/User/.local/bin/uv",
51
+ "args": ["--directory", "/home/User/file-convert-mcp/src/file_convert_mcp", "run", "server.py"],
52
+ "env": {
53
+ "CONVERT_GURU_API_KEY": "your_api_key_here"
54
+ }
55
+ }
56
+ }
57
+ }
58
+ ```
59
+
60
+ **Using local `uvx`:**
61
+
62
+ ```json
63
+ {
64
+ "mcpServers": {
65
+ "file-convert": {
66
+ "command": "uvx",
67
+ "args": ["--from", "/home/User/file-convert-mcp", "file-convert-mcp"],
68
+ "env": {
69
+ "CONVERT_GURU_API_KEY": "your_api_key_here"
70
+ }
71
+ }
72
+ }
73
+ }
74
+ ```
75
+
76
+ ### Environment Variables
77
+
78
+ The MCP server supports the following environment variables:
79
+
80
+ - **`CONVERT_GURU_API_KEY`**: Your Convert.Guru API key (optional for development)
81
+ - **`TRANSPORT`**: Transport method for the MCP server
82
+ - `"stdio"` (default): Standard input/output transport
83
+ - `"http"`: HTTP transport (streamable) with REST API endpoints
84
+ - **`PORT`**: Port number for HTTP transport (default: 8000, only used when `TRANSPORT=http`)
85
+
86
+ When using HTTP transport, the server provides additional endpoints:
87
+ - `GET /health` - Health check endpoint
88
+ - `GET /tools` - List of available MCP tools
89
+
90
+ Also, see this guide on how to [set up MCP tools in Claude Desktop](https://modelcontextprotocol.io/quickstart/user#for-claude-desktop-users).
91
+
92
+ ### Installing via Smithery
93
+
94
+ To install file-convert-mcp for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@convertguru/file-convert-mcp):
95
+
96
+ ```bash
97
+ npx -y @smithery/cli install @convertguru/file-convert-mcp --client claude
98
+ ```
99
+
100
+ ## 🛠️ Development
101
+
102
+ Get started with local development by following these steps:
103
+
104
+ **1. Clone the repository:**
105
+
106
+ ```bash
107
+ git clone https://github.com/convertguru/file-convert-mcp.git
108
+ ```
109
+
110
+ **2. Fetch and cache Python dependencies (optional, `uvx` handles this automatically):**
111
+
112
+ ```bash
113
+ cd file-convert-mcp
114
+ uv sync
115
+ cd ..
116
+ ```
117
+
118
+ **3. Create .env file with your (optional for now) API key:**
119
+
120
+ ```bash
121
+ echo "CONVERT_GURU_API_KEY=your_api_key_here" > file-convert-mcp/.env
122
+ ```
123
+
124
+ **4. Run the MCP server locally using various `uv` commands:**
125
+
126
+ ```bash
127
+ # Using uvx with .env file from the local directory
128
+ cd file-convert-mcp
129
+ UV_ENV_FILE=.env uvx --from ./file-convert-mcp file-convert-mcp
130
+
131
+ # OR using uv directly to run the server script
132
+ uv run file-convert-mcp/src/file_convert_mcp/server.py
133
+
134
+ # OR specifying the directory for uv
135
+ uv --directory file-convert-mcp/src/file_convert_mcp run server.py
136
+
137
+ # OR navigating into the server directory
138
+ cd file-convert-mcp/src/file_convert_mcp
139
+ uv run server.py
140
+
141
+ # OR using uvx to fetch the core from the GitHub repository + local .env file
142
+ UV_ENV_FILE=.env uvx --from git+https://github.com/convertguru/file-convert-mcp.git file-convert-mcp
143
+
144
+ # OR run with HTTP transport on custom port
145
+ TRANSPORT=http PORT=9000 uv run file-convert-mcp/src/file_convert_mcp/server.py
146
+
147
+ # OR run with stdio transport (default)
148
+ TRANSPORT=stdio uv run file-convert-mcp/src/file_convert_mcp/server.py
149
+ ```
150
+
151
+ **5. Create .env file with your configuration (optional for now):**
152
+
153
+ ```bash
154
+ # Basic configuration
155
+ echo "CONVERT_GURU_API_KEY=your_api_key_here" > file-convert-mcp/.env
156
+
157
+ # OR with transport configuration
158
+ echo "CONVERT_GURU_API_KEY=your_api_key_here" > file-convert-mcp/.env
159
+ echo "TRANSPORT=http" >> file-convert-mcp/.env
160
+ echo "PORT=8000" >> file-convert-mcp/.env
161
+ ```
162
+
163
+ **6. Modify the server logic if needed:**
164
+ Edit the main server file located at `src/file_convert_mcp/server.py.`
165
+
166
+ **7. Clearing the `uv` Cache (if needed):**
167
+ If `uv` has cached an older version of the code in `~/.cache/uv`, you might need to clear the cache. Alternatively, use `uv` with the `-n` or `--no-cache` option to bypass it.
168
+
169
+ ## ⚙️ Available Tools
170
+
171
+ The MCP server provides the following tools:
172
+
173
+ * `detect_file_type`: Analyzes the first 200 bytes of an uploaded file and uses the Convert.Guru API to determine its type.
174
+ * `convert_file`: Converts a given file to a specified output format. The desired file extension should be passed as the `ext_out` parameter.
175
+
176
+ Files larger than 40 MiB are rejected before upload. All result files are saved
177
+ beside the input file. Existing files are preserved by adding numbered suffixes.
178
+ The result contains `converted_file_paths` for all downloaded files and retains
179
+ `converted_file_path` for the first file for compatibility. If a later download
180
+ fails, the error response includes the paths already downloaded.
181
+
182
+ ### Local development and checks (shared user installation)
183
+
184
+ ```powershell
185
+ python -m pip install --user pytest build hatchling editables
186
+ python -m pip install --user --no-build-isolation -e .
187
+ python -m pytest -q
188
+ python -m build --no-isolation
189
+ ```
190
+
191
+ These tests use a local API substitute and do not upload files to Convert.Guru.
192
+ They cover the 40 MiB boundary, multiple results, existing files, partial failure,
193
+ native file detection, and the installed STDIO/HTTP command. Large temporary test
194
+ files are removed automatically. Building creates local artifacts in `dist/`;
195
+ it does not publish them.
196
+
197
+ On Windows, the package uses `python-magic-bin`, which bundles libmagic.
198
+ FastMCP is constrained to the tested 2.x API. If the user Scripts directory is not
199
+ on PATH, launch `file-convert-mcp.exe` using its full path, or run
200
+ `python -m file_convert_mcp.server`.
201
+
202
+ ## 📜 License
203
+
204
+ Visit us [https://convert.guru](https://convert.guru/)
205
+
206
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for complete details.
@@ -0,0 +1,27 @@
1
+ [project]
2
+ name = "file-convert-mcp"
3
+ version = "0.1.2"
4
+ readme = "README.md"
5
+ license = { text = "MIT" }
6
+ description = "MCP tool for file conversion and file format detection by Convert.Guru"
7
+ authors = []
8
+ requires-python = ">=3.12"
9
+ dependencies = [
10
+ "fastmcp>=2.14.7,<3",
11
+ "aiohttp",
12
+ "python-dotenv",
13
+ "python-magic; sys_platform != 'win32'",
14
+ "python-magic-bin==0.4.14; sys_platform == 'win32'",
15
+ "starlette"
16
+ ]
17
+
18
+ [project.urls]
19
+ Homepage = "https://convert.guru"
20
+ Repository = "https://github.com/convertguru/file-convert-mcp"
21
+
22
+ [build-system]
23
+ requires = ["hatchling"]
24
+ build-backend = "hatchling.build"
25
+
26
+ [project.scripts]
27
+ file-convert-mcp = "file_convert_mcp.server:main"
@@ -0,0 +1,36 @@
1
+ # Smithery configuration file: https://smithery.ai/docs/build/project-config
2
+
3
+ startCommand:
4
+ type: stdio
5
+ configSchema:
6
+ # JSON Schema defining the configuration options for the MCP.
7
+ type: object
8
+ properties:
9
+ apiKey:
10
+ type: string
11
+ default: ""
12
+ description: Convert.Guru API key (optional for development)
13
+ transport:
14
+ type: string
15
+ enum: ["stdio", "http"]
16
+ default: "http"
17
+ description: Transport method for the MCP server
18
+ port:
19
+ type: integer
20
+ default: 8000
21
+ description: Port number for HTTP transport (optional, only used when transport is http)
22
+ commandFunction:
23
+ # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
24
+ |-
25
+ (config) => ({
26
+ command: 'file-convert-mcp',
27
+ env: {
28
+ CONVERT_GURU_API_KEY: config.apiKey,
29
+ TRANSPORT: (config.transport || "http").toUpperCase(),
30
+ PORT: config.transport === "http" ? (config.port?.toString() || "8000") : undefined
31
+ }
32
+ })
33
+ exampleConfig:
34
+ apiKey: ""
35
+ transport: "http"
36
+ port: 8000
@@ -0,0 +1,3 @@
1
+ """file-convert-mcp - MCP tool for file conversion and file format detection by Convert.Guru"""
2
+
3
+ __version__ = "0.1.2"
@@ -0,0 +1,254 @@
1
+ import os
2
+ import sys
3
+ import re
4
+ import magic
5
+ import json
6
+ from typing import Dict, Any
7
+ from fastmcp import FastMCP
8
+ from dotenv import load_dotenv
9
+ import aiohttp
10
+ from starlette.responses import JSONResponse
11
+ import asyncio
12
+
13
+ # Load environment variables
14
+ load_dotenv()
15
+
16
+ # Initialize FastMCP
17
+ mcp = FastMCP("file-convert-mcp")
18
+ base_url = 'https://convert.guru'
19
+ MAX_FILE_SIZE = 40 * 1024 * 1024
20
+ api_key = os.getenv("CONVERT_GURU_API_KEY")
21
+ if api_key is None:
22
+ api_key = ''
23
+
24
+ @mcp.custom_route("/health", methods=["GET"])
25
+ async def health_check(request):
26
+ return JSONResponse({"status": "healthy", "service": "mcp-server"})
27
+
28
+ @mcp.custom_route("/tools", methods=["GET"])
29
+ async def list_tools(request):
30
+ tools_info = []
31
+ for tool_name, tool in (await mcp.get_tools()).items():
32
+ tools_info.append({
33
+ "name": tool_name,
34
+ "description": tool.description or "No description available",
35
+ "async": asyncio.iscoroutinefunction(tool.fn)
36
+ })
37
+
38
+ return JSONResponse({
39
+ "tools": tools_info,
40
+ "total_tools": len(tools_info)
41
+ })
42
+
43
+ @mcp.tool()
44
+ async def detect_file_type(file_path: str) -> Dict[str, Any]:
45
+ """Detect file type by sending the first 200 bytes of the file to Convert.Guru API."""
46
+ if not file_path:
47
+ return {"error": "file_path parameter is required"}
48
+
49
+ if not os.path.exists(file_path):
50
+ return {"error": f"File not found (use the absolute file path): {file_path}"}
51
+
52
+ if not os.path.isfile(file_path):
53
+ return {"error": f"Not a file: {file_path}"}
54
+
55
+ try:
56
+ with open(file_path, "rb") as f:
57
+ first_200_bytes = f.read(200) # Read only the first 200 bytes
58
+
59
+ _, file_extension = os.path.splitext(file_path)
60
+ file_extension = file_extension.lstrip('.') # Remove dot
61
+ filename = os.path.basename(file_path)
62
+ file_size_bytes = os.path.getsize(file_path)
63
+ size_hex = hex(file_size_bytes)
64
+ mime_type = magic.Magic(mime=True).from_file(file_path)
65
+
66
+ data = [file_extension,size_hex,mime_type,filename]
67
+ separator = '\xFE'
68
+ joined_data = separator.join(data)
69
+ headers = {'cache-control': 'no-cache', 'api-key': api_key}
70
+
71
+ data_array = bytearray()
72
+ for char in joined_data:
73
+ data_array.append(ord(char) & 0xFF)
74
+
75
+ data_array.append(0xFE)
76
+ data_array.extend(first_200_bytes)
77
+
78
+ async with aiohttp.ClientSession() as session:
79
+ async with session.post(base_url + '/api/v1/detect_file_type', data=data_array, headers=headers) as response:
80
+ if response.status == 200:
81
+ content = await response.text()
82
+ comment_pattern = re.compile(r'<!--.+-->')
83
+ # Remove all occurrences of the pattern from the string
84
+ content = comment_pattern.sub('', content)
85
+ return {"result": {"file_type_description": content.strip()}}
86
+ else:
87
+ error_text = await response.text()
88
+ return {"error": f"API request failed with status {response.status}: {error_text}"}
89
+
90
+ except aiohttp.ClientError as e:
91
+ return {"error": f"Error making API request: {e}"}
92
+ except Exception as e:
93
+ return {"error": f"Error processing file: {e}"}
94
+
95
+ @mcp.tool()
96
+ async def convert_file(file_path: str, ext_out: str) -> Dict[str, Any]:
97
+ """Convert a file to other format via Convert.Guru API. Pass the file path and output extension."""
98
+ if not file_path:
99
+ return {"error": "file_path parameter is required"}
100
+
101
+ if not ext_out:
102
+ return {"error": "ext_out parameter is required"}
103
+
104
+ if not os.path.exists(file_path):
105
+ return {"error": f"File not found (use the absolute file path): {file_path}"}
106
+
107
+ if not os.path.isfile(file_path):
108
+ return {"error": f"Not a file: {file_path}"}
109
+
110
+ try:
111
+ with open(file_path, "rb") as f:
112
+ chunk = f.read(MAX_FILE_SIZE + 1)
113
+ if len(chunk) > MAX_FILE_SIZE:
114
+ return {"error": "File exceeds the maximum upload size of 40 MiB."}
115
+
116
+ filename = os.path.basename(file_path)
117
+
118
+ data = aiohttp.FormData()
119
+ data.add_field('file', chunk, filename=filename)
120
+ headers = {'cache-control': 'no-cache', 'api-key': api_key}
121
+
122
+ async with aiohttp.ClientSession() as session:
123
+ async with session.post(base_url + '/api/v1/upload', data=data, headers=headers) as response:
124
+ if response.status == 200:
125
+ response_text = await response.text()
126
+ try:
127
+ # Explicitly parse using json.loads
128
+ json_result = json.loads(response_text)
129
+ json_payload = {
130
+ "filename": json_result['Filename'], "ext_out": ext_out.lower()}
131
+ conv_response_text = await post_json(base_url + '/api/v1/convert', json_payload, headers)
132
+ try:
133
+ # Explicitly parse using json.loads
134
+ conv_json_result = json.loads(
135
+ conv_response_text)
136
+
137
+ ## check for error in conv_json_result
138
+ if conv_json_result.get('error'):
139
+ conv_json_result['error'] = f"File conversion failed: {conv_json_result.get('error')}"
140
+ return conv_json_result
141
+
142
+ file_info = conv_json_result.get('file')
143
+ if file_info and 'ext_out' in file_info and 'urls' in file_info and file_info['urls']:
144
+ file_extension = file_info.get('ext_out')
145
+ save_path = file_path + '.converted' + \
146
+ f".{file_extension}"
147
+ saved_paths = []
148
+ for file_url_path in file_info['urls']:
149
+ full_file_url = (base_url + file_url_path
150
+ if file_url_path.startswith('/') else file_url_path)
151
+ download_result = await download_file(session, full_file_url, save_path)
152
+ if 'error' in download_result:
153
+ download_result['result'] = {"converted_file_paths": saved_paths}
154
+ return download_result
155
+ saved_paths.append(download_result['result']['converted_file_path'])
156
+ return {"result": {
157
+ "converted_file_path": saved_paths[0],
158
+ "converted_file_paths": saved_paths,
159
+ }}
160
+
161
+ else:
162
+ return {"error": f"File URL information not found in the API response.: {conv_response_text}"}
163
+ except json.JSONDecodeError:
164
+ # If the response is not JSON (convert)
165
+ return {"error": f"API convert request failed - response text is not JSON: {conv_response_text}"}
166
+
167
+ except json.JSONDecodeError:
168
+ # If the response is not JSON (upload)
169
+ return {"error": f"API upload request failed - response text is not JSON: {response_text}"}
170
+ else:
171
+ error_text = await response.text()
172
+ return {"error": f"API request failed with status {response.status}: {error_text}"}
173
+
174
+ except aiohttp.ClientError as e:
175
+ return {"error": f"Error making API request: {e}"}
176
+ except Exception as e:
177
+ return {"error": f"Error processing file: {e}"}
178
+
179
+ async def post_json(url: str, data: dict, headers):
180
+ """
181
+ Posts JSON data to a specified URL using aiohttp.
182
+
183
+ Args:
184
+ url: The URL to post the JSON data to.
185
+ data: A Python dictionary that will be converted to JSON.
186
+ """
187
+ cloned_headers = headers.copy()
188
+ cloned_headers['Content-Type'] = 'application/json'
189
+ async with aiohttp.ClientSession() as session:
190
+ try:
191
+ async with session.post(url, json=data, headers=cloned_headers) as response:
192
+ response_text = await response.text()
193
+ status = response.status
194
+ err = {}
195
+
196
+ if status == 200 or status == 400:
197
+ return response_text
198
+ elif status == 524:
199
+ err = {"error": f"Error HTTP 524 - operation takes too long to complete, timeout occured."}
200
+ elif status == 500:
201
+ err = {"error": f"Error HTTP 500 - operation takes too long to complete, internal server error."}
202
+ else:
203
+ err = {"error": f"Error HTTP {status})."}
204
+
205
+ return json.dumps(err)
206
+ except aiohttp.ClientError as e:
207
+ err = {"error": f"An error occurred during the request (exception): {e}"}
208
+ return json.dumps(err)
209
+
210
+ async def download_file(session: aiohttp.ClientSession, url: str, save_path: str) -> Dict[str, Any]:
211
+ """Downloads a file from a URL and saves it to the specified path."""
212
+ try:
213
+ async with session.get(url) as response:
214
+ if response.status == 200:
215
+ root, extension = os.path.splitext(save_path)
216
+ counter = 1
217
+ while True:
218
+ try:
219
+ f = open(save_path, 'xb')
220
+ break
221
+ except FileExistsError:
222
+ counter += 1
223
+ save_path = f"{root}_{counter}{extension}"
224
+ try:
225
+ with f:
226
+ async for chunk in response.content.iter_chunked(8192):
227
+ f.write(chunk)
228
+ except BaseException:
229
+ os.remove(save_path)
230
+ raise
231
+ return {"result": {"converted_file_path": save_path}}
232
+ else:
233
+ response_text = await response.text()
234
+ return {"error": f"File download from {url} failed with status {response.status}: {response_text}"}
235
+ except aiohttp.ClientError as e:
236
+ return {"error": f"An error occurred during file download from {url}: {e}"}
237
+
238
+ def main():
239
+ """Start the installed command and direct script with the same settings."""
240
+ transport = os.getenv("TRANSPORT", "").upper()
241
+ if transport == "HTTP":
242
+ port = os.getenv("PORT", "8000")
243
+ try:
244
+ port_int = int(port)
245
+ mcp.run(transport="http", host="0.0.0.0", port=port_int)
246
+ except ValueError:
247
+ print(f"Invalid PORT value: {port}. Using default stdio transport.", file=sys.stderr)
248
+ mcp.run()
249
+ else:
250
+ mcp.run()
251
+
252
+
253
+ if __name__ == "__main__":
254
+ main()
@@ -0,0 +1,172 @@
1
+ import asyncio
2
+ import os
3
+ from pathlib import Path
4
+ import socket
5
+ import subprocess
6
+ import sysconfig
7
+ import tempfile
8
+ import time
9
+ from urllib.request import urlopen
10
+
11
+ from aiohttp import web
12
+ from fastmcp import Client
13
+ from fastmcp.client.transports import StdioTransport
14
+ import magic
15
+ import pytest
16
+
17
+ from file_convert_mcp import server
18
+
19
+
20
+ @pytest.fixture
21
+ def work_dir():
22
+ # Large boundary-test files live in TEMP and are removed after each test.
23
+ with tempfile.TemporaryDirectory(prefix="file-convert-mcp-test-", ignore_cleanup_errors=True) as folder:
24
+ yield Path(folder)
25
+
26
+
27
+ async def conversion_case(monkeypatch, source, *, outputs=1, failed_output=None):
28
+ uploads = []
29
+
30
+ async def upload(request):
31
+ part = await (await request.multipart()).next()
32
+ size = 0
33
+ while chunk := await part.read_chunk():
34
+ size += len(chunk)
35
+ uploads.append(size)
36
+ return web.json_response({"Filename": "uploaded.bin"})
37
+
38
+ async def convert(request):
39
+ assert (await request.json())["ext_out"] == "txt"
40
+ return web.json_response({"file": {
41
+ "ext_out": "txt", "urls": [f"/result/{n}" for n in range(outputs)]
42
+ }})
43
+
44
+ async def result(request):
45
+ n = int(request.match_info["n"])
46
+ if n == failed_output:
47
+ return web.Response(status=500, text="Test download failure")
48
+ return web.Response(body=f"Result {n}".encode())
49
+
50
+ app = web.Application()
51
+ app.router.add_post('/api/v1/upload', upload)
52
+ app.router.add_post('/api/v1/convert', convert)
53
+ app.router.add_get('/result/{n}', result)
54
+ runner = web.AppRunner(app)
55
+ await runner.setup()
56
+ sock = socket.socket()
57
+ sock.bind(('127.0.0.1', 0))
58
+ port = sock.getsockname()[1]
59
+ await web.SockSite(runner, sock).start()
60
+ monkeypatch.setattr(server, 'base_url', f'http://127.0.0.1:{port}')
61
+ try:
62
+ response = await server.convert_file.fn(str(source), 'TXT')
63
+ return response, uploads
64
+ finally:
65
+ await runner.cleanup()
66
+
67
+
68
+ @pytest.mark.parametrize('offset', [-1, 0, 1])
69
+ def test_upload_size_boundary(monkeypatch, work_dir, offset):
70
+ source = work_dir / 'boundary.bin'
71
+ size = server.MAX_FILE_SIZE + offset
72
+ with source.open('wb') as f:
73
+ f.truncate(size)
74
+ response, uploads = asyncio.run(conversion_case(monkeypatch, source))
75
+ if offset > 0:
76
+ assert '40 MiB' in response['error']
77
+ assert uploads == []
78
+ else:
79
+ assert uploads == [size]
80
+ assert Path(response['result']['converted_file_path']).read_bytes() == b'Result 0'
81
+
82
+
83
+ def test_multiple_results_and_repeated_conversion(monkeypatch, work_dir):
84
+ source = work_dir / 'input.bin'
85
+ source.write_bytes(b'input')
86
+ original = work_dir / 'input.bin.converted.txt'
87
+ original.write_bytes(b'Keep me')
88
+ seen = set()
89
+ for _ in range(2):
90
+ response, uploads = asyncio.run(conversion_case(monkeypatch, source, outputs=3))
91
+ paths = response['result']['converted_file_paths']
92
+ assert response['result']['converted_file_path'] == paths[0]
93
+ assert len(paths) == 3
94
+ for n, path in enumerate(paths):
95
+ assert path not in seen
96
+ seen.add(path)
97
+ assert Path(path).read_bytes() == f'Result {n}'.encode()
98
+ assert uploads == [5]
99
+ assert original.read_bytes() == b'Keep me'
100
+ assert source.read_bytes() == b'input'
101
+
102
+
103
+ def test_failed_download_reports_completed_files(monkeypatch, work_dir):
104
+ source = work_dir / 'input.bin'
105
+ source.write_bytes(b'input')
106
+ response, _ = asyncio.run(conversion_case(monkeypatch, source, outputs=3, failed_output=1))
107
+ assert '500' in response['error']
108
+ paths = response['result']['converted_file_paths']
109
+ assert len(paths) == 1
110
+ assert Path(paths[0]).read_bytes() == b'Result 0'
111
+
112
+
113
+ def installed_command():
114
+ scheme = 'nt_user' if os.name == 'nt' else 'posix_user'
115
+ suffix = '.exe' if os.name == 'nt' else ''
116
+ command = Path(sysconfig.get_path('scripts', scheme)) / f'file-convert-mcp{suffix}'
117
+ assert command.is_file(), 'Install the package before running the transport tests.'
118
+ return str(command)
119
+
120
+
121
+ async def check_mcp(transport):
122
+ async with Client(transport, timeout=10) as client:
123
+ tools = await client.list_tools()
124
+ assert {tool.name for tool in tools} == {'detect_file_type', 'convert_file'}
125
+ # An actual tool call, deliberately rejected before any external API request.
126
+ response = await client.call_tool('convert_file', {'file_path': '', 'ext_out': 'txt'})
127
+ assert 'file_path parameter is required' in str(response)
128
+
129
+
130
+ @pytest.mark.parametrize('port', [None, 'invalid'])
131
+ def test_installed_stdio_start(work_dir, port):
132
+ env = dict(os.environ, PYTHONIOENCODING='utf-8', TRANSPORT='stdio')
133
+ if port:
134
+ env.update(TRANSPORT='http', PORT=port)
135
+ transport = StdioTransport(command=installed_command(), args=[], env=env, cwd=str(work_dir))
136
+ asyncio.run(check_mcp(transport))
137
+
138
+
139
+ def test_installed_http_start(work_dir):
140
+ with socket.socket() as sock:
141
+ sock.bind(('127.0.0.1', 0))
142
+ port = sock.getsockname()[1]
143
+ env = dict(os.environ, PYTHONIOENCODING='utf-8', TRANSPORT='http', PORT=str(port))
144
+ log_path = work_dir / 'http.log'
145
+ with log_path.open('w', encoding='utf-8') as log:
146
+ process = subprocess.Popen(
147
+ [installed_command()], env=env, cwd=work_dir,
148
+ stdout=log, stderr=log,
149
+ creationflags=subprocess.CREATE_NO_WINDOW if os.name == 'nt' else 0,
150
+ )
151
+ try:
152
+ deadline = time.monotonic() + 25
153
+ while time.monotonic() < deadline:
154
+ assert process.poll() is None, log_path.read_text(encoding='utf-8')
155
+ try:
156
+ with urlopen(f'http://127.0.0.1:{port}/health', timeout=1) as response:
157
+ assert response.status == 200
158
+ break
159
+ except OSError:
160
+ time.sleep(0.1)
161
+ else:
162
+ pytest.fail(log_path.read_text(encoding='utf-8'))
163
+ with urlopen(f'http://127.0.0.1:{port}/tools', timeout=2) as response:
164
+ assert b'convert_file' in response.read()
165
+ asyncio.run(check_mcp(f'http://127.0.0.1:{port}/mcp'))
166
+ finally:
167
+ process.terminate()
168
+ process.wait(timeout=10)
169
+
170
+
171
+ def test_native_file_detection():
172
+ assert magic.from_buffer(b'%PDF-1.4\n', mime=True) == 'application/pdf'