beplus-mcp 0.8.0 → 0.9.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 +7 -0
- package/dist/index.js +134 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -89,6 +89,13 @@ Reinicie o cliente. Rode a tool **`whoami`** para confirmar o vínculo.
|
|
|
89
89
|
| `estimate_cost` | Custo em diamantes antes de gerar. |
|
|
90
90
|
| `read_image_metadata` / `analyze_media` | Lê a proveniência embutida numa imagem / analisa mídia (imagem/vídeo/áudio/PDF) com prompt. |
|
|
91
91
|
|
|
92
|
+
> **Referências de arquivo local (desde 0.8.1):** os campos de mídia — `reference_images`,
|
|
93
|
+
> `first_frame_image`, `last_frame_image`, `reference_videos`, `reference_audios`, `motion_video`
|
|
94
|
+
> (geração) e `media_urls` (`analyze_media`) — aceitam **URL pública OU caminho de arquivo local**
|
|
95
|
+
> (`/caminho/foto.png`, `~/Desktop/ref.jpg`, `file://…`). Caminhos locais sobem automaticamente
|
|
96
|
+
> pro R2 da BePlus e viram URL pública antes de chegar ao provider — não precisa mais hospedar a
|
|
97
|
+
> imagem você mesmo.
|
|
98
|
+
|
|
92
99
|
### Calls (reuniões gravadas e transcritas)
|
|
93
100
|
| Tool | O que faz |
|
|
94
101
|
|------|-----------|
|
package/dist/index.js
CHANGED
|
@@ -245,6 +245,110 @@ async function pollUntilDone(client, generationId, budgetMs, jitterSeed = 0) {
|
|
|
245
245
|
return { settled: last.status === "completed" || last.status === "failed", status: last };
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
+
// src/media.ts
|
|
249
|
+
import { readFile } from "fs/promises";
|
|
250
|
+
import { basename, extname, isAbsolute, resolve } from "path";
|
|
251
|
+
import { homedir } from "os";
|
|
252
|
+
import { fileURLToPath } from "url";
|
|
253
|
+
var MIME_BY_EXT = {
|
|
254
|
+
".png": "image/png",
|
|
255
|
+
".jpg": "image/jpeg",
|
|
256
|
+
".jpeg": "image/jpeg",
|
|
257
|
+
".webp": "image/webp",
|
|
258
|
+
".gif": "image/gif",
|
|
259
|
+
".bmp": "image/bmp",
|
|
260
|
+
".svg": "image/svg+xml",
|
|
261
|
+
".mp4": "video/mp4",
|
|
262
|
+
".mov": "video/quicktime",
|
|
263
|
+
".webm": "video/webm",
|
|
264
|
+
".m4v": "video/x-m4v",
|
|
265
|
+
".mp3": "audio/mpeg",
|
|
266
|
+
".wav": "audio/wav",
|
|
267
|
+
".m4a": "audio/mp4",
|
|
268
|
+
".aac": "audio/aac",
|
|
269
|
+
".ogg": "audio/ogg",
|
|
270
|
+
".flac": "audio/flac",
|
|
271
|
+
".pdf": "application/pdf"
|
|
272
|
+
};
|
|
273
|
+
var MAX_UPLOAD_BYTES = 300 * 1024 * 1024;
|
|
274
|
+
var UPLOAD_TIMEOUT_MS = 12e4;
|
|
275
|
+
function isRemoteRef(ref) {
|
|
276
|
+
return /^https?:\/\//i.test(ref);
|
|
277
|
+
}
|
|
278
|
+
function toLocalPath(ref) {
|
|
279
|
+
if (ref.startsWith("file://")) return fileURLToPath(ref);
|
|
280
|
+
if (ref === "~") return homedir();
|
|
281
|
+
if (ref.startsWith("~/")) return resolve(homedir(), ref.slice(2));
|
|
282
|
+
return isAbsolute(ref) ? ref : resolve(process.cwd(), ref);
|
|
283
|
+
}
|
|
284
|
+
function mimeForPath(path) {
|
|
285
|
+
return MIME_BY_EXT[extname(path).toLowerCase()] ?? "application/octet-stream";
|
|
286
|
+
}
|
|
287
|
+
async function uploadFile(cfg, ref) {
|
|
288
|
+
const abs = toLocalPath(ref);
|
|
289
|
+
let buf;
|
|
290
|
+
try {
|
|
291
|
+
buf = await readFile(abs);
|
|
292
|
+
} catch {
|
|
293
|
+
throw new ApiError(
|
|
294
|
+
0,
|
|
295
|
+
`Arquivo local n\xE3o encontrado: "${ref}". Passe um caminho v\xE1lido ou uma URL p\xFAblica (http/https).`
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
const name = basename(abs);
|
|
299
|
+
if (buf.byteLength > MAX_UPLOAD_BYTES) {
|
|
300
|
+
throw new ApiError(
|
|
301
|
+
413,
|
|
302
|
+
`"${name}" tem ${(buf.byteLength / 1024 / 1024).toFixed(1)}MB \u2014 acima do limite de 300MB para upload.`
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
const form = new FormData();
|
|
306
|
+
form.append("file", new Blob([buf], { type: mimeForPath(abs) }), name);
|
|
307
|
+
const controller = new AbortController();
|
|
308
|
+
const timer = setTimeout(() => controller.abort(), UPLOAD_TIMEOUT_MS);
|
|
309
|
+
let res;
|
|
310
|
+
try {
|
|
311
|
+
res = await fetch(`${cfg.baseUrl}/api/v1/files/upload`, {
|
|
312
|
+
method: "POST",
|
|
313
|
+
headers: { Authorization: `Bearer ${cfg.token}` },
|
|
314
|
+
body: form,
|
|
315
|
+
signal: controller.signal
|
|
316
|
+
});
|
|
317
|
+
} catch (e) {
|
|
318
|
+
if (e?.name === "AbortError") {
|
|
319
|
+
throw new ApiError(408, `Tempo esgotado ao subir "${name}" para o R2.`);
|
|
320
|
+
}
|
|
321
|
+
throw new ApiError(0, `Falha de rede ao subir "${name}" para o R2. Confira BEPLUS_API_URL.`);
|
|
322
|
+
} finally {
|
|
323
|
+
clearTimeout(timer);
|
|
324
|
+
}
|
|
325
|
+
const text = await res.text();
|
|
326
|
+
let json;
|
|
327
|
+
if (text) {
|
|
328
|
+
try {
|
|
329
|
+
json = JSON.parse(text);
|
|
330
|
+
} catch {
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (!res.ok) {
|
|
334
|
+
const message = json?.message || res.statusText || `HTTP ${res.status}`;
|
|
335
|
+
throw new ApiError(res.status, `Upload de "${name}" falhou: ${message}`, json);
|
|
336
|
+
}
|
|
337
|
+
const fileUrl = json?.result?.file_url ?? json?.file_url;
|
|
338
|
+
if (!fileUrl) {
|
|
339
|
+
throw new ApiError(0, `O upload de "${name}" n\xE3o retornou uma URL p\xFAblica.`);
|
|
340
|
+
}
|
|
341
|
+
return fileUrl;
|
|
342
|
+
}
|
|
343
|
+
async function resolveRef(cfg, ref) {
|
|
344
|
+
if (!ref) return ref;
|
|
345
|
+
return isRemoteRef(ref) ? ref : uploadFile(cfg, ref);
|
|
346
|
+
}
|
|
347
|
+
async function resolveRefs(cfg, refs) {
|
|
348
|
+
if (!refs || refs.length === 0) return refs;
|
|
349
|
+
return Promise.all(refs.map((r) => isRemoteRef(r) ? Promise.resolve(r) : uploadFile(cfg, r)));
|
|
350
|
+
}
|
|
351
|
+
|
|
248
352
|
// src/log.ts
|
|
249
353
|
function fmt(a) {
|
|
250
354
|
return typeof a === "string" ? a : JSON.stringify(a);
|
|
@@ -325,6 +429,7 @@ var IMAGE_MODELS = [
|
|
|
325
429
|
var VIDEO_MODELS = [
|
|
326
430
|
"bytedance/seedance-2.0",
|
|
327
431
|
"bytedance/seedance-2.0-fast",
|
|
432
|
+
"bytedance/seedance-2.0-mini",
|
|
328
433
|
"kwaivgi/kling-v2.5-turbo-pro",
|
|
329
434
|
"kwaivgi/kling-v2.6",
|
|
330
435
|
"kwaivgi/kling-v2.6-motion-control",
|
|
@@ -396,13 +501,15 @@ function registerGenerationTools(server, client, cfg) {
|
|
|
396
501
|
"generate_image",
|
|
397
502
|
{
|
|
398
503
|
title: "Gerar imagem",
|
|
399
|
-
description: "Gera uma imagem a partir de um prompt usando o IA Lab da BePlus. R\xE1pido (~10-30s). Consome diamantes da conta vinculada. Apenas `prompt` \xE9 obrigat\xF3rio; passe `reference_images` (URLs p\xFAblicas) para image-to-image. Veja custos com estimate_cost / list_models." + TEAM_GATING_NOTE,
|
|
504
|
+
description: "Gera uma imagem a partir de um prompt usando o IA Lab da BePlus. R\xE1pido (~10-30s). Consome diamantes da conta vinculada. Apenas `prompt` \xE9 obrigat\xF3rio; passe `reference_images` (URLs p\xFAblicas OU caminhos de arquivo local \u2014 o MCP sobe pro R2 automaticamente) para image-to-image. Veja custos com estimate_cost / list_models." + TEAM_GATING_NOTE,
|
|
400
505
|
inputSchema: {
|
|
401
506
|
prompt: z.string().min(1).max(4e3).describe("Descri\xE7\xE3o da imagem desejada."),
|
|
402
507
|
model: z.enum(IMAGE_MODELS).default(DEFAULTS.imageModel).describe("nano-banana-2 = barato/r\xE1pido; nano-banana-pro = melhor qualidade; gpt-image-2 = OpenAI."),
|
|
403
508
|
aspect_ratio: z.enum(ASPECT_RATIOS).optional().describe("Propor\xE7\xE3o (nano-banana). Padr\xE3o 1:1."),
|
|
404
509
|
resolution: z.enum(IMAGE_RESOLUTIONS).optional().describe("Resolu\xE7\xE3o (nano-banana): 1K/2K/4K. Padr\xE3o 1K."),
|
|
405
|
-
reference_images: z.array(z.string().
|
|
510
|
+
reference_images: z.array(z.string().min(1)).max(16).optional().describe(
|
|
511
|
+
"Imagens de refer\xEAncia (image-to-image): URL p\xFAblica OU caminho de arquivo local (o MCP faz upload pro R2 automaticamente). At\xE9 14 (nano-banana) / 16 (gpt-image-2)."
|
|
512
|
+
),
|
|
406
513
|
google_search: z.boolean().optional().describe("nano-banana-2: aterra a imagem em busca web do Google (fatos atuais). Padr\xE3o false."),
|
|
407
514
|
image_search: z.boolean().optional().describe("nano-banana-2: usa busca de imagens do Google como refer\xEAncia visual. Padr\xE3o false."),
|
|
408
515
|
size: z.enum(["1024x1024", "1536x1024", "1024x1536", "auto"]).optional().describe("Tamanho (gpt-image-2)."),
|
|
@@ -418,11 +525,12 @@ function registerGenerationTools(server, client, cfg) {
|
|
|
418
525
|
async (args) => {
|
|
419
526
|
try {
|
|
420
527
|
const isGoogle = args.model.startsWith("google/");
|
|
528
|
+
const reference_images = await resolveRefs(cfg, args.reference_images);
|
|
421
529
|
const input = compact({
|
|
422
530
|
prompt: args.prompt,
|
|
423
531
|
aspect_ratio: args.aspect_ratio ?? (isGoogle ? DEFAULTS.imageAspect : void 0),
|
|
424
532
|
resolution: args.resolution ?? (isGoogle ? DEFAULTS.imageResolution : void 0),
|
|
425
|
-
reference_images
|
|
533
|
+
reference_images,
|
|
426
534
|
google_search: args.google_search,
|
|
427
535
|
image_search: args.image_search,
|
|
428
536
|
size: args.size,
|
|
@@ -483,23 +591,27 @@ ${footer}` }, ...blocks]
|
|
|
483
591
|
negative_prompt: z.string().max(2500).optional().describe("O que evitar no v\xEDdeo (Kling)."),
|
|
484
592
|
mode: z.enum(["standard", "pro"]).optional().describe("Qualidade de gera\xE7\xE3o (Kling): standard ou pro."),
|
|
485
593
|
generate_audio: z.boolean().optional().describe("Gerar trilha de \xE1udio junto com o v\xEDdeo (Seedance/Kling). Na web fica ligado por padr\xE3o."),
|
|
486
|
-
first_frame_image: z.string().
|
|
487
|
-
last_frame_image: z.string().
|
|
488
|
-
reference_images: z.array(z.string().
|
|
489
|
-
reference_videos: z.array(z.string().
|
|
490
|
-
reference_audios: z.array(z.string().
|
|
491
|
-
motion_video: z.string().
|
|
594
|
+
first_frame_image: z.string().min(1).optional().describe("Imagem inicial (image-to-video / primeiro frame): URL p\xFAblica OU caminho local (sobe pro R2)."),
|
|
595
|
+
last_frame_image: z.string().min(1).optional().describe("Imagem do \xFAltimo frame (Seedance/Kling): URL p\xFAblica OU caminho local (sobe pro R2)."),
|
|
596
|
+
reference_images: z.array(z.string().min(1)).max(9).optional().describe("Imagens de refer\xEAncia (personagem/estilo) \u2014 omni/Seedance. URL p\xFAblica OU caminho local. At\xE9 9."),
|
|
597
|
+
reference_videos: z.array(z.string().min(1)).max(4).optional().describe("V\xEDdeos de refer\xEAncia (Seedance reference_videos / Kling reference_video): URL OU caminho local."),
|
|
598
|
+
reference_audios: z.array(z.string().min(1)).max(4).optional().describe("\xC1udios de refer\xEAncia (Seedance): URL p\xFAblica OU caminho local (sobe pro R2)."),
|
|
599
|
+
motion_video: z.string().min(1).optional().describe("V\xEDdeo de movimento/m\xE1scara din\xE2mica (kling-v2.6-motion-control): URL OU caminho local."),
|
|
492
600
|
character_orientation: z.string().optional().describe("Orienta\xE7\xE3o do personagem (alguns modelos Kling via Replicate)."),
|
|
493
601
|
keep_original_sound: z.boolean().optional().describe("Manter o som original do v\xEDdeo de refer\xEAncia (alguns modelos Kling)."),
|
|
494
|
-
reference_image: z.string().
|
|
602
|
+
reference_image: z.string().min(1).optional().describe("[Obsoleto \u2014 use first_frame_image] Imagem de refer\xEAncia: URL p\xFAblica OU caminho local."),
|
|
495
603
|
seed: z.number().int().optional(),
|
|
496
604
|
project: z.string().max(64).optional().describe('Projeto (uuid OU code curto, ex.: "VRAO-26"). S\xF3 omita se o usu\xE1rio J\xC1 confirmou o projeto ativo nesta sess\xE3o.')
|
|
497
605
|
}
|
|
498
606
|
},
|
|
499
607
|
async (args) => {
|
|
500
608
|
try {
|
|
501
|
-
const firstFrame = args.first_frame_image ?? args.reference_image;
|
|
502
|
-
const
|
|
609
|
+
const firstFrame = await resolveRef(cfg, args.first_frame_image ?? args.reference_image);
|
|
610
|
+
const endImage = await resolveRef(cfg, args.last_frame_image);
|
|
611
|
+
const referenceImages = await resolveRefs(cfg, args.reference_images);
|
|
612
|
+
const referenceAudios = await resolveRefs(cfg, args.reference_audios);
|
|
613
|
+
const motionVideo = await resolveRef(cfg, args.motion_video);
|
|
614
|
+
const refVideos = await resolveRefs(cfg, args.reference_videos);
|
|
503
615
|
const input = compact({
|
|
504
616
|
prompt: args.prompt,
|
|
505
617
|
aspect_ratio: args.aspect_ratio ?? DEFAULTS.videoAspect,
|
|
@@ -509,12 +621,12 @@ ${footer}` }, ...blocks]
|
|
|
509
621
|
mode: args.mode,
|
|
510
622
|
generate_audio: args.generate_audio,
|
|
511
623
|
image: firstFrame,
|
|
512
|
-
end_image:
|
|
513
|
-
reference_images:
|
|
624
|
+
end_image: endImage,
|
|
625
|
+
reference_images: referenceImages,
|
|
514
626
|
reference_videos: refVideos,
|
|
515
627
|
reference_video: refVideos && refVideos.length > 0 ? refVideos[0] : void 0,
|
|
516
|
-
reference_audios:
|
|
517
|
-
motion_video:
|
|
628
|
+
reference_audios: referenceAudios,
|
|
629
|
+
motion_video: motionVideo,
|
|
518
630
|
character_orientation: args.character_orientation,
|
|
519
631
|
keep_original_sound: args.keep_original_sound,
|
|
520
632
|
seed: args.seed
|
|
@@ -891,7 +1003,7 @@ Limites simult\xE2neos por usu\xE1rio: imagem=${m.limits.perUserMaxImage}, v\xED
|
|
|
891
1003
|
|
|
892
1004
|
// src/tools/analyze.ts
|
|
893
1005
|
import { z as z5 } from "zod";
|
|
894
|
-
function registerAnalyzeTools(server, client,
|
|
1006
|
+
function registerAnalyzeTools(server, client, cfg) {
|
|
895
1007
|
server.registerTool(
|
|
896
1008
|
"analyze_media",
|
|
897
1009
|
{
|
|
@@ -899,16 +1011,19 @@ function registerAnalyzeTools(server, client, _cfg) {
|
|
|
899
1011
|
description: "Envia um prompt + m\xEDdia (imagem, v\xEDdeo, \xE1udio ou PDF) para o Gemini e devolve uma resposta em texto. Use para descrever/analisar/transcrever/resumir/extrair informa\xE7\xE3o de m\xEDdia, ou perguntar algo sobre ela. A m\xEDdia entra como `media_urls` (URLs p\xFAblicas \u2014 tudo que o Google aceita: imagens, v\xEDdeos, \xE1udios, PDFs). V\xEDdeo/\xE1udio grandes funcionam (v\xE3o pela Files API do Google). Consome diamantes (reembolsados se falhar). Veja custo/limites com list_models.",
|
|
900
1012
|
inputSchema: {
|
|
901
1013
|
prompt: z5.string().min(1).max(5e4).describe('O que voc\xEA quer do modelo sobre a m\xEDdia (ex.: "descreva este v\xEDdeo", "transcreva este \xE1udio").'),
|
|
902
|
-
media_urls: z5.array(z5.string().
|
|
1014
|
+
media_urls: z5.array(z5.string().min(1)).max(50).optional().describe(
|
|
1015
|
+
"M\xEDdia (imagem/v\xEDdeo/\xE1udio/PDF): URLs p\xFAblicas OU caminhos de arquivo local (o MCP sobe pro R2 automaticamente). Pode misturar tipos. Opcional (prompt s\xF3-texto tamb\xE9m funciona)."
|
|
1016
|
+
),
|
|
903
1017
|
model: z5.enum(["gemini-flash", "gemini-pro"]).default("gemini-flash").optional().describe("gemini-flash = r\xE1pido/barato (Gemini 3.5 Flash); gemini-pro = mais capaz (Gemini 3.1 Pro).")
|
|
904
1018
|
}
|
|
905
1019
|
},
|
|
906
1020
|
async (args) => {
|
|
907
1021
|
try {
|
|
1022
|
+
const media_urls = await resolveRefs(cfg, args.media_urls);
|
|
908
1023
|
const res = await client.analyzeMedia(
|
|
909
1024
|
compact({
|
|
910
1025
|
prompt: args.prompt,
|
|
911
|
-
media_urls
|
|
1026
|
+
media_urls,
|
|
912
1027
|
model: args.model
|
|
913
1028
|
})
|
|
914
1029
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "beplus-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Conector MCP da equipe BePlus — gere imagens/vídeos/áudio e consulte calls, projetos e clientes pela sua conta BePlus (diamantes, limites e histórico aplicados pela plataforma).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|