@justmpm/mmx-cli 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/dist/media.js ADDED
@@ -0,0 +1,148 @@
1
+ /**
2
+ * @justmcp/mmx-cli - Helpers para MCP content types
3
+ *
4
+ * Converte saídas do CLI (paths de arquivo, buffers, URLs) em MCP content blocks
5
+ * com os tipos corretos:
6
+ * - text: stdout puro ou mensagens
7
+ * - image: base64 + mimeType (PNG/JPG/WebP)
8
+ * - audio: base64 + mimeType (MP3/WAV/PCM)
9
+ * - resource_link: file URI (vídeo, ou arquivos grandes)
10
+ *
11
+ * Estrutura dos content types segue o MCP SDK oficial
12
+ * (`@modelcontextprotocol/sdk/types`).
13
+ */
14
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
15
+ import { basename, extname, isAbsolute, resolve as resolvePath } from "node:path";
16
+ // =============================================================================
17
+ // MIME detection
18
+ // =============================================================================
19
+ const IMAGE_MIME = {
20
+ ".png": "image/png",
21
+ ".jpg": "image/jpeg",
22
+ ".jpeg": "image/jpeg",
23
+ ".webp": "image/webp",
24
+ ".gif": "image/gif",
25
+ };
26
+ const AUDIO_MIME = {
27
+ ".mp3": "audio/mpeg",
28
+ ".wav": "audio/wav",
29
+ ".pcm": "audio/pcm",
30
+ ".m4a": "audio/mp4",
31
+ ".ogg": "audio/ogg",
32
+ };
33
+ const VIDEO_MIME = {
34
+ ".mp4": "video/mp4",
35
+ ".mov": "video/quicktime",
36
+ ".webm": "video/webm",
37
+ };
38
+ /** Detecta MIME type baseado na extensão. Retorna `null` se não reconhecido. */
39
+ export function detectMime(path) {
40
+ const ext = extname(path).toLowerCase();
41
+ return IMAGE_MIME[ext] ?? AUDIO_MIME[ext] ?? VIDEO_MIME[ext] ?? null;
42
+ }
43
+ export function isImageMime(mime) {
44
+ return mime.startsWith("image/");
45
+ }
46
+ export function isAudioMime(mime) {
47
+ return mime.startsWith("audio/");
48
+ }
49
+ export function isVideoMime(mime) {
50
+ return mime.startsWith("video/");
51
+ }
52
+ /** Normaliza path relativo ao cwd e resolve symlinks quando possível. */
53
+ function normalizePath(path) {
54
+ const str = path.toString();
55
+ const abs = isAbsolute(str) ? str : resolvePath(process.cwd(), str);
56
+ // Resolve symlinks (defesa em profundidade — symlinks enganam validação superficial)
57
+ try {
58
+ return realpathSync(abs);
59
+ }
60
+ catch {
61
+ // arquivo não existe ou FS sem suporte a realpath — mantém path absoluto
62
+ return abs;
63
+ }
64
+ }
65
+ // =============================================================================
66
+ // Builders
67
+ // =============================================================================
68
+ /** Content block de texto. */
69
+ export function textContent(text) {
70
+ return { type: "text", text };
71
+ }
72
+ /** Content block de imagem a partir de path em disco. */
73
+ export function imageFromPath(path, mimeType) {
74
+ const absolute = normalizePath(path);
75
+ const buf = readFileSync(absolute);
76
+ const mime = mimeType ?? detectMime(absolute);
77
+ if (!mime || !isImageMime(mime)) {
78
+ throw new Error(`imageFromPath: MIME não detectado ou não é imagem para "${absolute}". Use um dos: ${Object.keys(IMAGE_MIME).join(", ")}`);
79
+ }
80
+ return {
81
+ type: "image",
82
+ data: buf.toString("base64"),
83
+ mimeType: mime,
84
+ };
85
+ }
86
+ /** Content block de imagem a partir de buffer + mimeType explícito. */
87
+ export function imageFromBuffer(buf, mimeType) {
88
+ if (!isImageMime(mimeType)) {
89
+ throw new Error(`imageFromBuffer: mimeType inválido: "${mimeType}"`);
90
+ }
91
+ return { type: "image", data: buf.toString("base64"), mimeType };
92
+ }
93
+ /** Content block de áudio a partir de path em disco. */
94
+ export function audioFromPath(path, mimeType) {
95
+ const absolute = normalizePath(path);
96
+ const buf = readFileSync(absolute);
97
+ const mime = mimeType ?? detectMime(absolute);
98
+ if (!mime || !isAudioMime(mime)) {
99
+ throw new Error(`audioFromPath: MIME não detectado ou não é áudio para "${absolute}". Use um dos: ${Object.keys(AUDIO_MIME).join(", ")}`);
100
+ }
101
+ return {
102
+ type: "audio",
103
+ data: buf.toString("base64"),
104
+ mimeType: mime,
105
+ };
106
+ }
107
+ /** Content block de áudio a partir de buffer + mimeType explícito. */
108
+ export function audioFromBuffer(buf, mimeType) {
109
+ if (!isAudioMime(mimeType)) {
110
+ throw new Error(`audioFromBuffer: mimeType inválido: "${mimeType}"`);
111
+ }
112
+ return { type: "audio", data: buf.toString("base64"), mimeType };
113
+ }
114
+ /**
115
+ * Content block de resource link (file URI).
116
+ *
117
+ * IMPORTANTE: tipo MCP é "resource_link" (não "resource") — definido pelo SDK v1+.
118
+ * Não inclui `blob` nem `text` — apenas a referência ao arquivo.
119
+ *
120
+ * Recomendado para vídeos e arquivos grandes (>1MB) pra evitar base64 no JSON-RPC.
121
+ */
122
+ export function resourceFromPath(path, mimeType, description) {
123
+ const absolute = normalizePath(path);
124
+ if (!existsSync(absolute)) {
125
+ throw new Error(`resourceFromPath: arquivo não existe: "${absolute}"`);
126
+ }
127
+ const mime = mimeType ?? detectMime(absolute) ?? "application/octet-stream";
128
+ const uri = `file:///${absolute.replace(/\\/g, "/")}`;
129
+ let size;
130
+ try {
131
+ size = statSync(absolute).size;
132
+ }
133
+ catch {
134
+ // size é opcional
135
+ }
136
+ const link = {
137
+ type: "resource_link",
138
+ uri,
139
+ name: basename(absolute),
140
+ mimeType: mime,
141
+ };
142
+ if (description !== undefined)
143
+ link.description = description;
144
+ if (size !== undefined)
145
+ link.size = size;
146
+ return link;
147
+ }
148
+ //# sourceMappingURL=media.js.map
@@ -0,0 +1,30 @@
1
+ /**
2
+ * @justmpm/mmx-cli - Tools de autenticação
3
+ *
4
+ * 3 tools relacionadas ao ciclo de auth do `mmx`:
5
+ * - mmx_login: faz login (não-interativo via --api-key)
6
+ * - mmx_auth_status: checa método de auth atual
7
+ * - mmx_quota_show: exibe quotas do Token Plan
8
+ */
9
+ import { z } from "zod";
10
+ import type { McpTool, ToolResult } from "../types.js";
11
+ export declare const loginName = "mmx_login";
12
+ export declare const loginDescription: string;
13
+ export declare const loginInputSchema: z.ZodObject<{
14
+ apiKey: z.ZodOptional<z.ZodString>;
15
+ region: z.ZodOptional<z.ZodEnum<{
16
+ global: "global";
17
+ cn: "cn";
18
+ }>>;
19
+ }, z.core.$strip>;
20
+ export declare function loginHandler(args: unknown): Promise<ToolResult>;
21
+ export declare const authStatusName = "mmx_auth_status";
22
+ export declare const authStatusDescription: string;
23
+ export declare const authStatusInputSchema: z.ZodObject<{}, z.core.$strip>;
24
+ export declare function authStatusHandler(_args: unknown): Promise<ToolResult>;
25
+ export declare const quotaShowName = "mmx_quota_show";
26
+ export declare const quotaShowDescription: string;
27
+ export declare const quotaShowInputSchema: z.ZodObject<{}, z.core.$strip>;
28
+ export declare function quotaShowHandler(_args: unknown): Promise<ToolResult>;
29
+ export declare const authTools: McpTool[];
30
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1,137 @@
1
+ /**
2
+ * @justmpm/mmx-cli - Tools de autenticação
3
+ *
4
+ * 3 tools relacionadas ao ciclo de auth do `mmx`:
5
+ * - mmx_login: faz login (não-interativo via --api-key)
6
+ * - mmx_auth_status: checa método de auth atual
7
+ * - mmx_quota_show: exibe quotas do Token Plan
8
+ */
9
+ import { z } from "zod";
10
+ import { handleToolError, runMmx } from "../utils.js";
11
+ // =============================================================================
12
+ // mmx_login
13
+ // =============================================================================
14
+ export const loginName = "mmx_login";
15
+ export const loginDescription = "Autentica no mmx-cli (MiniMax). " +
16
+ "**Use quando outras tools retornarem 401/403/Unauthorized** — esta é a primeira coisa a tentar. " +
17
+ "Recomendado: defina a variável de ambiente MINIMAX_API_KEY (Pay-as-you-go) e reinicie o cliente MCP — é mais persistente que passar apiKey aqui. " +
18
+ "Se chamar esta tool passando apiKey direto, ela autentica apenas nesta sessão (CLI não persiste em todos os sistemas). " +
19
+ "**Não use Token Plan (sk-cp-...)** — plataforma aplica throttling agressivo em batch. Use Pay-as-you-go (sk-...).";
20
+ export const loginInputSchema = z.object({
21
+ apiKey: z
22
+ .string()
23
+ .optional()
24
+ .describe("API key (sk-xxx). Se fornecida, usa diretamente; do contrário, usa MINIMAX_API_KEY do env."),
25
+ region: z
26
+ .enum(["global", "cn"])
27
+ .optional()
28
+ .describe("Região da API. Default: global."),
29
+ });
30
+ export async function loginHandler(args) {
31
+ const parsed = loginInputSchema.safeParse(args);
32
+ if (!parsed.success) {
33
+ return {
34
+ content: [
35
+ {
36
+ type: "text",
37
+ text: `Erro de validação: ${parsed.error.issues
38
+ .map((i) => `${i.path.join(".")}: ${i.message}`)
39
+ .join("; ")}`,
40
+ },
41
+ ],
42
+ isError: true,
43
+ };
44
+ }
45
+ const cliArgs = ["auth", "login", "--non-interactive", "--quiet"];
46
+ if (parsed.data.region)
47
+ cliArgs.push("--region", parsed.data.region);
48
+ // SECURITY: passa apiKey via env var em vez de --api-key arg.
49
+ // Evita leak em `ps aux` (Linux) ou Get-CimInstance (Windows).
50
+ const env = {};
51
+ if (parsed.data.apiKey)
52
+ env.MINIMAX_API_KEY = parsed.data.apiKey;
53
+ const result = await runMmx(cliArgs, { timeoutMs: 60_000, env });
54
+ if (result.exitCode !== 0) {
55
+ return handleToolError("autenticar no mmx-cli", result);
56
+ }
57
+ const stdout = result.stdout.trim();
58
+ const content = [
59
+ {
60
+ type: "text",
61
+ text: stdout ||
62
+ "Login concluído. A variável de ambiente MINIMAX_API_KEY foi usada (ou a apiKey passada como argumento).",
63
+ },
64
+ {
65
+ type: "text",
66
+ text: "Dica: pra produção, use uma chave Pay-as-you-go (Token Plan não suporta multi-agentes concorrentes).",
67
+ },
68
+ ];
69
+ return { content };
70
+ }
71
+ // =============================================================================
72
+ // mmx_auth_status
73
+ // =============================================================================
74
+ export const authStatusName = "mmx_auth_status";
75
+ export const authStatusDescription = "Mostra o método de autenticação ativo no mmx-cli (api-key, oauth, etc) e a source (config.json, env, flag). " +
76
+ "**Use antes de operações pesadas** (geração em batch, vídeo) pra confirmar que está autenticado. " +
77
+ "**Se retornar erro de auth**, chame mmx_login em seguida.";
78
+ export const authStatusInputSchema = z.object({});
79
+ export async function authStatusHandler(_args) {
80
+ const result = await runMmx(["auth", "status", "--output", "json"]);
81
+ if (result.exitCode !== 0) {
82
+ return handleToolError("checar status de autenticação", result);
83
+ }
84
+ // Tenta parsear pra apresentar formatado; senão, retorna raw.
85
+ let pretty = result.stdout;
86
+ try {
87
+ const parsed = JSON.parse(result.stdout);
88
+ const method = parsed.method ?? "?";
89
+ const source = parsed.source ?? "?";
90
+ const hasKey = typeof parsed.key === "string";
91
+ pretty = `Método: ${method}\nSource: ${source}\nAPI key presente: ${hasKey ? "sim" : "não"}`;
92
+ }
93
+ catch {
94
+ // mantém stdout cru
95
+ }
96
+ return { content: [{ type: "text", text: pretty }] };
97
+ }
98
+ // =============================================================================
99
+ // mmx_quota_show
100
+ // =============================================================================
101
+ export const quotaShowName = "mmx_quota_show";
102
+ export const quotaShowDescription = "Exibe o uso atual de Token Plan (janelas de 5h e semanal). " +
103
+ "**Use antes de gerar imagens/vídeos em batch** para evitar estouro de quota. " +
104
+ "Para Pay-as-you-go, mostra saldo disponível. " +
105
+ "Combine com mmx_auth_status pra confirmar método de billing.";
106
+ export const quotaShowInputSchema = z.object({});
107
+ export async function quotaShowHandler(_args) {
108
+ const result = await runMmx(["quota", "show", "--output", "json"]);
109
+ if (result.exitCode !== 0) {
110
+ return handleToolError("consultar quota", result);
111
+ }
112
+ return { content: [{ type: "text", text: result.stdout || "Sem dados de quota." }] };
113
+ }
114
+ // =============================================================================
115
+ // Aggregate export (consumido por tools/index.ts)
116
+ // =============================================================================
117
+ export const authTools = [
118
+ {
119
+ name: "mmx_login",
120
+ description: loginDescription,
121
+ inputSchema: loginInputSchema,
122
+ handler: loginHandler,
123
+ },
124
+ {
125
+ name: "mmx_auth_status",
126
+ description: authStatusDescription,
127
+ inputSchema: authStatusInputSchema,
128
+ handler: authStatusHandler,
129
+ },
130
+ {
131
+ name: "mmx_quota_show",
132
+ description: quotaShowDescription,
133
+ inputSchema: quotaShowInputSchema,
134
+ handler: quotaShowHandler,
135
+ },
136
+ ];
137
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @justmpm/mmx-cli - Tools de file storage
3
+ *
4
+ * 3 tools para upload, listagem e deleção de arquivos no storage MiniMax:
5
+ * - mmx_file_upload: envia arquivo, recebe file_id
6
+ * - mmx_file_list: lista arquivos do storage
7
+ * - mmx_file_delete: remove um arquivo por ID
8
+ */
9
+ import { z } from "zod";
10
+ import type { McpTool, ToolResult } from "../types.js";
11
+ export declare const fileUploadName = "mmx_file_upload";
12
+ export declare const fileUploadDescription: string;
13
+ export declare const fileUploadInputSchema: z.ZodObject<{
14
+ file: z.ZodString;
15
+ }, z.core.$strip>;
16
+ export declare function fileUploadHandler(args: unknown): Promise<ToolResult>;
17
+ export declare const fileListName = "mmx_file_list";
18
+ export declare const fileListDescription: string;
19
+ export declare const fileListInputSchema: z.ZodObject<{}, z.core.$strip>;
20
+ export declare function fileListHandler(_args: unknown): Promise<ToolResult>;
21
+ export declare const fileDeleteName = "mmx_file_delete";
22
+ export declare const fileDeleteDescription: string;
23
+ export declare const fileDeleteInputSchema: z.ZodObject<{
24
+ fileId: z.ZodString;
25
+ }, z.core.$strip>;
26
+ export declare function fileDeleteHandler(args: unknown): Promise<ToolResult>;
27
+ export declare const fileTools: McpTool[];
28
+ //# sourceMappingURL=files.d.ts.map
@@ -0,0 +1,126 @@
1
+ /**
2
+ * @justmpm/mmx-cli - Tools de file storage
3
+ *
4
+ * 3 tools para upload, listagem e deleção de arquivos no storage MiniMax:
5
+ * - mmx_file_upload: envia arquivo, recebe file_id
6
+ * - mmx_file_list: lista arquivos do storage
7
+ * - mmx_file_delete: remove um arquivo por ID
8
+ */
9
+ import { z } from "zod";
10
+ import { handleToolError, runMmx, validateInputFile } from "../utils.js";
11
+ // =============================================================================
12
+ // mmx_file_upload
13
+ // =============================================================================
14
+ export const fileUploadName = "mmx_file_upload";
15
+ export const fileUploadDescription = "Faz upload de arquivo para o storage MiniMax e retorna file_id. " +
16
+ "**Use o file_id em mmx_vision_describe** e mmx_video_generate (first-frame) para evitar base64. " +
17
+ "Recomendado para imagens >1MB e áudios longos. " +
18
+ "Para usar o fileId em mmx_vision_describe, basta passar como parâmetro 'fileId'.";
19
+ export const fileUploadInputSchema = z.object({
20
+ file: z
21
+ .string()
22
+ .describe("Path local do arquivo (absoluto recomendado). Use barras normais ou escapadas em Windows."),
23
+ });
24
+ export async function fileUploadHandler(args) {
25
+ const parsed = fileUploadInputSchema.safeParse(args);
26
+ if (!parsed.success) {
27
+ return {
28
+ content: [{ type: "text", text: `Erro de validação: ${parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}` }],
29
+ isError: true,
30
+ };
31
+ }
32
+ // SECURITY: valida path (rejeita traversal, device files, e arquivos >10MB).
33
+ const fileCheck = validateInputFile(parsed.data.file);
34
+ if (!fileCheck.ok) {
35
+ return {
36
+ content: [{ type: "text", text: `Erro de validação do path: ${fileCheck.error}` }],
37
+ isError: true,
38
+ };
39
+ }
40
+ const result = await runMmx(["file", "upload", "--file", fileCheck.resolved, "--quiet"]);
41
+ if (result.exitCode !== 0) {
42
+ return handleToolError(`upload do arquivo "${parsed.data.file}"`, result, {
43
+ filePath: fileCheck.resolved,
44
+ });
45
+ }
46
+ return { content: [{ type: "text", text: result.stdout }] };
47
+ }
48
+ // =============================================================================
49
+ // mmx_file_list
50
+ // =============================================================================
51
+ export const fileListName = "mmx_file_list";
52
+ export const fileListDescription = "Lista todos os arquivos no storage MiniMax (retorna JSON com IDs e metadados). " +
53
+ "**Use os file_ids retornados** em mmx_vision_describe, mmx_video_generate (first-frame) ou mmx_file_delete. " +
54
+ "Para ver detalhes de um arquivo específico, faça upload novamente ou procure pelo fileId.";
55
+ export const fileListInputSchema = z.object({});
56
+ export async function fileListHandler(_args) {
57
+ const result = await runMmx(["file", "list", "--output", "json"]);
58
+ if (result.exitCode !== 0) {
59
+ return handleToolError("listar arquivos no storage", result);
60
+ }
61
+ return { content: [{ type: "text", text: result.stdout }] };
62
+ }
63
+ // =============================================================================
64
+ // mmx_file_delete
65
+ // =============================================================================
66
+ export const fileDeleteName = "mmx_file_delete";
67
+ export const fileDeleteDescription = "Remove um arquivo do storage MiniMax por file_id. " +
68
+ "**Esta operação é PERMANENTE e não pode ser desfeita.** " +
69
+ "**Use mmx_file_list primeiro** para descobrir o file_id correto. " +
70
+ "Após deletar, o file_id deixa de funcionar em mmx_vision_describe e mmx_video_generate.";
71
+ export const fileDeleteInputSchema = z.object({
72
+ fileId: z
73
+ .string()
74
+ .describe("ID do arquivo a remover. Use mmx_file_list para descobrir IDs válidos."),
75
+ });
76
+ export async function fileDeleteHandler(args) {
77
+ const parsed = fileDeleteInputSchema.safeParse(args);
78
+ if (!parsed.success) {
79
+ return {
80
+ content: [{ type: "text", text: `Erro de validação: ${parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ")}` }],
81
+ isError: true,
82
+ };
83
+ }
84
+ const result = await runMmx([
85
+ "file",
86
+ "delete",
87
+ "--file-id",
88
+ parsed.data.fileId,
89
+ "--quiet",
90
+ ]);
91
+ if (result.exitCode !== 0) {
92
+ return handleToolError(`deletar arquivo "${parsed.data.fileId}"`, result);
93
+ }
94
+ return {
95
+ content: [
96
+ {
97
+ type: "text",
98
+ text: result.stdout.trim() || `Arquivo ${parsed.data.fileId} removido com sucesso.`,
99
+ },
100
+ ],
101
+ };
102
+ }
103
+ // =============================================================================
104
+ // Aggregate export
105
+ // =============================================================================
106
+ export const fileTools = [
107
+ {
108
+ name: "mmx_file_upload",
109
+ description: fileUploadDescription,
110
+ inputSchema: fileUploadInputSchema,
111
+ handler: fileUploadHandler,
112
+ },
113
+ {
114
+ name: "mmx_file_list",
115
+ description: fileListDescription,
116
+ inputSchema: fileListInputSchema,
117
+ handler: fileListHandler,
118
+ },
119
+ {
120
+ name: "mmx_file_delete",
121
+ description: fileDeleteDescription,
122
+ inputSchema: fileDeleteInputSchema,
123
+ handler: fileDeleteHandler,
124
+ },
125
+ ];
126
+ //# sourceMappingURL=files.js.map
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @justmpm/mmx-cli - Tool: mmx_image_generate
3
+ *
4
+ * Gera imagem com o modelo image-01 da MiniMax. Salva no outputDir do user
5
+ * e retorna resource_link (file URI). Não usa tmpdir() — o user controla
6
+ * onde o arquivo vai.
7
+ *
8
+ * IMPORTANTE: outputDir é OBRIGATÓRIO. Defesa contra:
9
+ * - Path traversal (validado)
10
+ * - Disk leak (user escolhe onde; MCP não acumula temp)
11
+ */
12
+ import { z } from "zod";
13
+ import type { McpTool, ToolResult } from "../types.js";
14
+ export declare const name = "mmx_image_generate";
15
+ export declare const description: string;
16
+ export declare const inputSchema: z.ZodObject<{
17
+ outputDir: z.ZodString;
18
+ prompt: z.ZodString;
19
+ n: z.ZodOptional<z.ZodNumber>;
20
+ aspectRatio: z.ZodOptional<z.ZodEnum<{
21
+ "1:1": "1:1";
22
+ "16:9": "16:9";
23
+ "9:16": "9:16";
24
+ "4:3": "4:3";
25
+ "3:4": "3:4";
26
+ "3:2": "3:2";
27
+ "2:3": "2:3";
28
+ }>>;
29
+ width: z.ZodOptional<z.ZodNumber>;
30
+ height: z.ZodOptional<z.ZodNumber>;
31
+ seed: z.ZodOptional<z.ZodNumber>;
32
+ promptOptimizer: z.ZodOptional<z.ZodBoolean>;
33
+ aigcWatermark: z.ZodOptional<z.ZodBoolean>;
34
+ subjectRef: z.ZodOptional<z.ZodString>;
35
+ }, z.core.$strip>;
36
+ export declare function handler(args: unknown): Promise<ToolResult>;
37
+ export declare const imageGenerateTool: McpTool;
38
+ //# sourceMappingURL=image-generate.d.ts.map
@@ -0,0 +1,174 @@
1
+ /**
2
+ * @justmpm/mmx-cli - Tool: mmx_image_generate
3
+ *
4
+ * Gera imagem com o modelo image-01 da MiniMax. Salva no outputDir do user
5
+ * e retorna resource_link (file URI). Não usa tmpdir() — o user controla
6
+ * onde o arquivo vai.
7
+ *
8
+ * IMPORTANTE: outputDir é OBRIGATÓRIO. Defesa contra:
9
+ * - Path traversal (validado)
10
+ * - Disk leak (user escolhe onde; MCP não acumula temp)
11
+ */
12
+ import { z } from "zod";
13
+ import { basename, isAbsolute, join } from "node:path";
14
+ import { handleToolError, runMmx, validateOutputDir, } from "../utils.js";
15
+ import { resourceFromPath, textContent } from "../media.js";
16
+ // =============================================================================
17
+ // Definição da Tool
18
+ // =============================================================================
19
+ export const name = "mmx_image_generate";
20
+ export const description = "Gera uma ou mais imagens a partir de prompt usando o modelo image-01 da MiniMax. " +
21
+ "**outputDir é OBRIGATÓRIO** — escolha onde salvar (ex: ~/Pictures/mmx). " +
22
+ "Retorna resource_link com .png/.jpg/.webp via file URI. " +
23
+ "Rate limit: 10 RPM. Custo: ~$0.0035/imagem. " +
24
+ "Para descrever/analisar imagem existente use mmx_vision_describe. " +
25
+ "Para batch grande (>20 imagens), consulte mmx_quota_show antes. " +
26
+ "**Não substitui** mmx_vision_describe (gera, não analisa).";
27
+ export const inputSchema = z.object({
28
+ outputDir: z
29
+ .string()
30
+ .min(1)
31
+ .describe("Diretório ABSOLUTO onde salvar a imagem. Obrigatório. Ex: 'C:\\\\Users\\\\x\\\\Pictures\\\\mmx' ou '/home/user/pictures/mmx'."),
32
+ prompt: z
33
+ .string()
34
+ .min(1)
35
+ .describe("Descrição detalhada da imagem. Quanto mais específica, melhor o resultado."),
36
+ n: z
37
+ .number()
38
+ .int()
39
+ .min(1)
40
+ .max(4)
41
+ .optional()
42
+ .describe("Quantidade de imagens. Default 1 (recomendado), max 4."),
43
+ aspectRatio: z
44
+ .enum(["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3"])
45
+ .optional()
46
+ .describe("Proporção da imagem. Ignorado se width+height forem ambos informados."),
47
+ width: z
48
+ .number()
49
+ .int()
50
+ .min(512)
51
+ .max(2048)
52
+ .optional()
53
+ .describe("Largura em pixels (múltiplo de 8). Requer height."),
54
+ height: z
55
+ .number()
56
+ .int()
57
+ .min(512)
58
+ .max(2048)
59
+ .optional()
60
+ .describe("Altura em pixels (múltiplo de 8). Requer width."),
61
+ seed: z
62
+ .number()
63
+ .int()
64
+ .optional()
65
+ .describe("Seed para resultado reproduzível."),
66
+ promptOptimizer: z
67
+ .boolean()
68
+ .optional()
69
+ .describe("Otimiza o prompt antes de gerar. Default: false."),
70
+ aigcWatermark: z
71
+ .boolean()
72
+ .optional()
73
+ .describe("Embutir watermark de conteúdo gerado por IA."),
74
+ subjectRef: z
75
+ .string()
76
+ .optional()
77
+ .describe("Referência de sujeito. Formato: 'type=character,image=path-or-url'."),
78
+ });
79
+ // =============================================================================
80
+ // Handler
81
+ // =============================================================================
82
+ export async function handler(args) {
83
+ const parsed = inputSchema.safeParse(args);
84
+ if (!parsed.success) {
85
+ return {
86
+ content: [
87
+ {
88
+ type: "text",
89
+ text: `Erro de validação: ${parsed.error.issues
90
+ .map((i) => `${i.path.join(".")}: ${i.message}`)
91
+ .join("; ")}`,
92
+ },
93
+ ],
94
+ isError: true,
95
+ };
96
+ }
97
+ const data = parsed.data;
98
+ // Valida outputDir antes de gastar tempo/API
99
+ const dirCheck = validateOutputDir(data.outputDir);
100
+ if (!dirCheck.ok) {
101
+ return {
102
+ content: [{ type: "text", text: `Erro no outputDir: ${dirCheck.error}\nUse path absoluto.` }],
103
+ isError: true,
104
+ };
105
+ }
106
+ const outputDir = dirCheck.resolved;
107
+ const count = data.n ?? 1;
108
+ // CLI salva direto no outputDir (sem temp). Usa --out-prefix pra organizar.
109
+ const prefix = `mmx-img-${Date.now()}`;
110
+ const cliArgs = [
111
+ "image",
112
+ "generate",
113
+ "--prompt",
114
+ data.prompt,
115
+ "--n",
116
+ String(count),
117
+ "--out-dir",
118
+ outputDir,
119
+ "--out-prefix",
120
+ prefix,
121
+ "--quiet",
122
+ ];
123
+ if (data.aspectRatio)
124
+ cliArgs.push("--aspect-ratio", data.aspectRatio);
125
+ if (data.width && data.height) {
126
+ cliArgs.push("--width", String(data.width), "--height", String(data.height));
127
+ }
128
+ if (data.seed !== undefined)
129
+ cliArgs.push("--seed", String(data.seed));
130
+ if (data.promptOptimizer)
131
+ cliArgs.push("--prompt-optimizer");
132
+ if (data.aigcWatermark)
133
+ cliArgs.push("--aigc-watermark");
134
+ if (data.subjectRef)
135
+ cliArgs.push("--subject-ref", data.subjectRef);
136
+ const result = await runMmx(cliArgs, { timeoutMs: 180_000 });
137
+ if (result.exitCode !== 0) {
138
+ return handleToolError("gerar imagem", result);
139
+ }
140
+ // stdout retorna paths salvos, um por linha
141
+ const savedPaths = result.stdout
142
+ .trim()
143
+ .split(/\r?\n/)
144
+ .filter(Boolean)
145
+ .map((p) => (isAbsolute(p) ? p : join(outputDir, basename(p))));
146
+ if (savedPaths.length === 0) {
147
+ return {
148
+ content: [
149
+ textContent("mmx não reportou arquivos salvos. Possível falha silenciosa."),
150
+ ],
151
+ isError: true,
152
+ };
153
+ }
154
+ const content = savedPaths.map((path) => {
155
+ try {
156
+ return resourceFromPath(path, undefined, `Imagem gerada (${basename(path)})`);
157
+ }
158
+ catch (err) {
159
+ const msg = err instanceof Error ? err.message : String(err);
160
+ return textContent(`Imagem gerada mas arquivo não encontrado em ${path}: ${msg}`);
161
+ }
162
+ });
163
+ return { content };
164
+ }
165
+ // =============================================================================
166
+ // Aggregate export
167
+ // =============================================================================
168
+ export const imageGenerateTool = {
169
+ name: "mmx_image_generate",
170
+ description,
171
+ inputSchema,
172
+ handler,
173
+ };
174
+ //# sourceMappingURL=image-generate.js.map