From 6b5eb9839b3dd295319555cd06547fd6000e5cf0 Mon Sep 17 00:00:00 2001 From: Jawish Hameed Date: Sat, 5 Sep 2026 23:54:33 +0800 Subject: [PATCH 01/12] feat: add plugin distribution manifests --- .agents/plugins/marketplace.json | 20 ++ .claude-plugin/marketplace.json | 24 ++ .grok-plugin/marketplace.json | 25 ++ .mcp.json | 8 + catalog/plugin.json | 33 +++ gemini-extension.json | 10 + .../.claude-plugin/plugin.json | 9 + .../.codex-plugin/plugin.json | 26 ++ .../.grok-plugin/plugin.json | 9 + plugins/lottiefiles-creator/.mcp.json | 8 + plugins/lottiefiles-creator/assets/README.md | 7 + .../skills/animation-review/SKILL.md | 17 ++ .../references/tool-contract.md | 10 + .../skills/animation-workflows/SKILL.md | 17 ++ .../references/tool-contract.md | 16 ++ .../skills/lottiefiles-library/SKILL.md | 17 ++ .../references/tool-contract.md | 13 + scripts/generate.py | 231 ++++++++++++++++++ scripts/validate.py | 182 ++++++++++++++ server.json | 13 + 20 files changed, 695 insertions(+) create mode 100644 .agents/plugins/marketplace.json create mode 100644 .claude-plugin/marketplace.json create mode 100644 .grok-plugin/marketplace.json create mode 100644 .mcp.json create mode 100644 catalog/plugin.json create mode 100644 gemini-extension.json create mode 100644 plugins/lottiefiles-creator/.claude-plugin/plugin.json create mode 100644 plugins/lottiefiles-creator/.codex-plugin/plugin.json create mode 100644 plugins/lottiefiles-creator/.grok-plugin/plugin.json create mode 100644 plugins/lottiefiles-creator/.mcp.json create mode 100644 plugins/lottiefiles-creator/assets/README.md create mode 100644 plugins/lottiefiles-creator/skills/animation-review/SKILL.md create mode 100644 plugins/lottiefiles-creator/skills/animation-review/references/tool-contract.md create mode 100644 plugins/lottiefiles-creator/skills/animation-workflows/SKILL.md create mode 100644 plugins/lottiefiles-creator/skills/animation-workflows/references/tool-contract.md create mode 100644 plugins/lottiefiles-creator/skills/lottiefiles-library/SKILL.md create mode 100644 plugins/lottiefiles-creator/skills/lottiefiles-library/references/tool-contract.md create mode 100644 scripts/generate.py create mode 100644 scripts/validate.py create mode 100644 server.json diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json new file mode 100644 index 0000000..be547ff --- /dev/null +++ b/.agents/plugins/marketplace.json @@ -0,0 +1,20 @@ +{ + "interface": { + "displayName": "LottieFiles" + }, + "name": "lottiefiles", + "plugins": [ + { + "category": "Productivity", + "name": "lottiefiles-creator", + "policy": { + "authentication": "ON_INSTALL", + "installation": "AVAILABLE" + }, + "source": { + "path": "./plugins/lottiefiles-creator", + "source": "local" + } + } + ] +} diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..3942bbf --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-marketplace.json", + "description": "Authenticated animation creation, export, document management, and LottieFiles API workflows.", + "name": "lottiefiles", + "owner": { + "name": "LottieFiles" + }, + "plugins": [ + { + "category": "productivity", + "description": "Authenticated animation creation, export, document management, and LottieFiles API workflows.", + "keywords": [ + "lottiefiles", + "lottie", + "animation", + "graphql" + ], + "name": "lottiefiles-creator", + "source": "./plugins/lottiefiles-creator", + "version": "0.1.0" + } + ], + "version": "0.1.0" +} diff --git a/.grok-plugin/marketplace.json b/.grok-plugin/marketplace.json new file mode 100644 index 0000000..d469aa5 --- /dev/null +++ b/.grok-plugin/marketplace.json @@ -0,0 +1,25 @@ +{ + "description": "Authenticated animation creation, export, document management, and LottieFiles API workflows.", + "name": "lottiefiles", + "owner": { + "name": "LottieFiles" + }, + "plugins": [ + { + "category": "productivity", + "description": "Authenticated animation creation, export, document management, and LottieFiles API workflows.", + "keywords": [ + "lottiefiles", + "lottie", + "animation", + "graphql" + ], + "name": "lottiefiles-creator", + "source": { + "path": "./plugins/lottiefiles-creator", + "type": "local" + }, + "version": "0.1.0" + } + ] +} diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..0bc8e59 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "lottiefiles-creator": { + "type": "http", + "url": "https://creator-mcp.lottiefiles.com/mcp" + } + } +} diff --git a/catalog/plugin.json b/catalog/plugin.json new file mode 100644 index 0000000..25afb9c --- /dev/null +++ b/catalog/plugin.json @@ -0,0 +1,33 @@ +{ + "id": "lottiefiles-creator", + "displayName": "LottieFiles Creator", + "marketplaceId": "lottiefiles", + "version": "0.1.0", + "description": "Authenticated animation creation, export, document management, and LottieFiles API workflows.", + "publisher": { + "name": "LottieFiles" + }, + "remote": { + "productionUrl": "https://creator-mcp.lottiefiles.com/mcp", + "stagingUrl": "https://creator-mcp-staging.lottiefiles.com/mcp" + }, + "mcpServerName": "lottiefiles-creator", + "toolNames": [ + "engine_exec", + "engine_docs", + "request_upload", + "lf_graphql", + "engine_document", + "engine_export", + "engine_job_status", + "engine_job_result", + "engine_cancel_job" + ], + "category": "Productivity", + "keywords": [ + "lottiefiles", + "lottie", + "animation", + "graphql" + ] +} diff --git a/gemini-extension.json b/gemini-extension.json new file mode 100644 index 0000000..5046947 --- /dev/null +++ b/gemini-extension.json @@ -0,0 +1,10 @@ +{ + "description": "Authenticated animation creation, export, document management, and LottieFiles API workflows.", + "mcpServers": { + "lottiefiles-creator": { + "httpUrl": "https://creator-mcp.lottiefiles.com/mcp" + } + }, + "name": "lottiefiles-creator", + "version": "0.1.0" +} diff --git a/plugins/lottiefiles-creator/.claude-plugin/plugin.json b/plugins/lottiefiles-creator/.claude-plugin/plugin.json new file mode 100644 index 0000000..5f5ce37 --- /dev/null +++ b/plugins/lottiefiles-creator/.claude-plugin/plugin.json @@ -0,0 +1,9 @@ +{ + "author": { + "name": "LottieFiles" + }, + "description": "Authenticated animation creation, export, document management, and LottieFiles API workflows.", + "mcpServers": "./.mcp.json", + "name": "lottiefiles-creator", + "version": "0.1.0" +} diff --git a/plugins/lottiefiles-creator/.codex-plugin/plugin.json b/plugins/lottiefiles-creator/.codex-plugin/plugin.json new file mode 100644 index 0000000..334bde8 --- /dev/null +++ b/plugins/lottiefiles-creator/.codex-plugin/plugin.json @@ -0,0 +1,26 @@ +{ + "author": { + "name": "LottieFiles" + }, + "description": "Authenticated animation creation, export, document management, and LottieFiles API workflows.", + "interface": { + "capabilities": [ + "Read", + "Write" + ], + "category": "Productivity", + "defaultPrompt": [ + "Create or edit an authenticated LottieFiles animation.", + "Export an animation or inspect an export job.", + "Use the LottieFiles API for an animation workflow." + ], + "developerName": "LottieFiles", + "displayName": "LottieFiles Creator", + "longDescription": "Use the authenticated LottieFiles Creator MCP server for animation creation, export, document management, and LottieFiles API workflows.", + "shortDescription": "Authenticated animation creation, export, document management, and LottieFiles API workflows." + }, + "mcpServers": "./.mcp.json", + "name": "lottiefiles-creator", + "skills": "./skills/", + "version": "0.1.0" +} diff --git a/plugins/lottiefiles-creator/.grok-plugin/plugin.json b/plugins/lottiefiles-creator/.grok-plugin/plugin.json new file mode 100644 index 0000000..5f5ce37 --- /dev/null +++ b/plugins/lottiefiles-creator/.grok-plugin/plugin.json @@ -0,0 +1,9 @@ +{ + "author": { + "name": "LottieFiles" + }, + "description": "Authenticated animation creation, export, document management, and LottieFiles API workflows.", + "mcpServers": "./.mcp.json", + "name": "lottiefiles-creator", + "version": "0.1.0" +} diff --git a/plugins/lottiefiles-creator/.mcp.json b/plugins/lottiefiles-creator/.mcp.json new file mode 100644 index 0000000..0bc8e59 --- /dev/null +++ b/plugins/lottiefiles-creator/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "lottiefiles-creator": { + "type": "http", + "url": "https://creator-mcp.lottiefiles.com/mcp" + } + } +} diff --git a/plugins/lottiefiles-creator/assets/README.md b/plugins/lottiefiles-creator/assets/README.md new file mode 100644 index 0000000..4109515 --- /dev/null +++ b/plugins/lottiefiles-creator/assets/README.md @@ -0,0 +1,7 @@ +# Brand assets + +No binary brand asset is included in this package. + +The only approved source is [LottieFiles brand assets](https://lottiefiles.com/brand-assets). The source could not be reliably retrieved during package preparation. A human brand owner must verify provenance and approve any downloaded asset before publication. + +Do not redraw, recolor, crop, stretch, or otherwise modify an approved logo. diff --git a/plugins/lottiefiles-creator/skills/animation-review/SKILL.md b/plugins/lottiefiles-creator/skills/animation-review/SKILL.md new file mode 100644 index 0000000..2b5a498 --- /dev/null +++ b/plugins/lottiefiles-creator/skills/animation-review/SKILL.md @@ -0,0 +1,17 @@ +--- +name: lottiefiles-animation-review +description: Inspect, review, compare, or diagnose an animation result with installed Engine tools. Use when the user asks why an animation differs, fails, renders poorly, or needs a quality review. +--- + +# Animation review and diagnosis + +Use `engine_docs` to discover the installed Engine contract. Use `engine_document` to inspect, `engine_exec` to render or inspect when the contract requires it, and `engine_job_status` or `engine_job_result` for async work. Read [the contract reference](references/tool-contract.md) before a tool call. + +1. Inspect the source, result, and reported symptom before you diagnose. +2. Compare the smallest relevant artifact, frame range, or property set. +3. Separate confirmed facts from hypotheses. State the evidence for each finding. +4. Do not change the animation during a review unless the user asks for a fix. +5. If a tool returns an async job, inspect the job result before you report a render or export failure. +6. Recommend the smallest safe next action and identify any missing input. + +Do not claim visual correctness from metadata alone. Do not expose private document content in diagnostics. diff --git a/plugins/lottiefiles-creator/skills/animation-review/references/tool-contract.md b/plugins/lottiefiles-creator/skills/animation-review/references/tool-contract.md new file mode 100644 index 0000000..8f0bd4d --- /dev/null +++ b/plugins/lottiefiles-creator/skills/animation-review/references/tool-contract.md @@ -0,0 +1,10 @@ +# Engine review tool contract reconciliation + +Reconcile exact argument and result shapes with the released public Engine contract fixture before publication. + +| Review need | Released tool name | Evidence to report | +| --- | --- | --- | +| Discover contract | `engine_docs` | Current capability and argument requirements | +| Inspect document | `engine_document` | Document state and relevant properties | +| Run review action | `engine_exec` | Contract-defined artifact or diagnostic result | +| Inspect async job | `engine_job_status` / `engine_job_result` | Job state, error code, and output location | diff --git a/plugins/lottiefiles-creator/skills/animation-workflows/SKILL.md b/plugins/lottiefiles-creator/skills/animation-workflows/SKILL.md new file mode 100644 index 0000000..0d9ca1e --- /dev/null +++ b/plugins/lottiefiles-creator/skills/animation-workflows/SKILL.md @@ -0,0 +1,17 @@ +--- +name: lottiefiles-animation-workflows +description: Create, edit, save, export, or manage an animation document with installed Engine tools. Use when the user asks to build or change an animation, export a file, or check an async Engine job. +--- + +# Animation workflows + +Use `engine_docs` to discover the installed Engine contract. Use `engine_document` to inspect a document, `engine_exec` to make a document change, `engine_export` to export, and `engine_job_status` or `engine_job_result` to inspect async work. Read [the contract reference](references/tool-contract.md) before a tool call. + +1. Ask for the target document, change, output format, and destination when they are not clear. +2. Inspect the document before you change it. Report the current state that affects the request. +3. Make the smallest requested change. Do not replace unrelated content. +4. Save or export only after the user confirms the destination when the action writes a file or replaces an asset. +5. For an async operation, keep its job identifier. Check its state until it succeeds, fails, or needs user action. +6. Report the document or job identifier, result location, and any warning. Do not expose credentials or private data. + +Do not invent tool arguments. Do not assume that an export completed until the job result confirms it. diff --git a/plugins/lottiefiles-creator/skills/animation-workflows/references/tool-contract.md b/plugins/lottiefiles-creator/skills/animation-workflows/references/tool-contract.md new file mode 100644 index 0000000..cfdf022 --- /dev/null +++ b/plugins/lottiefiles-creator/skills/animation-workflows/references/tool-contract.md @@ -0,0 +1,16 @@ +# Engine tool contract reconciliation + +The released public tool names are listed below. Reconcile exact argument and result shapes with the published contract fixture before publication. + +Required mapping: + +| Operation | Released tool name | Required arguments | Result fields | +| --- | --- | --- | --- | +| Discover contract | `engine_docs` | Reconcile with fixture | Reconcile with fixture | +| Inspect document | `engine_document` | Reconcile with fixture | Reconcile with fixture | +| Create or edit document | `engine_exec` | Reconcile with fixture | Reconcile with fixture | +| Upload input | `request_upload` | Reconcile with fixture | Reconcile with fixture | +| Export document | `engine_export` | Reconcile with fixture | Reconcile with fixture | +| Inspect job state | `engine_job_status` | Reconcile with fixture | Reconcile with fixture | +| Get job result | `engine_job_result` | Reconcile with fixture | Reconcile with fixture | +| Cancel job | `engine_cancel_job` | Reconcile with fixture | Reconcile with fixture | diff --git a/plugins/lottiefiles-creator/skills/lottiefiles-library/SKILL.md b/plugins/lottiefiles-creator/skills/lottiefiles-library/SKILL.md new file mode 100644 index 0000000..0b23915 --- /dev/null +++ b/plugins/lottiefiles-creator/skills/lottiefiles-library/SKILL.md @@ -0,0 +1,17 @@ +--- +name: lottiefiles-library-account +description: Search or inspect LottieFiles library or account data with lf_graphql. Use when the user asks to find, list, inspect, upload, update, delete, or organize LottieFiles resources. +--- + +# LottieFiles library and account workflows + +Full LottieFiles GraphQL access is intentional. It supports the capabilities that the connected account has. It does not grant extra scopes. + +1. Discover the needed schema and capability first. Use only fields that the current public schema confirms. +2. Read before a change. Inspect the target resource and its ownership or current state. +3. Request the minimum fields. Send values as variables. Do not put secrets in a query. +4. Paginate lists. Use the returned cursor and a user-approved limit. +5. Before a mutation or delete, state the target and effect. Require explicit user intent and host confirmation. +6. Inspect the response after a mutation. Handle GraphQL partial errors and report which data is reliable. + +Never bypass scopes. Never log tokens or query variables. Do not infer a field or mutation name from a human label. diff --git a/plugins/lottiefiles-creator/skills/lottiefiles-library/references/tool-contract.md b/plugins/lottiefiles-creator/skills/lottiefiles-library/references/tool-contract.md new file mode 100644 index 0000000..dae96cf --- /dev/null +++ b/plugins/lottiefiles-creator/skills/lottiefiles-library/references/tool-contract.md @@ -0,0 +1,13 @@ +# GraphQL contract reference + +Use `lf_graphql` for LottieFiles GraphQL operations. The public schema fixture is required before publication of schema-specific examples or field names. + +Safe request shape: + +- Use one operation with an `operationName`. +- Put user and resource values in `variables`. +- Request only required fields. +- Preserve `data` when GraphQL returns both `data` and `errors`. +- Treat `errors` as an incomplete result until the response identifies the failed path. + +Schema placeholder notation in this repository uses ``, ``, and ``. It is not an API claim. diff --git a/scripts/generate.py b/scripts/generate.py new file mode 100644 index 0000000..22e5b6b --- /dev/null +++ b/scripts/generate.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 +"""Generate deterministic platform manifests from the canonical plugin catalog.""" + +from __future__ import annotations + +import argparse +import filecmp +import json +import shutil +import sys +from pathlib import Path, PurePosixPath +from typing import Any + + +ROOT = Path(__file__).resolve().parent.parent +CATALOG_PATH = ROOT / "catalog" / "plugin.json" +PLUGIN_ROOT = ROOT / "plugins" / "lottiefiles-creator" + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--check", + action="store_true", + help="Fail if generated files differ from their canonical output.", + ) + return parser.parse_args() + + +def load_catalog() -> dict[str, Any]: + with CATALOG_PATH.open(encoding="utf-8") as handle: + payload = json.load(handle) + if not isinstance(payload, dict): + raise ValueError("catalog/plugin.json must contain a JSON object") + return payload + + +def remote_server(catalog: dict[str, Any], *, gemini: bool = False) -> dict[str, str]: + url = catalog["remote"]["productionUrl"] + if gemini: + return {"httpUrl": url} + return {"type": "http", "url": url} + + +def plugin_manifest(catalog: dict[str, Any]) -> dict[str, Any]: + return { + "name": catalog["id"], + "version": catalog["version"], + "description": catalog["description"], + "author": {"name": catalog["publisher"]["name"]}, + "skills": "./skills/", + "mcpServers": "./.mcp.json", + "interface": { + "displayName": catalog["displayName"], + "shortDescription": catalog["description"], + "longDescription": ( + "Use the authenticated LottieFiles Creator MCP server for animation " + "creation, export, document management, and LottieFiles API workflows." + ), + "developerName": catalog["publisher"]["name"], + "category": catalog["category"], + "capabilities": ["Read", "Write"], + "defaultPrompt": [ + "Create or edit an authenticated LottieFiles animation.", + "Export an animation or inspect an export job.", + "Use the LottieFiles API for an animation workflow." + ] + } + } + + +def claude_manifest(catalog: dict[str, Any]) -> dict[str, Any]: + return { + "name": catalog["id"], + "version": catalog["version"], + "description": catalog["description"], + "author": {"name": catalog["publisher"]["name"]}, + "mcpServers": "./.mcp.json" + } + + +def marketplace_entry(catalog: dict[str, Any], source: Any) -> dict[str, Any]: + return { + "name": catalog["id"], + "description": catalog["description"], + "version": catalog["version"], + "source": source, + "category": "productivity", + "keywords": catalog["keywords"] + } + + +def generated_json(catalog: dict[str, Any]) -> dict[Path, dict[str, Any]]: + server_name = catalog["mcpServerName"] + description = catalog["description"] + return { + ROOT / ".mcp.json": { + "mcpServers": {server_name: remote_server(catalog)} + }, + ROOT / "gemini-extension.json": { + "name": catalog["id"], + "version": catalog["version"], + "description": description, + "mcpServers": {server_name: remote_server(catalog, gemini=True)} + }, + ROOT / "server.json": { + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", + "name": "com.lottiefiles/creator", + "title": catalog["displayName"], + "description": description, + "version": catalog["version"], + "remotes": [{"type": "streamable-http", "url": catalog["remote"]["productionUrl"]}] + }, + ROOT / ".agents" / "plugins" / "marketplace.json": { + "name": catalog["marketplaceId"], + "interface": {"displayName": catalog["publisher"]["name"]}, + "plugins": [{ + "name": catalog["id"], + "source": {"source": "local", "path": f"./plugins/{catalog['id']}"}, + "policy": {"installation": "AVAILABLE", "authentication": "ON_INSTALL"}, + "category": catalog["category"] + }] + }, + ROOT / ".claude-plugin" / "marketplace.json": { + "$schema": "https://json.schemastore.org/claude-code-marketplace.json", + "name": catalog["marketplaceId"], + "version": catalog["version"], + "description": description, + "owner": {"name": catalog["publisher"]["name"]}, + "plugins": [marketplace_entry(catalog, f"./plugins/{catalog['id']}")] + }, + ROOT / ".grok-plugin" / "marketplace.json": { + "name": catalog["marketplaceId"], + "description": description, + "owner": {"name": catalog["publisher"]["name"]}, + "plugins": [marketplace_entry(catalog, { + "type": "local", "path": f"./plugins/{catalog['id']}" + })] + }, + PLUGIN_ROOT / ".mcp.json": { + "mcpServers": {server_name: remote_server(catalog)} + }, + PLUGIN_ROOT / ".codex-plugin" / "plugin.json": plugin_manifest(catalog), + PLUGIN_ROOT / ".grok-plugin" / "plugin.json": claude_manifest(catalog), + PLUGIN_ROOT / ".claude-plugin" / "plugin.json": claude_manifest(catalog) + } + + +def json_bytes(payload: dict[str, Any]) -> bytes: + return (json.dumps(payload, indent=2, sort_keys=True) + "\n").encode("utf-8") + + +def require_safe_relative(relative: Path) -> None: + posix = PurePosixPath(relative.as_posix()) + if posix.is_absolute() or ".." in posix.parts: + raise ValueError(f"unsafe generated path: {relative}") + + +def sync_tree(source: Path, destination: Path, *, check: bool) -> bool: + if source.is_symlink() or any(path.is_symlink() for path in source.rglob("*")): + raise ValueError(f"canonical content must not contain symlinks: {source}") + if destination.is_symlink() or ( + destination.exists() and any(path.is_symlink() for path in destination.rglob("*")) + ): + raise ValueError(f"generated content must not contain symlinks: {destination}") + source_files = {path.relative_to(source) for path in source.rglob("*") if path.is_file()} + destination_files = ( + {path.relative_to(destination) for path in destination.rglob("*") if path.is_file()} + if destination.exists() else set() + ) + stale = destination_files - source_files + changed = stale != set() + for relative in source_files: + require_safe_relative(relative) + target = destination / relative + if not target.is_file() or not filecmp.cmp(source / relative, target, shallow=False): + changed = True + if check: + return changed + for relative in stale: + (destination / relative).unlink() + for relative in sorted(source_files): + target = destination / relative + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(source / relative, target) + for directory in sorted((path for path in destination.rglob("*") if path.is_dir()), reverse=True): + if not any(directory.iterdir()): + directory.rmdir() + return changed + + +def sync_content(*, check: bool) -> bool: + changed = False + for directory in ("skills", "assets"): + source = ROOT / "content" / directory + destination = PLUGIN_ROOT / directory + if source.exists(): + changed = sync_tree(source, destination, check=check) or changed + elif destination.exists(): + if check: + changed = True + else: + shutil.rmtree(destination) + return changed + + +def main() -> int: + args = parse_args() + catalog = load_catalog() + stale: list[Path] = [] + for path, payload in generated_json(catalog).items(): + expected = json_bytes(payload) + if not path.is_file() or path.read_bytes() != expected: + stale.append(path.relative_to(ROOT)) + if not args.check: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(expected) + if sync_content(check=args.check): + stale.append(Path("plugins/lottiefiles-creator/content")) + if args.check and stale: + print("stale generated outputs:", file=sys.stderr) + for path in stale: + print(f"- {path.as_posix()}", file=sys.stderr) + return 1 + if not args.check: + print("generated platform manifests") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/validate.py b/scripts/validate.py new file mode 100644 index 0000000..b17860d --- /dev/null +++ b/scripts/validate.py @@ -0,0 +1,182 @@ +#!/usr/bin/env python3 +"""Validate source metadata and generated plugin distribution files without a network.""" + +from __future__ import annotations + +import json +import re +import subprocess +import sys +from pathlib import Path +from typing import Any +from urllib.parse import urlparse + + +ROOT = Path(__file__).resolve().parent.parent +CATALOG = ROOT / "catalog" / "plugin.json" +EXPECTED = ( + ROOT / ".mcp.json", + ROOT / "gemini-extension.json", + ROOT / "server.json", + ROOT / ".agents" / "plugins" / "marketplace.json", + ROOT / ".claude-plugin" / "marketplace.json", + ROOT / ".grok-plugin" / "marketplace.json", + ROOT / "plugins" / "lottiefiles-creator" / ".mcp.json", + ROOT / "plugins" / "lottiefiles-creator" / ".codex-plugin" / "plugin.json", + ROOT / "plugins" / "lottiefiles-creator" / ".grok-plugin" / "plugin.json", + ROOT / "plugins" / "lottiefiles-creator" / ".claude-plugin" / "plugin.json" +) +SEMVER = re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$") +SECRET_KEY = re.compile(r"(api[-_]?key|secret|password|token|authorization|credential)", re.I) +SECRET_VALUE = re.compile(r"(sk-[A-Za-z0-9_-]{16,}|AKIA[0-9A-Z]{16}|gh[pousr]_[A-Za-z0-9_]{20,})") +PATH_KEY = re.compile(r"^(path|source|skills|assets)$", re.I) + + +def no_duplicate_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]: + payload: dict[str, Any] = {} + for key, value in pairs: + if key in payload: + raise ValueError(f"duplicate JSON key: {key}") + payload[key] = value + return payload + + +def load_json(path: Path, errors: list[str]) -> dict[str, Any] | None: + if not path.is_file(): + errors.append(f"missing required file: {path.relative_to(ROOT)}") + return None + try: + value = json.loads(path.read_text(encoding="utf-8"), object_pairs_hook=no_duplicate_object) + except (OSError, ValueError, json.JSONDecodeError) as error: + errors.append(f"invalid JSON in {path.relative_to(ROOT)}: {error}") + return None + if not isinstance(value, dict): + errors.append(f"JSON object required: {path.relative_to(ROOT)}") + return None + return value + + +def validate_tree(errors: list[str]) -> None: + for path in ROOT.rglob("*"): + if ".git" in path.relative_to(ROOT).parts: + continue + if path.is_symlink(): + errors.append(f"symlinks are not allowed: {path.relative_to(ROOT)}") + + +def validate_secrets(value: Any, location: str, errors: list[str]) -> None: + if isinstance(value, dict): + for key, child in value.items(): + if SECRET_KEY.search(key): + errors.append(f"secret-looking key is not allowed: {location}.{key}") + validate_secrets(child, f"{location}.{key}", errors) + elif isinstance(value, list): + for index, child in enumerate(value): + validate_secrets(child, f"{location}[{index}]", errors) + elif isinstance(value, str) and SECRET_VALUE.search(value): + errors.append(f"secret-looking value is not allowed: {location}") + + +def validate_paths(value: Any, location: str, errors: list[str]) -> None: + if isinstance(value, dict): + for key, child in value.items(): + child_location = f"{location}.{key}" + if PATH_KEY.fullmatch(key) and isinstance(child, str) and not child.startswith("https://"): + parts = Path(child).parts + if Path(child).is_absolute() or ".." in parts: + errors.append(f"path traversal is not allowed: {child_location}") + validate_paths(child, child_location, errors) + elif isinstance(value, list): + for index, child in enumerate(value): + validate_paths(child, f"{location}[{index}]", errors) + + +def validate_https(value: Any, location: str, errors: list[str]) -> None: + if not isinstance(value, str): + errors.append(f"HTTPS URL required: {location}") + return + parsed = urlparse(value) + if parsed.scheme != "https" or not parsed.netloc: + errors.append(f"HTTPS URL required: {location}") + + +def value_at(payload: dict[str, Any], *keys: str) -> Any: + value: Any = payload + for key in keys: + if not isinstance(value, dict): + return None + value = value.get(key) + return value + + +def run_generator_check(errors: list[str]) -> None: + result = subprocess.run( + [sys.executable, str(ROOT / "scripts" / "generate.py"), "--check"], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + if result.returncode: + detail = (result.stderr or result.stdout).strip() + errors.append(f"generated outputs are stale: {detail}") + + +def main() -> int: + errors: list[str] = [] + validate_tree(errors) + catalog = load_json(CATALOG, errors) + payloads = {path: load_json(path, errors) for path in EXPECTED} + for path, payload in payloads.items(): + if payload is not None: + validate_secrets(payload, path.relative_to(ROOT).as_posix(), errors) + validate_paths(payload, path.relative_to(ROOT).as_posix(), errors) + if catalog is not None: + plugin_id = catalog.get("id") + version = catalog.get("version") + remote = value_at(catalog, "remote", "productionUrl") + if plugin_id != "lottiefiles-creator": + errors.append("catalog id must be lottiefiles-creator") + if not isinstance(version, str) or not SEMVER.fullmatch(version): + errors.append("catalog version must be strict semver") + validate_https(remote, "catalog.remote.productionUrl", errors) + if remote == value_at(catalog, "remote", "stagingUrl"): + errors.append("staging URL must not be the public default") + for path, payload in payloads.items(): + if payload is None: + continue + candidate_id = payload.get("name") + if path.name == "server.json": + if candidate_id != "com.lottiefiles/creator": + errors.append("server.json name must be com.lottiefiles/creator") + elif candidate_id is not None and candidate_id not in {plugin_id, catalog.get("marketplaceId")}: + errors.append(f"name drift in {path.relative_to(ROOT)}") + candidate_version = payload.get("version") + if candidate_version is not None and candidate_version != version: + errors.append(f"version drift in {path.relative_to(ROOT)}") + for path, payload in payloads.items(): + if payload is None: + continue + for server in value_at(payload, "mcpServers").values() if isinstance(value_at(payload, "mcpServers"), dict) else []: + validate_https(server.get("url", server.get("httpUrl")), f"{path.relative_to(ROOT)} MCP URL", errors) + server = payloads.get(ROOT / "server.json") + if server is not None: + remotes = server.get("remotes") + if not isinstance(remotes, list) or len(remotes) != 1 or remotes[0].get("type") != "streamable-http": + errors.append("server.json must define one streamable-http remote") + else: + validate_https(remotes[0].get("url"), "server.json remotes[0].url", errors) + if "allowedTools" in catalog or "toolAllowlist" in catalog: + errors.append("catalog must not narrow the LottieFiles GraphQL surface") + run_generator_check(errors) + if errors: + print("validation failed:", file=sys.stderr) + for error in errors: + print(f"- {error}", file=sys.stderr) + return 1 + print("validation passed") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/server.json b/server.json new file mode 100644 index 0000000..4ae44cc --- /dev/null +++ b/server.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", + "description": "Authenticated animation creation, export, document management, and LottieFiles API workflows.", + "name": "com.lottiefiles/creator", + "remotes": [ + { + "type": "streamable-http", + "url": "https://creator-mcp.lottiefiles.com/mcp" + } + ], + "title": "LottieFiles Creator", + "version": "0.1.0" +} From 65bee5dff4ed749c2a8affdc4b7d1157a50f0720 Mon Sep 17 00:00:00 2001 From: Jawish Hameed Date: Sat, 5 Sep 2026 23:55:18 +0800 Subject: [PATCH 02/12] docs: add public distribution content --- .gitignore | 8 +++ AGENTS.md | 9 ++++ CHANGELOG.md | 9 ++++ CONTRIBUTING.md | 13 +++++ LICENSE | 21 ++++++++ README.md | 49 ++++++++++++++++++- SECURITY.md | 7 +++ SUPPORT.md | 5 ++ content/assets/README.md | 7 +++ content/skills/animation-review/SKILL.md | 17 +++++++ .../references/tool-contract.md | 10 ++++ content/skills/animation-workflows/SKILL.md | 17 +++++++ .../references/tool-contract.md | 16 ++++++ content/skills/lottiefiles-library/SKILL.md | 17 +++++++ .../references/tool-contract.md | 13 +++++ docs/installation.md | 25 ++++++++++ docs/privacy.md | 12 +++++ docs/release.md | 34 +++++++++++++ docs/research.md | 30 ++++++++++++ docs/security.md | 20 ++++++++ examples/README.md | 13 +++++ examples/engine-async-job.md | 10 ++++ examples/engine-create-export.md | 12 +++++ examples/graphql-mutation-confirmed.md | 22 +++++++++ examples/graphql-pagination.md | 13 +++++ examples/graphql-read-only.md | 13 +++++ submission/checklist.md | 28 +++++++++++ submission/listing.md | 23 +++++++++ submission/release-notes.md | 10 ++++ submission/test-cases.md | 19 +++++++ 30 files changed, 501 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 SECURITY.md create mode 100644 SUPPORT.md create mode 100644 content/assets/README.md create mode 100644 content/skills/animation-review/SKILL.md create mode 100644 content/skills/animation-review/references/tool-contract.md create mode 100644 content/skills/animation-workflows/SKILL.md create mode 100644 content/skills/animation-workflows/references/tool-contract.md create mode 100644 content/skills/lottiefiles-library/SKILL.md create mode 100644 content/skills/lottiefiles-library/references/tool-contract.md create mode 100644 docs/installation.md create mode 100644 docs/privacy.md create mode 100644 docs/release.md create mode 100644 docs/research.md create mode 100644 docs/security.md create mode 100644 examples/README.md create mode 100644 examples/engine-async-job.md create mode 100644 examples/engine-create-export.md create mode 100644 examples/graphql-mutation-confirmed.md create mode 100644 examples/graphql-pagination.md create mode 100644 examples/graphql-read-only.md create mode 100644 submission/checklist.md create mode 100644 submission/listing.md create mode 100644 submission/release-notes.md create mode 100644 submission/test-cases.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bf0e989 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +node_modules/ +dist/ +coverage/ +*.log +.env +.env.* +!.env.example diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..c43bcf3 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,9 @@ +# Public package guidance + +Use the canonical public content under `content/skills` and `content/assets`. The generator creates platform copies. Do not edit generated copies by hand. + +Keep the package public. Do not add internal paths, hostnames, private source, secrets, user data, debug payloads, or proprietary schema. + +For GraphQL, discover the published schema first, request minimum fields, use variables, paginate lists, inspect before mutation, and require explicit user intent and host confirmation for writes or deletes. Never bypass scopes or log tokens or variables. Handle partial errors. + +Use exact names from the released public tool contract. Verify links and content generation before publishing. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8e08752 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,9 @@ +# Changelog + +All notable changes use this file. + +## 0.1.0 - 2026-09-05 + +- Add initial public skills for Engine workflows, LottieFiles GraphQL workflows, and animation review. +- Add safe workflow examples and marketplace submission material. +- Add installation, security, privacy, release, and support documentation. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..086bb7a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,13 @@ +# Contributing + +Keep public content accurate, minimal, and safe. + +1. Change canonical skills only under `content/skills` and assets only under `content/assets`. +2. Do not add private paths, internal hostnames, secrets, user data, debug payloads, or proprietary schema. +3. Do not publish a GraphQL field, type, or mutation name until a public schema confirms it. +4. Use exact released tool names. Keep unverified argument shapes in a clearly marked contract reference. +5. Keep instructions in active voice and Simplified Technical English. +6. Update examples, submission tests, and release notes for behavior changes. +7. Run the generator and validation commands defined by the maintainers before a release. + +Do not modify generated platform copies by hand. Update canonical content and regenerate. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f185d7f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 LottieFiles + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 12b89f1..0d93166 100644 --- a/README.md +++ b/README.md @@ -1 +1,48 @@ -Distribution metadata for the LottieFiles Creator agent plugin. +# LottieFiles Creator + +LottieFiles Creator is a public agent plugin package for animation document workflows and approved LottieFiles account access. It supplies task skills, safe examples, and submission material for MCP-capable hosts. + +Use it to: + +- Create, edit, inspect, export, and review animation documents with Engine tools. +- Search and inspect LottieFiles resources through `lf_graphql`. +- Make account changes only with explicit user intent, host confirmation, and approved scopes. + +The package does not claim a live directory listing or public endpoint. Read [installation guidance](docs/installation.md) before you connect it. + +## Safe use + +1. Discover the installed tool contract and GraphQL schema before an operation. +2. Request the minimum fields and use variables for GraphQL values. +3. Paginate bounded lists. +4. Inspect a resource before you change it. +5. Confirm the target and effect before a write, delete, export, or overwrite. +6. Keep tokens and variables out of logs. +7. Treat GraphQL `data` with `errors` as partial data. + +Full LottieFiles GraphQL access is intentional. The connected account scopes still define what the tool can access. The plugin never bypasses scopes. + +## Contents + +- [Skills](content/skills/) +- [Examples](examples/) +- [Installation matrix](docs/installation.md) +- [Privacy and data flow](docs/privacy.md) +- [Security and threat model](docs/security.md) +- [Versioning, release, and rollback](docs/release.md) +- [Research and support status](docs/research.md) +- [Submission materials](submission/) + +## Support and legal + +- [LottieFiles](https://lottiefiles.com) +- [LottieFiles Help Center](https://help.lottiefiles.com) +- [Privacy Policy](https://lottiefiles.com/page/privacy-policy) +- [Terms and Conditions](https://lottiefiles.com/page/terms-and-conditions) +- [Brand assets](https://lottiefiles.com/brand-assets) + +See [SUPPORT.md](SUPPORT.md), [SECURITY.md](SECURITY.md), and [CONTRIBUTING.md](CONTRIBUTING.md) for project processes. + +## License + +This package uses the [MIT License](LICENSE). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..380601a --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,7 @@ +# Security policy + +Do not report security issues in public issues or discussions. + +Report a suspected vulnerability through the [LottieFiles Help Center](https://help.lottiefiles.com). Include a clear impact summary, safe reproduction steps, affected package version, and any relevant error code. Do not include credentials, access tokens, private URLs, user data, or full query variables. + +The team will assess the report and provide a response through the support process. See the [threat model](docs/security.md) for package security controls. diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 0000000..d3c4724 --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,5 @@ +# Support + +For installation, account, or product support, use the [LottieFiles Help Center](https://help.lottiefiles.com). + +Include the package version, host name and version, sanitized error text, and steps to reproduce the issue. Do not include tokens, GraphQL variables, private document data, or unredacted exports. diff --git a/content/assets/README.md b/content/assets/README.md new file mode 100644 index 0000000..4109515 --- /dev/null +++ b/content/assets/README.md @@ -0,0 +1,7 @@ +# Brand assets + +No binary brand asset is included in this package. + +The only approved source is [LottieFiles brand assets](https://lottiefiles.com/brand-assets). The source could not be reliably retrieved during package preparation. A human brand owner must verify provenance and approve any downloaded asset before publication. + +Do not redraw, recolor, crop, stretch, or otherwise modify an approved logo. diff --git a/content/skills/animation-review/SKILL.md b/content/skills/animation-review/SKILL.md new file mode 100644 index 0000000..2b5a498 --- /dev/null +++ b/content/skills/animation-review/SKILL.md @@ -0,0 +1,17 @@ +--- +name: lottiefiles-animation-review +description: Inspect, review, compare, or diagnose an animation result with installed Engine tools. Use when the user asks why an animation differs, fails, renders poorly, or needs a quality review. +--- + +# Animation review and diagnosis + +Use `engine_docs` to discover the installed Engine contract. Use `engine_document` to inspect, `engine_exec` to render or inspect when the contract requires it, and `engine_job_status` or `engine_job_result` for async work. Read [the contract reference](references/tool-contract.md) before a tool call. + +1. Inspect the source, result, and reported symptom before you diagnose. +2. Compare the smallest relevant artifact, frame range, or property set. +3. Separate confirmed facts from hypotheses. State the evidence for each finding. +4. Do not change the animation during a review unless the user asks for a fix. +5. If a tool returns an async job, inspect the job result before you report a render or export failure. +6. Recommend the smallest safe next action and identify any missing input. + +Do not claim visual correctness from metadata alone. Do not expose private document content in diagnostics. diff --git a/content/skills/animation-review/references/tool-contract.md b/content/skills/animation-review/references/tool-contract.md new file mode 100644 index 0000000..8f0bd4d --- /dev/null +++ b/content/skills/animation-review/references/tool-contract.md @@ -0,0 +1,10 @@ +# Engine review tool contract reconciliation + +Reconcile exact argument and result shapes with the released public Engine contract fixture before publication. + +| Review need | Released tool name | Evidence to report | +| --- | --- | --- | +| Discover contract | `engine_docs` | Current capability and argument requirements | +| Inspect document | `engine_document` | Document state and relevant properties | +| Run review action | `engine_exec` | Contract-defined artifact or diagnostic result | +| Inspect async job | `engine_job_status` / `engine_job_result` | Job state, error code, and output location | diff --git a/content/skills/animation-workflows/SKILL.md b/content/skills/animation-workflows/SKILL.md new file mode 100644 index 0000000..0d9ca1e --- /dev/null +++ b/content/skills/animation-workflows/SKILL.md @@ -0,0 +1,17 @@ +--- +name: lottiefiles-animation-workflows +description: Create, edit, save, export, or manage an animation document with installed Engine tools. Use when the user asks to build or change an animation, export a file, or check an async Engine job. +--- + +# Animation workflows + +Use `engine_docs` to discover the installed Engine contract. Use `engine_document` to inspect a document, `engine_exec` to make a document change, `engine_export` to export, and `engine_job_status` or `engine_job_result` to inspect async work. Read [the contract reference](references/tool-contract.md) before a tool call. + +1. Ask for the target document, change, output format, and destination when they are not clear. +2. Inspect the document before you change it. Report the current state that affects the request. +3. Make the smallest requested change. Do not replace unrelated content. +4. Save or export only after the user confirms the destination when the action writes a file or replaces an asset. +5. For an async operation, keep its job identifier. Check its state until it succeeds, fails, or needs user action. +6. Report the document or job identifier, result location, and any warning. Do not expose credentials or private data. + +Do not invent tool arguments. Do not assume that an export completed until the job result confirms it. diff --git a/content/skills/animation-workflows/references/tool-contract.md b/content/skills/animation-workflows/references/tool-contract.md new file mode 100644 index 0000000..cfdf022 --- /dev/null +++ b/content/skills/animation-workflows/references/tool-contract.md @@ -0,0 +1,16 @@ +# Engine tool contract reconciliation + +The released public tool names are listed below. Reconcile exact argument and result shapes with the published contract fixture before publication. + +Required mapping: + +| Operation | Released tool name | Required arguments | Result fields | +| --- | --- | --- | --- | +| Discover contract | `engine_docs` | Reconcile with fixture | Reconcile with fixture | +| Inspect document | `engine_document` | Reconcile with fixture | Reconcile with fixture | +| Create or edit document | `engine_exec` | Reconcile with fixture | Reconcile with fixture | +| Upload input | `request_upload` | Reconcile with fixture | Reconcile with fixture | +| Export document | `engine_export` | Reconcile with fixture | Reconcile with fixture | +| Inspect job state | `engine_job_status` | Reconcile with fixture | Reconcile with fixture | +| Get job result | `engine_job_result` | Reconcile with fixture | Reconcile with fixture | +| Cancel job | `engine_cancel_job` | Reconcile with fixture | Reconcile with fixture | diff --git a/content/skills/lottiefiles-library/SKILL.md b/content/skills/lottiefiles-library/SKILL.md new file mode 100644 index 0000000..0b23915 --- /dev/null +++ b/content/skills/lottiefiles-library/SKILL.md @@ -0,0 +1,17 @@ +--- +name: lottiefiles-library-account +description: Search or inspect LottieFiles library or account data with lf_graphql. Use when the user asks to find, list, inspect, upload, update, delete, or organize LottieFiles resources. +--- + +# LottieFiles library and account workflows + +Full LottieFiles GraphQL access is intentional. It supports the capabilities that the connected account has. It does not grant extra scopes. + +1. Discover the needed schema and capability first. Use only fields that the current public schema confirms. +2. Read before a change. Inspect the target resource and its ownership or current state. +3. Request the minimum fields. Send values as variables. Do not put secrets in a query. +4. Paginate lists. Use the returned cursor and a user-approved limit. +5. Before a mutation or delete, state the target and effect. Require explicit user intent and host confirmation. +6. Inspect the response after a mutation. Handle GraphQL partial errors and report which data is reliable. + +Never bypass scopes. Never log tokens or query variables. Do not infer a field or mutation name from a human label. diff --git a/content/skills/lottiefiles-library/references/tool-contract.md b/content/skills/lottiefiles-library/references/tool-contract.md new file mode 100644 index 0000000..dae96cf --- /dev/null +++ b/content/skills/lottiefiles-library/references/tool-contract.md @@ -0,0 +1,13 @@ +# GraphQL contract reference + +Use `lf_graphql` for LottieFiles GraphQL operations. The public schema fixture is required before publication of schema-specific examples or field names. + +Safe request shape: + +- Use one operation with an `operationName`. +- Put user and resource values in `variables`. +- Request only required fields. +- Preserve `data` when GraphQL returns both `data` and `errors`. +- Treat `errors` as an incomplete result until the response identifies the failed path. + +Schema placeholder notation in this repository uses ``, ``, and ``. It is not an API claim. diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..fd8af4c --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,25 @@ +# Install and use + +This repository is a source package. It does not prove that a public directory listing or remote MCP endpoint is live. The release manager must publish and validate the package before users can install it from a directory. + +## Client matrix + +| Client | Native package support | Manual MCP connection | Use after connection | +| --- | --- | --- | --- | +| ChatGPT and Codex | Not confirmed for this package. Use only a released host install path. | Pending a published public MCP endpoint and host support. | Discover the tools, then follow the included skills. | +| Claude Code | Not confirmed for this package. Claude Code supports plugin-provided and manually configured MCP servers. | Confirmed client capability; add the released endpoint or command with Claude Code MCP settings. | Approve the server, authenticate if required, then verify tools. | +| Grok Build | Not confirmed for this package. Grok Build supports plugins and MCP configuration. | Confirmed client capability; add the released endpoint or command with `grok mcp add`. | Run `grok inspect`, then use the discovered tools. | +| ZCode | Not confirmed for this package. ZCode supports plugins that bundle MCP servers. | Confirmed client capability; add the released endpoint or command in Settings -> MCP Servers. | Confirm enabled server and inspect its tools. | +| Gemini CLI | Not confirmed for this package. Gemini CLI supports MCP servers. | Confirmed client capability; add the released endpoint or command with `gemini mcp add` or `settings.json`. | Run `/mcp list` and inspect available tools. | +| Generic MCP client | No native package claim. | Use the client’s streamable HTTP or stdio configuration after the server is published. | Discover capabilities before a tool call. | +| MCP Registry | Not listed at publication time. | Registry registration is an administrator action after release validation. | Install only from the registry entry after it exists. | + +## Safe connection procedure + +1. Get the endpoint or command from a signed release. Do not use a staging or private address. +2. Add only the required environment variables or OAuth connection. Keep tokens in the client secret store or environment. +3. Connect and inspect the tool list. Confirm that only expected tools appear. +4. Run a read-only example first. +5. For writes or deletes, require explicit user intent and the host confirmation prompt. + +If a connection fails, check the release notes and support documentation. Do not disable client security controls to make it work. diff --git a/docs/privacy.md b/docs/privacy.md new file mode 100644 index 0000000..2c690dc --- /dev/null +++ b/docs/privacy.md @@ -0,0 +1,12 @@ +# Privacy and data flow + +The plugin sends only the tool input needed for the user request. + +| Flow | Data | Control | +| --- | --- | --- | +| Host to Engine tools | Requested document operation and required identifiers | Inspect first; minimize input; host confirmation for writes. | +| Host to LottieFiles GraphQL | Query, variables, and account authorization context | Use variables; request minimum fields; scopes limit access. | +| Engine export | Requested format and approved destination | Confirm write or replacement destination. | +| Tool result to user | Minimum result, job state, warnings, and errors | Do not disclose tokens, variables, private content, or debug payloads. | + +The package does not require users to put access tokens in source files. Follow the host’s secure credential storage method. See the [LottieFiles Privacy Policy](https://lottiefiles.com/page/privacy-policy) and [Terms and Conditions](https://lottiefiles.com/page/terms-and-conditions). diff --git a/docs/release.md b/docs/release.md new file mode 100644 index 0000000..e679c0c --- /dev/null +++ b/docs/release.md @@ -0,0 +1,34 @@ +# Versioning, release, and rollback + +Use semantic versioning. + +- Increase PATCH for compatible fixes and documentation corrections. +- Increase MINOR for compatible capabilities. +- Increase MAJOR for a breaking package or tool-contract change. + +## Release procedure + +1. Generate platform copies from canonical `content/skills` and `content/assets`. +2. Validate generated output, metadata, links, and public-contract synchronization. +3. Run the release test suite and the submission test cases in each supported host. +4. Record the tag, commit, package checksum, public endpoint, and contract fixture hash. +5. Publish only after the checklist human and administrator gates pass. +6. Publish the directory entry only after the package is available from its public source. + +## Contract synchronization + +Treat the released public tool and GraphQL contracts as versioned inputs. Before each release: + +1. Compare the contract fixture with skill references and examples. +2. Replace only verified placeholders. +3. Add or update test fixtures for changed result shapes and error behavior. +4. Document the contract version and hash in the release record. +5. Block publication if a tool name, argument, result, scope, or security rule cannot be confirmed. + +## Rollback + +1. Disable the affected directory listing or release channel. +2. Restore the last verified package version and endpoint configuration. +3. Revoke or narrow credentials if the incident involves access control. +4. Tell users what changed, what data may be affected, and the safe upgrade or removal action. +5. Preserve release evidence and add a regression test before republishing. diff --git a/docs/research.md b/docs/research.md new file mode 100644 index 0000000..7871c83 --- /dev/null +++ b/docs/research.md @@ -0,0 +1,30 @@ +# Research and support status + +Reviewed 2026-09-05. This document records official sources used for packaging decisions. It does not claim that this package is listed or available in any directory. + +## Client support + +| Product | Official source | Confirmed capability | Package status | +| --- | --- | --- | --- | +| OpenAI | [OpenAI Apps SDK](https://developers.openai.com/apps-sdk/) | Official OpenAI documentation is the source for ChatGPT app and MCP integration requirements. | Native package support and directory listing require release validation. | +| Claude Code | [MCP documentation](https://code.claude.com/docs/en/mcp) | Supports remote HTTP and local stdio MCP servers. It also supports plugin-provided servers. | Manual MCP connection is supported by the client; this package needs host validation. | +| Grok Build | [MCP servers](https://docs.x.ai/build/features/mcp-servers) | Supports configured MCP servers. | Manual MCP connection is supported by the client; this package needs host validation. | +| ZCode | [Plugin documentation](https://zcode.z.ai/en/docs/plugin) and [MCP documentation](https://zcode.z.ai/en/docs/mcp-services) | Supports plugins and manual MCP servers. | Manual MCP connection is supported by the client; this package needs host validation. | +| Gemini CLI | [MCP server documentation](https://geminicli.com/docs/tools/mcp-server/) and [source repository](https://github.com/google-gemini/gemini-cli) | Supports MCP server configuration. | Manual MCP connection is supported by the client; this package needs host validation. | +| MCP Registry | [MCP Registry](https://registry.modelcontextprotocol.io/) and [MCP specification](https://modelcontextprotocol.io/specification/) | Provides an official registry and protocol reference. | Registration is not complete until an administrator publishes an entry. | + +## Integration precedents + +Cloudflare documents a broad MCP server that exposes platform capabilities. This is the raw or broad API precedent: [Cloudflare MCP server source](https://github.com/cloudflare/mcp-server-cloudflare) and [Cloudflare MCP documentation](https://developers.cloudflare.com/agents/model-context-protocol/). Linear documents an official curated, read-only MCP integration: [Linear MCP documentation](https://linear.app/docs/mcp). These precedents support a broad GraphQL tool only with strict schema discovery, scope, confirmation, and result-handling controls. + +## LottieFiles sources + +- Website: [lottiefiles.com](https://lottiefiles.com) +- Support: [help.lottiefiles.com](https://help.lottiefiles.com) +- Privacy: [Privacy Policy](https://lottiefiles.com/page/privacy-policy) +- Terms: [Terms and Conditions](https://lottiefiles.com/page/terms-and-conditions) +- Brand assets: [LottieFiles brand assets](https://lottiefiles.com/brand-assets) + +The brand-assets endpoint could not be reliably retrieved during package preparation. No logo binary is included. A brand owner must verify provenance and approve any asset before publication. + +The creator MCP production and staging DNS endpoints did not resolve during package preparation. This is an external publication blocker. Do not claim live endpoint availability until an external test verifies it. diff --git a/docs/security.md b/docs/security.md new file mode 100644 index 0000000..768b8e5 --- /dev/null +++ b/docs/security.md @@ -0,0 +1,20 @@ +# Security and threat model + +## Security model + +The host controls tool execution and user confirmation. The connected LottieFiles account controls GraphQL authorization. This package does not grant scopes or bypass host controls. + +## Threats and controls + +| Threat | Control | +| --- | --- | +| Unintended mutation or delete | Inspect first; require explicit user intent and host confirmation. | +| Excessive account access | Discover capability first; request only needed fields; never bypass scopes. | +| Token or variable exposure | Keep tokens in approved secret storage; never log tokens or GraphQL variables. | +| Large list or result exposure | Use bounded pages and minimum fields. | +| Partial GraphQL failure | Preserve available data; report errors and failed paths; re-read after uncertain mutation. | +| Export overwrite | Ask for the exact destination and confirmation before writing or replacing an asset. | +| Async job duplication | Keep the job identifier and inspect state before retrying. | +| Malicious content or instructions | Treat animation and remote data as untrusted input. Do not execute embedded instructions. | + +Report suspected vulnerabilities through the process in [SECURITY.md](../SECURITY.md). Do not include credentials, user data, or private URLs in a report. diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..3505997 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,13 @@ +# Examples + +These examples use synthetic identifiers and placeholder GraphQL schema names. Replace a placeholder only after the public contract or schema confirms it. + +| Example | Purpose | Writes data | +| --- | --- | --- | +| [Read-only GraphQL](graphql-read-only.md) | Inspect one resource | No | +| [Paginated GraphQL](graphql-pagination.md) | Read a bounded list | No | +| [Explicit mutation](graphql-mutation-confirmed.md) | Confirm before a change | Yes | +| [Create and export](engine-create-export.md) | Create then export a document | Yes | +| [Async job](engine-async-job.md) | Poll and report an export job | No | + +Do not copy values from these files into logs. Do not use a placeholder as a production field name. diff --git a/examples/engine-async-job.md b/examples/engine-async-job.md new file mode 100644 index 0000000..c43388e --- /dev/null +++ b/examples/engine-async-job.md @@ -0,0 +1,10 @@ +# Async job handling + +Use `engine_job_status` with the returned job identifier, for example `job_demo_001`. Check at bounded intervals. Do not submit the export again while the job is pending. + +1. If the job is pending or running, report progress and check again. +2. If the job succeeds, call `engine_job_result` and report the output location or artifact identifier. +3. If the job fails, report its stable error code and safe remediation. Do not expose private payloads. +4. If the user asks to stop it, explain the effect and then call `engine_cancel_job` after host confirmation. + +Use `engine_docs` to confirm job states and arguments before calling a tool. diff --git a/examples/engine-create-export.md b/examples/engine-create-export.md new file mode 100644 index 0000000..944dcb1 --- /dev/null +++ b/examples/engine-create-export.md @@ -0,0 +1,12 @@ +# Create and export an animation + +Use `engine_docs` first. It defines the valid arguments for `engine_exec`, `engine_document`, and `engine_export`. + +1. Ask for the document purpose, output format, and destination. +2. Call `engine_exec` with the published create action and minimum document content. +3. Call `engine_document` to confirm the created document and capture its returned identifier. +4. State the export target and request confirmation if the export writes or replaces a file. +5. Call `engine_export` with the published export arguments. +6. If it returns a job identifier, follow [the async job example](engine-async-job.md). + +Synthetic identifiers, such as `document_demo_001`, show flow only. The published fixture must supply argument names and result fields. diff --git a/examples/graphql-mutation-confirmed.md b/examples/graphql-mutation-confirmed.md new file mode 100644 index 0000000..c12670c --- /dev/null +++ b/examples/graphql-mutation-confirmed.md @@ -0,0 +1,22 @@ +# Explicit mutation flow + +This flow changes data. Do not call `lf_graphql` until the user explicitly confirms the target and effect and the host asks for confirmation. + +1. Read `(id: $id)` and show the relevant current values. +2. State: “Update resource `resource_demo_001` with the requested name. Continue?” +3. After explicit confirmation, send a minimum mutation: + +```json +{ + "operationName": "UpdateResource", + "query": "mutation UpdateResource($id: ID!, $input: !) { (id: $id, input: $input) { id name } }", + "variables": { + "id": "resource_demo_001", + "input": { "name": "Demo animation" } + } +} +``` + +4. Inspect the returned resource. If the response includes `errors`, treat the change as uncertain until the target is read again. + +All angle-bracket names are schema placeholders, not published LottieFiles fields. diff --git a/examples/graphql-pagination.md b/examples/graphql-pagination.md new file mode 100644 index 0000000..3ff8f47 --- /dev/null +++ b/examples/graphql-pagination.md @@ -0,0 +1,13 @@ +# Paginated GraphQL + +Use `lf_graphql` only after the schema confirms ``, ``, and the page type. + +```json +{ + "operationName": "ListResources", + "query": "query ListResources($first: Int!, $after: String) { (first: $first, after: $after) { nodes { id name } pageInfo { hasNextPage endCursor } } }", + "variables": { "first": 20, "after": null } +} +``` + +Use `endCursor` only when `hasNextPage` is true. Stop at the user-approved limit. Do not request all pages by default. diff --git a/examples/graphql-read-only.md b/examples/graphql-read-only.md new file mode 100644 index 0000000..89d49dc --- /dev/null +++ b/examples/graphql-read-only.md @@ -0,0 +1,13 @@ +# Read-only GraphQL + +Use `lf_graphql` only after the current schema confirms `` and ``. + +```json +{ + "operationName": "GetResource", + "query": "query GetResource($id: ID!) { (id: $id) { id name } }", + "variables": { "id": "resource_demo_001" } +} +``` + +Request only the fields needed for the answer. If `errors` appears with `data`, report the available data as partial and state the failed path. diff --git a/submission/checklist.md b/submission/checklist.md new file mode 100644 index 0000000..3cace41 --- /dev/null +++ b/submission/checklist.md @@ -0,0 +1,28 @@ +# Submission checklist + +## Package + +- [ ] Run the content generator and confirm generated copies match `content/skills` and `content/assets`. +- [ ] Validate manifests, metadata, and version against the release tag. +- [ ] Replace contract reconciliation notes with the released public fixture path and hash. +- [ ] Replace schema placeholders only with fields confirmed by a public schema. +- [ ] Confirm every public URL resolves from an external network. +- [ ] Verify the package contains no secrets, private paths, user data, debug payloads, or proprietary schema. + +## Human and administrator gates + +- [ ] Product owner approves listing copy, prompts, and examples. +- [ ] LottieFiles administrator approves GraphQL scopes and mutation behavior. +- [ ] Security owner approves the threat model and data-flow notes. +- [ ] Legal and brand owner approves license, trademarks, links, and asset provenance. +- [ ] Marketplace administrator approves the final package and directory submission. +- [ ] Release manager records the package version, commit, checksum, and rollback target. + +## Live acceptance + +- [ ] Test each positive and negative case in the target host. +- [ ] Confirm the host shows write and delete confirmation. +- [ ] Confirm OAuth and revoked-scope behavior. +- [ ] Confirm partial GraphQL errors retain safe data and report the failure. +- [ ] Confirm an async job succeeds, fails, and cancels as expected. +- [ ] Confirm support contact and privacy links in the published listing. diff --git a/submission/listing.md b/submission/listing.md new file mode 100644 index 0000000..22145b3 --- /dev/null +++ b/submission/listing.md @@ -0,0 +1,23 @@ +# Listing copy + +## Name + +LottieFiles Creator + +## Short description + +Create, inspect, export, and review animation documents. Access LottieFiles resources through approved GraphQL scopes. + +## Description + +Use this plugin to create and change animation documents with Engine tools, export approved results, and inspect async jobs. Use `lf_graphql` to read or change LottieFiles resources only within the connected account scopes. + +The plugin reads the installed tool contract before it acts. It requests only required data, uses variables for GraphQL values, paginates bounded lists, and asks for explicit confirmation before writes or deletes. + +Links: [LottieFiles](https://lottiefiles.com) · [Support](https://help.lottiefiles.com) · [Privacy](https://lottiefiles.com/page/privacy-policy) · [Terms](https://lottiefiles.com/page/terms-and-conditions) + +## Starter prompts + +- Create a short loading animation and export it after I approve the destination. +- List my recent animations in pages of 20. Show only names and IDs. +- Review this animation result and identify the smallest safe fix. diff --git a/submission/release-notes.md b/submission/release-notes.md new file mode 100644 index 0000000..e5b18f0 --- /dev/null +++ b/submission/release-notes.md @@ -0,0 +1,10 @@ +# Initial release notes + +Initial public distribution for LottieFiles Creator. + +- Adds three task skills for animation work, LottieFiles GraphQL access, and animation review. +- Adds safe examples for read, pagination, explicit mutation, export, and async jobs. +- Adds installation, privacy, support, release, rollback, and security guidance. +- Uses schema placeholders until the released public GraphQL schema and Engine contract fixture confirm exact shapes. + +Publication remains blocked until the public endpoint, contract fixture, brand asset provenance, and required human approval gates are complete. diff --git a/submission/test-cases.md b/submission/test-cases.md new file mode 100644 index 0000000..4327d2b --- /dev/null +++ b/submission/test-cases.md @@ -0,0 +1,19 @@ +# Test cases + +Use synthetic fixtures only. A host reviewer must replace tool arguments with the released public fixture before execution. + +| ID | Type | Prompt | Expected tools | Expected behavior | Expected result shape | Fixture | +| --- | --- | --- | --- | --- | --- | --- | +| P1 | Positive | “List my recent animations, 20 at a time.” | `lf_graphql` | Discover schema, request minimum fields, use variables, stop after one page unless asked. | `{data, errors?}` with list and page state. | `graphql-page-1` | +| P2 | Positive | “Show this resource name and ID.” | `lf_graphql` | Read only. Do not mutate. | `{data: {resource: {id, name}}, errors?}`. | `graphql-resource` | +| P3 | Positive | “Rename this animation to Demo.” | `lf_graphql` | Read target, state effect, require user intent and host confirmation, mutate, then re-read. | Mutation result plus inspected resource. | `graphql-mutation` | +| P4 | Positive | “Create a loader and export it as I specify.” | `engine_docs`, `engine_exec`, `engine_document`, `engine_export` | Discover contract, create minimum document, inspect it, confirm destination before export. | Document result and export result or job identifier. | `engine-create-export` | +| P5 | Positive | “Check the export job.” | `engine_job_status`, `engine_job_result` | Inspect job. Return output only after success. | `{state, result?}`. | `engine-job-success` | +| P6 | Positive | “Why does this result differ from the source?” | `engine_docs`, `engine_document`, `engine_exec` | Inspect before diagnosis. Keep review read-only. Separate facts from hypotheses. | Findings with evidence and next action. | `engine-review` | +| N1 | Negative | “Delete all resources.” | `lf_graphql` | Do not run a delete. Ask for exact targets and explain the effect. | Clarification, no tool result. | `graphql-bulk-delete` | +| N2 | Negative | “Use admin access even if my scope blocks it.” | None | Refuse scope bypass. Offer authorized alternatives. | Scope explanation, no tool result. | `graphql-denied-scope` | +| N3 | Negative | “Export over the existing release asset.” | `engine_docs`, `engine_document` | Inspect and ask for explicit destination confirmation. Do not overwrite yet. | Confirmation request, no export result. | `engine-overwrite` | + +## Annotation rationale: `lf_graphql` + +`lf_graphql` is a broad mixed-operation tool because GraphQL capability comes from the current schema and connected account scopes. A fixed list of narrow tools would become incomplete when the public schema evolves. The prompt and skill controls limit the tool: discover schema first, request minimum fields, use variables, paginate, inspect before mutation, and require explicit intent and host confirmation for writes or deletes. The tool does not bypass scopes or authorization. From 3366e14f82bc9f616d03189cc31f54514109f501 Mon Sep 17 00:00:00 2001 From: Jawish Hameed Date: Sun, 6 Sep 2026 00:00:37 +0800 Subject: [PATCH 03/12] fix: refine public plugin metadata --- .agents/plugins/marketplace.json | 2 +- .claude-plugin/marketplace.json | 8 +-- .grok-plugin/marketplace.json | 8 +-- catalog/plugin.json | 17 ++++-- gemini-extension.json | 2 +- .../.claude-plugin/plugin.json | 2 +- .../.codex-plugin/plugin.json | 22 ++++++-- .../.grok-plugin/plugin.json | 2 +- scripts/generate.py | 54 ++++++++++++------- scripts/validate.py | 12 ++++- server.json | 2 +- skills/animation-review/SKILL.md | 17 ++++++ .../references/tool-contract.md | 10 ++++ skills/animation-workflows/SKILL.md | 17 ++++++ .../references/tool-contract.md | 16 ++++++ skills/lottiefiles-library/SKILL.md | 17 ++++++ .../references/tool-contract.md | 13 +++++ 17 files changed, 178 insertions(+), 43 deletions(-) create mode 100644 skills/animation-review/SKILL.md create mode 100644 skills/animation-review/references/tool-contract.md create mode 100644 skills/animation-workflows/SKILL.md create mode 100644 skills/animation-workflows/references/tool-contract.md create mode 100644 skills/lottiefiles-library/SKILL.md create mode 100644 skills/lottiefiles-library/references/tool-contract.md diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index be547ff..be199a4 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -5,7 +5,7 @@ "name": "lottiefiles", "plugins": [ { - "category": "Productivity", + "category": "Creativity", "name": "lottiefiles-creator", "policy": { "authentication": "ON_INSTALL", diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 3942bbf..86a993d 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1,19 +1,19 @@ { "$schema": "https://json.schemastore.org/claude-code-marketplace.json", - "description": "Authenticated animation creation, export, document management, and LottieFiles API workflows.", + "description": "Create, manage, and export Lottie animations.", "name": "lottiefiles", "owner": { "name": "LottieFiles" }, "plugins": [ { - "category": "productivity", - "description": "Authenticated animation creation, export, document management, and LottieFiles API workflows.", + "category": "creativity", + "description": "Create, manage, and export Lottie animations.", "keywords": [ "lottiefiles", "lottie", "animation", - "graphql" + "creator" ], "name": "lottiefiles-creator", "source": "./plugins/lottiefiles-creator", diff --git a/.grok-plugin/marketplace.json b/.grok-plugin/marketplace.json index d469aa5..2a10bec 100644 --- a/.grok-plugin/marketplace.json +++ b/.grok-plugin/marketplace.json @@ -1,18 +1,18 @@ { - "description": "Authenticated animation creation, export, document management, and LottieFiles API workflows.", + "description": "Create, manage, and export Lottie animations.", "name": "lottiefiles", "owner": { "name": "LottieFiles" }, "plugins": [ { - "category": "productivity", - "description": "Authenticated animation creation, export, document management, and LottieFiles API workflows.", + "category": "creativity", + "description": "Create, manage, and export Lottie animations.", "keywords": [ "lottiefiles", "lottie", "animation", - "graphql" + "creator" ], "name": "lottiefiles-creator", "source": { diff --git a/catalog/plugin.json b/catalog/plugin.json index 25afb9c..ee55173 100644 --- a/catalog/plugin.json +++ b/catalog/plugin.json @@ -3,13 +3,20 @@ "displayName": "LottieFiles Creator", "marketplaceId": "lottiefiles", "version": "0.1.0", - "description": "Authenticated animation creation, export, document management, and LottieFiles API workflows.", + "description": "Create, manage, and export Lottie animations.", + "longDescription": "Use the authenticated LottieFiles Creator MCP server for animation creation, export, document management, and LottieFiles API workflows.", "publisher": { "name": "LottieFiles" }, "remote": { - "productionUrl": "https://creator-mcp.lottiefiles.com/mcp", - "stagingUrl": "https://creator-mcp-staging.lottiefiles.com/mcp" + "productionUrl": "https://creator-mcp.lottiefiles.com/mcp" + }, + "links": { + "homepage": "https://github.com/LottieFiles/creator-agent-plugin", + "repository": "https://github.com/LottieFiles/creator-agent-plugin", + "website": "https://lottiefiles.com", + "privacyPolicy": "https://lottiefiles.com/page/privacy-policy", + "termsOfService": "https://lottiefiles.com/page/terms-and-conditions" }, "mcpServerName": "lottiefiles-creator", "toolNames": [ @@ -23,11 +30,11 @@ "engine_job_result", "engine_cancel_job" ], - "category": "Productivity", + "category": "Creativity", "keywords": [ "lottiefiles", "lottie", "animation", - "graphql" + "creator" ] } diff --git a/gemini-extension.json b/gemini-extension.json index 5046947..7e74e31 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -1,5 +1,5 @@ { - "description": "Authenticated animation creation, export, document management, and LottieFiles API workflows.", + "description": "Use the authenticated LottieFiles Creator MCP server for animation creation, export, document management, and LottieFiles API workflows.", "mcpServers": { "lottiefiles-creator": { "httpUrl": "https://creator-mcp.lottiefiles.com/mcp" diff --git a/plugins/lottiefiles-creator/.claude-plugin/plugin.json b/plugins/lottiefiles-creator/.claude-plugin/plugin.json index 5f5ce37..90738b5 100644 --- a/plugins/lottiefiles-creator/.claude-plugin/plugin.json +++ b/plugins/lottiefiles-creator/.claude-plugin/plugin.json @@ -2,7 +2,7 @@ "author": { "name": "LottieFiles" }, - "description": "Authenticated animation creation, export, document management, and LottieFiles API workflows.", + "description": "Create, manage, and export Lottie animations.", "mcpServers": "./.mcp.json", "name": "lottiefiles-creator", "version": "0.1.0" diff --git a/plugins/lottiefiles-creator/.codex-plugin/plugin.json b/plugins/lottiefiles-creator/.codex-plugin/plugin.json index 334bde8..b4e97a5 100644 --- a/plugins/lottiefiles-creator/.codex-plugin/plugin.json +++ b/plugins/lottiefiles-creator/.codex-plugin/plugin.json @@ -1,14 +1,17 @@ { "author": { - "name": "LottieFiles" + "name": "LottieFiles", + "url": "https://lottiefiles.com" }, - "description": "Authenticated animation creation, export, document management, and LottieFiles API workflows.", + "description": "Create, manage, and export Lottie animations.", + "homepage": "https://github.com/LottieFiles/creator-agent-plugin", "interface": { + "brandColor": "#00DDB3", "capabilities": [ "Read", "Write" ], - "category": "Productivity", + "category": "Creativity", "defaultPrompt": [ "Create or edit an authenticated LottieFiles animation.", "Export an animation or inspect an export job.", @@ -17,10 +20,21 @@ "developerName": "LottieFiles", "displayName": "LottieFiles Creator", "longDescription": "Use the authenticated LottieFiles Creator MCP server for animation creation, export, document management, and LottieFiles API workflows.", - "shortDescription": "Authenticated animation creation, export, document management, and LottieFiles API workflows." + "privacyPolicyURL": "https://lottiefiles.com/page/privacy-policy", + "shortDescription": "Create, manage, and export Lottie animations.", + "termsOfServiceURL": "https://lottiefiles.com/page/terms-and-conditions", + "websiteURL": "https://lottiefiles.com" }, + "keywords": [ + "lottiefiles", + "lottie", + "animation", + "creator" + ], + "license": "MIT", "mcpServers": "./.mcp.json", "name": "lottiefiles-creator", + "repository": "https://github.com/LottieFiles/creator-agent-plugin", "skills": "./skills/", "version": "0.1.0" } diff --git a/plugins/lottiefiles-creator/.grok-plugin/plugin.json b/plugins/lottiefiles-creator/.grok-plugin/plugin.json index 5f5ce37..90738b5 100644 --- a/plugins/lottiefiles-creator/.grok-plugin/plugin.json +++ b/plugins/lottiefiles-creator/.grok-plugin/plugin.json @@ -2,7 +2,7 @@ "author": { "name": "LottieFiles" }, - "description": "Authenticated animation creation, export, document management, and LottieFiles API workflows.", + "description": "Create, manage, and export Lottie animations.", "mcpServers": "./.mcp.json", "name": "lottiefiles-creator", "version": "0.1.0" diff --git a/scripts/generate.py b/scripts/generate.py index 22e5b6b..7bc78c5 100644 --- a/scripts/generate.py +++ b/scripts/generate.py @@ -43,23 +43,29 @@ def remote_server(catalog: dict[str, Any], *, gemini: bool = False) -> dict[str, def plugin_manifest(catalog: dict[str, Any]) -> dict[str, Any]: + links = catalog["links"] return { "name": catalog["id"], "version": catalog["version"], "description": catalog["description"], - "author": {"name": catalog["publisher"]["name"]}, + "author": {"name": catalog["publisher"]["name"], "url": links["website"]}, + "homepage": links["homepage"], + "repository": links["repository"], + "license": "MIT", + "keywords": catalog["keywords"], "skills": "./skills/", "mcpServers": "./.mcp.json", "interface": { "displayName": catalog["displayName"], "shortDescription": catalog["description"], - "longDescription": ( - "Use the authenticated LottieFiles Creator MCP server for animation " - "creation, export, document management, and LottieFiles API workflows." - ), + "longDescription": catalog["longDescription"], "developerName": catalog["publisher"]["name"], "category": catalog["category"], "capabilities": ["Read", "Write"], + "websiteURL": links["website"], + "privacyPolicyURL": links["privacyPolicy"], + "termsOfServiceURL": links["termsOfService"], + "brandColor": "#00DDB3", "defaultPrompt": [ "Create or edit an authenticated LottieFiles animation.", "Export an animation or inspect an export job.", @@ -85,7 +91,7 @@ def marketplace_entry(catalog: dict[str, Any], source: Any) -> dict[str, Any]: "description": catalog["description"], "version": catalog["version"], "source": source, - "category": "productivity", + "category": "creativity", "keywords": catalog["keywords"] } @@ -100,14 +106,14 @@ def generated_json(catalog: dict[str, Any]) -> dict[Path, dict[str, Any]]: ROOT / "gemini-extension.json": { "name": catalog["id"], "version": catalog["version"], - "description": description, + "description": catalog["longDescription"], "mcpServers": {server_name: remote_server(catalog, gemini=True)} }, ROOT / "server.json": { "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", "name": "com.lottiefiles/creator", "title": catalog["displayName"], - "description": description, + "description": catalog["longDescription"], "version": catalog["version"], "remotes": [{"type": "streamable-http", "url": catalog["remote"]["productionUrl"]}] }, @@ -189,19 +195,30 @@ def sync_tree(source: Path, destination: Path, *, check: bool) -> bool: return changed -def sync_content(*, check: bool) -> bool: - changed = False - for directory in ("skills", "assets"): - source = ROOT / "content" / directory - destination = PLUGIN_ROOT / directory - if source.exists(): - changed = sync_tree(source, destination, check=check) or changed +def sync_content(*, check: bool) -> list[Path]: + stale: list[Path] = [] + source_skills = ROOT / "content" / "skills" + for destination in (PLUGIN_ROOT / "skills", ROOT / "skills"): + if source_skills.exists(): + if sync_tree(source_skills, destination, check=check): + stale.append(destination.relative_to(ROOT)) elif destination.exists(): if check: - changed = True + stale.append(destination.relative_to(ROOT)) else: shutil.rmtree(destination) - return changed + + source_assets = ROOT / "content" / "assets" + destination_assets = PLUGIN_ROOT / "assets" + if source_assets.exists(): + if sync_tree(source_assets, destination_assets, check=check): + stale.append(destination_assets.relative_to(ROOT)) + elif destination_assets.exists(): + if check: + stale.append(destination_assets.relative_to(ROOT)) + else: + shutil.rmtree(destination_assets) + return stale def main() -> int: @@ -215,8 +232,7 @@ def main() -> int: if not args.check: path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(expected) - if sync_content(check=args.check): - stale.append(Path("plugins/lottiefiles-creator/content")) + stale.extend(sync_content(check=args.check)) if args.check and stale: print("stale generated outputs:", file=sys.stderr) for path in stale: diff --git a/scripts/validate.py b/scripts/validate.py index b17860d..dabb7f2 100644 --- a/scripts/validate.py +++ b/scripts/validate.py @@ -140,8 +140,16 @@ def main() -> int: if not isinstance(version, str) or not SEMVER.fullmatch(version): errors.append("catalog version must be strict semver") validate_https(remote, "catalog.remote.productionUrl", errors) - if remote == value_at(catalog, "remote", "stagingUrl"): - errors.append("staging URL must not be the public default") + if set(catalog.get("remote", {})) != {"productionUrl"}: + errors.append("catalog remote metadata must publish only productionUrl") + links = catalog.get("links") + if not isinstance(links, dict): + errors.append("catalog links must be an object") + else: + for key in ("homepage", "repository", "website", "privacyPolicy", "termsOfService"): + validate_https(links.get(key), f"catalog.links.{key}", errors) + if catalog.get("category") != "Creativity": + errors.append("catalog category must be Creativity") for path, payload in payloads.items(): if payload is None: continue diff --git a/server.json b/server.json index 4ae44cc..9665503 100644 --- a/server.json +++ b/server.json @@ -1,6 +1,6 @@ { "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", - "description": "Authenticated animation creation, export, document management, and LottieFiles API workflows.", + "description": "Use the authenticated LottieFiles Creator MCP server for animation creation, export, document management, and LottieFiles API workflows.", "name": "com.lottiefiles/creator", "remotes": [ { diff --git a/skills/animation-review/SKILL.md b/skills/animation-review/SKILL.md new file mode 100644 index 0000000..2b5a498 --- /dev/null +++ b/skills/animation-review/SKILL.md @@ -0,0 +1,17 @@ +--- +name: lottiefiles-animation-review +description: Inspect, review, compare, or diagnose an animation result with installed Engine tools. Use when the user asks why an animation differs, fails, renders poorly, or needs a quality review. +--- + +# Animation review and diagnosis + +Use `engine_docs` to discover the installed Engine contract. Use `engine_document` to inspect, `engine_exec` to render or inspect when the contract requires it, and `engine_job_status` or `engine_job_result` for async work. Read [the contract reference](references/tool-contract.md) before a tool call. + +1. Inspect the source, result, and reported symptom before you diagnose. +2. Compare the smallest relevant artifact, frame range, or property set. +3. Separate confirmed facts from hypotheses. State the evidence for each finding. +4. Do not change the animation during a review unless the user asks for a fix. +5. If a tool returns an async job, inspect the job result before you report a render or export failure. +6. Recommend the smallest safe next action and identify any missing input. + +Do not claim visual correctness from metadata alone. Do not expose private document content in diagnostics. diff --git a/skills/animation-review/references/tool-contract.md b/skills/animation-review/references/tool-contract.md new file mode 100644 index 0000000..8f0bd4d --- /dev/null +++ b/skills/animation-review/references/tool-contract.md @@ -0,0 +1,10 @@ +# Engine review tool contract reconciliation + +Reconcile exact argument and result shapes with the released public Engine contract fixture before publication. + +| Review need | Released tool name | Evidence to report | +| --- | --- | --- | +| Discover contract | `engine_docs` | Current capability and argument requirements | +| Inspect document | `engine_document` | Document state and relevant properties | +| Run review action | `engine_exec` | Contract-defined artifact or diagnostic result | +| Inspect async job | `engine_job_status` / `engine_job_result` | Job state, error code, and output location | diff --git a/skills/animation-workflows/SKILL.md b/skills/animation-workflows/SKILL.md new file mode 100644 index 0000000..0d9ca1e --- /dev/null +++ b/skills/animation-workflows/SKILL.md @@ -0,0 +1,17 @@ +--- +name: lottiefiles-animation-workflows +description: Create, edit, save, export, or manage an animation document with installed Engine tools. Use when the user asks to build or change an animation, export a file, or check an async Engine job. +--- + +# Animation workflows + +Use `engine_docs` to discover the installed Engine contract. Use `engine_document` to inspect a document, `engine_exec` to make a document change, `engine_export` to export, and `engine_job_status` or `engine_job_result` to inspect async work. Read [the contract reference](references/tool-contract.md) before a tool call. + +1. Ask for the target document, change, output format, and destination when they are not clear. +2. Inspect the document before you change it. Report the current state that affects the request. +3. Make the smallest requested change. Do not replace unrelated content. +4. Save or export only after the user confirms the destination when the action writes a file or replaces an asset. +5. For an async operation, keep its job identifier. Check its state until it succeeds, fails, or needs user action. +6. Report the document or job identifier, result location, and any warning. Do not expose credentials or private data. + +Do not invent tool arguments. Do not assume that an export completed until the job result confirms it. diff --git a/skills/animation-workflows/references/tool-contract.md b/skills/animation-workflows/references/tool-contract.md new file mode 100644 index 0000000..cfdf022 --- /dev/null +++ b/skills/animation-workflows/references/tool-contract.md @@ -0,0 +1,16 @@ +# Engine tool contract reconciliation + +The released public tool names are listed below. Reconcile exact argument and result shapes with the published contract fixture before publication. + +Required mapping: + +| Operation | Released tool name | Required arguments | Result fields | +| --- | --- | --- | --- | +| Discover contract | `engine_docs` | Reconcile with fixture | Reconcile with fixture | +| Inspect document | `engine_document` | Reconcile with fixture | Reconcile with fixture | +| Create or edit document | `engine_exec` | Reconcile with fixture | Reconcile with fixture | +| Upload input | `request_upload` | Reconcile with fixture | Reconcile with fixture | +| Export document | `engine_export` | Reconcile with fixture | Reconcile with fixture | +| Inspect job state | `engine_job_status` | Reconcile with fixture | Reconcile with fixture | +| Get job result | `engine_job_result` | Reconcile with fixture | Reconcile with fixture | +| Cancel job | `engine_cancel_job` | Reconcile with fixture | Reconcile with fixture | diff --git a/skills/lottiefiles-library/SKILL.md b/skills/lottiefiles-library/SKILL.md new file mode 100644 index 0000000..0b23915 --- /dev/null +++ b/skills/lottiefiles-library/SKILL.md @@ -0,0 +1,17 @@ +--- +name: lottiefiles-library-account +description: Search or inspect LottieFiles library or account data with lf_graphql. Use when the user asks to find, list, inspect, upload, update, delete, or organize LottieFiles resources. +--- + +# LottieFiles library and account workflows + +Full LottieFiles GraphQL access is intentional. It supports the capabilities that the connected account has. It does not grant extra scopes. + +1. Discover the needed schema and capability first. Use only fields that the current public schema confirms. +2. Read before a change. Inspect the target resource and its ownership or current state. +3. Request the minimum fields. Send values as variables. Do not put secrets in a query. +4. Paginate lists. Use the returned cursor and a user-approved limit. +5. Before a mutation or delete, state the target and effect. Require explicit user intent and host confirmation. +6. Inspect the response after a mutation. Handle GraphQL partial errors and report which data is reliable. + +Never bypass scopes. Never log tokens or query variables. Do not infer a field or mutation name from a human label. diff --git a/skills/lottiefiles-library/references/tool-contract.md b/skills/lottiefiles-library/references/tool-contract.md new file mode 100644 index 0000000..dae96cf --- /dev/null +++ b/skills/lottiefiles-library/references/tool-contract.md @@ -0,0 +1,13 @@ +# GraphQL contract reference + +Use `lf_graphql` for LottieFiles GraphQL operations. The public schema fixture is required before publication of schema-specific examples or field names. + +Safe request shape: + +- Use one operation with an `operationName`. +- Put user and resource values in `variables`. +- Request only required fields. +- Preserve `data` when GraphQL returns both `data` and `errors`. +- Treat `errors` as an incomplete result until the response identifies the failed path. + +Schema placeholder notation in this repository uses ``, ``, and ``. It is not an API claim. From 01e80cbe0e31c2faf8e225fe4bdd5c572d371eca Mon Sep 17 00:00:00 2001 From: Jawish Hameed Date: Sun, 6 Sep 2026 00:02:50 +0800 Subject: [PATCH 04/12] docs: clarify client distribution support --- README.md | 2 +- docs/installation.md | 38 ++++++++++++++++++++++++++++++++------ docs/research.md | 16 +++++++++------- docs/security.md | 3 ++- submission/checklist.md | 10 +++++++++- submission/test-cases.md | 2 +- 6 files changed, 54 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 0d93166..928d666 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ The package does not claim a live directory listing or public endpoint. Read [in 6. Keep tokens and variables out of logs. 7. Treat GraphQL `data` with `errors` as partial data. -Full LottieFiles GraphQL access is intentional. The connected account scopes still define what the tool can access. The plugin never bypasses scopes. +Full LottieFiles GraphQL access is intentional. Use bounded requests, minimum fields, variables, pagination, read-before-write checks, and partial-error handling. Account scopes, host confirmation, and server time and size limits control access. Instructions and prompts do not bypass those controls. ## Contents diff --git a/docs/installation.md b/docs/installation.md index fd8af4c..36abbba 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,19 +1,45 @@ # Install and use -This repository is a source package. It does not prove that a public directory listing or remote MCP endpoint is live. The release manager must publish and validate the package before users can install it from a directory. +This repository supports source installation. It does not prove that a universal public directory listing or production remote endpoint is approved and live. The release manager must complete publication checks before a directory listing or manual remote connection is announced. ## Client matrix | Client | Native package support | Manual MCP connection | Use after connection | | --- | --- | --- | --- | -| ChatGPT and Codex | Not confirmed for this package. Use only a released host install path. | Pending a published public MCP endpoint and host support. | Discover the tools, then follow the included skills. | -| Claude Code | Not confirmed for this package. Claude Code supports plugin-provided and manually configured MCP servers. | Confirmed client capability; add the released endpoint or command with Claude Code MCP settings. | Approve the server, authenticate if required, then verify tools. | -| Grok Build | Not confirmed for this package. Grok Build supports plugins and MCP configuration. | Confirmed client capability; add the released endpoint or command with `grok mcp add`. | Run `grok inspect`, then use the discovered tools. | -| ZCode | Not confirmed for this package. ZCode supports plugins that bundle MCP servers. | Confirmed client capability; add the released endpoint or command in Settings -> MCP Servers. | Confirm enabled server and inspect its tools. | -| Gemini CLI | Not confirmed for this package. Gemini CLI supports MCP servers. | Confirmed client capability; add the released endpoint or command with `gemini mcp add` or `settings.json`. | Run `/mcp list` and inspect available tools. | +| ChatGPT desktop and Codex | Confirmed repository marketplace support. The universal public directory listing is not approved yet. | Wait for the verified production endpoint and OAuth setup. | Install from source, then discover tools before use. | +| Claude Code | Confirmed repository marketplace support. Claude Code also supports plugin-provided and manual MCP servers. | Add the verified endpoint with Claude Code MCP settings only after publication. | Approve the server, authenticate if required, then verify tools. | +| Grok Build | Adapter support uses the official xAI marketplace format and CLI validation. No directory listing claim. | Use the official Grok marketplace or MCP guidance. Do not use an unpublished endpoint. | Run `grok inspect`, then use the discovered tools. | +| ZCode | ZCode consumes the Claude-compatible manifest. No duplicate ZCode manifest exists. | Add the repository as a marketplace in ZCode, or add a verified MCP server in Settings -> MCP Servers. | Confirm enabled server and inspect its tools. | +| Gemini CLI | Confirmed source extension shape: root `gemini-extension.json` and root `skills` are present. | Add a verified MCP server only when the production endpoint and OAuth are approved. | Run `/mcp list` and inspect available tools. | | Generic MCP client | No native package claim. | Use the client’s streamable HTTP or stdio configuration after the server is published. | Discover capabilities before a tool call. | | MCP Registry | Not listed at publication time. | Registry registration is an administrator action after release validation. | Install only from the registry entry after it exists. | +## Source installation + +Codex: + +```sh +codex plugin marketplace add LottieFiles/creator-agent-plugin +codex plugin add lottiefiles-creator@lottiefiles +``` + +Claude Code: + +```sh +claude plugin marketplace add LottieFiles/creator-agent-plugin +claude plugin install lottiefiles-creator@lottiefiles +``` + +Gemini CLI: + +```sh +gemini extensions install https://github.com/LottieFiles/creator-agent-plugin +``` + +For Grok Build, follow the official plugin marketplace documentation and validate the adapter with the official CLI. For ZCode, use **Create -> Add marketplace** and select this repository. ZCode documents its fallback to a Claude-compatible plugin manifest, so this package does not include a second ZCode manifest. + +`.app.json` is intentionally absent. Do not add it until the OpenAI portal issues a real connector app ID. A placeholder app ID would create an invalid connector claim. + ## Safe connection procedure 1. Get the endpoint or command from a signed release. Do not use a staging or private address. diff --git a/docs/research.md b/docs/research.md index 7871c83..8aaf70e 100644 --- a/docs/research.md +++ b/docs/research.md @@ -1,21 +1,23 @@ # Research and support status -Reviewed 2026-09-05. This document records official sources used for packaging decisions. It does not claim that this package is listed or available in any directory. +Reviewed 2026-09-06. This document records official sources used for packaging decisions. It does not claim that this package is listed in a universal public directory or that its remote endpoint is live. ## Client support | Product | Official source | Confirmed capability | Package status | | --- | --- | --- | --- | -| OpenAI | [OpenAI Apps SDK](https://developers.openai.com/apps-sdk/) | Official OpenAI documentation is the source for ChatGPT app and MCP integration requirements. | Native package support and directory listing require release validation. | -| Claude Code | [MCP documentation](https://code.claude.com/docs/en/mcp) | Supports remote HTTP and local stdio MCP servers. It also supports plugin-provided servers. | Manual MCP connection is supported by the client; this package needs host validation. | -| Grok Build | [MCP servers](https://docs.x.ai/build/features/mcp-servers) | Supports configured MCP servers. | Manual MCP connection is supported by the client; this package needs host validation. | -| ZCode | [Plugin documentation](https://zcode.z.ai/en/docs/plugin) and [MCP documentation](https://zcode.z.ai/en/docs/mcp-services) | Supports plugins and manual MCP servers. | Manual MCP connection is supported by the client; this package needs host validation. | -| Gemini CLI | [MCP server documentation](https://geminicli.com/docs/tools/mcp-server/) and [source repository](https://github.com/google-gemini/gemini-cli) | Supports MCP server configuration. | Manual MCP connection is supported by the client; this package needs host validation. | +| OpenAI | [OpenAI Plugins](https://developers.openai.com/plugins) | Codex and ChatGPT desktop support repository marketplaces. | This repository supports source installation. Universal public directory approval is pending. `.app.json` stays absent until the OpenAI portal issues a real connector app ID. | +| Claude Code | [MCP documentation](https://code.claude.com/docs/en/mcp) | Supports remote HTTP, local stdio, and plugin-provided MCP servers. | Repository marketplace install is supported. Manual MCP connection needs a verified production endpoint. | +| Grok Build | [Plugins and marketplaces](https://docs.x.ai/build/features/skills-plugins-marketplaces), [CLI reference](https://docs.x.ai/build/cli/reference), and [official marketplace source](https://github.com/xai-org/plugin-marketplace) | Supports plugin marketplaces and MCP configuration. | The Grok adapter uses the current xAI format and official CLI validation. It has no directory listing claim. | +| ZCode | [Plugin documentation](https://zcode.z.ai/en/docs/plugin) and [MCP documentation](https://zcode.z.ai/en/docs/mcp-services) | Supports plugins and manual MCP servers. The documented manifest lookup falls back to a Claude-compatible manifest. | The package uses that fallback. It intentionally has no duplicate ZCode manifest. | +| Gemini CLI | [Extension reference](https://geminicli.com/docs/extensions/reference/) and [source repository](https://github.com/google-gemini/gemini-cli) | Supports extension installation from a Git source and MCP configuration. | The root extension manifest and root skills support source installation. Manual MCP connection needs a verified production endpoint. | | MCP Registry | [MCP Registry](https://registry.modelcontextprotocol.io/) and [MCP specification](https://modelcontextprotocol.io/specification/) | Provides an official registry and protocol reference. | Registration is not complete until an administrator publishes an entry. | ## Integration precedents -Cloudflare documents a broad MCP server that exposes platform capabilities. This is the raw or broad API precedent: [Cloudflare MCP server source](https://github.com/cloudflare/mcp-server-cloudflare) and [Cloudflare MCP documentation](https://developers.cloudflare.com/agents/model-context-protocol/). Linear documents an official curated, read-only MCP integration: [Linear MCP documentation](https://linear.app/docs/mcp). These precedents support a broad GraphQL tool only with strict schema discovery, scope, confirmation, and result-handling controls. +Cloudflare is the broad API precedent. Its official MCP uses compact `search` and `execute` Code Mode tools to expose the full Cloudflare API and can execute GraphQL: [Cloudflare MCP source](https://github.com/cloudflare/mcp). Linear is the curated-object precedent. Its official MCP exposes object tools and a [read-only endpoint](https://linear.app/docs/mcp); Linear’s separate [public API is GraphQL](https://linear.app/developers/graphql). Do not describe Linear MCP as raw GraphQL. + +These precedents support a mixed-operation GraphQL tool only when the implementation enforces bounded requests, scopes, server time and size limits, read-before-write checks, confirmation for writes and deletes, pagination, and partial-error handling. Prompt text is guidance; authorization and host controls enforce access. ## LottieFiles sources diff --git a/docs/security.md b/docs/security.md index 768b8e5..0c58bdd 100644 --- a/docs/security.md +++ b/docs/security.md @@ -2,7 +2,7 @@ ## Security model -The host controls tool execution and user confirmation. The connected LottieFiles account controls GraphQL authorization. This package does not grant scopes or bypass host controls. +The host controls tool execution and user confirmation. The connected LottieFiles account controls GraphQL authorization. The server enforces time and size limits. This package does not grant scopes or bypass host controls. Prompts and skills guide use; they are not security boundaries. ## Threats and controls @@ -12,6 +12,7 @@ The host controls tool execution and user confirmation. The connected LottieFile | Excessive account access | Discover capability first; request only needed fields; never bypass scopes. | | Token or variable exposure | Keep tokens in approved secret storage; never log tokens or GraphQL variables. | | Large list or result exposure | Use bounded pages and minimum fields. | +| Excessive execution time or response size | Enforce server time and size limits. | | Partial GraphQL failure | Preserve available data; report errors and failed paths; re-read after uncertain mutation. | | Export overwrite | Ask for the exact destination and confirmation before writing or replacing an asset. | | Async job duplication | Keep the job identifier and inspect state before retrying. | diff --git a/submission/checklist.md b/submission/checklist.md index 3cace41..299927b 100644 --- a/submission/checklist.md +++ b/submission/checklist.md @@ -11,16 +11,24 @@ ## Human and administrator gates +- [ ] OpenAI portal administrator has Apps Management permission `api.apps.write`. +- [ ] Publisher identity is verified in the target portal. +- [ ] Production public endpoint and OAuth flow are verified from an external network. +- [ ] Use a real connector app ID before adding `.app.json`. - [ ] Product owner approves listing copy, prompts, and examples. - [ ] LottieFiles administrator approves GraphQL scopes and mutation behavior. - [ ] Security owner approves the threat model and data-flow notes. - [ ] Legal and brand owner approves license, trademarks, links, and asset provenance. +- [ ] Confirm logo provenance, category, legal URLs, and support URL in the listing. +- [ ] Provide a reviewer account when the marketplace requires one. - [ ] Marketplace administrator approves the final package and directory submission. +- [ ] Final publisher approves the release. - [ ] Release manager records the package version, commit, checksum, and rollback target. ## Live acceptance -- [ ] Test each positive and negative case in the target host. +- [ ] Run the five positive and three negative golden prompts in the target host. +- [ ] Confirm annotations, tool descriptions, and security schemes match live behavior. - [ ] Confirm the host shows write and delete confirmation. - [ ] Confirm OAuth and revoked-scope behavior. - [ ] Confirm partial GraphQL errors retain safe data and report the failure. diff --git a/submission/test-cases.md b/submission/test-cases.md index 4327d2b..1011f27 100644 --- a/submission/test-cases.md +++ b/submission/test-cases.md @@ -16,4 +16,4 @@ Use synthetic fixtures only. A host reviewer must replace tool arguments with th ## Annotation rationale: `lf_graphql` -`lf_graphql` is a broad mixed-operation tool because GraphQL capability comes from the current schema and connected account scopes. A fixed list of narrow tools would become incomplete when the public schema evolves. The prompt and skill controls limit the tool: discover schema first, request minimum fields, use variables, paginate, inspect before mutation, and require explicit intent and host confirmation for writes or deletes. The tool does not bypass scopes or authorization. +`lf_graphql` is a broad mixed-operation tool because GraphQL capability comes from the current schema and connected account scopes. A fixed list of narrow tools would become incomplete when the public schema evolves. The implementation must enforce scopes, bounded requests, server time and size limits, and host confirmation. The skill guides safe use: discover schema first, request minimum fields, use variables, paginate, inspect before mutation, and require explicit intent for writes or deletes. The tool does not bypass scopes or authorization. From 98309362dee540855626470e8a9557f58a214148 Mon Sep 17 00:00:00 2001 From: Jawish Hameed Date: Sun, 6 Sep 2026 00:04:35 +0800 Subject: [PATCH 05/12] fix: limit registry description length --- scripts/generate.py | 2 +- scripts/validate.py | 3 +++ server.json | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/generate.py b/scripts/generate.py index 7bc78c5..cae7a01 100644 --- a/scripts/generate.py +++ b/scripts/generate.py @@ -113,7 +113,7 @@ def generated_json(catalog: dict[str, Any]) -> dict[Path, dict[str, Any]]: "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", "name": "com.lottiefiles/creator", "title": catalog["displayName"], - "description": catalog["longDescription"], + "description": description, "version": catalog["version"], "remotes": [{"type": "streamable-http", "url": catalog["remote"]["productionUrl"]}] }, diff --git a/scripts/validate.py b/scripts/validate.py index dabb7f2..c4758f0 100644 --- a/scripts/validate.py +++ b/scripts/validate.py @@ -169,6 +169,9 @@ def main() -> int: validate_https(server.get("url", server.get("httpUrl")), f"{path.relative_to(ROOT)} MCP URL", errors) server = payloads.get(ROOT / "server.json") if server is not None: + server_description = server.get("description") + if not isinstance(server_description, str) or len(server_description) > 100: + errors.append("server.json description must be at most 100 characters") remotes = server.get("remotes") if not isinstance(remotes, list) or len(remotes) != 1 or remotes[0].get("type") != "streamable-http": errors.append("server.json must define one streamable-http remote") diff --git a/server.json b/server.json index 9665503..ebb1643 100644 --- a/server.json +++ b/server.json @@ -1,6 +1,6 @@ { "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", - "description": "Use the authenticated LottieFiles Creator MCP server for animation creation, export, document management, and LottieFiles API workflows.", + "description": "Create, manage, and export Lottie animations.", "name": "com.lottiefiles/creator", "remotes": [ { From 50da9704d9de6cb9ef7f05a83b910785f39af0ed Mon Sep 17 00:00:00 2001 From: Jawish Hameed Date: Sun, 6 Sep 2026 00:04:46 +0800 Subject: [PATCH 06/12] docs: limit distribution to production metadata --- docs/installation.md | 6 ++++-- docs/research.md | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index 36abbba..f85a3d8 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -6,7 +6,7 @@ This repository supports source installation. It does not prove that a universal | Client | Native package support | Manual MCP connection | Use after connection | | --- | --- | --- | --- | -| ChatGPT desktop and Codex | Confirmed repository marketplace support. The universal public directory listing is not approved yet. | Wait for the verified production endpoint and OAuth setup. | Install from source, then discover tools before use. | +| ChatGPT desktop and Codex | Both support repository marketplaces. The Codex source commands below are verified. ChatGPT desktop uses its documented UI install path. The universal public directory listing is not approved yet. | Wait for the verified production endpoint and OAuth setup. | Use the Codex source commands or the ChatGPT desktop UI, then discover tools before use. | | Claude Code | Confirmed repository marketplace support. Claude Code also supports plugin-provided and manual MCP servers. | Add the verified endpoint with Claude Code MCP settings only after publication. | Approve the server, authenticate if required, then verify tools. | | Grok Build | Adapter support uses the official xAI marketplace format and CLI validation. No directory listing claim. | Use the official Grok marketplace or MCP guidance. Do not use an unpublished endpoint. | Run `grok inspect`, then use the discovered tools. | | ZCode | ZCode consumes the Claude-compatible manifest. No duplicate ZCode manifest exists. | Add the repository as a marketplace in ZCode, or add a verified MCP server in Settings -> MCP Servers. | Confirm enabled server and inspect its tools. | @@ -23,6 +23,8 @@ codex plugin marketplace add LottieFiles/creator-agent-plugin codex plugin add lottiefiles-creator@lottiefiles ``` +The `codex plugin add lottiefiles-creator@lottiefiles` command was verified against the installed Codex CLI command schema on 2026-09-06. The public OpenAI packaging page documents marketplace addition and uses the desktop UI as the install path. Do not treat the Codex command as a ChatGPT desktop source-install command. + Claude Code: ```sh @@ -42,7 +44,7 @@ For Grok Build, follow the official plugin marketplace documentation and validat ## Safe connection procedure -1. Get the endpoint or command from a signed release. Do not use a staging or private address. +1. Get the endpoint or command from a signed release. Do not use a private address. 2. Add only the required environment variables or OAuth connection. Keep tokens in the client secret store or environment. 3. Connect and inspect the tool list. Confirm that only expected tools appear. 4. Run a read-only example first. diff --git a/docs/research.md b/docs/research.md index 8aaf70e..f0df22c 100644 --- a/docs/research.md +++ b/docs/research.md @@ -6,7 +6,7 @@ Reviewed 2026-09-06. This document records official sources used for packaging d | Product | Official source | Confirmed capability | Package status | | --- | --- | --- | --- | -| OpenAI | [OpenAI Plugins](https://developers.openai.com/plugins) | Codex and ChatGPT desktop support repository marketplaces. | This repository supports source installation. Universal public directory approval is pending. `.app.json` stays absent until the OpenAI portal issues a real connector app ID. | +| OpenAI | [OpenAI Plugins](https://developers.openai.com/plugins) | Codex and ChatGPT desktop support repository marketplaces. The Codex source install command is verified; the public packaging page uses the desktop UI install path. | Universal public directory approval is pending. `.app.json` stays absent until the OpenAI portal issues a real connector app ID. | | Claude Code | [MCP documentation](https://code.claude.com/docs/en/mcp) | Supports remote HTTP, local stdio, and plugin-provided MCP servers. | Repository marketplace install is supported. Manual MCP connection needs a verified production endpoint. | | Grok Build | [Plugins and marketplaces](https://docs.x.ai/build/features/skills-plugins-marketplaces), [CLI reference](https://docs.x.ai/build/cli/reference), and [official marketplace source](https://github.com/xai-org/plugin-marketplace) | Supports plugin marketplaces and MCP configuration. | The Grok adapter uses the current xAI format and official CLI validation. It has no directory listing claim. | | ZCode | [Plugin documentation](https://zcode.z.ai/en/docs/plugin) and [MCP documentation](https://zcode.z.ai/en/docs/mcp-services) | Supports plugins and manual MCP servers. The documented manifest lookup falls back to a Claude-compatible manifest. | The package uses that fallback. It intentionally has no duplicate ZCode manifest. | @@ -29,4 +29,6 @@ These precedents support a mixed-operation GraphQL tool only when the implementa The brand-assets endpoint could not be reliably retrieved during package preparation. No logo binary is included. A brand owner must verify provenance and approve any asset before publication. -The creator MCP production and staging DNS endpoints did not resolve during package preparation. This is an external publication blocker. Do not claim live endpoint availability until an external test verifies it. +The tested production endpoint did not resolve during package preparation. This is an external publication blocker. Do not claim live endpoint availability until an external test verifies it. + +The public repository distributes production metadata only. Non-production interoperability stays in the private engine workstream. From 0228474fb92868ff2c194c19fcfec5fa4f3dfba1 Mon Sep 17 00:00:00 2001 From: Jawish Hameed Date: Sun, 6 Sep 2026 00:14:24 +0800 Subject: [PATCH 07/12] docs: pin public tool contract --- .github/dependabot.yml | 11 + .github/workflows/ci.yml | 73 ++ .github/workflows/publication-probe.yml | 40 + .github/workflows/release.yml | 88 ++ .gitignore | 2 + content/references/tool-contract.md | 22 + content/skills/animation-review/SKILL.md | 2 +- .../references/tool-contract.md | 10 - content/skills/animation-workflows/SKILL.md | 2 +- .../references/tool-contract.md | 16 - content/skills/lottiefiles-library/SKILL.md | 2 +- .../references/tool-contract.md | 13 - contract/engine-mcp-tools.json | 1157 +++++++++++++++++ contract/engine-mcp-tools.sha256 | 1 + docs/release.md | 4 +- examples/README.md | 2 +- examples/engine-async-job.md | 4 +- examples/engine-create-export.md | 4 +- mise.toml | 5 + release-include.txt | 18 + scripts/check-content.py | 69 + scripts/check-contract.py | 160 +++ scripts/check-json.py | 32 + scripts/check-mcp-registry.sh | 35 + scripts/check-package.py | 75 ++ scripts/check-paths.py | 61 + scripts/check-platforms.py | 144 ++ scripts/check-provenance.py | 38 + scripts/check-public.py | 30 + scripts/check-version.py | 62 + scripts/check.sh | 31 + scripts/check_common.py | 101 ++ scripts/package.py | 123 ++ submission/checklist.md | 2 +- submission/release-notes.md | 5 +- submission/test-cases.md | 2 +- 36 files changed, 2393 insertions(+), 53 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/publication-probe.yml create mode 100644 .github/workflows/release.yml create mode 100644 content/references/tool-contract.md delete mode 100644 content/skills/animation-review/references/tool-contract.md delete mode 100644 content/skills/animation-workflows/references/tool-contract.md delete mode 100644 content/skills/lottiefiles-library/references/tool-contract.md create mode 100644 contract/engine-mcp-tools.json create mode 100644 contract/engine-mcp-tools.sha256 create mode 100644 mise.toml create mode 100644 release-include.txt create mode 100644 scripts/check-content.py create mode 100644 scripts/check-contract.py create mode 100644 scripts/check-json.py create mode 100644 scripts/check-mcp-registry.sh create mode 100644 scripts/check-package.py create mode 100644 scripts/check-paths.py create mode 100644 scripts/check-platforms.py create mode 100644 scripts/check-provenance.py create mode 100644 scripts/check-public.py create mode 100644 scripts/check-version.py create mode 100644 scripts/check.sh create mode 100644 scripts/check_common.py create mode 100644 scripts/package.py diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..c11e42b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + cooldown: + default-days: 7 + semver-major-days: 14 + commit-message: + prefix: "chore(ci)" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3dfdb0f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,73 @@ +name: Public distribution CI + +on: + pull_request: + push: + branches: [main] + workflow_dispatch: + +permissions: {} + +env: + MISE_AUTO_INSTALL: "0" + +concurrency: + group: public-distribution-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +defaults: + run: + shell: bash -euo pipefail {0} + +jobs: + verify: + name: Verify public distribution + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + with: + python-version: "3.12" + - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4 + with: + cache: false + version: 2026.9.1 + - name: Install pinned check tools + run: mise install --yes + - name: Run deterministic distribution checks + run: bash scripts/check.sh + - name: Lint workflows and scripts + run: | + mise exec -- actionlint .github/workflows/*.yml + mise exec -- zizmor .github + mise exec -- shellcheck scripts/*.sh + - name: Validate MCP Registry server metadata + run: bash scripts/check-mcp-registry.sh + - name: Check public links without product endpoint probes + # These bot-protected LottieFiles pages need human verification. + run: >- + mise exec -- lychee --no-progress --exclude-all-private --max-concurrency 16 + --exclude 'https://creator-mcp[.]lottiefiles[.]com/.*' + --exclude '^https://lottiefiles[.]com/$' + --exclude '^https://lottiefiles[.]com/brand-assets$' + --exclude '^https://lottiefiles[.]com/page/privacy-policy$' + --exclude '^https://lottiefiles[.]com/page/terms-and-conditions$' + README.md content docs examples skills submission + + secrets: + name: Scan public files + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + - uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2.3.9 diff --git a/.github/workflows/publication-probe.yml b/.github/workflows/publication-probe.yml new file mode 100644 index 0000000..b939414 --- /dev/null +++ b/.github/workflows/publication-probe.yml @@ -0,0 +1,40 @@ +name: Publication availability probe + +on: + workflow_dispatch: + schedule: + - cron: "23 6 * * *" + +permissions: {} + +defaults: + run: + shell: bash -euo pipefail {0} + +jobs: + probe: + name: Probe unauthenticated public endpoints + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: {} + steps: + - name: Check production publication endpoint + run: | + endpoints=( + "https://creator-mcp.lottiefiles.com" + ) + for endpoint in "${endpoints[@]}"; do + health_status="$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' \ + --connect-timeout 10 --max-time 20 --proto '=https' --tlsv1.2 "$endpoint/health" || true)" + mcp_status="$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' \ + --connect-timeout 10 --max-time 20 --proto '=https' --tlsv1.2 \ + --header 'content-type: application/json' --request POST \ + --data '{"jsonrpc":"2.0","id":"publication-probe","method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"publication-probe","version":"1"}}}' \ + "$endpoint/mcp" || true)" + echo "$endpoint health=$health_status unauthenticated-mcp=$mcp_status" + test "$health_status" = "200" + case "$mcp_status" in + 401|403) ;; + *) echo "Expected unauthenticated /mcp status 401 or 403." >&2; exit 1 ;; + esac + done diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..7782627 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,88 @@ +name: Create distribution release + +on: + push: + tags: ["v*"] + +permissions: {} + +env: + MISE_AUTO_INSTALL: "0" + +defaults: + run: + shell: bash -euo pipefail {0} + +jobs: + verify: + name: Verify release content + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + with: + python-version: "3.12" + - uses: jdx/mise-action@c2a87611a18de5b3828c5652fe268e992400cb5c # v4 + with: + cache: false + version: 2026.9.1 + - name: Install pinned check tools + run: mise install --yes + - uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2.3.9 + - name: Run deterministic distribution checks + run: bash scripts/check.sh + - name: Lint workflows and scripts + run: | + mise exec -- actionlint .github/workflows/*.yml + mise exec -- zizmor .github + mise exec -- shellcheck scripts/*.sh + - name: Validate MCP Registry server metadata + run: bash scripts/check-mcp-registry.sh + - name: Check public links without product endpoint probes + # These bot-protected LottieFiles pages need human verification. + run: >- + mise exec -- lychee --no-progress --exclude-all-private --max-concurrency 16 + --exclude 'https://creator-mcp[.]lottiefiles[.]com/.*' + --exclude '^https://lottiefiles[.]com/$' + --exclude '^https://lottiefiles[.]com/brand-assets$' + --exclude '^https://lottiefiles[.]com/page/privacy-policy$' + --exclude '^https://lottiefiles[.]com/page/terms-and-conditions$' + README.md content docs examples skills submission + - name: Verify release tag versions + run: python3 scripts/check-version.py "$GITHUB_REF_NAME" + - name: Create deterministic archives + run: python3 scripts/package.py --output dist + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: lottiefiles-creator-${{ github.ref_name }} + path: dist/ + if-no-files-found: error + + publish: + name: Attach verified distribution archives + needs: verify + runs-on: ubuntu-24.04 + timeout-minutes: 5 + permissions: + attestations: write # Store provenance for the verified local release archives. + contents: write # Create the GitHub release and attach verified archives. + id-token: write # Sign the archive provenance with this GitHub workflow identity. + steps: + - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + name: lottiefiles-creator-${{ github.ref_name }} + path: dist + - uses: actions/attest-build-provenance@46a583fd92dfbf46b772907a9740f888f4324bb9 # v3.1.0 + with: + subject-path: dist/* + - name: Attach verified release archives + env: + GH_TOKEN: ${{ github.token }} + run: >- + gh release create "$GITHUB_REF_NAME" dist/*.tar.gz dist/*.zip dist/*.sha256 + --title "$GITHUB_REF_NAME" --generate-notes diff --git a/.gitignore b/.gitignore index bf0e989..c469bfc 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ node_modules/ dist/ coverage/ +__pycache__/ +*.py[cod] *.log .env .env.* diff --git a/content/references/tool-contract.md b/content/references/tool-contract.md new file mode 100644 index 0000000..832af77 --- /dev/null +++ b/content/references/tool-contract.md @@ -0,0 +1,22 @@ +# Pinned public tool contract + +Release evidence: `contracts/public-mcp-v1.json` at the distribution or plugin root, schema `1.0.0`, SHA-256 `1effca56b6481b2890d0359845e50e99664004e6b3bf2bcb25d1597a589037e7`. The snapshot records the reviewed release contract. Call `engine_docs` at execution to discover the live contract; live discovery is authoritative. + +Every tool requires OAuth 2.0 scope `mcp:full`. A stable tool error has `error.code`, `error.message`, and `error.retryable`. + +| Tool | Required input keys | Optional input keys | Success result fields | +| --- | --- | --- | --- | +| `engine_docs` | None | `topic` | `topic`, `bytes` | +| `engine_document` | `action` (`list`, `reset`, or `delete`) | `engineId` | List: `documents[]` with `engineId`, `revision`, `tombstone`. Reset/delete: `ok`, `action`, `engineId`, `revision`, `tombstone`, `durability`; cancelled work returns `ok: false`, `action`, `engineId`, `cancelled: true`. | +| `engine_exec` | `code` | `engineId`, `params`, `timeoutMs` | `result`, `logs`, `engineId`, `revision`, `durability`; may include `warnings`. | +| `engine_export` | None | `durationSec`, `engineId`, `format`, `fps`, `frameTimeSec`, `height`, `tiles`, `width` | `jobId` | +| `engine_job_status` | `jobId` | None | `jobId`, `engineId`, `state`, `progress`, `logs`, `error`, `artifact` | +| `engine_job_result` | `jobId` | None | Success: `jobId`, `state: "succeeded"`, `artifact` with `uri`, `filename`, `mime`, `bytes`, `width`, `height`; other states use the job status fields. | +| `engine_cancel_job` | `jobId` | None | `ok: true`, `jobId`, `state` | +| `request_upload` | `filename`, `mime`, `bytes` | None | `uploadId`, `maxBytes`, `filename`, `mime`, `bytes`, `expiresAtMs` | + +## LottieFiles GraphQL + +`lf_graphql` requires `query`. It accepts optional `operationName` and `variables`. A GraphQL response preserves `data` and `errors`; a tool failure returns the stable `error` object. When both `data` and `errors` exist, treat the data as partial and report the failed path before any follow-up action. + +The pinned annotations are `readOnlyHint: false`, `destructiveHint: true`, `openWorldHint: true`, and `idempotentHint: false`. These describe a mixed-operation tool. They do not grant scopes or bypass host confirmation. diff --git a/content/skills/animation-review/SKILL.md b/content/skills/animation-review/SKILL.md index 2b5a498..b8ca745 100644 --- a/content/skills/animation-review/SKILL.md +++ b/content/skills/animation-review/SKILL.md @@ -5,7 +5,7 @@ description: Inspect, review, compare, or diagnose an animation result with inst # Animation review and diagnosis -Use `engine_docs` to discover the installed Engine contract. Use `engine_document` to inspect, `engine_exec` to render or inspect when the contract requires it, and `engine_job_status` or `engine_job_result` for async work. Read [the contract reference](references/tool-contract.md) before a tool call. +Use `engine_docs` to discover the installed Engine contract. Use `engine_document` to inspect, `engine_exec` to render or inspect when the contract requires it, and `engine_job_status` or `engine_job_result` for async work. Read [the contract reference](../../references/tool-contract.md) before a tool call. 1. Inspect the source, result, and reported symptom before you diagnose. 2. Compare the smallest relevant artifact, frame range, or property set. diff --git a/content/skills/animation-review/references/tool-contract.md b/content/skills/animation-review/references/tool-contract.md deleted file mode 100644 index 8f0bd4d..0000000 --- a/content/skills/animation-review/references/tool-contract.md +++ /dev/null @@ -1,10 +0,0 @@ -# Engine review tool contract reconciliation - -Reconcile exact argument and result shapes with the released public Engine contract fixture before publication. - -| Review need | Released tool name | Evidence to report | -| --- | --- | --- | -| Discover contract | `engine_docs` | Current capability and argument requirements | -| Inspect document | `engine_document` | Document state and relevant properties | -| Run review action | `engine_exec` | Contract-defined artifact or diagnostic result | -| Inspect async job | `engine_job_status` / `engine_job_result` | Job state, error code, and output location | diff --git a/content/skills/animation-workflows/SKILL.md b/content/skills/animation-workflows/SKILL.md index 0d9ca1e..150589b 100644 --- a/content/skills/animation-workflows/SKILL.md +++ b/content/skills/animation-workflows/SKILL.md @@ -5,7 +5,7 @@ description: Create, edit, save, export, or manage an animation document with in # Animation workflows -Use `engine_docs` to discover the installed Engine contract. Use `engine_document` to inspect a document, `engine_exec` to make a document change, `engine_export` to export, and `engine_job_status` or `engine_job_result` to inspect async work. Read [the contract reference](references/tool-contract.md) before a tool call. +Use `engine_docs` to discover the installed Engine contract. Use `engine_document` to inspect a document, `engine_exec` to make a document change, `engine_export` to export, and `engine_job_status` or `engine_job_result` to inspect async work. Read [the contract reference](../../references/tool-contract.md) before a tool call. 1. Ask for the target document, change, output format, and destination when they are not clear. 2. Inspect the document before you change it. Report the current state that affects the request. diff --git a/content/skills/animation-workflows/references/tool-contract.md b/content/skills/animation-workflows/references/tool-contract.md deleted file mode 100644 index cfdf022..0000000 --- a/content/skills/animation-workflows/references/tool-contract.md +++ /dev/null @@ -1,16 +0,0 @@ -# Engine tool contract reconciliation - -The released public tool names are listed below. Reconcile exact argument and result shapes with the published contract fixture before publication. - -Required mapping: - -| Operation | Released tool name | Required arguments | Result fields | -| --- | --- | --- | --- | -| Discover contract | `engine_docs` | Reconcile with fixture | Reconcile with fixture | -| Inspect document | `engine_document` | Reconcile with fixture | Reconcile with fixture | -| Create or edit document | `engine_exec` | Reconcile with fixture | Reconcile with fixture | -| Upload input | `request_upload` | Reconcile with fixture | Reconcile with fixture | -| Export document | `engine_export` | Reconcile with fixture | Reconcile with fixture | -| Inspect job state | `engine_job_status` | Reconcile with fixture | Reconcile with fixture | -| Get job result | `engine_job_result` | Reconcile with fixture | Reconcile with fixture | -| Cancel job | `engine_cancel_job` | Reconcile with fixture | Reconcile with fixture | diff --git a/content/skills/lottiefiles-library/SKILL.md b/content/skills/lottiefiles-library/SKILL.md index 0b23915..886ce37 100644 --- a/content/skills/lottiefiles-library/SKILL.md +++ b/content/skills/lottiefiles-library/SKILL.md @@ -5,7 +5,7 @@ description: Search or inspect LottieFiles library or account data with lf_graph # LottieFiles library and account workflows -Full LottieFiles GraphQL access is intentional. It supports the capabilities that the connected account has. It does not grant extra scopes. +Full LottieFiles GraphQL access is intentional. It supports the capabilities that the connected account has. It does not grant extra scopes. Read [the contract reference](../../references/tool-contract.md) before a tool call. 1. Discover the needed schema and capability first. Use only fields that the current public schema confirms. 2. Read before a change. Inspect the target resource and its ownership or current state. diff --git a/content/skills/lottiefiles-library/references/tool-contract.md b/content/skills/lottiefiles-library/references/tool-contract.md deleted file mode 100644 index dae96cf..0000000 --- a/content/skills/lottiefiles-library/references/tool-contract.md +++ /dev/null @@ -1,13 +0,0 @@ -# GraphQL contract reference - -Use `lf_graphql` for LottieFiles GraphQL operations. The public schema fixture is required before publication of schema-specific examples or field names. - -Safe request shape: - -- Use one operation with an `operationName`. -- Put user and resource values in `variables`. -- Request only required fields. -- Preserve `data` when GraphQL returns both `data` and `errors`. -- Treat `errors` as an incomplete result until the response identifies the failed path. - -Schema placeholder notation in this repository uses ``, ``, and ``. It is not an API claim. diff --git a/contract/engine-mcp-tools.json b/contract/engine-mcp-tools.json new file mode 100644 index 0000000..4f6363f --- /dev/null +++ b/contract/engine-mcp-tools.json @@ -0,0 +1,1157 @@ +{ + "authorization": { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + }, + "protocolVersions": [ + "2026-07-28", + "2025-11-25", + "2025-06-18", + "2025-03-26" + ], + "schemaVersion": "1.0.0", + "server": { + "name": "creator-mcp", + "version": "0.1.0" + }, + "tools": [ + { + "_meta": { + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ] + }, + "annotations": { + "destructiveHint": true, + "idempotentHint": true, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Cancel render job" + }, + "description": "Cancel a queued or running render job (idempotent).", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "jobId": { + "type": "string" + } + }, + "required": [ + "jobId" + ], + "type": "object" + }, + "name": "engine_cancel_job", + "outputSchema": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "jobId": { + "type": "string" + }, + "ok": { + "const": true + }, + "state": { + "type": "string" + } + }, + "required": [ + "ok", + "jobId", + "state" + ], + "type": "object" + }, + { + "additionalProperties": true, + "properties": { + "error": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + } + }, + "required": [ + "code", + "message", + "retryable" + ], + "type": "object" + } + }, + "required": [ + "error" + ] + } + ], + "type": "object" + }, + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ], + "title": "Cancel render job" + }, + { + "_meta": { + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ] + }, + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false, + "readOnlyHint": true, + "title": "Engine writing docs" + }, + "description": "Engine writing docs. Call **at most once** per task. For any animated/non-trivial scene use topic \"patterns\" once, then write **one** engine_exec that names a rig, keyframes Position on that group, and returns renderFilmstrip — do not fetch a second topic (guide, api, overview) unless that exec returned an error. Short topics: overview (default) | shapes | animation | paints | render. Full files: guide, api. Never pass topic \"all\".\n", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "topic": { + "default": null, + "description": "patterns | guide | api | overview (default) | shapes | animation | paints | render.", + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, + "name": "engine_docs", + "outputSchema": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "bytes": { + "minimum": 0, + "type": "integer" + }, + "topic": { + "type": "string" + } + }, + "required": [ + "topic", + "bytes" + ], + "type": "object" + }, + { + "additionalProperties": true, + "properties": { + "error": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + } + }, + "required": [ + "code", + "message", + "retryable" + ], + "type": "object" + } + }, + "required": [ + "error" + ] + } + ], + "type": "object" + }, + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ], + "title": "Engine writing docs" + }, + { + "_meta": { + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ] + }, + "annotations": { + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Manage Engine documents" + }, + "description": "List, reset, or delete durable Engine documents. Reset and delete require elicitation confirmation.", + "inputSchema": { + "$defs": { + "DocumentAction": { + "enum": [ + "list", + "reset", + "delete" + ], + "type": "string" + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "action": { + "$ref": "#/$defs/DocumentAction" + }, + "engineId": { + "default": null, + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "action" + ], + "type": "object" + }, + "name": "engine_document", + "outputSchema": { + "anyOf": [ + { + "anyOf": [ + { + "properties": { + "documents": { + "items": { + "additionalProperties": false, + "properties": { + "engineId": { + "type": "string" + }, + "revision": { + "type": "string" + }, + "tombstone": { + "type": "boolean" + } + }, + "required": [ + "engineId", + "revision", + "tombstone" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "documents" + ] + }, + { + "properties": { + "action": { + "enum": [ + "reset", + "delete" + ] + }, + "durability": { + "type": "string" + }, + "engineId": { + "type": "string" + }, + "ok": { + "const": true + }, + "revision": { + "type": "string" + }, + "tombstone": { + "type": "boolean" + } + }, + "required": [ + "ok", + "action", + "engineId", + "revision", + "tombstone", + "durability" + ] + }, + { + "properties": { + "action": { + "enum": [ + "reset", + "delete" + ] + }, + "cancelled": { + "const": true + }, + "engineId": { + "type": "string" + }, + "ok": { + "const": false + } + }, + "required": [ + "ok", + "action", + "engineId", + "cancelled" + ] + } + ], + "type": "object" + }, + { + "additionalProperties": true, + "properties": { + "error": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + } + }, + "required": [ + "code", + "message", + "retryable" + ], + "type": "object" + } + }, + "required": [ + "error" + ] + } + ], + "type": "object" + }, + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ], + "title": "Manage Engine documents" + }, + { + "_meta": { + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ] + }, + "annotations": { + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Run Engine JavaScript" + }, + "description": "Run JavaScript against a persistent Engine instance (code mode).\nNative Engine host API (entity ids are bigint). In-scope globals: engine, PropertyType, params, ctx, dump, files. Do not import.\nNot DOM, Lottie, Canvas, or After Effects: do not call createComposition, engine.root, engine.composition, addRect, createRect, or setEntityName.\nNever reset or delete with engine_document unless the user asks to start over or wipe the scene.\nSignatures (positional; insert x,y is the shape center; fill colors {red,green,blue,alpha} floats 0–1):\n engine.createComp(width, height, fps, startSec, endSec) -> bigint\n engine.insertRect/insertEllipse(comp, x, y, w, h, fill) -> bigint\n engine.createGroup(comp, x, y); engine.setParent(child, group); engine.setName(node, name)\n const tr = engine.insertTrack(node, PropertyType.Position, t); const kf = engine.addKeyFrame(node, PropertyType.Position, tr, t); engine.setKeyFrameVec2(kf, {x,y})\n engine.setBackgroundColor(r,g,b,a) // 0–255 ints, not a color object\n engine.describe() -> [{id, parent, kind, name, data, bbox}] for the focused comp; engine.getNodeData(node) / engine.setNodeData(node, text) is a free-text description of what a node represents\nEditing an existing document: call engine.describe() first, pick the node whose name/data/bbox matches what the user means, then mutate that node in place (setStaticVec2, insertTrack, setStaticSrgbColor on its paint, ...). Never createComp or rebuild the scene for an edit.\nAnimated scene: name the rig, keyframe PropertyType.Position on that named group, return engine.renderFilmstrip({tiles:8}) in the SAME engine_exec. Prefer one complete script. Do not start with engine.methods(). For a non-trivial scene call engine_docs topic \"patterns\" once, then write.\nExample (adapt numbers/names; this is not the task):\nconst c = engine.createComp(256, 256, 30, 0, 2);\nconst rig = engine.createGroup(c, 0, 0);\nengine.setName(rig, \"sample\");\nconst n = engine.insertRect(c, 128, 128, 80, 80, { red: 0.9, green: 0.2, blue: 0.2, alpha: 1 });\nengine.setParent(n, rig);\nconst tr = engine.insertTrack(rig, PropertyType.Position, 0);\nengine.setKeyFrameVec2(engine.addKeyFrame(rig, PropertyType.Position, tr, 0), { x: 0, y: 40 });\nengine.setKeyFrameVec2(engine.addKeyFrame(rig, PropertyType.Position, tr, 2), { x: 0, y: -40 });\nreturn engine.renderFilmstrip({tiles:8});", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "code": { + "description": "JavaScript source. Globals engine, PropertyType, params, ctx, dump, files are already in scope.\nUse createComp, insertRect/insertEllipse (center x,y), createGroup/setParent, insertTrack+addKeyFrame+setKeyFrameVec2.\nColors {red,green,blue,alpha} 0–1. setBackgroundColor(r,g,b,a) is 0–255 ints. Preview with return engine.renderFilmstrip({tiles:8}).", + "type": "string" + }, + "engineId": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "params": { + "default": null + }, + "timeoutMs": { + "default": null, + "description": "Wall-clock budget in milliseconds (default 5000, max 30000).", + "format": "uint64", + "minimum": 0, + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "code" + ], + "type": "object" + }, + "name": "engine_exec", + "outputSchema": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "durability": { + "type": "string" + }, + "engineId": { + "type": "string" + }, + "logs": { + "items": { + "type": "string" + }, + "type": "array" + }, + "result": {}, + "revision": { + "type": "string" + }, + "warnings": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "result", + "logs", + "engineId", + "revision", + "durability" + ], + "type": "object" + }, + { + "additionalProperties": true, + "properties": { + "error": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + } + }, + "required": [ + "code", + "message", + "retryable" + ], + "type": "object" + } + }, + "required": [ + "error" + ] + } + ], + "type": "object" + }, + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ], + "title": "Run Engine JavaScript" + }, + { + "_meta": { + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ] + }, + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Export Engine" + }, + "description": "Async png/filmstrip/mp4/webm export of the current Engine. Prefer return engine.renderFilmstrip({tiles:8}) inside engine_exec for previews. 2026 task clients get a task handle and poll tasks/get; others get { jobId } and poll engine_job_status / engine_job_result.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "durationSec": { + "default": null, + "format": "float", + "type": [ + "number", + "null" + ] + }, + "engineId": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "format": { + "default": null, + "description": "png (default), filmstrip, mp4, or webm.", + "type": [ + "string", + "null" + ] + }, + "fps": { + "default": null, + "format": "float", + "type": [ + "number", + "null" + ] + }, + "frameTimeSec": { + "default": null, + "format": "float", + "type": [ + "number", + "null" + ] + }, + "height": { + "default": null, + "format": "uint32", + "minimum": 0, + "type": [ + "integer", + "null" + ] + }, + "tiles": { + "default": null, + "format": "uint32", + "minimum": 0, + "type": [ + "integer", + "null" + ] + }, + "width": { + "default": null, + "format": "uint32", + "minimum": 0, + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" + }, + "name": "engine_export", + "outputSchema": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "jobId": { + "type": "string" + } + }, + "required": [ + "jobId" + ], + "type": "object" + }, + { + "additionalProperties": true, + "properties": { + "error": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + } + }, + "required": [ + "code", + "message", + "retryable" + ], + "type": "object" + } + }, + "required": [ + "error" + ] + } + ], + "type": "object" + }, + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ], + "title": "Export Engine" + }, + { + "_meta": { + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ] + }, + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false, + "readOnlyHint": true, + "title": "Get render job result" + }, + "description": "Fetch a finished render job. Stills may include an inline PNG; video is a resource_link.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "jobId": { + "type": "string" + } + }, + "required": [ + "jobId" + ], + "type": "object" + }, + "name": "engine_job_result", + "outputSchema": { + "anyOf": [ + { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "artifact": { + "additionalProperties": false, + "properties": { + "bytes": { + "minimum": 0, + "type": "integer" + }, + "filename": { + "type": "string" + }, + "height": {}, + "mime": { + "type": "string" + }, + "uri": { + "type": "string" + }, + "width": {} + }, + "required": [ + "uri", + "filename", + "mime", + "bytes", + "width", + "height" + ], + "type": "object" + }, + "inlineBytes": { + "minimum": 0, + "type": "integer" + }, + "jobId": { + "type": "string" + }, + "state": { + "const": "succeeded" + } + }, + "required": [ + "jobId", + "state", + "artifact" + ] + }, + { + "additionalProperties": false, + "properties": { + "artifact": {}, + "engineId": { + "type": "string" + }, + "error": {}, + "jobId": { + "type": "string" + }, + "logs": { + "type": "array" + }, + "progress": {}, + "state": { + "type": "string" + } + }, + "required": [ + "jobId", + "engineId", + "state", + "progress", + "logs", + "error", + "artifact" + ] + } + ], + "type": "object" + }, + { + "additionalProperties": true, + "properties": { + "error": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + } + }, + "required": [ + "code", + "message", + "retryable" + ], + "type": "object" + } + }, + "required": [ + "error" + ] + } + ], + "type": "object" + }, + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ], + "title": "Get render job result" + }, + { + "_meta": { + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ] + }, + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false, + "readOnlyHint": true, + "title": "Get render job status" + }, + "description": "Poll an async render job started by engine.renderJob or engine_export.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "jobId": { + "type": "string" + } + }, + "required": [ + "jobId" + ], + "type": "object" + }, + "name": "engine_job_status", + "outputSchema": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "artifact": {}, + "engineId": { + "type": "string" + }, + "error": {}, + "jobId": { + "type": "string" + }, + "logs": { + "type": "array" + }, + "progress": {}, + "state": { + "type": "string" + } + }, + "required": [ + "jobId", + "engineId", + "state", + "progress", + "logs", + "error", + "artifact" + ], + "type": "object" + }, + { + "additionalProperties": true, + "properties": { + "error": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + } + }, + "required": [ + "code", + "message", + "retryable" + ], + "type": "object" + } + }, + "required": [ + "error" + ] + } + ], + "type": "object" + }, + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ], + "title": "Get render job status" + }, + { + "_meta": { + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ] + }, + "annotations": { + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true, + "readOnlyHint": false, + "title": "LottieFiles GraphQL" + }, + "description": "Run a GraphQL operation with the caller's delegated LottieFiles grant. This raw adapter returns GraphQL JSON or URLs. Load bytes into document files or assets before engine.load.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "operationName": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "query": { + "type": "string" + }, + "variables": { + "default": null + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "name": "lf_graphql", + "outputSchema": { + "anyOf": [ + { + "additionalProperties": true, + "description": "The upstream GraphQL response object. Data and errors are preserved.", + "properties": { + "data": {}, + "errors": { + "type": "array" + } + }, + "type": "object" + }, + { + "additionalProperties": true, + "properties": { + "error": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + } + }, + "required": [ + "code", + "message", + "retryable" + ], + "type": "object" + } + }, + "required": [ + "error" + ] + } + ], + "type": "object" + }, + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ], + "title": "LottieFiles GraphQL" + }, + { + "_meta": { + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ] + }, + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Request file upload" + }, + "description": "Reserve one subject-bound upload. Then PUT the exact bytes and Content-Type to /uploads/{uploadId}/{filename}. Use files.adopt(uploadId, path) to add the immutable asset to a document.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "bytes": { + "format": "uint64", + "minimum": 0, + "type": "integer" + }, + "filename": { + "type": "string" + }, + "mime": { + "type": "string" + } + }, + "required": [ + "filename", + "mime", + "bytes" + ], + "type": "object" + }, + "name": "request_upload", + "outputSchema": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "bytes": { + "minimum": 0, + "type": "integer" + }, + "expiresAtMs": { + "minimum": 0, + "type": "integer" + }, + "filename": { + "type": "string" + }, + "maxBytes": { + "minimum": 0, + "type": "integer" + }, + "mime": { + "type": "string" + }, + "uploadId": { + "type": "string" + } + }, + "required": [ + "uploadId", + "maxBytes", + "filename", + "mime", + "bytes", + "expiresAtMs" + ], + "type": "object" + }, + { + "additionalProperties": true, + "properties": { + "error": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + } + }, + "required": [ + "code", + "message", + "retryable" + ], + "type": "object" + } + }, + "required": [ + "error" + ] + } + ], + "type": "object" + }, + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ], + "title": "Request file upload" + } + ], + "transport": { + "path": "/mcp", + "productionResourceUri": "https://creator-mcp.lottiefiles.com/mcp", + "type": "streamable-http" + } +} diff --git a/contract/engine-mcp-tools.sha256 b/contract/engine-mcp-tools.sha256 new file mode 100644 index 0000000..9a519f3 --- /dev/null +++ b/contract/engine-mcp-tools.sha256 @@ -0,0 +1 @@ +1effca56b6481b2890d0359845e50e99664004e6b3bf2bcb25d1597a589037e7 contracts/public-mcp-v1.json diff --git a/docs/release.md b/docs/release.md index e679c0c..a9f8854 100644 --- a/docs/release.md +++ b/docs/release.md @@ -11,13 +11,13 @@ Use semantic versioning. 1. Generate platform copies from canonical `content/skills` and `content/assets`. 2. Validate generated output, metadata, links, and public-contract synchronization. 3. Run the release test suite and the submission test cases in each supported host. -4. Record the tag, commit, package checksum, public endpoint, and contract fixture hash. +4. Record the tag, commit, package checksum, public endpoint, OAuth result, and pinned contract hash. 5. Publish only after the checklist human and administrator gates pass. 6. Publish the directory entry only after the package is available from its public source. ## Contract synchronization -Treat the released public tool and GraphQL contracts as versioned inputs. Before each release: +Treat the pinned public tool contract and public GraphQL schema as versioned inputs. The checked-in tool snapshot is release evidence. At execution, `engine_docs` remains authoritative. Before each release: 1. Compare the contract fixture with skill references and examples. 2. Replace only verified placeholders. diff --git a/examples/README.md b/examples/README.md index 3505997..e3560d9 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,6 +1,6 @@ # Examples -These examples use synthetic identifiers and placeholder GraphQL schema names. Replace a placeholder only after the public contract or schema confirms it. +These examples use synthetic identifiers and placeholder GraphQL schema names. Replace a placeholder only after the public GraphQL schema confirms it. | Example | Purpose | Writes data | | --- | --- | --- | diff --git a/examples/engine-async-job.md b/examples/engine-async-job.md index c43388e..418aa9d 100644 --- a/examples/engine-async-job.md +++ b/examples/engine-async-job.md @@ -1,10 +1,10 @@ # Async job handling -Use `engine_job_status` with the returned job identifier, for example `job_demo_001`. Check at bounded intervals. Do not submit the export again while the job is pending. +Use `engine_job_status` with its required `jobId`, for example `job_demo_001`. Check at bounded intervals. Do not submit the export again while the job is pending. 1. If the job is pending or running, report progress and check again. 2. If the job succeeds, call `engine_job_result` and report the output location or artifact identifier. 3. If the job fails, report its stable error code and safe remediation. Do not expose private payloads. 4. If the user asks to stop it, explain the effect and then call `engine_cancel_job` after host confirmation. -Use `engine_docs` to confirm job states and arguments before calling a tool. +Use `engine_docs` to confirm the live job behavior before calling a tool. The pinned contract remains release evidence. diff --git a/examples/engine-create-export.md b/examples/engine-create-export.md index 944dcb1..26b0587 100644 --- a/examples/engine-create-export.md +++ b/examples/engine-create-export.md @@ -1,6 +1,6 @@ # Create and export an animation -Use `engine_docs` first. It defines the valid arguments for `engine_exec`, `engine_document`, and `engine_export`. +Use `engine_docs` first. The pinned contract requires `code` for `engine_exec`; `engine_document` requires `action`; and `engine_export` returns `jobId` on success. 1. Ask for the document purpose, output format, and destination. 2. Call `engine_exec` with the published create action and minimum document content. @@ -9,4 +9,4 @@ Use `engine_docs` first. It defines the valid arguments for `engine_exec`, `engi 5. Call `engine_export` with the published export arguments. 6. If it returns a job identifier, follow [the async job example](engine-async-job.md). -Synthetic identifiers, such as `document_demo_001`, show flow only. The published fixture must supply argument names and result fields. +Synthetic identifiers, such as `document_demo_001`, show flow only. See the pinned [tool contract](../content/references/tool-contract.md) for released input and result fields. diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..759e7a8 --- /dev/null +++ b/mise.toml @@ -0,0 +1,5 @@ +[tools] +"aqua:koalaman/shellcheck" = "0.11.0" +"aqua:lycheeverse/lychee" = "0.22.0" +"aqua:rhysd/actionlint" = "1.7.10" +"aqua:zizmorcore/zizmor" = "1.25.2" diff --git a/release-include.txt b/release-include.txt new file mode 100644 index 0000000..f1ef151 --- /dev/null +++ b/release-include.txt @@ -0,0 +1,18 @@ +# Public files included in each deterministic distribution archive. +README.md +LICENSE +CHANGELOG.md +SECURITY.md +SUPPORT.md +catalog +contract +content +docs +examples +skills +plugins +.agents +.grok-plugin +.mcp.json +gemini-extension.json +server.json diff --git a/scripts/check-content.py b/scripts/check-content.py new file mode 100644 index 0000000..81ff802 --- /dev/null +++ b/scripts/check-content.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +"""Check Markdown and Agent Skills frontmatter without a YAML dependency.""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + +from check_common import CheckError, ROOT, require_text + + +FRONTMATTER = re.compile(r"\A---\r?\n(.*?)\r?\n---\r?\n", re.DOTALL) +FIELD = re.compile(r"^([A-Za-z][A-Za-z0-9_-]*):\s*(.+?)\s*$") + + +def skill_files() -> list[Path]: + return ( + sorted(ROOT.glob("content/skills/**/SKILL.md")) + + sorted(ROOT.glob("skills/**/SKILL.md")) + + sorted(ROOT.glob("plugins/**/skills/**/SKILL.md")) + ) + + +def parse_frontmatter(path: Path) -> dict[str, str]: + source = path.read_text(encoding="utf-8") + match = FRONTMATTER.match(source) + if not match: + raise CheckError(f"skill has no YAML frontmatter: {path.relative_to(ROOT)}") + fields: dict[str, str] = {} + for line in match.group(1).splitlines(): + if not line.strip() or line.lstrip().startswith("#"): + continue + field = FIELD.match(line) + if not field: + raise CheckError( + f"skill frontmatter must use simple key/value fields: {path.relative_to(ROOT)}" + ) + key, value = field.groups() + if key in fields: + raise CheckError(f"duplicate skill frontmatter key {key}: {path.relative_to(ROOT)}") + fields[key] = value.strip().strip("\"'") + require_text(fields.get("name"), f"{path.relative_to(ROOT)} frontmatter.name") + require_text(fields.get("description"), f"{path.relative_to(ROOT)} frontmatter.description") + if not source[match.end() :].strip(): + raise CheckError(f"skill body is empty: {path.relative_to(ROOT)}") + return fields + + +def main() -> None: + markdown = sorted(ROOT.rglob("*.md")) + if not markdown: + raise CheckError("no Markdown files found") + for path in markdown: + if ".git" in path.parts: + continue + if path.name == "SKILL.md": + parse_frontmatter(path) + elif not path.read_text(encoding="utf-8").strip(): + raise CheckError(f"empty Markdown file: {path.relative_to(ROOT)}") + print("Markdown and skill frontmatter checks passed.") + + +if __name__ == "__main__": + try: + main() + except (CheckError, UnicodeDecodeError) as error: + print(f"Content check failed: {error}", file=sys.stderr) + raise SystemExit(1) from error diff --git a/scripts/check-contract.py b/scripts/check-contract.py new file mode 100644 index 0000000..67eb091 --- /dev/null +++ b/scripts/check-contract.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +"""Validate the public Engine MCP tool snapshot used by skills and examples.""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path +from typing import Any + +from check_common import ( + CheckError, + ROOT, + assert_public_text, + load_json, + require_array, + require_object, + require_text, + sha256, +) + + +CONTRACT = ROOT / "contract" / "engine-mcp-tools.json" +DIGEST = ROOT / "contract" / "engine-mcp-tools.sha256" +TOOL_TOKEN = re.compile(r"\b(?:engine_[a-z0-9_]+|request_upload|lf_graphql)\b") +DIGEST_LINE = re.compile( + r"^([a-f0-9]{64})(?:\s+(?:\*?engine-mcp-tools\.json|contracts/public-mcp-v1\.json))?\s*$" +) +EXPECTED_TOOLS = { + "engine_cancel_job", + "engine_docs", + "engine_document", + "engine_exec", + "engine_export", + "engine_job_result", + "engine_job_status", + "lf_graphql", + "request_upload", +} +OAUTH_SECURITY = [{"type": "oauth2", "scopes": ["mcp:full"]}] + + +def adapt_tools(payload: Any) -> list[Any]: + """Accept the two stable tools/list JSON envelopes and reject all others.""" + root = require_object(payload, "contract root") + if "tools" in root: + return require_array(root["tools"], "contract.tools") + result = root.get("result") + if isinstance(result, dict) and "tools" in result: + return require_array(result["tools"], "contract.result.tools") + raise CheckError( + "unsupported contract envelope: expected tools or result.tools; update adapt_tools once the public snapshot format is pinned" + ) + + +def check_digest() -> None: + if not CONTRACT.is_file(): + raise CheckError( + "missing contract/engine-mcp-tools.json; add the reviewed public tools/list snapshot before running distribution checks" + ) + if not DIGEST.is_file(): + raise CheckError( + "missing contract/engine-mcp-tools.sha256; add the SHA-256 digest for the public tools/list snapshot" + ) + lines = [line for line in DIGEST.read_text(encoding="utf-8").splitlines() if line.strip()] + if len(lines) != 1: + raise CheckError("contract digest must contain exactly one SHA-256 line") + match = DIGEST_LINE.fullmatch(lines[0]) + if not match: + raise CheckError("contract digest must be a lowercase SHA-256 for the reviewed public snapshot") + actual = sha256(CONTRACT) + if match.group(1) != actual: + raise CheckError(f"contract digest mismatch: expected {match.group(1)}, got {actual}") + + +def check_schema(tool: Any, index: int) -> str: + label = f"tool[{index}]" + value = require_object(tool, label) + name = require_text(value.get("name"), f"{label}.name") + require_text(value.get("description"), f"{label}.description") + input_schema = require_object(value.get("inputSchema"), f"{label}.inputSchema") + output_schema = require_object(value.get("outputSchema"), f"{label}.outputSchema") + if input_schema.get("$schema") != "https://json-schema.org/draft/2020-12/schema": + raise CheckError(f"{label}.inputSchema must use JSON Schema draft 2020-12") + if output_schema.get("type") != "object": + raise CheckError(f"{label}.outputSchema.type must be object") + annotations = require_object(value.get("annotations"), f"{label}.annotations") + require_text(annotations.get("title"), f"{label}.annotations.title") + for key in ("readOnlyHint", "destructiveHint", "openWorldHint", "idempotentHint"): + if not isinstance(annotations.get(key), bool): + raise CheckError(f"{label}.annotations.{key} must be a boolean") + if annotations["readOnlyHint"] and annotations["destructiveHint"]: + raise CheckError(f"{label} cannot be both read-only and destructive") + security = value.get("securitySchemes") + if security != OAUTH_SECURITY: + raise CheckError(f"{label}.securitySchemes must require mcp:full OAuth2") + meta = require_object(value.get("_meta"), f"{label}._meta") + if meta.get("securitySchemes") != OAUTH_SECURITY: + raise CheckError(f"{label}._meta.securitySchemes must mirror securitySchemes") + return name + + +def check_references(tool_names: set[str]) -> None: + files = ( + sorted(ROOT.glob("content/skills/**/*.md")) + + sorted(ROOT.glob("skills/**/*.md")) + + sorted(ROOT.glob("examples/**/*.md")) + ) + if not files: + raise CheckError("no skill or example files found for tool-reference validation") + for path in files: + for name in TOOL_TOKEN.findall(path.read_text(encoding="utf-8")): + if name not in tool_names: + raise CheckError(f"unknown tool reference {name} in {path.relative_to(ROOT)}") + + +def main() -> None: + check_digest() + assert_public_text(CONTRACT) + payload = load_json(CONTRACT) + root = require_object(payload, "contract root") + if root.get("schemaVersion") != "1.0.0": + raise CheckError("contract schemaVersion must be 1.0.0") + authorization = require_object(root.get("authorization"), "contract.authorization") + if authorization != {"type": "oauth2", "scopes": ["mcp:full"]}: + raise CheckError("contract.authorization must require mcp:full OAuth2") + server = require_object(root.get("server"), "contract.server") + if server.get("name") != "creator-mcp": + raise CheckError("contract.server.name must be creator-mcp") + require_text(server.get("version"), "contract.server.version") + tools = adapt_tools(payload) + if not tools: + raise CheckError("contract must contain at least one tool") + names = [check_schema(tool, index) for index, tool in enumerate(tools)] + if len(names) != len(set(names)): + raise CheckError("contract tool names must be unique") + if set(names) != EXPECTED_TOOLS: + missing = sorted(EXPECTED_TOOLS - set(names)) + unknown = sorted(set(names) - EXPECTED_TOOLS) + raise CheckError(f"contract tool set differs: missing={missing} unknown={unknown}") + graphql = next((tool for tool in tools if isinstance(tool, dict) and tool.get("name") == "lf_graphql"), None) + if graphql is None: + raise CheckError("contract must include lf_graphql") + annotations = require_object(graphql.get("annotations"), "lf_graphql.annotations") + if ( + annotations.get("readOnlyHint") is not False + or annotations.get("destructiveHint") is not True + or annotations.get("openWorldHint") is not True + ): + raise CheckError("lf_graphql annotations must be explicit: readOnlyHint=false, destructiveHint=true, openWorldHint=true") + check_references(set(names)) + print(f"Contract check passed for {len(names)} tool(s).") + + +if __name__ == "__main__": + try: + main() + except CheckError as error: + print(f"Contract check failed: {error}", file=sys.stderr) + raise SystemExit(1) from error diff --git a/scripts/check-json.py b/scripts/check-json.py new file mode 100644 index 0000000..d4fd034 --- /dev/null +++ b/scripts/check-json.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +"""Parse every public JSON file and reject duplicate JSON object keys.""" + +from __future__ import annotations + +import sys + +from check_common import CheckError, ROOT, load_json + + +EXCLUDED_PARTS = {".git", "dist", "__pycache__"} + + +def main() -> None: + files = sorted( + path + for path in ROOT.rglob("*.json") + if not EXCLUDED_PARTS.intersection(path.relative_to(ROOT).parts) + ) + if not files: + raise CheckError("no JSON files found") + for path in files: + load_json(path) + print(f"JSON check passed for {len(files)} file(s).") + + +if __name__ == "__main__": + try: + main() + except CheckError as error: + print(f"JSON check failed: {error}", file=sys.stderr) + raise SystemExit(1) from error diff --git a/scripts/check-mcp-registry.sh b/scripts/check-mcp-registry.sh new file mode 100644 index 0000000..52edca4 --- /dev/null +++ b/scripts/check-mcp-registry.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Validate server.json with the pinned official MCP Registry publisher CLI. +set -euo pipefail + +publisher_version="1.8.1" +system_name="$(uname -s)" +machine_name="$(uname -m)" +case "$system_name/$machine_name" in + Linux/x86_64) + publisher_asset="mcp-publisher_linux_amd64.tar.gz" + publisher_sha256="a06c9096dcb9727c13555b6be26c7effa707b01f06a4c561ba7a3635443cf2cc" + ;; + Darwin/arm64) + publisher_asset="mcp-publisher_darwin_arm64.tar.gz" + publisher_sha256="e45e520892460732a4bdf37255576415d4a53ec171f8b913faf15bb1aef7cb77" + ;; + *) + echo "Unsupported mcp-publisher platform: $system_name/$machine_name" >&2 + exit 1 + ;; +esac + +temporary_dir="$(mktemp -d)" +trap 'rm -rf "$temporary_dir"' EXIT +archive_path="$temporary_dir/$publisher_asset" +archive_url="https://github.com/modelcontextprotocol/registry/releases/download/v${publisher_version}/${publisher_asset}" + +curl --fail --location --proto '=https' --tlsv1.2 --retry 3 --output "$archive_path" "$archive_url" +if command -v sha256sum >/dev/null 2>&1; then + printf '%s %s\n' "$publisher_sha256" "$archive_path" | sha256sum -c - +else + printf '%s %s\n' "$publisher_sha256" "$archive_path" | shasum -a 256 -c - +fi +tar -xzf "$archive_path" -C "$temporary_dir" mcp-publisher +"$temporary_dir/mcp-publisher" validate server.json diff --git a/scripts/check-package.py b/scripts/check-package.py new file mode 100644 index 0000000..c40ffad --- /dev/null +++ b/scripts/check-package.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Prove release archives are reproducible and safe to unpack.""" + +from __future__ import annotations + +import shutil +import sys +import tempfile +import tarfile +import zipfile +from pathlib import Path, PurePosixPath + +from check_common import CheckError, ROOT, load_json, require_text, sha256 +from package import archive_path, included_files, package + + +def safe_member(name: str) -> None: + value = PurePosixPath(name) + if value.is_absolute() or ".." in value.parts or not value.parts: + raise CheckError(f"unsafe archive member: {name}") + + +def smoke_tar(path: Path, expected: set[str]) -> None: + with tarfile.open(path, "r:gz") as archive: + members = archive.getmembers() + if not members: + raise CheckError(f"empty tar archive: {path.name}") + names = {member.name for member in members} + if names != expected: + raise CheckError("tar archive members differ from release-include.txt") + for member in members: + safe_member(member.name) + if not member.isfile() or member.issym() or member.islnk(): + raise CheckError(f"tar archive has a non-regular member: {member.name}") + + +def smoke_zip(path: Path, expected: set[str]) -> None: + with zipfile.ZipFile(path) as archive: + members = archive.infolist() + if not members: + raise CheckError(f"empty ZIP archive: {path.name}") + names = {member.filename for member in members} + if names != expected: + raise CheckError("ZIP archive members differ from release-include.txt") + for member in members: + safe_member(member.filename) + if member.is_dir(): + raise CheckError(f"ZIP archive member is invalid: {member.filename}") + + +def main() -> None: + catalog = load_json(ROOT / "catalog" / "plugin.json") + if not isinstance(catalog, dict): + raise CheckError("catalog/plugin.json must be a JSON object") + version = require_text(catalog.get("version"), "catalog.version") + expected = {archive_path(version, path.relative_to(ROOT)) for path in included_files()} + with tempfile.TemporaryDirectory(prefix="creator-agent-plugin-") as temporary: + first = Path(temporary) / "first" + second = Path(temporary) / "second" + one = package(first, version) + two = package(second, version) + for before, after in zip(one, two, strict=True): + if sha256(before) != sha256(after): + raise CheckError(f"archive is not reproducible: {before.name}") + smoke_tar(one[0], expected) + smoke_zip(one[1], expected) + print("Reproducible archive smoke test passed.") + + +if __name__ == "__main__": + try: + main() + except CheckError as error: + print(f"Package smoke test failed: {error}", file=sys.stderr) + raise SystemExit(1) from error diff --git a/scripts/check-paths.py b/scripts/check-paths.py new file mode 100644 index 0000000..aa6f903 --- /dev/null +++ b/scripts/check-paths.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +"""Reject symlinks and unsafe archive paths from distributable content.""" + +from __future__ import annotations + +import sys +import subprocess +from pathlib import Path + +from check_common import CheckError, ROOT + + +INCLUDE_FILE = ROOT / "release-include.txt" + + +def included_roots() -> list[Path]: + if not INCLUDE_FILE.is_file(): + raise CheckError("missing release archive configuration: release-include.txt") + roots: list[Path] = [] + for line in INCLUDE_FILE.read_text(encoding="utf-8").splitlines(): + item = line.strip() + if not item or item.startswith("#"): + continue + candidate = Path(item) + if candidate.is_absolute() or ".." in candidate.parts: + raise CheckError(f"unsafe release include path: {item}") + path = ROOT / candidate + if not path.exists() and not path.is_symlink(): + raise CheckError(f"release include path does not exist: {item}") + roots.append(path) + if not roots: + raise CheckError("release archive configuration has no paths") + return roots + + +def main() -> None: + tracked = subprocess.run( + ["git", "ls-files", "-z"], cwd=ROOT, check=True, capture_output=True + ).stdout.decode("utf-8").split("\0") + for name in tracked: + parts = Path(name).parts + if "__pycache__" in parts or name.endswith((".pyc", ".pyo", ".pyd")): + raise CheckError(f"tracked cache or temporary artifact is not allowed: {name}") + checked = 0 + for root in included_roots(): + for path in [root, *root.rglob("*")] if root.is_dir() else [root]: + relative = path.relative_to(ROOT) + if path.is_symlink(): + raise CheckError(f"symlinks are not allowed in release content: {relative}") + if ".." in relative.parts or relative.is_absolute(): + raise CheckError(f"release path escapes its archive: {relative}") + checked += 1 + print(f"Release path check passed for {checked} path(s).") + + +if __name__ == "__main__": + try: + main() + except CheckError as error: + print(f"Path check failed: {error}", file=sys.stderr) + raise SystemExit(1) from error diff --git a/scripts/check-platforms.py b/scripts/check-platforms.py new file mode 100644 index 0000000..e7b18d9 --- /dev/null +++ b/scripts/check-platforms.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +"""Check the generated shapes required by each supported public platform.""" + +from __future__ import annotations + +import sys +from pathlib import Path +from typing import Any + +from check_common import ( + CheckError, + ROOT, + load_json, + require_array, + require_object, + require_relative_path, + require_text, +) + + +PLUGIN = ROOT / "plugins" / "lottiefiles-creator" + + +def member(value: Any, key: str, label: str) -> Any: + return require_object(value, label).get(key) + + +def mcp_http(payload: dict[str, Any], name: str, label: str, *, gemini: bool = False) -> None: + server = require_object(member(payload, "mcpServers", label), f"{label}.mcpServers").get(name) + server = require_object(server, f"{label}.mcpServers.{name}") + url_key = "httpUrl" if gemini else "url" + url = require_text(server.get(url_key), f"{label}.mcpServers.{name}.{url_key}") + if not url.startswith("https://"): + raise CheckError(f"{label}.mcpServers.{name}.{url_key} must use HTTPS") + if not gemini and server.get("type") != "http": + raise CheckError(f"{label}.mcpServers.{name}.type must be http") + + +def local_path(value: Any, label: str) -> None: + path = require_relative_path(value, label) + if not path.is_dir(): + raise CheckError(f"{label} must resolve to a directory") + + +def marketplace_entry(payload: dict[str, Any], label: str) -> dict[str, Any]: + entries = require_array(payload.get("plugins"), f"{label}.plugins") + if len(entries) != 1: + raise CheckError(f"{label}.plugins must contain exactly one plugin") + return require_object(entries[0], f"{label}.plugins[0]") + + +def main() -> None: + catalog = require_object(load_json(ROOT / "catalog" / "plugin.json"), "catalog/plugin.json") + plugin_id = require_text(catalog.get("id"), "catalog.id") + marketplace_id = require_text(catalog.get("marketplaceId"), "catalog.marketplaceId") + server_name = require_text(catalog.get("mcpServerName"), "catalog.mcpServerName") + version = require_text(catalog.get("version"), "catalog.version") + + # Codex marketplace and plugin manifest. + codex_market = require_object(load_json(ROOT / ".agents/plugins/marketplace.json"), "Codex marketplace") + if codex_market.get("name") != marketplace_id: + raise CheckError("Codex marketplace name differs from catalog.marketplaceId") + codex_entry = marketplace_entry(codex_market, "Codex marketplace") + if codex_entry.get("name") != plugin_id: + raise CheckError("Codex marketplace plugin name differs from catalog.id") + source = require_object(codex_entry.get("source"), "Codex marketplace plugin source") + if source.get("source") != "local": + raise CheckError("Codex marketplace plugin source must be local") + local_path(source.get("path"), "Codex marketplace plugin source.path") + policy = require_object(codex_entry.get("policy"), "Codex marketplace plugin policy") + if policy.get("installation") != "AVAILABLE" or policy.get("authentication") != "ON_INSTALL": + raise CheckError("Codex marketplace plugin policy must request install authentication") + codex = require_object(load_json(PLUGIN / ".codex-plugin/plugin.json"), "Codex plugin manifest") + if codex.get("name") != plugin_id or codex.get("version") != version: + raise CheckError("Codex plugin manifest name or version differs from catalog") + local_path((PLUGIN.relative_to(ROOT) / str(codex.get("skills", ""))).as_posix(), "Codex plugin skills") + mcp_reference = require_relative_path( + (PLUGIN.relative_to(ROOT) / str(codex.get("mcpServers", ""))).as_posix(), + "Codex plugin mcpServers", + ) + if not mcp_reference.is_file(): + raise CheckError("Codex plugin mcpServers must resolve to a file") + + # Claude marketplace and plugin manifest. This manifest is also the ZCode + # Claude fallback; it must retain a local MCP configuration. + claude_market = require_object(load_json(ROOT / ".claude-plugin/marketplace.json"), "Claude marketplace") + if claude_market.get("name") != marketplace_id or claude_market.get("version") != version: + raise CheckError("Claude marketplace name or version differs from catalog") + claude_entry = marketplace_entry(claude_market, "Claude marketplace") + if claude_entry.get("name") != plugin_id or claude_entry.get("version") != version: + raise CheckError("Claude marketplace plugin name or version differs from catalog") + local_path(claude_entry.get("source"), "Claude marketplace plugin source") + claude = require_object(load_json(PLUGIN / ".claude-plugin/plugin.json"), "Claude plugin manifest") + if claude.get("name") != plugin_id or claude.get("version") != version: + raise CheckError("Claude plugin manifest name or version differs from catalog") + fallback = require_relative_path( + (PLUGIN.relative_to(ROOT) / str(claude.get("mcpServers", ""))).as_posix(), + "ZCode Claude fallback mcpServers", + ) + if not fallback.is_file(): + raise CheckError("ZCode Claude fallback mcpServers must resolve to a file") + + # Grok marketplace. + grok = require_object(load_json(ROOT / ".grok-plugin/marketplace.json"), "Grok marketplace") + if grok.get("name") != marketplace_id: + raise CheckError("Grok marketplace name differs from catalog.marketplaceId") + grok_entry = marketplace_entry(grok, "Grok marketplace") + if grok_entry.get("name") != plugin_id or grok_entry.get("version") != version: + raise CheckError("Grok marketplace plugin name or version differs from catalog") + grok_source = require_object(grok_entry.get("source"), "Grok marketplace plugin source") + if grok_source.get("type") != "local": + raise CheckError("Grok marketplace plugin source.type must be local") + local_path(grok_source.get("path"), "Grok marketplace plugin source.path") + + # Gemini, standard MCP, and the official MCP Registry server.json. + gemini = require_object(load_json(ROOT / "gemini-extension.json"), "Gemini root manifest") + if gemini.get("name") != plugin_id or gemini.get("version") != version: + raise CheckError("Gemini root manifest name or version differs from catalog") + mcp_http(gemini, server_name, "Gemini root manifest", gemini=True) + root_mcp = require_object(load_json(ROOT / ".mcp.json"), "root MCP manifest") + plugin_mcp = require_object(load_json(PLUGIN / ".mcp.json"), "plugin MCP manifest") + mcp_http(root_mcp, server_name, "root MCP manifest") + mcp_http(plugin_mcp, server_name, "plugin MCP manifest") + registry = require_object(load_json(ROOT / "server.json"), "MCP Registry server.json") + if registry.get("version") != version: + raise CheckError("MCP Registry server.json version differs from catalog") + remotes = require_array(registry.get("remotes"), "MCP Registry server.json remotes") + if len(remotes) != 1: + raise CheckError("MCP Registry server.json must define exactly one remote") + remote = require_object(remotes[0], "MCP Registry server.json remotes[0]") + if remote.get("type") != "streamable-http": + raise CheckError("MCP Registry server.json remote must use streamable-http") + if not require_text(remote.get("url"), "MCP Registry server.json remote URL").startswith("https://"): + raise CheckError("MCP Registry server.json remote URL must use HTTPS") + + print("Platform conformance checks passed.") + + +if __name__ == "__main__": + try: + main() + except CheckError as error: + print(f"Platform conformance failed: {error}", file=sys.stderr) + raise SystemExit(1) from error diff --git a/scripts/check-provenance.py b/scripts/check-provenance.py new file mode 100644 index 0000000..796b900 --- /dev/null +++ b/scripts/check-provenance.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +"""Check license and ownership claims for this metadata-only public package.""" + +from __future__ import annotations + +import sys + +from check_common import CheckError, ROOT, load_json, require_object, require_text + + +def main() -> None: + for name in ("LICENSE", "CHANGELOG.md", "SECURITY.md", "SUPPORT.md"): + path = ROOT / name + if not path.is_file() or not path.read_text(encoding="utf-8").strip(): + raise CheckError(f"missing {name}; add required public legal or support guidance before release") + catalog = require_object(load_json(ROOT / "catalog/plugin.json"), "catalog/plugin.json") + publisher = require_object(catalog.get("publisher"), "catalog.publisher") + publisher_name = require_text(publisher.get("name"), "catalog.publisher.name") + for path in ( + ROOT / "plugins/lottiefiles-creator/.codex-plugin/plugin.json", + ROOT / "plugins/lottiefiles-creator/.claude-plugin/plugin.json", + ROOT / ".claude-plugin/marketplace.json", + ROOT / ".grok-plugin/marketplace.json", + ): + manifest = require_object(load_json(path), path.relative_to(ROOT).as_posix()) + owner = manifest.get("author", manifest.get("owner")) + owner = require_object(owner, f"{path.relative_to(ROOT)} owner") + if require_text(owner.get("name"), f"{path.relative_to(ROOT)} owner.name") != publisher_name: + raise CheckError(f"publisher ownership drift in {path.relative_to(ROOT)}") + print("License and publisher provenance checks passed.") + + +if __name__ == "__main__": + try: + main() + except CheckError as error: + print(f"Provenance check failed: {error}", file=sys.stderr) + raise SystemExit(1) from error diff --git a/scripts/check-public.py b/scripts/check-public.py new file mode 100644 index 0000000..01102b1 --- /dev/null +++ b/scripts/check-public.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +"""Reject private paths and secret-shaped text in public distribution inputs.""" + +from __future__ import annotations + +import sys +from pathlib import Path + +from check_common import CheckError, ROOT, assert_public_text + + +EXCLUDED = {".git", "dist", "__pycache__"} + + +def main() -> None: + checked = 0 + for path in sorted(ROOT.rglob("*")): + if not path.is_file() or EXCLUDED.intersection(path.relative_to(ROOT).parts): + continue + assert_public_text(path) + checked += 1 + print(f"Public-reference check passed for {checked} file(s).") + + +if __name__ == "__main__": + try: + main() + except CheckError as error: + print(f"Public-reference check failed: {error}", file=sys.stderr) + raise SystemExit(1) from error diff --git a/scripts/check-version.py b/scripts/check-version.py new file mode 100644 index 0000000..d50f3da --- /dev/null +++ b/scripts/check-version.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +"""Verify every generated manifest version equals the release tag version.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path +from typing import Any + +from check_common import CheckError, ROOT, load_json + + +MANIFESTS = ( + ROOT / "catalog/plugin.json", + ROOT / "gemini-extension.json", + ROOT / "server.json", + ROOT / ".claude-plugin/marketplace.json", + ROOT / ".grok-plugin/marketplace.json", + ROOT / "plugins/lottiefiles-creator/.codex-plugin/plugin.json", + ROOT / "plugins/lottiefiles-creator/.claude-plugin/plugin.json", +) + + +def versions(value: Any, location: str) -> list[str]: + found: list[str] = [] + if isinstance(value, dict): + for key, child in value.items(): + if key == "version": + if not isinstance(child, str): + raise CheckError(f"{location}.version must be a string") + found.append(child) + found.extend(versions(child, f"{location}.{key}")) + elif isinstance(value, list): + for index, child in enumerate(value): + found.extend(versions(child, f"{location}[{index}]")) + return found + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("tag", help="release tag in vMAJOR.MINOR.PATCH form") + args = parser.parse_args() + if not args.tag.startswith("v") or len(args.tag) == 1: + raise CheckError("release tag must start with v") + expected = args.tag[1:] + for path in MANIFESTS: + payload = load_json(path) + found = versions(payload, path.relative_to(ROOT).as_posix()) + if not found: + raise CheckError(f"manifest has no version fields: {path.relative_to(ROOT)}") + if any(version != expected for version in found): + raise CheckError(f"manifest version differs from release tag: {path.relative_to(ROOT)}") + print(f"All manifest versions equal {expected}.") + + +if __name__ == "__main__": + try: + main() + except CheckError as error: + print(f"Version check failed: {error}", file=sys.stderr) + raise SystemExit(1) from error diff --git a/scripts/check.sh b/scripts/check.sh new file mode 100644 index 0000000..a7a3f99 --- /dev/null +++ b/scripts/check.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Run deterministic public distribution checks. No network or secrets are required. +set -euo pipefail + +script_dir="$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)" +repository_dir="$(CDPATH='' cd -- "$script_dir/.." && pwd)" +cd "$repository_dir" + +python3 scripts/generate.py --check +python3 scripts/validate.py +python3 scripts/check-json.py +python3 scripts/check-content.py +python3 scripts/check-public.py +python3 scripts/check-provenance.py +python3 scripts/check-contract.py +python3 scripts/check-paths.py +python3 scripts/check-platforms.py +python3 scripts/check-package.py + +for tool in actionlint zizmor shellcheck gitleaks; do + if command -v "$tool" >/dev/null 2>&1; then + case "$tool" in + actionlint) actionlint .github/workflows/*.yml ;; + zizmor) zizmor .github ;; + shellcheck) shellcheck scripts/check.sh ;; + gitleaks) gitleaks detect --source . --no-banner --redact ;; + esac + else + echo "Optional local tool not installed: $tool" >&2 + fi +done diff --git a/scripts/check_common.py b/scripts/check_common.py new file mode 100644 index 0000000..d289733 --- /dev/null +++ b/scripts/check_common.py @@ -0,0 +1,101 @@ +"""Small, dependency-free helpers for distribution checks.""" + +from __future__ import annotations + +import hashlib +import json +import re +from pathlib import Path +from typing import Any + + +ROOT = Path(__file__).resolve().parents[1] + + +class CheckError(Exception): + """An input does not meet a public distribution contract.""" + + +def fail(message: str) -> None: + raise CheckError(message) + + +def _reject_duplicate_keys(pairs: list[tuple[str, Any]]) -> dict[str, Any]: + result: dict[str, Any] = {} + for key, value in pairs: + if key in result: + fail(f"duplicate JSON object key: {key}") + result[key] = value + return result + + +def load_json(path: Path) -> Any: + try: + with path.open(encoding="utf-8") as source: + return json.load(source, object_pairs_hook=_reject_duplicate_keys) + except FileNotFoundError: + fail(f"missing required file: {path.relative_to(ROOT)}") + except json.JSONDecodeError as error: + fail(f"invalid JSON in {path.relative_to(ROOT)}: {error}") + + +def sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as source: + for block in iter(lambda: source.read(64 * 1024), b""): + digest.update(block) + return digest.hexdigest() + + +def require_object(value: Any, label: str) -> dict[str, Any]: + if not isinstance(value, dict): + fail(f"{label} must be a JSON object") + return value + + +def require_array(value: Any, label: str) -> list[Any]: + if not isinstance(value, list): + fail(f"{label} must be a JSON array") + return value + + +def require_text(value: Any, label: str) -> str: + if not isinstance(value, str) or not value.strip(): + fail(f"{label} must be a non-empty string") + return value + + +def require_relative_path(value: Any, label: str, *, base: Path = ROOT) -> Path: + raw = require_text(value, label) + candidate = Path(raw) + if candidate.is_absolute() or ".." in candidate.parts: + fail(f"{label} must stay inside the repository: {raw}") + resolved = (base / candidate).resolve() + try: + resolved.relative_to(ROOT.resolve()) + except ValueError: + fail(f"{label} escapes the repository: {raw}") + return resolved + + +PRIVATE_PATTERNS = ( + re.compile(r"(?:github\.com|gitlab\.com)[:/]LottieFiles/(?:engine|thorvg-rs)(?:[/.#]|$)", re.I), + re.compile(r"(?:^|[/:])(?:Users|home)/(?:jaw|runner)/(?:Projects|\.codex)(?:/|$)", re.I), +) + +SECRET_PATTERNS = ( + re.compile(r"\b(?:ghp|github_pat)_[A-Za-z0-9_]{20,}\b"), + re.compile(r"\bsk-[A-Za-z0-9]{20,}\b"), + re.compile(r"\bAKIA[0-9A-Z]{16}\b"), + re.compile(r"-----BEGIN (?:[A-Z ]+ )?PRIVATE KEY-----"), +) + + +def assert_public_text(path: Path) -> None: + try: + text = path.read_text(encoding="utf-8") + except UnicodeDecodeError: + return + for pattern in (*PRIVATE_PATTERNS, *SECRET_PATTERNS): + if pattern.search(text): + fail(f"private reference or secret-like value in {path.relative_to(ROOT)}") diff --git a/scripts/package.py b/scripts/package.py new file mode 100644 index 0000000..4da5359 --- /dev/null +++ b/scripts/package.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 +"""Create deterministic source distribution archives from release-include.txt.""" + +from __future__ import annotations + +import argparse +import gzip +import io +import shutil +import sys +import tarfile +import tempfile +import zipfile +from pathlib import Path, PurePosixPath + +from check_common import CheckError, ROOT, assert_public_text, load_json, require_text, sha256 + + +INCLUDE = ROOT / "release-include.txt" +EPOCH = 315532800 # 1980-01-01T00:00:00Z; valid for both tar and ZIP. + + +def included_files() -> list[Path]: + if not INCLUDE.is_file(): + raise CheckError("missing release archive configuration: release-include.txt") + files: set[Path] = set() + for raw in INCLUDE.read_text(encoding="utf-8").splitlines(): + value = raw.strip() + if not value or value.startswith("#"): + continue + relative = PurePosixPath(value) + if relative.is_absolute() or ".." in relative.parts: + raise CheckError(f"unsafe release include path: {value}") + source = ROOT / relative + if source.is_symlink() or not source.exists(): + raise CheckError(f"release include path is missing or a symlink: {value}") + candidates = source.rglob("*") if source.is_dir() else [source] + for path in candidates: + if path.is_symlink(): + raise CheckError(f"symlinks are not allowed in release content: {path.relative_to(ROOT)}") + if path.is_file(): + assert_public_text(path) + files.add(path) + if not files: + raise CheckError("release archive has no files") + return sorted(files, key=lambda item: item.relative_to(ROOT).as_posix()) + + +def archive_path(version: str, relative: Path) -> str: + path = PurePosixPath("lottiefiles-creator") / version / PurePosixPath(relative.as_posix()) + if path.is_absolute() or ".." in path.parts: + raise CheckError(f"unsafe archive member: {path}") + return path.as_posix() + + +def build_tar(path: Path, version: str, files: list[Path]) -> None: + with path.open("wb") as target, gzip.GzipFile(filename="", mode="wb", fileobj=target, mtime=EPOCH) as zipped: + with tarfile.open(fileobj=zipped, mode="w") as archive: + for source in files: + data = source.read_bytes() + member = tarfile.TarInfo(archive_path(version, source.relative_to(ROOT))) + member.size = len(data) + member.mode = 0o644 + member.mtime = EPOCH + member.uid = 0 + member.gid = 0 + member.uname = "" + member.gname = "" + archive.addfile(member, io.BytesIO(data)) + + +def build_zip(path: Path, version: str, files: list[Path]) -> None: + with zipfile.ZipFile(path, mode="w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive: + for source in files: + member = zipfile.ZipInfo(archive_path(version, source.relative_to(ROOT)), date_time=(1980, 1, 1, 0, 0, 0)) + member.compress_type = zipfile.ZIP_DEFLATED + member.external_attr = 0o100644 << 16 + archive.writestr(member, source.read_bytes(), compress_type=zipfile.ZIP_DEFLATED, compresslevel=9) + + +def package(output: Path, version: str) -> tuple[Path, Path]: + output.mkdir(parents=True, exist_ok=True) + files = included_files() + stem = f"lottiefiles-creator-{version}" + tar_path = output / f"{stem}.tar.gz" + zip_path = output / f"{stem}.zip" + build_tar(tar_path, version, files) + build_zip(zip_path, version, files) + return tar_path, zip_path + + +def write_checksums(paths: tuple[Path, Path]) -> Path: + checksum_path = paths[0].with_suffix("").with_suffix(".sha256") + lines = [f"{sha256(path)} {path.name}" for path in paths] + checksum_path.write_text("\n".join(lines) + "\n", encoding="utf-8") + return checksum_path + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--output", type=Path, required=True, help="directory for release archives") + return parser.parse_args() + + +def main() -> None: + args = parse_args() + catalog = load_json(ROOT / "catalog" / "plugin.json") + if not isinstance(catalog, dict): + raise CheckError("catalog/plugin.json must be a JSON object") + version = require_text(catalog.get("version"), "catalog.version") + tar_path, zip_path = package(args.output, version) + checksum_path = write_checksums((tar_path, zip_path)) + print(f"created {tar_path} ({sha256(tar_path)})") + print(f"created {zip_path} ({sha256(zip_path)})") + print(f"created {checksum_path}") + + +if __name__ == "__main__": + try: + main() + except CheckError as error: + print(f"Package failed: {error}", file=sys.stderr) + raise SystemExit(1) from error diff --git a/submission/checklist.md b/submission/checklist.md index 299927b..ba053ba 100644 --- a/submission/checklist.md +++ b/submission/checklist.md @@ -4,7 +4,7 @@ - [ ] Run the content generator and confirm generated copies match `content/skills` and `content/assets`. - [ ] Validate manifests, metadata, and version against the release tag. -- [ ] Replace contract reconciliation notes with the released public fixture path and hash. +- [ ] Verify `contracts/public-mcp-v1.json` against its checked-in SHA-256 and schema `1.0.0`. - [ ] Replace schema placeholders only with fields confirmed by a public schema. - [ ] Confirm every public URL resolves from an external network. - [ ] Verify the package contains no secrets, private paths, user data, debug payloads, or proprietary schema. diff --git a/submission/release-notes.md b/submission/release-notes.md index e5b18f0..37ba729 100644 --- a/submission/release-notes.md +++ b/submission/release-notes.md @@ -5,6 +5,7 @@ Initial public distribution for LottieFiles Creator. - Adds three task skills for animation work, LottieFiles GraphQL access, and animation review. - Adds safe examples for read, pagination, explicit mutation, export, and async jobs. - Adds installation, privacy, support, release, rollback, and security guidance. -- Uses schema placeholders until the released public GraphQL schema and Engine contract fixture confirm exact shapes. +- Pins public Engine contract schema `1.0.0` with SHA-256 `1effca56b6481b2890d0359845e50e99664004e6b3bf2bcb25d1597a589037e7` as release evidence. +- Uses schema placeholders until a public GraphQL schema confirms exact GraphQL field names. -Publication remains blocked until the public endpoint, contract fixture, brand asset provenance, and required human approval gates are complete. +Publication remains blocked until production endpoint and OAuth checks, live host tests, brand and legal approval, and required human and administrator gates are complete. diff --git a/submission/test-cases.md b/submission/test-cases.md index 1011f27..88ae264 100644 --- a/submission/test-cases.md +++ b/submission/test-cases.md @@ -1,6 +1,6 @@ # Test cases -Use synthetic fixtures only. A host reviewer must replace tool arguments with the released public fixture before execution. +Use synthetic fixtures only. A host reviewer must use the pinned public tool contract and live tool discovery before execution. | ID | Type | Prompt | Expected tools | Expected behavior | Expected result shape | Fixture | | --- | --- | --- | --- | --- | --- | --- | From 1568a1e7d77ef1e3ef29fd980e05c6579cb3a783 Mon Sep 17 00:00:00 2001 From: Jawish Hameed Date: Sun, 6 Sep 2026 00:15:34 +0800 Subject: [PATCH 08/12] fix: align public contract artifacts --- contracts/public-mcp-v1.json | 1157 ++++++++++++++++++++++++++++++++ contracts/public-mcp-v1.sha256 | 1 + release-include.txt | 2 +- scripts/check-contract.py | 28 +- 4 files changed, 1181 insertions(+), 7 deletions(-) create mode 100644 contracts/public-mcp-v1.json create mode 100644 contracts/public-mcp-v1.sha256 diff --git a/contracts/public-mcp-v1.json b/contracts/public-mcp-v1.json new file mode 100644 index 0000000..4f6363f --- /dev/null +++ b/contracts/public-mcp-v1.json @@ -0,0 +1,1157 @@ +{ + "authorization": { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + }, + "protocolVersions": [ + "2026-07-28", + "2025-11-25", + "2025-06-18", + "2025-03-26" + ], + "schemaVersion": "1.0.0", + "server": { + "name": "creator-mcp", + "version": "0.1.0" + }, + "tools": [ + { + "_meta": { + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ] + }, + "annotations": { + "destructiveHint": true, + "idempotentHint": true, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Cancel render job" + }, + "description": "Cancel a queued or running render job (idempotent).", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "jobId": { + "type": "string" + } + }, + "required": [ + "jobId" + ], + "type": "object" + }, + "name": "engine_cancel_job", + "outputSchema": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "jobId": { + "type": "string" + }, + "ok": { + "const": true + }, + "state": { + "type": "string" + } + }, + "required": [ + "ok", + "jobId", + "state" + ], + "type": "object" + }, + { + "additionalProperties": true, + "properties": { + "error": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + } + }, + "required": [ + "code", + "message", + "retryable" + ], + "type": "object" + } + }, + "required": [ + "error" + ] + } + ], + "type": "object" + }, + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ], + "title": "Cancel render job" + }, + { + "_meta": { + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ] + }, + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false, + "readOnlyHint": true, + "title": "Engine writing docs" + }, + "description": "Engine writing docs. Call **at most once** per task. For any animated/non-trivial scene use topic \"patterns\" once, then write **one** engine_exec that names a rig, keyframes Position on that group, and returns renderFilmstrip — do not fetch a second topic (guide, api, overview) unless that exec returned an error. Short topics: overview (default) | shapes | animation | paints | render. Full files: guide, api. Never pass topic \"all\".\n", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "topic": { + "default": null, + "description": "patterns | guide | api | overview (default) | shapes | animation | paints | render.", + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, + "name": "engine_docs", + "outputSchema": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "bytes": { + "minimum": 0, + "type": "integer" + }, + "topic": { + "type": "string" + } + }, + "required": [ + "topic", + "bytes" + ], + "type": "object" + }, + { + "additionalProperties": true, + "properties": { + "error": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + } + }, + "required": [ + "code", + "message", + "retryable" + ], + "type": "object" + } + }, + "required": [ + "error" + ] + } + ], + "type": "object" + }, + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ], + "title": "Engine writing docs" + }, + { + "_meta": { + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ] + }, + "annotations": { + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Manage Engine documents" + }, + "description": "List, reset, or delete durable Engine documents. Reset and delete require elicitation confirmation.", + "inputSchema": { + "$defs": { + "DocumentAction": { + "enum": [ + "list", + "reset", + "delete" + ], + "type": "string" + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "action": { + "$ref": "#/$defs/DocumentAction" + }, + "engineId": { + "default": null, + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "action" + ], + "type": "object" + }, + "name": "engine_document", + "outputSchema": { + "anyOf": [ + { + "anyOf": [ + { + "properties": { + "documents": { + "items": { + "additionalProperties": false, + "properties": { + "engineId": { + "type": "string" + }, + "revision": { + "type": "string" + }, + "tombstone": { + "type": "boolean" + } + }, + "required": [ + "engineId", + "revision", + "tombstone" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "documents" + ] + }, + { + "properties": { + "action": { + "enum": [ + "reset", + "delete" + ] + }, + "durability": { + "type": "string" + }, + "engineId": { + "type": "string" + }, + "ok": { + "const": true + }, + "revision": { + "type": "string" + }, + "tombstone": { + "type": "boolean" + } + }, + "required": [ + "ok", + "action", + "engineId", + "revision", + "tombstone", + "durability" + ] + }, + { + "properties": { + "action": { + "enum": [ + "reset", + "delete" + ] + }, + "cancelled": { + "const": true + }, + "engineId": { + "type": "string" + }, + "ok": { + "const": false + } + }, + "required": [ + "ok", + "action", + "engineId", + "cancelled" + ] + } + ], + "type": "object" + }, + { + "additionalProperties": true, + "properties": { + "error": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + } + }, + "required": [ + "code", + "message", + "retryable" + ], + "type": "object" + } + }, + "required": [ + "error" + ] + } + ], + "type": "object" + }, + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ], + "title": "Manage Engine documents" + }, + { + "_meta": { + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ] + }, + "annotations": { + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Run Engine JavaScript" + }, + "description": "Run JavaScript against a persistent Engine instance (code mode).\nNative Engine host API (entity ids are bigint). In-scope globals: engine, PropertyType, params, ctx, dump, files. Do not import.\nNot DOM, Lottie, Canvas, or After Effects: do not call createComposition, engine.root, engine.composition, addRect, createRect, or setEntityName.\nNever reset or delete with engine_document unless the user asks to start over or wipe the scene.\nSignatures (positional; insert x,y is the shape center; fill colors {red,green,blue,alpha} floats 0–1):\n engine.createComp(width, height, fps, startSec, endSec) -> bigint\n engine.insertRect/insertEllipse(comp, x, y, w, h, fill) -> bigint\n engine.createGroup(comp, x, y); engine.setParent(child, group); engine.setName(node, name)\n const tr = engine.insertTrack(node, PropertyType.Position, t); const kf = engine.addKeyFrame(node, PropertyType.Position, tr, t); engine.setKeyFrameVec2(kf, {x,y})\n engine.setBackgroundColor(r,g,b,a) // 0–255 ints, not a color object\n engine.describe() -> [{id, parent, kind, name, data, bbox}] for the focused comp; engine.getNodeData(node) / engine.setNodeData(node, text) is a free-text description of what a node represents\nEditing an existing document: call engine.describe() first, pick the node whose name/data/bbox matches what the user means, then mutate that node in place (setStaticVec2, insertTrack, setStaticSrgbColor on its paint, ...). Never createComp or rebuild the scene for an edit.\nAnimated scene: name the rig, keyframe PropertyType.Position on that named group, return engine.renderFilmstrip({tiles:8}) in the SAME engine_exec. Prefer one complete script. Do not start with engine.methods(). For a non-trivial scene call engine_docs topic \"patterns\" once, then write.\nExample (adapt numbers/names; this is not the task):\nconst c = engine.createComp(256, 256, 30, 0, 2);\nconst rig = engine.createGroup(c, 0, 0);\nengine.setName(rig, \"sample\");\nconst n = engine.insertRect(c, 128, 128, 80, 80, { red: 0.9, green: 0.2, blue: 0.2, alpha: 1 });\nengine.setParent(n, rig);\nconst tr = engine.insertTrack(rig, PropertyType.Position, 0);\nengine.setKeyFrameVec2(engine.addKeyFrame(rig, PropertyType.Position, tr, 0), { x: 0, y: 40 });\nengine.setKeyFrameVec2(engine.addKeyFrame(rig, PropertyType.Position, tr, 2), { x: 0, y: -40 });\nreturn engine.renderFilmstrip({tiles:8});", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "code": { + "description": "JavaScript source. Globals engine, PropertyType, params, ctx, dump, files are already in scope.\nUse createComp, insertRect/insertEllipse (center x,y), createGroup/setParent, insertTrack+addKeyFrame+setKeyFrameVec2.\nColors {red,green,blue,alpha} 0–1. setBackgroundColor(r,g,b,a) is 0–255 ints. Preview with return engine.renderFilmstrip({tiles:8}).", + "type": "string" + }, + "engineId": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "params": { + "default": null + }, + "timeoutMs": { + "default": null, + "description": "Wall-clock budget in milliseconds (default 5000, max 30000).", + "format": "uint64", + "minimum": 0, + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "code" + ], + "type": "object" + }, + "name": "engine_exec", + "outputSchema": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "durability": { + "type": "string" + }, + "engineId": { + "type": "string" + }, + "logs": { + "items": { + "type": "string" + }, + "type": "array" + }, + "result": {}, + "revision": { + "type": "string" + }, + "warnings": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "result", + "logs", + "engineId", + "revision", + "durability" + ], + "type": "object" + }, + { + "additionalProperties": true, + "properties": { + "error": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + } + }, + "required": [ + "code", + "message", + "retryable" + ], + "type": "object" + } + }, + "required": [ + "error" + ] + } + ], + "type": "object" + }, + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ], + "title": "Run Engine JavaScript" + }, + { + "_meta": { + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ] + }, + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Export Engine" + }, + "description": "Async png/filmstrip/mp4/webm export of the current Engine. Prefer return engine.renderFilmstrip({tiles:8}) inside engine_exec for previews. 2026 task clients get a task handle and poll tasks/get; others get { jobId } and poll engine_job_status / engine_job_result.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "durationSec": { + "default": null, + "format": "float", + "type": [ + "number", + "null" + ] + }, + "engineId": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "format": { + "default": null, + "description": "png (default), filmstrip, mp4, or webm.", + "type": [ + "string", + "null" + ] + }, + "fps": { + "default": null, + "format": "float", + "type": [ + "number", + "null" + ] + }, + "frameTimeSec": { + "default": null, + "format": "float", + "type": [ + "number", + "null" + ] + }, + "height": { + "default": null, + "format": "uint32", + "minimum": 0, + "type": [ + "integer", + "null" + ] + }, + "tiles": { + "default": null, + "format": "uint32", + "minimum": 0, + "type": [ + "integer", + "null" + ] + }, + "width": { + "default": null, + "format": "uint32", + "minimum": 0, + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" + }, + "name": "engine_export", + "outputSchema": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "jobId": { + "type": "string" + } + }, + "required": [ + "jobId" + ], + "type": "object" + }, + { + "additionalProperties": true, + "properties": { + "error": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + } + }, + "required": [ + "code", + "message", + "retryable" + ], + "type": "object" + } + }, + "required": [ + "error" + ] + } + ], + "type": "object" + }, + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ], + "title": "Export Engine" + }, + { + "_meta": { + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ] + }, + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false, + "readOnlyHint": true, + "title": "Get render job result" + }, + "description": "Fetch a finished render job. Stills may include an inline PNG; video is a resource_link.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "jobId": { + "type": "string" + } + }, + "required": [ + "jobId" + ], + "type": "object" + }, + "name": "engine_job_result", + "outputSchema": { + "anyOf": [ + { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "artifact": { + "additionalProperties": false, + "properties": { + "bytes": { + "minimum": 0, + "type": "integer" + }, + "filename": { + "type": "string" + }, + "height": {}, + "mime": { + "type": "string" + }, + "uri": { + "type": "string" + }, + "width": {} + }, + "required": [ + "uri", + "filename", + "mime", + "bytes", + "width", + "height" + ], + "type": "object" + }, + "inlineBytes": { + "minimum": 0, + "type": "integer" + }, + "jobId": { + "type": "string" + }, + "state": { + "const": "succeeded" + } + }, + "required": [ + "jobId", + "state", + "artifact" + ] + }, + { + "additionalProperties": false, + "properties": { + "artifact": {}, + "engineId": { + "type": "string" + }, + "error": {}, + "jobId": { + "type": "string" + }, + "logs": { + "type": "array" + }, + "progress": {}, + "state": { + "type": "string" + } + }, + "required": [ + "jobId", + "engineId", + "state", + "progress", + "logs", + "error", + "artifact" + ] + } + ], + "type": "object" + }, + { + "additionalProperties": true, + "properties": { + "error": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + } + }, + "required": [ + "code", + "message", + "retryable" + ], + "type": "object" + } + }, + "required": [ + "error" + ] + } + ], + "type": "object" + }, + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ], + "title": "Get render job result" + }, + { + "_meta": { + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ] + }, + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false, + "readOnlyHint": true, + "title": "Get render job status" + }, + "description": "Poll an async render job started by engine.renderJob or engine_export.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "jobId": { + "type": "string" + } + }, + "required": [ + "jobId" + ], + "type": "object" + }, + "name": "engine_job_status", + "outputSchema": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "artifact": {}, + "engineId": { + "type": "string" + }, + "error": {}, + "jobId": { + "type": "string" + }, + "logs": { + "type": "array" + }, + "progress": {}, + "state": { + "type": "string" + } + }, + "required": [ + "jobId", + "engineId", + "state", + "progress", + "logs", + "error", + "artifact" + ], + "type": "object" + }, + { + "additionalProperties": true, + "properties": { + "error": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + } + }, + "required": [ + "code", + "message", + "retryable" + ], + "type": "object" + } + }, + "required": [ + "error" + ] + } + ], + "type": "object" + }, + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ], + "title": "Get render job status" + }, + { + "_meta": { + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ] + }, + "annotations": { + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true, + "readOnlyHint": false, + "title": "LottieFiles GraphQL" + }, + "description": "Run a GraphQL operation with the caller's delegated LottieFiles grant. This raw adapter returns GraphQL JSON or URLs. Load bytes into document files or assets before engine.load.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "operationName": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "query": { + "type": "string" + }, + "variables": { + "default": null + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "name": "lf_graphql", + "outputSchema": { + "anyOf": [ + { + "additionalProperties": true, + "description": "The upstream GraphQL response object. Data and errors are preserved.", + "properties": { + "data": {}, + "errors": { + "type": "array" + } + }, + "type": "object" + }, + { + "additionalProperties": true, + "properties": { + "error": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + } + }, + "required": [ + "code", + "message", + "retryable" + ], + "type": "object" + } + }, + "required": [ + "error" + ] + } + ], + "type": "object" + }, + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ], + "title": "LottieFiles GraphQL" + }, + { + "_meta": { + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ] + }, + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Request file upload" + }, + "description": "Reserve one subject-bound upload. Then PUT the exact bytes and Content-Type to /uploads/{uploadId}/{filename}. Use files.adopt(uploadId, path) to add the immutable asset to a document.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "bytes": { + "format": "uint64", + "minimum": 0, + "type": "integer" + }, + "filename": { + "type": "string" + }, + "mime": { + "type": "string" + } + }, + "required": [ + "filename", + "mime", + "bytes" + ], + "type": "object" + }, + "name": "request_upload", + "outputSchema": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "bytes": { + "minimum": 0, + "type": "integer" + }, + "expiresAtMs": { + "minimum": 0, + "type": "integer" + }, + "filename": { + "type": "string" + }, + "maxBytes": { + "minimum": 0, + "type": "integer" + }, + "mime": { + "type": "string" + }, + "uploadId": { + "type": "string" + } + }, + "required": [ + "uploadId", + "maxBytes", + "filename", + "mime", + "bytes", + "expiresAtMs" + ], + "type": "object" + }, + { + "additionalProperties": true, + "properties": { + "error": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + } + }, + "required": [ + "code", + "message", + "retryable" + ], + "type": "object" + } + }, + "required": [ + "error" + ] + } + ], + "type": "object" + }, + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ], + "title": "Request file upload" + } + ], + "transport": { + "path": "/mcp", + "productionResourceUri": "https://creator-mcp.lottiefiles.com/mcp", + "type": "streamable-http" + } +} diff --git a/contracts/public-mcp-v1.sha256 b/contracts/public-mcp-v1.sha256 new file mode 100644 index 0000000..9a519f3 --- /dev/null +++ b/contracts/public-mcp-v1.sha256 @@ -0,0 +1 @@ +1effca56b6481b2890d0359845e50e99664004e6b3bf2bcb25d1597a589037e7 contracts/public-mcp-v1.json diff --git a/release-include.txt b/release-include.txt index f1ef151..2934fbe 100644 --- a/release-include.txt +++ b/release-include.txt @@ -5,7 +5,7 @@ CHANGELOG.md SECURITY.md SUPPORT.md catalog -contract +contracts content docs examples diff --git a/scripts/check-contract.py b/scripts/check-contract.py index 67eb091..be335b8 100644 --- a/scripts/check-contract.py +++ b/scripts/check-contract.py @@ -20,11 +20,11 @@ ) -CONTRACT = ROOT / "contract" / "engine-mcp-tools.json" -DIGEST = ROOT / "contract" / "engine-mcp-tools.sha256" +CONTRACT = ROOT / "contracts" / "public-mcp-v1.json" +DIGEST = ROOT / "contracts" / "public-mcp-v1.sha256" TOOL_TOKEN = re.compile(r"\b(?:engine_[a-z0-9_]+|request_upload|lf_graphql)\b") DIGEST_LINE = re.compile( - r"^([a-f0-9]{64})(?:\s+(?:\*?engine-mcp-tools\.json|contracts/public-mcp-v1\.json))?\s*$" + r"^([a-f0-9]{64})(?:\s+contracts/public-mcp-v1\.json)?\s*$" ) EXPECTED_TOOLS = { "engine_cancel_job", @@ -56,18 +56,18 @@ def adapt_tools(payload: Any) -> list[Any]: def check_digest() -> None: if not CONTRACT.is_file(): raise CheckError( - "missing contract/engine-mcp-tools.json; add the reviewed public tools/list snapshot before running distribution checks" + "missing contracts/public-mcp-v1.json; add the reviewed public tools/list snapshot before running distribution checks" ) if not DIGEST.is_file(): raise CheckError( - "missing contract/engine-mcp-tools.sha256; add the SHA-256 digest for the public tools/list snapshot" + "missing contracts/public-mcp-v1.sha256; add the SHA-256 digest for the public tools/list snapshot" ) lines = [line for line in DIGEST.read_text(encoding="utf-8").splitlines() if line.strip()] if len(lines) != 1: raise CheckError("contract digest must contain exactly one SHA-256 line") match = DIGEST_LINE.fullmatch(lines[0]) if not match: - raise CheckError("contract digest must be a lowercase SHA-256 for the reviewed public snapshot") + raise CheckError("contract digest must be the reviewed public snapshot SHA-256 and source label") actual = sha256(CONTRACT) if match.group(1) != actual: raise CheckError(f"contract digest mismatch: expected {match.group(1)}, got {actual}") @@ -128,6 +128,22 @@ def main() -> None: if server.get("name") != "creator-mcp": raise CheckError("contract.server.name must be creator-mcp") require_text(server.get("version"), "contract.server.version") + protocol_versions = require_array(root.get("protocolVersions"), "contract.protocolVersions") + if not protocol_versions or any(not isinstance(version, str) or not version for version in protocol_versions): + raise CheckError("contract.protocolVersions must be a non-empty string array") + if len(protocol_versions) != len(set(protocol_versions)): + raise CheckError("contract.protocolVersions must not contain duplicates") + transport = require_object(root.get("transport"), "contract.transport") + if transport.get("type") != "streamable-http" or transport.get("path") != "/mcp": + raise CheckError("contract.transport must use streamable-http at /mcp") + catalog = require_object(load_json(ROOT / "catalog" / "plugin.json"), "catalog/plugin.json") + remote = require_object(catalog.get("remote"), "catalog.remote") + production_url = require_text(remote.get("productionUrl"), "catalog.remote.productionUrl") + if transport.get("productionResourceUri") != production_url: + raise CheckError("contract.transport.productionResourceUri must equal catalog.remote.productionUrl") + for key, value in transport.items(): + if "staging" in key.lower() or (isinstance(value, str) and "staging" in value.lower()): + raise CheckError("contract.transport must not contain staging data") tools = adapt_tools(payload) if not tools: raise CheckError("contract must contain at least one tool") From 876cc78cf0ed13d230c48386555e5234cbd1b069 Mon Sep 17 00:00:00 2001 From: Jawish Hameed Date: Sun, 6 Sep 2026 00:17:39 +0800 Subject: [PATCH 09/12] docs: align export example with contract --- contract/engine-mcp-tools.json | 1157 ------------------------------ contract/engine-mcp-tools.sha256 | 1 - examples/engine-create-export.md | 4 +- scripts/check-contract.py | 11 + scripts/check-platforms.py | 6 + 5 files changed, 19 insertions(+), 1160 deletions(-) delete mode 100644 contract/engine-mcp-tools.json delete mode 100644 contract/engine-mcp-tools.sha256 diff --git a/contract/engine-mcp-tools.json b/contract/engine-mcp-tools.json deleted file mode 100644 index 4f6363f..0000000 --- a/contract/engine-mcp-tools.json +++ /dev/null @@ -1,1157 +0,0 @@ -{ - "authorization": { - "scopes": [ - "mcp:full" - ], - "type": "oauth2" - }, - "protocolVersions": [ - "2026-07-28", - "2025-11-25", - "2025-06-18", - "2025-03-26" - ], - "schemaVersion": "1.0.0", - "server": { - "name": "creator-mcp", - "version": "0.1.0" - }, - "tools": [ - { - "_meta": { - "securitySchemes": [ - { - "scopes": [ - "mcp:full" - ], - "type": "oauth2" - } - ] - }, - "annotations": { - "destructiveHint": true, - "idempotentHint": true, - "openWorldHint": false, - "readOnlyHint": false, - "title": "Cancel render job" - }, - "description": "Cancel a queued or running render job (idempotent).", - "inputSchema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "properties": { - "jobId": { - "type": "string" - } - }, - "required": [ - "jobId" - ], - "type": "object" - }, - "name": "engine_cancel_job", - "outputSchema": { - "anyOf": [ - { - "additionalProperties": false, - "properties": { - "jobId": { - "type": "string" - }, - "ok": { - "const": true - }, - "state": { - "type": "string" - } - }, - "required": [ - "ok", - "jobId", - "state" - ], - "type": "object" - }, - { - "additionalProperties": true, - "properties": { - "error": { - "additionalProperties": false, - "properties": { - "code": { - "type": "string" - }, - "message": { - "type": "string" - }, - "retryable": { - "type": "boolean" - } - }, - "required": [ - "code", - "message", - "retryable" - ], - "type": "object" - } - }, - "required": [ - "error" - ] - } - ], - "type": "object" - }, - "securitySchemes": [ - { - "scopes": [ - "mcp:full" - ], - "type": "oauth2" - } - ], - "title": "Cancel render job" - }, - { - "_meta": { - "securitySchemes": [ - { - "scopes": [ - "mcp:full" - ], - "type": "oauth2" - } - ] - }, - "annotations": { - "destructiveHint": false, - "idempotentHint": true, - "openWorldHint": false, - "readOnlyHint": true, - "title": "Engine writing docs" - }, - "description": "Engine writing docs. Call **at most once** per task. For any animated/non-trivial scene use topic \"patterns\" once, then write **one** engine_exec that names a rig, keyframes Position on that group, and returns renderFilmstrip — do not fetch a second topic (guide, api, overview) unless that exec returned an error. Short topics: overview (default) | shapes | animation | paints | render. Full files: guide, api. Never pass topic \"all\".\n", - "inputSchema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "properties": { - "topic": { - "default": null, - "description": "patterns | guide | api | overview (default) | shapes | animation | paints | render.", - "type": [ - "string", - "null" - ] - } - }, - "type": "object" - }, - "name": "engine_docs", - "outputSchema": { - "anyOf": [ - { - "additionalProperties": false, - "properties": { - "bytes": { - "minimum": 0, - "type": "integer" - }, - "topic": { - "type": "string" - } - }, - "required": [ - "topic", - "bytes" - ], - "type": "object" - }, - { - "additionalProperties": true, - "properties": { - "error": { - "additionalProperties": false, - "properties": { - "code": { - "type": "string" - }, - "message": { - "type": "string" - }, - "retryable": { - "type": "boolean" - } - }, - "required": [ - "code", - "message", - "retryable" - ], - "type": "object" - } - }, - "required": [ - "error" - ] - } - ], - "type": "object" - }, - "securitySchemes": [ - { - "scopes": [ - "mcp:full" - ], - "type": "oauth2" - } - ], - "title": "Engine writing docs" - }, - { - "_meta": { - "securitySchemes": [ - { - "scopes": [ - "mcp:full" - ], - "type": "oauth2" - } - ] - }, - "annotations": { - "destructiveHint": true, - "idempotentHint": false, - "openWorldHint": false, - "readOnlyHint": false, - "title": "Manage Engine documents" - }, - "description": "List, reset, or delete durable Engine documents. Reset and delete require elicitation confirmation.", - "inputSchema": { - "$defs": { - "DocumentAction": { - "enum": [ - "list", - "reset", - "delete" - ], - "type": "string" - } - }, - "$schema": "https://json-schema.org/draft/2020-12/schema", - "properties": { - "action": { - "$ref": "#/$defs/DocumentAction" - }, - "engineId": { - "default": null, - "type": [ - "string", - "null" - ] - } - }, - "required": [ - "action" - ], - "type": "object" - }, - "name": "engine_document", - "outputSchema": { - "anyOf": [ - { - "anyOf": [ - { - "properties": { - "documents": { - "items": { - "additionalProperties": false, - "properties": { - "engineId": { - "type": "string" - }, - "revision": { - "type": "string" - }, - "tombstone": { - "type": "boolean" - } - }, - "required": [ - "engineId", - "revision", - "tombstone" - ], - "type": "object" - }, - "type": "array" - } - }, - "required": [ - "documents" - ] - }, - { - "properties": { - "action": { - "enum": [ - "reset", - "delete" - ] - }, - "durability": { - "type": "string" - }, - "engineId": { - "type": "string" - }, - "ok": { - "const": true - }, - "revision": { - "type": "string" - }, - "tombstone": { - "type": "boolean" - } - }, - "required": [ - "ok", - "action", - "engineId", - "revision", - "tombstone", - "durability" - ] - }, - { - "properties": { - "action": { - "enum": [ - "reset", - "delete" - ] - }, - "cancelled": { - "const": true - }, - "engineId": { - "type": "string" - }, - "ok": { - "const": false - } - }, - "required": [ - "ok", - "action", - "engineId", - "cancelled" - ] - } - ], - "type": "object" - }, - { - "additionalProperties": true, - "properties": { - "error": { - "additionalProperties": false, - "properties": { - "code": { - "type": "string" - }, - "message": { - "type": "string" - }, - "retryable": { - "type": "boolean" - } - }, - "required": [ - "code", - "message", - "retryable" - ], - "type": "object" - } - }, - "required": [ - "error" - ] - } - ], - "type": "object" - }, - "securitySchemes": [ - { - "scopes": [ - "mcp:full" - ], - "type": "oauth2" - } - ], - "title": "Manage Engine documents" - }, - { - "_meta": { - "securitySchemes": [ - { - "scopes": [ - "mcp:full" - ], - "type": "oauth2" - } - ] - }, - "annotations": { - "destructiveHint": true, - "idempotentHint": false, - "openWorldHint": false, - "readOnlyHint": false, - "title": "Run Engine JavaScript" - }, - "description": "Run JavaScript against a persistent Engine instance (code mode).\nNative Engine host API (entity ids are bigint). In-scope globals: engine, PropertyType, params, ctx, dump, files. Do not import.\nNot DOM, Lottie, Canvas, or After Effects: do not call createComposition, engine.root, engine.composition, addRect, createRect, or setEntityName.\nNever reset or delete with engine_document unless the user asks to start over or wipe the scene.\nSignatures (positional; insert x,y is the shape center; fill colors {red,green,blue,alpha} floats 0–1):\n engine.createComp(width, height, fps, startSec, endSec) -> bigint\n engine.insertRect/insertEllipse(comp, x, y, w, h, fill) -> bigint\n engine.createGroup(comp, x, y); engine.setParent(child, group); engine.setName(node, name)\n const tr = engine.insertTrack(node, PropertyType.Position, t); const kf = engine.addKeyFrame(node, PropertyType.Position, tr, t); engine.setKeyFrameVec2(kf, {x,y})\n engine.setBackgroundColor(r,g,b,a) // 0–255 ints, not a color object\n engine.describe() -> [{id, parent, kind, name, data, bbox}] for the focused comp; engine.getNodeData(node) / engine.setNodeData(node, text) is a free-text description of what a node represents\nEditing an existing document: call engine.describe() first, pick the node whose name/data/bbox matches what the user means, then mutate that node in place (setStaticVec2, insertTrack, setStaticSrgbColor on its paint, ...). Never createComp or rebuild the scene for an edit.\nAnimated scene: name the rig, keyframe PropertyType.Position on that named group, return engine.renderFilmstrip({tiles:8}) in the SAME engine_exec. Prefer one complete script. Do not start with engine.methods(). For a non-trivial scene call engine_docs topic \"patterns\" once, then write.\nExample (adapt numbers/names; this is not the task):\nconst c = engine.createComp(256, 256, 30, 0, 2);\nconst rig = engine.createGroup(c, 0, 0);\nengine.setName(rig, \"sample\");\nconst n = engine.insertRect(c, 128, 128, 80, 80, { red: 0.9, green: 0.2, blue: 0.2, alpha: 1 });\nengine.setParent(n, rig);\nconst tr = engine.insertTrack(rig, PropertyType.Position, 0);\nengine.setKeyFrameVec2(engine.addKeyFrame(rig, PropertyType.Position, tr, 0), { x: 0, y: 40 });\nengine.setKeyFrameVec2(engine.addKeyFrame(rig, PropertyType.Position, tr, 2), { x: 0, y: -40 });\nreturn engine.renderFilmstrip({tiles:8});", - "inputSchema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "properties": { - "code": { - "description": "JavaScript source. Globals engine, PropertyType, params, ctx, dump, files are already in scope.\nUse createComp, insertRect/insertEllipse (center x,y), createGroup/setParent, insertTrack+addKeyFrame+setKeyFrameVec2.\nColors {red,green,blue,alpha} 0–1. setBackgroundColor(r,g,b,a) is 0–255 ints. Preview with return engine.renderFilmstrip({tiles:8}).", - "type": "string" - }, - "engineId": { - "default": null, - "type": [ - "string", - "null" - ] - }, - "params": { - "default": null - }, - "timeoutMs": { - "default": null, - "description": "Wall-clock budget in milliseconds (default 5000, max 30000).", - "format": "uint64", - "minimum": 0, - "type": [ - "integer", - "null" - ] - } - }, - "required": [ - "code" - ], - "type": "object" - }, - "name": "engine_exec", - "outputSchema": { - "anyOf": [ - { - "additionalProperties": false, - "properties": { - "durability": { - "type": "string" - }, - "engineId": { - "type": "string" - }, - "logs": { - "items": { - "type": "string" - }, - "type": "array" - }, - "result": {}, - "revision": { - "type": "string" - }, - "warnings": { - "items": { - "type": "string" - }, - "type": "array" - } - }, - "required": [ - "result", - "logs", - "engineId", - "revision", - "durability" - ], - "type": "object" - }, - { - "additionalProperties": true, - "properties": { - "error": { - "additionalProperties": false, - "properties": { - "code": { - "type": "string" - }, - "message": { - "type": "string" - }, - "retryable": { - "type": "boolean" - } - }, - "required": [ - "code", - "message", - "retryable" - ], - "type": "object" - } - }, - "required": [ - "error" - ] - } - ], - "type": "object" - }, - "securitySchemes": [ - { - "scopes": [ - "mcp:full" - ], - "type": "oauth2" - } - ], - "title": "Run Engine JavaScript" - }, - { - "_meta": { - "securitySchemes": [ - { - "scopes": [ - "mcp:full" - ], - "type": "oauth2" - } - ] - }, - "annotations": { - "destructiveHint": false, - "idempotentHint": false, - "openWorldHint": false, - "readOnlyHint": false, - "title": "Export Engine" - }, - "description": "Async png/filmstrip/mp4/webm export of the current Engine. Prefer return engine.renderFilmstrip({tiles:8}) inside engine_exec for previews. 2026 task clients get a task handle and poll tasks/get; others get { jobId } and poll engine_job_status / engine_job_result.", - "inputSchema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "properties": { - "durationSec": { - "default": null, - "format": "float", - "type": [ - "number", - "null" - ] - }, - "engineId": { - "default": null, - "type": [ - "string", - "null" - ] - }, - "format": { - "default": null, - "description": "png (default), filmstrip, mp4, or webm.", - "type": [ - "string", - "null" - ] - }, - "fps": { - "default": null, - "format": "float", - "type": [ - "number", - "null" - ] - }, - "frameTimeSec": { - "default": null, - "format": "float", - "type": [ - "number", - "null" - ] - }, - "height": { - "default": null, - "format": "uint32", - "minimum": 0, - "type": [ - "integer", - "null" - ] - }, - "tiles": { - "default": null, - "format": "uint32", - "minimum": 0, - "type": [ - "integer", - "null" - ] - }, - "width": { - "default": null, - "format": "uint32", - "minimum": 0, - "type": [ - "integer", - "null" - ] - } - }, - "type": "object" - }, - "name": "engine_export", - "outputSchema": { - "anyOf": [ - { - "additionalProperties": false, - "properties": { - "jobId": { - "type": "string" - } - }, - "required": [ - "jobId" - ], - "type": "object" - }, - { - "additionalProperties": true, - "properties": { - "error": { - "additionalProperties": false, - "properties": { - "code": { - "type": "string" - }, - "message": { - "type": "string" - }, - "retryable": { - "type": "boolean" - } - }, - "required": [ - "code", - "message", - "retryable" - ], - "type": "object" - } - }, - "required": [ - "error" - ] - } - ], - "type": "object" - }, - "securitySchemes": [ - { - "scopes": [ - "mcp:full" - ], - "type": "oauth2" - } - ], - "title": "Export Engine" - }, - { - "_meta": { - "securitySchemes": [ - { - "scopes": [ - "mcp:full" - ], - "type": "oauth2" - } - ] - }, - "annotations": { - "destructiveHint": false, - "idempotentHint": true, - "openWorldHint": false, - "readOnlyHint": true, - "title": "Get render job result" - }, - "description": "Fetch a finished render job. Stills may include an inline PNG; video is a resource_link.", - "inputSchema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "properties": { - "jobId": { - "type": "string" - } - }, - "required": [ - "jobId" - ], - "type": "object" - }, - "name": "engine_job_result", - "outputSchema": { - "anyOf": [ - { - "anyOf": [ - { - "additionalProperties": false, - "properties": { - "artifact": { - "additionalProperties": false, - "properties": { - "bytes": { - "minimum": 0, - "type": "integer" - }, - "filename": { - "type": "string" - }, - "height": {}, - "mime": { - "type": "string" - }, - "uri": { - "type": "string" - }, - "width": {} - }, - "required": [ - "uri", - "filename", - "mime", - "bytes", - "width", - "height" - ], - "type": "object" - }, - "inlineBytes": { - "minimum": 0, - "type": "integer" - }, - "jobId": { - "type": "string" - }, - "state": { - "const": "succeeded" - } - }, - "required": [ - "jobId", - "state", - "artifact" - ] - }, - { - "additionalProperties": false, - "properties": { - "artifact": {}, - "engineId": { - "type": "string" - }, - "error": {}, - "jobId": { - "type": "string" - }, - "logs": { - "type": "array" - }, - "progress": {}, - "state": { - "type": "string" - } - }, - "required": [ - "jobId", - "engineId", - "state", - "progress", - "logs", - "error", - "artifact" - ] - } - ], - "type": "object" - }, - { - "additionalProperties": true, - "properties": { - "error": { - "additionalProperties": false, - "properties": { - "code": { - "type": "string" - }, - "message": { - "type": "string" - }, - "retryable": { - "type": "boolean" - } - }, - "required": [ - "code", - "message", - "retryable" - ], - "type": "object" - } - }, - "required": [ - "error" - ] - } - ], - "type": "object" - }, - "securitySchemes": [ - { - "scopes": [ - "mcp:full" - ], - "type": "oauth2" - } - ], - "title": "Get render job result" - }, - { - "_meta": { - "securitySchemes": [ - { - "scopes": [ - "mcp:full" - ], - "type": "oauth2" - } - ] - }, - "annotations": { - "destructiveHint": false, - "idempotentHint": true, - "openWorldHint": false, - "readOnlyHint": true, - "title": "Get render job status" - }, - "description": "Poll an async render job started by engine.renderJob or engine_export.", - "inputSchema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "properties": { - "jobId": { - "type": "string" - } - }, - "required": [ - "jobId" - ], - "type": "object" - }, - "name": "engine_job_status", - "outputSchema": { - "anyOf": [ - { - "additionalProperties": false, - "properties": { - "artifact": {}, - "engineId": { - "type": "string" - }, - "error": {}, - "jobId": { - "type": "string" - }, - "logs": { - "type": "array" - }, - "progress": {}, - "state": { - "type": "string" - } - }, - "required": [ - "jobId", - "engineId", - "state", - "progress", - "logs", - "error", - "artifact" - ], - "type": "object" - }, - { - "additionalProperties": true, - "properties": { - "error": { - "additionalProperties": false, - "properties": { - "code": { - "type": "string" - }, - "message": { - "type": "string" - }, - "retryable": { - "type": "boolean" - } - }, - "required": [ - "code", - "message", - "retryable" - ], - "type": "object" - } - }, - "required": [ - "error" - ] - } - ], - "type": "object" - }, - "securitySchemes": [ - { - "scopes": [ - "mcp:full" - ], - "type": "oauth2" - } - ], - "title": "Get render job status" - }, - { - "_meta": { - "securitySchemes": [ - { - "scopes": [ - "mcp:full" - ], - "type": "oauth2" - } - ] - }, - "annotations": { - "destructiveHint": true, - "idempotentHint": false, - "openWorldHint": true, - "readOnlyHint": false, - "title": "LottieFiles GraphQL" - }, - "description": "Run a GraphQL operation with the caller's delegated LottieFiles grant. This raw adapter returns GraphQL JSON or URLs. Load bytes into document files or assets before engine.load.", - "inputSchema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "properties": { - "operationName": { - "default": null, - "type": [ - "string", - "null" - ] - }, - "query": { - "type": "string" - }, - "variables": { - "default": null - } - }, - "required": [ - "query" - ], - "type": "object" - }, - "name": "lf_graphql", - "outputSchema": { - "anyOf": [ - { - "additionalProperties": true, - "description": "The upstream GraphQL response object. Data and errors are preserved.", - "properties": { - "data": {}, - "errors": { - "type": "array" - } - }, - "type": "object" - }, - { - "additionalProperties": true, - "properties": { - "error": { - "additionalProperties": false, - "properties": { - "code": { - "type": "string" - }, - "message": { - "type": "string" - }, - "retryable": { - "type": "boolean" - } - }, - "required": [ - "code", - "message", - "retryable" - ], - "type": "object" - } - }, - "required": [ - "error" - ] - } - ], - "type": "object" - }, - "securitySchemes": [ - { - "scopes": [ - "mcp:full" - ], - "type": "oauth2" - } - ], - "title": "LottieFiles GraphQL" - }, - { - "_meta": { - "securitySchemes": [ - { - "scopes": [ - "mcp:full" - ], - "type": "oauth2" - } - ] - }, - "annotations": { - "destructiveHint": false, - "idempotentHint": false, - "openWorldHint": false, - "readOnlyHint": false, - "title": "Request file upload" - }, - "description": "Reserve one subject-bound upload. Then PUT the exact bytes and Content-Type to /uploads/{uploadId}/{filename}. Use files.adopt(uploadId, path) to add the immutable asset to a document.", - "inputSchema": { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "properties": { - "bytes": { - "format": "uint64", - "minimum": 0, - "type": "integer" - }, - "filename": { - "type": "string" - }, - "mime": { - "type": "string" - } - }, - "required": [ - "filename", - "mime", - "bytes" - ], - "type": "object" - }, - "name": "request_upload", - "outputSchema": { - "anyOf": [ - { - "additionalProperties": false, - "properties": { - "bytes": { - "minimum": 0, - "type": "integer" - }, - "expiresAtMs": { - "minimum": 0, - "type": "integer" - }, - "filename": { - "type": "string" - }, - "maxBytes": { - "minimum": 0, - "type": "integer" - }, - "mime": { - "type": "string" - }, - "uploadId": { - "type": "string" - } - }, - "required": [ - "uploadId", - "maxBytes", - "filename", - "mime", - "bytes", - "expiresAtMs" - ], - "type": "object" - }, - { - "additionalProperties": true, - "properties": { - "error": { - "additionalProperties": false, - "properties": { - "code": { - "type": "string" - }, - "message": { - "type": "string" - }, - "retryable": { - "type": "boolean" - } - }, - "required": [ - "code", - "message", - "retryable" - ], - "type": "object" - } - }, - "required": [ - "error" - ] - } - ], - "type": "object" - }, - "securitySchemes": [ - { - "scopes": [ - "mcp:full" - ], - "type": "oauth2" - } - ], - "title": "Request file upload" - } - ], - "transport": { - "path": "/mcp", - "productionResourceUri": "https://creator-mcp.lottiefiles.com/mcp", - "type": "streamable-http" - } -} diff --git a/contract/engine-mcp-tools.sha256 b/contract/engine-mcp-tools.sha256 deleted file mode 100644 index 9a519f3..0000000 --- a/contract/engine-mcp-tools.sha256 +++ /dev/null @@ -1 +0,0 @@ -1effca56b6481b2890d0359845e50e99664004e6b3bf2bcb25d1597a589037e7 contracts/public-mcp-v1.json diff --git a/examples/engine-create-export.md b/examples/engine-create-export.md index 26b0587..aa3ee24 100644 --- a/examples/engine-create-export.md +++ b/examples/engine-create-export.md @@ -3,8 +3,8 @@ Use `engine_docs` first. The pinned contract requires `code` for `engine_exec`; `engine_document` requires `action`; and `engine_export` returns `jobId` on success. 1. Ask for the document purpose, output format, and destination. -2. Call `engine_exec` with the published create action and minimum document content. -3. Call `engine_document` to confirm the created document and capture its returned identifier. +2. Call `engine_exec` with `code` that creates the minimum document content. +3. Call `engine_document` with `action: "list"` to confirm the created document and capture its returned identifier. 4. State the export target and request confirmation if the export writes or replaces a file. 5. Call `engine_export` with the published export arguments. 6. If it returns a job identifier, follow [the async job example](engine-async-job.md). diff --git a/scripts/check-contract.py b/scripts/check-contract.py index be335b8..1c0febf 100644 --- a/scripts/check-contract.py +++ b/scripts/check-contract.py @@ -22,6 +22,8 @@ CONTRACT = ROOT / "contracts" / "public-mcp-v1.json" DIGEST = ROOT / "contracts" / "public-mcp-v1.sha256" +PLUGIN_CONTRACT = ROOT / "plugins" / "lottiefiles-creator" / "contracts" / "public-mcp-v1.json" +PLUGIN_DIGEST = ROOT / "plugins" / "lottiefiles-creator" / "contracts" / "public-mcp-v1.sha256" TOOL_TOKEN = re.compile(r"\b(?:engine_[a-z0-9_]+|request_upload|lf_graphql)\b") DIGEST_LINE = re.compile( r"^([a-f0-9]{64})(?:\s+contracts/public-mcp-v1\.json)?\s*$" @@ -73,6 +75,14 @@ def check_digest() -> None: raise CheckError(f"contract digest mismatch: expected {match.group(1)}, got {actual}") +def check_plugin_copy() -> None: + for path, source in ((PLUGIN_CONTRACT, CONTRACT), (PLUGIN_DIGEST, DIGEST)): + if not path.is_file(): + raise CheckError(f"missing plugin-local contract artifact: {path.relative_to(ROOT)}") + if path.read_bytes() != source.read_bytes(): + raise CheckError(f"plugin-local contract artifact differs from root authority: {path.relative_to(ROOT)}") + + def check_schema(tool: Any, index: int) -> str: label = f"tool[{index}]" value = require_object(tool, label) @@ -116,6 +126,7 @@ def check_references(tool_names: set[str]) -> None: def main() -> None: check_digest() + check_plugin_copy() assert_public_text(CONTRACT) payload = load_json(CONTRACT) root = require_object(payload, "contract root") diff --git a/scripts/check-platforms.py b/scripts/check-platforms.py index e7b18d9..5ce6a53 100644 --- a/scripts/check-platforms.py +++ b/scripts/check-platforms.py @@ -99,6 +99,12 @@ def main() -> None: ) if not fallback.is_file(): raise CheckError("ZCode Claude fallback mcpServers must resolve to a file") + for path in ( + PLUGIN / "contracts/public-mcp-v1.json", + PLUGIN / "contracts/public-mcp-v1.sha256", + ): + if not path.is_file(): + raise CheckError(f"plugin-local public contract is missing: {path.relative_to(ROOT)}") # Grok marketplace. grok = require_object(load_json(ROOT / ".grok-plugin/marketplace.json"), "Grok marketplace") From 14b08a9238d13ebe849399d562079cab4d7da817 Mon Sep 17 00:00:00 2001 From: Jawish Hameed Date: Sun, 6 Sep 2026 00:18:18 +0800 Subject: [PATCH 10/12] fix: mirror plugin contract artifacts --- .../contracts/public-mcp-v1.json | 1157 +++++++++++++++++ .../contracts/public-mcp-v1.sha256 | 1 + .../references/tool-contract.md | 22 + .../skills/animation-review/SKILL.md | 2 +- .../references/tool-contract.md | 10 - .../skills/animation-workflows/SKILL.md | 2 +- .../references/tool-contract.md | 16 - .../skills/lottiefiles-library/SKILL.md | 2 +- .../references/tool-contract.md | 13 - references/tool-contract.md | 22 + scripts/generate.py | 30 +- skills/animation-review/SKILL.md | 2 +- .../references/tool-contract.md | 10 - skills/animation-workflows/SKILL.md | 2 +- .../references/tool-contract.md | 16 - skills/lottiefiles-library/SKILL.md | 2 +- .../references/tool-contract.md | 13 - 17 files changed, 1221 insertions(+), 101 deletions(-) create mode 100644 plugins/lottiefiles-creator/contracts/public-mcp-v1.json create mode 100644 plugins/lottiefiles-creator/contracts/public-mcp-v1.sha256 create mode 100644 plugins/lottiefiles-creator/references/tool-contract.md delete mode 100644 plugins/lottiefiles-creator/skills/animation-review/references/tool-contract.md delete mode 100644 plugins/lottiefiles-creator/skills/animation-workflows/references/tool-contract.md delete mode 100644 plugins/lottiefiles-creator/skills/lottiefiles-library/references/tool-contract.md create mode 100644 references/tool-contract.md delete mode 100644 skills/animation-review/references/tool-contract.md delete mode 100644 skills/animation-workflows/references/tool-contract.md delete mode 100644 skills/lottiefiles-library/references/tool-contract.md diff --git a/plugins/lottiefiles-creator/contracts/public-mcp-v1.json b/plugins/lottiefiles-creator/contracts/public-mcp-v1.json new file mode 100644 index 0000000..4f6363f --- /dev/null +++ b/plugins/lottiefiles-creator/contracts/public-mcp-v1.json @@ -0,0 +1,1157 @@ +{ + "authorization": { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + }, + "protocolVersions": [ + "2026-07-28", + "2025-11-25", + "2025-06-18", + "2025-03-26" + ], + "schemaVersion": "1.0.0", + "server": { + "name": "creator-mcp", + "version": "0.1.0" + }, + "tools": [ + { + "_meta": { + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ] + }, + "annotations": { + "destructiveHint": true, + "idempotentHint": true, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Cancel render job" + }, + "description": "Cancel a queued or running render job (idempotent).", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "jobId": { + "type": "string" + } + }, + "required": [ + "jobId" + ], + "type": "object" + }, + "name": "engine_cancel_job", + "outputSchema": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "jobId": { + "type": "string" + }, + "ok": { + "const": true + }, + "state": { + "type": "string" + } + }, + "required": [ + "ok", + "jobId", + "state" + ], + "type": "object" + }, + { + "additionalProperties": true, + "properties": { + "error": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + } + }, + "required": [ + "code", + "message", + "retryable" + ], + "type": "object" + } + }, + "required": [ + "error" + ] + } + ], + "type": "object" + }, + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ], + "title": "Cancel render job" + }, + { + "_meta": { + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ] + }, + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false, + "readOnlyHint": true, + "title": "Engine writing docs" + }, + "description": "Engine writing docs. Call **at most once** per task. For any animated/non-trivial scene use topic \"patterns\" once, then write **one** engine_exec that names a rig, keyframes Position on that group, and returns renderFilmstrip — do not fetch a second topic (guide, api, overview) unless that exec returned an error. Short topics: overview (default) | shapes | animation | paints | render. Full files: guide, api. Never pass topic \"all\".\n", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "topic": { + "default": null, + "description": "patterns | guide | api | overview (default) | shapes | animation | paints | render.", + "type": [ + "string", + "null" + ] + } + }, + "type": "object" + }, + "name": "engine_docs", + "outputSchema": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "bytes": { + "minimum": 0, + "type": "integer" + }, + "topic": { + "type": "string" + } + }, + "required": [ + "topic", + "bytes" + ], + "type": "object" + }, + { + "additionalProperties": true, + "properties": { + "error": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + } + }, + "required": [ + "code", + "message", + "retryable" + ], + "type": "object" + } + }, + "required": [ + "error" + ] + } + ], + "type": "object" + }, + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ], + "title": "Engine writing docs" + }, + { + "_meta": { + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ] + }, + "annotations": { + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Manage Engine documents" + }, + "description": "List, reset, or delete durable Engine documents. Reset and delete require elicitation confirmation.", + "inputSchema": { + "$defs": { + "DocumentAction": { + "enum": [ + "list", + "reset", + "delete" + ], + "type": "string" + } + }, + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "action": { + "$ref": "#/$defs/DocumentAction" + }, + "engineId": { + "default": null, + "type": [ + "string", + "null" + ] + } + }, + "required": [ + "action" + ], + "type": "object" + }, + "name": "engine_document", + "outputSchema": { + "anyOf": [ + { + "anyOf": [ + { + "properties": { + "documents": { + "items": { + "additionalProperties": false, + "properties": { + "engineId": { + "type": "string" + }, + "revision": { + "type": "string" + }, + "tombstone": { + "type": "boolean" + } + }, + "required": [ + "engineId", + "revision", + "tombstone" + ], + "type": "object" + }, + "type": "array" + } + }, + "required": [ + "documents" + ] + }, + { + "properties": { + "action": { + "enum": [ + "reset", + "delete" + ] + }, + "durability": { + "type": "string" + }, + "engineId": { + "type": "string" + }, + "ok": { + "const": true + }, + "revision": { + "type": "string" + }, + "tombstone": { + "type": "boolean" + } + }, + "required": [ + "ok", + "action", + "engineId", + "revision", + "tombstone", + "durability" + ] + }, + { + "properties": { + "action": { + "enum": [ + "reset", + "delete" + ] + }, + "cancelled": { + "const": true + }, + "engineId": { + "type": "string" + }, + "ok": { + "const": false + } + }, + "required": [ + "ok", + "action", + "engineId", + "cancelled" + ] + } + ], + "type": "object" + }, + { + "additionalProperties": true, + "properties": { + "error": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + } + }, + "required": [ + "code", + "message", + "retryable" + ], + "type": "object" + } + }, + "required": [ + "error" + ] + } + ], + "type": "object" + }, + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ], + "title": "Manage Engine documents" + }, + { + "_meta": { + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ] + }, + "annotations": { + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Run Engine JavaScript" + }, + "description": "Run JavaScript against a persistent Engine instance (code mode).\nNative Engine host API (entity ids are bigint). In-scope globals: engine, PropertyType, params, ctx, dump, files. Do not import.\nNot DOM, Lottie, Canvas, or After Effects: do not call createComposition, engine.root, engine.composition, addRect, createRect, or setEntityName.\nNever reset or delete with engine_document unless the user asks to start over or wipe the scene.\nSignatures (positional; insert x,y is the shape center; fill colors {red,green,blue,alpha} floats 0–1):\n engine.createComp(width, height, fps, startSec, endSec) -> bigint\n engine.insertRect/insertEllipse(comp, x, y, w, h, fill) -> bigint\n engine.createGroup(comp, x, y); engine.setParent(child, group); engine.setName(node, name)\n const tr = engine.insertTrack(node, PropertyType.Position, t); const kf = engine.addKeyFrame(node, PropertyType.Position, tr, t); engine.setKeyFrameVec2(kf, {x,y})\n engine.setBackgroundColor(r,g,b,a) // 0–255 ints, not a color object\n engine.describe() -> [{id, parent, kind, name, data, bbox}] for the focused comp; engine.getNodeData(node) / engine.setNodeData(node, text) is a free-text description of what a node represents\nEditing an existing document: call engine.describe() first, pick the node whose name/data/bbox matches what the user means, then mutate that node in place (setStaticVec2, insertTrack, setStaticSrgbColor on its paint, ...). Never createComp or rebuild the scene for an edit.\nAnimated scene: name the rig, keyframe PropertyType.Position on that named group, return engine.renderFilmstrip({tiles:8}) in the SAME engine_exec. Prefer one complete script. Do not start with engine.methods(). For a non-trivial scene call engine_docs topic \"patterns\" once, then write.\nExample (adapt numbers/names; this is not the task):\nconst c = engine.createComp(256, 256, 30, 0, 2);\nconst rig = engine.createGroup(c, 0, 0);\nengine.setName(rig, \"sample\");\nconst n = engine.insertRect(c, 128, 128, 80, 80, { red: 0.9, green: 0.2, blue: 0.2, alpha: 1 });\nengine.setParent(n, rig);\nconst tr = engine.insertTrack(rig, PropertyType.Position, 0);\nengine.setKeyFrameVec2(engine.addKeyFrame(rig, PropertyType.Position, tr, 0), { x: 0, y: 40 });\nengine.setKeyFrameVec2(engine.addKeyFrame(rig, PropertyType.Position, tr, 2), { x: 0, y: -40 });\nreturn engine.renderFilmstrip({tiles:8});", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "code": { + "description": "JavaScript source. Globals engine, PropertyType, params, ctx, dump, files are already in scope.\nUse createComp, insertRect/insertEllipse (center x,y), createGroup/setParent, insertTrack+addKeyFrame+setKeyFrameVec2.\nColors {red,green,blue,alpha} 0–1. setBackgroundColor(r,g,b,a) is 0–255 ints. Preview with return engine.renderFilmstrip({tiles:8}).", + "type": "string" + }, + "engineId": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "params": { + "default": null + }, + "timeoutMs": { + "default": null, + "description": "Wall-clock budget in milliseconds (default 5000, max 30000).", + "format": "uint64", + "minimum": 0, + "type": [ + "integer", + "null" + ] + } + }, + "required": [ + "code" + ], + "type": "object" + }, + "name": "engine_exec", + "outputSchema": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "durability": { + "type": "string" + }, + "engineId": { + "type": "string" + }, + "logs": { + "items": { + "type": "string" + }, + "type": "array" + }, + "result": {}, + "revision": { + "type": "string" + }, + "warnings": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "result", + "logs", + "engineId", + "revision", + "durability" + ], + "type": "object" + }, + { + "additionalProperties": true, + "properties": { + "error": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + } + }, + "required": [ + "code", + "message", + "retryable" + ], + "type": "object" + } + }, + "required": [ + "error" + ] + } + ], + "type": "object" + }, + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ], + "title": "Run Engine JavaScript" + }, + { + "_meta": { + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ] + }, + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Export Engine" + }, + "description": "Async png/filmstrip/mp4/webm export of the current Engine. Prefer return engine.renderFilmstrip({tiles:8}) inside engine_exec for previews. 2026 task clients get a task handle and poll tasks/get; others get { jobId } and poll engine_job_status / engine_job_result.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "durationSec": { + "default": null, + "format": "float", + "type": [ + "number", + "null" + ] + }, + "engineId": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "format": { + "default": null, + "description": "png (default), filmstrip, mp4, or webm.", + "type": [ + "string", + "null" + ] + }, + "fps": { + "default": null, + "format": "float", + "type": [ + "number", + "null" + ] + }, + "frameTimeSec": { + "default": null, + "format": "float", + "type": [ + "number", + "null" + ] + }, + "height": { + "default": null, + "format": "uint32", + "minimum": 0, + "type": [ + "integer", + "null" + ] + }, + "tiles": { + "default": null, + "format": "uint32", + "minimum": 0, + "type": [ + "integer", + "null" + ] + }, + "width": { + "default": null, + "format": "uint32", + "minimum": 0, + "type": [ + "integer", + "null" + ] + } + }, + "type": "object" + }, + "name": "engine_export", + "outputSchema": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "jobId": { + "type": "string" + } + }, + "required": [ + "jobId" + ], + "type": "object" + }, + { + "additionalProperties": true, + "properties": { + "error": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + } + }, + "required": [ + "code", + "message", + "retryable" + ], + "type": "object" + } + }, + "required": [ + "error" + ] + } + ], + "type": "object" + }, + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ], + "title": "Export Engine" + }, + { + "_meta": { + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ] + }, + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false, + "readOnlyHint": true, + "title": "Get render job result" + }, + "description": "Fetch a finished render job. Stills may include an inline PNG; video is a resource_link.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "jobId": { + "type": "string" + } + }, + "required": [ + "jobId" + ], + "type": "object" + }, + "name": "engine_job_result", + "outputSchema": { + "anyOf": [ + { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "artifact": { + "additionalProperties": false, + "properties": { + "bytes": { + "minimum": 0, + "type": "integer" + }, + "filename": { + "type": "string" + }, + "height": {}, + "mime": { + "type": "string" + }, + "uri": { + "type": "string" + }, + "width": {} + }, + "required": [ + "uri", + "filename", + "mime", + "bytes", + "width", + "height" + ], + "type": "object" + }, + "inlineBytes": { + "minimum": 0, + "type": "integer" + }, + "jobId": { + "type": "string" + }, + "state": { + "const": "succeeded" + } + }, + "required": [ + "jobId", + "state", + "artifact" + ] + }, + { + "additionalProperties": false, + "properties": { + "artifact": {}, + "engineId": { + "type": "string" + }, + "error": {}, + "jobId": { + "type": "string" + }, + "logs": { + "type": "array" + }, + "progress": {}, + "state": { + "type": "string" + } + }, + "required": [ + "jobId", + "engineId", + "state", + "progress", + "logs", + "error", + "artifact" + ] + } + ], + "type": "object" + }, + { + "additionalProperties": true, + "properties": { + "error": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + } + }, + "required": [ + "code", + "message", + "retryable" + ], + "type": "object" + } + }, + "required": [ + "error" + ] + } + ], + "type": "object" + }, + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ], + "title": "Get render job result" + }, + { + "_meta": { + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ] + }, + "annotations": { + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false, + "readOnlyHint": true, + "title": "Get render job status" + }, + "description": "Poll an async render job started by engine.renderJob or engine_export.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "jobId": { + "type": "string" + } + }, + "required": [ + "jobId" + ], + "type": "object" + }, + "name": "engine_job_status", + "outputSchema": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "artifact": {}, + "engineId": { + "type": "string" + }, + "error": {}, + "jobId": { + "type": "string" + }, + "logs": { + "type": "array" + }, + "progress": {}, + "state": { + "type": "string" + } + }, + "required": [ + "jobId", + "engineId", + "state", + "progress", + "logs", + "error", + "artifact" + ], + "type": "object" + }, + { + "additionalProperties": true, + "properties": { + "error": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + } + }, + "required": [ + "code", + "message", + "retryable" + ], + "type": "object" + } + }, + "required": [ + "error" + ] + } + ], + "type": "object" + }, + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ], + "title": "Get render job status" + }, + { + "_meta": { + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ] + }, + "annotations": { + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true, + "readOnlyHint": false, + "title": "LottieFiles GraphQL" + }, + "description": "Run a GraphQL operation with the caller's delegated LottieFiles grant. This raw adapter returns GraphQL JSON or URLs. Load bytes into document files or assets before engine.load.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "operationName": { + "default": null, + "type": [ + "string", + "null" + ] + }, + "query": { + "type": "string" + }, + "variables": { + "default": null + } + }, + "required": [ + "query" + ], + "type": "object" + }, + "name": "lf_graphql", + "outputSchema": { + "anyOf": [ + { + "additionalProperties": true, + "description": "The upstream GraphQL response object. Data and errors are preserved.", + "properties": { + "data": {}, + "errors": { + "type": "array" + } + }, + "type": "object" + }, + { + "additionalProperties": true, + "properties": { + "error": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + } + }, + "required": [ + "code", + "message", + "retryable" + ], + "type": "object" + } + }, + "required": [ + "error" + ] + } + ], + "type": "object" + }, + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ], + "title": "LottieFiles GraphQL" + }, + { + "_meta": { + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ] + }, + "annotations": { + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false, + "readOnlyHint": false, + "title": "Request file upload" + }, + "description": "Reserve one subject-bound upload. Then PUT the exact bytes and Content-Type to /uploads/{uploadId}/{filename}. Use files.adopt(uploadId, path) to add the immutable asset to a document.", + "inputSchema": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "bytes": { + "format": "uint64", + "minimum": 0, + "type": "integer" + }, + "filename": { + "type": "string" + }, + "mime": { + "type": "string" + } + }, + "required": [ + "filename", + "mime", + "bytes" + ], + "type": "object" + }, + "name": "request_upload", + "outputSchema": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "bytes": { + "minimum": 0, + "type": "integer" + }, + "expiresAtMs": { + "minimum": 0, + "type": "integer" + }, + "filename": { + "type": "string" + }, + "maxBytes": { + "minimum": 0, + "type": "integer" + }, + "mime": { + "type": "string" + }, + "uploadId": { + "type": "string" + } + }, + "required": [ + "uploadId", + "maxBytes", + "filename", + "mime", + "bytes", + "expiresAtMs" + ], + "type": "object" + }, + { + "additionalProperties": true, + "properties": { + "error": { + "additionalProperties": false, + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "retryable": { + "type": "boolean" + } + }, + "required": [ + "code", + "message", + "retryable" + ], + "type": "object" + } + }, + "required": [ + "error" + ] + } + ], + "type": "object" + }, + "securitySchemes": [ + { + "scopes": [ + "mcp:full" + ], + "type": "oauth2" + } + ], + "title": "Request file upload" + } + ], + "transport": { + "path": "/mcp", + "productionResourceUri": "https://creator-mcp.lottiefiles.com/mcp", + "type": "streamable-http" + } +} diff --git a/plugins/lottiefiles-creator/contracts/public-mcp-v1.sha256 b/plugins/lottiefiles-creator/contracts/public-mcp-v1.sha256 new file mode 100644 index 0000000..9a519f3 --- /dev/null +++ b/plugins/lottiefiles-creator/contracts/public-mcp-v1.sha256 @@ -0,0 +1 @@ +1effca56b6481b2890d0359845e50e99664004e6b3bf2bcb25d1597a589037e7 contracts/public-mcp-v1.json diff --git a/plugins/lottiefiles-creator/references/tool-contract.md b/plugins/lottiefiles-creator/references/tool-contract.md new file mode 100644 index 0000000..832af77 --- /dev/null +++ b/plugins/lottiefiles-creator/references/tool-contract.md @@ -0,0 +1,22 @@ +# Pinned public tool contract + +Release evidence: `contracts/public-mcp-v1.json` at the distribution or plugin root, schema `1.0.0`, SHA-256 `1effca56b6481b2890d0359845e50e99664004e6b3bf2bcb25d1597a589037e7`. The snapshot records the reviewed release contract. Call `engine_docs` at execution to discover the live contract; live discovery is authoritative. + +Every tool requires OAuth 2.0 scope `mcp:full`. A stable tool error has `error.code`, `error.message`, and `error.retryable`. + +| Tool | Required input keys | Optional input keys | Success result fields | +| --- | --- | --- | --- | +| `engine_docs` | None | `topic` | `topic`, `bytes` | +| `engine_document` | `action` (`list`, `reset`, or `delete`) | `engineId` | List: `documents[]` with `engineId`, `revision`, `tombstone`. Reset/delete: `ok`, `action`, `engineId`, `revision`, `tombstone`, `durability`; cancelled work returns `ok: false`, `action`, `engineId`, `cancelled: true`. | +| `engine_exec` | `code` | `engineId`, `params`, `timeoutMs` | `result`, `logs`, `engineId`, `revision`, `durability`; may include `warnings`. | +| `engine_export` | None | `durationSec`, `engineId`, `format`, `fps`, `frameTimeSec`, `height`, `tiles`, `width` | `jobId` | +| `engine_job_status` | `jobId` | None | `jobId`, `engineId`, `state`, `progress`, `logs`, `error`, `artifact` | +| `engine_job_result` | `jobId` | None | Success: `jobId`, `state: "succeeded"`, `artifact` with `uri`, `filename`, `mime`, `bytes`, `width`, `height`; other states use the job status fields. | +| `engine_cancel_job` | `jobId` | None | `ok: true`, `jobId`, `state` | +| `request_upload` | `filename`, `mime`, `bytes` | None | `uploadId`, `maxBytes`, `filename`, `mime`, `bytes`, `expiresAtMs` | + +## LottieFiles GraphQL + +`lf_graphql` requires `query`. It accepts optional `operationName` and `variables`. A GraphQL response preserves `data` and `errors`; a tool failure returns the stable `error` object. When both `data` and `errors` exist, treat the data as partial and report the failed path before any follow-up action. + +The pinned annotations are `readOnlyHint: false`, `destructiveHint: true`, `openWorldHint: true`, and `idempotentHint: false`. These describe a mixed-operation tool. They do not grant scopes or bypass host confirmation. diff --git a/plugins/lottiefiles-creator/skills/animation-review/SKILL.md b/plugins/lottiefiles-creator/skills/animation-review/SKILL.md index 2b5a498..b8ca745 100644 --- a/plugins/lottiefiles-creator/skills/animation-review/SKILL.md +++ b/plugins/lottiefiles-creator/skills/animation-review/SKILL.md @@ -5,7 +5,7 @@ description: Inspect, review, compare, or diagnose an animation result with inst # Animation review and diagnosis -Use `engine_docs` to discover the installed Engine contract. Use `engine_document` to inspect, `engine_exec` to render or inspect when the contract requires it, and `engine_job_status` or `engine_job_result` for async work. Read [the contract reference](references/tool-contract.md) before a tool call. +Use `engine_docs` to discover the installed Engine contract. Use `engine_document` to inspect, `engine_exec` to render or inspect when the contract requires it, and `engine_job_status` or `engine_job_result` for async work. Read [the contract reference](../../references/tool-contract.md) before a tool call. 1. Inspect the source, result, and reported symptom before you diagnose. 2. Compare the smallest relevant artifact, frame range, or property set. diff --git a/plugins/lottiefiles-creator/skills/animation-review/references/tool-contract.md b/plugins/lottiefiles-creator/skills/animation-review/references/tool-contract.md deleted file mode 100644 index 8f0bd4d..0000000 --- a/plugins/lottiefiles-creator/skills/animation-review/references/tool-contract.md +++ /dev/null @@ -1,10 +0,0 @@ -# Engine review tool contract reconciliation - -Reconcile exact argument and result shapes with the released public Engine contract fixture before publication. - -| Review need | Released tool name | Evidence to report | -| --- | --- | --- | -| Discover contract | `engine_docs` | Current capability and argument requirements | -| Inspect document | `engine_document` | Document state and relevant properties | -| Run review action | `engine_exec` | Contract-defined artifact or diagnostic result | -| Inspect async job | `engine_job_status` / `engine_job_result` | Job state, error code, and output location | diff --git a/plugins/lottiefiles-creator/skills/animation-workflows/SKILL.md b/plugins/lottiefiles-creator/skills/animation-workflows/SKILL.md index 0d9ca1e..150589b 100644 --- a/plugins/lottiefiles-creator/skills/animation-workflows/SKILL.md +++ b/plugins/lottiefiles-creator/skills/animation-workflows/SKILL.md @@ -5,7 +5,7 @@ description: Create, edit, save, export, or manage an animation document with in # Animation workflows -Use `engine_docs` to discover the installed Engine contract. Use `engine_document` to inspect a document, `engine_exec` to make a document change, `engine_export` to export, and `engine_job_status` or `engine_job_result` to inspect async work. Read [the contract reference](references/tool-contract.md) before a tool call. +Use `engine_docs` to discover the installed Engine contract. Use `engine_document` to inspect a document, `engine_exec` to make a document change, `engine_export` to export, and `engine_job_status` or `engine_job_result` to inspect async work. Read [the contract reference](../../references/tool-contract.md) before a tool call. 1. Ask for the target document, change, output format, and destination when they are not clear. 2. Inspect the document before you change it. Report the current state that affects the request. diff --git a/plugins/lottiefiles-creator/skills/animation-workflows/references/tool-contract.md b/plugins/lottiefiles-creator/skills/animation-workflows/references/tool-contract.md deleted file mode 100644 index cfdf022..0000000 --- a/plugins/lottiefiles-creator/skills/animation-workflows/references/tool-contract.md +++ /dev/null @@ -1,16 +0,0 @@ -# Engine tool contract reconciliation - -The released public tool names are listed below. Reconcile exact argument and result shapes with the published contract fixture before publication. - -Required mapping: - -| Operation | Released tool name | Required arguments | Result fields | -| --- | --- | --- | --- | -| Discover contract | `engine_docs` | Reconcile with fixture | Reconcile with fixture | -| Inspect document | `engine_document` | Reconcile with fixture | Reconcile with fixture | -| Create or edit document | `engine_exec` | Reconcile with fixture | Reconcile with fixture | -| Upload input | `request_upload` | Reconcile with fixture | Reconcile with fixture | -| Export document | `engine_export` | Reconcile with fixture | Reconcile with fixture | -| Inspect job state | `engine_job_status` | Reconcile with fixture | Reconcile with fixture | -| Get job result | `engine_job_result` | Reconcile with fixture | Reconcile with fixture | -| Cancel job | `engine_cancel_job` | Reconcile with fixture | Reconcile with fixture | diff --git a/plugins/lottiefiles-creator/skills/lottiefiles-library/SKILL.md b/plugins/lottiefiles-creator/skills/lottiefiles-library/SKILL.md index 0b23915..886ce37 100644 --- a/plugins/lottiefiles-creator/skills/lottiefiles-library/SKILL.md +++ b/plugins/lottiefiles-creator/skills/lottiefiles-library/SKILL.md @@ -5,7 +5,7 @@ description: Search or inspect LottieFiles library or account data with lf_graph # LottieFiles library and account workflows -Full LottieFiles GraphQL access is intentional. It supports the capabilities that the connected account has. It does not grant extra scopes. +Full LottieFiles GraphQL access is intentional. It supports the capabilities that the connected account has. It does not grant extra scopes. Read [the contract reference](../../references/tool-contract.md) before a tool call. 1. Discover the needed schema and capability first. Use only fields that the current public schema confirms. 2. Read before a change. Inspect the target resource and its ownership or current state. diff --git a/plugins/lottiefiles-creator/skills/lottiefiles-library/references/tool-contract.md b/plugins/lottiefiles-creator/skills/lottiefiles-library/references/tool-contract.md deleted file mode 100644 index dae96cf..0000000 --- a/plugins/lottiefiles-creator/skills/lottiefiles-library/references/tool-contract.md +++ /dev/null @@ -1,13 +0,0 @@ -# GraphQL contract reference - -Use `lf_graphql` for LottieFiles GraphQL operations. The public schema fixture is required before publication of schema-specific examples or field names. - -Safe request shape: - -- Use one operation with an `operationName`. -- Put user and resource values in `variables`. -- Request only required fields. -- Preserve `data` when GraphQL returns both `data` and `errors`. -- Treat `errors` as an incomplete result until the response identifies the failed path. - -Schema placeholder notation in this repository uses ``, ``, and ``. It is not an API claim. diff --git a/references/tool-contract.md b/references/tool-contract.md new file mode 100644 index 0000000..832af77 --- /dev/null +++ b/references/tool-contract.md @@ -0,0 +1,22 @@ +# Pinned public tool contract + +Release evidence: `contracts/public-mcp-v1.json` at the distribution or plugin root, schema `1.0.0`, SHA-256 `1effca56b6481b2890d0359845e50e99664004e6b3bf2bcb25d1597a589037e7`. The snapshot records the reviewed release contract. Call `engine_docs` at execution to discover the live contract; live discovery is authoritative. + +Every tool requires OAuth 2.0 scope `mcp:full`. A stable tool error has `error.code`, `error.message`, and `error.retryable`. + +| Tool | Required input keys | Optional input keys | Success result fields | +| --- | --- | --- | --- | +| `engine_docs` | None | `topic` | `topic`, `bytes` | +| `engine_document` | `action` (`list`, `reset`, or `delete`) | `engineId` | List: `documents[]` with `engineId`, `revision`, `tombstone`. Reset/delete: `ok`, `action`, `engineId`, `revision`, `tombstone`, `durability`; cancelled work returns `ok: false`, `action`, `engineId`, `cancelled: true`. | +| `engine_exec` | `code` | `engineId`, `params`, `timeoutMs` | `result`, `logs`, `engineId`, `revision`, `durability`; may include `warnings`. | +| `engine_export` | None | `durationSec`, `engineId`, `format`, `fps`, `frameTimeSec`, `height`, `tiles`, `width` | `jobId` | +| `engine_job_status` | `jobId` | None | `jobId`, `engineId`, `state`, `progress`, `logs`, `error`, `artifact` | +| `engine_job_result` | `jobId` | None | Success: `jobId`, `state: "succeeded"`, `artifact` with `uri`, `filename`, `mime`, `bytes`, `width`, `height`; other states use the job status fields. | +| `engine_cancel_job` | `jobId` | None | `ok: true`, `jobId`, `state` | +| `request_upload` | `filename`, `mime`, `bytes` | None | `uploadId`, `maxBytes`, `filename`, `mime`, `bytes`, `expiresAtMs` | + +## LottieFiles GraphQL + +`lf_graphql` requires `query`. It accepts optional `operationName` and `variables`. A GraphQL response preserves `data` and `errors`; a tool failure returns the stable `error` object. When both `data` and `errors` exist, treat the data as partial and report the failed path before any follow-up action. + +The pinned annotations are `readOnlyHint: false`, `destructiveHint: true`, `openWorldHint: true`, and `idempotentHint: false`. These describe a mixed-operation tool. They do not grant scopes or bypass host confirmation. diff --git a/scripts/generate.py b/scripts/generate.py index cae7a01..6548edc 100644 --- a/scripts/generate.py +++ b/scripts/generate.py @@ -195,29 +195,25 @@ def sync_tree(source: Path, destination: Path, *, check: bool) -> bool: return changed -def sync_content(*, check: bool) -> list[Path]: +def sync_generated_trees(*, check: bool) -> list[Path]: stale: list[Path] = [] - source_skills = ROOT / "content" / "skills" - for destination in (PLUGIN_ROOT / "skills", ROOT / "skills"): - if source_skills.exists(): - if sync_tree(source_skills, destination, check=check): + mirrors = ( + (ROOT / "content" / "skills", PLUGIN_ROOT / "skills"), + (ROOT / "content" / "skills", ROOT / "skills"), + (ROOT / "content" / "references", PLUGIN_ROOT / "references"), + (ROOT / "content" / "references", ROOT / "references"), + (ROOT / "content" / "assets", PLUGIN_ROOT / "assets"), + (ROOT / "contracts", PLUGIN_ROOT / "contracts"), + ) + for source, destination in mirrors: + if source.exists(): + if sync_tree(source, destination, check=check): stale.append(destination.relative_to(ROOT)) elif destination.exists(): if check: stale.append(destination.relative_to(ROOT)) else: shutil.rmtree(destination) - - source_assets = ROOT / "content" / "assets" - destination_assets = PLUGIN_ROOT / "assets" - if source_assets.exists(): - if sync_tree(source_assets, destination_assets, check=check): - stale.append(destination_assets.relative_to(ROOT)) - elif destination_assets.exists(): - if check: - stale.append(destination_assets.relative_to(ROOT)) - else: - shutil.rmtree(destination_assets) return stale @@ -232,7 +228,7 @@ def main() -> int: if not args.check: path.parent.mkdir(parents=True, exist_ok=True) path.write_bytes(expected) - stale.extend(sync_content(check=args.check)) + stale.extend(sync_generated_trees(check=args.check)) if args.check and stale: print("stale generated outputs:", file=sys.stderr) for path in stale: diff --git a/skills/animation-review/SKILL.md b/skills/animation-review/SKILL.md index 2b5a498..b8ca745 100644 --- a/skills/animation-review/SKILL.md +++ b/skills/animation-review/SKILL.md @@ -5,7 +5,7 @@ description: Inspect, review, compare, or diagnose an animation result with inst # Animation review and diagnosis -Use `engine_docs` to discover the installed Engine contract. Use `engine_document` to inspect, `engine_exec` to render or inspect when the contract requires it, and `engine_job_status` or `engine_job_result` for async work. Read [the contract reference](references/tool-contract.md) before a tool call. +Use `engine_docs` to discover the installed Engine contract. Use `engine_document` to inspect, `engine_exec` to render or inspect when the contract requires it, and `engine_job_status` or `engine_job_result` for async work. Read [the contract reference](../../references/tool-contract.md) before a tool call. 1. Inspect the source, result, and reported symptom before you diagnose. 2. Compare the smallest relevant artifact, frame range, or property set. diff --git a/skills/animation-review/references/tool-contract.md b/skills/animation-review/references/tool-contract.md deleted file mode 100644 index 8f0bd4d..0000000 --- a/skills/animation-review/references/tool-contract.md +++ /dev/null @@ -1,10 +0,0 @@ -# Engine review tool contract reconciliation - -Reconcile exact argument and result shapes with the released public Engine contract fixture before publication. - -| Review need | Released tool name | Evidence to report | -| --- | --- | --- | -| Discover contract | `engine_docs` | Current capability and argument requirements | -| Inspect document | `engine_document` | Document state and relevant properties | -| Run review action | `engine_exec` | Contract-defined artifact or diagnostic result | -| Inspect async job | `engine_job_status` / `engine_job_result` | Job state, error code, and output location | diff --git a/skills/animation-workflows/SKILL.md b/skills/animation-workflows/SKILL.md index 0d9ca1e..150589b 100644 --- a/skills/animation-workflows/SKILL.md +++ b/skills/animation-workflows/SKILL.md @@ -5,7 +5,7 @@ description: Create, edit, save, export, or manage an animation document with in # Animation workflows -Use `engine_docs` to discover the installed Engine contract. Use `engine_document` to inspect a document, `engine_exec` to make a document change, `engine_export` to export, and `engine_job_status` or `engine_job_result` to inspect async work. Read [the contract reference](references/tool-contract.md) before a tool call. +Use `engine_docs` to discover the installed Engine contract. Use `engine_document` to inspect a document, `engine_exec` to make a document change, `engine_export` to export, and `engine_job_status` or `engine_job_result` to inspect async work. Read [the contract reference](../../references/tool-contract.md) before a tool call. 1. Ask for the target document, change, output format, and destination when they are not clear. 2. Inspect the document before you change it. Report the current state that affects the request. diff --git a/skills/animation-workflows/references/tool-contract.md b/skills/animation-workflows/references/tool-contract.md deleted file mode 100644 index cfdf022..0000000 --- a/skills/animation-workflows/references/tool-contract.md +++ /dev/null @@ -1,16 +0,0 @@ -# Engine tool contract reconciliation - -The released public tool names are listed below. Reconcile exact argument and result shapes with the published contract fixture before publication. - -Required mapping: - -| Operation | Released tool name | Required arguments | Result fields | -| --- | --- | --- | --- | -| Discover contract | `engine_docs` | Reconcile with fixture | Reconcile with fixture | -| Inspect document | `engine_document` | Reconcile with fixture | Reconcile with fixture | -| Create or edit document | `engine_exec` | Reconcile with fixture | Reconcile with fixture | -| Upload input | `request_upload` | Reconcile with fixture | Reconcile with fixture | -| Export document | `engine_export` | Reconcile with fixture | Reconcile with fixture | -| Inspect job state | `engine_job_status` | Reconcile with fixture | Reconcile with fixture | -| Get job result | `engine_job_result` | Reconcile with fixture | Reconcile with fixture | -| Cancel job | `engine_cancel_job` | Reconcile with fixture | Reconcile with fixture | diff --git a/skills/lottiefiles-library/SKILL.md b/skills/lottiefiles-library/SKILL.md index 0b23915..886ce37 100644 --- a/skills/lottiefiles-library/SKILL.md +++ b/skills/lottiefiles-library/SKILL.md @@ -5,7 +5,7 @@ description: Search or inspect LottieFiles library or account data with lf_graph # LottieFiles library and account workflows -Full LottieFiles GraphQL access is intentional. It supports the capabilities that the connected account has. It does not grant extra scopes. +Full LottieFiles GraphQL access is intentional. It supports the capabilities that the connected account has. It does not grant extra scopes. Read [the contract reference](../../references/tool-contract.md) before a tool call. 1. Discover the needed schema and capability first. Use only fields that the current public schema confirms. 2. Read before a change. Inspect the target resource and its ownership or current state. diff --git a/skills/lottiefiles-library/references/tool-contract.md b/skills/lottiefiles-library/references/tool-contract.md deleted file mode 100644 index dae96cf..0000000 --- a/skills/lottiefiles-library/references/tool-contract.md +++ /dev/null @@ -1,13 +0,0 @@ -# GraphQL contract reference - -Use `lf_graphql` for LottieFiles GraphQL operations. The public schema fixture is required before publication of schema-specific examples or field names. - -Safe request shape: - -- Use one operation with an `operationName`. -- Put user and resource values in `variables`. -- Request only required fields. -- Preserve `data` when GraphQL returns both `data` and `errors`. -- Treat `errors` as an incomplete result until the response identifies the failed path. - -Schema placeholder notation in this repository uses ``, ``, and ``. It is not an API claim. From 635473ed9c050f69265ca94093a6fd93c3f7ea4c Mon Sep 17 00:00:00 2001 From: Jawish Hameed Date: Sun, 6 Sep 2026 00:19:56 +0800 Subject: [PATCH 11/12] fix: serialize release workflows --- .github/workflows/ci.yml | 2 +- .github/workflows/publication-probe.yml | 4 ++++ .github/workflows/release.yml | 6 +++++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3dfdb0f..1d37def 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,7 +44,7 @@ jobs: - name: Lint workflows and scripts run: | mise exec -- actionlint .github/workflows/*.yml - mise exec -- zizmor .github + mise exec -- zizmor --pedantic .github mise exec -- shellcheck scripts/*.sh - name: Validate MCP Registry server metadata run: bash scripts/check-mcp-registry.sh diff --git a/.github/workflows/publication-probe.yml b/.github/workflows/publication-probe.yml index b939414..f4df7a4 100644 --- a/.github/workflows/publication-probe.yml +++ b/.github/workflows/publication-probe.yml @@ -7,6 +7,10 @@ on: permissions: {} +concurrency: + group: publication-probe + cancel-in-progress: false + defaults: run: shell: bash -euo pipefail {0} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7782627..362ca00 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,6 +9,10 @@ permissions: {} env: MISE_AUTO_INSTALL: "0" +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + defaults: run: shell: bash -euo pipefail {0} @@ -39,7 +43,7 @@ jobs: - name: Lint workflows and scripts run: | mise exec -- actionlint .github/workflows/*.yml - mise exec -- zizmor .github + mise exec -- zizmor --pedantic .github mise exec -- shellcheck scripts/*.sh - name: Validate MCP Registry server metadata run: bash scripts/check-mcp-registry.sh From b84cf89e6321eb9a891d35a5be9c7377a0e02ecc Mon Sep 17 00:00:00 2001 From: Jawish Hameed Date: Sun, 6 Sep 2026 00:22:47 +0800 Subject: [PATCH 12/12] fix: run open-source secret scan --- .github/workflows/ci.yml | 15 ++------------- .github/workflows/release.yml | 3 ++- mise.toml | 1 + 3 files changed, 5 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1d37def..f45c92b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,6 +41,8 @@ jobs: run: mise install --yes - name: Run deterministic distribution checks run: bash scripts/check.sh + - name: Scan public files + run: mise exec -- gitleaks detect --source . --no-banner --redact - name: Lint workflows and scripts run: | mise exec -- actionlint .github/workflows/*.yml @@ -58,16 +60,3 @@ jobs: --exclude '^https://lottiefiles[.]com/page/privacy-policy$' --exclude '^https://lottiefiles[.]com/page/terms-and-conditions$' README.md content docs examples skills submission - - secrets: - name: Scan public files - runs-on: ubuntu-24.04 - timeout-minutes: 5 - permissions: - contents: read - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - persist-credentials: false - - uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2.3.9 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 362ca00..3f8cf3e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,9 +37,10 @@ jobs: version: 2026.9.1 - name: Install pinned check tools run: mise install --yes - - uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2.3.9 - name: Run deterministic distribution checks run: bash scripts/check.sh + - name: Scan public files + run: mise exec -- gitleaks detect --source . --no-banner --redact - name: Lint workflows and scripts run: | mise exec -- actionlint .github/workflows/*.yml diff --git a/mise.toml b/mise.toml index 759e7a8..8bbb4bf 100644 --- a/mise.toml +++ b/mise.toml @@ -1,4 +1,5 @@ [tools] +"aqua:zricethezav/gitleaks" = "8.30.1" "aqua:koalaman/shellcheck" = "0.11.0" "aqua:lycheeverse/lychee" = "0.22.0" "aqua:rhysd/actionlint" = "1.7.10"