# AdAnvil (pypi · adanvil)

Lets an AI agent edit vertical 9:16 video ads from your footage, a reference ad or an idea.

- Trust score: 64/100 (medium)
- Registry status: active
- Liveness: live
- Owner verified: no
- Last scored: 2026-10-17

## Components

- pypi · `adanvil`: 64/100 (this document), [markdown](https://verifymcp.io/servers/maxbuildog-adanvil/adanvil.md), [page](https://verifymcp.io/servers/maxbuildog-adanvil/adanvil)

## Channel facts

- Registry: `pypi`
- Package: `adanvil`
- Version: `0.3.1`
- Transport: `stdio`

## Trust breakdown

How this component scores in each security and reliability category. Every signal is checked automatically from public evidence about the published package, including repeated runs of it in an isolated sandbox, and we only credit what we can confirm. Scores are 0–100 per category. Scoring method: https://verifymcp.io/docs/scoring (what has changed: https://verifymcp.io/docs/scoring/changelog)

Scored 2026-10-17.

- **Supply Chain Security**: 100/100
  - No malware found by supply-chain analysis.
  - No known CVEs affecting this package version or its production dependencies.
  - Runs hatchling.build at install time, a recognised build step with no custom scripting around it.
  - 1 of 31 dependencies flagged as unhealthy.
- **Provenance & Transparency**: 48/100
  - Source repository is publicly reachable at the declared URL.
  - Provenance check failed: no build-provenance attestation is published.
  - Clear OSI-approved license (MIT).
  - Actively maintained (last published 0 days ago).
  - Publishes a security disclosure policy (SECURITY.md).
- **Schema Quality & AI Usability**: 50/100
  - AI-judged instruction clarity (good).
  - Context-footprint check failed: tool/resource definitions use about 11894 tokens (~276/item across 43 items; 43 tools + 0 resources), over budget; trim descriptions and params.
  - Usage-examples check failed: none of the tools include examples.
- **Stability & Change Management**: 10/100
  - Stability observed for 3 of 30 days with no destabilising changes; credit accrues until the full window elapses.
- **Tool Coverage**: 67/100
  - 100% of tools have a non-trivial description (not blank, and not just the tool's name).
  - 0% of tool parameters carry a description.
- **Tool Safety**: 75/100
  - No prompt-injection markers were found in the server instructions, tool names or descriptions we captured.
  - 0 of 1 tool(s) whose name or description implies an irreversible operation declare an MCP destructiveHint annotation; "library_annotate" implies "merge" and declares no destructiveHint at all, which the MCP spec reads as destructive by default.
  - An AI judge read all 44 captured unit(s) of tool text and found none that tries to manipulate the model reading it.
- **Capabilities**: 100/100
  - Implements a current MCP spec version (2026-07-28).

## Install

### How do I install the AdAnvil MCP server?

AdAnvil runs locally as a PyPI package, launched with uvx adanvil. Ready-made configuration for Claude, Cursor, VS Code, Codex and 5 more is on this page, copied from each client's own documentation.

### Claude

```bash
claude mcp add maxbuildog-adanvil -- uvx adanvil
```

### Cursor

```json
{
  "mcpServers": {
    "maxbuildog-adanvil": {
      "command": "uvx",
      "args": [
        "adanvil"
      ]
    }
  }
}
```

### VS Code

```json
{
  "servers": {
    "maxbuildog-adanvil": {
      "command": "uvx",
      "args": [
        "adanvil"
      ]
    }
  }
}
```

### Codex

```bash
codex mcp add maxbuildog-adanvil -- uvx adanvil
```

### opencode

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "maxbuildog-adanvil": {
      "type": "local",
      "command": [
        "uvx",
        "adanvil"
      ],
      "enabled": true
    }
  }
}
```

### OpenClaw

```bash
openclaw mcp add maxbuildog-adanvil --command uvx --arg adanvil
```

### Hermes

```yaml
mcp_servers:
  maxbuildog-adanvil:
    command: "uvx"
    args: ["adanvil"]
```

### Netclaw

```json
{
  "McpServers": {
    "maxbuildog-adanvil": {
      "Transport": "stdio",
      "Command": "uvx",
      "Arguments": [
        "adanvil"
      ]
    }
  }
}
```

### Vellum

```bash
assistant mcp add maxbuildog-adanvil -t stdio -c uvx -a adanvil
```

### Other

```json
{
  "mcpServers": {
    "maxbuildog-adanvil": {
      "command": "uvx",
      "args": [
        "adanvil"
      ]
    }
  }
}
```

## Changelog

Every change recorded for this component, newest first. Days that predate change tracking, or that we cannot explain, say so: "we were watching and nothing happened" and "we were not watching" are different claims.

### 2026-10-17 (score 64, +1)

- [functional improvement] Security disclosure: fail → pass

### 2026-10-16 (score 63, +24)

- [security improvement] Injection markers: unverified → pass
- [security] First check of Judged manipulation: pass
- [security] Tool safety: Tool safety not yet verified: we do not have a sandbox capture of the tool definitions this version of the package serves yet.
- [security] Stability: Stability not yet verified: we do not have a sandbox capture of the MCP schema this version of the package serves yet.
- [functional improvement] Tool coverage: unverified → 100
- [functional improvement] Stability: unverified → 0.07
- [functional improvement] MCP protocol: unverified → pass
- [functional] First check of Tool coverage: 0
- [functional] First check of Schema quality: fail
- [functional] First check of Schema quality: fail
- [functional] First check of Destructive annotations: 0
- [functional] First check of Schema quality: good
- [functional] Capabilities: Protocol version not yet verified: we do not have a sandbox capture of the MCP handshake this version of the package performs yet.
- [functional] Tool coverage: Tool coverage not yet verified: we do not have a sandbox capture of the tool definitions this version of the package serves yet.
- [functional] Schema quality: Schema quality not yet verified: we do not have a sandbox capture of the MCP schema this version of the package serves yet.
- [functional] Package version: 0.3.0 → 0.3.1

### 2026-10-15 (score 39, 0)

- [security] Stability: Stability not yet verified: this package installed, but its server did not start, so we have no schema to compare.
- [security] Tool safety: Tool safety not yet verified: this package installed, but its server did not start, so we have no tool text to scan.
- [functional] Schema quality: Schema quality not yet verified: this package installed, but its server did not start, so we have no schema to assess.
- [functional] Capabilities: Protocol version not yet verified: this package installed, but its server did not start, so we never saw its MCP handshake.
- [functional] Tool coverage: Tool coverage not yet verified: this package installed, but its server did not start, so we have no tool definitions to assess.

### 2026-10-14 (score 39, 0)

- [security improvement] Injection markers: unverified → pass
- [security] First check of Judged manipulation: pass
- [security] Stability: Stability not yet verified: not enough scan history yet (needs a 30-day window).
- [functional improvement] Tool coverage: unverified → 100
- [functional improvement] MCP protocol: unverified → pass
- [functional] First check of Tool coverage: 0
- [functional] First check of Schema quality: fail
- [functional] First check of Schema quality: fail
- [functional] First check of Destructive annotations: 0
- [functional] First check of Schema quality: good
- [functional] Package version: 0.2.0 → 0.3.0

### 2026-10-13 (score 39)

First indexed and scored.

## MCP tools (43)

### `project_init` (~51 tokens)

Create an AdAnvil project skeleton.

Use this first for a new absolute project directory, then inspect the
returned folders before adding media.

Input parameters:

- `name` (string, required)
- `project_dir` (string, required)

### `project_read_text` (~213 tokens)

Read a UTF-8 project text file using the project's safe path rules.

Only AdAnvil's own files can be read: ``adanvil.json``, ``brand.yaml``, ``catalog.json``
and ``brand_brief.md`` at the top of the project, and the files under ``brand/``,
\``reference/``, ``renders/``, ``music/``, ``storyboards/``, ``exports/``, ``cache/``,
\``voice/``, ``sfx/`` and ``footage/`` (extensions .txt, .md, .json, .yaml, .yml, .csv).
Any other file, and any name that starts with a dot, is refused. ``max_bytes`` (default
65536\) is capped at 1 MB; a larger value is clamped and the result ``note`` says so.

Input parameters:

- `max_bytes` (integer)
- `path` (string, required)
- `project_dir` (string, required)

### `brand_onboard` (~142 tokens)

Run brand onboarding step one or two.

Use without answers to discover site facts and questions, then call
with answers to write brand.yaml and brand_brief.md. Use open_questions
to decide what the human must answer. ``facts`` is a compact summary of
the site (title, colours, fonts, headings, prices, guarantee phrases,
a few products); the complete facts are saved in ``facts_file``
(brand/site_facts.json): read it with project_read_text only when you
need more. ``more`` counts what the summary left out.

Input parameters:

- `answers`
- `project_dir` (string, required)
- `url`

### `brand_get` (~126 tokens)

Read the current brand profile and brief.

Use this before writing captions or storyboards so copy follows the
brand defaults and constraints. ``brand.product_versions`` says how to
tell the product versions apart (match it with the ``product_version``
annotation of library entries). ``brand.cta_image`` is the brand's
ready-made call-to-action image, to use as the last segment.
\``emoji_available`` lists the emoji characters of the brand's emoji
folder, or is "all" for twemoji (and empty for none).

Input parameters:

- `project_dir` (string, required)

### `library_index` (~655 tokens)

Index or refresh media under a project-relative folder or file.

Use it before selection. ``source`` is a folder or a single media file.
Up to 8 new or changed files are indexed in this call; more than 8 run
as a detached job and the call returns at once with ``job_id`` (wait
with job_status(job_id, wait_seconds=90), progress is per file). The
result is compact: counts, one line per new or updated entry (id, path,
duration, people, product_visible, ``speech`` when the clip speaks) and
errors. Contact sheets are not sent inline: use library_view, or
library_search with full=true for entries in full.

\``transcribe`` controls the speech of the clips: "auto" (default)
transcribes, with word timestamps, the clips that have an audio stream
that is not near-silent; true transcribes every clip with an audio
stream; false does no speech work. It uses faster-whisper when
installed, else the voice environment (adanvil voice-setup); with
neither, entries get a clear ``speech`` status ("unavailable: ...") and
indexing still succeeds. The model is ADANVIL_SPEECH_MODEL (default
base: fast, good enough to search) and the language is the brand
language, else auto-detected; the brand name, the product name and
\``speech.vocabulary`` of brand.yaml are given to the transcriber. The
speech of each file is saved as soon as that file is transcribed, so
library_search(text=...) works while the job runs (stage
"transcribing"). Words are right about nine times in ten: run
speech_refine on the clips you will use before captioning. Audio files
(.m4a, .wav, .mp3, .aac) are indexed as ``audio`` entries, with their
speech, and can be used as ``audio_from`` sources. A video whose sound
is silent gets the speech status ``silent_audio`` and, when audio files
with a similar name are nearby, ``separate_audio_candidates``. The
result has a ``speech`` count by status.

\``vision`` controls the vision pass: with the vision environment
installed (``adanvil vision-setup`` or the tool ``vision_setup``) each…

Input parameters:

- `project_dir` (string, required)
- `source` (string)
- `transcribe`
- `vision`

### `library_view` (~66 tokens)

Return one catalogue entry and its contact sheet.

Use it when a summary identifies a candidate; LOOK at the image before
choosing a shot. An audio entry has no sheet: only its data is returned.

Input parameters:

- `entry_id` (string, required)
- `project_dir` (string, required)

### `library_annotate` (~400 tokens)

Merge annotations into one catalogue entry.

Parameters: ``project_dir`` (str, absolute project path), ``entry_id``
(str, catalog entry id), ``description`` (str or null), ``tags``
(list[str] or null), ``people`` (list[str] or null), ``product_visible``
(bool or null), ``product_version`` (short free string such as
\``current``, ``old`` or ``v2``, or null), ``emotion`` (list of what the
faces express, a clip can show several: none, neutral, worried,
frustrated, sad, tired, closed, skeptical, shocked, curious, relaxed,
happy, satisfied; the same words as the breakdown, the segments and the
search filter, anything else is refused; or null), ``quality`` (int 1-5 or null),
\``usable`` (list of ``[start, end, note]``, ``[start, end]``, or objects with ``start``,
\``end`` and ``note``; or null), ``notes`` (str or null), ``exclude``
(bool or null), and ``exclude_reason`` (str or null).

Example: ``library_annotate('/abs/project', 'clip-a1b2c3',
tags=['hero'], quality=5, usable=[[1.2, 3.4, 'clear demo']])``.
Use after looking at the contact sheet; search can then filter by tags,
people, product version, emotion, quality and exclusion.

Input parameters:

- `description`
- `emotion`
- `entry_id` (string, required)
- `exclude`
- `exclude_reason`
- `notes`
- `people`
- `product_version`
- `product_visible`
- `project_dir` (string, required)
- `quality`
- `tags`
- `usable`

### `library_search` (~453 tokens)

Search indexed footage using annotations and technical metadata.

Use the returned entries to choose clips without rescanning the
filesystem. Each result is one compact line (id, path, duration,
people, product_visible, version, emotion, quality); ``full=true`` returns
the entries in full. ``product_version`` and ``emotion`` (a list: every
listed emotion must be on the clip) filter on what was set with
library_annotate; without a human ``product_version`` annotation the
guess made from the brand's reference photos is used (a human annotation
always wins). ``auto_tags`` filters on the automatic tags of the vision
pass (English labels such as ``bathroom`` or ``smiling``; common French,
Spanish and German words are understood).

\``query`` searches by SENTENCE across the whole library, with no sheet
to open: ``query="a woman with a sad face touching her jaw"`` (any
language) ranks SHOTS by text-image similarity and returns the best
\``limit`` (default 12, at most 48) as ``results`` (entry, shot, start,
end, time of the best frame, score) plus ONE mosaic image of those frames,
numbered like the list. All the other filters apply first. It needs the
vision environment (``vision_setup``) and entries indexed with it
(``library_index``); ``missing_vision`` counts the entries that have no
vision data yet. Look at the mosaic, then library_view the entry to confirm
before choosing. The score is a centred match probability: 0.5 and more is a
clear match, under 0.1 means nothing in the library looks like it.

Input parameters:

- `auto_tags`
- `emotion`
- `full` (boolean)
- `include_excluded` (boolean)
- `kind`
- `limit` (integer)
- `min_duration`
- `min_quality`
- `people`
- `product_version`
- `product_visible`
- `project_dir` (string, required)
- `query`
- `tags`
- `text`

### `vision_setup` (~174 tokens)

Install the optional vision environment (once, several minutes, about 2.5 GB).

It makes its own Python environment in the AdAnvil cache folder with a
small multilingual image and text embedding model (SigLIP 2, Apache-2.0)
and a face detector (YuNet, MIT), both free for commercial use, and
downloads their weights. It runs on the CPU. Afterwards ``library_index``
also looks at every image and video (shot search by sentence, automatic
tags, product versions, faces for ``focus: "face"``). Without it
everything else works as before. The environment is made from the
default Python of this machine: to build it from another interpreter,
a person runs ``adanvil vision-setup --python <path>`` in a terminal.

### `speech_view` (~543 tokens)

Read the speech of one indexed clip and get safe cuts for it.

Pass ``entry_id`` (from library_index or library_search) or ``source`` (the
project-relative path of a video or audio file). Needs the clip to be indexed with
speech (library_index transcribes clips with sound). The output is compact:
\``phrases`` (split on pauses longer than ``max_pause`` seconds and on sentence
punctuation), each with ``start``, ``end``, ``text`` and ``flags`` (``restart``: the
next phrase repeats it, a retake; ``filler``: it holds a hesitation; ``uncertain``:
the transcriber doubted a word, so it may be misheard); ``fillers`` (euh, heu, hum,
bah, ben, um, uh, er, and genre, du coup, like when said alone between pauses; the
lists can be replaced in brand.yaml under ``speech.fillers`` and
\``speech.isolated_fillers``); ``restarts`` (a phrase that repeats the start of the
next one: a retake inside the file); and ``clean_segments``: clip segments
\``{source, in, out}`` ready to paste into a storyboard, which keep the speech and drop
pauses over ``max_pause`` and the fillers (0.06 s kept before a word, 0.10 s after,
never cutting inside a word). Per-word data (start, end, text and ``filler`` for each
word) only comes with ``words=true``. At most ``max_phrases`` phrases are returned
(default 60) from ``first_phrase``: when there are more, ``note`` and
\``next_first_phrase`` tell how to get the next ones, and the other lists are those of
the phrases shown. With ``drop_restarts`` true the restart phrases are cut out of the
clean segments too (they are only flagged by default, because a deliberate repetition
looks the same). ``model`` says which transcription model made the words: if a clip
is used in the ad, run speech_refine on it first. A clip whose sound is silent says
\``speech_status: silent_audio`` and lists ``separate_audio_candidates``.

Input parameters:

- `drop_restarts` (boolean)
- `entry_id`
- `first_phrase` (integer)
- `max_pause` (number)
- `max_phrases` (integer)
- `project_dir` (string, required)
- `source`
- `words` (boolean)

### `speech_refine` (~329 tokens)

Transcribe chosen clips again with a stronger model and replace their words.

Indexing transcribes fast with a small model, so about one key word per line can be
misheard (names, numbers, rare words). Call this on the clips you will use in the ad
(before cutting and captioning): ``sources`` lists project paths of indexed clips (a
file, or a folder for every clip below it) or catalogue entry ids. It starts a
detached job (wait with job_status(job_id, wait_seconds=90); progress counts clips)
that transcribes them with ``model`` (one of tiny, base, small, medium, large-v2,
large-v3, large-v3-turbo; default medium: slower, much more accurate; a medium model
needs about 2 GB of memory) and the brand vocabulary (brand name,
product name and ``speech.vocabulary`` of brand.yaml), then replaces the words of
each clip in place (``speech_view`` reports the ``model``). A clip that fails keeps
its previous words. The language is the brand's, else detected, unless ``language``
is given. The result of the finished job is ``refine`` in job_status (refined ids,
skipped clips with the reason, errors). A word that is still wrong afterwards can be
shown correctly with ``speech_captions.corrections`` (close spellings only).

Input parameters:

- `language`
- `model` (string)
- `project_dir` (string, required)
- `sources` (array, required)

### `speech_takes` (~263 tokens)

Pick the best take of each line across several recordings of a script.

Pass ``source`` (a project folder such as footage/hooks, or one file) and/or
\``entry_ids``: the clips to compare, for example HOOK1.mov, HOOK1.1.mov and
HOOK1.2.mov. Phrases that say the same line (normalised text similarity of at
least 0.6, inside one file or across files) form a line; each take is ranked on
fluency (no filler, no restart), a complete ending (last word not cut), a speech
rate in the natural band (about 1.9 to 4.2 words per second) and the audio level.
Returns ``lines``: for each, the ``best`` take (entry_id, source, start, end,
text, score, ``why`` and ready-to-paste ``clean_segments``) and the ``others``
with the ``issues`` that put them behind. ``skipped`` lists clips without a
transcript (run library_index with transcribe=true on them).

Input parameters:

- `entry_ids`
- `max_pause` (number)
- `project_dir` (string, required)
- `source`

### `reference_decode` (~240 tokens)

Decode a reference video into shots, pacing, text, speech and sheets.

\``transcribe`` is true, false or "auto" (default: transcribe when the
reference has audible sound and a transcriber is available). Returns
the JSON, the overview sheet, then up to 6 shot sheets (one row per
shot, three frames each). LOOK at every shot sheet and read the
on-screen text, then call reference_breakdown with the filled
breakdown_template before choosing footage. The JSON is compact: the
per-word transcript and rms_per_second stay in reference.json (see
\``files``); each shot has its ``spoken`` text and ``transcript_text``
holds the whole transcript. ``breakdown_vocabularies`` lists the
allowed type, scale, role, motion, emotion, text_speaker and text_device
values and how to read who speaks in the on-screen text; ``music_profile`` is
the tempo and energy of the reference soundtrack (voice included).

Input parameters:

- `language`
- `project_dir` (string, required)
- `transcribe`
- `video` (string, required)

### `reference_breakdown` (~302 tokens)

Save the agent's shot-by-shot breakdown of a decoded reference.

\``reference`` is the decode folder (reference/<slug>) or its
reference.json. ``shots`` follows breakdown_template: one entry per
shot, or one entry for consecutive shots (``"shots": [4, 5]``); every
decoded shot must be covered exactly once. Controlled values: type,
scale, role, motion, emotion, text_speaker,
text_device (errors list the allowed ones); ``action`` is free short
text: what the person does (for example "wipes the counter and sighs"). For every
shot note what the person feels (``emotion``) and does, then pick a
library shot with the same emotion and the same kind of action.
text_speaker and text_device say who speaks in the on-screen text (an
emoji or name, ":" and quotes is a character talking); subject is a
short id (person_a, product, diagram), on_screen_text is read verbatim,
replacement is a library entry id or "missing: ...". Returns type
demand, scale mix, role sequence, repeated patterns, people, text
shots, text_devices, dialogue, emotion_by_role (the emotions used for each
role), pacing and missing shots. Use it before library_overview.

Input parameters:

- `project_dir` (string, required)
- `reference` (string, required)
- `shots` (array, required)

### `library_overview` (~159 tokens)

Overview of the footage folders without indexing, plus a mosaic.

Lists folders up to ``depth`` with file, video and image counts, total
video seconds and first file names. With ``mosaic``, returns a labelled
thumbnail mosaic of the media under ``path`` (48 per page, 6 columns);
\``pages`` tells how many, call again with ``page`` to see the next
ones. Run it on the whole footage
tree (B-roll folders included) to find each shot type the reference
breakdown needs, before choosing shots.

Input parameters:

- `depth` (integer)
- `mosaic` (boolean)
- `page` (integer)
- `path` (string)
- `project_dir` (string, required)

### `frame_view` (~213 tokens)

Extract one full-resolution frame from a project video or image.

Give the time ``t`` (seconds) or the frame number ``frame`` (video only, counted from
0). Returns the frame ON SCREEN at ``t`` (``floor(t * fps)``): at 2.87 s of a 30 fps
video that is the frame starting at 2.867 s, not the next one at 2.900 s. A ``t``
within 1 ms of a frame boundary is that frame (10.9333 s at 30 fps is frame 328), so a
time printed to four decimals lands on the right frame. ``frame_t`` is the frame's exact
start time and ``frame`` its number (video only). Use it to inspect the exact moment
behind a timecode; the saved frame is under cache/frames/.

Input parameters:

- `frame`
- `project_dir` (string, required)
- `source` (string, required)
- `t`

### `timeline_export` (~435 tokens)

Export a storyboard as an editable timeline for DaVinci Resolve or Premiere Pro.

Writes ``exports/<name>/`` with ``<name>.otio`` (OpenTimelineIO: video tracks, speed as
a time warp, stills, captions as markers on V1, overlay pictures, voice, music and one
audio track per sound effect), ``<name>.xml`` (Final Cut Pro 7 XML, read by Premiere
Pro and Resolve, with the real rate, size and duration of each source, speed as Time
Remap, music and sound effect levels and fades), ``<name>.edl`` (CMX 3600, video cuts
only, the fallback), ``<name>.srt`` (every caption at its final time), ``overlays/``
(each caption, sticker and card as a full-canvas transparent PNG named after its
storyboard overlay index, with its place in ``overlays.json``) and ``README.txt``.
\``formats`` is any of ``otio``, ``fcp7xml``, ``edl``, ``srt`` (all by default). Media
paths are absolute, as the project sees them; ``relative`` writes
\``exports/<name>-relative/`` with paths relative to the folder (and a ``relink.txt``),
\``copy_media`` writes ``exports/<name>-portable/`` with the used sources copied into
\``media/``; ``media_root`` is the absolute folder a relative export will live in. A
folder that holds an export of another media mode is only replaced with
\``overwrite=true``. The result lists ``warnings`` (what is approximated) and
\``not_exported`` (what no editor format here carries: grading, reframing, pop
animations, split layout, loudness, the EDL audio).

Input parameters:

- `copy_media` (boolean)
- `formats`
- `media_root`
- `overwrite` (boolean)
- `project_dir` (string, required)
- `relative` (boolean)
- `storyboard` (required)

### `recipe_list` (~37 tokens)

List available ad recipes with purpose and duration.

Use this to shortlist a shape, then read its checklist with recipe_get
before writing a storyboard.

### `recipe_get` (~35 tokens)

Return one complete recipe, including rules and example.

Use its checklist as the storyboard acceptance criteria.

Input parameters:

- `recipe_id` (string, required)

### `editing_guide` (~35 tokens)

Return the editing doctrine for footage, copy, rendering and QC.

Read it before the first storyboard and whenever a rule is unclear.

### `storyboard_schema` (~30 tokens)

Return the JSON Schema for Storyboard v1.

Use it to construct agent-authored storyboards before validation.

### `storyboard_save` (~137 tokens)

Save a storyboard in the project as ``storyboards/<name>.json`` and return its path.

Send the storyboard object once, then pass the returned path
(``"storyboards/<name>.json"``) as ``storyboard`` to ``storyboard_validate``,
\``render_start`` and ``caption_preview`` instead of repeating the whole object
(useful on a remote server). Saving again under the same name replaces the file.
The object is stored as sent, not validated: ``storyboard_validate`` checks it.

Input parameters:

- `name` (string, required)
- `project_dir` (string, required)
- `storyboard` (object, required)

### `storyboard_patch` (~233 tokens)

Edit a saved storyboard in place: ``changes`` is a list of ``{path, value}``.

A path walks the JSON with dots and indexes: ``segments.3.out`` (``segments[3].out``
works too). Setting a missing key of an object adds it; the index just past the end of
a list, or ``-``, appends; ``{"path": "overlays.1", "remove": true}`` deletes. The
changes apply in order to a copy: if one is wrong, nothing is saved and the error
names it. When the changes rename the storyboard (``name``), its ``output.path`` follows
if it was the default of the old name (a path set by hand stays). The file
\``storyboards/<name>.json`` is rewritten and the result lists the
schema problems left (``problems``, empty when the storyboard is valid; run
\``storyboard_validate`` for the full check).

Input parameters:

- `changes` (array, required)
- `name` (string, required)
- `project_dir` (string, required)

### `caption_preview` (~357 tokens)

Render a caption or comment over a real 9:16 frame.

Use it before rendering to inspect placement; the JSON reports bbox,
fitted size, overflow and final lines. ``emoji`` takes the storyboard
\``emoji`` values (twemoji, none or a project folder) so the preview
matches the render; ``emoji_missing`` lists emoji absent from a folder.
For a ``voice_captions`` overlay pass ``manifest``: the ``audio.voice``
path of your storyboard (voice/<name>/manifest.json); ``expanded``
lists the resulting captions once. A ``speech_captions`` overlay is
built from the speech of the clips: pass ``chunk_index`` to pick the
caption to draw and ``storyboard`` (a project path such as
\``storyboards/<name>.json`` or the object) so the timeline is the one of
your ad; without it the captions are those of ``source`` played whole.
\``expanded`` lists them all. An animated caption (``animation``
\``karaoke``, ``word`` or ``pop``) is drawn in one of its states, to check
where the highlight lands: put ``chunk_index`` and ``word_index`` in the
overlay object to pick the chunk and the lit word, or leave them out to
get the state on screen at timeline time ``t``. The result names the
\``word_index``, the ``window`` of the state and how many ``states`` the
chunk has.

Input parameters:

- `emoji`
- `overlay` (object, required)
- `project_dir` (string, required)
- `source` (string, required)
- `storyboard`
- `t` (number, required)

### `end_card` (~412 tokens)

Compose a clean end card image from a real product photo.

Use it only when the brand has no ready-made call-to-action image: when
it has one (``brand.cta_image``, a ``cta/`` folder or ``brand/``),
use that image as the last segment (``image``, ``zoom_in``, about 3 s)
instead. It writes
\``brand/endcards/<name>.png`` (1080x1920 unless ``spec.format`` says
otherwise) and returns the path, the layout boxes and the image: LOOK at
it. ``spec``: ``product_image`` (project path of a photo or a video, with
\``t`` for the frame, optional ``crop`` {x,y,w,h} in the source's display
pixels, before scaling: on a 1920x1080 frame ``{"x": 600, "y": 100,
"w": 700, "h": 700}`` keeps a 700 px square; the result returns
\``source_size`` [w, h] to measure it), ``pill``, ``title``
(a string or runs ``{text, accent}``), ``subline``, ``badges`` (up to 3,
\``{text, icon: check|star|none}``), ``button`` (``{text, arrow:
down|right|none}``), colours ``accent``, ``text``, ``muted``,
\``background`` (two colours), ``family``, ``emoji``. Colours default to
the brand's. Banned words of the brand on any text are an error. Use the
PNG as the last segment: ``type: image``, ``motion: zoom_in``, about 3 s,
label ``end_card``, no subtitles on it.

Input parameters:

- `name` (string, required)
- `project_dir` (string, required)
- `spec` (object, required)

### `storyboard_validate` (~217 tokens)

Validate a storyboard object or project-relative JSON file.

\``storyboard`` is the object, or the path saved by ``storyboard_save``
(``"storyboards/<name>.json"``). Every schema error is reported at once.

The check covers Storyboard v1, its files, brand and recipe. Fix every
error path before render_start; warnings are questions to review. With
\``reference`` set, ``ref_*`` warnings and a ``reference_comparison``
block compare the edit with the reference breakdown. With
\``score_check=true`` it also runs the quick pre-check of ``ad_score``
that needs no pixels, as warnings prefixed ``score_`` (reading time and
line length, product too early, call to action, platform safe zones of
the overlays); ``summary.score_precheck`` lists what passed and what
could not be checked.

Input parameters:

- `project_dir` (string, required)
- `recipe_id`
- `score_check` (boolean)
- `storyboard` (required)

### `render_start` (~80 tokens)

Validate and queue a background render.

\``storyboard`` is the object or a saved path (``"storyboards/<name>.json"``).

Use preview while iterating and final after QC. A rejected storyboard
returns validation errors and creates no job.

Input parameters:

- `project_dir` (string, required)
- `quality` (string)
- `storyboard` (required)

### `variants_build` (~636 tokens)

Build the matrix of variants of one storyboard: hooks x bodies x calls to action.

\``base`` is a storyboard (object or ``"storyboards/<name>.json"``). ``variants`` is
\``{axis: [{id, changes}]}`` for up to 3 axes (for example ``hook``, ``body``, ``cta``).
\``changes`` is a list applied in order, each a ``storyboard_patch`` change (``{path,
value}`` or ``{path, remove: true}``) or a SLOT REPLACEMENT ``{replace_label: "hook",
segments: [...], overlays: [...]}``: it swaps every segment of that label (they must be
next to each other) and shifts everything after it by the difference of duration
(overlays, sounds and word times follow). The new ``overlays`` are timed from the start
of the slot; the overlays that were on the replaced segments go away. A voice-over
(``audio.voice``) laid on the timeline must still match: a change of duration while one
is set is refused with the reason, as is an overlay that only partly overlaps the slot.
An option with no ``changes`` (or ``[]``) is the control: the base unchanged. Timed
sounds follow the slot: an ``sfx`` overlay after it shifts by the difference of duration,
one inside it goes away when the new slot brings its own sounds (else it keeps its
offset); each variant lists what moved under ``moved``. All the errors of all the
variants come back at once (variant, path, message).

Makes the cartesian product (at most 24, each validated), saves each as
\``storyboards/<name>.json`` (``name_pattern`` uses ``{base}`` and the axis names; the
default falls back to the axis ids in order when an axis is not hook or cta) and returns
the list with the path, ids, duration and warnings of each, plus a contact sheet: the
first 3 s (top row) and the last 3 s (bottom row, the call to action) of every variant
(LOOK at it, a hook that does not read at that size will not read in the feed). A base
that is itself a variant keeps its ids in the new ones (the manifest keeps
\``hook=h2;cta=ca``). Then ``render_batch`` renders them with a shared cache.
Vary…

Input parameters:

- `base` (required)
- `contact_sheet` (boolean)
- `name_pattern` (string)
- `overwrite` (boolean)
- `project_dir` (string, required)
- `variants` (object, required)

### `render_batch` (~398 tokens)

Queue ONE background job that renders several storyboards in order.

\``quality``: ``final``, ``preview`` or ``draft`` (an alias of ``preview``: half-size
\``*.preview.mp4`` files; the report and the manifest say ``preview``, and the manifest
has a ``quality`` column because a preview scores lower than its final).

\``storyboards``: objects or saved paths (typically the variants of
\``variants_build``). The segment cache is shared, so a segment used by several variants
(the body, the end card) is built once: the first render is the slow one. ``formats``
(any of ``"9:16"``, ``"4:5"``, ``"1:1"``) renders each storyboard in those ratios; when
omitted each storyboard's own ``formats`` field is used, else it renders as it is. Files
go to ``renders/<batch>/`` (``batch`` defaults to ``<first name>_batch``) named
\``<name>[_9x16|_4x5|_1x1].mp4``, and ``renders/<batch>/manifest.csv`` lists them for an
ads manager (file, variant ids, format, duration, score, hook text, CTA text).
\``score`` runs ``ad_score`` on each final render for the manifest (with the safe zones of
its placement). ``job_status`` shows the progress per render, each output as it
finishes, and at the end ``total_seconds``, ``cache_builds`` and the manifest path. A
storyboard that does not validate rejects the whole batch with its errors and no job.

Input parameters:

- `batch`
- `formats`
- `project_dir` (string, required)
- `quality` (string)
- `score` (boolean)
- `storyboards` (array, required)

### `render_formats` (~308 tokens)

Render one storyboard in several formats: ``9:16``, ``4:5``, ``1:1``.

\``formats`` defaults to the storyboard's own ``formats`` field, else all three. One
detached job (see ``render_batch``), one file per format: ``<name>_9x16.mp4``,
\``<name>_4x5.mp4``, ``<name>_1x1.mp4`` in ``renders/<batch>/`` (``batch`` defaults to
\``<name>_formats``). Each is the same ad laid out for its placement: the footage is
re-cropped (face-aware with ``focus: "face"``, else by ``focus``); a still or an end card
that does not fit is shown whole over its blurred copy; captions, stickers and images
keep their place by bands (the part the platform leaves free is mapped onto the free part
of the new frame, 4:5 and 1:1 being the Feed) and are pushed back inside when a block
would leave it. ``ad_score`` uses the safe zones of the matching placement (``feed`` for
4:5 and 1:1). LOOK at each format's contact sheet before delivery.

Input parameters:

- `batch`
- `formats`
- `project_dir` (string, required)
- `quality` (string)
- `score` (boolean)
- `storyboard` (required)

### `voice_generate` (~241 tokens)

Queue a checked voice-over generation job.

Poll job_status until it is done, then use each manifest line's
\``start`` and ``duration`` to place shots. If a line is ``suspect``,
regenerate it with slightly different wording or accept it when the
transcript differs only by punctuation; agents cannot listen to audio.
\``unmatched_words`` lists the key words (4+ letters) missing from the
transcript: two or more make the line suspect, so reword them.
A line is a string (said and shown) or ``{"say": ..., "show": ...}``
when captions must display something other than the spoken words
(say "forty-nine dollars ninety", show "$49.90"). Calling again with an
existing ``name`` replaces that voice (unchanged lines come from the
cache); the job summary then has ``replaced: true``. ``language`` in the
spec is optional: without it the ``language`` of brand.yaml is used,
else ``en``.

Input parameters:

- `name` (string, required)
- `project_dir` (string, required)
- `spec` (object, required)

### `sfx_list` (~317 tokens)

List the sound effects of the project's ``sfx/`` folder, and build the pack.

\``fetch=true`` first builds the missing sounds of the pack (about 18: whoosh short and
long, swoosh, swipe, pop, click, tap, ding, chime, notification, cash register,
camera shutter, record scratch, typing, riser 1 s and 2 s, impact, boom). Each is
downloaded from a pinned CC0 source (checked by SHA-1) or synthesised in code, trimmed
and set to one level, with the source and licence in ``sfx/LICENCES.txt``. A download
that fails is synthesised instead (``offline=true`` synthesises all of them). The
files you drop in ``sfx/`` yourself are listed too. Each sound returns its ``name``,
\``path`` (the ``source`` of an ``sfx`` overlay), ``duration`` and ``origin``. Use a
sound in an ``sfx`` overlay (``{"type": "sfx", "source": "sfx/pop.wav", "at": 3.2,
"volume": 0.8}``) or by name in ``audio.sfx_auto``. A riser is placed so that its end
meets the moment it builds towards: ``at`` = that moment minus its duration.

Input parameters:

- `fetch` (boolean)
- `offline` (boolean)
- `project_dir` (string, required)

### `music_beats` (~197 tokens)

Beat times of a music track, as it will play in the ad.

\``path`` is a project file (audio, or a video whose soundtrack is the music). ``start``
is the storyboard's ``audio.start``: the times are in seconds from the start of the
ad, so beat 0 of the result is the first beat heard. They come from the onset envelope
and the estimated tempo, on a regular grid whose phase is aligned to the strongest
onsets (within a few milliseconds on a steady beat). ``duration`` limits the analysis
(default the first 120 s). A track without a steady beat (a piano, a pad) returns no
beats. Cut on these times for a music-led ad, or let ``storyboard_snap`` do it.

Input parameters:

- `duration`
- `path` (string, required)
- `project_dir` (string, required)
- `start` (number)

### `storyboard_snap` (~346 tokens)

Move the cuts of a storyboard onto the beats of its music.

\``storyboard`` is the object or a saved path. Each cut within ``max_shift`` seconds
(default 0.2) of a beat of ``audio.music`` (from ``audio.start``) moves to it: the
\``out`` of the earlier clip (or the ``duration`` of an image) changes, ``in`` points and
segment order never do, no segment ends up under 0.5 s (or shorter than it was), the
other cuts and the total duration stay, and the captions, images and sounds that
started on a moved cut move with it. With ``audio.clips`` the cuts of talking segments
never move. The result lists ``moves``, the cuts ``kept`` with the reason, and how many
cuts are on a beat before and after. It returns the new ``storyboard``, or with
\``save_as`` (a name) writes ``storyboards/<name>.json`` and returns its path instead.
The snapped storyboard has its own identity so a render never overwrites the original:
with ``save_as`` its ``name`` is ``save_as``, without it the old name plus ``_snapped``,
and ``output.path`` is ``renders/<that name>.mp4`` (the quality is kept).
Run ``storyboard_validate`` afterwards: its ``beat_alignment`` info shows the share of
cuts on a beat.

Input parameters:

- `max_shift` (number)
- `project_dir` (string, required)
- `save_as`
- `storyboard` (required)
- `to` (string)

### `music_profile` (~168 tokens)

Tempo, loudness and energy of the music tracks, to match a reference.

Without ``path``, profiles every audio file of ``music/``; with ``path``
(project-relative), one file, which may be a video such as the reference
(its soundtrack mixes voice and music). Each track returns its duration,
integrated loudness (LUFS), estimated tempo (``tempo_bpm``), ``energy``
(onsets per second and loudness range in LU) and ``feel``: calm, medium or
energetic (thresholds in ``feel_thresholds``). Pick the library track whose
profile is closest to the reference's: a calm track under a fast reference
feels flat.

Input parameters:

- `path`
- `project_dir` (string, required)

### `music_search` (~409 tokens)

Find music whose licence lets an ad use it (commercial use and modification).

Searches Openverse (no key; ``source="openverse"``), Freesound (``"freesound"``, only with
the ``FREESOUND_API_KEY`` environment variable) or both (``"all"``; without the key the
answer says so and still lists Openverse). Kept licences: CC0, PDM, CC BY, CC BY-SA;
NC and ND are dropped. Each track returns ``source``, ``id``, ``title``, ``author``,
\``licence``, ``duration`` (s), ``source_url``, ``preview_url`` (listen before fetching)
and ``attribution_required`` (with the ``attribution_text`` to put in the ad's
description). ``mood`` adds words to the query ("calm", "upbeat"). ``min_duration`` and
\``max_duration`` filter in seconds. ``fit_to="storyboard"`` with ``storyboard`` (a
project path such as ``storyboards/summer.json``) ranks for that ad: tracks at least as
long as the ad first, then the wanted feel (``audio.music_feel``, or the ``feel`` argument:
calm, medium, energetic) against ``fit.feel_guess``, a guess from the track's OWN title,
tags and genres (``unknown`` when they say nothing; never copied from the wanted feel).
The real feel, tempo and loudness come from ``music_fetch``: trust that, not the guess.
Fetch the best ones, listen, and choose.

Input parameters:

- `feel`
- `fit_to`
- `limit` (integer)
- `max_duration`
- `min_duration`
- `mood`
- `project_dir` (string, required)
- `query` (string, required)
- `source` (string)
- `storyboard`

### `music_fetch` (~265 tokens)

Download a track from ``music_search`` into ``music/``, with its licence.

\``source`` and ``id`` come from the search result. Writes ``music/<file>``, the record
\``music/<file>.licence.json`` (source, id, URL, title, author, licence, licence URL,
attribution text, fetch date) and a line in ``music/LICENCES.txt``, then profiles the
track: the answer has ``path`` (the ``audio.music`` value), ``duration``, ``tempo_bpm``,
\``feel`` and ``loudness_lufs``. A licence that is not CC0, PDM, CC BY or CC BY-SA is
refused. An embedded cover picture is removed from the stored file. The render and
\``ad_score`` read the record (the sidecar, else the track's line in ``LICENCES.txt``):
\``music_licence_unknown`` when a track has neither,
\``music_attribution_required`` (with the exact text) when it needs one.
Freesound serves its high-quality MP3 preview (the original needs a login).

Input parameters:

- `id` (string, required)
- `project_dir` (string, required)
- `source` (string, required)

### `music_generate` (~193 tokens)

Make an instrumental track locally (optional ACE-Step environment), as a job.

Needs ``adanvil music-setup`` (about 3 GB of packages, then 8 GB of weights at the first
run). ``prompt`` is a list of tags ("upbeat acoustic guitar, warm, 110 bpm,
instrumental"); ``duration`` 10 to 120 s. On a CPU a 30 s track takes several minutes:
poll ``job_status``; its ``music`` field has the ``path`` (the ``audio.music`` value),
tempo, feel and loudness. The model and its weights are Apache-2.0: no attribution; the
licence record is written next to the file like for a fetched track.

Input parameters:

- `duration` (number)
- `project_dir` (string, required)
- `prompt` (string, required)
- `seed`
- `steps` (integer)

### `voice_view` (~92 tokens)

Read a voice manifest and suggest final-timeline shot boundaries.

\``offset`` is the intended voice start on the final video timeline.
Suspect lines should be regenerated with slightly different wording
or accepted only when the transcript difference is punctuation.
Each line's ``unmatched_words`` lists key words the transcript lacks.

Input parameters:

- `name` (string, required)
- `offset` (number)
- `project_dir` (string, required)

### `job_status` (~259 tokens)

Read a queued, running, completed or failed render, batch, voice, index or refine job.

With ``wait_seconds`` greater than zero, wait in this call (up to 600
seconds) instead of polling from the client; if the connection drops,
just call again, a restarted server still finds the job. Pass
\``project_dir`` when it says "job not found". Completed index jobs
carry the compact index summary; completed speech_refine jobs carry
\``refine`` (refined clips, skipped clips with the reason, errors). Completed render jobs
include the persisted RenderReport JSON; batch jobs (``render_batch``,
\``render_formats``) carry ``batch``: one entry per render (status, file, format,
duration, score, seconds, error) as they finish, then ``total_seconds``,
\``cache_builds`` and the ``manifest`` path; completed voice jobs include
each manifest line and its warnings. If a line is ``suspect``, an
agent cannot listen to it: regenerate with slightly different wording,
or accept it when the transcript differs only in punctuation.

Input parameters:

- `job_id` (string, required)
- `project_dir`
- `wait_seconds` (number)

### `qc_run` (~190 tokens)

Run technical QC on a rendered project-relative video.

Pass a job id or saved report path when available. Inspect the returned
sheet, the end card frame (``end.jpg``) and up to four overlay
crops before delivery. ``storyboard`` is the EFFECTIVE storyboard of that render (the
one of its format after the re-layout: ``renders/<batch>/_storyboards/<name>.json``,
found by itself next to a batch render): the overlay crops are then taken where its
overlays really are (a caption moved by ``render_formats`` is not cut in half), and
\``caption_on_face`` checks the rendered frames for a caption on a face (needs the
vision environment; every format, 9:16 too).

Input parameters:

- `project_dir` (string, required)
- `render_report`
- `storyboard`
- `video` (string, required)

### `ad_score` (~450 tokens)

Score a rendered ad out of 100 for creative quality, with findings to fix.

\``qc_run`` says the file is technically correct; this says whether the ad
is any good: a first second that holds attention (motion or a cut, a face
or the product, a text within 0.5 s, audio at once), a pacing without a
dip in the middle, captions that can be read (0.25 s per word + 0.6 s,
7 words a line at most, contrast of 3:1 or more on the real frames), the
problem before the product, a call to action and an end card in the last
4 s, overlays out of the platform interface (``platforms``: any of
\``tiktok``, ``reels``, ``stories``, ``feed``; the first three by default, ``feed``
for a 4:5 or 1:1 video) and a clear
loudness, a voice 6 dB over the music and no silence over 0.7 s.
Pass the ``storyboard`` it was rendered from (object or saved path) for
exact timings, words, roles and overlays; without it the checks that
need them are listed in ``skipped``. The result has ``score``,
\``sub_scores`` (hook, pacing, reading, story, safe_zones, audio),
\``findings`` sorted by ``impact`` (each with ``time``, ``severity``,
\``message`` and ``fix``), ``passed``, ``skipped``, the pixel boxes of the
\``safe_zones`` and the ``thresholds`` (guidelines, not rules). Images:
\``safe_zones_preview.jpg`` (the zones of each platform over a mid-ad
frame, red where an overlay falls inside) and the frame of each top
finding: LOOK at them. Run it before delivery, fix the top findings,
render again, and aim for 80 or more.

Input parameters:

- `platforms`
- `project_dir` (string, required)
- `storyboard`
- `video` (string, required)

### `compare_pair` (~302 tokens)

Measure how different a before and an after look, to rank candidate pairs.

\``before`` and ``after`` are project files: images, or clips (the frame
at ``before_t`` / ``after_t`` seconds, the middle of the clip by default).
\``before_crop`` / ``after_crop`` (``{x, y, w, h}`` in source pixels) cut
one half out of a side-by-side composite, the same crop a ``reveal`` side
takes. The comparison is made on the face when a skin-region heuristic
finds one in both pictures (so a different distance still lines up), else
on the whole frame after aligning sizes. ``visible_difference`` is 0 to
100 (under 12 not visible, under 30 subtle, under 55 visible, above that
obvious at a glance); ``region`` says what was compared. The preview image
shows the two pictures side by side: LOOK at it. Compare every available
pair (photos, composites split with ``crop``, frames of before/after
clips) and use the one whose difference is obvious; a still pair of the
same pose beats moving clips for a reveal.

Input parameters:

- `after` (string, required)
- `after_crop`
- `after_t`
- `before` (string, required)
- `before_crop`
- `before_t`
- `project_dir` (string, required)

## Diagnostics

Captured diagnostic sections: Provenance, Install scripts, Dependencies. The full working is on the page: https://verifymcp.io/servers/maxbuildog-adanvil/adanvil#diagnostics

## Score history

- 2026-10-17: 64
- 2026-10-16: 63
- 2026-10-15: 39
- 2026-10-14: 39
- 2026-10-13: 39

## Common questions

### What is the AdAnvil MCP server?

AdAnvil is an MCP server listed in the public MCP registry as io.github.maxbuildog/adanvil. Lets an AI agent edit vertical 9:16 video ads from your footage, a reference ad or an idea. This page covers its PyPI package (adanvil).

### Is the AdAnvil MCP server safe to use?

AdAnvil scores 64 out of 100 on VerifyMCP. We found no known CVEs affecting it as of 17 October 2026. That is a record of what we were able to check automatically, not an endorsement. The category breakdown on this page shows every signal behind the number, including the ones we could not confirm.

### What tools does the AdAnvil MCP server expose?

AdAnvil exposes 43 tools: project_init, project_read_text, brand_onboard, brand_get, library_index, and 38 more. Their descriptions and schemas cost roughly 11,108 tokens of context every time the server is loaded.

### Is the AdAnvil MCP server still maintained?

AdAnvil is still listed as active in the MCP registry. We last reached this channel on 17 October 2026. Those dates come from our own scans of the registry and the channel itself, not from anything the publisher announced.

### What licence is the AdAnvil MCP server under?

AdAnvil declares the MIT licence, which is OSI-approved. That covers the source only, and says nothing about the cost of any service it calls.

## Links

- PyPI project: https://pypi.org/project/adanvil/
- Socket report: https://socket.dev/pypi/package/adanvil
- Repository: https://github.com/maxbuildog/adanvil
- Changelog RSS feed: https://verifymcp.io/servers/maxbuildog-adanvil/adanvil.xml
- Changelog JSON feed: https://verifymcp.io/servers/maxbuildog-adanvil/adanvil.json
- HTML version of this page: https://verifymcp.io/servers/maxbuildog-adanvil/adanvil
