aimakeall-mcp 0.6.0 → 0.7.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 CHANGED
@@ -16,7 +16,7 @@ Claude Code·Codex 같은 MCP 클라이언트에서 자연어로 AImakeAll 영
16
16
  "mcpServers": {
17
17
  "aimakeall": {
18
18
  "command": "npx",
19
- "args": ["-y", "aimakeall-mcp@0.6.0"],
19
+ "args": ["-y", "aimakeall-mcp@0.7.0"],
20
20
  "env": { "AIMAKEALL_PAT": "aio_pat_..." }
21
21
  }
22
22
  }
@@ -35,7 +35,7 @@ Claude Code·Codex 같은 MCP 클라이언트에서 자연어로 AImakeAll 영
35
35
  ## 기본 플로우 (쇼츠)
36
36
 
37
37
  ```
38
- plan_shorts_video → (씬마다) generate_scene_image → generate_scene_video_prompt → generate_scene_video
38
+ plan_shorts_video → (씬마다) generate_scene_image → [QC: 동봉 이미지 직접 확인 또는 verify_scene_image] → generate_scene_video_prompt → generate_scene_video → [QC: 동봉 프레임 확인 또는 verify_scene_video]
39
39
  → (보이스 미지정 시) recommend_voice → tts_narration_with_captions(하단자막·강조용) 또는 tts_narration → stitch_timeline → render_start → render_status(폴링) → render_result
40
40
  → ai_publish_metadata → publish_youtube
41
41
  ```
@@ -44,6 +44,7 @@ plan_shorts_video → (씬마다) generate_scene_image → generate_scene_video_
44
44
  - 렌더 잡은 `~/.aimakeall/render-jobs.json`에 기록되어 프록시 재시작 후 `list_render_jobs`로 복구할 수 있습니다.
45
45
  - 대용량 산출물(TTS mp3, 스티치 매니페스트)은 파일 경로로 주고받아 모델 컨텍스트를 오염시키지 않습니다.
46
46
  - `recommend_voice`는 주제·샘플 영상(유튜브)에 맞는 보이스를 자동 선정합니다 — 샘플 화자 분석은 컴패니언 앱이 실행 중일 때만 동작합니다.
47
+ - 자체검증(QC): 씬 이미지/영상 결과에 이미지·프레임 블록이 동봉되어 에이전트가 직접 보고 판정합니다. 비전 미지원 클라이언트는 `verify_scene_image`/`verify_scene_video`(서버 Gemini 판정)를 쓰세요 — 불합격 시 `regenerationHint`를 반영해 재생성.
47
48
  - 하단자막 강조: `tts_narration_with_captions`의 `subtitleLines`와 `plan_shorts_video`의 `emphasisKeywords`를 `stitch_timeline`에 함께 넘기면 자막 안 해당 단어만 노랑/큰 글씨로 강조됩니다(최신 컴패니언 런타임 필요 — 구버전은 일반 자막으로 안전 강등).
48
49
 
49
50
  ## 한도
@@ -95,6 +95,49 @@ function jsonResult(value) {
95
95
  return textResult(JSON.stringify(value, null, 2));
96
96
  }
97
97
 
98
+ // JSON + 이미지 블록 결과 — 비전 지원 클라이언트의 에이전트가 산출물을 "직접 보고"
99
+ // QC(1층 검증)할 수 있게 한다. 비전 미지원 클라이언트는 이미지 블록을 무시한다.
100
+ function jsonWithImagesResult(value, images = []) {
101
+ return {
102
+ content: [
103
+ { text: JSON.stringify(value, null, 2), type: "text" },
104
+ ...images
105
+ .filter((image) => image?.base64)
106
+ .map((image) => ({ data: image.base64, mimeType: image.mimeType || "image/jpeg", type: "image" })),
107
+ ],
108
+ };
109
+ }
110
+
111
+ // 인라인 이미지 블록에 허용하는 MIME — MCP SDK 안전 목록(png/jpeg/webp/gif)만.
112
+ const ALLOWED_INLINE_IMAGE_MIME = new Set(["image/gif", "image/jpeg", "image/jpg", "image/png", "image/webp"]);
113
+ // 모델 API 의 이미지당 한도(약 5MB)를 넘지 않게 — 초과분은 동봉 생략(검증 도구 폴백 안내).
114
+ const MAX_INLINE_IMAGE_BYTES = 4 * 1024 * 1024;
115
+
116
+ // 결과 이미지 다운로드 — research_product 이미지 가드와 동일 규칙
117
+ // (공개 URL만, redirect 미추적, image/* 검증, 8MB 스트리밍 캡).
118
+ async function downloadImageAsBase64(imageUrl) {
119
+ if (!isSafePublicImageUrl(imageUrl)) return null;
120
+ try {
121
+ const response = await fetch(imageUrl, { redirect: "manual", signal: AbortSignal.timeout(30_000) });
122
+ if (!response.ok) return null;
123
+ const contentType = String(response.headers.get("content-type") || "").toLowerCase().split(";")[0].trim();
124
+ if (!ALLOWED_INLINE_IMAGE_MIME.has(contentType)) return null;
125
+ if (!response.body) return null;
126
+ const parts = [];
127
+ let received = 0;
128
+ for await (const chunk of response.body) {
129
+ const piece = Buffer.from(chunk);
130
+ received += piece.length;
131
+ if (received > MAX_INLINE_IMAGE_BYTES) return null;
132
+ parts.push(piece);
133
+ }
134
+ if (!received) return null;
135
+ return { base64: Buffer.concat(parts).toString("base64"), mimeType: contentType };
136
+ } catch {
137
+ return null;
138
+ }
139
+ }
140
+
98
141
  const NO_PAT_GUIDE = "AIMAKEALL_PAT가 설정되지 않았습니다. aimakeall.com → API 키 설정 → MCP 토큰에서 발급한 뒤 MCP 설정의 env에 넣어주세요.";
99
142
 
100
143
  function describeLocalError(error) {
@@ -318,16 +361,17 @@ export function registerCloudTools(server, config, api) {
318
361
 
319
362
  server.tool(
320
363
  "generate_scene_image",
321
- "씬 이미지를 생성합니다 (기획 결과의 imagePrompt 사용). 반환된 imageUrl을 씬 영상 생성의 입력으로 쓰세요. 인물·제품 일관성은 이 참조 방식이 권장 경로입니다: 첫 씬(또는 캐릭터 시트/제품 사진)의 이미지를 referenceImageUrls·referenceImagePaths로 모든 씬에 앵커로 전달하세요.",
364
+ "씬 이미지를 생성합니다 (기획 결과의 imagePrompt 사용). 반환된 imageUrl을 씬 영상 생성의 입력으로 쓰세요. 인물·제품 일관성은 이 참조 방식이 권장 경로입니다: 첫 씬(또는 캐릭터 시트/제품 사진)의 이미지를 referenceImageUrls·referenceImagePaths로 모든 씬에 앵커로 전달하세요. 결과에 이미지 블록이 동봉됩니다 — 반드시 직접 보고 QC(캐릭터 앵커·왜곡·지시 이행) 후 불합격 시 재생성하세요. 이미지를 볼 수 없으면 verify_scene_image 사용.",
322
365
  {
323
366
  prompt: z.string().describe("이미지 프롬프트 (plan 결과의 imagePrompt)"),
324
367
  aspectRatio: z.string().optional().describe("기본 9:16"),
325
368
  model: z.enum(["gpt-image-2-beta", "gemini-3.1-flash-image-preview", "doubao-seedream-5.0-lite", "google-flow-nano-banana-pro", "kie-grok-imagine-image"]).optional().describe("이미지 모델, 기본 gpt-image-2-beta"),
369
+ returnImage: z.boolean().optional().describe("기본 true — 결과 이미지를 직접 보고 QC 할 수 있게 이미지 블록으로 함께 반환"),
326
370
  resolution: z.string().optional().describe("기본 1K"),
327
371
  referenceImageUrls: z.array(z.string()).max(6).optional().describe("참조 이미지 URL (씬1 앵커 등)"),
328
372
  referenceImagePaths: z.array(z.string()).max(4).optional().describe("참조 이미지 로컬 경로 (제품 사진 등)"),
329
373
  },
330
- wrapCloudHandler(config, async ({ prompt, aspectRatio = "9:16", model = "gpt-image-2-beta", resolution = "1K", referenceImageUrls = [], referenceImagePaths = [] }) => {
374
+ wrapCloudHandler(config, async ({ prompt, aspectRatio = "9:16", model = "gpt-image-2-beta", resolution = "1K", referenceImageUrls = [], referenceImagePaths = [], returnImage = true }) => {
331
375
  for (const filePath of referenceImagePaths) assertAllowedInputFile(filePath, ALLOWED_IMAGE_EXTS, { kind: "참조 이미지" });
332
376
  assertEncodedBudget(referenceImagePaths, { context: "참조 이미지" });
333
377
  const referenceImages = [
@@ -354,6 +398,7 @@ export function registerCloudTools(server, config, api) {
354
398
 
355
399
  let imageUrl = String(payload?.imageUrl || "");
356
400
  let savedPath = "";
401
+ let inlineImage = null;
357
402
  if (!imageUrl && payload?.imageDataUrl) {
358
403
  // URL 없이 base64만 온 경우 — 컨텍스트로 돌려주지 않고 파일로 저장.
359
404
  const match = String(payload.imageDataUrl).match(/^data:([^;]+);base64,(.+)$/s);
@@ -361,14 +406,24 @@ export function registerCloudTools(server, config, api) {
361
406
  const ext = match[1].includes("png") ? "png" : "jpg";
362
407
  const saved = saveMediaBuffer(config.stateDir, `scene-image.${ext}`, Buffer.from(match[2], "base64"));
363
408
  savedPath = saved.filePath;
409
+ const inlineMime = String(match[1]).toLowerCase();
410
+ if (returnImage && ALLOWED_INLINE_IMAGE_MIME.has(inlineMime) && match[2].length <= MAX_INLINE_IMAGE_BYTES * 4 / 3) {
411
+ inlineImage = { base64: match[2], mimeType: inlineMime };
412
+ }
364
413
  }
414
+ } else if (imageUrl && returnImage) {
415
+ inlineImage = await downloadImageAsBase64(imageUrl);
365
416
  }
366
- return jsonResult({
417
+ const resultBody = {
367
418
  imageUrl: imageUrl || undefined,
368
419
  model: payload?.model,
420
+ qc: inlineImage
421
+ ? "동봉된 이미지를 직접 확인하세요: 캐릭터 앵커 일치·손가락/글자 왜곡·프롬프트 이행. 불합격이면 사유를 프롬프트에 반영해 재생성하세요. 이미지를 볼 수 없는 클라이언트라면 verify_scene_image 를 호출하세요."
422
+ : "이미지를 동봉하지 못했습니다 — verify_scene_image 로 서버측 검증을 수행하세요 (imageUrl 이 없으면 savedPath 를 imagePath 로 넘기세요).",
369
423
  savedPath: savedPath || undefined,
370
424
  taskId: payload?.taskId,
371
- });
425
+ };
426
+ return inlineImage ? jsonWithImagesResult(resultBody, [inlineImage]) : jsonResult(resultBody);
372
427
  }),
373
428
  );
374
429
 
@@ -415,7 +470,7 @@ export function registerCloudTools(server, config, api) {
415
470
 
416
471
  server.tool(
417
472
  "generate_scene_video",
418
- "씬 영상을 생성합니다 (동기 호출, 최대 ~30분 — MCP 클라이언트의 툴 타임아웃(MCP_TIMEOUT)을 그 이상으로 늘려두세요. 타임아웃되면 이미 과금된 결과를 회수할 수 없으니 주의). 반환된 videoUrl을 stitch_timeline의 sceneVideos에 넣으세요.",
473
+ "씬 영상을 생성합니다 (동기 호출, 최대 ~30분 — MCP 클라이언트의 툴 타임아웃(MCP_TIMEOUT)을 그 이상으로 늘려두세요. 타임아웃되면 이미 과금된 결과를 회수할 수 없으니 주의). 반환된 videoUrl을 stitch_timeline의 sceneVideos에 넣으세요. 결과에 대표 프레임 3장이 동봉됩니다 — 반드시 직접 보고 QC 후 불합격 시 재생성하세요. 볼 수 없으면 verify_scene_video 사용.",
419
474
  {
420
475
  prompt: z.string().describe("영상 프롬프트 (generate_scene_video_prompt의 videoPrompt)"),
421
476
  sceneImageUrl: z.string().optional().describe("씬 이미지 URL (i2v 입력)"),
@@ -423,8 +478,9 @@ export function registerCloudTools(server, config, api) {
423
478
  durationSec: z.number().optional().describe("기본 8 (모델 상한으로 클램프됨)"),
424
479
  modelId: z.enum(["kie-grok-imagine", "seedance-2.0", "seedance-2.0-mini", "kling-3.0"]).optional().describe("i2v 영상 모델, 기본 kie-grok-imagine"),
425
480
  quality: z.string().optional().describe("기본 720p"),
481
+ returnFrames: z.boolean().optional().describe("기본 true — 결과 영상의 대표 프레임 3장을 직접 보고 QC 할 수 있게 이미지 블록으로 반환"),
426
482
  },
427
- wrapCloudHandler(config, async ({ prompt, sceneImageUrl = "", aspectRatio = "9:16", durationSec = 8, modelId = "kie-grok-imagine", quality = "720p" }) => {
483
+ wrapCloudHandler(config, async ({ prompt, sceneImageUrl = "", aspectRatio = "9:16", durationSec = 8, modelId = "kie-grok-imagine", quality = "720p", returnFrames = true }) => {
428
484
  const payload = await api.request("/api/tracker/gemini/storyboard-scene-video", {
429
485
  body: {
430
486
  aspectRatio,
@@ -446,13 +502,31 @@ export function registerCloudTools(server, config, api) {
446
502
  // 서버 최악 경로(KIE 900s + Evolink 폴백 3회×900s)를 넘겨 잡아 조기 abort 로 과금-미회수를 방지.
447
503
  timeoutMs: 1_800_000,
448
504
  });
449
- return jsonResult({
505
+ // 대표 프레임 동봉(1층 QC) — 실패는 조용히 넘기고 verify_scene_video 안내로 대체.
506
+ let frames = [];
507
+ if (returnFrames && payload?.videoUrl) {
508
+ try {
509
+ const frameResp = await api.request("/api/tracker/video/frames", {
510
+ body: { count: 3, maxWidth: 480, videoUrl: payload.videoUrl },
511
+ method: "POST",
512
+ timeoutMs: 60_000, // 과금 완료된 결과를 오래 붙들지 않기 — 실패해도 verify 폴백 안내
513
+ });
514
+ if (frameResp?.ok && Array.isArray(frameResp?.frames)) frames = frameResp.frames;
515
+ } catch {
516
+ frames = [];
517
+ }
518
+ }
519
+ const resultBody = {
450
520
  durationLabel: payload?.durationLabel,
451
521
  model: payload?.model,
522
+ qc: frames.length
523
+ ? "동봉된 대표 프레임 3장을 직접 확인하세요: 캐릭터 붕괴·모션 파탄·왜곡. 불합격이면 프롬프트를 교정해 재생성하세요. 프레임을 볼 수 없는 클라이언트라면 verify_scene_video 를 호출하세요."
524
+ : "프레임을 동봉하지 못했습니다 — verify_scene_video 로 서버측 검증을 수행하세요.",
452
525
  resolvedRouteLabel: payload?.resolvedRouteLabel,
453
526
  taskId: payload?.taskId,
454
527
  videoUrl: payload?.videoUrl,
455
- });
528
+ };
529
+ return frames.length ? jsonWithImagesResult(resultBody, frames) : jsonResult(resultBody);
456
530
  }),
457
531
  );
458
532
 
@@ -1182,6 +1256,82 @@ export function registerCloudTools(server, config, api) {
1182
1256
  }),
1183
1257
  );
1184
1258
 
1259
+ server.tool(
1260
+ "verify_scene_image",
1261
+ "생성된 키프레임 이미지를 서버 비전(Gemini)이 QC 판정합니다 — 캐릭터 참조 대조·아티팩트·프롬프트 이행. 이미지 블록을 직접 볼 수 없는 클라이언트의 폴백이며, 판정 JSON(pass/violations/regenerationHint)만 읽으면 됩니다. 불합격이면 regenerationHint 를 프롬프트에 반영해 generate_scene_image 를 재호출하세요.",
1262
+ {
1263
+ imageUrl: z.string().optional().describe("generate_scene_image 가 반환한 imageUrl"),
1264
+ imagePath: z.string().optional().describe("imageUrl 이 없을 때 — 생성 결과의 savedPath (로컬 파일)"),
1265
+ prompt: z.string().optional().describe("기대 내용(생성에 쓴 프롬프트 요약)"),
1266
+ referenceImagePaths: z.array(z.string()).max(4).optional().describe("참조 캐릭터 이미지 로컬 경로 (생성에 쓴 것과 동일하게)"),
1267
+ checklist: z.array(z.string()).max(8).optional().describe("추가 검수 항목 (예: 후드티는 남색)"),
1268
+ },
1269
+ wrapCloudHandler(config, async ({ imageUrl = "", imagePath = "", prompt = "", referenceImagePaths = [], checklist = [] }) => {
1270
+ if (!imageUrl && !imagePath) {
1271
+ return textResult("imageUrl 또는 imagePath 중 하나는 필요합니다.", { isError: true });
1272
+ }
1273
+ if (imagePath) {
1274
+ assertAllowedInputFile(imagePath, ALLOWED_IMAGE_EXTS, { kind: "검증 대상 이미지" });
1275
+ }
1276
+ for (const filePath of referenceImagePaths) assertAllowedInputFile(filePath, ALLOWED_IMAGE_EXTS, { kind: "참조 이미지" });
1277
+ assertEncodedBudget([imagePath, ...referenceImagePaths].filter(Boolean), { context: "검증 이미지" });
1278
+ const candidateUrl = imageUrl || fileToDataUrl(imagePath, { allowedExts: ALLOWED_IMAGE_EXTS, kind: "검증 대상 이미지" });
1279
+ const payload = await api.request("/api/tracker/gemini/verify-scene", {
1280
+ body: {
1281
+ checklist,
1282
+ imageUrl: candidateUrl,
1283
+ kind: "image",
1284
+ prompt,
1285
+ referenceImages: referenceImagePaths.map((filePath) => ({
1286
+ name: path.basename(filePath),
1287
+ previewUrl: fileToDataUrl(filePath, { allowedExts: ALLOWED_IMAGE_EXTS, kind: "참조 이미지" }),
1288
+ })),
1289
+ },
1290
+ method: "POST",
1291
+ timeoutMs: 300_000,
1292
+ });
1293
+ if (payload?.ok === false) {
1294
+ return textResult(`씬 검증 실패: ${payload?.error || "알 수 없는 오류"}\n${payload?.guidance || ""}`.trim(), { isError: true });
1295
+ }
1296
+ return jsonResult({ referenceLoaded: payload?.referenceLoaded, verdict: payload?.verdict || null });
1297
+ }),
1298
+ );
1299
+
1300
+ server.tool(
1301
+ "verify_scene_video",
1302
+ "생성된 씬 영상을 서버가 대표 프레임으로 QC 판정합니다 (Gemini 비전) — 캐릭터 참조 대조·붕괴/왜곡·프롬프트 이행. 프레임 블록을 직접 볼 수 없는 클라이언트의 폴백. 불합격이면 regenerationHint 를 반영해 generate_scene_video 를 재호출하세요.",
1303
+ {
1304
+ videoUrl: z.string().describe("generate_scene_video 가 반환한 videoUrl"),
1305
+ prompt: z.string().optional().describe("기대 내용(생성에 쓴 프롬프트 요약)"),
1306
+ referenceImagePaths: z.array(z.string()).max(4).optional().describe("참조 캐릭터 이미지 로컬 경로"),
1307
+ checklist: z.array(z.string()).max(8).optional().describe("추가 검수 항목"),
1308
+ frameCount: z.number().int().min(2).max(4).optional().describe("판정에 쓸 프레임 수, 기본 3"),
1309
+ },
1310
+ wrapCloudHandler(config, async ({ videoUrl, prompt = "", referenceImagePaths = [], checklist = [], frameCount = 3 }) => {
1311
+ for (const filePath of referenceImagePaths) assertAllowedInputFile(filePath, ALLOWED_IMAGE_EXTS, { kind: "참조 이미지" });
1312
+ assertEncodedBudget(referenceImagePaths, { context: "참조 이미지" });
1313
+ const payload = await api.request("/api/tracker/gemini/verify-scene", {
1314
+ body: {
1315
+ checklist,
1316
+ frameCount,
1317
+ kind: "video",
1318
+ prompt,
1319
+ referenceImages: referenceImagePaths.map((filePath) => ({
1320
+ name: path.basename(filePath),
1321
+ previewUrl: fileToDataUrl(filePath, { allowedExts: ALLOWED_IMAGE_EXTS, kind: "참조 이미지" }),
1322
+ })),
1323
+ videoUrl,
1324
+ },
1325
+ method: "POST",
1326
+ timeoutMs: 600_000, // 다운로드 120s + ffmpeg + Gemini 최악 경로 여유
1327
+ });
1328
+ if (payload?.ok === false) {
1329
+ return textResult(`씬 검증 실패: ${payload?.error || "알 수 없는 오류"}\n${payload?.guidance || ""}`.trim(), { isError: true });
1330
+ }
1331
+ return jsonResult({ referenceLoaded: payload?.referenceLoaded, verdict: payload?.verdict || null });
1332
+ }),
1333
+ );
1334
+
1185
1335
  // ── 상세페이지(PDP) ─────────────────────────────────────────────────────────
1186
1336
  server.tool(
1187
1337
  "generate_pdp",
package/lib/config.mjs CHANGED
@@ -2,7 +2,7 @@ import { homedir } from "node:os";
2
2
  import path from "node:path";
3
3
 
4
4
  // 프록시 버전 — 서버가 X-AImakeAll-MCP-Version 으로 하한을 강제(426)할 수 있다.
5
- export const MCP_PROXY_VERSION = "0.6.0";
5
+ export const MCP_PROXY_VERSION = "0.7.0";
6
6
 
7
7
  export const DEFAULT_API_BASE = "https://aimakeall.com";
8
8
  export const DEFAULT_COMPANION_URL = "http://127.0.0.1:9876";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aimakeall-mcp",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "AImakeAll MCP 서버 — Claude Code/Codex에서 자연어로 영상 기획·생성·렌더·퍼블리시 (렌더는 로컬 컴패니언)",
5
5
  "type": "module",
6
6
  "bin": {