@craftstory/mcp 0.1.1 → 0.1.3
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/CHANGELOG.md +23 -0
- package/README.md +21 -9
- package/dist/client.js +19 -12
- package/dist/server.js +34 -7
- package/package.json +12 -5
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.3 - 2026-09-12
|
|
4
|
+
- tool annotations (readOnlyHint / destructiveHint) on every tool; Privacy Policy section in the README
|
|
5
|
+
- MCPB bundle for Claude Desktop (manifest.json, icon) attached to GitHub releases
|
|
6
|
+
- `mcpName` for the official MCP Registry; server.json
|
|
7
|
+
|
|
8
|
+
## 0.1.2 - 2026-09-12
|
|
9
|
+
- package metadata: public repository and issue tracker on GitHub (itseez3d/craftstory-mcp); CHANGELOG shipped
|
|
10
|
+
- wait_for_job: every request inside the wait is capped by the time left, so the call really returns within timeout_s (+ a few seconds for the result fetch)
|
|
11
|
+
- create_audio_clip rejects ambiguous input (both voice ids, or a file together with text)
|
|
12
|
+
- image_path / file_path accept ~/ and are documented as absolute paths
|
|
13
|
+
- CRAFTSTORY_API_BASE must be https unless CRAFTSTORY_ALLOW_HTTP=1
|
|
14
|
+
- Node >= 20 (matches the dependency tree); prepack builds; SECURITY.md; GitHub Actions CI
|
|
15
|
+
|
|
16
|
+
## 0.1.1 - 2026-09-12
|
|
17
|
+
- wait_for_job: default 45 s, max 55 s, deadline-aware polling (stays under the 60 s tool-call limit of most MCP clients)
|
|
18
|
+
- every API request has a 25 s HTTP timeout; network errors report their cause
|
|
19
|
+
- removed an internal repository URL from package metadata (0.1.0 is deprecated for that reason)
|
|
20
|
+
|
|
21
|
+
## 0.1.0 - 2026-09-12
|
|
22
|
+
- first release: 11 tools over the CraftStory public API (models, voices, avatars, audio clips, cost preview,
|
|
23
|
+
CraftStory 2.0 and MiniMax H3 generation, status, result, bounded wait, upscale) and one prompt
|
package/README.md
CHANGED
|
@@ -5,19 +5,21 @@ Generate talking-avatar videos from Claude, Cursor, Claude Code or any other [MC
|
|
|
5
5
|
- **CraftStory 2.0** - a talking video of any length from one photo plus an audio clip (script + voice, your own recording, or a custom avatar). 8-15 minutes per video.
|
|
6
6
|
- **MiniMax H3** - a clip of up to 15 s from one photo: description-driven with generated sound, or audio-driven with lip-sync. 1-3 minutes.
|
|
7
7
|
|
|
8
|
-
You need a CraftStory account on a plan with API access and an API key (app: **Account -> API Access**, keys look like `sk-cs-...`). Generations are billed in credits exactly like in the app; failed jobs are refunded.
|
|
8
|
+
You need Node.js 20 or newer, a CraftStory account on a plan with API access, and an API key (app: **Account -> API Access**, keys look like `sk-cs-...`). Generations are billed in credits exactly like in the app; failed jobs are refunded.
|
|
9
9
|
|
|
10
10
|
## Install
|
|
11
11
|
|
|
12
12
|
### Claude Code
|
|
13
13
|
|
|
14
14
|
```bash
|
|
15
|
-
claude mcp add craftstory -e CRAFTSTORY_API_KEY=sk-cs-... -- npx -y @craftstory/mcp
|
|
15
|
+
claude mcp add --scope user craftstory -e CRAFTSTORY_API_KEY=sk-cs-... -- npx -y @craftstory/mcp
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
+
(`--scope user` makes it available in every project; drop it to install for the current project only.)
|
|
19
|
+
|
|
18
20
|
### Claude Desktop
|
|
19
21
|
|
|
20
|
-
Add to `claude_desktop_config.json` (Settings -> Developer -> Edit Config):
|
|
22
|
+
Add to `claude_desktop_config.json` (Settings -> Developer -> Edit Config), then restart Claude Desktop; the server shows up under the tools icon:
|
|
21
23
|
|
|
22
24
|
```json
|
|
23
25
|
{
|
|
@@ -33,9 +35,9 @@ Add to `claude_desktop_config.json` (Settings -> Developer -> Edit Config):
|
|
|
33
35
|
|
|
34
36
|
### Cursor / other clients
|
|
35
37
|
|
|
36
|
-
Same shape in
|
|
38
|
+
Same shape in `~/.cursor/mcp.json` (user-level, so the key never lands in a repository): command `npx`, args `["-y", "@craftstory/mcp"]`, env `CRAFTSTORY_API_KEY`. In a project-level `.cursor/mcp.json` prefer `"CRAFTSTORY_API_KEY": "${env:CRAFTSTORY_API_KEY}"` and keep the real key in your shell environment.
|
|
37
39
|
|
|
38
|
-
Environment variables: `CRAFTSTORY_API_KEY` (required), `CRAFTSTORY_API_BASE` (optional, default `https://api.craftstory.com/api/v1`).
|
|
40
|
+
Environment variables: `CRAFTSTORY_API_KEY` (required), `CRAFTSTORY_API_BASE` (optional, default `https://api.craftstory.com/api/v1`; must be https unless `CRAFTSTORY_ALLOW_HTTP=1`).
|
|
39
41
|
|
|
40
42
|
## Tools
|
|
41
43
|
|
|
@@ -57,27 +59,37 @@ Plus the prompt `talking_video_from_photo` (script + photo) that walks the model
|
|
|
57
59
|
|
|
58
60
|
## Example
|
|
59
61
|
|
|
60
|
-
> Make a
|
|
62
|
+
> Make a portrait video of the person in `/Users/me/photos/portrait.jpg` saying "Welcome to our spring collection", calm gestures.
|
|
61
63
|
|
|
62
64
|
The assistant will: `list_voices` -> `create_audio_clip` -> `wait_for_job(audio-clip)` -> `preview_cost` -> `create_craftstory2_video` (resolution `720_1280`, gestures `calm`) -> `wait_for_job(craftstory-2)` a few times -> `get_job_result` -> the video URL.
|
|
63
65
|
|
|
64
|
-
Long jobs: `wait_for_job` returns
|
|
66
|
+
Long jobs: `wait_for_job` returns within `timeout_s` (default 45 s, max 55 s) plus a few seconds for the final result fetch; every request inside it is capped by the time left, so it stays under the 60 s tool-call limit of most clients. A CraftStory 2.0 video needs several calls; that is by design so agent runtimes do not time out. A CraftStory 2.0 video needs several calls; that is by design so agent runtimes do not time out.
|
|
65
67
|
|
|
66
68
|
## Local files vs URLs
|
|
67
69
|
|
|
68
|
-
Photos accept `image_url` or `image_path
|
|
70
|
+
Photos accept `image_url` (JPG/PNG) or `image_path` (absolute path or `~/...`; JPG/PNG/HEIC, up to 20 MB, uploaded as multipart). Recordings and extra references are local paths too. Anything you pass as a path is uploaded to the CraftStory API, so keep your client's tool-approval prompts on (see SECURITY.md).
|
|
69
71
|
|
|
70
72
|
## Development
|
|
71
73
|
|
|
72
74
|
```bash
|
|
73
75
|
npm install
|
|
74
76
|
npm run build
|
|
75
|
-
CRAFTSTORY_API_KEY=sk-cs-... npm run smoke # live check over stdio
|
|
77
|
+
CRAFTSTORY_API_KEY=sk-cs-... npm run smoke # live check over stdio: lists tools, makes an audio clip, previews cost
|
|
78
|
+
# add "-- --h3" and SMOKE_IMAGE=/path/to/photo.jpg to also render a 5 s MiniMax H3 clip (costs 17 credits)
|
|
76
79
|
npm test
|
|
77
80
|
```
|
|
78
81
|
|
|
79
82
|
Full API reference: https://api.craftstory.com/api/v1/docs/public/ and the curl walkthrough at https://api.craftstory.com/api/v1/docs/samples/curl/.
|
|
80
83
|
|
|
84
|
+
## Privacy Policy
|
|
85
|
+
|
|
86
|
+
This server runs on your machine and keeps no data of its own. It sends your API key and the inputs you pass to tools
|
|
87
|
+
(text, photos, audio, reference files) only to the CraftStory API at `CRAFTSTORY_API_BASE` (default
|
|
88
|
+
`https://api.craftstory.com`), where they are processed under the CraftStory privacy policy:
|
|
89
|
+
https://craftstory.com/privacy/. Generated videos and uploaded files are stored in your CraftStory account and can be
|
|
90
|
+
deleted there or via the API; nothing is retained locally by the server. No analytics or third-party services are
|
|
91
|
+
called by the server itself. Questions: support via https://craftstory.com/contacts/.
|
|
92
|
+
|
|
81
93
|
## License
|
|
82
94
|
|
|
83
95
|
MIT
|
package/dist/client.js
CHANGED
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
* Every method maps 1:1 onto a documented endpoint; no business logic lives here.
|
|
4
4
|
*/
|
|
5
5
|
import { readFile } from "node:fs/promises";
|
|
6
|
+
import { homedir } from "node:os";
|
|
6
7
|
import { basename } from "node:path";
|
|
7
8
|
export const DEFAULT_BASE = "https://api.craftstory.com/api/v1";
|
|
8
|
-
export const USER_AGENT = "craftstory-mcp/0.1.
|
|
9
|
+
export const USER_AGENT = "craftstory-mcp/0.1.3";
|
|
9
10
|
/** Per-request HTTP timeout; keeps every tool call well under MCP clients' ~60 s limit. */
|
|
10
11
|
export const REQUEST_TIMEOUT_MS = 25_000;
|
|
11
12
|
export class ApiError extends Error {
|
|
@@ -24,8 +25,9 @@ export async function fileOrUrl(input, field) {
|
|
|
24
25
|
if (input.url)
|
|
25
26
|
return { url: input.url };
|
|
26
27
|
if (input.path) {
|
|
27
|
-
const
|
|
28
|
-
|
|
28
|
+
const path = input.path.startsWith("~/") ? homedir() + input.path.slice(1) : input.path;
|
|
29
|
+
const bytes = await readFile(path);
|
|
30
|
+
return { blob: new Blob([bytes]), name: basename(path) };
|
|
29
31
|
}
|
|
30
32
|
throw new Error(`${field}: a URL or a local file path is required`);
|
|
31
33
|
}
|
|
@@ -36,9 +38,14 @@ export class CraftStoryClient {
|
|
|
36
38
|
constructor(opts) {
|
|
37
39
|
this.opts = opts;
|
|
38
40
|
this.base = (opts.baseUrl ?? DEFAULT_BASE).replace(/\/+$/, "");
|
|
41
|
+
// The bearer key and uploads travel to this host: plaintext HTTP only on explicit request.
|
|
42
|
+
if (!this.base.startsWith("https://") && process.env.CRAFTSTORY_ALLOW_HTTP !== "1") {
|
|
43
|
+
throw new Error(`CRAFTSTORY_API_BASE must use https:// (got ${this.base}); set CRAFTSTORY_ALLOW_HTTP=1 to override for local testing`);
|
|
44
|
+
}
|
|
39
45
|
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
40
46
|
}
|
|
41
|
-
|
|
47
|
+
/** timeoutMs caps this one request (default REQUEST_TIMEOUT_MS); wait_for_job passes its remaining budget. */
|
|
48
|
+
async request(method, path, body, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
42
49
|
const headers = { Authorization: `Bearer ${this.opts.apiKey}`, "User-Agent": USER_AGENT, Accept: "application/json" };
|
|
43
50
|
let payload;
|
|
44
51
|
if (body instanceof FormData)
|
|
@@ -49,12 +56,12 @@ export class CraftStoryClient {
|
|
|
49
56
|
}
|
|
50
57
|
let res;
|
|
51
58
|
try {
|
|
52
|
-
res = await this.fetchImpl(`${this.base}${path}`, { method, headers, body: payload, signal: AbortSignal.timeout(
|
|
59
|
+
res = await this.fetchImpl(`${this.base}${path}`, { method, headers, body: payload, signal: AbortSignal.timeout(Math.max(1000, timeoutMs)) });
|
|
53
60
|
}
|
|
54
61
|
catch (e) {
|
|
55
62
|
// Node's fetch hides the reason behind "fetch failed"; surface the cause (DNS, TLS, reset...).
|
|
56
63
|
if (e.name === "TimeoutError")
|
|
57
|
-
throw new Error(`Timeout after ${
|
|
64
|
+
throw new Error(`Timeout after ${Math.round(Math.max(1000, timeoutMs) / 1000)}s calling ${method} ${path}`);
|
|
58
65
|
const cause = e.cause;
|
|
59
66
|
throw new Error(`Network error calling ${method} ${path}: ${cause?.code ?? ""} ${cause?.message ?? e.message}`.trim());
|
|
60
67
|
}
|
|
@@ -70,8 +77,8 @@ export class CraftStoryClient {
|
|
|
70
77
|
throw new ApiError(res.status, data, describeError(res.status, data));
|
|
71
78
|
return data;
|
|
72
79
|
}
|
|
73
|
-
get(path) {
|
|
74
|
-
return this.request("GET", path);
|
|
80
|
+
get(path, timeoutMs) {
|
|
81
|
+
return this.request("GET", path, undefined, timeoutMs);
|
|
75
82
|
}
|
|
76
83
|
post(path, body) {
|
|
77
84
|
return this.request("POST", path, body);
|
|
@@ -163,11 +170,11 @@ export class CraftStoryClient {
|
|
|
163
170
|
jobPath(kind, id) {
|
|
164
171
|
return kind === "audio-clip" ? `/audio/clips/${id}/` : `/${kind}/${id}/`;
|
|
165
172
|
}
|
|
166
|
-
getStatus(kind, id) {
|
|
167
|
-
return this.get(`${this.jobPath(kind, id)}status
|
|
173
|
+
getStatus(kind, id, timeoutMs) {
|
|
174
|
+
return this.get(`${this.jobPath(kind, id)}status/`, timeoutMs);
|
|
168
175
|
}
|
|
169
|
-
getResult(kind, id) {
|
|
170
|
-
return this.get(this.jobPath(kind, id));
|
|
176
|
+
getResult(kind, id, timeoutMs) {
|
|
177
|
+
return this.get(this.jobPath(kind, id), timeoutMs);
|
|
171
178
|
}
|
|
172
179
|
}
|
|
173
180
|
/** Terminal-state classification shared by wait_for_job and the tool descriptions. */
|
package/dist/server.js
CHANGED
|
@@ -14,9 +14,10 @@ const JOB_KINDS = ["craftstory-2", "minimax-h3", "audio-clip"];
|
|
|
14
14
|
const text = (data) => ({ content: [{ type: "text", text: typeof data === "string" ? data : JSON.stringify(data, null, 2) }] });
|
|
15
15
|
const fail = (err) => ({ isError: true, content: [{ type: "text", text: err instanceof Error ? err.message : String(err) }] });
|
|
16
16
|
export function buildServer(client) {
|
|
17
|
-
const server = new McpServer({ name: "craftstory", version: "0.1.
|
|
17
|
+
const server = new McpServer({ name: "craftstory", version: "0.1.3" });
|
|
18
18
|
server.registerTool("list_models", {
|
|
19
19
|
title: "List CraftStory video models",
|
|
20
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
20
21
|
description: "Catalogue of the video models behind this server with their status, modes, limits and credit prices. " +
|
|
21
22
|
"Two models today: craftstory-2 (a talking video of any length from one photo plus an audio clip; 8-15 min) " +
|
|
22
23
|
"and minimax-h3 (a clip of up to 15 s from one photo, either description-driven with generated sound or audio-driven with lip-sync; 1-3 min). " +
|
|
@@ -32,6 +33,7 @@ export function buildServer(client) {
|
|
|
32
33
|
});
|
|
33
34
|
server.registerTool("list_voices", {
|
|
34
35
|
title: "List voices for text-to-speech",
|
|
36
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
35
37
|
description: "Library voices (id, name, language, gender) usable as voice_id in create_audio_clip. " +
|
|
36
38
|
"With include_cloned=true also returns the account's own cloned voices, usable as voice_user_id. Voices are cloned in the CraftStory app, not via the API.",
|
|
37
39
|
inputSchema: { include_cloned: z.boolean().optional().describe("Also return the account's cloned voices (default false)") },
|
|
@@ -47,6 +49,7 @@ export function buildServer(client) {
|
|
|
47
49
|
});
|
|
48
50
|
server.registerTool("list_avatars", {
|
|
49
51
|
title: "List custom avatars (and their scenes)",
|
|
52
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
50
53
|
description: "Custom avatars trained in the CraftStory app that craftstory-2 can generate with (pass an id as avatar_id). " +
|
|
51
54
|
"Each avatar may carry a default voice {id, voice_kind}: voice_kind 'user' means send it as voice_user_id, 'library' as voice_id in create_audio_clip. " +
|
|
52
55
|
"Pass avatar_id to list that avatar's scenes; a scene id can replace the photo (scene_id) in create_craftstory2_video.",
|
|
@@ -61,6 +64,7 @@ export function buildServer(client) {
|
|
|
61
64
|
});
|
|
62
65
|
server.registerTool("create_audio_clip", {
|
|
63
66
|
title: "Create an audio clip (speech from text, or upload a recording)",
|
|
67
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
64
68
|
description: "The soundtrack every video model takes as input. Either text (up to 2000 characters) plus exactly one voice (voice_id from list_voices, or voice_user_id for a cloned voice), " +
|
|
65
69
|
"or file_path to upload a local WAV/MP3/M4A recording. Returns the clip id; it is ready when wait_for_job(model='audio-clip') reports done (usually seconds). " +
|
|
66
70
|
"Longer scripts: create several clips and pass all ids to create_craftstory2_video in order.",
|
|
@@ -72,10 +76,14 @@ export function buildServer(client) {
|
|
|
72
76
|
},
|
|
73
77
|
}, async ({ text: script, voice_id, voice_user_id, file_path }) => {
|
|
74
78
|
try {
|
|
79
|
+
if (file_path && (script || voice_id || voice_user_id))
|
|
80
|
+
throw new Error("Pass either file_path or text + one voice, not both");
|
|
75
81
|
if (file_path)
|
|
76
82
|
return text(await client.createAudioClipFromFile(file_path));
|
|
77
83
|
if (!script)
|
|
78
84
|
throw new Error("Pass text (with a voice) or file_path");
|
|
85
|
+
if (voice_id && voice_user_id)
|
|
86
|
+
throw new Error("Pass exactly one of voice_id or voice_user_id");
|
|
79
87
|
if (!voice_id && !voice_user_id)
|
|
80
88
|
throw new Error("Pass voice_id (library voice) or voice_user_id (cloned voice) with text");
|
|
81
89
|
return text(await client.createAudioClipFromText(script, voice_id ? { voice_id } : { voice_user_id }));
|
|
@@ -86,6 +94,7 @@ export function buildServer(client) {
|
|
|
86
94
|
});
|
|
87
95
|
server.registerTool("preview_cost", {
|
|
88
96
|
title: "Estimate the credit cost of a CraftStory 2.0 video",
|
|
97
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
89
98
|
description: "Credits a craftstory-2 job would cost for the given audio clips and settings, without creating anything. " +
|
|
90
99
|
"Rate per second of audio: 480p 2.2 (2 with lipsync_mode=empty), 720p 3.3 (3 with empty); rounded up per job. MiniMax H3 is a flat 3.3 credits per billed second.",
|
|
91
100
|
inputSchema: {
|
|
@@ -103,13 +112,14 @@ export function buildServer(client) {
|
|
|
103
112
|
});
|
|
104
113
|
server.registerTool("create_craftstory2_video", {
|
|
105
114
|
title: "Create a CraftStory 2.0 talking video (photo + audio)",
|
|
115
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
106
116
|
description: "Start a craftstory-2 generation: a photo of a person (image_url or image_path, or a custom avatar scene via scene_id) speaks the given audio clips with lip-sync, gestures and natural motion; any length. " +
|
|
107
117
|
"resolution is WIDTH_HEIGHT (480_832 / 720_1280 portrait, 832_480 / 1280_720 landscape); 1080p is available afterwards via upscale_video. " +
|
|
108
118
|
"Credits are charged on create (see preview_cost) and refunded if the job fails. Returns the job id and initial status; generation takes 8-15 minutes, " +
|
|
109
119
|
"so call wait_for_job(model='craftstory-2') repeatedly until it reports done, then get_job_result for the video URL.",
|
|
110
120
|
inputSchema: {
|
|
111
121
|
image_url: z.string().url().optional().describe("Public URL of the photo (JPG/PNG)"),
|
|
112
|
-
image_path: z.string().optional().describe("
|
|
122
|
+
image_path: z.string().optional().describe("Absolute local path of the photo to upload (JPG/PNG/HEIC, <= 20 MB); ~/ is expanded"),
|
|
113
123
|
scene_id: z.string().uuid().optional().describe("Custom avatar scene id (from list_avatars) used instead of a photo"),
|
|
114
124
|
avatar_id: z.string().uuid().optional().describe("Custom avatar id (from list_avatars); its trained model drives identity"),
|
|
115
125
|
audio_clip_ids: z.array(z.string().uuid()).min(1).describe("Audio clip ids (from create_audio_clip), played in order"),
|
|
@@ -144,6 +154,7 @@ export function buildServer(client) {
|
|
|
144
154
|
});
|
|
145
155
|
server.registerTool("create_minimax_h3_video", {
|
|
146
156
|
title: "Create a MiniMax H3 clip (up to 15 s)",
|
|
157
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
147
158
|
description: "Start a minimax-h3 generation from one photo. mode='basic': user_prompt (scene description) + requested_duration_s (5-15); the model animates the photo and generates the soundtrack itself. " +
|
|
148
159
|
"mode='reference': one audio_clip_id drives the clip with lip-sync (first 15 s billed); user_prompt is optional; up to 8 extra image / 3 video / 2 audio reference_files with reference_captions keep a product or background consistent. " +
|
|
149
160
|
"Output is 768 px on the short side, orientation follows the photo. Cost 3.3 credits per billed second, charged on create. Returns the job id; call wait_for_job(model='minimax-h3') until done (1-3 min).",
|
|
@@ -184,6 +195,7 @@ export function buildServer(client) {
|
|
|
184
195
|
});
|
|
185
196
|
server.registerTool("get_job_status", {
|
|
186
197
|
title: "Get a job's status",
|
|
198
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
187
199
|
description: "Status of a video job or audio clip: status, status_percentage, status_failed, credits_refunded. Terminal states: done; failed*, rejected_* and not_pass_moderation (audio) are failures. " +
|
|
188
200
|
"Prefer wait_for_job, which polls for you.",
|
|
189
201
|
inputSchema: { model: z.enum(JOB_KINDS), id: z.string().uuid() },
|
|
@@ -197,6 +209,7 @@ export function buildServer(client) {
|
|
|
197
209
|
});
|
|
198
210
|
server.registerTool("get_job_result", {
|
|
199
211
|
title: "Get a finished job (video URL and details)",
|
|
212
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
200
213
|
description: "Full record of a job. For craftstory-2 the video is in `video`, for minimax-h3 in `video_url`, for audio clips in `file`; all are signed URLs valid for 7 days (call again for a fresh link). Also returns the parameters used and the credits charged.",
|
|
201
214
|
inputSchema: { model: z.enum(JOB_KINDS), id: z.string().uuid() },
|
|
202
215
|
}, async ({ model, id }) => {
|
|
@@ -209,6 +222,7 @@ export function buildServer(client) {
|
|
|
209
222
|
});
|
|
210
223
|
server.registerTool("wait_for_job", {
|
|
211
224
|
title: "Wait for a job (bounded polling)",
|
|
225
|
+
annotations: { readOnlyHint: true, openWorldHint: true },
|
|
212
226
|
description: "Polls a job's status every few seconds for up to timeout_s (default 45, max 55 - most MCP clients cut a tool call at 60 s) and returns as soon as it is terminal. " +
|
|
213
227
|
"If it returns state='running', call it again - craftstory-2 jobs take 8-15 minutes, minimax-h3 1-3 minutes, audio clips seconds. Reports progress notifications when the client supports them.",
|
|
214
228
|
inputSchema: {
|
|
@@ -220,11 +234,15 @@ export function buildServer(client) {
|
|
|
220
234
|
const deadline = Date.now() + (timeout_s ?? 45) * 1000;
|
|
221
235
|
const token = extra._meta?.progressToken;
|
|
222
236
|
let last;
|
|
237
|
+
const running = () => text({ state: "running", ...last, hint: "still running - call wait_for_job again" });
|
|
223
238
|
try {
|
|
224
239
|
while (true) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
240
|
+
// Every request is capped by the time left, so the call returns within timeout_s
|
|
241
|
+
// (plus at most a few seconds for the final result fetch).
|
|
242
|
+
const budget = deadline - Date.now();
|
|
243
|
+
if (budget <= 0 && last)
|
|
244
|
+
return running();
|
|
245
|
+
last = await client.getStatus(model, id, Math.min(25_000, Math.max(3_000, budget)));
|
|
228
246
|
const state = classify(last.status);
|
|
229
247
|
if (token !== undefined) {
|
|
230
248
|
await extra.sendNotification({
|
|
@@ -233,12 +251,20 @@ export function buildServer(client) {
|
|
|
233
251
|
});
|
|
234
252
|
}
|
|
235
253
|
if (state !== "running") {
|
|
236
|
-
|
|
254
|
+
let result;
|
|
255
|
+
if (state === "done") {
|
|
256
|
+
try {
|
|
257
|
+
result = await client.getResult(model, id, Math.min(10_000, Math.max(3_000, deadline - Date.now())));
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return text({ state, ...last, hint: "finished - call get_job_result for the video URL" });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
237
263
|
return text({ state, ...last, ...(result ? { result } : {}) });
|
|
238
264
|
}
|
|
239
265
|
const remaining = deadline - Date.now();
|
|
240
266
|
if (remaining <= 0)
|
|
241
|
-
return
|
|
267
|
+
return running();
|
|
242
268
|
await new Promise((r) => setTimeout(r, Math.min(model === "audio-clip" ? 2000 : 5000, remaining)));
|
|
243
269
|
}
|
|
244
270
|
}
|
|
@@ -248,6 +274,7 @@ export function buildServer(client) {
|
|
|
248
274
|
});
|
|
249
275
|
server.registerTool("upscale_video", {
|
|
250
276
|
title: "Upscale a finished video (new job)",
|
|
277
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
251
278
|
description: "Creates a NEW job with the upscaled result; the original stays. craftstory-2: only 720p sources, resolution 1080_1920 (from 720_1280) or 1920_1080 (from 1280_720). minimax-h3: always 2x, no resolution needed. " +
|
|
252
279
|
"Costs 0.2 credits per second. Poll the returned id with wait_for_job.",
|
|
253
280
|
inputSchema: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@craftstory/mcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"mcpName": "io.github.itseez3d/craftstory-mcp",
|
|
4
5
|
"description": "MCP server for the CraftStory API: talking-avatar videos from a photo (CraftStory 2.0) and short clips with generated sound (MiniMax H3), from Claude, Cursor and other MCP clients.",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"bin": {
|
|
@@ -10,16 +11,18 @@
|
|
|
10
11
|
"files": [
|
|
11
12
|
"dist",
|
|
12
13
|
"README.md",
|
|
14
|
+
"CHANGELOG.md",
|
|
13
15
|
"LICENSE"
|
|
14
16
|
],
|
|
15
17
|
"engines": {
|
|
16
|
-
"node": ">=
|
|
18
|
+
"node": ">=20"
|
|
17
19
|
},
|
|
18
20
|
"scripts": {
|
|
19
21
|
"build": "tsc -p tsconfig.json && chmod +x dist/index.js",
|
|
20
22
|
"start": "node dist/index.js",
|
|
21
23
|
"test": "node --import tsx --test test/*.test.ts",
|
|
22
|
-
"smoke": "tsx test/smoke.ts"
|
|
24
|
+
"smoke": "tsx test/smoke.ts",
|
|
25
|
+
"prepack": "npm run build"
|
|
23
26
|
},
|
|
24
27
|
"keywords": [
|
|
25
28
|
"mcp",
|
|
@@ -32,7 +35,7 @@
|
|
|
32
35
|
"minimax"
|
|
33
36
|
],
|
|
34
37
|
"license": "MIT",
|
|
35
|
-
"homepage": "https://
|
|
38
|
+
"homepage": "https://github.com/itseez3d/craftstory-mcp#readme",
|
|
36
39
|
"dependencies": {
|
|
37
40
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
38
41
|
"zod": "^3.24.0"
|
|
@@ -43,6 +46,10 @@
|
|
|
43
46
|
"typescript": "^5.6.0"
|
|
44
47
|
},
|
|
45
48
|
"bugs": {
|
|
46
|
-
"url": "https://
|
|
49
|
+
"url": "https://github.com/itseez3d/craftstory-mcp/issues"
|
|
50
|
+
},
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "git+https://github.com/itseez3d/craftstory-mcp.git"
|
|
47
54
|
}
|
|
48
55
|
}
|