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.
- maginary_mcp-0.3.0/.env.example +22 -0
- maginary_mcp-0.3.0/.github/workflows/publish.yml +50 -0
- maginary_mcp-0.3.0/.gitignore +15 -0
- maginary_mcp-0.3.0/.python-version +1 -0
- maginary_mcp-0.3.0/Dockerfile +25 -0
- maginary_mcp-0.3.0/LICENSE +21 -0
- maginary_mcp-0.3.0/PKG-INFO +267 -0
- maginary_mcp-0.3.0/README.md +237 -0
- maginary_mcp-0.3.0/_actvenv.sh +24 -0
- maginary_mcp-0.3.0/_dev.sh +19 -0
- maginary_mcp-0.3.0/_ensurepyenv.sh +71 -0
- maginary_mcp-0.3.0/_install.sh +32 -0
- maginary_mcp-0.3.0/claude-skill/README.md +34 -0
- maginary_mcp-0.3.0/pyproject.toml +55 -0
- maginary_mcp-0.3.0/scripts/refresh_snapshot.py +52 -0
- maginary_mcp-0.3.0/server.json +33 -0
- maginary_mcp-0.3.0/src/maginary_mcp/SKILL.md +71 -0
- maginary_mcp-0.3.0/src/maginary_mcp/__init__.py +6 -0
- maginary_mcp-0.3.0/src/maginary_mcp/__main__.py +14 -0
- maginary_mcp-0.3.0/src/maginary_mcp/api.py +574 -0
- maginary_mcp-0.3.0/src/maginary_mcp/http_app.py +339 -0
- maginary_mcp-0.3.0/src/maginary_mcp/index.html +228 -0
- maginary_mcp-0.3.0/src/maginary_mcp/parameters_snapshot.json +573 -0
- maginary_mcp-0.3.0/src/maginary_mcp/params.py +176 -0
- maginary_mcp-0.3.0/src/maginary_mcp/server.py +646 -0
- maginary_mcp-0.3.0/src/maginary_mcp/skill.py +57 -0
- maginary_mcp-0.3.0/tests/conftest.py +18 -0
- maginary_mcp-0.3.0/tests/test_api_key_resolution.py +106 -0
- maginary_mcp-0.3.0/tests/test_checkout_and_polling.py +85 -0
- maginary_mcp-0.3.0/tests/test_error_detail_fallback.py +39 -0
- maginary_mcp-0.3.0/tests/test_forwarded_headers.py +91 -0
- maginary_mcp-0.3.0/tests/test_generate_error_shapes.py +245 -0
- maginary_mcp-0.3.0/tests/test_health.py +24 -0
- maginary_mcp-0.3.0/tests/test_hosted_host_header.py +58 -0
- maginary_mcp-0.3.0/tests/test_http_auth.py +29 -0
- maginary_mcp-0.3.0/tests/test_oauth_resource_server.py +127 -0
- maginary_mcp-0.3.0/tests/test_param_map.py +55 -0
- maginary_mcp-0.3.0/tests/test_payment_required.py +89 -0
- maginary_mcp-0.3.0/tests/test_skill.py +60 -0
- maginary_mcp-0.3.0/tests/test_tool_layer.py +80 -0
- maginary_mcp-0.3.0/tests/test_wait_for_generation.py +76 -0
- 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 @@
|
|
|
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
|