aimakeall-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 +50 -0
- package/index.mjs +38 -0
- package/lib/api-client.mjs +143 -0
- package/lib/cloud-tools.mjs +644 -0
- package/lib/companion-client.mjs +153 -0
- package/lib/companion-tools.mjs +210 -0
- package/lib/config.mjs +41 -0
- package/lib/config.test.mjs +43 -0
- package/lib/media-store.mjs +136 -0
- package/lib/media-store.test.mjs +99 -0
- package/lib/mp3-duration.mjs +96 -0
- package/lib/mp3-duration.test.mjs +50 -0
- package/lib/payload-guard.mjs +103 -0
- package/lib/payload-guard.test.mjs +76 -0
- package/lib/render-job-store.mjs +67 -0
- package/lib/render-job-store.test.mjs +46 -0
- package/lib/usage-event.mjs +7 -0
- package/package.json +25 -0
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# aimakeall-mcp
|
|
2
|
+
|
|
3
|
+
Claude Code·Codex 같은 MCP 클라이언트에서 자연어로 AImakeAll 영상을 만드는 stdio MCP 서버입니다.
|
|
4
|
+
|
|
5
|
+
- **클라우드 툴**: 기획 → 씬 이미지/영상 생성 → TTS/음악 → 타임라인 스티치 → YouTube 퍼블리시 (PAT 인증, aimakeall.com API)
|
|
6
|
+
- **로컬 툴**: mp4 렌더 — **항상 이 PC의 AImakeAll 컴패니언(127.0.0.1:9876)에서 수행**됩니다. 서버는 렌더하지 않습니다.
|
|
7
|
+
|
|
8
|
+
## 설정
|
|
9
|
+
|
|
10
|
+
1. [aimakeall.com](https://aimakeall.com) 로그인 → **API 키 설정 → MCP 토큰**에서 토큰 발급 (발급 직후 1회만 표시)
|
|
11
|
+
2. AImakeAll 컴패니언 앱 설치·실행 (렌더용)
|
|
12
|
+
3. MCP 클라이언트 설정 — 버전을 고정해 주세요:
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{
|
|
16
|
+
"mcpServers": {
|
|
17
|
+
"aimakeall": {
|
|
18
|
+
"command": "npx",
|
|
19
|
+
"args": ["-y", "aimakeall-mcp@0.1.0"],
|
|
20
|
+
"env": { "AIMAKEALL_PAT": "aio_pat_..." }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 환경변수
|
|
27
|
+
|
|
28
|
+
| 변수 | 기본값 | 설명 |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| `AIMAKEALL_PAT` | (필수) | MCP 토큰 (`aio_pat_...`) |
|
|
31
|
+
| `AIMAKEALL_API_BASE` | `https://aimakeall.com` | API 서버 |
|
|
32
|
+
| `AIMAKEALL_COMPANION_URL` | `http://127.0.0.1:9876` | 로컬 컴패니언 (SSH 터널 시 변경) |
|
|
33
|
+
| `AIMAKEALL_OUTPUT_DIR` | `~/Downloads/AImakeAll` | 렌더 결과 mp4 저장 위치 |
|
|
34
|
+
|
|
35
|
+
## 기본 플로우 (쇼츠)
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
plan_shorts_video → (씬마다) generate_scene_image → generate_scene_video_prompt → generate_scene_video
|
|
39
|
+
→ tts_narration → stitch_timeline → render_start → render_status(폴링) → render_result
|
|
40
|
+
→ ai_publish_metadata → publish_youtube
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- 씬 영상 생성은 동기 호출로 최대 15분 걸립니다 — MCP 클라이언트의 툴 타임아웃(`MCP_TIMEOUT`)을 충분히 잡아주세요.
|
|
44
|
+
- 렌더 잡은 `~/.aimakeall/render-jobs.json`에 기록되어 프록시 재시작 후 `list_render_jobs`로 복구할 수 있습니다.
|
|
45
|
+
- 대용량 산출물(TTS mp3, 스티치 매니페스트)은 파일 경로로 주고받아 모델 컨텍스트를 오염시키지 않습니다.
|
|
46
|
+
|
|
47
|
+
## 한도
|
|
48
|
+
|
|
49
|
+
- 쓰기(생성) 호출 분당 20회, 조회 분당 120회, 동시 생성 2개 (서버 강제)
|
|
50
|
+
- 사용자당 활성 PAT 5개, 기본 만료 90일
|
package/index.mjs
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// AImakeAll MCP 서버 (stdio) — Claude Code/Codex에서 자연어로 영상 제작.
|
|
3
|
+
// 클라우드 툴은 PAT(AIMAKEALL_PAT)로 aimakeall.com API를, 렌더 툴은 이 PC의
|
|
4
|
+
// 컴패니언(127.0.0.1:9876)을 호출한다. 렌더는 절대 서버에서 수행되지 않는다.
|
|
5
|
+
|
|
6
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
|
+
|
|
9
|
+
import { createApiClient } from "./lib/api-client.mjs";
|
|
10
|
+
import { registerCloudTools } from "./lib/cloud-tools.mjs";
|
|
11
|
+
import { registerCompanionTools } from "./lib/companion-tools.mjs";
|
|
12
|
+
import { MCP_PROXY_VERSION, resolveConfig } from "./lib/config.mjs";
|
|
13
|
+
import { pruneOldMedia } from "./lib/media-store.mjs";
|
|
14
|
+
|
|
15
|
+
const config = resolveConfig();
|
|
16
|
+
|
|
17
|
+
// 오래된 스테이징 미디어 정리 (48h TTL) — 숨김 디렉터리가 무한정 커지는 것 방지.
|
|
18
|
+
try {
|
|
19
|
+
pruneOldMedia(config.stateDir);
|
|
20
|
+
} catch {
|
|
21
|
+
// 정리 실패는 무시
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!config.pat) {
|
|
25
|
+
console.error("[aimakeall-mcp] AIMAKEALL_PAT 미설정 — 클라우드 툴은 안내만 반환합니다. aimakeall.com → API 키 설정에서 MCP 토큰을 발급해 환경변수로 넣어주세요.");
|
|
26
|
+
} else if (!config.patLooksValid) {
|
|
27
|
+
console.error("[aimakeall-mcp] AIMAKEALL_PAT 형식이 올바르지 않습니다 (aio_pat_... 이어야 함).");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const server = new McpServer({ name: "aimakeall", version: MCP_PROXY_VERSION });
|
|
31
|
+
|
|
32
|
+
const api = createApiClient({ apiBase: config.apiBase, pat: config.pat });
|
|
33
|
+
registerCloudTools(server, config, api);
|
|
34
|
+
registerCompanionTools(server, config);
|
|
35
|
+
|
|
36
|
+
const transport = new StdioServerTransport();
|
|
37
|
+
await server.connect(transport);
|
|
38
|
+
console.error(`[aimakeall-mcp] v${MCP_PROXY_VERSION} 시작 — API ${config.apiBase}, 컴패니언 ${config.companionUrl}`);
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { MCP_PROXY_VERSION } from "./config.mjs";
|
|
2
|
+
|
|
3
|
+
// aimakeall.com API 클라이언트 — PAT(Bearer) 인증 + 프록시 버전 핸드셰이크.
|
|
4
|
+
// 서버 오류 코드를 사용자 행동 가능한 한국어 안내로 매핑한다.
|
|
5
|
+
|
|
6
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
7
|
+
|
|
8
|
+
export class AimakeallApiError extends Error {
|
|
9
|
+
constructor(message, { errorCode = "", retryAfterSeconds = 0, status = 0 } = {}) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.errorCode = errorCode;
|
|
12
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
13
|
+
this.status = status;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function describeApiFailure(status, payload) {
|
|
18
|
+
const code = String(payload?.error || "");
|
|
19
|
+
if (code === "MACHINE_TOKEN_INVALID_TOKEN") return "PAT가 유효하지 않습니다. AIMAKEALL_PAT 값을 다시 확인하세요.";
|
|
20
|
+
if (code === "MACHINE_TOKEN_EXPIRED") return "PAT가 만료되었습니다. aimakeall.com → API 키 설정에서 새 토큰을 발급해 주세요.";
|
|
21
|
+
if (code === "MACHINE_TOKEN_REVOKED") return "회수된 PAT입니다. aimakeall.com → API 키 설정에서 새 토큰을 발급해 주세요.";
|
|
22
|
+
if (code === "MACHINE_TOKEN_TEACHER_FORBIDDEN") return "교사 계정은 MCP를 사용할 수 없습니다.";
|
|
23
|
+
if (code === "MACHINE_ROUTE_FORBIDDEN") return "이 작업은 MCP 토큰으로 호출할 수 없는 경로입니다.";
|
|
24
|
+
if (code === "MACHINE_RATE_LIMITED") return "요청 한도를 초과했습니다. 잠시 후 다시 시도하세요.";
|
|
25
|
+
if (code === "MACHINE_CONCURRENCY_LIMITED") return "동시 생성 호출 한도(2개)에 도달했습니다. 진행 중인 작업이 끝난 뒤 다시 시도하세요.";
|
|
26
|
+
if (code === "PLAN_UPGRADE_REQUIRED") return String(payload?.message || "플랜 업그레이드가 필요한 기능입니다.");
|
|
27
|
+
if (status === 413) return "요청 크기가 서버 상한을 초과했습니다. 오디오는 더 짧게/낮은 비트레이트로, 이미지는 더 작게 줄여 다시 시도하세요.";
|
|
28
|
+
if (status === 431) return "요청 헤더가 너무 큽니다. 설명(description)이나 제목을 줄여 다시 시도하세요.";
|
|
29
|
+
if (status === 426) return "MCP 설정(mcpServers.args)의 aimakeall-mcp 버전을 최신으로 올린 뒤 클라이언트를 재시작하세요.";
|
|
30
|
+
const message = String(payload?.message || payload?.error || "");
|
|
31
|
+
return message || `AImakeAll API 요청 실패 (HTTP ${status})`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createApiClient({ apiBase, pat }) {
|
|
35
|
+
async function request(pathName, { method = "GET", body, timeoutMs = DEFAULT_TIMEOUT_MS, rawBody, headers = {} } = {}) {
|
|
36
|
+
const requestHeaders = {
|
|
37
|
+
Authorization: `Bearer ${pat}`,
|
|
38
|
+
"X-AImakeAll-MCP-Version": MCP_PROXY_VERSION,
|
|
39
|
+
...headers,
|
|
40
|
+
};
|
|
41
|
+
let requestBody;
|
|
42
|
+
if (rawBody !== undefined) {
|
|
43
|
+
requestBody = rawBody;
|
|
44
|
+
} else if (body !== undefined) {
|
|
45
|
+
requestHeaders["Content-Type"] = "application/json";
|
|
46
|
+
requestBody = JSON.stringify(body);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const response = await fetch(`${apiBase}${pathName}`, {
|
|
50
|
+
body: requestBody,
|
|
51
|
+
// Node fetch 는 스트림 바디에 duplex 지정이 필요하다.
|
|
52
|
+
...(rawBody !== undefined && typeof rawBody?.pipe !== "function" && rawBody?.[Symbol.asyncIterator] ? { duplex: "half" } : {}),
|
|
53
|
+
headers: requestHeaders,
|
|
54
|
+
method,
|
|
55
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const text = await response.text();
|
|
59
|
+
let payload = null;
|
|
60
|
+
try {
|
|
61
|
+
payload = text ? JSON.parse(text) : null;
|
|
62
|
+
} catch {
|
|
63
|
+
payload = null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!response.ok) {
|
|
67
|
+
throw new AimakeallApiError(describeApiFailure(response.status, payload), {
|
|
68
|
+
errorCode: String(payload?.error || ""),
|
|
69
|
+
retryAfterSeconds: Number(payload?.retryAfterSeconds) || Number(response.headers.get("retry-after")) || 0,
|
|
70
|
+
status: response.status,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return payload;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 바이너리 응답 라우트(TTS 합성, suno 다운로드) — 실패면 JSON 에러 파싱, 성공이면 buffer.
|
|
77
|
+
async function requestBinary(pathName, { method = "GET", body, timeoutMs = DEFAULT_TIMEOUT_MS } = {}) {
|
|
78
|
+
const headers = {
|
|
79
|
+
Authorization: `Bearer ${pat}`,
|
|
80
|
+
"X-AImakeAll-MCP-Version": MCP_PROXY_VERSION,
|
|
81
|
+
};
|
|
82
|
+
let requestBody;
|
|
83
|
+
if (body !== undefined) {
|
|
84
|
+
headers["Content-Type"] = "application/json";
|
|
85
|
+
requestBody = JSON.stringify(body);
|
|
86
|
+
}
|
|
87
|
+
const response = await fetch(`${apiBase}${pathName}`, {
|
|
88
|
+
body: requestBody,
|
|
89
|
+
headers,
|
|
90
|
+
method,
|
|
91
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
92
|
+
});
|
|
93
|
+
if (!response.ok) {
|
|
94
|
+
let payload = null;
|
|
95
|
+
try {
|
|
96
|
+
payload = JSON.parse(await response.text());
|
|
97
|
+
} catch {
|
|
98
|
+
payload = null;
|
|
99
|
+
}
|
|
100
|
+
throw new AimakeallApiError(describeApiFailure(response.status, payload), {
|
|
101
|
+
errorCode: String(payload?.error || ""),
|
|
102
|
+
status: response.status,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
buffer: Buffer.from(await response.arrayBuffer()),
|
|
107
|
+
contentType: response.headers.get("content-type") || "application/octet-stream",
|
|
108
|
+
headers: response.headers,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 스트림 업로드(YouTube 퍼블리시) — 로컬 파일을 raw body 로 전송.
|
|
113
|
+
async function requestStreamUpload(pathName, { stream, contentType, contentLength, timeoutMs }) {
|
|
114
|
+
const response = await fetch(`${apiBase}${pathName}`, {
|
|
115
|
+
body: stream,
|
|
116
|
+
duplex: "half",
|
|
117
|
+
headers: {
|
|
118
|
+
Authorization: `Bearer ${pat}`,
|
|
119
|
+
"Content-Length": String(contentLength),
|
|
120
|
+
"Content-Type": contentType,
|
|
121
|
+
"X-AImakeAll-MCP-Version": MCP_PROXY_VERSION,
|
|
122
|
+
},
|
|
123
|
+
method: "POST",
|
|
124
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
125
|
+
});
|
|
126
|
+
const text = await response.text();
|
|
127
|
+
let payload = null;
|
|
128
|
+
try {
|
|
129
|
+
payload = text ? JSON.parse(text) : null;
|
|
130
|
+
} catch {
|
|
131
|
+
payload = null;
|
|
132
|
+
}
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
throw new AimakeallApiError(describeApiFailure(response.status, payload), {
|
|
135
|
+
errorCode: String(payload?.error || ""),
|
|
136
|
+
status: response.status,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return payload;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { request, requestBinary, requestStreamUpload };
|
|
143
|
+
}
|