Initial commit: AYON MCP server — AI agents publish CGI assets

Tools:
- ayon_list_projects / ayon_list_folders / ayon_list_products
- ayon_get_product
- ayon_publish_texture_set (multiple maps as one product)
- ayon_publish_model (ABC, USD, FBX, ...)

FastMCP-based, wraps AYON REST API with X-Api-Key auth.
Compatible with Hermes native MCP client and any MCP-compatible agent.
This commit is contained in:
2026-06-07 00:00:02 +00:00
commit 8ffa9588fe
6 changed files with 545 additions and 0 deletions
+267
View File
@@ -0,0 +1,267 @@
"""AYON MCP Server — main entry point.
Usage:
AYON_SERVER_URL=https://ayon.example.com \\
AYON_API_KEY=*** \\
ayon-mcp
Hermes config.yaml:
mcp_servers:
ayon:
command: "ayon-mcp"
env:
AYON_SERVER_URL: "https://ayon.niklashmotion.art"
AYON_API_KEY: "sk-..."
"""
import json
import os
import sys
from typing import Optional
from mcp.server.fastmcp import FastMCP
from .client import AyonClient
# ── Bootstrap ────────────────────────────────────────────
app = FastMCP("ayon-publish")
client = AyonClient()
def _ok(**kwargs) -> str:
return json.dumps(dict(status="ok", **kwargs), indent=2, ensure_ascii=False)
def _err(msg: str) -> str:
return json.dumps({"error": msg}, indent=2)
# ── Discovery Tools ──────────────────────────────────────
@app.tool()
async def ayon_list_projects() -> str:
"""List all available AYON projects."""
try:
projects = client.list_projects()
summary = [{"name": p["name"], "code": p.get("code")} for p in projects]
return json.dumps(summary, indent=2)
except Exception as e:
return _err(str(e))
@app.tool()
async def ayon_list_folders(project: str) -> str:
"""List folders in an AYON project.
Args:
project: Project name (e.g. 'MyProject')
"""
try:
folders = client.list_folders(project)
summary = [
{"path": f.get("path"), "type": f.get("folderType"), "id": f.get("id")}
for f in folders
]
return json.dumps(summary, indent=2)
except Exception as e:
return _err(str(e))
@app.tool()
async def ayon_list_products(project: str, folder_path: str) -> str:
"""List published products in a folder.
Args:
project: Project name
folder_path: Folder path (e.g. '/assets/chair')
"""
try:
folder = client.find_folder(project, folder_path)
if not folder:
return _err(f"Folder '{folder_path}' not found in '{project}'")
products = client.list_products(project, folder["id"])
summary = [
{"name": p["name"], "type": p.get("productType"), "id": p["id"]}
for p in products
]
return json.dumps(summary, indent=2)
except Exception as e:
return _err(str(e))
@app.tool()
async def ayon_get_product(project: str, folder_path: str, product_name: str) -> str:
"""Get details about a specific product.
Args:
project: Project name
folder_path: Folder path
product_name: Product name to find
"""
try:
folder = client.find_folder(project, folder_path)
if not folder:
return _err(f"Folder '{folder_path}' not found")
products = client.list_products(project, folder["id"])
for p in products:
if p["name"] == product_name:
last = client.get_last_version(project, p["id"])
return json.dumps({
"name": p["name"],
"type": p.get("productType"),
"id": p["id"],
"latest_version": last["version"] if last else None,
}, indent=2)
return _err(f"Product '{product_name}' not found in '{folder_path}'")
except Exception as e:
return _err(str(e))
# ── Publishing Tools ─────────────────────────────────────
@app.tool()
async def ayon_publish_texture_set(
project: str,
folder_path: str,
product_name: str,
variant: str = "",
textures: str = "[]",
) -> str:
"""Publish a texture set (multiple maps) as a single product.
Args:
project: Project name (e.g. 'DemoProject')
folder_path: Folder path (e.g. '/assets/chair')
product_name: Base product name (e.g. 'texture_chair')
variant: Optional variant (e.g. 'brown_leather')
textures: JSON array of texture info:
[{"name": "BaseColor.png", "size": 1048576}, ...]
"""
try:
textures_list = json.loads(textures)
folder = client.find_folder(project, folder_path)
if not folder:
return _err(f"Folder '{folder_path}' not found in '{project}'")
full_name = product_name
if variant:
full_name = f"{product_name}_{variant}"
product = client.create_product(
project=project,
name=full_name,
product_type="texture",
folder_id=folder["id"],
)
last = client.get_last_version(project, product["id"])
version_num = (last["version"] + 1) if last else 1
version = client.create_version(
project=project,
product_id=product["id"],
version=version_num,
)
rep = client.create_representation(
project=project,
version_id=version["id"],
name="texture_set",
files=textures_list,
)
return _ok(
product=full_name,
type="texture",
version=version_num,
maps=[t["name"] for t in textures_list],
folder=folder_path,
product_id=product["id"],
version_id=version["id"],
)
except Exception as e:
return _err(str(e))
@app.tool()
async def ayon_publish_model(
project: str,
folder_path: str,
product_name: str,
model_type: str = "abc",
variant: str = "",
files: str = "[]",
) -> str:
"""Publish a 3D model (ABC, USD, FBX, etc.).
Args:
project: Project name
folder_path: Folder path (e.g. '/assets/chair')
product_name: Product name (e.g. 'modelMain')
model_type: File format: 'abc', 'usd', 'fbx', 'obj', 'blend'
variant: Optional variant (e.g. 'highpoly')
files: JSON array of file info:
[{"name": "chair.abc", "size": 2048000}]
"""
try:
files_list = json.loads(files)
folder = client.find_folder(project, folder_path)
if not folder:
return _err(f"Folder '{folder_path}' not found")
full_name = product_name
if variant:
full_name = f"{product_name}_{variant}"
product = client.create_product(
project=project,
name=full_name,
product_type="model",
folder_id=folder["id"],
)
last = client.get_last_version(project, product["id"])
version_num = (last["version"] + 1) if last else 1
version = client.create_version(
project=project,
product_id=product["id"],
version=version_num,
)
rep = client.create_representation(
project=project,
version_id=version["id"],
name=model_type,
files=files_list,
)
return _ok(
product=full_name,
type="model",
format=model_type,
version=version_num,
files=[f["name"] for f in files_list],
folder=folder_path,
)
except Exception as e:
return _err(str(e))
# ── Entry Point ──────────────────────────────────────────
def main():
if not os.environ.get("AYON_SERVER_URL"):
print("ERROR: AYON_SERVER_URL not set", file=sys.stderr)
sys.exit(1)
if not os.environ.get("AYON_API_KEY"):
print("ERROR: AYON_API_KEY not set", file=sys.stderr)
sys.exit(1)
app.run()
if __name__ == "__main__":
main()