fpt-mcp
# fpt-mcp
> Connect Claude to Autodesk Flow Production Tracking (ShotGrid) for production management using the Model Context Protocol (MCP)
> [!WARNING]
> **Experimental project β use at your own risk.**
> This is an independent, unofficial experiment created with [Claude Code](https://claude.com/claude-code). It is **not** affiliated with, endorsed by, or officially supported by Autodesk in any way. The ShotGrid / Flow Production Tracking name and trademarks belong to Autodesk, Inc.
>
> Allowing AI-generated operations against a live ShotGrid instance carries real risks: **unintended data modifications, accidental entity deletion, incorrect publishes, or metadata corruption.** Always test against a dedicated sandbox project first. Never run this against production data without understanding the operations being performed. The author(s) accept no responsibility for data loss, corruption, or any other damage resulting from its use.
## π Code knowledge graph
Interactive, auto-published map of this codebase β modules, functions, call/import edges and community clusters β rebuilt by [graphify](https://github.com/safishamsi/graphify) and deployed to GitHub Pages on every push to `src/`:
**[abrahamadsk.github.io/fpt-mcp](https://abrahamadsk.github.io/fpt-mcp/)** Β· part of the [MCP ecosystem graph hub](https://abrahamadsk.github.io/mcp-graphs/).
MCP server for **Autodesk Flow Production Tracking** (formerly ShotGrid).
Gives any MCP-compatible AI assistant (Claude Desktop, Claude Code, or any MCP client) full access to the ShotGrid API, Toolkit path resolution, and a RAG-powered knowledge engine that prevents common API hallucinations.
```
Claude Desktop / Claude Code / any MCP client
βββ fpt-mcp
βββ stdio β Claude Desktop / Claude Code
βββ HTTP β scripts, inter-service calls
βββ Qt console β native chat app via fpt-mcp:// protocol handler
```
## Features
### Unrestricted ShotGrid API Access
fpt-mcp exposes the full `shotgun_api3` Python SDK without locking down entity types or fields. Any entity β Asset, Shot, Sequence, Version, Task, PublishedFile, or custom entities β can be queried, created, updated, deleted, or batched through a single consistent set of tools. This matters because production pipelines vary widely: the server never assumes which entity types or field names a studio uses.
### Toolkit Path Resolution
When a project has an Advanced Setup in ShotGrid, the server queries the `PipelineConfiguration` entity, reads `roots.yml` and `templates.yml` directly from the installed Toolkit config, and resolves publish paths using the project's real template definitions. No paths are hardcoded β the resolution uses whatever tk-config is installed, whether default, custom, or forked. Projects without a PipelineConfiguration still get full publish support through explicit path fallback.
### RAG Anti-Hallucination Engine
LLMs hallucinate ShotGrid API details constantly β invalid filter operators, wrong entity reference formats, non-existent Toolkit template tokens. fpt-mcp counters this with a hybrid retrieval system: at query time, `search_sg_docs` performs semantic search (ChromaDB + BAAI/bge-large-en-v1.5) and lexical search (BM25) against three verified API reference documents, fuses the rankings with RRF, and injects the most relevant chunks into Claude's context. The result is correct filter syntax and valid entity formats on the first attempt instead of the third.
### Safety Layer
The `safety.py` module scans every tool call before execution against twelve regex patterns that cover the most destructive operations: bulk delete without specific IDs, unfiltered queries with no limit, path traversal in publish paths, PublishedFile deletion, invalid filter operators, large batch operations, and schema modifications. Blocked operations return a warning with a safe alternative β they never reach the ShotGrid API.
The two tools that actually write files β `tk_publish` (copies a source file to a publish path) and `sg_download` (writes a downloaded attachment) β additionally enforce **write-path containment** via `paths.py`. Each write destination is anchored on a legitimate project root before any bytes are written, computed on the *real* path (`os.path.realpath` + `Path.is_relative_to`), so it catches dot-dot traversal, absolute escapes with no `..` (e.g. `/etc/passwd`), and symlink escapes that the detection-only `safety.py` regex cannot. Allowed roots = the discovered `TkConfig.project_root` (when a PipelineConfiguration resolves) plus the `FPT_MCP_ALLOWED_WRITE_ROOTS` allowlist. The default policy is **warn-and-allow** (a destination outside the roots is logged and permitted, so existing workflows are unaffected); set `FPT_MCP_STRICT_PATHS=1` to turn it into a hard refusal that writes nothing. See [`FPT_MCP_ALLOWED_WRITE_ROOTS` / `FPT_MCP_STRICT_PATHS`](#optional-server-behaviour-env-vars) below.
### Structured ShotGrid error responses
When a ShotGrid call fails on authentication, connectivity, or a protocol error, the tool no longer surfaces an opaque `Error executing tool ...` string. Instead `sg_errors.py` translates the `shotgun_api3` fault family (`AuthenticationFault`, `Fault`, `MissingTwoFactorAuthenticationFault`, `ProtocolError`, `ResponseError`, `ShotgunFileDownloadError`) plus the underlying socket/`urllib`/SSL/timeout errors β and the credential `EnvironmentError` raised at startup by `_validate_config` β into a consistent JSON object the model can branch on:
```json
{
"error": "<scrubbed, truncated server message>",
"error_type": "authentication_failed",
"hint": "ShotGrid rejected the credentials. Your session may have expired β sign in again with `python -m fpt_mcp.auth`; otherwise the account may have been disabled.",
"retryable": false
}
```
`error_type` is a stable machine-readable class (`authentication_failed`, `two_factor_required`, `sso_credentials_rejected`, `shotgrid_api_fault`, `protocol_error`, `malformed_response`, `download_failed`, `ssl_error`, `timeout`, `connection_error`, `config_error`), `hint` is concrete remediation guidance, and `retryable` is an **advisory** label (the server does not auto-retry β a 5xx/timeout is worth retrying, a bad key is not). The translation is applied by the `@sg_errors_to_json` decorator at the `*_impl` / `*_do_*` tool-boundary layer, reusing the standard top-level `error` key so the result is counted as a failed turn (p_fallo) and skips suggestion annotation like every other error path. The echoed server message is scrubbed of credential-shaped tokens and truncated to 300 characters. Unrecognised exceptions (genuine bugs) are re-raised with their traceback rather than swallowed.
### Qt Console and Protocol Handler
fpt-mcp ships a native PySide6 chat window that routes messages through the Claude Code CLI and renders responses with full Markdown support. The console registers the `fpt-mcp://` custom URL scheme on macOS, which means a ShotGrid Action Menu Item can open a chat window with full entity context (entity type, ID, project) pre-populated in a single click β no browser tab, no copy-paste of IDs.
## Requirements
- Python **3.13.x** β `>=3.13,<3.14`. The upper bound is not caution, it is a
hard constraint: `sgtk` below has no Python 3.14 support from Autodesk
(tk-core v0.24.2, the newest release, bundles dependencies for
3.9/3.10/3.11/3.13 only). Lift it when Autodesk ships 3.14.
- macOS (for protocol handler; Qt console also works on Linux/Windows without protocol handler)
- `shotgun_api3` (ShotGrid Python API)
- `sgtk` (Toolkit core) β authenticates as the **signed-in human** rather than
as the API Script. Pulled from Autodesk's official repository, pinned to the
tag the pipeline configuration localises. It is **not** on PyPI; the package
named `tk-core` there belongs to an unrelated project. `git` must be
available at install time. Requires no Flow Production Tracking Desktop
install, and shares the session cache with it and with `tank`.
- `mcp[cli]` (MCP Python SDK with FastMCP)
- `pydantic` >= 2.0
- `PySide6` >= 6.6 (Qt for Python)
- `python-dotenv`
- `httpx`
- `pyyaml` (Toolkit config parsing)
- `chromadb` >= 0.5.0 (RAG vector database)
- `sentence-transformers` >= 2.2.0 (RAG embeddings β BAAI/bge-large-en-v1.5)
- `rank-bm25` >= 0.2.2 (RAG lexical search)
- Claude Code CLI (`npm install -g @anthropic-ai/claude-code`)
**Optional β local / free inference with Ollama:**
- [Ollama](https://ollama.com) >= 0.17.6
- macOS: `brew install ollama && brew services start ollama`
- Linux: https://ollama.com/download/linux (systemd)
- Verify: `ollama --version`
- Create the `qwen3.5-mcp` model (required for Ollama backends):
```bash
ollama pull qwen3.5:9b
cat > /tmp/Modelfile.qwen35mcp <<'EOF'
FROM qwen3.5:9b
PARAMETER num_ctx 16384
PARAMETER temperature 0.7
PARAMETER top_p 0.8
PARAMETER top_k 20
EOF
ollama create qwen3.5-mcp -f /tmp/Modelfile.qwen35mcp
```
- See [MODEL_STRATEGY.md](MODEL_STRATEGY.md) for the full rationale (num_ctx bump, `think: false` requirement, KEEP_ALIVE tuning, KV-cache dtype)
> β οΈ The Ollama backends are **experimental** β recommended for offline or lightweight single-tool use. For the full pipeline use the Anthropic backend (see [LLM backends](#llm-backends)).
## Install
```bash
cd fpt-mcp
python3 -m venv .venv
source .venv/bin/activate
pip install -e .
```
Or use the automated installer (creates venv, installs deps, builds RAG index, registers in Claude Code, pre-approves tools):
```bash
chmod +x install.sh
./install.sh
```
After installing, run the doctor to verify everything is wired correctly:
```bash
./install.sh --doctor
```
A legacy `setup_venv.sh` script also exists (creates the venv and the Qt console .app bundle on macOS) but `install.sh` is the recommended entry point.
## Configure (MANDATORY β do not skip)
> [!IMPORTANT]
> Running `setup_venv.sh` or `install.sh` on its own is **not enough**.
> The installer creates `.env` from the template but leaves the fields
> holding placeholder values. Until you edit `.env` with your real
> ShotGrid credentials, every MCP call fails with an SSL
> `CERTIFICATE_VERIFY_FAILED` error.
Copy `.env.example` β `.env` (or let the installer do it) and replace
**every** field with your real values:
```
SHOTGRID_URL=https://your-actual-site.shotgrid.autodesk.com
SHOTGRID_PROJECT_ID=123
```
**Where each field comes from**:
- `SHOTGRID_URL` β the exact URL you use to log into your ShotGrid site via browser, in the form `https://<your-site>.shotgrid.autodesk.com`.
Then sign in once β there is no credential to paste into `.env`:
```bash
.venv/bin/python -m fpt_mcp.auth
```
- `SHOTGRID_PROJECT_ID` β integer ID of the project you work in most often. Used as a default filter for `sg_find`, `sg_create`, `sg_upload`, and as the key for Toolkit `PipelineConfiguration` lookup. Set to `0` to disable the default filter (every call must then specify project explicitly).
After editing `.env`, restart any running fpt-mcp process (Qt console, MCP server) so it picks up the new values.
### Optional: server-behaviour env vars
These are optional and control server-side behaviour, not ShotGrid identity:
- `FPT_MCP_ALLOWED_WRITE_ROOTS` β `os.pathsep`-separated list of absolute directory roots that `tk_publish` and `sg_download` are permitted to write under. The effective allowlist is this list UNION the discovered `TkConfig.project_root` (when a PipelineConfiguration resolves). Leave unset to rely solely on the discovered project root (or, with no config, no root β see the policy below).
- `FPT_MCP_STRICT_PATHS` β set to `1` to **enforce** write-path containment: a destination outside the allowed roots is refused with an `{"error": ...}` and nothing is written. **Default (unset / any other value) is warn-and-allow**: the out-of-root destination is logged and the write proceeds, so no existing workflow breaks. Turn this on once you have declared your write roots via `FPT_MCP_ALLOWED_WRITE_ROOTS`.
The installer scripts now detect placeholder values left in `.env` and emit a visible warning at the end of the install. The MCP server itself will also refuse to start with a clear error message pointing to `.env` if placeholders remain. Both safeguards exist specifically to prevent confusing SSL errors on the first real call.
### Verify credentials
After editing `.env`, run the doctor to validate connectivity end-to-end:
```bash
./install.sh --doctor
```
The doctor performs five independent checks β claude.json registration, `.env` placeholder detection, venv importability, live ShotGrid API connectivity, and Qt dependency availability. Any `FAIL` line includes a concrete remediation sentence.
**Common pitfalls:**
- **Placeholder values left in `.env`** β the most frequent cause of `CERTIFICATE_VERIFY_FAILED` errors on first use. The doctor detects these automatically.
- **`SHOTGRID_PROJECT_ID=0`** β disables default project scoping. Every `sg_find`, `sg_create`, and `sg_upload` call must then specify a project filter explicitly. This is valid for multi-project workflows but unexpected for single-project setups.
- **No credential in `.env`** β the API script key was removed. Authentication is a per-user session; if a call is rejected, run `python -m fpt_mcp.auth --status`.
- **Stale `.env` after site migration** β if your ShotGrid site URL changes (e.g. during an Autodesk ID migration), update `SHOTGRID_URL` and re-run `--doctor`.
## Usage
Once configured, fpt-mcp is available through Claude Code, Claude Desktop, or the Qt console. Connect to your ShotGrid instance and start a conversation:
```text
You: "Find all Character assets in the Sunrise project that are currently in Pending Review"
Claude β search_sg_docs (filter syntax for Asset) β sg_find (entity=Asset, filters=[project, sg_asset_type, sg_status_list]) β Returns asset list with name, status, and assigned tasks
```
```text
You: "Create a new Shot called sh0150 in sequence SQ010 for project Sunrise, cut in 1001 cut out 1024"
Claude β search_sg_docs (Shot entity format) β sg_create (entity=Shot, fields={code, sg_sequence, project, sg_cut_in, sg_cut_out}) β Shot created and linked to sequence
```
```text
You: "Publish /jobs/sunrise/assets/char_hero/maya/publish/char_hero_v003.ma to the Rigging task on asset Hero"
Claude β search_sg_docs (publish pattern) β tk_resolve_path (PipelineConfiguration lookup) β tk_publish (copy file, find/create PublishedFileType, link Task, register PublishedFile) β Publish registered in ShotGrid
```
```text
You: "How do I filter Versions by review status using the ShotGrid Python API?"
Claude β search_sg_docs (status filter operators, Version entity) β Returns verified filter syntax, valid operator names, and a working code example from the RAG knowledge base
```
<!-- concept:mcp_tool_count start -->
## Tools (18 MCP tool registrations β dispatcher pattern)
<!-- concept:mcp_tool_count end -->
General-purpose tools with no entity restrictions β works with any ShotGrid entity type and field. Bulk and reporting operations are consolidated behind two dispatcher tools to reduce tool-count overhead for the LLM.
<!-- concept:mcp_tool_table start -->
### ShotGrid API β Direct Tools (6 tools)
| Tool | Description |
|------|-------------|
| `sg_find` | Search any entity type with any filters and fields |
| `sg_create` | Create any entity with any fields (project auto-linked) |
| `sg_update` | Update any field on any entity |
| `sg_schema` | Inspect available fields for any entity type |
| `sg_upload` | Upload file to any entity field (thumbnail, movie, attachment) |
| `sg_download` | Download attachment from any entity field |
### Source Resolver (1 tool)
| Tool | Description |
|------|-------------|
| `sg_resolve_source` | Resolve an Asset's best generation input for the World Labs / Vision3D entry flow β ranks linked Version stills, the Asset thumbnail, and the Asset description by priority (image over text; video deferred) and returns resolved / requires_choice / text_only / no_source, downloading the chosen image when a download path is given |
### ShotGrid API β Bulk Dispatcher (`fpt_bulk` β 1 tool, 5 actions)
<!-- concept:fpt_bulk_actions start -->
| Action | Description |
|--------|-------------|
| `fpt_bulk(action="delete")` | Soft-delete (retire) any entity. Can be restored from trash |
| `fpt_bulk(action="revive")` | Restore a previously retired entity |
| `fpt_bulk(action="batch")` | Transactional bulk operations β all succeed or all fail |
| `fpt_bulk(action="editorial")` | Deterministically create a Cut + one CutItem per shot. Edit and source ranges follow Autodesk's Cut-importer convention (1-based, inclusive) and are computed in Python (see `editorial.py`), not by hand; `handles > 0` are written to each Shot (`sg_head_in`/`sg_tail_out`) in the same transaction, never to the CutItem. Required `params`: `{"cut": {"entity": {...}, "code": "...", "fps": 24.0}, "shots": [{"shot": {"type": "Shot", "id": N}, "duration": <frames>}]}`; optional cut keys `source_start_frame` (default 1001), `handles` (default 0), `revision_number` |
| `fpt_bulk(action="link_task")` | Link a native tk-flame delivery to its Task. tk-flame resolves its context from the `.batch` path and tk-core only yields a Task from a Task-typed schema folder, so the Version and its render / `.batch` / quicktime publishes arrive Task-less. Required `params`: `{"version_id": N, "step": "CMP"}` (Step code or short_name). Links the Version, its publishes and the entity's Task-less publishes with the same version number in ONE transaction; exactly one Task must match, and nothing is written if any record already sits on a different Task. Idempotent |
<!-- concept:fpt_bulk_actions end -->
### ShotGrid API β Reporting Dispatcher (`fpt_reporting` β 1 tool, 4 actions)
<!-- concept:fpt_reporting_actions start -->
| Action | Description |
|--------|-------------|
| `fpt_reporting(action="text_search")` | Full-text search across multiple entity types simultaneously |
| `fpt_reporting(action="summarize")` | Server-side aggregation: count, sum, avg, min, max with grouping |
| `fpt_reporting(action="note_thread")` | Read the full reply thread of a Note with all nested replies |
| `fpt_reporting(action="activity")` | Read the activity stream (updates, status changes, notes) for an entity |
<!-- concept:fpt_reporting_actions end -->
### Toolkit (2 tools)
| Tool | Description |
|------|-------------|
| `tk_resolve_path` | Resolve publish path from the project's real PipelineConfiguration |
| `tk_publish` | Publish file: resolve path, copy file, find/create PublishedFileType, link Task, register in ShotGrid |
| `cut_to_edl` | Generate a CMX 3600 EDL from a ShotGrid Cut + CutItems (drives Flame's native Conform) |
| `openclip_create` | Write a versioned Flame Open Clip (.clip) from a shot's published render sequences (Source Versions in the conformed timeline). Task/Step selection is explicit (`task_id` / `step`); with neither it returns the shot's candidate Tasks (+ dependency suggestion) to confirm β never guesses which step feeds the conform |
### Launcher (1 tool)
| Tool | Description |
|------|-------------|
| `fpt_launch_app` | Launch a DCC (Maya, Flame) scoped to a ShotGrid entity. OS-first discovery with the FPT-selected Software version authoritative over "newest installed". Maya routes through Toolkit `tank` when available (`open -a` fallback); Flame launches directly into the matching local project via `startApplication --start-project` (route `auto`/`direct`/`toolkit`). Returns a launch plan with `pid`, `argv`, `launch_method`, `warnings`. See [Launcher prerequisites](#launcher-prerequisites) before first use. |
### RAG β API Knowledge Engine (4 tools)
| Tool | Description |
|------|-------------|
| `search_sg_docs` | Hybrid search across ShotGrid API documentation (ChromaDB + BM25 + HyDE + RRF). Returns relevant API patterns, correct filter syntax, and entity format examples. **Called automatically before complex queries** |
| `learn_pattern` | Persist validated API patterns into the knowledge base. Model trust gates: Opus/Fable write directly, other models stage candidates for human review |
| `session_stats` | Token usage statistics: calls, tokens in/out, RAG savings, cache hits, efficiency ratio, p_fallo |
| `reset_session_stats` | Zero the session counters immediately (manual companion to the 30-min idle auto-reset) |
<!-- concept:mcp_tool_table end -->
## Skills (2)
Tools are what the server can *do*. Skills are the **recipe** β which tools, in
what order, and what fails silently. They live in [`docs/skills/`](docs/skills/),
version controlled with the code they describe, and load only when the request
matches their trigger, so they cost almost nothing until they are relevant.
| Skill | Fires on |
|---|---|
| `fpt-query` | Any read-only analysis: counts, rollups, breakdowns, note threads, activity. The risk here is not damage but token exhaustion β `summarize` over `find`, explicit `fields`, `sg_schema` before an unfamiliar filter |
| `fpt-publish` | The publish chain: StepβTask resolution, the `{name}`-from-Step contract, publish types, image sequences (`%04d`), and why a published path is version-upped rather than rewritten |
Activate them on a fresh clone by symlinking into your user skills directory:
```bash
for s in fpt-query fpt-publish; do
ln -s "$PWD/docs/skills/$s" ~/.claude/skills/"$s"
done
```
Where `safety.py` refuses something, the skill's job is to say what to do
**instead** β never to restate the prohibition, which would give one rule two
places to drift. See [`docs/skills/README.md`](docs/skills/README.md) for the
four-layer split (tool / skill / RAG / memory).
## Approach
Full ShotGrid API access via `shotgun_api3` with no entity restrictions.
### Toolkit path resolution
**Projects with Advanced Setup** (PipelineConfiguration exists):
The server queries the `PipelineConfiguration` entity from ShotGrid, reads the local `roots.yml` and `templates.yml`, and resolves publish paths using the project's real Toolkit config. This works with local configs, `dev` descriptors, and distributed configs. No hardcoded templates β paths come from the actual tk-config.
**Projects without Advanced Setup:**
If no `PipelineConfiguration` is found, `tk_publish` asks for an explicit publish path. The file is copied to the given location and registered as a PublishedFile in ShotGrid. If the project has a Local File Storage configured (ShotGrid β File Management β Local File Storage), the path will be resolvable from the ShotGrid web UI. Without Local Storage, the path is still stored in the PublishedFile `path` field and accessible to any script or loader that reads it.
The `tk_config.py` module reads whatever Toolkit config is installed β default, custom, or forked.
### Launcher prerequisites
`fpt_launch_app` uses an OS-first resolver (`software_resolver.py`) to find the DCC binary on the local machine, then upgrades the launch to route through Toolkit's `tank` CLI when the project has an Advanced Setup PipelineConfiguration. On fresh machines, two one-time setup steps are required before the tool can launch a DCC in context:
**1. Tank CLI authentication (per user, per site)**
Toolkit's `tank` CLI authenticates through the same browser-based session the server uses (see *Who authenticates with what*). The cached session expires periodically. On first use (or after expiry), you must run once interactively:
```bash
/path/to/PipelineConfiguration/tank <EntityType> <entity_id>
```
The CLI will open a browser for Autodesk SSO, approve, and the session token is cached under `~/Library/Caches/Shotgun/<site>/`. After that, all subsequent tank invocations β including the ones `fpt_launch_app` spawns β work non-interactively.
If you see an error like `EOF when reading a line` or `Authentication ... expired` when calling `fpt_launch_app`, your tank session needs a refresh via the manual step above.
**2. `bundle_cache_fallback_roots` in pipeline_configuration.yml**
Classic Advanced Setup configs created by `setup_project` expect bundles (engines, apps, frameworks) to live under `<config>/install/engines/`, `<config>/install/apps/`, etc. When the config was set up without running the bundle-cache step, or when it shares bundles with other projects via the global ShotGrid cache, the local `install/` directory will only contain `core/` and tank will fail with `Cannot start engine! tk-shell v<X> does not exist on disk`.
Fix by adding a fallback path to `<config>/config/core/pipeline_configuration.yml`:
```yaml
pc_id: <project-pc-id>
pc_name: Primary
project_id: <project-id>
project_name: <project-name>
published_file_entity_type: PublishedFile
use_shotgun_path_cache: true
bundle_cache_fallback_roots:
- /Users/<you>/Library/Caches/Shotgun/bundle_cache
```
This is an additive change: classic localized bundles under `<config>/install/` still win when present; the fallback kicks in only for bundles that are not in the local install dir but exist in the global ShotGrid cache from a previous FPT Desktop sync.
**3. Tank command naming convention**
`tk-multi-launchapp` registers its launcher command under two common names depending on the pipeline:
- `launch_<app>` β the default when the pipeline exposes a single DCC version.
- `<app>_<version>` β the convention when the pipeline registers one launcher per installed version (`maya_2027`, `nuke_16.0v4`, etc.).
`fpt_launch_app` prefers the version-specific form when the OS scan parses a version from the install path, and falls back to `launch_<app>` otherwise. Pipelines with yet another convention will need a wrapper that maps to the right tank command.
### Flame context launch
Flame does not need the tank prerequisites above: by default (`route="auto"` or `"direct"`) `fpt_launch_app` composes the direct CLI launch
```bash
/opt/Autodesk/flame_<ver>/bin/startApplication \
--start-project=<name> [--start-workspace=<ws> | --create-workspace] --closed-libs
```
with three guard rails, in order:
1. **Version**: the FPT-selected `Software.version_names` entry wins over the newest local install (held-back versions are intentional); a warning names both when the selected version is not installed.
2. **Project mapping**: the SG project name is slugified with tk-flame's exact convention (`re.sub(r"\W+", "_", name)`) and validated against the projects that actually exist locally (Stone+Wire `sw_listProjects`, fallback `/opt/Autodesk/project` scan). An unknown project is refused β Flame errors on non-existent `--start-project` names β with `route="toolkit"` suggested, since the tk-flame route pre-creates missing projects via Wiretap.
3. **Single instance**: if a Flame-family GUI is already running the launch is refused (Flame holds exclusive per-project locks); `force=true` overrides explicitly.
`route="toolkit"` opts into the tank route (pipeline hooks + project auto-creation) and then the tank prerequisites above apply.
## RAG β Anti-hallucination Engine
fpt-mcp includes a hybrid Retrieval-Augmented Generation (RAG) system that provides Claude with verified ShotGrid API knowledge at query time, eliminating common hallucinations like invalid filter operators, incorrect entity reference formats, and wrong Toolkit template tokens.
### Architecture
```
User query β search_sg_docs tool
β
βββββββββ΄ββββββββ
β HyDE Expander β β Adaptive: detects shotgun_api3 / Toolkit / REST
βββββββββ¬ββββββββ
β
βββββββββββββΌββββββββββββ
β β β
ChromaDB BM25 Index In-session
(semantic) (lexical) Cache
β β
βββββββ¬ββββββ
β
RRF Fusion (k=60)
β
Top-N chunks + relevance score
```
### Technology stack
| Component | Technology | Purpose |
|-----------|-----------|---------|
| Vector DB | ChromaDB (persistent) | Semantic search with cosine similarity |
| Embeddings | BAAI/bge-large-en-v1.5 | Document and query encoding (~570 MB model) |
| Lexical search | BM25Okapi (rank_bm25) | Exact API method name matching |
| Query expansion | HyDE (adaptive) | Generates domain-specific hypothetical code before embedding |
| Rank fusion | RRF (k=60) | Combines semantic + BM25 rankings without score calibration |
| Safety | 12+ regex patterns | Detects dangerous operations before execution |
| Token tracking | Session stats | Measures tokens used vs saved by RAG, calculates efficiency |
| Self-learning | learn_pattern + model gates | Grows the knowledge base from validated patterns |
| Cache | In-session dict | Avoids redundant ChromaDB queries within a session |
### Knowledge corpus
The RAG indexes three ShotGrid API reference documents covering distinct domains:
| Document | Content | Size |
|----------|---------|------|
| `docs/SG_API.md` | shotgun_api3 Python SDK β methods, filter operators by field type, entity format rules, anti-patterns | ~7 KB |
| `docs/TK_API.md` | Toolkit (sgtk) β PipelineConfiguration discovery, template tokens (case-sensitive), descriptor types, path resolution | ~7 KB |
| `docs/REST_API.md` | REST API β comparison table vs Python SDK, filter syntax differences | ~2.5 KB |
### HyDE adaptive expansion
Unlike generic HyDE, fpt-mcp detects which API domain the query targets and generates a domain-specific hypothetical document:
- **Toolkit queries** (template, publish path, roots.yml) β generates `import sgtk` code skeleton
- **REST API queries** (oauth, bearer, endpoint) β generates `import requests` HTTP skeleton
- **Default** (most queries) β generates `from shotgun_api3 import Shotgun` skeleton
This produces embeddings closer to the relevant corpus section, improving retrieval precision.
### Dangerous pattern detection
The `safety.py` module scans tool parameters before execution and blocks or warns about dangerous operations:
- Bulk delete without specific IDs
- Unfiltered search with no limit (returns entire database)
- Entity reference format errors (int instead of `{type, id}` dict)
- Path traversal in publish paths (`../`)
- Schema modifications (field create/delete)
- PublishedFile deletion (breaks Toolkit references)
- Invalid filter operators (hallucinated by LLMs)
- Large batch operations (>100 entities)
- Incorrect template tokens
### Building the RAG index
After installing dependencies, build the ChromaDB index from the documentation corpus:
```bash
# From the project directory, with venv activated:
source .venv/bin/activate
python -m fpt_mcp.rag.build_index
```
This creates the persistent ChromaDB database and BM25 corpus.json. The first run downloads the BAAI/bge-large-en-v1.5 embedding model (~570 MB). The index only needs rebuilding when the documentation files in `docs/` change.
## Self-Learning
When `search_sg_docs` returns a low-relevance score (below 60%) but the operation succeeds, Claude can call `learn_pattern` to persist the working pattern into the knowledge base for future sessions. **Model trust gates** control who can write directly: only the two top cloud tiers β Opus and Fable β append the pattern to the docs (status `appended_pending_index`; it becomes retrievable on the next `build_index`). Every other model, including Sonnet and local Ollama models, is read-only and stages candidates in `rag/candidates.json` for human review before promotion. The allow-list lives in `write_allowed_models` in `config.json` (default `["claude-opus", "claude-fable"]`).
## Token Tracking
Every tool call tracks tokens consumed in and out. The `session_stats` tool reports the full session breakdown: total calls, tokens used, tokens saved by RAG (versus loading raw documentation), cache hits, patterns learned, and an efficiency ratio. This makes the RAG savings measurable and visible rather than implicit.
## Transports
### stdio (Claude Desktop / Claude Code)
Default mode. The server communicates via standard input/output as a subprocess.
```bash
python -m fpt_mcp.server
```
### HTTP (external MCP clients)
Runs on a network port so an MCP client that cannot spawn a stdio process β
Claude Desktop over HTTP, a script, the MCP Inspector β can connect.
```bash
python -m fpt_mcp.server --http # port 8090 (default)
python -m fpt_mcp.server --http --port 9000 # custom port
```
**Start it deliberately; nothing installs it as a service.** The endpoint has
**no authentication**: anything able to reach the port drives production
ShotGrid with full write access. Keep it on `127.0.0.1` and do not put it
behind `--host 0.0.0.0` without an authentication layer in front.
The Qt console does **not** use this transport β it spawns `claude`, which
starts its own private fpt-mcp over stdio for each message.
## Qt Console (native chat app)
Native PySide6 chat window that routes messages through Claude Code CLI. Replaces the browser-based AMI console β removed along with its HTTP endpoint β with a proper desktop app registered as the `fpt-mcp://` protocol handler.
Features:
- Markdown rendering (bold, italic, code, headings, lists)
- Dark theme matching ShotGrid aesthetic
- Protocol handler (`fpt-mcp://`) for direct launch from ShotGrid AMIs
- ShotGrid entity context passed automatically via URL params
- Light Payload support (fetches full context from EventLogEntry API)
- No HTTP server dependency β launches as a standalone app
> **Project context (zero silent defaults).** The console resolves its ShotGrid project ONLY from the launch context. An AMI fired **from within a project** (an entity or project page) binds to that project **authoritatively** β ShotGrid passes the page (`page_id`), which the console resolves to the page's project β so `sg_create` / `sg_find` target the project you are viewing. Launched from the **global user menu** or standalone, it has no project: at launch it **detects your most-recent-activity project** (from the ShotGrid event log), **pins it for the session**, and **asks you to confirm or pick another** before any create/update/delete/publish β it never falls back to the `.env` project. Changing project = **relaunch** the console (the project is a launch-time decision); for automatic binding, trigger the AMI from a page inside the target project.
### Launch
```bash
# Direct
fpt-console
# With entity context
fpt-console --entity-type Shot --entity-id 456 --project-id 123
# Via protocol handler (from ShotGrid AMI or terminal)
open "fpt-mcp://chat?entity_type=Asset&selected_ids=123&project_id=456"
```
### ShotGrid AMI setup
Admin β Action Menu Items β Add:
- **Title**: FPT Console
- **Entity types**: Asset, Shot, Sequence, Version, Task (or any)
- **URL**: `fpt-mcp://chat`
ShotGrid automatically appends entity context parameters (`entity_type`, `selected_ids`, `project_id`, `project_name`, `user_login`) to custom protocol URLs. Do not add `{placeholder}` tokens β they are only substituted for `http://` and `https://` URLs.
If **Light Payload** is enabled in the AMI configuration, ShotGrid sends only an `event_log_entry_id` instead of the full entity context. The Qt console detects this automatically and fetches the real entity context from the ShotGrid API via `EventLogEntry.meta.ami_payload`. This requires valid ShotGrid API credentials in `.env`.
After changing an AMI URL in ShotGrid, you may need to hard-refresh the browser (Cmd+Shift+R) to clear the cached AMI configuration.
When launched from an AMI, the entity context is displayed in the header badge and included in every message sent to Claude.
### LLM backends
The console runs Claude Code CLI as a subprocess and lets you pick the model per session from the header dropdown. Three backend families are available:
| Backend | Best suited for | Status |
|---|---|---|
| **Anthropic (Claude)** β Opus / Sonnet / Fable | Full pipeline and multi-tool agentic workflows (cross-MCP orchestration, multi-step publishes, large context) | **Recommended (default)** |
| **Local Ollama** β π Mac-local Β· π₯ LAN | Offline use and lightweight, single-domain requests | **Experimental** |
> **Note on local backends.** The local Ollama options (π Mac-local, π₯ LAN) are provided for offline and experimental use. The combined MCP tool inventory (fpt-mcp + maya-mcp + flame-mcp) together with the workflow system prompt requires a large context window, and complex multi-step tool routing benefits from a frontier model. For the full pipeline and any non-trivial agentic task we therefore **recommend the Anthropic backend**; local models are best kept to short, single-tool requests. Broader local-workflow support is an area of ongoing development.
## Client configurations
### Claude Desktop
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
```json
{
"mcpServers": {
"fpt-mcp": {
"command": "/path/to/fpt-mcp/.venv/bin/python",
"args": ["-m", "fpt_mcp.server"],
"cwd": "/path/to/fpt-mcp",
"env": {
"SHOTGRID_URL": "https://yoursite.shotgrid.autodesk.com",
"SHOTGRID_PROJECT_ID": "123"
}
}
}
}
```
The `cwd` field is required so the server can find the `.env` file and resolve relative paths correctly.
### Claude Code
Claude Code uses **two separate files** for MCP configuration:
**1. MCP server definitions** β `~/.claude.json` (note: file in home dir, not inside `~/.claude/`):
```bash
# Add the server via CLI (recommended):
claude mcp add fpt-mcp -s user -e SHOTGRID_URL=https://yoursite.shotgrid.autodesk.com -- /path/to/fpt-mcp/.venv/bin/python -m fpt_mcp.server
# Or edit ~/.claude.json manually:
```
```json
{
"mcpServers": {
"fpt-mcp": {
"command": "/path/to/fpt-mcp/.venv/bin/python",
"args": ["-m", "fpt_mcp.server"],
"env": {
"SHOTGRID_URL": "https://yoursite.shotgrid.autodesk.com",
}
}
}
}
```
**2. Tool permissions** β `~/.claude/settings.json`:
```json
{
"permissions": {
"allow": [
"mcp__fpt-mcp__sg_find",
"mcp__fpt-mcp__sg_create",
"mcp__fpt-mcp__sg_update",
"mcp__fpt-mcp__sg_schema",
"mcp__fpt-mcp__sg_upload",
"mcp__fpt-mcp__sg_download",
"mcp__fpt-mcp__sg_resolve_source",
"mcp__fpt-mcp__fpt_bulk",
"mcp__fpt-mcp__fpt_reporting",
"mcp__fpt-mcp__fpt_launch_app",
"mcp__fpt-mcp__tk_resolve_path",
"mcp__fpt-mcp__tk_publish",
"mcp__fpt-mcp__search_sg_docs",
"mcp__fpt-mcp__learn_pattern",
"mcp__fpt-mcp__session_stats",
"mcp__fpt-mcp__reset_session_stats"
]
}
}
```
> **Important:** `mcpServers` must be in `~/.claude.json`, NOT in `~/.claude/settings.json`. The `settings.json` file is only for permissions and other settings. If you put `mcpServers` in the wrong file, `claude mcp list` will not show the server.
The `permissions.allow` list auto-approves all fpt-mcp tools so Claude Code (and the Qt console, which uses Claude Code CLI internally) can call them without manual confirmation each time.
## Cross-MCP orchestration (optional)
fpt-mcp works standalone, but when combined with other MCP servers in the same Claude session, Claude can orchestrate multi-tool workflows automatically. For example, with a DCC MCP server configured alongside fpt-mcp, Claude can query ShotGrid for asset data, download references, and register publishes β all in a single conversation.
## Qt console app bundle (macOS)
The `setup_venv.sh` script (legacy) builds the desktop entry point:
1. Creates the venv and installs dependencies
2. Builds the Qt console .app bundle
3. Registers it with macOS Launch Services as the `fpt-mcp://` protocol handler
**No background service is installed.** Earlier versions installed a launchd
agent running the HTTP transport on port 8090; it is removed, and the installer
now unloads any copy it finds. It existed for the original browser-based AMI
console, which could not spawn a stdio process β the Qt console can, so nothing
has called it since. Run the HTTP transport by hand if an external MCP client
needs it (see *Transports* above).
For most users, `install.sh` is the recommended entry point (handles venv, deps, RAG index, Claude Code registration, and tool permissions). Use `setup_venv.sh` only if you need the Qt console .app bundle.
```bash
./setup_venv.sh
```
Manage the MCP server:
- `launchctl stop com.fpt-mcp.server` β stop
- `launchctl start com.fpt-mcp.server` β start
- `launchctl unload ~/Library/LaunchAgents/com.fpt-mcp.server.plist` β uninstall
Logs: `/tmp/fpt-mcp.log` and `/tmp/fpt-mcp.err`
Qt console logs: `/tmp/fpt-console.log`
## Architecture
```
ShotGrid AMI click
β fpt-mcp://chat (macOS appends entity params automatically)
β macOS opens FPT-MCP Console.app (protocol handler via Apple Events)
β QFileOpenEvent delivers the URL to the Qt app
β If Light Payload: fetch real context from EventLogEntry API
β Qt chat window with entity context badge
β User types natural language
β Claude Code CLI (claude -p "message" --output-format text)
β Claude calls fpt-mcp tools via MCP (stdio)
β ShotGrid API response
β Markdown rendered in Qt chat window
```
The invariant: **a custom protocol handler binds the `fpt-mcp://` scheme to one
specific app.** `qt/build_app_bundle.py` writes `CFBundleURLSchemes` into the
bundle's `Info.plist`, and `setup_venv.sh` registers it with `lsregister`. That
is the entry point β there is no HTTP endpoint and no background service in this
path. Each message starts a private fpt-mcp over stdio that dies with it.
### Who authenticates with what
The `.app` itself authenticates against nothing β it is a launcher. Credentials
enter two hops further down, and **both hops now carry the same human identity**.
| Destination | Credential | Effective identity |
|---|---|---|
| ShotGrid API (all `sg_*`, `tk_publish`, conform tools) | **User session token**, resolved by the console | The signed-in human |
| Toolkit (`tank` launches) | **Browser SSO session**, cached under `~/Library/Caches/Shotgun/` | The signed-in human |
The two used to diverge β the API ran on a shared **API Script key** while
`tank` ran as the person, which is why `setup/config/core/shotgun.yml` carries
`host:` and no `api_key`. They are unified now: both read the **same** session
cache, so one sign-in covers Desktop, `tank` and this server.
### How the session is established
`fpt_mcp/auth.py` resolves it, and the **console** calls it at launch β before
the first ShotGrid request β then exports it so the `claude` subprocess, and
therefore fpt-mcp, inherits it:
1. **A cached session**, shared with Desktop and `tank`. Never prompts.
2. **The App Session Launcher** when there is none or it has expired: the site
opens in the default browser, the person approves, a token comes back.
The launcher imports **no Qt**, which is what lets one code path serve the Qt
consoles and a plain terminal alike. `ShotgunAuthenticator.get_user()` is
deliberately never called: it prints a method-selection menu and blocks on
`input()`, which raises `EOFError` in an MCP server on stdio.
The **server never opens a browser**. It uses the injected session if there is
one, otherwise it reads the **shared cache** β a lookup that returns `None`
rather than prompting. With neither, it refuses to connect β there is no
fallback credential. So a session established anywhere is picked up everywhere.
### Signing in from a terminal
The browser flow needs no Qt, so a plain terminal signs in just as well as the
Qt console:
```bash
.venv/bin/python -m fpt_mcp.auth # opens the browser, caches the session
.venv/bin/python -m fpt_mcp.auth --status # who is signed in, no prompting
.venv/bin/python -m fpt_mcp.auth --list # every cached site and user
.venv/bin/python -m fpt_mcp.auth --logout # forget it, so the next sign-in can differ
.venv/bin/python -m fpt_mcp.auth --host URL # authenticate against another portal
```
The token lands in the shared Toolkit cache, so the console, the MCP server,
`tank` and Desktop all pick it up afterwards without signing in again. This is
the path for a Claude Code session, a first-time setup, or any context with no
Qt console to launch.
### Switching portal or user
Sessions are cached **per site**, so several portals coexist; `--list` marks the
current one with `*`. Switching user on the same site needs `--logout` first β
otherwise the cached identity simply wins and nothing prompts.
> **The cached session must match `SHOTGRID_URL`.** `client.py` compares them and
> **ignores** a token issued by a different site, with a warning naming the
> command to fix it. A token from portal A sent to portal B fails in a confusing
> way β a valid-looking credential rejected by a server that never issued it β so
> the mismatch is refused rather than passed through.
> **Sites that disable the App Session Launcher** have no headless path at all β
> Toolkit would fall back to that terminal prompt. `auth.py` detects this and
> says so instead of hanging; sign in through Desktop and retry.
### No fallback credential
The API Script key is gone. With no injected session and nothing in the shared
cache, `client.py` refuses to connect and names the command that fixes it
(`python -m fpt_mcp.auth`). Every write is attributed to a person.
**The AMI's `user_login` is still not an authentication factor.** It travels in
the URL for display and context (`qt/app.py`) and is never sent to the API. What
changed is that it no longer matters: the identity now comes from a real session,
so supplying a different `user_login` buys nothing.
## Project Structure
```
fpt-mcp/
βββ pyproject.toml # Package metadata and dependencies
βββ install.sh # One-step installation script (venv, .env, RAG index, MCP registration)
βββ setup_venv.sh # Venv setup; builds the Qt console .app and registers fpt-mcp://
βββ .env.example # Environment variables template
βββ .concepts.yml # Concept registry (cross-cutting invariants, strict mode)
βββ .pre-commit-config.yaml # Pre-commit hooks (verify_concepts, verify_templates)
βββ CHANGELOG.md # Keep a Changelog + SemVer
βββ CLAUDE.md # Project context for Claude sessions
βββ MODEL_STRATEGY.md # LLM backend strategy (cloud + local models)
βββ LICENSE / NOTICE.md # License and third-party notices
βββ docs/
β βββ DEPLOY.md # Reinstall recipes and deploy workflow
β βββ BUCKET_F_PLAN.md # server.py refactor plan (Bucket F)
β βββ O3_NEXT_SUGGESTED_ACTIONS.md # Chaining-hints design (next_suggested_actions)
β βββ skills/ # Claude Code skills (symlinked into ~/.claude/skills/)
β βββ fpt-query/ # Read-only analysis: rollups, counts, field discipline
β βββ fpt-publish/ # The publish chain: StepβTask, types, version-up
βββ scripts/
β βββ cut-release.sh # Canonical release script (the only supported release path)
β βββ verify_concepts.py # Concept-registry drift checker (pre-commit)
β βββ gen_tk_templates_doc.py # Generates TK_API.md's template block from the real templates.yml
β βββ verify_templates.py # Toolkit templates vs TK_API.md checker (pre-commit; check 8 = real config)
β βββ check_adversarial_count.py # F3b precondition gate (adversarial test count)
β βββ invariant_types.py # Shared invariant engine types
βββ src/
β βββ fpt_mcp/
β βββ __init__.py
β βββ server.py # MCP server entry point (FastMCP) β tool registrations
β βββ shotgrid.py # Bodies of the direct SG tools + fpt_bulk dispatcher handlers
β βββ reporting.py # fpt_reporting dispatcher handlers
β βββ toolkit_tools.py # Bodies of tk_resolve_path and tk_publish
β βββ launcher.py # Body of the fpt_launch_app tool
β βββ rag_tools.py # Bodies of search_sg_docs and learn_pattern
β βββ client.py # ShotGrid API client wrapper
β βββ filters.py # ShotGrid filter validation and safety constants
β βββ models.py # Pydantic input models for every MCP tool (extra="forbid")
β βββ safety.py # Safety module β blocks dangerous write patterns
β βββ software_resolver.py # DCC discovery for fpt_launch_app (OS-first cascade)
β βββ suggestions.py # Per-tool chaining hints (next_suggested_actions)
β βββ tk_config.py # Toolkit config loader (PipelineConfiguration discovery)
β βββ _session_stats.py # Session reset + F0 telemetry
β βββ qt/
β β βββ app.py # Qt application entry point
β β βββ chat_window.py # Chat window widget
β β βββ claude_worker.py # Claude subprocess worker (visible-progress streaming, canonical)
β β βββ build_app_bundle.py # macOS .app bundle builder (registers the fpt-mcp:// URL scheme)
β βββ rag/
β β βββ build_index.py # RAG index builder (run to rebuild)
β β βββ config.py # RAG configuration (chunk size, model)
β β βββ corpus.json # Parsed documentation corpus
β β βββ search.py # Hybrid search (ChromaDB semantic + BM25 + HyDE + RRF)
β β βββ index/ # auto-generated (ChromaDB vector store)
β βββ docs/
β β βββ REST_API.md # ShotGrid REST API documentation corpus
β β βββ SG_API.md # ShotGrid Python API documentation corpus
β β βββ TK_API.md # Toolkit API documentation corpus
β βββ skills/
β βββ asset-creation/
β βββ SKILL.md # Claude skill for asset creation workflows
βββ tests/ # Mock suites + golden transcripts + real-index guards
βββ conftest.py
βββ fixtures/ # Mock Toolkit templates and fixtures
βββ golden/ # Golden transcripts (determinism guards)
```
> **No machine-specific files in the repo.** `setup_venv.sh` writes nothing to
> `~/Library/LaunchAgents/` any more β the launchd agent it used to install is
> gone, and the installer removes any copy it finds. The `fpt-mcp://` AMI URL
> handler is registered by the Qt `.app` bundle
> (`qt/build_app_bundle.py`), which derives every absolute path from wherever
> the repo was cloned, so the built bundle is machine-local by design and never
> enters version control.
## Troubleshooting
**Connection refused on ShotGrid API**
- Verify `SHOTGRID_URL` in `.env`, then `python -m fpt_mcp.auth --status`
- Check that the Script Application is active in ShotGrid Admin β Scripts
- Test connectivity: `curl -s https://YOUR_SITE.shotgrid.autodesk.com/api/v1`
**RAG index not found**
- Run `python -m fpt_mcp.rag.build_index` to rebuild
- Check that `docs/` directory contains the ShotGrid API documentation corpus
**Toolkit path resolution fails**
- Verify that a PipelineConfiguration entity exists for the project in ShotGrid
- Check `roots.yml` and `templates.yml` paths in the PipelineConfiguration's `descriptor` field
- For distributed configs, only `dev` descriptor type is currently supported
## Ecosystem
`fpt-mcp` is part of a four-component VFX pipeline. Each component has a defined role:
| Repo | Role |
|------|------|
| [flame-mcp](https://github.com/abrahamADSK/flame-mcp) | Controls Autodesk Flame for compositing, conform, and finishing |
| [maya-mcp](https://github.com/abrahamADSK/maya-mcp) | Controls Autodesk Maya for 3D modeling, animation, and rendering |
| [fpt-mcp](https://github.com/abrahamADSK/fpt-mcp) | Connects to Autodesk Flow Production Tracking (ShotGrid) for production tracking, asset management, and publishes |
| [vision3d](https://github.com/abrahamADSK/vision3d) | GPU inference server for AI-powered 3D generation β the remote backend for maya-mcp's image-to-3D and text-to-3D tools |
`fpt-mcp` is the production backbone of the pipeline. It provides asset metadata, task assignments, path resolution, and publish registration for the other tools. `maya-mcp` and `flame-mcp` both consume `fpt-mcp` data β Maya for asset context and publish targets, Flame for shot and sequence lookup. `vision3d` has no direct connection to `fpt-mcp`.
## License
[MIT](LICENSE)
TDQS
Scored across 18 tools
Many tools have distinct roles (launch, EDL, schema, path resolve), but several overlap: sg_find vs fpt_reporting's text_search both search entities, sg_create vs fpt_bulk's batch both create, and the publishing surface (tk_publish, openclip_create, plus a referenced-but-absent maya_session publish) is crowded. Descriptions disambiguate intent well, but an agent still faces genuine choice points.
Names are uniformly snake_case and mostly verb_noun, with subsystem prefixes (sg_, tk_, fpt_) that usefully group capabilities. Minor deviations: some tools are noun-first (session_stats, cut_to_edl) and prefixes are applied unevenly (openclip_create, search_sg_docs, learn_pattern have none), but the pattern stays readable.
18 tools is on the heavy side but justified by the broad domain spanning ShotGrid CRUD, DCC launch, Toolkit publish/path resolution, Flame conform, and RAG meta-tools. No tool appears purely redundant, though the meta/RAG trio (search_sg_docs, learn_pattern, session_stats/reset_session_stats) pushes the count up.
Core ShotGrid lifecycle is covered (find, create, update, upload, download, schema, bulk delete/revive/batch, linking), plus Toolkit and Flame-specific flows. Minor gaps: no single-entity get (must use sg_find), deletion only via the bulk wrapper, and tk_publish references a maya_session native publisher that is not present in this tool set.