indox-mcp 0.7.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.
- indox_mcp-0.7.0/LICENSE +67 -0
- indox_mcp-0.7.0/PKG-INFO +200 -0
- indox_mcp-0.7.0/README.md +181 -0
- indox_mcp-0.7.0/indox_mcp/__init__.py +3 -0
- indox_mcp-0.7.0/indox_mcp/_version.py +1 -0
- indox_mcp-0.7.0/indox_mcp/auth.py +59 -0
- indox_mcp-0.7.0/indox_mcp/catalog.py +44 -0
- indox_mcp-0.7.0/indox_mcp/cli.py +35 -0
- indox_mcp-0.7.0/indox_mcp/config.py +76 -0
- indox_mcp-0.7.0/indox_mcp/hosted.py +45 -0
- indox_mcp-0.7.0/indox_mcp/jobs.py +65 -0
- indox_mcp-0.7.0/indox_mcp/runtime.py +53 -0
- indox_mcp-0.7.0/indox_mcp/server.py +35 -0
- indox_mcp-0.7.0/indox_mcp/tools/__init__.py +3 -0
- indox_mcp-0.7.0/indox_mcp/tools/_hints.py +14 -0
- indox_mcp-0.7.0/indox_mcp/tools/account.py +34 -0
- indox_mcp-0.7.0/indox_mcp/tools/convert.py +102 -0
- indox_mcp-0.7.0/indox_mcp/tools/pdf.py +49 -0
- indox_mcp-0.7.0/indox_mcp/tools/storage.py +38 -0
- indox_mcp-0.7.0/indox_mcp/tools/webhooks.py +59 -0
- indox_mcp-0.7.0/indox_mcp/workspace.py +70 -0
- indox_mcp-0.7.0/indox_mcp.egg-info/PKG-INFO +200 -0
- indox_mcp-0.7.0/indox_mcp.egg-info/SOURCES.txt +27 -0
- indox_mcp-0.7.0/indox_mcp.egg-info/dependency_links.txt +1 -0
- indox_mcp-0.7.0/indox_mcp.egg-info/entry_points.txt +2 -0
- indox_mcp-0.7.0/indox_mcp.egg-info/requires.txt +8 -0
- indox_mcp-0.7.0/indox_mcp.egg-info/top_level.txt +1 -0
- indox_mcp-0.7.0/pyproject.toml +43 -0
- indox_mcp-0.7.0/setup.cfg +4 -0
indox_mcp-0.7.0/LICENSE
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Copyright (c) 2026 Indox. All rights reserved.
|
|
2
|
+
|
|
3
|
+
PROPRIETARY LICENSE
|
|
4
|
+
|
|
5
|
+
This software (the "SDK") comprises the official client libraries for the Indox
|
|
6
|
+
API. It is made publicly readable so that developers can inspect, evaluate and
|
|
7
|
+
integrate against the Indox service. It is not open-source software.
|
|
8
|
+
|
|
9
|
+
GRANT
|
|
10
|
+
|
|
11
|
+
Subject to the terms below, Indox grants you a non-exclusive, non-transferable,
|
|
12
|
+
non-sublicensable, revocable, royalty-free licence to:
|
|
13
|
+
|
|
14
|
+
1. install and copy the SDK ONLY to the extent technically necessary to run it
|
|
15
|
+
within your own application, and ONLY for the purpose of calling the Indox
|
|
16
|
+
API under a valid Indox account;
|
|
17
|
+
2. modify the SDK ONLY as required to integrate it with that application; and
|
|
18
|
+
3. distribute the SDK, modified or unmodified, ONLY as a non-primary embedded
|
|
19
|
+
component of such an application, with this notice retained in full.
|
|
20
|
+
|
|
21
|
+
No other right is granted. Any use not expressly permitted above is reserved.
|
|
22
|
+
|
|
23
|
+
RESTRICTIONS
|
|
24
|
+
|
|
25
|
+
You may not, in whole or in part:
|
|
26
|
+
|
|
27
|
+
1. copy, publish, mirror, host or redistribute the SDK or its source as a
|
|
28
|
+
standalone work, under any name, on any package registry, repository or
|
|
29
|
+
distribution channel;
|
|
30
|
+
2. create, publish or distribute a derivative SDK, client library, wrapper,
|
|
31
|
+
port or translation of the SDK into any other language;
|
|
32
|
+
3. use the SDK, or knowledge derived from it, to build, operate, market or
|
|
33
|
+
assist any product or service that competes with Indox;
|
|
34
|
+
4. use the SDK or its source as input to train, fine-tune, evaluate or ground
|
|
35
|
+
any machine-learning model, or include it in any dataset for that purpose;
|
|
36
|
+
5. reverse engineer, decompile or study the SDK to replicate, reimplement or
|
|
37
|
+
circumvent the Indox API or any part of the Indox service;
|
|
38
|
+
6. use the SDK to access the Indox API without a valid account and API key, or
|
|
39
|
+
to exceed, evade or disguise the limits of your plan;
|
|
40
|
+
7. sell, lease, sublicense, rent or otherwise transfer the SDK or any right in
|
|
41
|
+
it to a third party;
|
|
42
|
+
8. remove, obscure or alter any copyright, trademark or licence notice; or
|
|
43
|
+
9. use the Indox name, logo or trademarks except to state factually that your
|
|
44
|
+
application integrates with the Indox API.
|
|
45
|
+
|
|
46
|
+
Where mandatory law grants you a right that this licence purports to restrict,
|
|
47
|
+
that restriction does not apply to you to the minimum extent required by law.
|
|
48
|
+
|
|
49
|
+
Use of the Indox API itself is governed separately by the Indox Terms of Service
|
|
50
|
+
and requires a valid account and API key. This licence covers the SDK source
|
|
51
|
+
only and grants no right to the service.
|
|
52
|
+
|
|
53
|
+
NO WARRANTY
|
|
54
|
+
|
|
55
|
+
THE SDK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
|
56
|
+
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
|
57
|
+
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL INDOX BE LIABLE FOR
|
|
58
|
+
ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT
|
|
59
|
+
OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SDK OR THE USE OR
|
|
60
|
+
OTHER DEALINGS IN THE SDK.
|
|
61
|
+
|
|
62
|
+
TERMINATION
|
|
63
|
+
|
|
64
|
+
This licence terminates automatically if you breach it. On termination you must
|
|
65
|
+
cease all use and destroy all copies of the SDK.
|
|
66
|
+
|
|
67
|
+
Questions: support@indox.org
|
indox_mcp-0.7.0/PKG-INFO
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: indox-mcp
|
|
3
|
+
Version: 0.7.0
|
|
4
|
+
Summary: MCP server for the Indox document and media conversion API.
|
|
5
|
+
Author-email: Indox <support@indox.org>
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
Keywords: indox,mcp,model-context-protocol,document-conversion
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: indox-client==0.7.0
|
|
12
|
+
Requires-Dist: mcp<3,>=2.3
|
|
13
|
+
Requires-Dist: uvicorn>=0.31
|
|
14
|
+
Provides-Extra: test
|
|
15
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
16
|
+
Requires-Dist: responses>=0.23.0; extra == "test"
|
|
17
|
+
Requires-Dist: ruff==0.17.3; extra == "test"
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
# indox-mcp
|
|
21
|
+
|
|
22
|
+
<!-- mcp-name: org.indox/indox-mcp -->
|
|
23
|
+
|
|
24
|
+
MCP server for the [Indox](https://indox.org) conversion API. It lets an AI client — Claude Code,
|
|
25
|
+
Claude Desktop, Cursor, VS Code — convert documents, images, video, 3D models and fonts, check
|
|
26
|
+
credits, manage stored files and webhooks, and ask questions about PDFs. Every tool is a thin
|
|
27
|
+
wrapper over the official Python SDK, [`indox-client`](https://pypi.org/project/indox-client/).
|
|
28
|
+
|
|
29
|
+
**Requires:** Python ≥ 3.11 · an Indox API key (`ak_…`, created in the dashboard under **API Keys**)
|
|
30
|
+
|
|
31
|
+
It runs two ways from the same tools:
|
|
32
|
+
|
|
33
|
+
| Mode | Run | Auth | Files |
|
|
34
|
+
|---|---|---|---|
|
|
35
|
+
| Local | `indox-mcp` (stdio) | `INDOX_API_KEY` in the server's environment | read and written inside `INDOX_MCP_WORKSPACE_DIR` only |
|
|
36
|
+
| Hosted | `https://mcp.indox.org/mcp` (Streamable HTTP) | `Authorization: Bearer <your API key>` | `file_url` or `s3_key` in, `download_url` out |
|
|
37
|
+
|
|
38
|
+
## Tools
|
|
39
|
+
|
|
40
|
+
| Tool | What it does | Mode |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| `health` | Check that the Indox API is reachable | both |
|
|
43
|
+
| `credits_balance` | Remaining credits — check before large batches | both |
|
|
44
|
+
| `usage_summary` | Recent conversion usage | both |
|
|
45
|
+
| `list_formats` | Input and output formats for a kind: `image`, `video`, `model`, `pdf`, `font` | both |
|
|
46
|
+
| `convert_file` | Start a conversion from one source: `file_path`, `file_url` or `s3_key`. Spends credits | both (`file_path` local only) |
|
|
47
|
+
| `get_conversion` | Current status of a conversion | both |
|
|
48
|
+
| `wait_conversion` | Poll until it finishes or the time limit passes (`finished=false` → call again) | both |
|
|
49
|
+
| `download_result` | Local: save the result into the workspace, never overwriting. Hosted: return its link | both |
|
|
50
|
+
| `list_files` | Files in your Indox storage | both |
|
|
51
|
+
| `create_upload_url` | A presigned upload URL; pass the returned `s3_key` to `convert_file` | both |
|
|
52
|
+
| `list_webhooks` · `create_webhook` · `test_webhook` · `delete_webhook` | Manage webhooks. The signing secret is never returned to the model | both |
|
|
53
|
+
| `pdf_form_fields` | List the fillable fields of a local PDF | local |
|
|
54
|
+
| `pdf_ai_ask` | Ask a question about a local PDF. Spends credits | local |
|
|
55
|
+
| `pdf_ai_job` | Status and answer of a `pdf_ai_ask` job | both |
|
|
56
|
+
|
|
57
|
+
Tools that spend credits or change data are marked as not read-only, so clients can ask before
|
|
58
|
+
running them. Text extracted from your documents is treated as data, never as instructions.
|
|
59
|
+
|
|
60
|
+
## Local mode (stdio)
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install indox-mcp
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The server starts only when every variable below is set, and names the first one missing.
|
|
67
|
+
|
|
68
|
+
| Variable | Value |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `INDOX_API_KEY` | your `ak_…` key |
|
|
71
|
+
| `INDOX_BASE_URL` | `https://indox.org` |
|
|
72
|
+
| `INDOX_MCP_WORKSPACE_DIR` | absolute path of the folder the server may read from and save results to |
|
|
73
|
+
| `INDOX_MCP_WAIT_MAX_SECONDS` | longest one `wait_conversion` call may poll, e.g. `120` |
|
|
74
|
+
| `INDOX_MCP_POLL_SECONDS` | seconds between status checks, e.g. `2` |
|
|
75
|
+
|
|
76
|
+
If a client cannot find `indox-mcp`, use the full path that `which indox-mcp` prints.
|
|
77
|
+
|
|
78
|
+
## Hosted mode
|
|
79
|
+
|
|
80
|
+
The hosted server is **not live yet**; it will be at `https://mcp.indox.org/mcp`. Send your API key
|
|
81
|
+
as `Authorization: Bearer ak_…`; each call runs with that key alone.
|
|
82
|
+
|
|
83
|
+
**Downloads:** a hosted `download_result` returns a link with `needs_api_key=true` — fetch it with
|
|
84
|
+
the same API key. Key-free signed links are planned; until they ship, hosted downloads need the
|
|
85
|
+
caller's key.
|
|
86
|
+
|
|
87
|
+
## Client setup
|
|
88
|
+
|
|
89
|
+
Replace `ak_your_key` and `/absolute/path/to/files` with your own values.
|
|
90
|
+
|
|
91
|
+
**Claude Code** — local, then hosted:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
claude mcp add --env INDOX_API_KEY=ak_your_key --env INDOX_BASE_URL=https://indox.org \
|
|
95
|
+
--env INDOX_MCP_WORKSPACE_DIR=/absolute/path/to/files \
|
|
96
|
+
--env INDOX_MCP_WAIT_MAX_SECONDS=120 --env INDOX_MCP_POLL_SECONDS=2 \
|
|
97
|
+
--transport stdio indox -- indox-mcp
|
|
98
|
+
|
|
99
|
+
claude mcp add --transport http indox-hosted https://mcp.indox.org/mcp \
|
|
100
|
+
--header "Authorization: Bearer ak_your_key"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Add `--scope project` to share the server through `.mcp.json`, or `--scope user` for every project.
|
|
104
|
+
|
|
105
|
+
**Claude Desktop** — Settings › Developer › Edit Config opens `claude_desktop_config.json`
|
|
106
|
+
(macOS `~/Library/Application Support/Claude/`, Windows `%APPDATA%\Claude\`):
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
{
|
|
110
|
+
"mcpServers": {
|
|
111
|
+
"indox": {
|
|
112
|
+
"command": "indox-mcp",
|
|
113
|
+
"env": {
|
|
114
|
+
"INDOX_API_KEY": "ak_your_key",
|
|
115
|
+
"INDOX_BASE_URL": "https://indox.org",
|
|
116
|
+
"INDOX_MCP_WORKSPACE_DIR": "/absolute/path/to/files",
|
|
117
|
+
"INDOX_MCP_WAIT_MAX_SECONDS": "120",
|
|
118
|
+
"INDOX_MCP_POLL_SECONDS": "2"
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The hosted server is added as a custom connector (Customize › Connectors › Add custom connector),
|
|
126
|
+
not in this file. An API key goes under **Request headers** as `Bearer ak_your_key`; that option is
|
|
127
|
+
in beta, and on Team and Enterprise plans every member would share one key — use local mode there.
|
|
128
|
+
|
|
129
|
+
**Cursor** — `.cursor/mcp.json` in a project, or `~/.cursor/mcp.json` for every project:
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"mcpServers": {
|
|
134
|
+
"indox": {
|
|
135
|
+
"type": "stdio",
|
|
136
|
+
"command": "indox-mcp",
|
|
137
|
+
"env": {
|
|
138
|
+
"INDOX_API_KEY": "${env:INDOX_API_KEY}",
|
|
139
|
+
"INDOX_BASE_URL": "https://indox.org",
|
|
140
|
+
"INDOX_MCP_WORKSPACE_DIR": "/absolute/path/to/files",
|
|
141
|
+
"INDOX_MCP_WAIT_MAX_SECONDS": "120",
|
|
142
|
+
"INDOX_MCP_POLL_SECONDS": "2"
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
"indox-hosted": {
|
|
146
|
+
"url": "https://mcp.indox.org/mcp",
|
|
147
|
+
"headers": { "Authorization": "Bearer ${env:INDOX_API_KEY}" }
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
**VS Code** (Copilot agent mode) — `.vscode/mcp.json`; VS Code prompts for the key and hides what you type:
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
{
|
|
157
|
+
"inputs": [
|
|
158
|
+
{ "type": "promptString", "id": "indox-key", "description": "Indox API key", "password": true }
|
|
159
|
+
],
|
|
160
|
+
"servers": {
|
|
161
|
+
"indox": {
|
|
162
|
+
"type": "stdio",
|
|
163
|
+
"command": "indox-mcp",
|
|
164
|
+
"env": {
|
|
165
|
+
"INDOX_API_KEY": "${input:indox-key}",
|
|
166
|
+
"INDOX_BASE_URL": "https://indox.org",
|
|
167
|
+
"INDOX_MCP_WORKSPACE_DIR": "/absolute/path/to/files",
|
|
168
|
+
"INDOX_MCP_WAIT_MAX_SECONDS": "120",
|
|
169
|
+
"INDOX_MCP_POLL_SECONDS": "2"
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
"indox-hosted": {
|
|
173
|
+
"type": "http",
|
|
174
|
+
"url": "https://mcp.indox.org/mcp",
|
|
175
|
+
"headers": { "Authorization": "Bearer ${input:indox-key}" }
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Run the HTTP server yourself
|
|
182
|
+
|
|
183
|
+
`indox-mcp --http` serves Streamable HTTP at `/mcp` and an open `/healthz`. Each request uses the
|
|
184
|
+
caller's own key; the server holds no key of its own. Required variables:
|
|
185
|
+
|
|
186
|
+
| Variable | Value |
|
|
187
|
+
|---|---|
|
|
188
|
+
| `INDOX_BASE_URL` | `https://indox.org` |
|
|
189
|
+
| `MCP_HOST` · `MCP_PORT` | bind address and port |
|
|
190
|
+
| `MCP_ALLOWED_HOSTS` | comma-separated `Host` values to accept, e.g. `mcp.example.com` |
|
|
191
|
+
| `MCP_ALLOWED_ORIGINS` | comma-separated browser origins to accept |
|
|
192
|
+
| `MCP_KEY_CACHE_SECONDS` · `MCP_KEY_CACHE_SIZE` | how long, and how many, verified keys are remembered (hashed, in memory) |
|
|
193
|
+
| `INDOX_MCP_WAIT_MAX_SECONDS` · `INDOX_MCP_POLL_SECONDS` | as in local mode; keep the wait low when hosted |
|
|
194
|
+
|
|
195
|
+
Client config formats checked on 2026-10-29 against
|
|
196
|
+
[Claude Code](https://code.claude.com/docs/en/mcp),
|
|
197
|
+
[Claude Desktop](https://modelcontextprotocol.io/docs/develop/connect-local-servers) and
|
|
198
|
+
[custom connectors](https://claude.com/docs/connectors/custom/remote-mcp),
|
|
199
|
+
[Cursor](https://cursor.com/docs/context/mcp) and
|
|
200
|
+
[VS Code](https://code.visualstudio.com/docs/copilot/reference/mcp-configuration).
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# indox-mcp
|
|
2
|
+
|
|
3
|
+
<!-- mcp-name: org.indox/indox-mcp -->
|
|
4
|
+
|
|
5
|
+
MCP server for the [Indox](https://indox.org) conversion API. It lets an AI client — Claude Code,
|
|
6
|
+
Claude Desktop, Cursor, VS Code — convert documents, images, video, 3D models and fonts, check
|
|
7
|
+
credits, manage stored files and webhooks, and ask questions about PDFs. Every tool is a thin
|
|
8
|
+
wrapper over the official Python SDK, [`indox-client`](https://pypi.org/project/indox-client/).
|
|
9
|
+
|
|
10
|
+
**Requires:** Python ≥ 3.11 · an Indox API key (`ak_…`, created in the dashboard under **API Keys**)
|
|
11
|
+
|
|
12
|
+
It runs two ways from the same tools:
|
|
13
|
+
|
|
14
|
+
| Mode | Run | Auth | Files |
|
|
15
|
+
|---|---|---|---|
|
|
16
|
+
| Local | `indox-mcp` (stdio) | `INDOX_API_KEY` in the server's environment | read and written inside `INDOX_MCP_WORKSPACE_DIR` only |
|
|
17
|
+
| Hosted | `https://mcp.indox.org/mcp` (Streamable HTTP) | `Authorization: Bearer <your API key>` | `file_url` or `s3_key` in, `download_url` out |
|
|
18
|
+
|
|
19
|
+
## Tools
|
|
20
|
+
|
|
21
|
+
| Tool | What it does | Mode |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| `health` | Check that the Indox API is reachable | both |
|
|
24
|
+
| `credits_balance` | Remaining credits — check before large batches | both |
|
|
25
|
+
| `usage_summary` | Recent conversion usage | both |
|
|
26
|
+
| `list_formats` | Input and output formats for a kind: `image`, `video`, `model`, `pdf`, `font` | both |
|
|
27
|
+
| `convert_file` | Start a conversion from one source: `file_path`, `file_url` or `s3_key`. Spends credits | both (`file_path` local only) |
|
|
28
|
+
| `get_conversion` | Current status of a conversion | both |
|
|
29
|
+
| `wait_conversion` | Poll until it finishes or the time limit passes (`finished=false` → call again) | both |
|
|
30
|
+
| `download_result` | Local: save the result into the workspace, never overwriting. Hosted: return its link | both |
|
|
31
|
+
| `list_files` | Files in your Indox storage | both |
|
|
32
|
+
| `create_upload_url` | A presigned upload URL; pass the returned `s3_key` to `convert_file` | both |
|
|
33
|
+
| `list_webhooks` · `create_webhook` · `test_webhook` · `delete_webhook` | Manage webhooks. The signing secret is never returned to the model | both |
|
|
34
|
+
| `pdf_form_fields` | List the fillable fields of a local PDF | local |
|
|
35
|
+
| `pdf_ai_ask` | Ask a question about a local PDF. Spends credits | local |
|
|
36
|
+
| `pdf_ai_job` | Status and answer of a `pdf_ai_ask` job | both |
|
|
37
|
+
|
|
38
|
+
Tools that spend credits or change data are marked as not read-only, so clients can ask before
|
|
39
|
+
running them. Text extracted from your documents is treated as data, never as instructions.
|
|
40
|
+
|
|
41
|
+
## Local mode (stdio)
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install indox-mcp
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The server starts only when every variable below is set, and names the first one missing.
|
|
48
|
+
|
|
49
|
+
| Variable | Value |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `INDOX_API_KEY` | your `ak_…` key |
|
|
52
|
+
| `INDOX_BASE_URL` | `https://indox.org` |
|
|
53
|
+
| `INDOX_MCP_WORKSPACE_DIR` | absolute path of the folder the server may read from and save results to |
|
|
54
|
+
| `INDOX_MCP_WAIT_MAX_SECONDS` | longest one `wait_conversion` call may poll, e.g. `120` |
|
|
55
|
+
| `INDOX_MCP_POLL_SECONDS` | seconds between status checks, e.g. `2` |
|
|
56
|
+
|
|
57
|
+
If a client cannot find `indox-mcp`, use the full path that `which indox-mcp` prints.
|
|
58
|
+
|
|
59
|
+
## Hosted mode
|
|
60
|
+
|
|
61
|
+
The hosted server is **not live yet**; it will be at `https://mcp.indox.org/mcp`. Send your API key
|
|
62
|
+
as `Authorization: Bearer ak_…`; each call runs with that key alone.
|
|
63
|
+
|
|
64
|
+
**Downloads:** a hosted `download_result` returns a link with `needs_api_key=true` — fetch it with
|
|
65
|
+
the same API key. Key-free signed links are planned; until they ship, hosted downloads need the
|
|
66
|
+
caller's key.
|
|
67
|
+
|
|
68
|
+
## Client setup
|
|
69
|
+
|
|
70
|
+
Replace `ak_your_key` and `/absolute/path/to/files` with your own values.
|
|
71
|
+
|
|
72
|
+
**Claude Code** — local, then hosted:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
claude mcp add --env INDOX_API_KEY=ak_your_key --env INDOX_BASE_URL=https://indox.org \
|
|
76
|
+
--env INDOX_MCP_WORKSPACE_DIR=/absolute/path/to/files \
|
|
77
|
+
--env INDOX_MCP_WAIT_MAX_SECONDS=120 --env INDOX_MCP_POLL_SECONDS=2 \
|
|
78
|
+
--transport stdio indox -- indox-mcp
|
|
79
|
+
|
|
80
|
+
claude mcp add --transport http indox-hosted https://mcp.indox.org/mcp \
|
|
81
|
+
--header "Authorization: Bearer ak_your_key"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Add `--scope project` to share the server through `.mcp.json`, or `--scope user` for every project.
|
|
85
|
+
|
|
86
|
+
**Claude Desktop** — Settings › Developer › Edit Config opens `claude_desktop_config.json`
|
|
87
|
+
(macOS `~/Library/Application Support/Claude/`, Windows `%APPDATA%\Claude\`):
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"mcpServers": {
|
|
92
|
+
"indox": {
|
|
93
|
+
"command": "indox-mcp",
|
|
94
|
+
"env": {
|
|
95
|
+
"INDOX_API_KEY": "ak_your_key",
|
|
96
|
+
"INDOX_BASE_URL": "https://indox.org",
|
|
97
|
+
"INDOX_MCP_WORKSPACE_DIR": "/absolute/path/to/files",
|
|
98
|
+
"INDOX_MCP_WAIT_MAX_SECONDS": "120",
|
|
99
|
+
"INDOX_MCP_POLL_SECONDS": "2"
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The hosted server is added as a custom connector (Customize › Connectors › Add custom connector),
|
|
107
|
+
not in this file. An API key goes under **Request headers** as `Bearer ak_your_key`; that option is
|
|
108
|
+
in beta, and on Team and Enterprise plans every member would share one key — use local mode there.
|
|
109
|
+
|
|
110
|
+
**Cursor** — `.cursor/mcp.json` in a project, or `~/.cursor/mcp.json` for every project:
|
|
111
|
+
|
|
112
|
+
```json
|
|
113
|
+
{
|
|
114
|
+
"mcpServers": {
|
|
115
|
+
"indox": {
|
|
116
|
+
"type": "stdio",
|
|
117
|
+
"command": "indox-mcp",
|
|
118
|
+
"env": {
|
|
119
|
+
"INDOX_API_KEY": "${env:INDOX_API_KEY}",
|
|
120
|
+
"INDOX_BASE_URL": "https://indox.org",
|
|
121
|
+
"INDOX_MCP_WORKSPACE_DIR": "/absolute/path/to/files",
|
|
122
|
+
"INDOX_MCP_WAIT_MAX_SECONDS": "120",
|
|
123
|
+
"INDOX_MCP_POLL_SECONDS": "2"
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
"indox-hosted": {
|
|
127
|
+
"url": "https://mcp.indox.org/mcp",
|
|
128
|
+
"headers": { "Authorization": "Bearer ${env:INDOX_API_KEY}" }
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**VS Code** (Copilot agent mode) — `.vscode/mcp.json`; VS Code prompts for the key and hides what you type:
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"inputs": [
|
|
139
|
+
{ "type": "promptString", "id": "indox-key", "description": "Indox API key", "password": true }
|
|
140
|
+
],
|
|
141
|
+
"servers": {
|
|
142
|
+
"indox": {
|
|
143
|
+
"type": "stdio",
|
|
144
|
+
"command": "indox-mcp",
|
|
145
|
+
"env": {
|
|
146
|
+
"INDOX_API_KEY": "${input:indox-key}",
|
|
147
|
+
"INDOX_BASE_URL": "https://indox.org",
|
|
148
|
+
"INDOX_MCP_WORKSPACE_DIR": "/absolute/path/to/files",
|
|
149
|
+
"INDOX_MCP_WAIT_MAX_SECONDS": "120",
|
|
150
|
+
"INDOX_MCP_POLL_SECONDS": "2"
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
"indox-hosted": {
|
|
154
|
+
"type": "http",
|
|
155
|
+
"url": "https://mcp.indox.org/mcp",
|
|
156
|
+
"headers": { "Authorization": "Bearer ${input:indox-key}" }
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Run the HTTP server yourself
|
|
163
|
+
|
|
164
|
+
`indox-mcp --http` serves Streamable HTTP at `/mcp` and an open `/healthz`. Each request uses the
|
|
165
|
+
caller's own key; the server holds no key of its own. Required variables:
|
|
166
|
+
|
|
167
|
+
| Variable | Value |
|
|
168
|
+
|---|---|
|
|
169
|
+
| `INDOX_BASE_URL` | `https://indox.org` |
|
|
170
|
+
| `MCP_HOST` · `MCP_PORT` | bind address and port |
|
|
171
|
+
| `MCP_ALLOWED_HOSTS` | comma-separated `Host` values to accept, e.g. `mcp.example.com` |
|
|
172
|
+
| `MCP_ALLOWED_ORIGINS` | comma-separated browser origins to accept |
|
|
173
|
+
| `MCP_KEY_CACHE_SECONDS` · `MCP_KEY_CACHE_SIZE` | how long, and how many, verified keys are remembered (hashed, in memory) |
|
|
174
|
+
| `INDOX_MCP_WAIT_MAX_SECONDS` · `INDOX_MCP_POLL_SECONDS` | as in local mode; keep the wait low when hosted |
|
|
175
|
+
|
|
176
|
+
Client config formats checked on 2026-10-29 against
|
|
177
|
+
[Claude Code](https://code.claude.com/docs/en/mcp),
|
|
178
|
+
[Claude Desktop](https://modelcontextprotocol.io/docs/develop/connect-local-servers) and
|
|
179
|
+
[custom connectors](https://claude.com/docs/connectors/custom/remote-mcp),
|
|
180
|
+
[Cursor](https://cursor.com/docs/context/mcp) and
|
|
181
|
+
[VS Code](https://code.visualstudio.com/docs/copilot/reference/mcp-configuration).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.7.0"
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import logging
|
|
5
|
+
import time
|
|
6
|
+
from collections import OrderedDict
|
|
7
|
+
from enum import Enum
|
|
8
|
+
|
|
9
|
+
import anyio
|
|
10
|
+
from indox_client import AuthenticationError, Indox, IndoxError, PermissionDeniedError
|
|
11
|
+
from mcp.server.auth.provider import AccessToken
|
|
12
|
+
|
|
13
|
+
log = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Probe(Enum):
|
|
17
|
+
VALID = "valid"
|
|
18
|
+
INVALID = "invalid"
|
|
19
|
+
UNKNOWN = "unknown"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ApiKeyVerifier:
|
|
23
|
+
def __init__(self, base_url: str, *, ttl_seconds: float, max_entries: int) -> None:
|
|
24
|
+
self._base_url = base_url
|
|
25
|
+
self._ttl = ttl_seconds
|
|
26
|
+
self._max = max_entries
|
|
27
|
+
self._accepted_until: OrderedDict[str, float] = OrderedDict()
|
|
28
|
+
|
|
29
|
+
async def verify_token(self, token: str) -> AccessToken | None:
|
|
30
|
+
digest = hashlib.sha256(token.encode()).hexdigest()
|
|
31
|
+
now = time.monotonic()
|
|
32
|
+
if self._accepted_until.get(digest, 0.0) > now:
|
|
33
|
+
self._accepted_until.move_to_end(digest)
|
|
34
|
+
else:
|
|
35
|
+
self._accepted_until.pop(digest, None)
|
|
36
|
+
probe = await anyio.to_thread.run_sync(self._probe, token)
|
|
37
|
+
if probe is Probe.INVALID:
|
|
38
|
+
return None
|
|
39
|
+
if probe is Probe.VALID:
|
|
40
|
+
self._remember(digest, now + self._ttl)
|
|
41
|
+
return AccessToken(token=token, client_id=f"api-key:{digest[:16]}", scopes=[])
|
|
42
|
+
|
|
43
|
+
def _remember(self, digest: str, until: float) -> None:
|
|
44
|
+
self._accepted_until[digest] = until
|
|
45
|
+
while len(self._accepted_until) > self._max:
|
|
46
|
+
self._accepted_until.popitem(last=False)
|
|
47
|
+
|
|
48
|
+
def _probe(self, token: str) -> Probe:
|
|
49
|
+
with Indox(api_key=token, base_url=self._base_url, max_retries=0) as client:
|
|
50
|
+
try:
|
|
51
|
+
client.billing.credits_balance()
|
|
52
|
+
except AuthenticationError:
|
|
53
|
+
return Probe.INVALID
|
|
54
|
+
except PermissionDeniedError:
|
|
55
|
+
return Probe.VALID
|
|
56
|
+
except IndoxError as exc:
|
|
57
|
+
log.warning("API key check unavailable, deferring to the API: %s", type(exc).__name__)
|
|
58
|
+
return Probe.UNKNOWN
|
|
59
|
+
return Probe.VALID
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
_MEDIA_GET = "GET /api/v1/media_core/conversion/{conversion_id}/"
|
|
2
|
+
_PDF_GET = "GET /api/v1/pdf_handler/conversion/{conversion_id}/"
|
|
3
|
+
_FONT_GET = "GET /api/v1/fonts/conversion/{conversion_id}/"
|
|
4
|
+
_STATUS = (_MEDIA_GET, _PDF_GET, _FONT_GET)
|
|
5
|
+
|
|
6
|
+
OPS: dict[str, tuple[str, ...]] = {
|
|
7
|
+
"health": ("GET /api/v1/health/",),
|
|
8
|
+
"credits_balance": ("GET /api/v1/subscription/credits/balance/",),
|
|
9
|
+
"usage_summary": ("GET /api/v1/user/usage/summary/",),
|
|
10
|
+
"list_formats": (
|
|
11
|
+
"GET /api/v1/convert/image/formats/",
|
|
12
|
+
"GET /api/v1/convert/video/formats/",
|
|
13
|
+
"GET /api/v1/convert/model/formats/",
|
|
14
|
+
"GET /api/v1/pdf_handler/formats/",
|
|
15
|
+
"GET /api/v1/fonts/formats/",
|
|
16
|
+
),
|
|
17
|
+
"convert_file": (
|
|
18
|
+
"POST /api/v1/convert/image/convert/",
|
|
19
|
+
"POST /api/v1/convert/video/convert/",
|
|
20
|
+
"POST /api/v1/convert/model/convert/",
|
|
21
|
+
"POST /api/v1/pdf_handler/convert/",
|
|
22
|
+
"POST /api/v1/fonts/upload/",
|
|
23
|
+
"POST /api/v1/fonts/convert/",
|
|
24
|
+
),
|
|
25
|
+
"get_conversion": _STATUS,
|
|
26
|
+
"wait_conversion": _STATUS,
|
|
27
|
+
"download_result": (
|
|
28
|
+
*_STATUS,
|
|
29
|
+
"GET /api/v1/media_core/files/{conversion_id}/download/",
|
|
30
|
+
"GET /api/v1/pdf_handler/{conversion_id}/download/",
|
|
31
|
+
"GET /api/v1/fonts/{conversion_id}/download/{filename}",
|
|
32
|
+
),
|
|
33
|
+
"list_files": ("GET /api/v1/user/storage/files/",),
|
|
34
|
+
"create_upload_url": ("POST /api/v1/user/storage/files/presign/",),
|
|
35
|
+
"list_webhooks": ("GET /api/v1/webhooks/",),
|
|
36
|
+
"create_webhook": ("POST /api/v1/webhooks/",),
|
|
37
|
+
"test_webhook": ("POST /api/v1/webhooks/{webhook_id}/test/",),
|
|
38
|
+
"delete_webhook": ("DELETE /api/v1/webhooks/{webhook_id}/",),
|
|
39
|
+
"pdf_form_fields": ("POST /api/v1/pdf_handler/form-fields/",),
|
|
40
|
+
"pdf_ai_ask": ("POST /api/v1/pdf_handler/ai/ask/",),
|
|
41
|
+
"pdf_ai_job": ("GET /api/v1/pdf_handler/ai/jobs/{job_id}/",),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
LOCAL_ONLY = frozenset({"pdf_form_fields", "pdf_ai_ask"})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
|
|
6
|
+
from ._version import __version__
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main(argv: Sequence[str] | None = None) -> None:
|
|
10
|
+
parser = argparse.ArgumentParser(
|
|
11
|
+
prog="indox-mcp",
|
|
12
|
+
description="Indox MCP server. Runs over stdio unless --http is given.",
|
|
13
|
+
)
|
|
14
|
+
parser.add_argument("--http", action="store_true", help="serve Streamable HTTP (hosted mode)")
|
|
15
|
+
parser.add_argument("--version", action="version", version=f"indox-mcp {__version__}")
|
|
16
|
+
args = parser.parse_args(argv)
|
|
17
|
+
if args.http:
|
|
18
|
+
from .config import hosted_config
|
|
19
|
+
from .hosted import serve
|
|
20
|
+
|
|
21
|
+
serve(hosted_config())
|
|
22
|
+
return
|
|
23
|
+
from .config import local_config
|
|
24
|
+
from .runtime import Runtime
|
|
25
|
+
from .server import build
|
|
26
|
+
|
|
27
|
+
cfg = local_config()
|
|
28
|
+
rt = Runtime(
|
|
29
|
+
base_url=cfg.base_url,
|
|
30
|
+
wait_max_seconds=cfg.wait_max_seconds,
|
|
31
|
+
poll_seconds=cfg.poll_seconds,
|
|
32
|
+
workspace=cfg.workspace,
|
|
33
|
+
api_key=cfg.api_key,
|
|
34
|
+
)
|
|
35
|
+
build(rt).run("stdio")
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def require(name: str) -> str:
|
|
9
|
+
value = os.environ.get(name, "").strip()
|
|
10
|
+
if not value:
|
|
11
|
+
raise SystemExit(f"{name} missing from the environment")
|
|
12
|
+
return value
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _number(name: str) -> float:
|
|
16
|
+
raw = require(name)
|
|
17
|
+
try:
|
|
18
|
+
value = float(raw)
|
|
19
|
+
except ValueError:
|
|
20
|
+
raise SystemExit(f"{name} must be a number, got {raw!r}") from None
|
|
21
|
+
if value <= 0:
|
|
22
|
+
raise SystemExit(f"{name} must be positive, got {raw!r}")
|
|
23
|
+
return value
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _list(name: str) -> list[str]:
|
|
27
|
+
return [item.strip() for item in require(name).split(",") if item.strip()]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class LocalConfig:
|
|
32
|
+
api_key: str
|
|
33
|
+
base_url: str
|
|
34
|
+
workspace: Path
|
|
35
|
+
wait_max_seconds: float
|
|
36
|
+
poll_seconds: float
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class HostedConfig:
|
|
41
|
+
base_url: str
|
|
42
|
+
host: str
|
|
43
|
+
port: int
|
|
44
|
+
allowed_hosts: list[str]
|
|
45
|
+
allowed_origins: list[str]
|
|
46
|
+
key_cache_seconds: float
|
|
47
|
+
key_cache_size: int
|
|
48
|
+
wait_max_seconds: float
|
|
49
|
+
poll_seconds: float
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def local_config() -> LocalConfig:
|
|
53
|
+
workspace = Path(require("INDOX_MCP_WORKSPACE_DIR")).expanduser().resolve()
|
|
54
|
+
if not workspace.is_dir():
|
|
55
|
+
raise SystemExit(f"INDOX_MCP_WORKSPACE_DIR is not a directory: {workspace}")
|
|
56
|
+
return LocalConfig(
|
|
57
|
+
api_key=require("INDOX_API_KEY"),
|
|
58
|
+
base_url=require("INDOX_BASE_URL"),
|
|
59
|
+
workspace=workspace,
|
|
60
|
+
wait_max_seconds=_number("INDOX_MCP_WAIT_MAX_SECONDS"),
|
|
61
|
+
poll_seconds=_number("INDOX_MCP_POLL_SECONDS"),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def hosted_config() -> HostedConfig:
|
|
66
|
+
return HostedConfig(
|
|
67
|
+
base_url=require("INDOX_BASE_URL"),
|
|
68
|
+
host=require("MCP_HOST"),
|
|
69
|
+
port=int(_number("MCP_PORT")),
|
|
70
|
+
allowed_hosts=_list("MCP_ALLOWED_HOSTS"),
|
|
71
|
+
allowed_origins=_list("MCP_ALLOWED_ORIGINS"),
|
|
72
|
+
key_cache_seconds=_number("MCP_KEY_CACHE_SECONDS"),
|
|
73
|
+
key_cache_size=int(_number("MCP_KEY_CACHE_SIZE")),
|
|
74
|
+
wait_max_seconds=_number("INDOX_MCP_WAIT_MAX_SECONDS"),
|
|
75
|
+
poll_seconds=_number("INDOX_MCP_POLL_SECONDS"),
|
|
76
|
+
)
|