maginary-mcp 0.3.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.
Files changed (42) hide show
  1. maginary_mcp-0.3.0/.env.example +22 -0
  2. maginary_mcp-0.3.0/.github/workflows/publish.yml +50 -0
  3. maginary_mcp-0.3.0/.gitignore +15 -0
  4. maginary_mcp-0.3.0/.python-version +1 -0
  5. maginary_mcp-0.3.0/Dockerfile +25 -0
  6. maginary_mcp-0.3.0/LICENSE +21 -0
  7. maginary_mcp-0.3.0/PKG-INFO +267 -0
  8. maginary_mcp-0.3.0/README.md +237 -0
  9. maginary_mcp-0.3.0/_actvenv.sh +24 -0
  10. maginary_mcp-0.3.0/_dev.sh +19 -0
  11. maginary_mcp-0.3.0/_ensurepyenv.sh +71 -0
  12. maginary_mcp-0.3.0/_install.sh +32 -0
  13. maginary_mcp-0.3.0/claude-skill/README.md +34 -0
  14. maginary_mcp-0.3.0/pyproject.toml +55 -0
  15. maginary_mcp-0.3.0/scripts/refresh_snapshot.py +52 -0
  16. maginary_mcp-0.3.0/server.json +33 -0
  17. maginary_mcp-0.3.0/src/maginary_mcp/SKILL.md +71 -0
  18. maginary_mcp-0.3.0/src/maginary_mcp/__init__.py +6 -0
  19. maginary_mcp-0.3.0/src/maginary_mcp/__main__.py +14 -0
  20. maginary_mcp-0.3.0/src/maginary_mcp/api.py +574 -0
  21. maginary_mcp-0.3.0/src/maginary_mcp/http_app.py +339 -0
  22. maginary_mcp-0.3.0/src/maginary_mcp/index.html +228 -0
  23. maginary_mcp-0.3.0/src/maginary_mcp/parameters_snapshot.json +573 -0
  24. maginary_mcp-0.3.0/src/maginary_mcp/params.py +176 -0
  25. maginary_mcp-0.3.0/src/maginary_mcp/server.py +646 -0
  26. maginary_mcp-0.3.0/src/maginary_mcp/skill.py +57 -0
  27. maginary_mcp-0.3.0/tests/conftest.py +18 -0
  28. maginary_mcp-0.3.0/tests/test_api_key_resolution.py +106 -0
  29. maginary_mcp-0.3.0/tests/test_checkout_and_polling.py +85 -0
  30. maginary_mcp-0.3.0/tests/test_error_detail_fallback.py +39 -0
  31. maginary_mcp-0.3.0/tests/test_forwarded_headers.py +91 -0
  32. maginary_mcp-0.3.0/tests/test_generate_error_shapes.py +245 -0
  33. maginary_mcp-0.3.0/tests/test_health.py +24 -0
  34. maginary_mcp-0.3.0/tests/test_hosted_host_header.py +58 -0
  35. maginary_mcp-0.3.0/tests/test_http_auth.py +29 -0
  36. maginary_mcp-0.3.0/tests/test_oauth_resource_server.py +127 -0
  37. maginary_mcp-0.3.0/tests/test_param_map.py +55 -0
  38. maginary_mcp-0.3.0/tests/test_payment_required.py +89 -0
  39. maginary_mcp-0.3.0/tests/test_skill.py +60 -0
  40. maginary_mcp-0.3.0/tests/test_tool_layer.py +80 -0
  41. maginary_mcp-0.3.0/tests/test_wait_for_generation.py +76 -0
  42. maginary_mcp-0.3.0/tests/test_x402_bridge.py +177 -0
@@ -0,0 +1,22 @@
1
+ # Copy to .env and fill in what you need. Nothing here is required to run the
2
+ # stdio server against the hosted backend — every var below has a working
3
+ # default. See README.md for the full explanation of each one.
4
+
5
+ # Bearer token from https://app.maginary.ai/dashboard#api-keys. Required for
6
+ # generate / get_generation / wait_for_generation. Catalog tools work without it.
7
+ MAGINARY_API_KEY=
8
+
9
+ # Override for staging or a self-hosted backend.
10
+ # MAGINARY_BASE_URL=https://app.maginary.ai/api
11
+
12
+ # --- hosted (maginary-mcp-http) mode only, below this line ---
13
+
14
+ # Sent to the backend as X-Forwarded-Host when MAGINARY_BASE_URL is internal.
15
+ # MAGINARY_PUBLIC_HOST=app.maginary.ai
16
+
17
+ # On: every /mcp call needs a Bearer (OAuth token or API key).
18
+ # MAGINARY_MCP_REQUIRE_AUTH=0
19
+
20
+ # MAGINARY_OAUTH_ISSUER=https://app.maginary.ai/o
21
+ # MAGINARY_MCP_RESOURCE_URL=https://mcp.maginary.ai/mcp
22
+ # MAGINARY_MCP_LOG_LEVEL=INFO
@@ -0,0 +1,50 @@
1
+ name: publish to pypi
2
+
3
+ # Triggered by deploy.prod.mcp.sh pushing a version tag (v0.3.1, etc.) after
4
+ # the maintainer has already reviewed and pushed the code itself — this
5
+ # workflow only ever republishes a commit that was already public and
6
+ # already reviewed locally, it doesn't add a new review step of its own.
7
+ #
8
+ # No PyPI token exists anywhere: this uses Trusted Publishing (OIDC). PyPI
9
+ # trusts THIS exact workflow file, in THIS exact repo, deploying to THIS
10
+ # exact GitHub Actions environment — configured once at
11
+ # https://pypi.org/manage/account/publishing/. The "pypi" environment
12
+ # below should have a required reviewer set in this repo's Settings ->
13
+ # Environments, so pushing a tag still needs one manual approval click
14
+ # before anything actually publishes.
15
+
16
+ on:
17
+ push:
18
+ tags:
19
+ - "v*"
20
+
21
+ permissions:
22
+ contents: read
23
+
24
+ jobs:
25
+ build:
26
+ runs-on: ubuntu-latest
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+ - uses: actions/setup-python@v5
30
+ with:
31
+ python-version: "3.11"
32
+ - run: python -m pip install --upgrade build
33
+ - run: python -m build
34
+ - uses: actions/upload-artifact@v4
35
+ with:
36
+ name: dist
37
+ path: dist/
38
+
39
+ publish:
40
+ needs: build
41
+ runs-on: ubuntu-latest
42
+ environment: pypi
43
+ permissions:
44
+ id-token: write # required for OIDC trusted publishing — nothing else needs it
45
+ steps:
46
+ - uses: actions/download-artifact@v4
47
+ with:
48
+ name: dist
49
+ path: dist/
50
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,15 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ venv/
6
+ build/
7
+ dist/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+ .public-mirror/
12
+ .pypi-build-venv/
13
+ .env
14
+ .env.pypi
15
+ .env.local
@@ -0,0 +1 @@
1
+ 3.11.8
@@ -0,0 +1,25 @@
1
+ # Hosted, multi-tenant Streamable-HTTP MCP server (mcp.maginary.ai).
2
+ # Runs alongside the engine on the same box; Django/agents reach it by URL.
3
+ # stdio distribution (uvx/pip) is unaffected — this image is HTTP-only.
4
+ FROM python:3.12-slim
5
+
6
+ WORKDIR /app
7
+
8
+ # Install the package with the `http` extra (pulls uvicorn). Copy metadata
9
+ # first for layer caching, then the source.
10
+ COPY pyproject.toml README.md ./
11
+ COPY src ./src
12
+ RUN pip install --no-cache-dir ".[http]"
13
+
14
+ # Backend REST base the tools forward to. Override at deploy time if the API
15
+ # lives elsewhere. The caller's own API key arrives per-request (Bearer header),
16
+ # so NO MAGINARY_API_KEY is set here — this server is multi-tenant.
17
+ ENV MAGINARY_BASE_URL="https://app.maginary.ai/api" \
18
+ MAGINARY_MCP_HOST="0.0.0.0" \
19
+ MAGINARY_MCP_PORT="8642"
20
+
21
+ EXPOSE 8642
22
+
23
+ # The MCP endpoint is served at /mcp. Front with the same reverse proxy as the
24
+ # engine, mapping mcp.maginary.ai -> this container:8642.
25
+ CMD ["maginary-mcp-http"]
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Maginary
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,267 @@
1
+ Metadata-Version: 2.5
2
+ Name: maginary-mcp
3
+ Version: 0.3.0
4
+ Summary: Model Context Protocol server for Maginary — enumerate flags, kick off generations, poll results.
5
+ Project-URL: Homepage, https://maginary.ai
6
+ Project-URL: Documentation, https://maginary.ai/docs
7
+ Project-URL: Repository, https://github.com/maginaryai/maginary-mcp
8
+ Project-URL: Parameters JSON, https://maginary.ai/docs/parameters.json
9
+ Author-email: Maginary <hi@maginary.ai>
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: ai,image-generation,llm-tools,maginary,mcp,midjourney,video-generation
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Software Development :: Libraries
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: httpx>=0.27.0
23
+ Requires-Dist: mcp<2,>=1.19.0
24
+ Provides-Extra: http
25
+ Requires-Dist: uvicorn>=0.30; extra == 'http'
26
+ Provides-Extra: test
27
+ Requires-Dist: pytest>=8; extra == 'test'
28
+ Requires-Dist: x402>=2.20.0; extra == 'test'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # maginary-mcp
32
+
33
+ Model Context Protocol server for [Maginary](https://maginary.ai) — enumerate the prompt-DSL flags the engine accepts, kick off generations, and poll for results, all from inside your MCP-compatible client (Claude Desktop, Cursor, Continue, custom).
34
+
35
+ ## why
36
+
37
+ Maginary uses a Midjourney-style `--flag` prompt DSL over an async HTTP API. This server:
38
+
39
+ - surfaces the full parameter catalog to your LLM so it can pick the right flags
40
+ - offers a one-shot `generate` tool that hits `POST /api/gens/`
41
+ - offers `get_generation` + `wait_for_generation` for polling to a terminal state
42
+ - works offline for the catalog tools (ships a bundled snapshot; refreshed from the live docs endpoint at startup when reachable)
43
+
44
+ ## install
45
+
46
+ ```bash
47
+ uvx maginary-mcp # ephemeral run via uv
48
+ # — or —
49
+ pip install maginary-mcp
50
+ maginary-mcp
51
+ ```
52
+
53
+ Requires Python 3.10+.
54
+
55
+ ## configuration
56
+
57
+ Environment variables:
58
+
59
+ | var | default | meaning |
60
+ |---|---|---|
61
+ | `MAGINARY_API_KEY` | — | Bearer token from [app.maginary.ai/dashboard#api-keys](https://app.maginary.ai/dashboard#api-keys). **Required** for `generate` / `get_generation` / `wait_for_generation`. Catalog tools work without it. |
62
+ | `MAGINARY_BASE_URL` | `https://app.maginary.ai/api` | Override for staging or self-hosted. |
63
+ | `MAGINARY_PUBLIC_HOST` | `app.maginary.ai` | Hosted mode only. Sent to the backend as `X-Forwarded-Host` (with `-Proto`/`-For`) when `MAGINARY_BASE_URL` is an internal address, so the backend builds public URLs. |
64
+ | `MAGINARY_MCP_REQUIRE_AUTH` | off | Hosted mode only. On: every `/mcp` call needs a Bearer (OAuth token or API key); without one the server answers 401 + `WWW-Authenticate` pointing at `/.well-known/oauth-protected-resource`, which is how Claude/ChatGPT start the login. Trade-off: a wallet-only agent has no Bearer to send, so with the gate on it must make its first x402 payment over plain HTTP (`POST /api/gens/` returns an API key) and connect with that key; the 401 body says so. |
65
+ | `MAGINARY_OAUTH_ISSUER` | `https://app.maginary.ai/o` | The authorization server named in the protected-resource metadata (the backend, django-oauth-toolkit). |
66
+ | `MAGINARY_MCP_RESOURCE_URL` | `https://mcp.maginary.ai/mcp` | This server's canonical resource identifier (RFC 8707 audience). |
67
+ | `MAGINARY_MCP_LOG_LEVEL` | `INFO` | Standard Python log level; goes to stderr (stdout is reserved for MCP JSON-RPC). |
68
+
69
+ ### Claude Desktop config
70
+
71
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or the equivalent on your OS:
72
+
73
+ ```json
74
+ {
75
+ "mcpServers": {
76
+ "maginary": {
77
+ "command": "uvx",
78
+ "args": ["maginary-mcp"],
79
+ "env": {
80
+ "MAGINARY_API_KEY": "sk-mag-…"
81
+ }
82
+ }
83
+ }
84
+ }
85
+ ```
86
+
87
+ ## hosted (no-install) — Streamable HTTP
88
+
89
+ Connect a client straight to the hosted server at `https://mcp.maginary.ai/mcp`.
90
+ Zero install — the server is multi-tenant, so each request is scoped to
91
+ whatever credential it arrives with. Two ways to authenticate, pick whichever
92
+ fits the client:
93
+
94
+ **Connect (OAuth)** — for Claude Desktop, claude.ai, and any other client that
95
+ speaks MCP's OAuth spec. Add the server with no headers at all:
96
+
97
+ ```json
98
+ {
99
+ "mcpServers": {
100
+ "maginary": { "url": "https://mcp.maginary.ai/mcp" }
101
+ }
102
+ }
103
+ ```
104
+
105
+ Click "Connect" in the client. It opens a login page on `app.maginary.ai`,
106
+ you sign in and approve the requested scopes, and the client holds the token
107
+ from then on — no key to generate or paste. Requires the server to be running
108
+ with `MAGINARY_MCP_REQUIRE_AUTH=1`; without it, no login is asked for at all.
109
+
110
+ **API key** — for any client that doesn't do the OAuth dance (or if you'd
111
+ rather not click through a login), generate a key at
112
+ [app.maginary.ai/dashboard#api-keys](https://app.maginary.ai/dashboard#api-keys)
113
+ and send it yourself:
114
+
115
+ ```json
116
+ {
117
+ "mcpServers": {
118
+ "maginary": {
119
+ "url": "https://mcp.maginary.ai/mcp",
120
+ "headers": { "Authorization": "Bearer sk-mag-…" }
121
+ }
122
+ }
123
+ }
124
+ ```
125
+
126
+ Both are equivalent once connected — same tools, same account. Catalog tools
127
+ work with no credential either way; `generate` / `get_generation` /
128
+ `wait_for_generation` need one. Run the hosted server yourself with:
129
+
130
+ ### paying inside the tool call (x402 over MCP)
131
+
132
+ No key at all? Call `generate` anyway. Out of credits (or no account), the
133
+ result is `isError: true` with the x402 PaymentRequired at the top level
134
+ (`accepts`, `resource`, …) plus `error: "payment_required"`. An x402-capable
135
+ MCP client — the x402 SDK's `x402MCPSession` — signs `accepts[0]` and calls
136
+ the same tool again with the payment in `_meta["x402/payment"]`. The server
137
+ forwards it to the backend as `PAYMENT-SIGNATURE`; the backend verifies,
138
+ settles on Base and, for a wallet with no account, creates one. The settled
139
+ result carries the on-chain receipt in `_meta["x402/payment-response"]` and
140
+ `x402_receipt`, and a first settlement returns `x402_account: {api_key,
141
+ wallet}`. Pass that key as `_meta["maginary/api_key"]` on later calls
142
+ (polling needs it), or open a new connection with it as the Bearer header.
143
+ The server holds no payment logic; everything is decided by the backend's
144
+ `/api/gens/` contract.
145
+
146
+ ### lost the key? recover it, no new payment
147
+
148
+ A key returned by `x402_account` is shown exactly once. If it's gone — the
149
+ agent never persisted it, or a human never wrote it down — paying again from
150
+ the *same* wallet does **not** hand back a second one: repeat payments just
151
+ add credits to the account. That's deliberate (an unbounded stream of fresh
152
+ keys from routine top-ups would be a bigger secret-exposure surface than
153
+ losing one, and would remove any reason to persist a key at all), so
154
+ recovery is a separate, explicit step: prove you hold the private key by
155
+ signing a short message, and the backend reissues a key.
156
+
157
+ ```
158
+ POST https://app.maginary.ai/api/auth/x402/recover-key/
159
+ {
160
+ "address": "0xYourWalletAddress",
161
+ "timestamp": 1741000000,
162
+ "signature": "0x..."
163
+ }
164
+ ```
165
+
166
+ `signature` is a standard `personal_sign` (EIP-191 — the same call MetaMask,
167
+ ethers' `signer.signMessage(str)`, or `eth_account`'s `Account.sign_message`
168
+ already expose) over the literal string:
169
+
170
+ ```
171
+ Maginary: issue a new API key for <address> at <timestamp>. This does not move funds.
172
+ ```
173
+
174
+ with `<address>` lowercased and `<timestamp>` the same unix seconds sent in
175
+ the body. The timestamp must be within 5 minutes of the server's clock (30 s
176
+ of future skew tolerated) — it's the only replay defense, so a stale or
177
+ reused signature is rejected the same as a wrong one. A 200 revokes every
178
+ existing key on the account and returns exactly one fresh one, in the body
179
+ and in `X-Maginary-Api-Key` — same shape as `x402_account`, so code that
180
+ already handles the first-payment response handles this response too.
181
+
182
+ **This is a plain backend REST call, not an MCP tool** — deliberately, for
183
+ the same reason the payment logic itself lives in the backend and not here:
184
+ the server holds no identity logic of its own, and any agent that can
185
+ already construct and sign the x402 payment above can construct and sign
186
+ this one the same way. The one place this needs to be *discoverable* from
187
+ inside an MCP session is the 402 itself: an anonymous `generate` call always
188
+ comes back `payment_required` before the backend has any idea which wallet
189
+ is asking, so its `error` text always names this endpoint alongside the
190
+ payment instructions — an agent that gets stuck here learns about it from
191
+ the exact same message it already parses to learn about paying in the first
192
+ place, no separate discovery step.
193
+
194
+ ```bash
195
+ pip install "maginary-mcp[http]"
196
+ maginary-mcp-http # serves /mcp on 0.0.0.0:8642 (MAGINARY_MCP_PORT to change)
197
+ # — or —
198
+ docker build -t maginary-mcp . && docker run -p 8642:8642 maginary-mcp
199
+ ```
200
+
201
+ The hosted server sets **no** `MAGINARY_API_KEY` (keys come per-request). Extra
202
+ env: `MAGINARY_MCP_HOST` (default `0.0.0.0`), `MAGINARY_MCP_PORT` (default `8642`).
203
+
204
+ ## Claude Skill
205
+
206
+ The server ships an [Agent Skill](https://docs.claude.com/en/docs/agents/skills) that
207
+ teaches the `--flag` DSL, model selection, and the async generate→poll flow:
208
+
209
+ ```bash
210
+ maginary-mcp --install-skill # -> ~/.claude/skills/maginary-image-gen/SKILL.md
211
+ ```
212
+
213
+ The skill stands on its own — hosts without MCP get the DSL plus the raw REST
214
+ calls (`POST /gens/` → poll). With the server connected, Claude instead calls
215
+ `search_parameters` for the authoritative flag list and `generate`/`wait_for_generation`
216
+ natively. Re-running updates it; local edits are protected unless you pass `--force`.
217
+ Source: [`src/maginary_mcp/SKILL.md`](src/maginary_mcp/SKILL.md).
218
+
219
+ ## tools
220
+
221
+ ### catalog (no auth)
222
+
223
+ - **`list_parameters(category?, status?, include_reserved=false)`** — enumerate the catalog
224
+ - **`search_parameters(query, category?, include_reserved=false)`** — text search over names / aliases / desc / examples
225
+ - **`get_parameter(name)`** — full record for one flag (canonical name or alias)
226
+
227
+ `list_parameters` responses include the `categories` / `statuses` taxonomy, and both
228
+ list/search responses carry `source` (`live` vs `bundled-snapshot`).
229
+
230
+ ### generation (auth required)
231
+
232
+ - **`generate(prompt, callback_url?)`** — `POST /api/gens/`
233
+ - **`get_generation(uuid)`** — `GET /api/gens/{uuid}/`
234
+ - **`wait_for_generation(uuid, timeout_s=45)`** — poll to `done` / `failed`; a `timeout` result means still running — call again
235
+
236
+ ## worked example
237
+
238
+ Inside an MCP-capable client, once configured:
239
+
240
+ > "Search the maginary catalog for anything about aspect ratio."
241
+
242
+ The LLM calls `search_parameters("aspect")` and gets back the `--ar` entry with values, examples, and supported models.
243
+
244
+ > "Now generate a cinematic portrait 16:9 with the flagship model."
245
+
246
+ The LLM calls `generate("a cinematic portrait --ar 16:9 --flagship")`, gets a `uuid`, then `wait_for_generation(uuid)` and reads `image_urls[]` out of the terminal record.
247
+
248
+ ## catalog freshness
249
+
250
+ - **Live fetch** on startup from `https://maginary.ai/docs/parameters.json`, 5-second timeout.
251
+ - **Bundled snapshot** at `src/maginary_mcp/parameters_snapshot.json` used as a fallback whenever live fetch fails (no network, docs site down, etc.).
252
+ - The snapshot is refreshed manually by the maintainer via `python scripts/refresh_snapshot.py` — deliberately not baked into the wheel build so a new snapshot always corresponds to a reviewed commit.
253
+
254
+ The `source` field on `list_parameters` / `search_parameters` responses tells you which one is active.
255
+
256
+ ## development
257
+
258
+ ```bash
259
+ cd mcp
260
+ python -m venv venv && source venv/bin/activate
261
+ pip install -e .
262
+ maginary-mcp # runs on stdio; kill with Ctrl+D
263
+ ```
264
+
265
+ ## license
266
+
267
+ MIT.
@@ -0,0 +1,237 @@
1
+ # maginary-mcp
2
+
3
+ Model Context Protocol server for [Maginary](https://maginary.ai) — enumerate the prompt-DSL flags the engine accepts, kick off generations, and poll for results, all from inside your MCP-compatible client (Claude Desktop, Cursor, Continue, custom).
4
+
5
+ ## why
6
+
7
+ Maginary uses a Midjourney-style `--flag` prompt DSL over an async HTTP API. This server:
8
+
9
+ - surfaces the full parameter catalog to your LLM so it can pick the right flags
10
+ - offers a one-shot `generate` tool that hits `POST /api/gens/`
11
+ - offers `get_generation` + `wait_for_generation` for polling to a terminal state
12
+ - works offline for the catalog tools (ships a bundled snapshot; refreshed from the live docs endpoint at startup when reachable)
13
+
14
+ ## install
15
+
16
+ ```bash
17
+ uvx maginary-mcp # ephemeral run via uv
18
+ # — or —
19
+ pip install maginary-mcp
20
+ maginary-mcp
21
+ ```
22
+
23
+ Requires Python 3.10+.
24
+
25
+ ## configuration
26
+
27
+ Environment variables:
28
+
29
+ | var | default | meaning |
30
+ |---|---|---|
31
+ | `MAGINARY_API_KEY` | — | Bearer token from [app.maginary.ai/dashboard#api-keys](https://app.maginary.ai/dashboard#api-keys). **Required** for `generate` / `get_generation` / `wait_for_generation`. Catalog tools work without it. |
32
+ | `MAGINARY_BASE_URL` | `https://app.maginary.ai/api` | Override for staging or self-hosted. |
33
+ | `MAGINARY_PUBLIC_HOST` | `app.maginary.ai` | Hosted mode only. Sent to the backend as `X-Forwarded-Host` (with `-Proto`/`-For`) when `MAGINARY_BASE_URL` is an internal address, so the backend builds public URLs. |
34
+ | `MAGINARY_MCP_REQUIRE_AUTH` | off | Hosted mode only. On: every `/mcp` call needs a Bearer (OAuth token or API key); without one the server answers 401 + `WWW-Authenticate` pointing at `/.well-known/oauth-protected-resource`, which is how Claude/ChatGPT start the login. Trade-off: a wallet-only agent has no Bearer to send, so with the gate on it must make its first x402 payment over plain HTTP (`POST /api/gens/` returns an API key) and connect with that key; the 401 body says so. |
35
+ | `MAGINARY_OAUTH_ISSUER` | `https://app.maginary.ai/o` | The authorization server named in the protected-resource metadata (the backend, django-oauth-toolkit). |
36
+ | `MAGINARY_MCP_RESOURCE_URL` | `https://mcp.maginary.ai/mcp` | This server's canonical resource identifier (RFC 8707 audience). |
37
+ | `MAGINARY_MCP_LOG_LEVEL` | `INFO` | Standard Python log level; goes to stderr (stdout is reserved for MCP JSON-RPC). |
38
+
39
+ ### Claude Desktop config
40
+
41
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or the equivalent on your OS:
42
+
43
+ ```json
44
+ {
45
+ "mcpServers": {
46
+ "maginary": {
47
+ "command": "uvx",
48
+ "args": ["maginary-mcp"],
49
+ "env": {
50
+ "MAGINARY_API_KEY": "sk-mag-…"
51
+ }
52
+ }
53
+ }
54
+ }
55
+ ```
56
+
57
+ ## hosted (no-install) — Streamable HTTP
58
+
59
+ Connect a client straight to the hosted server at `https://mcp.maginary.ai/mcp`.
60
+ Zero install — the server is multi-tenant, so each request is scoped to
61
+ whatever credential it arrives with. Two ways to authenticate, pick whichever
62
+ fits the client:
63
+
64
+ **Connect (OAuth)** — for Claude Desktop, claude.ai, and any other client that
65
+ speaks MCP's OAuth spec. Add the server with no headers at all:
66
+
67
+ ```json
68
+ {
69
+ "mcpServers": {
70
+ "maginary": { "url": "https://mcp.maginary.ai/mcp" }
71
+ }
72
+ }
73
+ ```
74
+
75
+ Click "Connect" in the client. It opens a login page on `app.maginary.ai`,
76
+ you sign in and approve the requested scopes, and the client holds the token
77
+ from then on — no key to generate or paste. Requires the server to be running
78
+ with `MAGINARY_MCP_REQUIRE_AUTH=1`; without it, no login is asked for at all.
79
+
80
+ **API key** — for any client that doesn't do the OAuth dance (or if you'd
81
+ rather not click through a login), generate a key at
82
+ [app.maginary.ai/dashboard#api-keys](https://app.maginary.ai/dashboard#api-keys)
83
+ and send it yourself:
84
+
85
+ ```json
86
+ {
87
+ "mcpServers": {
88
+ "maginary": {
89
+ "url": "https://mcp.maginary.ai/mcp",
90
+ "headers": { "Authorization": "Bearer sk-mag-…" }
91
+ }
92
+ }
93
+ }
94
+ ```
95
+
96
+ Both are equivalent once connected — same tools, same account. Catalog tools
97
+ work with no credential either way; `generate` / `get_generation` /
98
+ `wait_for_generation` need one. Run the hosted server yourself with:
99
+
100
+ ### paying inside the tool call (x402 over MCP)
101
+
102
+ No key at all? Call `generate` anyway. Out of credits (or no account), the
103
+ result is `isError: true` with the x402 PaymentRequired at the top level
104
+ (`accepts`, `resource`, …) plus `error: "payment_required"`. An x402-capable
105
+ MCP client — the x402 SDK's `x402MCPSession` — signs `accepts[0]` and calls
106
+ the same tool again with the payment in `_meta["x402/payment"]`. The server
107
+ forwards it to the backend as `PAYMENT-SIGNATURE`; the backend verifies,
108
+ settles on Base and, for a wallet with no account, creates one. The settled
109
+ result carries the on-chain receipt in `_meta["x402/payment-response"]` and
110
+ `x402_receipt`, and a first settlement returns `x402_account: {api_key,
111
+ wallet}`. Pass that key as `_meta["maginary/api_key"]` on later calls
112
+ (polling needs it), or open a new connection with it as the Bearer header.
113
+ The server holds no payment logic; everything is decided by the backend's
114
+ `/api/gens/` contract.
115
+
116
+ ### lost the key? recover it, no new payment
117
+
118
+ A key returned by `x402_account` is shown exactly once. If it's gone — the
119
+ agent never persisted it, or a human never wrote it down — paying again from
120
+ the *same* wallet does **not** hand back a second one: repeat payments just
121
+ add credits to the account. That's deliberate (an unbounded stream of fresh
122
+ keys from routine top-ups would be a bigger secret-exposure surface than
123
+ losing one, and would remove any reason to persist a key at all), so
124
+ recovery is a separate, explicit step: prove you hold the private key by
125
+ signing a short message, and the backend reissues a key.
126
+
127
+ ```
128
+ POST https://app.maginary.ai/api/auth/x402/recover-key/
129
+ {
130
+ "address": "0xYourWalletAddress",
131
+ "timestamp": 1741000000,
132
+ "signature": "0x..."
133
+ }
134
+ ```
135
+
136
+ `signature` is a standard `personal_sign` (EIP-191 — the same call MetaMask,
137
+ ethers' `signer.signMessage(str)`, or `eth_account`'s `Account.sign_message`
138
+ already expose) over the literal string:
139
+
140
+ ```
141
+ Maginary: issue a new API key for <address> at <timestamp>. This does not move funds.
142
+ ```
143
+
144
+ with `<address>` lowercased and `<timestamp>` the same unix seconds sent in
145
+ the body. The timestamp must be within 5 minutes of the server's clock (30 s
146
+ of future skew tolerated) — it's the only replay defense, so a stale or
147
+ reused signature is rejected the same as a wrong one. A 200 revokes every
148
+ existing key on the account and returns exactly one fresh one, in the body
149
+ and in `X-Maginary-Api-Key` — same shape as `x402_account`, so code that
150
+ already handles the first-payment response handles this response too.
151
+
152
+ **This is a plain backend REST call, not an MCP tool** — deliberately, for
153
+ the same reason the payment logic itself lives in the backend and not here:
154
+ the server holds no identity logic of its own, and any agent that can
155
+ already construct and sign the x402 payment above can construct and sign
156
+ this one the same way. The one place this needs to be *discoverable* from
157
+ inside an MCP session is the 402 itself: an anonymous `generate` call always
158
+ comes back `payment_required` before the backend has any idea which wallet
159
+ is asking, so its `error` text always names this endpoint alongside the
160
+ payment instructions — an agent that gets stuck here learns about it from
161
+ the exact same message it already parses to learn about paying in the first
162
+ place, no separate discovery step.
163
+
164
+ ```bash
165
+ pip install "maginary-mcp[http]"
166
+ maginary-mcp-http # serves /mcp on 0.0.0.0:8642 (MAGINARY_MCP_PORT to change)
167
+ # — or —
168
+ docker build -t maginary-mcp . && docker run -p 8642:8642 maginary-mcp
169
+ ```
170
+
171
+ The hosted server sets **no** `MAGINARY_API_KEY` (keys come per-request). Extra
172
+ env: `MAGINARY_MCP_HOST` (default `0.0.0.0`), `MAGINARY_MCP_PORT` (default `8642`).
173
+
174
+ ## Claude Skill
175
+
176
+ The server ships an [Agent Skill](https://docs.claude.com/en/docs/agents/skills) that
177
+ teaches the `--flag` DSL, model selection, and the async generate→poll flow:
178
+
179
+ ```bash
180
+ maginary-mcp --install-skill # -> ~/.claude/skills/maginary-image-gen/SKILL.md
181
+ ```
182
+
183
+ The skill stands on its own — hosts without MCP get the DSL plus the raw REST
184
+ calls (`POST /gens/` → poll). With the server connected, Claude instead calls
185
+ `search_parameters` for the authoritative flag list and `generate`/`wait_for_generation`
186
+ natively. Re-running updates it; local edits are protected unless you pass `--force`.
187
+ Source: [`src/maginary_mcp/SKILL.md`](src/maginary_mcp/SKILL.md).
188
+
189
+ ## tools
190
+
191
+ ### catalog (no auth)
192
+
193
+ - **`list_parameters(category?, status?, include_reserved=false)`** — enumerate the catalog
194
+ - **`search_parameters(query, category?, include_reserved=false)`** — text search over names / aliases / desc / examples
195
+ - **`get_parameter(name)`** — full record for one flag (canonical name or alias)
196
+
197
+ `list_parameters` responses include the `categories` / `statuses` taxonomy, and both
198
+ list/search responses carry `source` (`live` vs `bundled-snapshot`).
199
+
200
+ ### generation (auth required)
201
+
202
+ - **`generate(prompt, callback_url?)`** — `POST /api/gens/`
203
+ - **`get_generation(uuid)`** — `GET /api/gens/{uuid}/`
204
+ - **`wait_for_generation(uuid, timeout_s=45)`** — poll to `done` / `failed`; a `timeout` result means still running — call again
205
+
206
+ ## worked example
207
+
208
+ Inside an MCP-capable client, once configured:
209
+
210
+ > "Search the maginary catalog for anything about aspect ratio."
211
+
212
+ The LLM calls `search_parameters("aspect")` and gets back the `--ar` entry with values, examples, and supported models.
213
+
214
+ > "Now generate a cinematic portrait 16:9 with the flagship model."
215
+
216
+ The LLM calls `generate("a cinematic portrait --ar 16:9 --flagship")`, gets a `uuid`, then `wait_for_generation(uuid)` and reads `image_urls[]` out of the terminal record.
217
+
218
+ ## catalog freshness
219
+
220
+ - **Live fetch** on startup from `https://maginary.ai/docs/parameters.json`, 5-second timeout.
221
+ - **Bundled snapshot** at `src/maginary_mcp/parameters_snapshot.json` used as a fallback whenever live fetch fails (no network, docs site down, etc.).
222
+ - The snapshot is refreshed manually by the maintainer via `python scripts/refresh_snapshot.py` — deliberately not baked into the wheel build so a new snapshot always corresponds to a reviewed commit.
223
+
224
+ The `source` field on `list_parameters` / `search_parameters` responses tells you which one is active.
225
+
226
+ ## development
227
+
228
+ ```bash
229
+ cd mcp
230
+ python -m venv venv && source venv/bin/activate
231
+ pip install -e .
232
+ maginary-mcp # runs on stdio; kill with Ctrl+D
233
+ ```
234
+
235
+ ## license
236
+
237
+ MIT.
@@ -0,0 +1,24 @@
1
+ #!/bin/bash
2
+
3
+ # Check if the script is being sourced
4
+ if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
5
+ echo "This script must be sourced (. _actvenv.sh), not executed directly."
6
+ exit 1
7
+ fi
8
+
9
+ # Returns 0 (true) if running inside a Docker/Podman container, 1 (false) otherwise.
10
+ # Uses Linux cgroup/mountinfo; on macOS both files are absent so both greps fail → not in container.
11
+ # Defined here so every script that sources _actvenv.sh can call it without duplicating the logic.
12
+ _in_container() {
13
+ grep -Eq "(docker|podman)" /proc/1/cgroup 2>/dev/null || \
14
+ grep -q docker /proc/self/mountinfo 2>/dev/null
15
+ }
16
+
17
+ if ! _in_container; then
18
+ # Not in a container — activate the local venv.
19
+ # Use BASH_SOURCE so this works regardless of the caller's CWD.
20
+ # Namespaced name + unset avoids polluting the caller's environment.
21
+ _ACTVENV_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
22
+ . "${_ACTVENV_DIR}/venv/bin/activate"
23
+ unset _ACTVENV_DIR
24
+ fi