@starreel/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/README.md +47 -0
- package/dist/client.js +145 -0
- package/dist/index.js +30 -0
- package/dist/tools/localize.js +91 -0
- package/dist/tools/produce.js +51 -0
- package/package.json +41 -0
- package/server.json +23 -0
package/README.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# @starreel/mcp
|
|
2
|
+
|
|
3
|
+
StarReel 的 MCP 服务器 —— 把 AI 短剧**编排产线**暴露给 Claude Code / Cursor /
|
|
4
|
+
任何 MCP 客户端:让 AI agent 一句话从**剧本跑到可下载的成片**。
|
|
5
|
+
|
|
6
|
+
完整接入文档:https://api.shortreelai.com/docs/mcp
|
|
7
|
+
|
|
8
|
+
## 接入
|
|
9
|
+
|
|
10
|
+
1. 在 StarReel → 设置 → API Key 创建一把 `produce` scope 的 key(`srk_live_...`,只展示一次)。
|
|
11
|
+
2. Claude Code:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
claude mcp add starreel -e STARREEL_API_KEY=srk_live_xxx -- npx -y @starreel/mcp
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
其他 MCP 客户端(Cursor 等)照各自配置格式填 `npx -y @starreel/mcp` + 环境变量即可。
|
|
18
|
+
|
|
19
|
+
## 产线工具(从剧本到成片)
|
|
20
|
+
|
|
21
|
+
一集短剧的完整链路,每个花钱阶段先报价、你确认后才执行:
|
|
22
|
+
|
|
23
|
+
| 阶段 | 工具 |
|
|
24
|
+
|---|---|
|
|
25
|
+
| 建剧 | `create_drama`(建剧壳+自动建集,返回 episode_id) |
|
|
26
|
+
| 灌本 | `set_script` |
|
|
27
|
+
| 拆镜 | `quote_storyboards` → `generate_storyboards` → `get_storyboards`(审阅) |
|
|
28
|
+
| 出首帧 | `quote_frames` → `generate_frames` |
|
|
29
|
+
| 出视频 | `quote_videos` → `generate_videos`(大额,报价与扣费同函数) |
|
|
30
|
+
| 成片 | `compose_episode`(免费终拼) → `get_final_cut`(拿 COS 下载链接) |
|
|
31
|
+
|
|
32
|
+
**批量报价确认**:每个 `quote_*` 返回预估点数,agent 应把点数告诉你、你同意后才用返回的
|
|
33
|
+
`quote_id` 调 `generate_*`。整集一次执行,不逐图打扰。长任务后台异步,用 `get_storyboards`/
|
|
34
|
+
`get_final_cut` 轮询到完成。
|
|
35
|
+
|
|
36
|
+
## 计费与安全
|
|
37
|
+
|
|
38
|
+
- 预付制:必须有余额才能生成,账户**永不为负**;成本在调厂商**之前**预授权,不够返回 402。
|
|
39
|
+
- 视频报价 == 实际扣费(同一函数);终拼(成片)免费。
|
|
40
|
+
- API key 只存哈希;换取的是 15 分钟短期令牌;泄露在设置页吊销即失效,不影响网页登录。
|
|
41
|
+
|
|
42
|
+
## 环境变量
|
|
43
|
+
|
|
44
|
+
| 变量 | 必填 | 默认 |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| `STARREEL_API_KEY` | ✅ | — |
|
|
47
|
+
| `STARREEL_AUTH_BASE` | | `https://api.shortreelai.com` |
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StarReel API 客户端 —— MCP 工具层与后端之间唯一的 HTTP 通道。
|
|
3
|
+
*
|
|
4
|
+
* 鉴权模型:长期 API Key(环境变量,绝不出现在工具参数/返回里)→ 换 15min
|
|
5
|
+
* 短期 JWT(POST {auth}/v1/agent/token)→ 带 Bearer 调业务 API。token 过期
|
|
6
|
+
* 或 401 时自动重换一次再重放;重放仍失败则原样抛错(不无限循环)。
|
|
7
|
+
*
|
|
8
|
+
* 安全边界:
|
|
9
|
+
* - key/token 永不写 stdout(stdio 传输的 JSON-RPC 通道)也不进工具返回值。
|
|
10
|
+
* - 返回给模型的下载链接只能是我方 COS 预签名 URL(服务端已保证,这里不转发
|
|
11
|
+
* 任何厂商直链字段)。
|
|
12
|
+
*/
|
|
13
|
+
import { createReadStream, statSync } from 'node:fs';
|
|
14
|
+
import { Readable } from 'node:stream';
|
|
15
|
+
const AUTH_BASE = process.env.STARREEL_AUTH_BASE ?? 'https://api.shortreelai.com';
|
|
16
|
+
const LOC_BASE = process.env.STARREEL_LOC_BASE ?? 'https://translator.starreel.ai';
|
|
17
|
+
export class StarReelClient {
|
|
18
|
+
apiKey;
|
|
19
|
+
token = null;
|
|
20
|
+
tokenExpiresAt = 0; // epoch ms
|
|
21
|
+
constructor(apiKey) {
|
|
22
|
+
this.apiKey = apiKey;
|
|
23
|
+
if (!apiKey?.startsWith('srk_')) {
|
|
24
|
+
throw new Error('STARREEL_API_KEY missing or malformed (expected srk_live_...)');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** API key → 短期 access token(提前 60s 视为过期,避免边界竞态)。 */
|
|
28
|
+
async ensureToken() {
|
|
29
|
+
if (this.token && Date.now() < this.tokenExpiresAt - 60_000)
|
|
30
|
+
return this.token;
|
|
31
|
+
const res = await fetch(`${AUTH_BASE}/v1/agent/token`, {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: { 'content-type': 'application/json' },
|
|
34
|
+
body: JSON.stringify({ api_key: this.apiKey }),
|
|
35
|
+
});
|
|
36
|
+
if (!res.ok) {
|
|
37
|
+
throw new Error(res.status === 401
|
|
38
|
+
? 'API key rejected (revoked or invalid) — create a new key in StarReel settings'
|
|
39
|
+
: `token exchange failed: HTTP ${res.status}`);
|
|
40
|
+
}
|
|
41
|
+
const body = (await res.json());
|
|
42
|
+
this.token = body.data.access_token;
|
|
43
|
+
this.tokenExpiresAt = Date.now() + body.data.expires_in * 1000;
|
|
44
|
+
return this.token;
|
|
45
|
+
}
|
|
46
|
+
/** 带 Bearer 的 fetch;401 时强制重换 token 重放一次。 */
|
|
47
|
+
async authedFetch(url, init = {}) {
|
|
48
|
+
const attempt = async () => {
|
|
49
|
+
const token = await this.ensureToken();
|
|
50
|
+
return fetch(url, { ...init, headers: { ...(init.headers ?? {}), authorization: `Bearer ${token}` } });
|
|
51
|
+
};
|
|
52
|
+
let res = await attempt();
|
|
53
|
+
if (res.status === 401) {
|
|
54
|
+
this.token = null; // 服务端时钟/吊销边界:强制重换一次
|
|
55
|
+
res = await attempt();
|
|
56
|
+
}
|
|
57
|
+
return res;
|
|
58
|
+
}
|
|
59
|
+
/** 本地化服务(translator.starreel.ai)的 JSON 调用。 */
|
|
60
|
+
async loc(path, init = {}) {
|
|
61
|
+
// 网络层失败(DNS/TLS/超时)与业务 4xx/5xx 分开报——agent 拿到 "fetch failed"
|
|
62
|
+
// 无从行动;点名服务与建议,它才能正确转述/重试/放弃。
|
|
63
|
+
let res;
|
|
64
|
+
try {
|
|
65
|
+
res = await this.authedFetch(`${LOC_BASE}${path}`, init);
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
throw new Error(`StarReel localization service unreachable (${LOC_BASE}): ${e?.cause?.code ?? e?.message ?? e}. ` +
|
|
69
|
+
'The service may be down or your network blocks it — retry later or check with StarReel.');
|
|
70
|
+
}
|
|
71
|
+
const text = await res.text();
|
|
72
|
+
let body;
|
|
73
|
+
try {
|
|
74
|
+
body = JSON.parse(text);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
body = { raw: text.slice(0, 300) };
|
|
78
|
+
}
|
|
79
|
+
if (!res.ok)
|
|
80
|
+
throw new Error(body?.error ?? `HTTP ${res.status} on ${path}`);
|
|
81
|
+
return body;
|
|
82
|
+
}
|
|
83
|
+
locPost(path, body) {
|
|
84
|
+
return this.loc(path, {
|
|
85
|
+
method: 'POST',
|
|
86
|
+
headers: { 'content-type': 'application/json' },
|
|
87
|
+
body: JSON.stringify(body),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/** 短剧编排产线(网关 /v1/produce/*)。返回 huobao 信封里的 data。 */
|
|
91
|
+
async produce(path, init = {}) {
|
|
92
|
+
let res;
|
|
93
|
+
try {
|
|
94
|
+
res = await this.authedFetch(`${AUTH_BASE}/v1/produce${path}`, init);
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
throw new Error(`StarReel production pipeline unreachable (${AUTH_BASE}): ${e?.cause?.code ?? e?.message ?? e}. ` +
|
|
98
|
+
'Retry later or check with StarReel.');
|
|
99
|
+
}
|
|
100
|
+
const text = await res.text();
|
|
101
|
+
let body;
|
|
102
|
+
try {
|
|
103
|
+
body = JSON.parse(text);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
body = { raw: text.slice(0, 300) };
|
|
107
|
+
}
|
|
108
|
+
if (!res.ok)
|
|
109
|
+
throw new Error(body?.message ?? body?.error ?? `HTTP ${res.status} on ${path}`);
|
|
110
|
+
return (body?.data ?? body); // huobao 信封 {code,data,message}
|
|
111
|
+
}
|
|
112
|
+
produceGet(path) {
|
|
113
|
+
return this.produce(path, { method: 'GET' });
|
|
114
|
+
}
|
|
115
|
+
producePost(path, body) {
|
|
116
|
+
return this.produce(path, {
|
|
117
|
+
method: 'POST',
|
|
118
|
+
headers: { 'content-type': 'application/json' },
|
|
119
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
producePut(path, body) {
|
|
123
|
+
return this.produce(path, {
|
|
124
|
+
method: 'PUT',
|
|
125
|
+
headers: { 'content-type': 'application/json' },
|
|
126
|
+
body: JSON.stringify(body),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* 把本地文件流式 PUT 到 COS 预签名 URL(字节不经我们任何服务器)。
|
|
131
|
+
* Content-Length 必带 —— 预签名 PUT 对 chunked 编码不友好。
|
|
132
|
+
*/
|
|
133
|
+
async uploadFile(uploadUrl, filePath) {
|
|
134
|
+
const size = statSync(filePath).size;
|
|
135
|
+
const res = await fetch(uploadUrl, {
|
|
136
|
+
method: 'PUT',
|
|
137
|
+
headers: { 'content-length': String(size) },
|
|
138
|
+
body: Readable.toWeb(createReadStream(filePath)),
|
|
139
|
+
// Node fetch(undici) 流式请求体必须显式声明半双工
|
|
140
|
+
duplex: 'half',
|
|
141
|
+
});
|
|
142
|
+
if (!res.ok)
|
|
143
|
+
throw new Error(`upload failed: HTTP ${res.status} (${(await res.text()).slice(0, 200)})`);
|
|
144
|
+
}
|
|
145
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* StarReel MCP 服务器(stdio 形态)。
|
|
4
|
+
*
|
|
5
|
+
* 接入(Claude Code):
|
|
6
|
+
* claude mcp add starreel -e STARREEL_API_KEY=srk_live_... -- npx -y @starreel/mcp
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ stdio 传输下 stdout 是 JSON-RPC 通道 —— 任何诊断输出只许走 stderr
|
|
9
|
+
* (console.error),console.log 一次就会把整条会话打坏。
|
|
10
|
+
*/
|
|
11
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
12
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
13
|
+
import { StarReelClient } from './client.js';
|
|
14
|
+
import { registerProduceTools } from './tools/produce.js';
|
|
15
|
+
const apiKey = process.env.STARREEL_API_KEY;
|
|
16
|
+
if (!apiKey) {
|
|
17
|
+
console.error('STARREEL_API_KEY is not set.\n' +
|
|
18
|
+
'Create one in StarReel → Settings → API Keys, then:\n' +
|
|
19
|
+
' claude mcp add starreel -e STARREEL_API_KEY=srk_live_... -- npx -y @starreel/mcp');
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
const server = new McpServer({ name: 'starreel', version: '0.1.0' });
|
|
23
|
+
const client = new StarReelClient(apiKey);
|
|
24
|
+
registerProduceTools(server, client); // 短剧编排产线(需 produce scope)——从剧本到成片
|
|
25
|
+
// 出海本地化工具:translator worker 暂时离线(2026-08),发布版先不注册,免得客户拿到只会
|
|
26
|
+
// 报错的工具。worker 重建后取消下一行注释即可(工具代码保留在 tools/localize.ts)。
|
|
27
|
+
// registerLocalizeTools(server, client)
|
|
28
|
+
const transport = new StdioServerTransport();
|
|
29
|
+
await server.connect(transport);
|
|
30
|
+
console.error('[starreel-mcp] ready (drama production toolset, stdio)');
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 本地化产线三件套工具(v1)。对接 translator.starreel.ai 既有端点,服务端零改动:
|
|
3
|
+
* presign(直传 COS)→ 登记 job → batch-process 提交处理 → 轮询状态。
|
|
4
|
+
*
|
|
5
|
+
* 工具设计铁律:
|
|
6
|
+
* - 卖"结果"不搬 REST:localize_video 一次调用替用户串完 4 个端点。
|
|
7
|
+
* - 会扣费的动作在 description 里写明单价来源,让 agent 能向用户转述;
|
|
8
|
+
* 余额不足时服务端预扣模式会快速失败并退款,这里如实转发错误。
|
|
9
|
+
* - 返回结构化 JSON 文本;下载链接是我方 COS 预签名 URL(短时效,过期重查)。
|
|
10
|
+
*/
|
|
11
|
+
import { basename, extname } from 'node:path';
|
|
12
|
+
import { existsSync } from 'node:fs';
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
const DEFAULT_SERIES = 'MCP Uploads';
|
|
15
|
+
function jsonResult(data) {
|
|
16
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
17
|
+
}
|
|
18
|
+
export function registerLocalizeTools(server, client) {
|
|
19
|
+
server.tool('localize_video', '把一个中文短剧视频(mp4/mov)做出海本地化:去除中文硬字幕并压制目标语言字幕。' +
|
|
20
|
+
'上传→登记→提交处理一步完成,返回 job_id;用 get_localization_job 轮询进度' +
|
|
21
|
+
'(1080p 约 1.25× 实时)。会消耗账户积分(约 65 点/分钟,处理前预扣、失败自动退款)。', {
|
|
22
|
+
file_path: z.string().describe('本地视频文件绝对路径(.mp4/.mov)'),
|
|
23
|
+
target_lang: z
|
|
24
|
+
.enum(['en', 'ja', 'ko', 'es', 'fr', 'vi', 'th', 'id', 'ar'])
|
|
25
|
+
.describe('目标语言(en=英语 ja=日语 ko=韩语 es=西语 fr=法语 vi=越南语 th=泰语 id=印尼语 ar=阿拉伯语)'),
|
|
26
|
+
series_name: z.string().max(100).optional()
|
|
27
|
+
.describe(`归属剧集名(同名复用,默认 "${DEFAULT_SERIES}")`),
|
|
28
|
+
}, async ({ file_path, target_lang, series_name }) => {
|
|
29
|
+
if (!existsSync(file_path))
|
|
30
|
+
throw new Error(`file not found: ${file_path}`);
|
|
31
|
+
const ext = extname(file_path).toLowerCase();
|
|
32
|
+
if (ext !== '.mp4' && ext !== '.mov')
|
|
33
|
+
throw new Error(`unsupported format ${ext} (mp4/mov only)`);
|
|
34
|
+
// 1) 找/建剧集(batch-process 以剧集为提交单元)
|
|
35
|
+
const wanted = (series_name ?? DEFAULT_SERIES).trim();
|
|
36
|
+
const { series } = await client.loc('/api/series');
|
|
37
|
+
let sid = series.find((s) => s.name === wanted)?.id;
|
|
38
|
+
if (!sid) {
|
|
39
|
+
const created = await client.locPost('/api/series', { name: wanted });
|
|
40
|
+
sid = created.id;
|
|
41
|
+
}
|
|
42
|
+
// 2) 预签名 → 浏览器同款直传 COS(字节不经任何 StarReel 服务器)
|
|
43
|
+
const filename = basename(file_path);
|
|
44
|
+
const presign = await client.locPost('/api/uploads/presign', { filename });
|
|
45
|
+
await client.uploadFile(presign.uploadUrl, file_path);
|
|
46
|
+
// 3) 登记任务 → 4) 提交处理(设语言、转 pending,worker 立即可领)
|
|
47
|
+
const job = await client.locPost('/api/jobs', {
|
|
48
|
+
key: presign.key,
|
|
49
|
+
name: filename,
|
|
50
|
+
seriesId: sid,
|
|
51
|
+
});
|
|
52
|
+
const submitted = await client.locPost(`/api/series/${sid}/batch-process`, { jobIds: [job.id], targetLang: target_lang });
|
|
53
|
+
if (submitted.submitted !== 1) {
|
|
54
|
+
throw new Error(`job ${job.id} registered but not submitted — check series ${sid} state`);
|
|
55
|
+
}
|
|
56
|
+
return jsonResult({
|
|
57
|
+
job_id: job.id,
|
|
58
|
+
series_id: sid,
|
|
59
|
+
status: 'pending',
|
|
60
|
+
target_lang,
|
|
61
|
+
note: 'poll with get_localization_job; processing ≈1.25× realtime',
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
server.tool('get_localization_job', '查询一个本地化任务的状态。status=done 时返回成片下载链接(短时效预签名 URL,过期就再查一次拿新链接)。', { job_id: z.number().int().positive().describe('localize_video 返回的 job_id') }, async ({ job_id }) => {
|
|
65
|
+
const row = await client.loc(`/api/jobs/${job_id}`);
|
|
66
|
+
if (!row?.id)
|
|
67
|
+
throw new Error(`job ${job_id} not found`);
|
|
68
|
+
return jsonResult({
|
|
69
|
+
job_id: row.id,
|
|
70
|
+
name: row.name,
|
|
71
|
+
status: row.status, // uploaded|pending|processing|done|failed
|
|
72
|
+
stage: row.stage,
|
|
73
|
+
target_lang: row.target_lang,
|
|
74
|
+
duration_sec: row.duration_sec,
|
|
75
|
+
points_charged: row.points,
|
|
76
|
+
download_url: row.output_url, // 我方 COS 预签名,非厂商直链
|
|
77
|
+
error: row.error,
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
server.tool('list_localization_jobs', '列出我的本地化任务(最近 200 条,可按剧集过滤)。', { series_id: z.number().int().positive().optional() }, async ({ series_id }) => {
|
|
81
|
+
const q = series_id ? `?seriesId=${series_id}` : '';
|
|
82
|
+
const { jobs } = await client.loc(`/api/jobs${q}`);
|
|
83
|
+
return jsonResult({
|
|
84
|
+
count: jobs.length,
|
|
85
|
+
jobs: jobs.map((r) => ({
|
|
86
|
+
job_id: r.id, name: r.name, status: r.status, stage: r.stage,
|
|
87
|
+
target_lang: r.target_lang, points: r.points, created_at: r.created_at,
|
|
88
|
+
})),
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 短剧编排产线工具集(P2)。对接网关 /v1/produce/*(门面),把
|
|
3
|
+
* 「建剧 → 灌本 → 拆镜 → 出帧 → 出视频 → 成片」整条产线包成 agent 可调的工具。
|
|
4
|
+
*
|
|
5
|
+
* 设计铁律 —— 批量报价确认(人在环):
|
|
6
|
+
* 每个花钱阶段拆成 quote_* + generate_*。agent **必须**先调 quote_*,把预估点数
|
|
7
|
+
* 原样告诉用户,等用户确认后才调 generate_*(带 quote_id)。绝不擅自确认——报价可能
|
|
8
|
+
* 是上万点的视频。generate_* 是**批量**执行(整集一次),不是一图一图,别循环逐条确认。
|
|
9
|
+
*
|
|
10
|
+
* 长任务(拆镜/出帧/出视频/成片)都是后台异步:generate_* 立即返回 status:'generating',
|
|
11
|
+
* 用 get_storyboards / get_episode_status 轮询到完成。下载链接只发我方 COS 链接。
|
|
12
|
+
*/
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
function jsonResult(data) {
|
|
15
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
16
|
+
}
|
|
17
|
+
const CONFIRM_HINT = '⚠️ 批量报价确认流程:先调对应的 quote_* 工具,把返回的 estimated_points 原样告诉用户,' +
|
|
18
|
+
'用户明确同意后,才用返回的 quote_id 调本工具。不要擅自确认。';
|
|
19
|
+
export function registerProduceTools(server, client) {
|
|
20
|
+
// ---------- 建剧 ----------
|
|
21
|
+
server.tool('create_drama', '新建一部短剧(剧壳)。按 total_episodes 自动建对应数量的空集,返回 drama_id 与各集 episode_id。免费。', {
|
|
22
|
+
title: z.string().describe('剧名'),
|
|
23
|
+
total_episodes: z.number().int().min(1).max(200).optional().describe('集数(默认 1),自动建 N 个空集'),
|
|
24
|
+
genre: z.string().optional().describe('题材,如 都市/悬疑/古装'),
|
|
25
|
+
style: z.string().optional().describe('风格描述'),
|
|
26
|
+
description: z.string().optional(),
|
|
27
|
+
}, async (args) => jsonResult(await client.producePost('/dramas', args)));
|
|
28
|
+
// ---------- 灌本 ----------
|
|
29
|
+
server.tool('set_script', '给某一集设置可拍剧本文本(拆镜的输入)。免费。', {
|
|
30
|
+
episode_id: z.number().int().positive(),
|
|
31
|
+
script: z.string().min(1).describe('该集的完整剧本文本'),
|
|
32
|
+
}, async ({ episode_id, script }) => jsonResult(await client.producePut(`/episodes/${episode_id}/script`, { script_content: script })));
|
|
33
|
+
// ---------- 拆镜(storyboards) ----------
|
|
34
|
+
server.tool('quote_storyboards', '报价:把某一集的剧本拆成分镜(storyboards)要多少点。返回 estimated_points 与 quote_id。零扣费。' +
|
|
35
|
+
'拿到后把点数告诉用户征求同意,再用 quote_id 调 generate_storyboards。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.producePost(`/episodes/${episode_id}/storyboards/quote`)));
|
|
36
|
+
server.tool('generate_storyboards', '确认后拆镜:后台跑(分钟级),立即返回 status:generating。用 get_storyboards 轮询,分镜出现即在生成、数量稳定即完成。' + CONFIRM_HINT, { episode_id: z.number().int().positive(), quote_id: z.string().describe('来自 quote_storyboards') }, async ({ episode_id, quote_id }) => jsonResult(await client.producePost(`/episodes/${episode_id}/storyboards/generate`, { quote_id })));
|
|
37
|
+
server.tool('get_storyboards', '读某一集的分镜列表(供审阅/查进度)。含每镜首帧(first_frame_image)与视频(video_url)是否就绪。免费。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.produceGet(`/episodes/${episode_id}/storyboards`)));
|
|
38
|
+
// ---------- 出首帧(frames) ----------
|
|
39
|
+
server.tool('quote_frames', '报价:给某一集缺首帧的分镜批量出首帧要多少点。返回 frames_to_generate、estimated_points、quote_id。零扣费。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.producePost(`/episodes/${episode_id}/frames/quote`)));
|
|
40
|
+
server.tool('generate_frames', '确认后批量出首帧:后台异步。用 get_storyboards 轮询,first_frame_image 逐镜填充即完成。' +
|
|
41
|
+
'出视频前必须先出帧,否则视频会退化成无一致性锚点的画面。' + CONFIRM_HINT, { episode_id: z.number().int().positive(), quote_id: z.string().describe('来自 quote_frames') }, async ({ episode_id, quote_id }) => jsonResult(await client.producePost(`/episodes/${episode_id}/frames/generate`, { quote_id })));
|
|
42
|
+
// ---------- 出视频(videos,大额) ----------
|
|
43
|
+
server.tool('quote_videos', '报价:给某一集所有分镜批量出视频要多少点(与实际扣费同函数,较准)。返回 estimated_points、quote_id。零扣费。' +
|
|
44
|
+
'⚠️ 视频是大额花费,务必把点数清楚告诉用户并等其确认。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.producePost(`/episodes/${episode_id}/videos/quote`)));
|
|
45
|
+
server.tool('generate_videos', '确认后批量出视频:一条后台链跑完整集,余额不足会自动中止整链(防重复扣)。' +
|
|
46
|
+
'要求本集已出首帧(未出会被拒)。用 get_storyboards 轮询 video_url 逐镜填充即完成。' + CONFIRM_HINT, { episode_id: z.number().int().positive(), quote_id: z.string().describe('来自 quote_videos') }, async ({ episode_id, quote_id }) => jsonResult(await client.producePost(`/episodes/${episode_id}/videos/generate`, { quote_id })));
|
|
47
|
+
// ---------- 成片(compose/终拼) ----------
|
|
48
|
+
server.tool('compose_episode', '把某一集所有镜头视频拼成一条成片(终拼)。**免费**(纯拼接,无需报价确认),后台异步。' +
|
|
49
|
+
'要求本集至少有一个镜有视频;部分就绪会拼出部分成片并在返回里标 partial。发起后用 get_final_cut 轮询。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.producePost(`/episodes/${episode_id}/compose`)));
|
|
50
|
+
server.tool('get_final_cut', '查某一集成片状态与下载链接。status=completed 时返回 download_url(我方 COS 直链,可直接下载)。免费。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.produceGet(`/episodes/${episode_id}/final-cut`)));
|
|
51
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@starreel/mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "StarReel MCP server — AI short-drama production & localization tools for Claude, Cursor, and any MCP client",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"starreel-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"server.json"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"dev": "tsx src/index.ts",
|
|
18
|
+
"typecheck": "tsc --noEmit",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=18"
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
26
|
+
"zod": "^3.23.8"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^25.5.0",
|
|
30
|
+
"tsx": "^4.19.0",
|
|
31
|
+
"typescript": "^5.7.0"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"mcp",
|
|
35
|
+
"model-context-protocol",
|
|
36
|
+
"starreel",
|
|
37
|
+
"video",
|
|
38
|
+
"localization",
|
|
39
|
+
"short-drama"
|
|
40
|
+
]
|
|
41
|
+
}
|
package/server.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json",
|
|
3
|
+
"name": "ai.starreel/starreel",
|
|
4
|
+
"description": "AI short-drama production pipeline: turn a script into a finished, downloadable episode — create drama, break into storyboards, generate frames and videos, and compose the final cut, all with batch quote/confirm billing.",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"websiteUrl": "https://starreel.ai",
|
|
7
|
+
"packages": [
|
|
8
|
+
{
|
|
9
|
+
"registryType": "npm",
|
|
10
|
+
"identifier": "@starreel/mcp",
|
|
11
|
+
"version": "0.1.0",
|
|
12
|
+
"transport": { "type": "stdio" },
|
|
13
|
+
"environmentVariables": [
|
|
14
|
+
{
|
|
15
|
+
"name": "STARREEL_API_KEY",
|
|
16
|
+
"description": "StarReel API key (create in Settings → API Keys)",
|
|
17
|
+
"isRequired": true,
|
|
18
|
+
"isSecret": true
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|