Drop the standalone [workspace] block from the crate's Cargo.toml and
add 'crates/cli/artcraft-mcp' to the root workspace members list.
Kept this as the final commit on the branch — until this lands the
crate was a separate workspace, which kept the root Cargo.toml
untouched during the spike (avoiding conflicts with parallel work on
main) but meant 'cargo build' at the repo root didn't include it.
After this commit, 'cargo check -p artcraft-mcp' works from the repo
root and the crate is a first-class workspace member.
When num_images>1, the omni_gen response is still a single job with a
single inline result; the rest of the batch is reachable through the
result's maybe_batch_token via GET /v1/media_files/batch_gen_redux/{token}.
Previously we only surfaced the single inline result, so 'num_images: 4'
returned just 1 image. Now after poll detects CompleteSuccess we follow
the batch token to fetch every member and return them all.
Inline rendering:
- Single result: 768 px thumbnail (unchanged).
- Batch: 512 px per image — 4 thumbnails at 512 px JPEG stay well under
Claude Desktop's 1 MB tool-result cap.
Per-image inline fetch failures are logged and skipped (still return
the URL as text), so a single bad thumbnail doesn't poison the whole
response.
Multi-image text output is one URL per line, prefixed 'Image N:'.
Make failure modes legible to both the LLM and the user:
errors.rs:
- ErrorCode enum: artcraft_not_installed, not_logged_in,
session_expired, backend_unavailable, invalid_params,
generation_timeout, generation_failed, reference_image_failed,
internal.
- ToolError { error_code, message, remediation, details? } that
serializes as JSON in CallToolResult::error content blocks.
- not_logged_in() detects whether the app is installed
(/Applications/ArtCraft.app or the cache dir) and returns the
appropriate code + remediation text.
creds.rs:
- load_session() now returns Result<_, ToolError>. The user sees a
proper sign-in remediation instead of a generic anyhow message.
tools/check_artcraft_connection.rs (new):
- Always succeeds, never errors. Reports desktop_app_installed,
signed_in, session_valid, error_code, remediation. Detects expired
sessions by making a cheap authenticated get_session_credits call.
tools/list_image_models.rs:
- 401/403 from the API now maps to session_expired.
All existing tools converted from anyhow::Result to Result<_, ToolError>
with appropriate error codes per failure path.
server.rs:
- Updated instructions teach Claude how to react to each error code
(open desktop app and sign in, download from getartcraft.com, etc.)
and to call check_artcraft_connection when troubleshooting.
- error_result() now serializes the full ToolError struct as JSON
inside the error content block.
main.rs:
- New '--check' / '-c' CLI flag prints a human-readable diagnostic to
stdout and exits, useful when the user can't see the Artcraft tools
in Claude and wants to debug from a terminal without round-tripping
through MCP.
Split main.rs (398 lines) into modules: main.rs, server.rs, creds.rs,
inline_image.rs, tools/{generate_image,list_image_models,
get_user_status,estimate_image_cost}.rs.
New tools:
- list_image_models — GET /v1/omni_gen/models/image; returns each
model's constraints (aspect_ratio_options, resolution_options,
quality_options, batch_size_max, image_refs_supported, image_refs_max,
text_prompt_max_length, etc.) so Claude can pick valid params before
calling generate_image. Optional provider filter.
- get_user_status — parallel GETs to /v1/credits/namespace/artcraft +
/v1/subscriptions/namespace/artcraft. Returns credits (free, monthly,
banked, total) and active subscription info.
- estimate_image_cost — POST /v1/generate/cost_estimate/image. Takes
model + num_reference_images + aspect_ratio + resolution + quality +
num_images. Returns cost_in_credits, cost_in_usd_cents, is_free,
is_unlimited, is_rate_limited, has_watermark — so Claude can confirm
with the user before spending credits.
Expanded generate_image:
- New optional params: aspect_ratio, resolution, quality, num_images,
reference_image_urls.
- reference_image_urls fetches each https URL, validates as image/*
(PNG/JPEG/GIF, max 20 MB), uploads via /v1/media_files/..., and
passes the resulting MediaFileToken to omni_gen. Marked as
intermediate system files so they don't appear in the user's gallery.
Constraint validation is left to backend's structured errors; Claude
is expected to consult list_image_models first.
The tool now accepts an optional `model` arg (snake_case id matching
CommonImageModel's serde rename, e.g. nano_banana_pro,
flux_pro_1p1_ultra, gpt_image_2, midjourney_8, seedream_4, etc.).
Defaults to nano_banana_pro when omitted. Unknown ids return a
structured error so the LLM can correct itself.
Valid model ids are listed in the JSON schema description so Claude
can pick one before calling the tool.
Claude Desktop rejects tool results over 1 MB; base64 adds ~33%
overhead so the raw payload must stay under ~750 KB. Full-res PNGs
from Artcraft (often 1024+ px) routinely exceed that.
Use media_links.maybe_thumbnail_template at width 768 for the inline
preview when the API returns one; fall back to the full cdn_url
otherwise. The full URL is still returned as text so the user can
open the original at full resolution.
Previously the tool returned only Content::text(cdn_url), which Claude
Desktop renders as a "show image" link. Switch to fetching the
generated image bytes from the CDN, base64-encoding them, and
returning both Content::image and Content::text so the chat shows the
image inline while keeping the URL clickable.
On fetch failure (timeout, CDN error, etc.) we fall back to the
URL-only response so the tool still succeeds.
The OmniGenImageCostAndGenerateRequest doc comment claims the server
auto-generates an idempotency_token when None is passed, but the
backend actually rejects requests without one ("idempotency token does
not match the expected format"). Match the desktop app's behavior at
handle_artcraft_via_omni_endpoint.rs:30 by sending a fresh hyphenated
v4 UUID per call.
Production desktop v0.32 keeps Storyteller cookies only in the vendored
tauri-plugin-http jar at <app_cache_dir>/.cookies; persist_all_to_disk()
for the text-file path in the dev source is commented out, so shipped
builds never populate those files.
Add a JSON-parsing fallback that reads the .cookies jar, filters to
api.storyteller.ai, and extracts the session + visitor cookies. The
text-file path stays as a secondary fallback for forward-compat.
macOS-only path resolution via directories::ProjectDirs for the
"ai.artcraft.app" bundle id. Linux/Windows resolutions differ from
Tauri's app_cache_dir() and are deferred.
Standalone crate at crates/cli/artcraft-mcp/ exposing a single MCP tool
that submits an image generation via /v1/omni_gen/generate/image, polls
/v1/jobs/session until the job completes, and returns the CDN URL.
Reads Storyteller cookies from ~/Artcraft/credentials/ written by the
desktop app — no separate sign-in needed. 90s poll timeout.
Kept as its own [workspace] during the spike so the parent workspace
Cargo.toml stays untouched and parallel work on main doesn't conflict.
Workspace registration deferred to a final commit once the spike is
validated end-to-end against Claude Desktop.
- Renamed the "Supercharge Credits" button to **Use on Web** (globe
icon) in the hero and bottom CTA. Routes subscribers to the webapp
homepage, everyone else to `/pricing`.
- Removed the "Create image / Create video" links from both sections.
- Split Discord + GitHub into a separate "Open source &
community-driven" CTA (subtle container) above the "Ready to craft?"
CTA.
MentionTextarea's DOM-sync effect rewrote innerHTML under the live caret
whenever buildHTML's identity changed, detaching the cursor on iOS
Safari mid-typing. Skip the rewrite when focused and the DOM already
matches value, and memoize colorMap in MobilePromptForm to stop the
per-render churn.