@starreel/mcp 0.1.36 → 0.1.38

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/dist/client.js CHANGED
@@ -13,6 +13,7 @@
13
13
  import { createReadStream, statSync } from 'node:fs';
14
14
  import { basename } from 'node:path';
15
15
  import { Readable } from 'node:stream';
16
+ import { assertSafeLocalMediaPath } from './path-guard.js'; // v0.1.37 — P0③ 路径安全闸
16
17
  /** 扩展名 → MIME(presign 需要 content_type)。 */
17
18
  const MIME_BY_EXT = {
18
19
  '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.webp': 'image/webp',
@@ -159,6 +160,9 @@ export class StarReelClient {
159
160
  * 字节不经我们的业务服务器,只走 COS。kind: image(默认)/video/audio。
160
161
  */
161
162
  async uploadLocalFile(filePath, kind = 'image') {
163
+ // v0.1.37 — P0③:媒体扩展名白名单 + 隐藏/系统目录拒绝(realpath 后判,防符号链接绕过)。
164
+ const safePath = assertSafeLocalMediaPath(filePath, kind);
165
+ filePath = safePath;
162
166
  const size = statSync(filePath).size;
163
167
  const filename = basename(filePath);
164
168
  const ext = (filename.match(/\.[a-z0-9]+$/i)?.[0] || '').toLowerCase();
@@ -0,0 +1,45 @@
1
+ /**
2
+ * v0.1.37 — 本地文件上传的路径安全闸(安全评估 P0③)。
3
+ *
4
+ * uploadLocalFile 此前接受任意绝对路径:一个被注入的第三方 agent 可以把
5
+ * ~/.ssh/id_rsa、.env、浏览器资料库当"素材"上传到 COS(公共可读 URL)。
6
+ * 本闸不引入 workspace 概念(MCP 进程不知道调用方的工作目录语义),
7
+ * 而是用两条与"上传媒体素材"这一合法用途严格对齐的规则:
8
+ * ① 扩展名必须是对应 kind 的媒体白名单(.env/.pem/id_rsa 天然出局);
9
+ * ② realpath 后的路径中不得含任何以 '.' 开头的路径段(~/.ssh、~/.codex、
10
+ * ~/.aws、.git 等敏感目录全部命中),也不得落在系统配置区(/etc、/private/etc)。
11
+ * 符号链接先解析再判(防 media.jpg -> ~/.ssh/id_rsa 绕过)。
12
+ */
13
+ import { realpathSync } from 'node:fs';
14
+ import { sep } from 'node:path';
15
+ export const MEDIA_EXT_BY_KIND = {
16
+ image: new Set(['.jpg', '.jpeg', '.png', '.webp', '.gif']),
17
+ video: new Set(['.mp4', '.mov', '.webm', '.m4v']),
18
+ audio: new Set(['.mp3', '.wav', '.m4a', '.aac', '.flac', '.ogg']),
19
+ };
20
+ const SYSTEM_PREFIXES = ['/etc/', '/private/etc/', '/proc/', '/sys/'];
21
+ /** 校验通过返回解析后的真实路径;不通过抛 Error(消息面向第三方 agent,说清正路)。 */
22
+ export function assertSafeLocalMediaPath(filePath, kind, resolve = realpathSync) {
23
+ const ext = (filePath.match(/\.[a-z0-9]+$/i)?.[0] || '').toLowerCase();
24
+ if (!MEDIA_EXT_BY_KIND[kind].has(ext)) {
25
+ throw new Error(`file_path 只接受${kind === 'image' ? '图片' : kind === 'video' ? '视频' : '音频'}媒体文件`
26
+ + `(${[...MEDIA_EXT_BY_KIND[kind]].join('/')});收到 "${ext || '无扩展名'}"。`
27
+ + '不要用本工具上传配置/密钥/文档类文件。');
28
+ }
29
+ let real;
30
+ try {
31
+ real = resolve(filePath);
32
+ }
33
+ catch {
34
+ throw new Error(`file_path 不存在或不可读:${filePath}`);
35
+ }
36
+ const segments = real.split(sep);
37
+ if (segments.some(s => s.startsWith('.') && s.length > 1)) {
38
+ throw new Error(`file_path 位于隐藏目录(${real}),已拒绝——敏感目录(如 ~/.ssh、~/.aws、.git)不允许作为上传源。`
39
+ + '把素材放到普通目录再上传。');
40
+ }
41
+ if (SYSTEM_PREFIXES.some(p => real.startsWith(p))) {
42
+ throw new Error(`file_path 位于系统目录(${real}),已拒绝。`);
43
+ }
44
+ return real;
45
+ }
@@ -189,7 +189,9 @@ export function registerProduceTools(server, client) {
189
189
  // ---------- 设定图(镜头一致性根锚·批量 / 一键) ----------
190
190
  server.tool('generate_character_sheets', '★批量给全剧角色出设定图(多视角 turnaround)。设定图是镜头帧/视频引用的**一致性根锚**——只出定妆图不出设定图,' +
191
191
  '镜头人物换角度/换光/服装会漂移(2.5 更直接用设定图切片做骨相锚)。只对缺设定图的角色出(不重复扣费)。' +
192
- '前置:角色须已有定妆图(缺则拦并提示先出定妆图)。图片步,按用量后付不欠费。轮询 get_characters 看 sheet_url 就绪。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.producePost(`/episodes/${episode_id}/character-sheets`)));
192
+ '前置:角色须已有定妆图(缺则拦并提示先出定妆图)。图片步,按用量后付不欠费。轮询 get_characters 看 sheet_status:' +
193
+ 'pending=生成/审计中(设定图约 2~4 分钟,别重复点);ready=入库;**rejected=图已出但被一致性闸拒收**,' +
194
+ '读 sheet_fail_hint 拒收判词——可重掷一次,连拒理由相同则是锚字段与人物档案冲突,先 run_precheck 再重掷。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.producePost(`/episodes/${episode_id}/character-sheets`)));
193
195
  server.tool('generate_portraits_and_sheets', '★一键:定妆图 + 设定图(推荐,产线标配)。设定图强依赖定妆图,故智能两阶段——有角色缺定妆图就先派定妆图并提示,' +
194
196
  '定妆图齐了再调一次即批量出设定图。避免只出定妆图导致镜头漂移。图片步,按用量后付不欠费。' +
195
197
  '轮询 get_characters 看 image/sheet_url 就绪;每调一次推进一步。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.producePost(`/episodes/${episode_id}/portraits-and-sheets`)));
@@ -488,7 +490,12 @@ export function registerProduceTools(server, client) {
488
490
  // ========== P1 · 读取层(闭环必需:能读回自己建的东西)==========
489
491
  server.tool('list_dramas', '列出我名下的所有短剧(drama_id/剧名/类型/集数/进度)。免费。', {}, async () => jsonResult(await client.produceGet('/dramas')));
490
492
  server.tool('get_drama', '读一部剧的详情(设定/角色数/场景数/资产统计等)。免费。', { drama_id: z.number().int().positive() }, async ({ drama_id }) => jsonResult(await client.produceGet(`/dramas/${drama_id}`)));
491
- server.tool('get_characters', '读某一集的角色列表(名字/外貌/定妆图/身份锚是否就绪)。免费。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.produceGet(`/episodes/${episode_id}/characters`)));
493
+ server.tool('get_characters', '读某一集的角色列表(名字/定妆图/设定图就绪状态)。免费。每角色带结构化状态:' +
494
+ 'portrait_status/sheet_status(ready=已入库/pending=生成或审计中·别重复点/rejected=图已生成但被' +
495
+ '一致性闸拒收未入库/failed/none)+ 拒收时的 sheet_fail_reason·sheet_retryable·sheet_fail_hint(拒收判词)。' +
496
+ '★sheet_status=rejected 时照 retryable 判:consistency_rejected 可重掷一次(generate_character_sheet);' +
497
+ '**连拒且 fail_hint 理由几乎相同 = 锚字段与人物档案冲突(结构性必拒),重掷纯白花钱**——' +
498
+ '先 run_precheck 看 anchor-conflict 警告,把 visual_lock/art_bible 里钉死的角色外观移除或对齐档案再重掷。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.produceGet(`/episodes/${episode_id}/characters`)));
492
499
  server.tool('get_scenes', '读某一集的场景列表(名字/描述/场景图是否就绪)。免费。', { episode_id: z.number().int().positive() }, async ({ episode_id }) => jsonResult(await client.produceGet(`/episodes/${episode_id}/scenes`)));
493
500
  server.tool('get_assets', '读一部剧的全部资产聚合(角色/场景/道具/图片)。免费。', { drama_id: z.number().int().positive() }, async ({ drama_id }) => jsonResult(await client.produceGet(`/dramas/${drama_id}/assets`)));
494
501
  // ========== P1 · 任务进度 + 成本/预算 ==========
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starreel/mcp",
3
- "version": "0.1.36",
3
+ "version": "0.1.38",
4
4
  "mcpName": "ai.starreel/starreel",
5
5
  "description": "StarReel MCP server — drive the AI short-drama production pipeline (script → storyboards → frames → video → final cut) from Claude, Cursor, or any MCP client",
6
6
  "license": "MIT",
package/server.json CHANGED
@@ -2,13 +2,13 @@
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json",
3
3
  "name": "ai.starreel/starreel",
4
4
  "description": "Turn a script into a finished, downloadable short-drama episode — via MCP or REST.",
5
- "version": "0.1.36",
5
+ "version": "0.1.38",
6
6
  "websiteUrl": "https://starreel.ai",
7
7
  "packages": [
8
8
  {
9
9
  "registryType": "npm",
10
10
  "identifier": "@starreel/mcp",
11
- "version": "0.1.36",
11
+ "version": "0.1.38",
12
12
  "transport": { "type": "stdio" },
13
13
  "environmentVariables": [
14
14
  {