ignis-mcp
v1.17.6
Published
MCP server exposing the Ignis commerce-growth platform (funnel + channel analytics, commerce reads over orders/returns/inventory/loyalty/creators, briefs, AI image/video generation + editing, asset library, Quick Cuts video projects) to Claude Code / Clau
Readme
Ignis MCP Server
Lets Claude Code / Claude Desktop drive the Ignis commerce-growth platform directly: read the store's funnel and channel performance, read its commerce operations (orders, returns, inventory, loyalty, creators), create briefs, generate images and video (AI Studio), and build Quick Cuts video projects. It is a thin wrapper over the platform API, all authorization and tenant isolation live in the API, keyed off an API key you generate. The key is team-scoped, so the server acts only within your own workspace (e.g. NESW is one such team).
What it exposes
| Tool | What it does |
|------|--------------|
| whoami | Verify the key and show the team it is scoped to. Run this first. |
| list_briefs | List content briefs (most recent first). |
| get_brief | Get one brief in full (prompt + image URL). |
| create_brief | Create a new brief. |
| generate_image | Generate an image for a brief. delivery_mode: "batch" is ~55% cheaper and resolves in minutes. The size bands are per model (1K/2K on the cheap tier, 1K/2K/4K on the premium one, 0.5K/1K/2K/4K on the middle one); a size a model does not have is folded onto its default and billed rather than refused, so it is refused here first. |
| studio_shot | Render ONE studio product shot from a brief (angle, crop, pose, styling, and, only when the person asks, how it is worn; never what a garment looks like). The platform's prompt writer upgrades it using the merchant's studio config as style (model, set, camera, light, realism, every garment's design reproduced exactly as in its reference), points at the product references by number and puts the model's approved photo last. Shot codes 01-08 (front close-up, back knee-up, front knee-up, three-quarter, back close-up, flat lay, neck-tag macro, back-design close-up) are optional shortcuts; view picks the lead photo, flat_lay makes a one-product flat lay. Products are exactly what the call names: no default bottoms or cap. Every call creates a NEW brief, so the history and the exact prompt stay readable. look names the merchant's set (for example product on beige, bundle on a coloured wall); with a photo of the real set the prompt points at it instead of describing the backdrop. |
| brand_asset_folders | The merchant's own folders for brand assets: list (with counts), create, rename, delete (the assets are kept, in no folder) and move an asset in or out. Folders are addressed by name; none is ever created unless asked. list_brand_assets shows each asset's folder and filters by one; upload_brand_asset can put a new asset straight into one. |
| product_references / add_product_reference / remove_product_reference | A product's reference photos by view (front, back, side, detail, flat, tag, macro) in the merchant's order (sort, then the order added): studio_shot uses the first of each view by itself. Add one from the asset library by id (nothing re-uploaded), from a local file (uploaded through the reference lane) or from a link on platform storage; a Google Drive link is refused. |
| studio_shot_status | Read one studio shot back: the prompt that was sent, the references in the order the prompt calls them by, the render's state and the quality report. |
| studio_models | The reference sets this store can shoot with and where each is in casting. ready is true only for a model with an approved photo and an identity line. |
| studio_config | The merchant's studio wording, the eight shot definitions, the phrases that are refused and the looks (named sets, with which shots use each). source says whether the merchant has edited it or the platform defaults are in use. Read-only. |
| studio_cast_model / studio_approve_model | Cast a reference set from its written description, then approve one of those renders as its photo. A render that itself used an approved photo cannot be approved: approving a copy of a copy is how a model drifts into somebody else. |
| generate_video | Generate a video for a brief. Start frame (start_image_url) or first/last frames, identity references, duration, resolution, aspect ratio, and any model the platform accepts. Models are named as the Studio names them (see Model names below): Veo 3.1 and Veo 3.1 Fast (first ref = start frame), Kling O3 Pro and Kling O3 Standard from references (garment @Elements, up to 4 products), Kling 3 Pro (real cuts inside one clip), Seedance 2.0 (up to 9 refs as @Image1..N, top realism + audio, best for UGC), Seedance 2.5 (up to 30 refs plus reference videos and audio, 4-30s single takes), MiniMax H3 Max (the cheapest per second, and silent). A reference list over the model's cap is refused here rather than trimmed server-side and billed. |
| describe_model | Call before generating with an unfamiliar model. Its Studio name and description, what it accepts and how its references are used, plus the ceilings from model_caps. An out-of-range duration is clamped server-side on several lanes, not refused, so guessing costs a render. |
| virtual_try_on | Put one of the merchant's garments onto a photo of a person. Two images in, images out, no prompt. |
| model_caps | The numbers on their own: reference images / videos / audio and the cross-modal total, durations, resolutions, aspect ratios, and which of seed, negative_prompt, cfg_scale, shot_type, an end frame and shot-by-shot mode a model supports. Two rules, enforced differently: the platform refuses a FIELD the model does not declare (a 422, nothing spent), and trims a reference COUNT that is over its cap, then renders and bills what it kept. generate_video therefore refuses an over-cap count itself, before the request is sent. A mode whose dispatch reads fewer references than its family declares (the start-frame and text modes) carries mode_limits, and those numbers override caps. resolutions is the same shape for image sizes: a band a model does not have is folded onto its default and billed, so it is refused before the request is sent. |
| generate_reference_video | Kling O3 Pro / Kling O3 Standard, from references, in full: opening frame, end frame, garment @Elements with front+back grouped per product, native audio switch, and shot-by-shot multi_prompt. |
| restyle_video | Put referenced garments into existing footage while keeping its motion and camera (Kling O3 Pro, restyle a video). |
| generate_animated_product_video | Render a starting frame and animate it in one call, for when there is no still yet. Poll check_pending_jobs. |
| generate_video_prompt_from_image | Analyse an image you already have and write the motion prompt for it. |
| angle_pack_status | Check every job in an angle pack at once, by brief or by the jobs list. |
| check_pending_jobs | Reconcile every generation still running for the team, the way back in when a job id was lost. |
| studio_qc | Re-run the pixel checks on a finished studio shot and read the report: is the mark on the garment whole, how far the colour drifted from the reference, how the subject sits in the frame, how evenly the backdrop falls, how big the mark was printed. The platform runs it by itself when a studio render lands. A check that did not run comes back as not run with a reason, never as a pass, and the mark is not checked on a shot with a person in it. A failing report is written onto the shot; it never touches the shot's status and it never starts another render. |
| list_products | Product catalog with front/back/side reference photo URLs. |
| generate_prompt | The platform's prompt engine writes the generation prompt (garment lock, brand style, ad formats). For videos pass video_model (same model you'll generate with) + video_style (ugc/cinematic) so it writes model-correct reference tags. |
| edit_video | Edit an existing video with a text instruction (Kling O3 Pro, edit a video: same motion/length). |
| upscale_video | Topaz Video Upscale, 1-4x + optional frame interpolation. |
| list_templates / get_template | The workspace's generation templates: the merchant's own ("yours") and the read-only starter ones. A generation template is the merchant's own system prompt, sent word for word, plus named image slots, fixed reference images, a model and an output size; the platform adds only one "Image N: the ." line per image, in the order slots then images. include_options lists the models, aspect ratios and sizes a template may use. |
| save_template / delete_template | Create a template, change one of the merchant's own (a settings change is merged onto the saved ones), or delete one. Starter templates are read-only. |
| run_template | Run a template: one image or a list per slot, plus optional extra_images for this run only (an asset id from the library, or a link on platform storage), optional one-run overrides. Images are sent slots, then the template's own images, then extras; over the model's reference cap is refused, never trimmed. Makes a new brief and returns brief_id + job_id to poll with job_status. |
| suggest_template | The template helper: turns a brief's prompt (or pasted text) into a product-neutral template draft, refined by chat. Saves nothing. |
| upscale_image / upscale_options | Upscale & clean one image to a chosen OUTPUT size (2K or 4K on the long edge, never a factor) on Best Quality (topaz_hifi, the default when models is left out), Best Value (seedvr), or both (all) side by side; one brief and job per model. The model keys are checked against the live list before anything is sent. upscale_options gives each model's estimate_credits for 2K and 4K (credits, never dollars). |
| relight_image / list_lighting_styles | Relight an image with a preset or custom lighting prompt. |
| inpaint_image | Regenerate one masked region of an image (Flux Pro Fill). |
| generate_angle_pack | One scene → up to 8 camera-angle variations (camera-only viewpoints, Front View through Back View). |
| use_result | Keep an edit's or a job's result as a brief's image. The old image moves to the brief's history and can be restored. Nothing is rendered or billed. |
| generate_briefs_batch | Kick off generation for many existing briefs at once. |
| list_assets / get_asset / save_asset / list_asset_tags / favorite_asset | Search, save into, and reuse the team asset library. |
| asset_folders | The asset library's folders: Products, Models and Scenes (always there) plus the merchant's own. List them, or move an asset into one; moving a photo into Scenes marks it as a scene, the place studio backgrounds come from. list_assets takes folder, and upload_reference_image takes purpose (product, model or scene) to file the upload. |
| list_brand_assets | The team's real logo/emblem/graphics/textures as reference URLs, plus the valid type labels. |
| upload_brand_asset | Add a local image to the brand assets, so generations use the real mark instead of inventing one. |
| job_status | Check a generation job; when done it carries the asset URL. |
| upload_media | Upload a local image/video/audio file → stable public URL (for references or Quick Cuts frames). |
| upload_reference_image | Upload a local image through the reference lane: converts to JPEG (strips the alpha channel image models mishandle) and resizes to the provider's limit. Prefer it for photos you are about to generate from. |
| upload_and_upscale_video | Upload a local video and upscale it in one step. The only upscale path that does not need an existing brief. |
| list_quickcuts_projects | List the team's cloud Quick Cuts projects. |
| get_quickcuts_project | Get one project's full timeline data. |
| create_quickcuts_project | Create a project from media URLs. It appears in the team's Quick Cuts editor immediately. |
| update_quickcuts_project | Save modified timeline data back (get → edit → update). |
| delete_quickcuts_project | Delete a project (team-wide, irreversible). |
Model names (v1.9.2)
Every model is called what the Studio's own picker calls it: the real engine
plus an input mode, never a tier word. The hand-written tier labels this
server carried until 1.9.1 are gone; describe_model and model_caps answer with the Studio's
name and description, and every tool hint that lists model keys is derived
from the same table. The table is a vendored copy of the dashboard catalog
(fixtures/dashboard-models.json, ignis-dashboard e265ca1f6da8), and
npm test fails if a name here and a name there ever differ.
| Key you send | Studio name | What it is |
|---|---|---|
| veo | Veo 3.1, from text | Polished film look from text alone. 4, 6 or 8 seconds, with sound. |
| veo-image | Veo 3.1, from a photo | Polished film look from a starting frame. 4, 6 or 8 seconds, with sound. |
| veo-reference | Veo 3.1, from references | Polished film look built from up to 5 reference photos. 8 seconds, with sound. |
| veo-first-last | Veo 3.1, two frames | Polished film look that starts on one frame and lands on another. 4, 6 or 8 seconds, with sound. |
| veo-fast | Veo 3.1 Fast, from a photo | Same film look at half the price. From a starting frame, 4, 6 or 8 seconds, with sound. |
| veo-fast-text | Veo 3.1 Fast, from text | Same film look at half the price. From text alone, 4, 6 or 8 seconds, with sound. |
| veo-fast-reference | Veo 3.1 Fast, from references | Same film look at half the price. Built from up to 5 reference photos, 8 seconds, with sound. |
| veo-fast-first-last | Veo 3.1 Fast, two frames | Same film look at half the price. Starts on one frame and lands on another, 4, 6 or 8 seconds, with sound. |
| kling-o3-pro-reference | Kling O3 Pro, from references | Locks a garment exactly across the shot. Up to 4 references, 3 to 15 seconds, with sound. Use it when the clothing has to be right. |
| kling-o3-standard-reference | Kling O3 Standard | Same garment lock as O3 Pro, roughly a quarter cheaper. Up to 4 references, 3 to 15 seconds, with sound. |
| kling-o3-v2v-reference | Kling O3 Pro, restyle a video | Feed a video you already have and change the setting or swap items in it. Up to 4 references. |
| kling-v3-pro | Kling 3 Pro, from a photo | Cuts between several shots inside one clip, starting from a photo. 3 to 15 seconds, with sound. |
| kling-v3-pro-text | Kling 3 Pro, from text | Cuts between several shots inside one clip, from text alone. 3 to 15 seconds, with sound. |
| seedance-2.0-reference | Seedance 2.0 | Same reference fidelity as 2.5, clips up to 15 seconds, about a third of the price. Up to 9 reference photos, sound included. |
| seedance-2.5-reference | Seedance 2.5, from references | One continuous take up to 30 seconds, up to 30 reference photos, sound made with the picture. The most expensive video tier. |
| seedance-2.5-image | Seedance 2.5, from a photo | Starts on your photo and runs as one take up to 30 seconds. Can land on an end frame. Sound made with the picture. |
| seedance-2.5-text | Seedance 2.5, from text | One continuous take up to 30 seconds from words alone. Sound made with the picture. |
| minimax-h3-max | MiniMax H3 Max, from a photo | Cheapest per second by a wide margin. 5 to 15 seconds, and it can land on an end frame. Silent, so add your own audio. |
| minimax-h3-max-text | MiniMax H3 Max, from text | Cheapest per second by a wide margin. 5 to 15 seconds from words alone. Silent, so add your own audio. |
| minimax-h3-max-reference | MiniMax H3 Max, from references | Cheapest per second by a wide margin. Up to 9 reference photos, 5 to 15 seconds. Silent, so add your own audio. |
| nano-banana | Nano Banana Pro | Best all-round product and edit engine. Up to 14 reference photos. The slowest and the most expensive of the set. |
| nano-banana-2 | Nano Banana 2 | Same 14 reference photos as Pro, about half the price, quicker. Use it for everything that is not a hero shot. |
| seedream-v5 | Seedream 5 Pro | Cheapest per image, up to 10 references. A larger size costs more. |
| gpt-image-2.5 | GPT Image 2.5 Flare | Follows written instructions most literally and preserves fine detail on an edit. Up to 16 references. |
| gpt-image-2.5-detail | GPT Image 2.5 Sunburst | Holds the most detail through an edit and follows instructions literally. Slower than the other one. |
| gpt-image-2 | GPT Image 2 | The previous generation of the pair above: still served, about four times the price, and no size band at all. Prefer gpt-image-2.5. |
| (edit_video) | Kling O3 Pro, edit a video | Change what is in a video you already have. The framing and the length stay as they are. |
| (upscale_video, upload_and_upscale_video) | Topaz Video Upscale | Raise the resolution of a clip you already have and smooth the motion. Up to 4x. |
flux (fal-ai/flux-lora) is still accepted by key; it is not a Studio card
and carries no Studio name.
Growth analytics (v1.7.0)
Read-only. Every number is computed by the platform API. These tools pass the window through and project the response down to what fits in an agent's context. None of them accepts a team, tenant or shop: the API key is the tenant.
🔴 A missing block is never a zero. When a source is not connected or its API
failed, it is reported as absent (sources / unavailable) rather than as 0. Since v1.11.1 a source that answered connected: false is listed in unavailable with the API's own reason (not_connected, read_failed, reauth_needed: the merchant reconnects) and sentence, instead of vanishing from the answer.
"Your email revenue is £0" and "we could not ask Klaviyo" are different answers
and only one of them is true.
Days are the store's days (v1.10.0). Every window is a run of the store's
own calendar days, in its Shopify timezone, the same days the dashboard counts.
period accepts today, yesterday, 7d, 30d, 90d; yesterday is resolved
from the API's reporting zone (read once an hour) and sent as an explicit date
to the endpoints that take dates. Answers that carry a timezone block name the
calendar they were counted in ({name, source}; source: "fallback" means UTC
because the store could not be read). Meta and Google count in the ad account's
own zone; platform_overview.timezone.mismatch says when Meta's differs from the
store's (Google's account zone is not read). Klaviyo counts in the Klaviyo
account's own zone too, and the API says so on those blocks
(timezone_source: "klaviyo_account" on platform_overview.klaviyo and
email_performance.overview, passed through as the producer sends it). A tool whose window this server itself resolved (yesterday, or a
one-day preset widened to attribution's 7-day minimum) says so in
window_chosen: the dates sent and the zone they were chosen in, from the
server's own read; zone.source: "fallback" there means the store's zone could
not be read and the date is UTC's yesterday (with a window_note), and
calendar_mismatch: true means the API named a calendar that is not the one
the date was chosen in, whatever its source: it could not read the store's zone
at that request, or the store's zone was re-read differently; an account calendar
named on a block (timezone_source, Meta's timezone.meta_ads) is a different
question and never this flag. An explicit days
below attribution's 7-day floor is widened the same way and says so. popup_performance's submitted is form submissions, one
per visitor; its subscribers block is what Klaviyo recorded, and partial: true (v1.10.1)
says a configured list was not found on the account, naming it in missing_list_ids.
subscribers.joined totals the joins on every list THIS API reports for the
popup, over distinct list ids. 🔴 Which channels are inside that number
depends on the API answering, and the payload says which. An API that
reports per channel sends channels_counted (the channels in the total),
by_channel (the split) and shared_list_ids (a list wired to both steps:
counted once in the total, shown under both channels). An API that does not
sends per_channel_omitted instead, and then joined is the popup's email
list only and SMS joins are counted nowhere in the answer, a missing
breakdown, never a store with no SMS signups. Where by_channel is present, a
channel the merchant never configured comes back as
{measured: false, reason, error} with no joined key at all, so never
write sms.joined || 0. Its verified block (v1.10.1) is what happened to each
person who signed up in the window, checked against the list.
| Tool | What it does |
|------|--------------|
| funnel_summary | Start here. One compact object for a period: pixel funnel + step rates, revenue/orders/AOV, spend and claimed revenue per channel, blended MER/aMER/CAC, email contribution, popup captures, new-vs-returning. |
| funnel_diagnosis | The daily digest for one of the store's calendar days, built and returned (it sends nothing): deltas vs the same weekday last week, the day's funnel, top campaign, and ⚠ anomaly lines. |
| channel_performance | Blended MER + aMER + CAC, and per-campaign TRUE ROAS. Meta's claimed spend/ROAS joined to what the first-party pixel actually witnessed. |
| platform_overview | Headline numbers per connected platform (Shopify, Meta, Google, Klaviyo) with period-over-period change. |
| pixel_funnel | The pixel funnel on its own, with segment (all/bundles/singles) and the optional per-bundle breakdown. |
| traffic_breakdowns | Device split, referrer hosts, landing and exit pages, on-site search terms. |
| cohorts_and_ltv | Monthly acquisition cohorts, first→second order retention, days-to-second-order, 3/6/12-month LTV. |
| email_performance | Klaviyo revenue split campaign vs flow, sends, open/click rates, live flows, recent campaigns. |
| ads_performance | Meta + Google account totals and campaign breakdown sorted by spend, optional daily series. |
| popup_performance | Popup Lab experiments and the cohort-correct per-variant funnel (assigned → submitted → orders/revenue); submitted is form submissions, phone_submitted (v1.11.0) is the subset of them who also finished the SMS step, and subscribers is the Klaviyo list joins beside it. The per-day, per-variant series comes back when you name one experiment_key or pass include_timeseries (v1.11.0). |
| connection_status | The first tool to call when a number looks wrong. Which integrations this team actually has, plus each connector's state (configured, connected, failing with the verifier's last_error, unreadable = reconnect needed), one row per connector (v1.11.1: it used to crash on the API's connector map). |
🔴 A null rate means "unknown" (v1.10.1). aov, conversion_rate_pct,
the *_change_pct fields, cpc, cpm, ctr, cost_per_result,
sms_click_rate, redemption_rate and average_balance_cents come back as
null when there was nothing to divide by (no orders, no clicks, no sessions,
no previous period, no credit issued) or the platform did not report them. That
is not 0 and not "not sent"; a measured 0 is still 0.
🔴 A null amount means "could not be read". In channel_performance and
platform_overview, revenue (revenue_cents, total_revenue), spend
(spend_cents, meta_spend_cents, google_spend_cents, prev_spend_cents,
total_ad_spend) and the MER (mer, mer.value) are null when the Shopify
revenue read or a connected ad platform's spend read did not complete. MER's
sources names which: shopify_revenue, meta_spend and google_spend are
ok, read_failed or not_connected. A ratio over one platform is only
served when the other is really not connected.
Commerce reads (v1.8.0)
Read-only, and the same rules apply: no team/tenant/shop parameter, and a missing number is an absence rather than a zero. Two more rules are specific to this group.
🔴 No tool here can name a shopper. These endpoints return operational rows,
and one of them (GET /api/admin/returns) hands back a whole database record
carrying a customer's email, their name, an IP hash and a live discount code.
Every tool below projects an explicit field list and drops the rest, and
verify_tools.mjs drives them against fixtures that deliberately contain those
fields so a pass-through fails the build rather than an agent's context.
🔴 Nothing here writes. There is no redeem, no adjust, no refund and no inventory write. The loyalty tool is the clearest case: a redemption pays out Shopify store credit that cannot be reversed, so the reporting surface and the payout path are kept structurally apart.
| Tool | What it does |
|------|--------------|
| list_orders | Recent orders from the platform's local cache, never a live Shopify walk. Id, order number, date, total (cents), financial status, cancelled. No shopper identity is available, and no parameter turns it on. 🔴 Do not sum it: it includes cancelled and refunded orders, so it is not the revenue population. |
| top_products | Best sellers for a period: name, units, revenue (🔴 dollars here, not cents). A disconnected Shopify is reported as unavailable, never as an empty catalogue. |
| returns_queue | The returns queue with status, resolution, credit (cents) and label state. 🔴 label_status: "unavailable" means Shopify could not be read, it is not "no label yet". |
| return_detail | One return in full by its numeric id, plus moderation and resolution detail. The label URL is never returned, only whether one exists. |
| store_credit_liability | Outstanding store-credit liability, customer-money vs promotional split, issuance and redemption. 🔴 last_reconciled_at: null means balances have never been checked against Shopify. |
| loyalty_program_stats | Loyalty in aggregate: config, members the ledger has touched, points outstanding/earned/redeemed, codes minted and unused. 🔴 ledger_populated: false makes every zero an absence; codes.unused is a ceiling, not a count. |
| creator_performance | Per-creator submissions, live ads, attributed revenue and commission (cleared / pending / paid out), in cents. 🔴 Needs a signed-in dashboard session, which this server does not have: the platform refuses an API key on this route because the answer names real creators and the money owed to each, so the tool returns that refusal instead of data. Read excluded before trusting a total. |
| inventory_status | Stock health: totals plus out-of-stock, critical, low-stock and overstocked buckets with days of cover. 🔴 Expensive (walks the catalogue live), and days_of_inventory: 999 is a no-velocity sentinel, not three years of stock. |
Popup Lab reads (v1.11.0)
The platform serves six popup read endpoints and this server wrapped one of them, so an agent could see a funnel and not the arms behind it, the code pools under it, the codes owed to real shoppers, or the opt-in evidence. Five of the six now have a tool. The same rules apply: no team parameter, lists capped by the tool, and an empty answer that says what it means.
🔴 Every capped list says so. When a tool drops rows it returns a
truncated map naming the field, how many of how many are here and which end
was kept; when truncated is absent, nothing was dropped. Arms are capped per
test, so experiments[].truncated.variants is the one to read before
concluding an arm does not exist. A capped list is missing rows from the
answer, never from the store.
🔴 The daily series in popup_performance is cut on DAY boundaries,
never inside a day: every date it returns carries every arm the API reported
for that date, so an arm missing from a day really was absent that day. Days
are never padded or dropped for being short, because a genuinely late-joining
arm is exactly the signal the series exists to show.
| Tool | What it does |
|------|--------------|
| popup_experiments | The roster: every popup test, each arm's key, type and weight (share of traffic; 0 means defined and shown to nobody), status and start date. 🔴 The only place you can see that an arm joined a test LATE, which makes whole-window totals non-comparable between arms. Name an experiment_key and each arm's full stored config comes with it: the steps (steps.phone is the SMS step), the copy, the prize pools and odds, the Klaviyo list ids, the timing. That config is merchant-confidential. |
| popup_codes | 🔴 Money and promises. Pool depth per discount tier (ready/minting/claimed/failed), the pool target and low_water refill line, the fixed static_codes, policy_conflicts in plain sentences, and owed: deferred is a promise still outstanding and self-healing, failed is a shopper who was promised a code and never got one. owed.unknown: true means the platform could not count, and then no count is shown at all rather than zeros that read as "everybody got their code". |
| popup_consents | The opt-in evidence, newest first: the exact wording shown, its version, where its links pointed, the channel, the surface and the server timestamp. No address and no visitor id: a six-character hash_tail is all, and it matches a popup_signups row for the same person. An empty list is an absence of records, not zero consents. |
| popup_signups | One store calendar day of signups, person by person and address-free: when, which arm, the checked outcome against the Klaviyo list, the prior_state before it, the submit count, the internal-traffic flag and the hash tail. Admin only. Capped at 25 rows, newest first, and it says when a day had more. |
| popup_config_schema | The settings dictionary: what every popup config key means and what values it accepts. No arguments gives the index; keys, group or search gives the full descriptors. 🔴 The schema, never a store's values: for those, name an experiment_key in popup_experiments. |
Three tools that were specified and deliberately not built, recorded here so nobody re-derives the reasoning:
popup_signup_lookup:POST .../experiments/{key}/signups/lookupanswers "what happened to THIS address", so the tool would take a raw email address as an argument. The platform handles the address correctly (request body, never a URL; hashed; never stored, logged or echoed; admin-gated; rate limited) and the merchant has the same lookup in the dashboard, so nothing is lost to them. The tool is the problem: an agent that has read a support email, a review body or a cart note could be steered into looking a person up, and this server's standing rule is that no tool names a person. Reopening it is a decision, not a line of code.order_tracking:GET /api/orders/{id}/trackingauthenticates a shopper, via a Shopify customer session token or an?email=parameter. An API key is neither, so wiring it would mean an agent putting a customer's email address into a URL. There is no merchant-authenticated tracking route.inventory_risk:/api/inventory/velocityreturns a 500 (it calls a function that does not exist), andreorder-plan/dead-stockreadinventory_snapshots/sales_velocitythrough accessors that carry no team filter.inventory_statusalready answers the risk question from the caller's own Shopify data.
Media references
Every reference argument takes either an https URL on platform storage or an absolute local file path. A local path is uploaded for you before the generation runs, so there is no separate upload step to remember.
🔴 A local file is uploaded only when a person said yes. Uploading publishes a file to a link anyone with the link can open, and the tools are driven by an agent whose context can contain text this server itself fetched (support messages, review bodies, shopper search terms). An injected sentence can make the agent call a tool with a path; it cannot answer a question put to you.
So there are three ways a file gets in, in this order:
- Drop the file into the chat window (Claude Code). Claude Code puts the
file's path in your prompt. Say what to do with it ("use this as the front
reference"), and when the tool runs you get one question per file:
Upload photo.jpg (2.1 MB, image/jpeg) from /Users/you/Shoot to your Ignis
Asset Library? It becomes a link that anyone with the link can open. Choose
Accept. Nothing is sent before you answer (the file is only fingerprinted on
your computer, so a swap while the question is open is caught), and a
no, a cancel or no answer within two minutes means nothing from that call
is sent. The question names the file the path REALLY points at, so a link
called
photo.jpgthat points somewhere else is shown by its real name and folder. A name or folder carrying a line break, a tab or a terminal escape is shown with it spelled out (\x0a), never acted on, and a long folder is shortened from the left (…/Shoot); the file name is always shown whole. This needs a client that can ask (MCP elicitation; Claude Code 2.1.76 or later) and no setting at all. - Claude Desktop, and any client that cannot ask: name one folder. Set
IGNIS_MCP_MEDIA_ROOTto a folder made for product photos, such as~/Ignis(a leading~is your home folder; several folders are separated by:on macOS and Linux). A file inside it is uploaded without a question, because putting it there was the yes. 🔴 Never your Downloads, Desktop or Documents, and never a bare~: on a client that cannot ask, every file in that folder is one injected sentence away from a public link, and those folders hold screenshots, scans and invoices. The dashboard's setup page (Developer, then Model Context Protocol) includes the line. With it unset, a client that cannot ask reads no local file at all. On a client that CAN ask it is optional: files inside it skip the question, files outside it are asked about. - The dashboard's Asset Library. Upload there and pass the link
(
list_assets,list_brand_assets,list_productsreturn them). This works in every client, including hosted ones with no disk.
Getting a file to the MCP
A file ATTACHED to a chat (Claude Desktop, claude.ai and other clients that hand attachments to the model only) never reaches this server, and a video cannot be attached at all. That is how every MCP works. Save the file to disk and give its path (door 2), or upload it through the Asset Library (door 3). Claude Code is different: dropping a file into its window puts the file's PATH in your prompt, and that is door 1 above.
Rules no answer can override, checked BEFORE anyone is asked:
- Only the formats this server advertises are read (JPEG, PNG, WebP, GIF, SVG,
MP4, MOV, WebM, AVI, MKV, MP3, WAV, M4A). A file with no extension, a
dotfile, or a credential shape (
.env,*.pem,*.key,id_rsa) is refused by name, and nobody is asked about it. The name is checked twice, on the path you gave and again on the file it resolves to, so a symlinkphoto.pngpointing at.envis refused as well. - The first bytes must be a format this server uploads, of the kind the name
claims (a PNG saved as
photo.jpgis sent as the PNG it is; a text file namedphoto.jpgis refused; SVG is checked by name only). The bytes you confirmed are the bytes sent: if the file's content changed after the question (a swapped link, a rewrite) nothing is sent; a touch, a permission change or a sync client's metadata write is not a change. A call you cancel starts nothing. A media folder that is your home folder, or any folder above it, is ignored. - The size is checked before the question: 200 MB for a reference or
upload_media, 100 MB forupload_and_upscale_video, 10 MB forupload_brand_asset. - One call asks about each of its files in order, and one no refuses the whole call: you never end up with half of a call uploaded. A call naming more local files than the largest reference budget (50) is refused before any question.
- On a client that cannot ask, a path is resolved with symlinks followed and
must land inside one of the named folders. A symlink in the media folder
pointing at
~/.ssh/id_rsaresolves outside it and is refused.
IGNIS_MCP_MEDIA_ROOT=~/Ignis🔴 Google Drive links are refused, here and by the platform. A link on somebody else's host can be revoked, moved or rate-limited, and a reference that disappears at render time produces a different render rather than an error. This really happened, and the wrong garment came back on the wrong body with nothing reporting a problem. Download the file and pass its path instead.
Reference order is load-bearing and is never changed: @Image1..@ImageN map
to array positions, and the frame lanes read [start, end].
Setup
1. Get a key. In the dashboard: Settings → Developers → New API key. Select the Full Access scope, then copy the key (shown once). It is scoped to the team you created it under.
2. Install:
npx -y ignis-mcp # no checkout needed once published3. Wire it into your client.
Claude Desktop, add to claude_desktop_config.json:
{
"mcpServers": {
"ignis": {
"command": "node",
"args": ["/path/to/ignis-mcp/src/index.js"],
"env": { "IGNIS_API_KEY": "YOUR_KEY_HERE", "IGNIS_MCP_MEDIA_ROOT": "~/Ignis" }
}
}
}Claude Code, from any project:
claude mcp add ignis -s user --env IGNIS_API_KEY=YOUR_KEY_HERE --env IGNIS_MCP_MEDIA_ROOT=~/Ignis -- node /path/to/ignis-mcp/src/index.jsTwo gotchas the command protects against:
-s usermakes the server available in every session on the machine. Without it, the server is scoped to whatever folder you happened to run the command in and silently missing everywhere else.- The full path after
nodeis required.-- nodealone registers a broken server that hangs on start.
4. Confirm. Ask Claude to run the whoami tool. It should return your team.
Config
| Env | Required | Default |
|-----|----------|---------|
| IGNIS_API_KEY | yes | none |
| IGNIS_API_URL | no | https://api.ignishq.com (set to https://ignis-api.up.railway.app to test against Development). Every *.up.railway.app host except ignis-api.up.railway.app is refused at startup (a retired Railway name can be claimed by anyone, and would receive the key). Must be https; plain http is refused except for localhost, 127.0.0.1 or [::1]. Redirects are never followed. |
| IGNIS_MCP_MEDIA_ROOT | no; optional where the client can ask | unset. On a client that can ask (Claude Code), every local file is uploaded only after you confirm it, and this adds folders whose files need no question. On a client that cannot ask (Claude Desktop), unset means local file paths are refused entirely. One or more folders (:-separated on macOS/Linux) the server may read media from; a leading ~ is the home folder (~/Ignis); name one folder made for product photos, never a private one; a bare ~ (the whole home folder) or anything above it is refused. On Windows the delimiter is ; and the tilde form needs a forward slash (~/Ignis). See Media references. |
| IGNIS_MCP_ZONE_TTL_MS / IGNIS_MCP_ZONE_FAIL_TTL_MS | no | unset, test knobs only: how long a store zone read (1 h) or a failed read (60 s) is kept, in ms; a positive number is clamped to 50 ms..24 h, anything else (0, negative, non-numeric) is the default. The verifier shortens them to observe expiry. |
| IGNIS_MCP_TIMEOUT_MS | no | 110000. How long any one JSON API call may take before it is aborted with a "poll, do not re-fire" message. |
| IGNIS_MCP_UPLOAD_TIMEOUT_MS | no | 600000. The same ceiling for a multipart upload, which carries a whole file. |
Legacy NESW_API_KEY / NESW_API_URL are still accepted as deprecated fallbacks,
so configs created before the Ignis rename keep working unchanged.
Security
The API key is a bearer credential scoped to one team. Anyone holding it can act as you within that team, so treat it like a password: keep it in the client's env config (never commit it), and revoke it in the dashboard (Settings → Developers) if it leaks. Revocation is immediate.
This process reads no file you have not allowed. The only filesystem read is
the local-media lane above: a file you confirmed in the prompt, or a file inside
a folder IGNIS_MCP_MEDIA_ROOT names. With neither, nothing is read. There is
no other path from a tool argument to a file on your machine.
