v0.2.0: Plug-and-play — dual transport (stdio+HTTP), AYON 1.15.4 compat, Dockerfile

- client.py: add verify=False for internal/self-signed SSL, catch 404
  on list_products() and get_last_version() (AYON >=1.15 compat)
- server.py: dual transport via AYON_TRANSPORT env (stdio default,
  http for Docker); uvicorn for HTTP mode
- pyproject.toml: bump to 0.2.0, add uvicorn dep, remove ayon-python-api
- README: full docs with Claude Desktop/Cursor/Hermes/Docker configs
- Dockerfile: multi-layer build, pip install from pyproject.toml
- .dockerignore: exclude venv, cache, dist
This commit is contained in:
Hermes Agent
2026-06-28 11:30:02 +00:00
parent 7a6e237c78
commit 3b0f64648b
6 changed files with 167 additions and 40 deletions
+7
View File
@@ -0,0 +1,7 @@
venv/
__pycache__/
*.pyc
.eggs/
dist/
*.egg-info/
.git/
+13
View File
@@ -0,0 +1,13 @@
FROM python:3.12-slim
WORKDIR /app
COPY pyproject.toml .
RUN pip install --no-cache-dir .
COPY src/ src/
ENV AYON_TRANSPORT=http
EXPOSE 8000
CMD ["ayon-mcp"]
+110 -23
View File
@@ -1,9 +1,13 @@
# AYON MCP Server # AYON MCP Server
**AI agents can publish CGI assets to AYON via Model Context Protocol.** **AI agents publish CGI assets to AYON via Model Context Protocol.**
MCP server that wraps AYON's REST API — agents use simple tool calls like `ayon-mcp` wraps AYON's REST API into six MCP tools — any MCP-compatible agent
`ayon_publish_texture_set` or `ayon_publish_model` instead of raw HTTP. can browse projects, list folders, and publish texture sets or 3D models with
simple tool calls. No raw HTTP, no manual JSON.
Works with **Claude Desktop**, **Cursor**, **Hermes Agent**, and any other
MCP client (stdio or HTTP transport).
## Tools ## Tools
@@ -24,7 +28,41 @@ MCP server that wraps AYON's REST API — agents use simple tool calls like
pip install git+https://git.niklashmotion.art/Hermes/ayon-mcp.git pip install git+https://git.niklashmotion.art/Hermes/ayon-mcp.git
``` ```
### 2. Configure Hermes Agent ### 2. Configure
Two environment variables required:
| Variable | Description |
|----------|-------------|
| `AYON_URL` | AYON server URL (e.g. `https://ayon.niklashmotion.art`) |
| `AYON_KEY` | AYON API key from Settings → API Keys |
Optional:
| Variable | Default | Description |
|----------|---------|-------------|
| `AYON_TRANSPORT` | `stdio` | `stdio` for Claude/Cursor, `http` for Docker |
| `PORT` | `8000` | HTTP port (only when `AYON_TRANSPORT=http`) |
### 3a. Use with Claude Desktop / Cursor (stdio)
Add to `claude_desktop_config.json` or Cursor's MCP config:
```json
{
"mcpServers": {
"ayon": {
"command": "ayon-mcp",
"env": {
"AYON_URL": "https://ayon.niklashmotion.art",
"AYON_KEY": "sk-..."
}
}
}
}
```
### 3b. Use with Hermes Agent (stdio)
Add to `~/.hermes/config.yaml`: Add to `~/.hermes/config.yaml`:
@@ -33,26 +71,45 @@ mcp_servers:
ayon: ayon:
command: "ayon-mcp" command: "ayon-mcp"
env: env:
AYON_SERVER_URL: "https://ayon.niklashmotion.art" AYON_URL: "https://ayon.niklashmotion.art"
AYON_API_KEY: "sk-..." AYON_KEY: "sk-..."
``` ```
Restart Hermes Agent — tools appear as `mcp_ayon_ayon_list_projects` etc. ### 3c. Docker (HTTP transport)
### 3. Use with any MCP client ```bash
docker run -d \
--name ayon-mcp \
-e AYON_URL=https://ayon.niklashmotion.art \
-e AYON_KEY=sk-... \
-e AYON_TRANSPORT=http \
-p 8000:8000 \
ghcr.io/niklas/ayon-mcp:latest
```
```json Then in Hermes `config.yaml`:
{
"mcpServers": { ```yaml
"ayon": { mcp_servers:
"command": "ayon-mcp", ayon:
"env": { url: "http://localhost:8000/mcp"
"AYON_SERVER_URL": "https://ayon.niklashmotion.art", timeout: 30
"AYON_API_KEY": "sk-..." ```
}
} ### 3d. Docker Compose
}
} ```yaml
services:
ayon-mcp:
build: .
container_name: mcp-ayon
environment:
- AYON_URL=https://ayon.example.com
- AYON_KEY=sk-...
- AYON_TRANSPORT=http
ports:
- "127.0.0.1:8001:8000"
restart: unless-stopped
``` ```
## Example: Agent publishes a texture set ## Example: Agent publishes a texture set
@@ -77,9 +134,9 @@ Agent calls ayon_publish_texture_set with:
## Architecture ## Architecture
``` ```
AI Agent (Hermes / Claude / Cursor) AI Agent (Claude Desktop / Cursor / Hermes)
│ MCP (stdio) │ MCP (stdio or HTTP)
ayon-mcp server ayon-mcp server
│ X-Api-Key header │ X-Api-Key header
@@ -87,12 +144,42 @@ ayon-mcp server
AYON REST API (/api/projects, /api/products, ...) AYON REST API (/api/projects, /api/products, ...)
``` ```
The MCP server is the **only gatekeeper** to AYON. All product types
(`imageMain`, `modelMain`) and naming conventions are enforced server-side.
The AI agent orchestrates the workflow but never touches raw API calls.
### Dual transport: stdio + HTTP
- **stdio** (default): `AYON_TRANSPORT=stdio` or unset. The MCP server communicates
over stdin/stdout — required for Claude Desktop and Cursor.
- **HTTP**: `AYON_TRANSPORT=http`. Listens on `PORT` (default 8000). Use for
Docker deployments or when the MCP server runs on a different host than the agent.
The same binary serves both modes — no separate builds needed.
## Compatibility
| Feature | AYON ≥1.15 | Older AYON |
|---------|-----------|------------|
| List projects | ✅ | ✅ |
| List folders | ✅ | ✅ |
| Create product | ✅ | ✅ |
| Create version | ✅ | ✅ |
| Create representation | ✅ | ✅ |
| List products (read-back) | ⚠️ falls back gracefully | ✅ |
| Get last version | ⚠️ falls back gracefully | ✅ |
AYON ≥1.15 removed the REST endpoints for listing products and versions.
The MCP server catches 404 and returns empty results — publish still works,
but read-back requires the GraphQL endpoint (not yet implemented in the MCP
tools).
## Limitations ## Limitations
- **REST-only**: Products are registered as metadata. Actual file upload into - **REST-only**: Products are registered as metadata. Actual file upload into
project anatomy requires the pyblish pipeline (DCC host). project anatomy requires the pyblish pipeline (DCC host).
- **No folder creation**: Folders must exist before publishing. Use AYON Web UI - **No folder creation**: Folders must exist before publishing. Use AYON Web UI
or Tray Publisher to create them first. or Tray Publisher to create them first, or create them via REST API.
- **Representations are metadata**: The `files` array contains names/sizes, - **Representations are metadata**: The `files` array contains names/sizes,
not uploaded content. Full file publishing needs a host with pyblish. not uploaded content. Full file publishing needs a host with pyblish.
+3 -3
View File
@@ -1,12 +1,12 @@
[project] [project]
name = "ayon-mcp" name = "ayon-mcp"
version = "0.1.0" version = "0.2.0"
description = "MCP server for AYON: AI agents can publish CGI assets via Model Context Protocol" description = "MCP server for AYON: AI agents publish CGI assets via Model Context Protocol. Works with Claude Desktop, Cursor, Hermes, and any MCP-compatible client."
requires-python = ">=3.10" requires-python = ">=3.10"
dependencies = [ dependencies = [
"mcp>=1.0.0", "mcp>=1.0.0",
"httpx>=0.27.0", "httpx>=0.27.0",
"ayon-python-api>=1.0.0", "uvicorn>=0.30.0",
] ]
[project.scripts] [project.scripts]
+26 -13
View File
@@ -22,6 +22,7 @@ class AyonClient:
self.url = (url or os.environ["AYON_URL"]).rstrip("/") self.url = (url or os.environ["AYON_URL"]).rstrip("/")
self.api_key = api_key or os.environ["AYON_KEY"] self.api_key = api_key or os.environ["AYON_KEY"]
self._client = httpx.Client( self._client = httpx.Client(
verify=False,
base_url=self.url, base_url=self.url,
headers={ headers={
"X-Api-Key": self.api_key, "X-Api-Key": self.api_key,
@@ -47,12 +48,18 @@ class AyonClient:
# ── Products ────────────────────────────────────────── # ── Products ──────────────────────────────────────────
def list_products(self, project: str, folder_id: str) -> list[dict]: def list_products(self, project: str, folder_id: str) -> list[dict]:
r = self._client.get( # AYON >=1.15 removed GET /products?folderId= — returns 404.
f"/api/projects/{project}/products", try:
params={"folderId": folder_id}, r = self._client.get(
) f"/api/projects/{project}/products",
r.raise_for_status() params={"folderId": folder_id},
return r.json()["products"] )
r.raise_for_status()
return r.json().get("products", [])
except httpx.HTTPStatusError as e:
if e.response.status_code == 404:
return []
raise
def create_product( def create_product(
self, self,
@@ -92,13 +99,19 @@ class AyonClient:
return r.json() return r.json()
def get_last_version(self, project: str, product_id: str) -> Optional[dict]: def get_last_version(self, project: str, product_id: str) -> Optional[dict]:
r = self._client.get( # AYON >=1.15 removed GET /versions?productId= — returns 404.
f"/api/projects/{project}/versions", try:
params={"productId": product_id, "latest": "true"}, r = self._client.get(
) f"/api/projects/{project}/versions",
r.raise_for_status() params={"productId": product_id, "latest": "true"},
versions = r.json().get("versions", []) )
return versions[0] if versions else None r.raise_for_status()
versions = r.json().get("versions", [])
return versions[0] if versions else None
except httpx.HTTPStatusError as e:
if e.response.status_code == 404:
return None
raise
# ── Representations ─────────────────────────────────── # ── Representations ───────────────────────────────────
+8 -1
View File
@@ -276,7 +276,14 @@ def main():
if not os.environ.get("AYON_KEY"): if not os.environ.get("AYON_KEY"):
print("ERROR: AYON_KEY not set", file=sys.stderr) print("ERROR: AYON_KEY not set", file=sys.stderr)
sys.exit(1) sys.exit(1)
app.run()
transport = os.environ.get("AYON_TRANSPORT", "stdio")
if transport == "http":
import uvicorn
port = int(os.environ.get("PORT", "8000"))
uvicorn.run(app.streamable_http_app(), host="0.0.0.0", port=port, log_level="info")
else:
app.run(transport="stdio")
if __name__ == "__main__": if __name__ == "__main__":