aspose-html-cloud-mcp 1.0.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,8 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .venv/
7
+ *.egg
8
+ .eggs/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Aspose Pty Ltd
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,199 @@
1
+ Metadata-Version: 2.4
2
+ Name: aspose-html-cloud-mcp
3
+ Version: 1.0.0
4
+ Summary: MCP server that exposes Aspose.HTML Cloud document conversion (HTML, Markdown, SVG, EPUB ? PDF, DOCX, PNG, etc.) as tools for AI assistants.
5
+ Project-URL: Homepage, https://github.com/aspose-html-cloud/Aspose.HTML-Cloud-MCP
6
+ Project-URL: Repository, https://github.com/aspose-html-cloud/Aspose.HTML-Cloud-MCP
7
+ Project-URL: Issues, https://github.com/aspose-html-cloud/Aspose.HTML-Cloud-MCP/issues
8
+ Author-email: Aspose <support@aspose.cloud>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: ai,aspose,claude,conversion,copilot,cursor,html,mcp,model-context-protocol,pdf
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Software Development :: Libraries
21
+ Classifier: Topic :: Text Processing :: Markup :: HTML
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: httpx>=0.27.0
24
+ Requires-Dist: mcp>=1.0.0
25
+ Description-Content-Type: text/markdown
26
+
27
+ # Aspose.HTML Cloud MCP Server (Python)
28
+
29
+ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that exposes **Aspose.HTML Cloud** document conversion as tools for AI assistants like Claude Desktop, Cursor, VS Code Copilot, and other MCP-compatible clients.
30
+
31
+ ## What it does
32
+
33
+ Converts documents between formats via the [Aspose.HTML Cloud API](https://products.aspose.cloud/html/).
34
+
35
+ | Supported input formats | Supported output formats |
36
+ |---|---|
37
+ | `html`, `mhtml`, `xhtml`, `epub`, `svg`, `md` | `pdf`, `xps`, `docx`, `doc`, `jpeg`, `png`, `bmp`, `gif`, `tiff`, `webp`, `md`, `mhtml`, `svg` |
38
+
39
+ **Available tools:**
40
+
41
+ | Tool | Input source |
42
+ |---|---|
43
+ | `convert_url_to_format` | Public URL |
44
+ | `convert_content_to_format` | Raw HTML / SVG / Markdown string |
45
+ | `convert_base64_to_format` | Base64-encoded document |
46
+ | `convert_file_to_format` | Local file path |
47
+ | `vectorize_image` | Raster image ? SVG vectorization |
48
+
49
+ **Example prompts:**
50
+
51
+ > "Convert https://example.com to PDF"
52
+
53
+ > "Convert this HTML to DOCX: \<html\>\<body\>\<h1\>Hello\</h1\>\</body\>\</html\>"
54
+
55
+ > "Convert C:\docs\report.html to PNG"
56
+
57
+ > "Vectorize this PNG image into SVG"
58
+
59
+ ## Prerequisites
60
+
61
+ - **Python 3.10+**
62
+ - A free **Aspose Cloud** account -- sign up at [dashboard.aspose.cloud](https://dashboard.aspose.cloud/) and create an application to get your **Client ID** and **Client Secret**
63
+
64
+ ## Installation
65
+
66
+ ```bash
67
+ pip install aspose-html-cloud-mcp
68
+ ```
69
+
70
+ Or run directly with [`uvx`](https://docs.astral.sh/uv/) (no install needed):
71
+
72
+ ```bash
73
+ uvx aspose-html-cloud-mcp
74
+ ```
75
+
76
+ ## Configuration with MCP Clients
77
+
78
+ Credentials are passed via environment variables `ASPOSE_CLIENT_ID` and `ASPOSE_CLIENT_SECRET`.
79
+
80
+ ### Claude Desktop
81
+
82
+ Edit your Claude Desktop config file:
83
+
84
+ - **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
85
+ - **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
86
+
87
+ ```json
88
+ {
89
+ "mcpServers": {
90
+ "aspose-html-cloud": {
91
+ "command": "uvx",
92
+ "args": ["aspose-html-cloud-mcp"],
93
+ "env": {
94
+ "ASPOSE_CLIENT_ID": "your-client-id",
95
+ "ASPOSE_CLIENT_SECRET": "your-client-secret"
96
+ }
97
+ }
98
+ }
99
+ }
100
+ ```
101
+
102
+ ### VS Code (Copilot)
103
+
104
+ Add to your `.vscode/settings.json` or user settings:
105
+
106
+ ```json
107
+ {
108
+ "mcp": {
109
+ "servers": {
110
+ "aspose-html-cloud": {
111
+ "command": "uvx",
112
+ "args": ["aspose-html-cloud-mcp"],
113
+ "env": {
114
+ "ASPOSE_CLIENT_ID": "your-client-id",
115
+ "ASPOSE_CLIENT_SECRET": "your-client-secret"
116
+ }
117
+ }
118
+ }
119
+ }
120
+ }
121
+ ```
122
+
123
+ ### Cursor
124
+
125
+ Add to your Cursor MCP configuration (`~/.cursor/mcp.json`):
126
+
127
+ ```json
128
+ {
129
+ "mcpServers": {
130
+ "aspose-html-cloud": {
131
+ "command": "uvx",
132
+ "args": ["aspose-html-cloud-mcp"],
133
+ "env": {
134
+ "ASPOSE_CLIENT_ID": "your-client-id",
135
+ "ASPOSE_CLIENT_SECRET": "your-client-secret"
136
+ }
137
+ }
138
+ }
139
+ }
140
+ ```
141
+
142
+ ### Alternative: Using pip + python
143
+
144
+ If you installed via `pip install` instead of `uvx`, replace the command:
145
+
146
+ ```json
147
+ {
148
+ "command": "aspose-html-cloud-mcp"
149
+ }
150
+ ```
151
+
152
+ Or:
153
+
154
+ ```json
155
+ {
156
+ "command": "python",
157
+ "args": ["-m", "aspose_html_cloud_mcp"]
158
+ }
159
+ ```
160
+
161
+ ## Environment Variables
162
+
163
+ | Variable | Required | Description |
164
+ |---|---|---|
165
+ | `ASPOSE_CLIENT_ID` | Yes | Your Aspose Cloud application Client ID |
166
+ | `ASPOSE_CLIENT_SECRET` | Yes | Your Aspose Cloud application Client Secret |
167
+ | `ASPOSE_HTML_API_URL` | No | Override the API base URL (default: `https://api.aspose.cloud`) |
168
+
169
+ ## Development
170
+
171
+ ```bash
172
+ # Clone the repository
173
+ git clone https://github.com/aspose-html-cloud/Aspose.HTML-Cloud-MCP.git
174
+ cd Aspose.HTML-Cloud-MCP/python
175
+
176
+ # Create a virtual environment and install in dev mode
177
+ python -m venv .venv
178
+ source .venv/bin/activate # or .venv\Scripts\activate on Windows
179
+ pip install -e .
180
+
181
+ # Run the server
182
+ export ASPOSE_CLIENT_ID="your-client-id"
183
+ export ASPOSE_CLIENT_SECRET="your-client-secret"
184
+ aspose-html-cloud-mcp
185
+ ```
186
+
187
+ ## Publishing to PyPI
188
+
189
+ ```bash
190
+ pip install build twine
191
+ python -m build
192
+ twine upload dist/*
193
+ ```
194
+
195
+ ## License
196
+
197
+ This project is licensed under the MIT License. See [LICENSE](LICENSE) for details.
198
+
199
+ The Aspose.HTML Cloud API itself requires a separate subscription -- a free tier is available at [aspose.cloud](https://purchase.aspose.cloud/pricing).
@@ -0,0 +1,173 @@
1
+ # Aspose.HTML Cloud MCP Server (Python)
2
+
3
+ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that exposes **Aspose.HTML Cloud** document conversion as tools for AI assistants like Claude Desktop, Cursor, VS Code Copilot, and other MCP-compatible clients.
4
+
5
+ ## What it does
6
+
7
+ Converts documents between formats via the [Aspose.HTML Cloud API](https://products.aspose.cloud/html/).
8
+
9
+ | Supported input formats | Supported output formats |
10
+ |---|---|
11
+ | `html`, `mhtml`, `xhtml`, `epub`, `svg`, `md` | `pdf`, `xps`, `docx`, `doc`, `jpeg`, `png`, `bmp`, `gif`, `tiff`, `webp`, `md`, `mhtml`, `svg` |
12
+
13
+ **Available tools:**
14
+
15
+ | Tool | Input source |
16
+ |---|---|
17
+ | `convert_url_to_format` | Public URL |
18
+ | `convert_content_to_format` | Raw HTML / SVG / Markdown string |
19
+ | `convert_base64_to_format` | Base64-encoded document |
20
+ | `convert_file_to_format` | Local file path |
21
+ | `vectorize_image` | Raster image ? SVG vectorization |
22
+
23
+ **Example prompts:**
24
+
25
+ > "Convert https://example.com to PDF"
26
+
27
+ > "Convert this HTML to DOCX: \<html\>\<body\>\<h1\>Hello\</h1\>\</body\>\</html\>"
28
+
29
+ > "Convert C:\docs\report.html to PNG"
30
+
31
+ > "Vectorize this PNG image into SVG"
32
+
33
+ ## Prerequisites
34
+
35
+ - **Python 3.10+**
36
+ - A free **Aspose Cloud** account -- sign up at [dashboard.aspose.cloud](https://dashboard.aspose.cloud/) and create an application to get your **Client ID** and **Client Secret**
37
+
38
+ ## Installation
39
+
40
+ ```bash
41
+ pip install aspose-html-cloud-mcp
42
+ ```
43
+
44
+ Or run directly with [`uvx`](https://docs.astral.sh/uv/) (no install needed):
45
+
46
+ ```bash
47
+ uvx aspose-html-cloud-mcp
48
+ ```
49
+
50
+ ## Configuration with MCP Clients
51
+
52
+ Credentials are passed via environment variables `ASPOSE_CLIENT_ID` and `ASPOSE_CLIENT_SECRET`.
53
+
54
+ ### Claude Desktop
55
+
56
+ Edit your Claude Desktop config file:
57
+
58
+ - **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
59
+ - **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
60
+
61
+ ```json
62
+ {
63
+ "mcpServers": {
64
+ "aspose-html-cloud": {
65
+ "command": "uvx",
66
+ "args": ["aspose-html-cloud-mcp"],
67
+ "env": {
68
+ "ASPOSE_CLIENT_ID": "your-client-id",
69
+ "ASPOSE_CLIENT_SECRET": "your-client-secret"
70
+ }
71
+ }
72
+ }
73
+ }
74
+ ```
75
+
76
+ ### VS Code (Copilot)
77
+
78
+ Add to your `.vscode/settings.json` or user settings:
79
+
80
+ ```json
81
+ {
82
+ "mcp": {
83
+ "servers": {
84
+ "aspose-html-cloud": {
85
+ "command": "uvx",
86
+ "args": ["aspose-html-cloud-mcp"],
87
+ "env": {
88
+ "ASPOSE_CLIENT_ID": "your-client-id",
89
+ "ASPOSE_CLIENT_SECRET": "your-client-secret"
90
+ }
91
+ }
92
+ }
93
+ }
94
+ }
95
+ ```
96
+
97
+ ### Cursor
98
+
99
+ Add to your Cursor MCP configuration (`~/.cursor/mcp.json`):
100
+
101
+ ```json
102
+ {
103
+ "mcpServers": {
104
+ "aspose-html-cloud": {
105
+ "command": "uvx",
106
+ "args": ["aspose-html-cloud-mcp"],
107
+ "env": {
108
+ "ASPOSE_CLIENT_ID": "your-client-id",
109
+ "ASPOSE_CLIENT_SECRET": "your-client-secret"
110
+ }
111
+ }
112
+ }
113
+ }
114
+ ```
115
+
116
+ ### Alternative: Using pip + python
117
+
118
+ If you installed via `pip install` instead of `uvx`, replace the command:
119
+
120
+ ```json
121
+ {
122
+ "command": "aspose-html-cloud-mcp"
123
+ }
124
+ ```
125
+
126
+ Or:
127
+
128
+ ```json
129
+ {
130
+ "command": "python",
131
+ "args": ["-m", "aspose_html_cloud_mcp"]
132
+ }
133
+ ```
134
+
135
+ ## Environment Variables
136
+
137
+ | Variable | Required | Description |
138
+ |---|---|---|
139
+ | `ASPOSE_CLIENT_ID` | Yes | Your Aspose Cloud application Client ID |
140
+ | `ASPOSE_CLIENT_SECRET` | Yes | Your Aspose Cloud application Client Secret |
141
+ | `ASPOSE_HTML_API_URL` | No | Override the API base URL (default: `https://api.aspose.cloud`) |
142
+
143
+ ## Development
144
+
145
+ ```bash
146
+ # Clone the repository
147
+ git clone https://github.com/aspose-html-cloud/Aspose.HTML-Cloud-MCP.git
148
+ cd Aspose.HTML-Cloud-MCP/python
149
+
150
+ # Create a virtual environment and install in dev mode
151
+ python -m venv .venv
152
+ source .venv/bin/activate # or .venv\Scripts\activate on Windows
153
+ pip install -e .
154
+
155
+ # Run the server
156
+ export ASPOSE_CLIENT_ID="your-client-id"
157
+ export ASPOSE_CLIENT_SECRET="your-client-secret"
158
+ aspose-html-cloud-mcp
159
+ ```
160
+
161
+ ## Publishing to PyPI
162
+
163
+ ```bash
164
+ pip install build twine
165
+ python -m build
166
+ twine upload dist/*
167
+ ```
168
+
169
+ ## License
170
+
171
+ This project is licensed under the MIT License. See [LICENSE](LICENSE) for details.
172
+
173
+ The Aspose.HTML Cloud API itself requires a separate subscription -- a free tier is available at [aspose.cloud](https://purchase.aspose.cloud/pricing).
@@ -0,0 +1,53 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "aspose-html-cloud-mcp"
7
+ version = "1.0.0"
8
+ description = "MCP server that exposes Aspose.HTML Cloud document conversion (HTML, Markdown, SVG, EPUB ? PDF, DOCX, PNG, etc.) as tools for AI assistants."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ { name = "Aspose", email = "support@aspose.cloud" },
14
+ ]
15
+ keywords = [
16
+ "mcp",
17
+ "model-context-protocol",
18
+ "aspose",
19
+ "html",
20
+ "conversion",
21
+ "pdf",
22
+ "ai",
23
+ "claude",
24
+ "copilot",
25
+ "cursor",
26
+ ]
27
+ classifiers = [
28
+ "Development Status :: 4 - Beta",
29
+ "Intended Audience :: Developers",
30
+ "License :: OSI Approved :: MIT License",
31
+ "Programming Language :: Python :: 3",
32
+ "Programming Language :: Python :: 3.10",
33
+ "Programming Language :: Python :: 3.11",
34
+ "Programming Language :: Python :: 3.12",
35
+ "Programming Language :: Python :: 3.13",
36
+ "Topic :: Software Development :: Libraries",
37
+ "Topic :: Text Processing :: Markup :: HTML",
38
+ ]
39
+ dependencies = [
40
+ "mcp>=1.0.0",
41
+ "httpx>=0.27.0",
42
+ ]
43
+
44
+ [project.urls]
45
+ Homepage = "https://github.com/aspose-html-cloud/Aspose.HTML-Cloud-MCP"
46
+ Repository = "https://github.com/aspose-html-cloud/Aspose.HTML-Cloud-MCP"
47
+ Issues = "https://github.com/aspose-html-cloud/Aspose.HTML-Cloud-MCP/issues"
48
+
49
+ [project.scripts]
50
+ aspose-html-cloud-mcp = "aspose_html_cloud_mcp:main"
51
+
52
+ [tool.hatch.build.targets.wheel]
53
+ packages = ["src/aspose_html_cloud_mcp"]
@@ -0,0 +1,5 @@
1
+ """Aspose.HTML Cloud MCP Server -- exposes document conversion as MCP tools."""
2
+
3
+ from .server import main
4
+
5
+ __all__ = ["main"]
@@ -0,0 +1,5 @@
1
+ """Allow running as ``python -m aspose_html_cloud_mcp``."""
2
+
3
+ from .server import main
4
+
5
+ main()
@@ -0,0 +1,65 @@
1
+ """Aspose Cloud OAuth2 client-credentials authentication.
2
+
3
+ Credentials are resolved from environment variables ``ASPOSE_CLIENT_ID`` and
4
+ ``ASPOSE_CLIENT_SECRET``. Tokens are cached and automatically refreshed when
5
+ they expire (with a 30-second safety buffer).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import time
12
+ from urllib.parse import quote
13
+
14
+ import httpx
15
+
16
+ TOKEN_ENDPOINT = "https://api.aspose.cloud/connect/token"
17
+
18
+
19
+ class AsposeHtmlAuth:
20
+ """Manages bearer-token acquisition for the Aspose.HTML Cloud API."""
21
+
22
+ def __init__(self, http_client: httpx.AsyncClient) -> None:
23
+ self._http = http_client
24
+ self._client_id = os.environ.get("ASPOSE_CLIENT_ID", "")
25
+ self._client_secret = os.environ.get("ASPOSE_CLIENT_SECRET", "")
26
+ if not self._client_id:
27
+ raise RuntimeError(
28
+ "Environment variable ASPOSE_CLIENT_ID is not set. "
29
+ "Get your free credentials at https://dashboard.aspose.cloud/"
30
+ )
31
+ if not self._client_secret:
32
+ raise RuntimeError(
33
+ "Environment variable ASPOSE_CLIENT_SECRET is not set. "
34
+ "Get your free credentials at https://dashboard.aspose.cloud/"
35
+ )
36
+ self._cached_token: str | None = None
37
+ self._token_expiry: float = 0.0
38
+
39
+ async def get_token(self) -> str:
40
+ """Return a valid bearer token, fetching a new one when needed."""
41
+ now = time.monotonic()
42
+ if self._cached_token is not None and now < self._token_expiry:
43
+ return self._cached_token
44
+
45
+ body = (
46
+ f"grant_type=client_credentials"
47
+ f"&client_id={quote(self._client_id, safe='')}"
48
+ f"&client_secret={quote(self._client_secret, safe='')}"
49
+ )
50
+ response = await self._http.post(
51
+ TOKEN_ENDPOINT,
52
+ content=body,
53
+ headers={"Content-Type": "application/x-www-form-urlencoded"},
54
+ )
55
+ if response.status_code >= 400:
56
+ raise RuntimeError(
57
+ f"Failed to obtain Aspose bearer token: "
58
+ f"{response.status_code} - {response.text}"
59
+ )
60
+
61
+ data = response.json()
62
+ self._cached_token = data["access_token"]
63
+ # Subtract a 30-second buffer to avoid using a just-expired token
64
+ self._token_expiry = now + data.get("expires_in", 3600) - 30
65
+ return self._cached_token
@@ -0,0 +1,482 @@
1
+ """Aspose.HTML Cloud MCP Server -- stdio-based Model Context Protocol server.
2
+
3
+ Exposes Aspose.HTML Cloud document conversion and image vectorization as MCP
4
+ tools for AI assistants (Claude Desktop, Cursor, VS Code Copilot, etc.).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import base64
10
+ import json
11
+ import logging
12
+ import os
13
+ import sys
14
+ import time
15
+ from pathlib import Path
16
+ from typing import Any
17
+ from urllib.parse import urlparse
18
+
19
+ import httpx
20
+ from mcp.server.fastmcp import FastMCP
21
+
22
+ from .auth import AsposeHtmlAuth
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # Constants
26
+ # ---------------------------------------------------------------------------
27
+
28
+ SUPPORTED_INPUT_FORMATS = {"html", "mhtml", "xhtml", "epub", "svg", "md"}
29
+ SUPPORTED_OUTPUT_FORMATS = {
30
+ "pdf", "xps", "docx", "doc",
31
+ "jpeg", "png", "bmp", "gif", "tiff", "webp",
32
+ "md", "mhtml", "svg",
33
+ }
34
+ SUPPORTED_VECTORIZATION_FORMATS = {"png", "jpeg", "gif", "tiff", "bmp", "webp"}
35
+
36
+ API_BASE = os.environ.get("ASPOSE_HTML_API_URL", "https://api.aspose.cloud")
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # Logging -- write to stderr so stdout stays clean for JSON-RPC
40
+ # ---------------------------------------------------------------------------
41
+
42
+ logging.basicConfig(
43
+ stream=sys.stderr,
44
+ level=logging.INFO,
45
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
46
+ )
47
+ logger = logging.getLogger("aspose_html_cloud_mcp")
48
+
49
+ # ---------------------------------------------------------------------------
50
+ # Shared HTTP client & auth (created lazily on first tool call)
51
+ # ---------------------------------------------------------------------------
52
+
53
+ _http_client: httpx.AsyncClient | None = None
54
+ _auth: AsposeHtmlAuth | None = None
55
+
56
+
57
+ def _get_http_client() -> httpx.AsyncClient:
58
+ global _http_client
59
+ if _http_client is None:
60
+ _http_client = httpx.AsyncClient(timeout=httpx.Timeout(300.0))
61
+ return _http_client
62
+
63
+
64
+ def _get_auth() -> AsposeHtmlAuth:
65
+ global _auth
66
+ if _auth is None:
67
+ _auth = AsposeHtmlAuth(_get_http_client())
68
+ return _auth
69
+
70
+
71
+ # ---------------------------------------------------------------------------
72
+ # Result helpers
73
+ # ---------------------------------------------------------------------------
74
+
75
+ def _success(base64_data: str, output_format: str, size_bytes: int, source: str) -> dict[str, Any]:
76
+ return {
77
+ "success": True,
78
+ "message": f"Successfully converted to {output_format.upper()} ({size_bytes:,} bytes).",
79
+ "fileBase64": base64_data,
80
+ "outputFormat": output_format,
81
+ "fileSizeBytes": size_bytes,
82
+ "source": source,
83
+ }
84
+
85
+
86
+ def _failure(message: str) -> dict[str, Any]:
87
+ return {
88
+ "success": False,
89
+ "message": message,
90
+ }
91
+
92
+
93
+ # ---------------------------------------------------------------------------
94
+ # Validation
95
+ # ---------------------------------------------------------------------------
96
+
97
+ def _validate_formats(input_format: str, output_format: str) -> dict[str, Any] | None:
98
+ if input_format not in SUPPORTED_INPUT_FORMATS:
99
+ return _failure(
100
+ f"Unsupported input format '{input_format}'. "
101
+ f"Supported: {', '.join(sorted(SUPPORTED_INPUT_FORMATS))}."
102
+ )
103
+ if output_format not in SUPPORTED_OUTPUT_FORMATS:
104
+ return _failure(
105
+ f"Unsupported output format '{output_format}'. "
106
+ f"Supported: {', '.join(sorted(SUPPORTED_OUTPUT_FORMATS))}."
107
+ )
108
+ return None
109
+
110
+
111
+ def _is_http_url(value: str) -> bool:
112
+ try:
113
+ parsed = urlparse(value)
114
+ return parsed.scheme in ("http", "https") and bool(parsed.netloc)
115
+ except Exception:
116
+ return False
117
+
118
+
119
+ # ---------------------------------------------------------------------------
120
+ # Core API calls
121
+ # ---------------------------------------------------------------------------
122
+
123
+ async def _execute_conversion(
124
+ input_format: str,
125
+ output_format: str,
126
+ request_body: dict[str, Any],
127
+ source: str,
128
+ ) -> dict[str, Any]:
129
+ auth = _get_auth()
130
+ http = _get_http_client()
131
+ token = await auth.get_token()
132
+ endpoint = f"{API_BASE}/v4.0/html/conversion/{input_format}-{output_format}/sync"
133
+
134
+ logger.info("POST %s", endpoint)
135
+ logger.info("Request body: %s", json.dumps(request_body))
136
+
137
+ start = time.monotonic()
138
+ response = await http.post(
139
+ endpoint,
140
+ json=request_body,
141
+ headers={"Authorization": f"Bearer {token}"},
142
+ )
143
+ elapsed_ms = (time.monotonic() - start) * 1000
144
+ logger.info("Response: %d in %.0fms", response.status_code, elapsed_ms)
145
+
146
+ if response.status_code >= 400:
147
+ logger.error(
148
+ "Conversion failed. Status=%d Body=%s",
149
+ response.status_code, response.text,
150
+ )
151
+ return _failure(
152
+ f"Conversion API returned {response.status_code}: {response.text}"
153
+ )
154
+
155
+ file_bytes = response.content
156
+ logger.info("Conversion succeeded. Response size=%d bytes", len(file_bytes))
157
+
158
+ b64 = base64.b64encode(file_bytes).decode("ascii")
159
+ return _success(b64, output_format, len(file_bytes), source)
160
+
161
+
162
+ async def _execute_vectorization(
163
+ image_format: str,
164
+ request_body: dict[str, Any],
165
+ source: str,
166
+ ) -> dict[str, Any]:
167
+ auth = _get_auth()
168
+ http = _get_http_client()
169
+ token = await auth.get_token()
170
+ endpoint = f"{API_BASE}/v4.0/html/vectorization/{image_format}/sync"
171
+
172
+ logger.info("POST %s", endpoint)
173
+ logger.info("Request body: %s", json.dumps(request_body))
174
+
175
+ start = time.monotonic()
176
+ response = await http.post(
177
+ endpoint,
178
+ json=request_body,
179
+ headers={"Authorization": f"Bearer {token}"},
180
+ )
181
+ elapsed_ms = (time.monotonic() - start) * 1000
182
+ logger.info("Response: %d in %.0fms", response.status_code, elapsed_ms)
183
+
184
+ if response.status_code >= 400:
185
+ logger.error(
186
+ "Vectorization failed. Status=%d Body=%s",
187
+ response.status_code, response.text,
188
+ )
189
+ return _failure(
190
+ f"Vectorization API returned {response.status_code}: {response.text}"
191
+ )
192
+
193
+ file_bytes = response.content
194
+ logger.info("Vectorization succeeded. Response size=%d bytes", len(file_bytes))
195
+
196
+ b64 = base64.b64encode(file_bytes).decode("ascii")
197
+ return _success(b64, "svg", len(file_bytes), source)
198
+
199
+
200
+ async def _upload_file(file_path: str) -> str:
201
+ """Upload a local file to Aspose Cloud storage and return the storage path."""
202
+ auth = _get_auth()
203
+ http = _get_http_client()
204
+ token = await auth.get_token()
205
+ endpoint = f"{API_BASE}/v4.0/html/file"
206
+
207
+ file_name = Path(file_path).name
208
+ with open(file_path, "rb") as f:
209
+ file_data = f.read()
210
+
211
+ response = await http.post(
212
+ endpoint,
213
+ files={"file": (file_name, file_data, "application/octet-stream")},
214
+ headers={"Authorization": f"Bearer {token}"},
215
+ )
216
+
217
+ if response.status_code >= 400:
218
+ raise RuntimeError(
219
+ f"Upload failed: {response.status_code} - {response.text}"
220
+ )
221
+
222
+ data = response.json()
223
+ uploaded = data.get("uploaded", [])
224
+ if not uploaded:
225
+ raise RuntimeError("Upload succeeded but no file paths were returned.")
226
+ return uploaded[0]
227
+
228
+
229
+ # ---------------------------------------------------------------------------
230
+ # MCP Server & Tools
231
+ # ---------------------------------------------------------------------------
232
+
233
+ mcp = FastMCP(
234
+ "aspose-html-cloud",
235
+ )
236
+
237
+
238
+ @mcp.tool(
239
+ name="convert_url_to_format",
240
+ description=(
241
+ "Converts a publicly accessible URL (HTML, MHTML, XHTML, EPUB, SVG, or Markdown page) "
242
+ "to a target document format using Aspose.HTML Cloud. "
243
+ "Returns the converted file encoded as a Base64 string together with metadata. "
244
+ "Supported input formats: html, mhtml, xhtml, epub, svg, md. "
245
+ "Supported output formats: pdf, xps, docx, doc, jpeg, png, bmp, gif, tiff, webp, md, mhtml, svg."
246
+ ),
247
+ )
248
+ async def convert_url_to_format(
249
+ source_url: str,
250
+ output_format: str,
251
+ input_format: str = "html",
252
+ ) -> str:
253
+ """Convert a public URL to the requested format.
254
+
255
+ Args:
256
+ source_url: Publicly accessible URL of the source document (e.g. https://example.com/page.html).
257
+ output_format: Output format. Supported values: pdf, xps, docx, doc, jpeg, png, bmp, gif, tiff, webp, md, mhtml, svg.
258
+ input_format: Input format of the source URL. Defaults to 'html'. Supported values: html, mhtml, xhtml, epub, svg, md.
259
+ """
260
+ input_format = input_format.lower().strip()
261
+ output_format = output_format.lower().strip()
262
+
263
+ logger.info(
264
+ "convert_url_to_format: sourceUrl=%s %s->%s",
265
+ source_url, input_format, output_format,
266
+ )
267
+
268
+ validation = _validate_formats(input_format, output_format)
269
+ if validation is not None:
270
+ return json.dumps(validation)
271
+
272
+ if not _is_http_url(source_url):
273
+ return json.dumps(
274
+ _failure(f"'{source_url}' is not a valid absolute HTTP/HTTPS URL.")
275
+ )
276
+
277
+ result = await _execute_conversion(
278
+ input_format, output_format, {"InputPath": source_url}, source_url,
279
+ )
280
+ return json.dumps(result)
281
+
282
+
283
+ @mcp.tool(
284
+ name="convert_content_to_format",
285
+ description=(
286
+ "Converts raw HTML (or SVG, Markdown, etc.) content provided as a string "
287
+ "to a target document format using Aspose.HTML Cloud. "
288
+ "Use this when you have the markup content directly rather than a URL or file. "
289
+ "Returns the converted file encoded as a Base64 string together with metadata. "
290
+ "Supported input formats: html, mhtml, xhtml, epub, svg, md. "
291
+ "Supported output formats: pdf, xps, docx, doc, jpeg, png, bmp, gif, tiff, webp, md, mhtml, svg."
292
+ ),
293
+ )
294
+ async def convert_content_to_format(
295
+ content: str,
296
+ output_format: str,
297
+ input_format: str = "html",
298
+ ) -> str:
299
+ """Convert raw markup content to the requested format.
300
+
301
+ Args:
302
+ content: The markup content to convert (e.g. '<html><body><h1>Hello</h1></body></html>').
303
+ output_format: Output format. Supported values: pdf, xps, docx, doc, jpeg, png, bmp, gif, tiff, webp, md, mhtml, svg.
304
+ input_format: Input format of the content. Defaults to 'html'. Supported values: html, mhtml, xhtml, epub, svg, md.
305
+ """
306
+ input_format = input_format.lower().strip()
307
+ output_format = output_format.lower().strip()
308
+
309
+ logger.info(
310
+ "convert_content_to_format: contentLength=%d %s->%s",
311
+ len(content), input_format, output_format,
312
+ )
313
+
314
+ validation = _validate_formats(input_format, output_format)
315
+ if validation is not None:
316
+ return json.dumps(validation)
317
+
318
+ if not content or not content.strip():
319
+ return json.dumps(_failure("Content must not be empty."))
320
+
321
+ result = await _execute_conversion(
322
+ input_format, output_format, {"InputContent": content}, "(content)",
323
+ )
324
+ return json.dumps(result)
325
+
326
+
327
+ @mcp.tool(
328
+ name="convert_base64_to_format",
329
+ description=(
330
+ "Converts a Base64-encoded document (HTML, SVG, Markdown, etc.) "
331
+ "to a target document format using Aspose.HTML Cloud. "
332
+ "Use this when the source document is available as a Base64 string. "
333
+ "Returns the converted file encoded as a Base64 string together with metadata. "
334
+ "Supported input formats: html, mhtml, xhtml, epub, svg, md. "
335
+ "Supported output formats: pdf, xps, docx, doc, jpeg, png, bmp, gif, tiff, webp, md, mhtml, svg."
336
+ ),
337
+ )
338
+ async def convert_base64_to_format(
339
+ input_base64: str,
340
+ output_format: str,
341
+ input_format: str = "html",
342
+ ) -> str:
343
+ """Convert a Base64-encoded document to the requested format.
344
+
345
+ Args:
346
+ input_base64: Base64-encoded source document content.
347
+ output_format: Output format. Supported values: pdf, xps, docx, doc, jpeg, png, bmp, gif, tiff, webp, md, mhtml, svg.
348
+ input_format: Input format of the content. Defaults to 'html'. Supported values: html, mhtml, xhtml, epub, svg, md.
349
+ """
350
+ input_format = input_format.lower().strip()
351
+ output_format = output_format.lower().strip()
352
+
353
+ logger.info(
354
+ "convert_base64_to_format: base64Length=%d %s->%s",
355
+ len(input_base64), input_format, output_format,
356
+ )
357
+
358
+ validation = _validate_formats(input_format, output_format)
359
+ if validation is not None:
360
+ return json.dumps(validation)
361
+
362
+ if not input_base64 or not input_base64.strip():
363
+ return json.dumps(_failure("Base64 input must not be empty."))
364
+
365
+ result = await _execute_conversion(
366
+ input_format, output_format, {"InputBase64": input_base64}, "(base64)",
367
+ )
368
+ return json.dumps(result)
369
+
370
+
371
+ @mcp.tool(
372
+ name="convert_file_to_format",
373
+ description=(
374
+ "Converts a local file (HTML, MHTML, XHTML, EPUB, SVG, or Markdown) "
375
+ "to a target document format using Aspose.HTML Cloud. "
376
+ "The file is uploaded to the cloud first, then converted. "
377
+ "Returns the converted file encoded as a Base64 string together with metadata. "
378
+ "Supported input formats: html, mhtml, xhtml, epub, svg, md. "
379
+ "Supported output formats: pdf, xps, docx, doc, jpeg, png, bmp, gif, tiff, webp, md, mhtml, svg."
380
+ ),
381
+ )
382
+ async def convert_file_to_format(
383
+ file_path: str,
384
+ output_format: str,
385
+ input_format: str = "html",
386
+ ) -> str:
387
+ """Convert a local file to the requested format.
388
+
389
+ Args:
390
+ file_path: Absolute path to the local file to convert (e.g. C:\\docs\\page.html or /home/user/page.html).
391
+ output_format: Output format. Supported values: pdf, xps, docx, doc, jpeg, png, bmp, gif, tiff, webp, md, mhtml, svg.
392
+ input_format: Input format of the file. Defaults to 'html'. Supported values: html, mhtml, xhtml, epub, svg, md.
393
+ """
394
+ input_format = input_format.lower().strip()
395
+ output_format = output_format.lower().strip()
396
+
397
+ logger.info(
398
+ "convert_file_to_format: filePath=%s %s->%s",
399
+ file_path, input_format, output_format,
400
+ )
401
+
402
+ validation = _validate_formats(input_format, output_format)
403
+ if validation is not None:
404
+ return json.dumps(validation)
405
+
406
+ if not Path(file_path).is_file():
407
+ return json.dumps(_failure(f"File not found: '{file_path}'."))
408
+
409
+ try:
410
+ storage_path = await _upload_file(file_path)
411
+ logger.info("File uploaded to storage: %s", storage_path)
412
+ except Exception as exc:
413
+ logger.error("File upload failed for %s: %s", file_path, exc)
414
+ return json.dumps(_failure(f"File upload failed: {exc}"))
415
+
416
+ result = await _execute_conversion(
417
+ input_format, output_format, {"InputPath": storage_path}, file_path,
418
+ )
419
+ return json.dumps(result)
420
+
421
+
422
+ @mcp.tool(
423
+ name="vectorize_image",
424
+ description=(
425
+ "Vectorizes a raster image (PNG, JPEG, GIF, TIFF, BMP, or WebP) into SVG format "
426
+ "using Aspose.HTML Cloud. This performs true image vectorization, converting pixels "
427
+ "into scalable vector paths, not simply wrapping the image in an SVG element. "
428
+ "The image can be provided as a public URL, a local file path, or a Base64-encoded string. "
429
+ "Returns the resulting SVG file encoded as a Base64 string together with metadata."
430
+ ),
431
+ )
432
+ async def vectorize_image(
433
+ image_source: str,
434
+ image_format: str,
435
+ ) -> str:
436
+ """Vectorize a raster image into SVG.
437
+
438
+ Args:
439
+ image_source: Image source -- a public URL (e.g. https://example.com/photo.png), an absolute local file path (e.g. C:\\images\\photo.png), or a Base64-encoded image string.
440
+ image_format: Image format. Supported values: png, jpeg, gif, tiff, bmp, webp.
441
+ """
442
+ image_format = image_format.lower().strip()
443
+
444
+ logger.info("vectorize_image: source=%s format=%s", image_source, image_format)
445
+
446
+ if image_format not in SUPPORTED_VECTORIZATION_FORMATS:
447
+ return json.dumps(
448
+ _failure(
449
+ f"Unsupported image format '{image_format}'. "
450
+ f"Supported: {', '.join(sorted(SUPPORTED_VECTORIZATION_FORMATS))}."
451
+ )
452
+ )
453
+
454
+ if _is_http_url(image_source):
455
+ request_body: dict[str, Any] = {"InputPath": image_source}
456
+ source = image_source
457
+ elif Path(image_source).is_file():
458
+ try:
459
+ storage_path = await _upload_file(image_source)
460
+ logger.info("File uploaded to storage: %s", storage_path)
461
+ except Exception as exc:
462
+ logger.error("File upload failed for %s: %s", image_source, exc)
463
+ return json.dumps(_failure(f"File upload failed: {exc}"))
464
+ request_body = {"InputPath": storage_path}
465
+ source = image_source
466
+ else:
467
+ if not image_source or not image_source.strip():
468
+ return json.dumps(_failure("Image source must not be empty."))
469
+ request_body = {"InputBase64": image_source}
470
+ source = "(base64)"
471
+
472
+ result = await _execute_vectorization(image_format, request_body, source)
473
+ return json.dumps(result)
474
+
475
+
476
+ # ---------------------------------------------------------------------------
477
+ # Entry point
478
+ # ---------------------------------------------------------------------------
479
+
480
+ def main() -> None:
481
+ """Run the MCP server over stdio."""
482
+ mcp.run(transport="stdio")
File without changes