@aiphotostudio/mcp
v0.6.5
Published
MCP server for Self Care (Photo Studio): products, free previews, material tuning, shader node graphs, model, texture and background uploads, paid silo shots (Blender only with no AI at all, or with the AI pass and four AI variants), product, room and roo
Maintainers
Readme
@aiphotostudio/mcp
An MCP server over Self Care's user API (/api/public/v1). It gives an agent
the Photo Studio tools — products, free previews, material tuning, model and
texture uploads, model collections, lifestyle backgrounds, paid renders
(product, drawn room, room photo), scene AI, albums — and, as its standing
instructions, a short map of this server's silo and free-check tools followed by
the briefing the API itself serves. Every public route is a tool
unless it is browser-only (see Routes without a tool).
Silo shots: submit_silo_render (the Blender render only — no AI at all:
it always sends aiPass: false) or submit_silo_render_with_ai (the render with
the platform's automatic AI pass + 4 AI variants from your prompt and optional
reference image). Both spend credits: the render at the same silo3d price, and
with AI also one sceneAiCandidate per variant, taken when each variant is
styled after the render is done (prices in get_credits). For the AI render,
get_render_job gives the AI-enhanced image and get_render_original the raw
Blender one; for the Blender-only render the two are the same image. A free look
at the Blender render is exposure_preview (16 samples, 1K). Where the render
platform cannot render without AI yet, submit_silo_render is refused with 503
ai_pass_opt_out_unavailable and nothing is charged (needs the Self Care API
with aiPass; MCP 0.6.1).
Shader node graphs: set_material_node_graph takes inline node groups
(groups, each used by a ShaderNodeGroup and wired by interface socket name),
frames (NodeFrame, and parent on any node), the newer node types (Combine /
Separate XYZ, Tangent, Vector Rotate, Vector Transform, Float Curve, legacy
MixRGB) and Attribute nodes reading the four values the renderer provides per
board — a connected piece of a part's mesh (board_coord, board_seed,
board_variation_a, board_variation_b), a Tangent uv_map of board_fiber_uv
(the per-board UV map), attributeBindings that map a shader's own attribute
names onto those (with scale and offset) and a display label on any node. Limits: 384 nodes and 512
links across the graph and each inline group definition, 64 inputs / 16 outputs per group, 32 groups, nesting
depth 3, a 1 MiB body (413 node_graph_too_large). A refusal's issues name the
group they sit in; read_guide has every setting and issue code (MCP 0.6.4).
Node templates: instead of building a whole shader, pin a vetted one. Every
ref names its scope and an exact version: public:[email protected] (written by the
integration's admins, usable by every brand) or brand:[email protected] (the
material's own brand). Find one with list_node_templates. Read it with
get_public_node_template / get_brand_node_template: inputs with defaults and
recommended ranges, presets such as oak (presetValues), the read-only definition, and
exampleGraph / presetGraphs ready for set_material_node_graph. Then
render it with your inputs for free: preview_public_node_template →
get_public_node_template_preview, on a sphere (budgeted per credential: 429
preview_budget_exhausted). Nothing upgrades a pinned ref, and a template's
nodes do not count against the graph's limits. A brand's users can also write
its templates (create_brand_node_template, fork_node_template_into_brand, …).
That is free but brand-wide: every colleague sees the result, so tell the
user first (MCP 0.6.5).
Moving materials between template versions: the brand bulk tools
(check_brand_node_template_compatibility, create_brand_node_template_from_material,
adopt_brand_node_template, upgrade_brand_node_template_materials,
get_brand_node_template_operation) only ever touch the brand's own materials.
They are free but brand-wide, so tell the user first. With
templateScope: "public" they use the public template instead, which is how a
brand moves its materials from public:[email protected] to public:[email protected].
Adopt and upgrade send dryRun: true unless you pass dryRun: false. Show the
plan and run it only after the user says yes. An operation is done when
complete is true, and a failed material keeps its previous render.
Before any render that spends credits, check the look for free, in this order — the server's standing instructions carry the same list, and every paid tool's description points at it right after its price:
- Framing:
framing_preview(seconds; keeparrangementApplied), saved and reused withcreate_frame/list_frames. - Product and configuration:
get_product_capabilities(the choices and theirmaterialIds),preview_product_configuration(a 256 px still of one configuration; a 422 lists the bad selections). - Materials:
render_material_preview→get_material_preview(the material on a cloth ball),exposure_preview→get_exposure_preview(a 16-sample 1K Blender render of the actual shot, whole frame or aregion),compare_material_to_reference/list_reference_comparisons(measured against a reference photo or the configurator image); fix it withupdate_material(reversible viaget_material_history) or the node-graph tools (set_material_node_graph→get_node_graph_build,verify_node_graph_build), or a vetted node template (list_node_templates,get_public_node_template,preview_public_node_template→get_public_node_template_preview). - Then ask the user to confirm, and only then call the paid tool.
Published on npm as @aiphotostudio/mcp.
It needs Node 18 or newer and nothing else — no install step, no dependencies:
npx -y --prefer-online @aiphotostudio/mcp@latestKeep --prefer-online: without it npx reuses the copy it cached the first
time, so after a new release it would silently keep starting the old version.
The version the server reports in its MCP handshake tells you which one is
running.
Run it
{
"mcpServers": {
"self-care": {
"command": "npx",
"args": ["-y", "--prefer-online", "@aiphotostudio/mcp@latest"],
"env": { "SELFCARE_API_BASE_URL": "https://api.ai-photo.studio/api/public/v1" }
}
}
}There is no token in that configuration. The server looks for one in this order:
| | Where | |
|---|---|---|
| 1 | SELFCARE_TOKEN | the value itself |
| 2 | SELFCARE_TOKEN_FILE | a path to a file holding it |
| 3 | ~/.config/selfcare/token | the default, and what a pairing link writes |
The user gets a pairing link from Photo Studio → Agent access and pastes it
into the agent; the page it serves carries a one-line command that writes the
token into that file without the agent ever reading it. See
bff/app/agent_pairing.py for the reasoning.
Why npx and not uvx
The criterion is what the user's MCP client already has on the machine, not what this repository is written in.
- Every MCP client we target documents
npxas the way to run a server, and their own example configurations use it.uvxneedsuvinstalled first — which is precisely the "go and install this thing" step a non-technical user is supposed to be spared. - Node 18+ ships
fetch, so this package has no dependencies and needs no build:npxfetches it and runs it. A Python package doing the same would still needuvon the box. - The repository already builds Node (the frontend), so a JS package here is not a new toolchain for us either.
The cost, stated plainly: this package is outside ./run-lint's Python gates.
Its invariants are held by mcp/test/ (node --test) and by
bff/tests/test_mcp_server_catalogue.py, which reads these sources from the
pytest suite so a tool that stops declaring its price fails the BFF gate.
What it does not do
It adds no rules. Every tool is a method and a path; budgets, brand scope,
credit gates, validation and refusals live in the API and are returned verbatim.
The one thing decided here is that every tool description opens with FREE — or
COSTS CREDITS —, because that is the sentence a model reads when it picks a
tool; a paid one follows it with its price sentence and the pointer to the free
checks.
Two tools take named arguments instead of a pass-through body, because one
route does two jobs an agent must not mix up (with or without the four AI
variants): submit_silo_render and
submit_silo_render_with_ai both call POST /render-jobs with type: "silo".
Each declares the body fields it may send; the pure one's list has no
aiVariants, so it can never send one, and the AI one always does. A body
outside a tool's declaration is refused before it leaves (the BFF test suite
checks each list against the route's own model).
Uploading files
Nine tools send files, and submit_silo_render_with_ai sends one inline. The server runs on the agent's machine, so each file
part is the path of a local file — absolute, or starting with ~/ — named
<part>Path (filePath, sceneGlbPath, photoPath, …), read there and
uploaded as multipart/form-data.
| Tool | Part | Accepts | Up to |
|---|---|---|---|
| upload_asset | file | .glb; .fbx; .zip or a folder with the model (FBX, OBJ, glTF) and its textures | 100 MB; 250 MB; 250 MB packed, 250 MB unpacked |
| upload_material_texture | file | .png, .jpg, .jpeg | 64 MB |
| compare_material_to_reference | reference (optional) | .png, .jpg, .jpeg, .webp | 25 MB |
| upload_background | background, foreground (optional) | .png, .jpg, .jpeg | 100 MB |
| upload_background | sceneGlb | .glb | 100 MB |
| submit_room_render (paid) | sceneGlb | .glb (the room surfaces) | 120 MB |
| submit_room_photo_render (paid) | sceneGlb | .glb (the composite scene) | 120 MB |
| scene_ai_enhance (paid) | screenshot, referenceImage (optional) | .png, .jpg, .jpeg, .webp | 100 MB |
| scene_ai_decompose | composedImage | .png, .jpg, .jpeg, .webp | 100 MB |
| upload_room_photo (paid) | photo | .jpg, .jpeg, .png, .webp | 100 MB |
| submit_silo_render_with_ai (paid) | aiReferenceImagePath (optional), sent as aiVariants.referenceImage | .png, .jpg, .jpeg, .webp | 12 MB |
The style reference of submit_silo_render_with_ai goes in the JSON body as a
base64 data: URL. Its type is the one its bytes say (PNG, JPEG or WebP
magic numbers), as the API decides it — a .png that is really a JPEG goes as
image/jpeg, and a file that is none of the three is refused here.
A form field the route requires (name on upload_background, cameraConfig
and payload on submit_room_render, prompt on scene_ai_enhance, width
and height on submit_room_photo_render) is checked before any file is read.
A field that carries JSON (cameraConfig, payload, aiVariants, scene, a
crop) is given as an object and sent as its JSON text.
A folder is packed into a ZIP by this server (Node's own zlib, no archiver
needed): exactly one model file (.fbx, .obj, .gltf, .glb) and the files
a model refers to (.mtl, .bin, images) at their relative paths. Hidden
files, other extensions and symlinks that lead out of the folder are left out;
a folder with no model file or with several is refused.
The path is checked before a byte is read: an extension the route does not take
(on the path or on what a symlink points at), a relative path, anything that is
not a regular file or a model folder, or a file over the route's cap is refused
as a tool error and nothing is sent. The limits are the API's own, and so are
the content types sent where a route checks them (the BFF test suite fails when
either drifts). What the file is is still decided by the API from its bytes,
and its refusals — invalid_glb, unsupported_file_type, model_archive_*,
invalid_texture, invalid_file, unsupported_content_type,
file_too_large — reach the agent word for word.
Upload the model as it is — no need to convert it to GLB yourself. A GLB is
usable on arrival. An FBX, OBJ or ZIP is converted: upload_asset answers
processing, and an FBX/OBJ reports awaiting_review when the conversion is
done; it can be previewed for free (framing_preview, exposure_preview) but
renders only after approve_asset — check its measuredDimensionsM against the
real product first and correct it with adjust_asset (approval is final).
Textures the upload did not carry do not fail it: the model is needs_fix, its
issues name the material, the missing files and the parts, and
set_part_materials with an active library material on every listed part makes
it ready. A failed conversion says why in statusReason.
A lifestyle render (exposure_preview, submit_render) takes its
lifestyle.backgroundId from list_backgrounds or list_my_backgrounds; any
other id is 404 background_not_found, and a background whose scene files are
gone is 409 scene_files_missing. A scene render that fails names its cause in
error.code: scene_files_missing, product_model_missing (detail
items[i]), product_out_of_frame or render_black_frame.
Admin tools
With an admin token configured, the server also offers 15 tools over Self
Care's admin API (/api/admin/v1). They manage the public node templates,
the ones every brand can pin. Without an admin token they are not offered at
all. Each description opens with ADMIN:, because each tool changes what every
brand of the integration can pick. None of them spends credits. The lifecycle,
the naming and versioning rules, seeding WOOD, and the dry-run-first procedure
for moving materials are in docs/ADMIN-AGENT.md in this repository.
| Variable | |
|---|---|
| SELFCARE_ADMIN_TOKEN_FILE | a path to a file holding the token (preferred: the value stays out of the client's configuration) |
| SELFCARE_ADMIN_TOKEN | the token itself |
| SELFCARE_ADMIN_API_BASE_URL | the admin API base; default: SELFCARE_API_BASE_URL with its /api/public/v1 replaced by /api/admin/v1 |
The token must be an admin personal access token (scadm_…), minted in Self
Care Admin → Access tokens. If a user token is put there, the server refuses
to start rather than send it to the admin API. The two credentials are never
mixed: the admin token goes only to the admin base URL, and the user token
never goes there. Neither is ever logged.
| Tool | What it does |
|---|---|
| list_public_node_template_versions | every public version, every status; filter by status, name |
| get_public_node_template_version | one version: definition, docs, presetValues, manifest, preview, lineage. Poll it after a draft preview |
| create_public_node_template | a DRAFT from a JSON definition: inline definition, or definitionPath, a local .json file of up to 1 MiB |
| update_public_node_template | replace a DRAFT (definition or definitionPath again); resets its preview |
| update_public_node_template_docs | description, category, tags, inputDocs, presets, at any status |
| preview_public_node_template_draft | dry build + verify of the version's exampleGraph on the sphere (202), then poll |
| publish_public_node_template | publish a draft whose preview is passed and current; moves no material |
| deprecate_public_node_template | graphs that pin it keep rendering; note says what to use instead |
| delete_public_node_template_draft | destructive: a draft only |
| fork_node_template_into_public | copy a version into a new public DRAFT (from, name?, version; lineage.forkedFrom) |
Admin bulk tools: moving materials
| Tool | What it does |
|---|---|
| check_public_node_template_compatibility | a static check (no Blender) of every brand's materials on public:name@from against this version |
| create_public_node_template_from_material | a DRAFT from a material's stored graph: expose (1..64 inputs, optional default), convertSource applied at publish; 422 invalid_expose lists error.problems |
| adopt_public_node_template | move structurally identical materials onto a published template made from a material; 409 node_template_not_adoptable |
| upgrade_public_node_template_materials | move materials between two public versions; 409 node_template_upgrade_breaking (error.breaking), 422 operation_too_large (over 200) |
| get_public_node_template_operation | an upgrade / adopt / convert operation: poll until complete is true |
adopt_public_node_template and upgrade_public_node_template_materials send
dryRun: true unless you pass dryRun: false explicitly; the API requires the
field. The plan comes back (per material: action, and the skip reason) and
nothing changes. Show the plan to the user, and run it for real only after they
confirm. A run refuses what it cannot do rather than skipping it silently. An
adopted material must render the same as before: a mean difference of at most
0.5/255 AND a 99th percentile of at most 4/255. Otherwise it fails with
adopt_render_mismatch and its graph is restored. publish_public_node_template
answers a publishReport (compatibility against the previous version, and
convertSource).
Tools
148 tools. The admin tools come on top, only with an admin
token. Every description opens with its price, and the paid ones also say
so in their first sentence and name their row in get_credits. The HTTP route
each one calls is at the end of its description.
Paid (spend the user's credits): submit_silo_render,
submit_silo_render_with_ai (plus four sceneAiCandidate charges when the
variants are styled), submit_render, submit_silo_batch,
retry_render, submit_room_render, submit_room_photo_render,
upload_room_photo, choose_room_removals (only when it removes something),
scene_ai_enhance, request_render_variants and refine_render_variant (their
charges land when each variant is styled). Every other tool is free.
| Area | Tools |
|---|---|
| Account | whoami, list_organizations, list_brands, get_features, get_credits, read_guide |
| Products | list_products, get_product, get_product_capabilities, get_product_configuration, preview_product_configuration, get_configurator_image |
| Materials | list_materials, get_material, create_material, create_material_from_choice, update_material, copy_material, delete_material (destructive), get_material_history, set_material_delegate, clear_material_delegate |
| Material textures | get_material_textures, upload_material_texture, clear_material_texture (destructive), generate_material_maps |
| Material look | render_material_preview, get_material_preview, compare_material_to_reference, list_reference_comparisons |
| Shader node graphs | set_material_node_graph, get_material_node_graph, get_node_graph_build, verify_node_graph_build |
| Node templates (read, preview) | list_node_templates, get_public_node_template_versions, get_public_node_template, preview_public_node_template, get_public_node_template_preview, get_brand_node_template_versions, get_brand_node_template, preview_brand_node_template, get_brand_node_template_preview |
| Brand node templates (brand-wide writes, free) | list_brand_node_template_versions, create_brand_node_template, update_brand_node_template, update_brand_node_template_docs, preview_brand_node_template_draft, publish_brand_node_template, deprecate_brand_node_template, delete_brand_node_template_draft (destructive, drafts only), fork_node_template_into_brand |
| Brand node templates, bulk (brand-wide, dry run first) | check_brand_node_template_compatibility, create_brand_node_template_from_material, adopt_brand_node_template, upgrade_brand_node_template_materials, get_brand_node_template_operation |
| Uploaded models | upload_asset, list_assets, get_asset, adjust_asset, approve_asset, rename_asset, set_asset_sharing, delete_asset (destructive), list_asset_parts, get_asset_part_label_images, infer_part_labels, set_part_label, get_part_materials, set_part_materials, clear_part_material |
| Model collections | list_asset_collections, create_asset_collection, get_asset_collection, rename_asset_collection, delete_asset_collection (destructive), add_assets_to_collection, remove_assets_from_collection |
| Backgrounds | list_backgrounds, list_my_backgrounds, upload_background, get_background_scene, delete_background (destructive), get_render_samples_lifestyle |
| Previews (free) | framing_preview, exposure_preview, get_exposure_preview, get_render_samples_silo |
| Silo shots | submit_silo_render (paid, no AI variants), submit_silo_render_with_ai (paid, + 4 AI variants) |
| Renders | submit_render (paid, lifestyle and raw pass-through), submit_silo_batch (paid, no AI variants), retry_render (paid), submit_room_render (paid), get_render_job, list_results, rename_render, delete_render (destructive, reversible), restore_render, get_render_original, get_render_input |
| AI on a render | request_render_variants (paid), list_render_variants, refine_render_variant (paid), approve_render_variant, submit_enhancement, get_enhancement, cancel_enhancement |
| Scene AI | scene_ai_enhance (paid), get_scene_ai_enhance, scene_ai_decompose, get_scene_ai_decompose |
| Room photo | list_room_photos, list_room_scenes, upload_room_photo (paid), get_room_photo, choose_room_removals (paid), reopen_room_selection, step_room_cleaning, approve_room_cleanup, reclean_room, restore_previous_room, get_room_scene, save_room_scene, set_room_scene_sharing, submit_room_photo_render (paid) |
| Albums | list_albums, create_album, get_album, update_album, delete_album (destructive, renders kept), add_to_album, remove_from_album, set_album_item_file_name, get_album_files |
| Saved framings, prompts, rooms | list_frames, create_frame, update_frame, delete_frame (destructive), list_prompts, create_prompt, update_prompt, delete_prompt (destructive), list_rooms, create_room, rename_room, delete_room (destructive) |
Each tool takes the query parameters its route reads, no more and no fewer —
and, where the route reads a form, exactly its form fields and file parts (the
BFF test suite fails when they drift). The one to know about is
brandId on list_materials: a token that carries several brands must name
one (whoami lists them), or the API refuses with a 422 whose brandIds
names them.
The server's serverInfo.version in the MCP handshake is read from this
package's package.json, so it always names the release that is running.
Routes without a tool
A public route has no tool only when an agent could not use it through one. The
BFF test suite holds this list (SKIPPED_ROUTES in
bff/tests/test_mcp_server_catalogue.py) and fails on any public route that is
neither a tool nor on it.
| Route | Why |
|---|---|
| GET /guide, /guide.md, /guide/summary | served by read_guide, the selfcare://guide resources and the standing instructions |
| GET/POST /pair/{code} | the pairing that produces this server's token — it runs before there is one |
| GET /me | the tenant's integration check; whoami and list_brands answer for the caller |
| GET /products/{id}/viewer-bootstrap | identifiers for the browser's 360 configurator embed |
| GET /assets/{id}/glb, GET /albums/{id}/download, GET /albums/{id}/items/{jobId}/download, GET /render-jobs/{id}/variants/{vid}/s/{token}/image | binary streams for the browser; the tools return the same files' URLs |
| GET /reconstruction/room-photo/{id}/original-photo, …/segmentation, …/planes, …/foreground, …/depth, …/depth-meta, …/cleaned-photo | same-origin copies for the browser canvas; get_room_photo returns each file's signed URL |
| POST/GET /sales-leads | the "Talk to sales" form — an enquiry to a person, which the user sends themselves |
Publishing
Published by the publish_mcp CI job, for maintainers:
- Bump
versioninpackage.jsonin a merge request, and merge it. - Tag the merged commit
mcp-vX.Y.Z, matching that version exactly. The job runs this package's tests, refuses a tag that disagrees withpackage.json, then runsnpm publish --access public.
An npm version cannot be reused or, after 72 hours, unpublished — so the tag is the release decision, not a formality.
