@marlinjai/studio-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Marlin Jai Pohl
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,241 @@
1
+ # @marlinjai/studio-mcp
2
+
3
+ A thin [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server over the **hosted Lumitra Studio HTTP API** (`studio.lumitra.co`). It gives any MCP client (Claude Code, Cursor, Claude Desktop, your own agent) typed tools for brand-consistent image generation, character minting, workflow authoring and runs, published recipes, and the still-swap video recipe, without cloning the Studio repo, without Postgres, without provider keys. The server is a pure HTTP client: every tool maps onto an existing `/api/**` route.
4
+
5
+ Every generation spends real money on the Studio's provider accounts (roughly $0.04 per image, $0.30 per character mint, $0.70 per video take). Treat the API key like a production credential.
6
+
7
+ ## Install
8
+
9
+ ### Claude Code
10
+
11
+ ```bash
12
+ # one-off, from npm (once published)
13
+ claude mcp add lumitra-studio \
14
+ --env LUMITRA_STUDIO_API_KEY=sk_live_... \
15
+ -- npx -y @marlinjai/studio-mcp
16
+
17
+ # from this monorepo (before publishing)
18
+ pnpm --filter @marlinjai/studio-mcp build
19
+ claude mcp add lumitra-studio \
20
+ --env LUMITRA_STUDIO_API_KEY=sk_live_... \
21
+ -- node /absolute/path/to/packages/studio-mcp/dist/cli.js
22
+ ```
23
+
24
+ If the key is already in your shell (for example injected by `cc.sh` as `LUMITRA_STUDIO_SERVICE_TOKEN`), drop the `--env` flag: the server reads the legacy variable too.
25
+
26
+ ### Cursor (`.cursor/mcp.json`) and Claude Desktop
27
+
28
+ ```json
29
+ {
30
+ "mcpServers": {
31
+ "lumitra-studio": {
32
+ "command": "npx",
33
+ "args": ["-y", "@marlinjai/studio-mcp"],
34
+ "env": {
35
+ "LUMITRA_STUDIO_API_KEY": "sk_live_...",
36
+ "LUMITRA_STUDIO_BASE_URL": "https://studio.lumitra.co"
37
+ }
38
+ }
39
+ }
40
+ }
41
+ ```
42
+
43
+ See [`examples/cursor-mcp.json`](examples/cursor-mcp.json).
44
+
45
+ ### Streamable HTTP (remote or shared)
46
+
47
+ ```bash
48
+ LUMITRA_STUDIO_API_KEY=sk_live_... studio-mcp --http --port 3939
49
+ # endpoint: http://127.0.0.1:3939/mcp health: http://127.0.0.1:3939/healthz
50
+ # bind elsewhere: --host 0.0.0.0
51
+ ```
52
+
53
+ Client side:
54
+
55
+ ```bash
56
+ claude mcp add --transport http lumitra-studio http://127.0.0.1:3939/mcp
57
+ ```
58
+
59
+ ```json
60
+ { "mcpServers": { "lumitra-studio": { "url": "http://127.0.0.1:3939/mcp" } } }
61
+ ```
62
+
63
+ The HTTP mode is stateless (one server instance per request, no sessions to expire). It carries no authentication of its own: the Studio key lives on the server process, so only expose the port on a network you trust (a Tailscale tailnet, a reverse proxy with auth). Run `node examples/smoke-http.mjs` to verify a build end to end without touching the real API.
64
+
65
+ ## Configuration
66
+
67
+ | Variable | Required | Default | Notes |
68
+ |---|---|---|---|
69
+ | `LUMITRA_STUDIO_API_KEY` | yes | | Bearer token for the Studio API. |
70
+ | `LUMITRA_STUDIO_SERVICE_TOKEN` | fallback | | Legacy name, accepted when `LUMITRA_STUDIO_API_KEY` is unset. |
71
+ | `LUMITRA_STUDIO_BASE_URL` | no | `https://studio.lumitra.co` | Point at a local Studio (`http://localhost:3000`) to test. |
72
+
73
+ An env value that is still a literal, unexpanded `${VAR}` placeholder (some plugin hosts only expand `${PLUGIN_ROOT}` and pass everything else through verbatim) is treated as unset and the variable is read from the server's own `process.env` instead.
74
+
75
+ The token is held in memory only. It is never logged, never serialized into a result, and every error message (including API bodies that echo request headers) is passed through a redactor before it reaches the client.
76
+
77
+ ## Tools
78
+
79
+ | Tool | What it does |
80
+ |---|---|
81
+ | `studio_list_projects` | List projects (id, slug, name). A `projectId` is needed by the workflow-based tools. |
82
+ | `studio_generate_image` | Submit a brand-styled image generation (optionally pinned to a character), wait for it, return the signed URL and cost. `wait=false` returns the `jobId`. |
83
+ | `studio_get_job` | Read a job's status, cost, error and fresh result URL; `wait=true` blocks until terminal. |
84
+ | `studio_list_characters` | List minted characters. |
85
+ | `studio_get_character` | One character with its frozen reference images (category + signed URL). |
86
+ | `studio_create_character` | Create an empty character record (for your own references). |
87
+ | `studio_mint_character` | Run the curated character-sheet workflow inline, wait, create the character, upload the 7 views as references. Returns `slug`, `referenceCount`, `costUsd`, `runId`. |
88
+ | `studio_create_brand` | Create a brand (style prefix + context). |
89
+ | `studio_upload_brand_reference` | Upload a reference image to a brand library from a local path or URL. |
90
+ | `studio_run_workflow` | Run an inline workflow DAG (or a saved one by `savedId`), with params, node subsets, repeat count and `visibility`; waits by default. |
91
+ | `studio_get_run` | Read a run: overall status, per-node status, URLs, cost, errors. |
92
+ | `studio_get_run_output` | Fresh signed URL for one node's output. |
93
+ | `studio_replace_character_in_video` | The proven still-swap recipe: image-edit the character into a scene frame, then image-to-video. Accepts `characterSlug` (full-body reference auto-resolved) or `characterReferenceUrl`, optional `endFrameUrl` (switches to Kling start+end keyframes). Returns `stillUrl`, `videoUrl`, `costUsd`, `runId`. |
94
+ | `studio_get_spend` | Spend ledger: today / 7-day / 30-day totals plus recent paid actions. |
95
+ | `studio_get_usage` | This key's tenant against its plan: month-to-date spend and job count, the monthly USD cap and requests-per-minute limit (`null` = unlimited), and a per-model breakdown. Call before a large batch to check remaining budget, or after a `402`/`429` to see what tripped. `usedUsd` includes in-flight (not yet settled) job estimates, so it can go DOWN as well as up: a job that later fails releases its estimate. |
96
+ | `studio_workflow_describe` | The node vocabulary: every task type with ports, prompt policy and runnability, the input types, the op types, the patch limits. Settings and models only with `includeSettings` / `includeModels`; filter with `tasks`. |
97
+ | `studio_workflow_get` | A saved workflow as a summary (nodes, wire counts, edge and group counts, `revision`, `readOnly`, `projection`), or the whole response with `full: true`. |
98
+ | `studio_workflow_create` | Build a new saved workflow from ops applied to an empty document. Returns `{ ok, savedId, revision, summary }`. |
99
+ | `studio_workflow_apply` | Change a saved workflow atomically on top of `baseRevision`. Returns counts, the affected nodes, the projection and `replayed`; every refusal (stale base, invalid patch, missing scope, not editable, lossy import, idempotency mismatch) comes back as a structured `{ ok: false, code, hint }` result. |
100
+ | `studio_workflow_validate` | Dry-run a document (inline or by `savedId`) with optional ops and run params: the Studio's `{ apply, document, projection, run }` verdict, no side effects, no cost. |
101
+ | `studio_workflow_screenshot` | Render a saved workflow's document as a diagram (labelled boxes and wires, positioned by the document's own layout), the Figma `get_screenshot` equivalent for a workflow graph. Not a pixel-exact canvas preview, a diagnostic diagram for checking topology. Returns `{ url, width, height }` (a 24h signed link). Free, read-only. |
102
+ | `studio_list_published` | List every published recipe: `{ recipes: [{ slug, label, publishedRevision, updatedAt, hasEffects }] }`. `hasEffects` tells you whether running it creates/attaches an entity record (a character, a brand reference) on success, versus a pure generation recipe. Free, read-only. |
103
+ | `studio_describe_published` | Describe one published recipe by slug: `{ slug, definition, hasEffects }`. `definition` is the FROZEN workflow (params, node shapes) exactly as it was at publish time, never the live authoring document. Read this before `studio_run_published` to know what params it needs. Free, read-only. |
104
+ | `studio_run_published` | Run a published recipe by slug against its frozen definition; `params` fill the definition's own param bindings. On success, the recipe's declared post-run effects fire automatically (e.g. minting creates the character and attaches its reference views); effects never fire on a failed or partial run. Each node is a paid job. |
105
+
106
+ All waiting tools accept `waitSeconds` (default 600) and back off between polls (2s doubling up to 15s). A timeout never cancels anything server-side: the tool error carries the id so the agent can poll again.
107
+
108
+ ### Tool signatures (the contract shared with the `lumitra-studio` plugin)
109
+
110
+ ```
111
+ studio_mint_character
112
+ in : { projectId, name, slug, description, descriptorPrompt?, negativePrompt?, defaultModel?,
113
+ seedModel?, viewModel?, visibility?: "listed"|"unlisted", waitSeconds? }
114
+ out: { slug, name, runId, referenceCount, references[{nodeId, category, referenceId, url}], costUsd }
115
+
116
+ studio_replace_character_in_video
117
+ in : { projectId, sceneFrameUrl, characterSlug? | characterReferenceUrl?, endFrameUrl?,
118
+ swapPrompt?, motionPrompt?, videoModel?, editModel?, aspectRatio?, visibility?, waitSeconds? }
119
+ out: { runId, status, stillUrl, videoUrl, costUsd, videoModel, characterReferenceUrl,
120
+ characterReferenceCategory, stillNodeId: "edit", videoNodeId: "video" }
121
+ ```
122
+
123
+ Reference categories written by the mint: `face`, `three-quarter`, `profile`, `full-body`, `close-up`, `lighting-warm`, `lighting-cool` (the Studio's canonical names, the same ones its in-app mint flow and try-on ranker use).
124
+
125
+ ### Authoring workflows: the command model
126
+
127
+ The six `studio_workflow_*` tools are the agent's side of the Studio's workflow command model (plan `docs/plans/2026-08-23-workflow-command-model.md`): the canvas, the HTTP API and this server all write through one revisioned, op-based path, so an agent can build a workflow, a human can fix it on the canvas, and the agent can append the next batch without anyone overwriting anyone. Authoring is free; only `studio_run_workflow` spends money.
128
+
129
+ ```
130
+ studio_workflow_describe what nodes exist, their ports, the op vocabulary
131
+ studio_workflow_create { ops, label? } -> { ok, savedId, revision, summary }
132
+ studio_workflow_get { savedId, full? } -> summary (revision = the baseRevision for apply)
133
+ studio_workflow_apply { savedId, baseRevision, ops, idempotencyKey?, acknowledgeLossy? }
134
+ -> { ok: true, revision, appliedCount, inverseCount, affectedNodes, projection, idempotencyKey, replayed }
135
+ -> { ok: false, code: "stale_revision", revision, summary } re-read, rebase, retry
136
+ -> { ok: false, code: "invalid_patch", error: { opIndex, code, message } }
137
+ -> { ok: false, code: "forbidden", opIndex, missingScope }
138
+ -> { ok: false, code: "not_editable", issues } not patchable at all
139
+ -> { ok: false, code: "lossy_import", notes } resend with acknowledgeLossy: true
140
+ -> { ok: false, code: "idempotency_mismatch", revision } use a fresh key
141
+ -> { ok: false, code: "network_error" | "server_error", idempotencyKey } retry with the SAME key
142
+ studio_workflow_validate { document | savedId, ops?, params? }
143
+ -> { apply, document, projection, run } as the Studio reports it
144
+ studio_run_workflow { projectId, savedId, params }
145
+ ```
146
+
147
+ Ops are the package's closed vocabulary (`addNode`, `removeNode`, `setNodeProps`, `setNodeSetting`, `setBakedInput`, `connect`, `disconnect`, `exposeAsInput`, `freezeInput`, `setActiveGeneration`, `setLayout`, `group`, `ungroup`, `setGroupProps`, `setDocumentProps`), at most 500 per patch, applied all-or-nothing. The tool input schemas are generated from the package's own Zod schemas, so an MCP client sees exactly which fields every op kind has and a malformed op is refused before any request is made. Node ids are yours to choose (UUIDv7 recommended); a later op in the same patch may reference an id added earlier.
148
+
149
+ A summary never carries the whole document: per node it lists `id`, `kind`, the task and model, the first 80 characters of a prompt (with `promptLength`), an input's `name` / `type` / `required`, and wire counts; per document the edge and group counts and the `projection` (whether it can run, and the per-node issues when it cannot). Ask for `full: true` on `studio_workflow_get` when you need every field.
150
+
151
+ Every `studio_workflow_apply` carries an idempotency key (generated when you omit one, returned in the result). After a network or server failure, retry with the SAME key: the Studio returns the original result if the first attempt landed (`replayed: true` in the success result), so a patch can never apply twice. A rebased patch after `stale_revision` is a new patch and gets a new key; reusing a key for different ops is an `idempotency_mismatch` refusal, not a replay.
152
+
153
+ Two workflows behave differently from a document built through this contract:
154
+
155
+ - **Read-only (API-only):** a workflow whose stored definition binds a run param the canvas cannot wire has no document. `studio_workflow_get` returns `{ readOnly: true, definitionNodeCount, projection, hint }` with no nodes, `studio_workflow_apply` refuses with `not_editable`, and `studio_workflow_validate` by `savedId` says so instead of validating nothing. It stays readable and runnable through `studio_run_workflow` with its `savedId`.
156
+ - **Lossy import:** a workflow saved before the command model whose import drops a binding the document cannot carry (an `endImageUrl` end frame, say) is patchable, but its FIRST patch must confirm the loss. `studio_workflow_get` flags it with `needsLossyAcknowledgement: true` and lists the `notes`; sending the patch without acknowledgement is refused with `lossy_import`. Read the notes, then resend the same ops with `acknowledgeLossy: true` and a new idempotency key.
157
+
158
+ ### Recipes: a published workflow with automatic follow-up
159
+
160
+ A **recipe** is a workflow that has been *published* under a stable public slug: its document and an optional declared list of post-run effects (create a character, attach a reference image, create a brand) are frozen together at publish time. `studio_list_published` / `studio_describe_published` / `studio_run_published` are the derived, read-only-until-you-run surface over every recipe that exists: the same three tools serve every recipe ever published, so the surface never grows as recipes are added.
161
+
162
+ Running a recipe (`studio_run_published`) always executes the FROZEN definition, never the live authoring document (which may have moved on since publish). If every node in the run succeeds, the recipe's declared effects fire automatically and exactly once; a failed or partial run never triggers them, so a broken run can never leave a half-created character or a reference attached to nothing. Publishing a workflow (setting the slug and the effect list) is not yet exposed as an MCP tool: it is done through the HTTP API (`POST /api/v1/workflows/:savedId/publish`) or on the canvas.
163
+
164
+ `studio_mint_character` and `studio_replace_character_in_video` (above) are today still hand-written tools, not recipes; migrating them onto published recipe twins is planned but not yet done, so both surfaces currently coexist.
165
+
166
+ ## Examples
167
+
168
+ Prompts you can give an agent once the server is connected:
169
+
170
+ ```text
171
+ List my Studio projects, then mint a character named "Bruno Hartmann" (slug bruno-hartmann)
172
+ in the default project: a man in his early 50s, short gray hair, trimmed gray beard,
173
+ weathered tanned skin, deep-set brown eyes, broad shoulders.
174
+ ```
175
+
176
+ ```text
177
+ Generate a 16:9 image for brand "busbasis" with Bruno at a workbench in a bright workshop,
178
+ model kie/nano-banana-2, and tell me the cost.
179
+ ```
180
+
181
+ ```text
182
+ Here is a frame from our header video: https://.../frame-012.png
183
+ Put bruno-hartmann into that shot and give me the clip. Use the full-body reference.
184
+ ```
185
+
186
+ ```text
187
+ Run this workflow in project <id>: a text-to-image node "hero" (fal/nano-banana-2) with a param
188
+ prompt, then an image-to-video node "clip" (fal/seedance-pro-image-to-video) whose inputImageUrl
189
+ refs hero.url. Params: { prompt: "..." }. Mark the run unlisted.
190
+ ```
191
+
192
+ ```text
193
+ How much did we spend on the Studio in the last 7 days?
194
+ ```
195
+
196
+ ```text
197
+ List every published recipe, describe the one called "character-sheet-mint",
198
+ then run it in project <id> with the params it needs.
199
+ ```
200
+
201
+ ```text
202
+ Describe the text-to-image and image-to-video node types, then create a workflow "Harbour
203
+ reel": a text input "prompt", a text-to-image node on fal/nano-banana-2 wired from it, and an
204
+ image-to-video node on fal/seedance-pro-image-to-video wired from the image. Validate it with
205
+ params { prompt: "a fisherman at dawn" }, then run it.
206
+ ```
207
+
208
+ Files in [`examples/`](examples/):
209
+
210
+ - `cursor-mcp.json`: drop-in MCP config for Cursor / Claude Desktop.
211
+ - `claude-code.sh`: the `claude mcp add` one-liners (stdio and HTTP).
212
+ - `smoke-http.mjs`: starts the built server in HTTP mode on a random port and checks the tool list.
213
+ - `workflow-character-sheet.json`: the inline character-sheet definition the mint tool posts, for reference or for hand-running with `studio_run_workflow`.
214
+
215
+ ## Programmatic use
216
+
217
+ ```ts
218
+ import { createStudioMcpServer, StudioClient, resolveConfig } from '@marlinjai/studio-mcp';
219
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
220
+
221
+ const { server } = createStudioMcpServer(); // reads env
222
+ await server.connect(new StdioServerTransport());
223
+
224
+ // or just the HTTP client
225
+ const client = new StudioClient(resolveConfig());
226
+ const { projects } = await client.listProjects();
227
+ ```
228
+
229
+ ## Development
230
+
231
+ ```bash
232
+ pnpm --filter @marlinjai/studio-core build # the parity spec imports the real prompt builders
233
+ pnpm --filter @marlinjai/studio-mcp typecheck
234
+ pnpm --filter @marlinjai/studio-mcp test
235
+ pnpm --filter @marlinjai/studio-mcp build
236
+ node packages/studio-mcp/examples/smoke-http.mjs
237
+ ```
238
+
239
+ The curated `character-sheet` definition and `buildCharacterSheetParams` are ported into `src/characterSheet.ts` as plain data (the hosted run route does not resolve curated `definitionId`s, so the mint must POST the inline definition). A parity test asserts the port stays byte-identical to `@marlinjai/studio-core`.
240
+
241
+ The workflow tools import the package's schemas directly (`OpSchema`, `PatchSchema`, `WorkflowDocumentSchema` from `@marlinjai/studio-core/workflow`, one source of truth with the Studio). `@marlinjai/studio-core` is a private workspace package that is not on npm, and the plugin starts this server with `npx -y @marlinjai/studio-mcp`, so it must never become a runtime dependency: `tsup.config.ts` inlines its code (`noExternal`) and its types (a `dts` alias onto the built declarations) into `dist/`. The bundle's only runtime imports stay the MCP SDK, `zod` and Node built-ins; `src/__tests__/package.spec.ts` pins this. Build `@marlinjai/studio-core` before this package (the workspace `pnpm -r --filter './packages/*' build` does it in order).