beplus-mcp 0.8.0 → 0.8.1
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 +133 -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);
|
|
@@ -396,13 +500,15 @@ function registerGenerationTools(server, client, cfg) {
|
|
|
396
500
|
"generate_image",
|
|
397
501
|
{
|
|
398
502
|
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,
|
|
503
|
+
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
504
|
inputSchema: {
|
|
401
505
|
prompt: z.string().min(1).max(4e3).describe("Descri\xE7\xE3o da imagem desejada."),
|
|
402
506
|
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
507
|
aspect_ratio: z.enum(ASPECT_RATIOS).optional().describe("Propor\xE7\xE3o (nano-banana). Padr\xE3o 1:1."),
|
|
404
508
|
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().
|
|
509
|
+
reference_images: z.array(z.string().min(1)).max(16).optional().describe(
|
|
510
|
+
"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)."
|
|
511
|
+
),
|
|
406
512
|
google_search: z.boolean().optional().describe("nano-banana-2: aterra a imagem em busca web do Google (fatos atuais). Padr\xE3o false."),
|
|
407
513
|
image_search: z.boolean().optional().describe("nano-banana-2: usa busca de imagens do Google como refer\xEAncia visual. Padr\xE3o false."),
|
|
408
514
|
size: z.enum(["1024x1024", "1536x1024", "1024x1536", "auto"]).optional().describe("Tamanho (gpt-image-2)."),
|
|
@@ -418,11 +524,12 @@ function registerGenerationTools(server, client, cfg) {
|
|
|
418
524
|
async (args) => {
|
|
419
525
|
try {
|
|
420
526
|
const isGoogle = args.model.startsWith("google/");
|
|
527
|
+
const reference_images = await resolveRefs(cfg, args.reference_images);
|
|
421
528
|
const input = compact({
|
|
422
529
|
prompt: args.prompt,
|
|
423
530
|
aspect_ratio: args.aspect_ratio ?? (isGoogle ? DEFAULTS.imageAspect : void 0),
|
|
424
531
|
resolution: args.resolution ?? (isGoogle ? DEFAULTS.imageResolution : void 0),
|
|
425
|
-
reference_images
|
|
532
|
+
reference_images,
|
|
426
533
|
google_search: args.google_search,
|
|
427
534
|
image_search: args.image_search,
|
|
428
535
|
size: args.size,
|
|
@@ -483,23 +590,27 @@ ${footer}` }, ...blocks]
|
|
|
483
590
|
negative_prompt: z.string().max(2500).optional().describe("O que evitar no v\xEDdeo (Kling)."),
|
|
484
591
|
mode: z.enum(["standard", "pro"]).optional().describe("Qualidade de gera\xE7\xE3o (Kling): standard ou pro."),
|
|
485
592
|
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().
|
|
593
|
+
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)."),
|
|
594
|
+
last_frame_image: z.string().min(1).optional().describe("Imagem do \xFAltimo frame (Seedance/Kling): URL p\xFAblica OU caminho local (sobe pro R2)."),
|
|
595
|
+
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."),
|
|
596
|
+
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."),
|
|
597
|
+
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)."),
|
|
598
|
+
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
599
|
character_orientation: z.string().optional().describe("Orienta\xE7\xE3o do personagem (alguns modelos Kling via Replicate)."),
|
|
493
600
|
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().
|
|
601
|
+
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
602
|
seed: z.number().int().optional(),
|
|
496
603
|
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
604
|
}
|
|
498
605
|
},
|
|
499
606
|
async (args) => {
|
|
500
607
|
try {
|
|
501
|
-
const firstFrame = args.first_frame_image ?? args.reference_image;
|
|
502
|
-
const
|
|
608
|
+
const firstFrame = await resolveRef(cfg, args.first_frame_image ?? args.reference_image);
|
|
609
|
+
const endImage = await resolveRef(cfg, args.last_frame_image);
|
|
610
|
+
const referenceImages = await resolveRefs(cfg, args.reference_images);
|
|
611
|
+
const referenceAudios = await resolveRefs(cfg, args.reference_audios);
|
|
612
|
+
const motionVideo = await resolveRef(cfg, args.motion_video);
|
|
613
|
+
const refVideos = await resolveRefs(cfg, args.reference_videos);
|
|
503
614
|
const input = compact({
|
|
504
615
|
prompt: args.prompt,
|
|
505
616
|
aspect_ratio: args.aspect_ratio ?? DEFAULTS.videoAspect,
|
|
@@ -509,12 +620,12 @@ ${footer}` }, ...blocks]
|
|
|
509
620
|
mode: args.mode,
|
|
510
621
|
generate_audio: args.generate_audio,
|
|
511
622
|
image: firstFrame,
|
|
512
|
-
end_image:
|
|
513
|
-
reference_images:
|
|
623
|
+
end_image: endImage,
|
|
624
|
+
reference_images: referenceImages,
|
|
514
625
|
reference_videos: refVideos,
|
|
515
626
|
reference_video: refVideos && refVideos.length > 0 ? refVideos[0] : void 0,
|
|
516
|
-
reference_audios:
|
|
517
|
-
motion_video:
|
|
627
|
+
reference_audios: referenceAudios,
|
|
628
|
+
motion_video: motionVideo,
|
|
518
629
|
character_orientation: args.character_orientation,
|
|
519
630
|
keep_original_sound: args.keep_original_sound,
|
|
520
631
|
seed: args.seed
|
|
@@ -891,7 +1002,7 @@ Limites simult\xE2neos por usu\xE1rio: imagem=${m.limits.perUserMaxImage}, v\xED
|
|
|
891
1002
|
|
|
892
1003
|
// src/tools/analyze.ts
|
|
893
1004
|
import { z as z5 } from "zod";
|
|
894
|
-
function registerAnalyzeTools(server, client,
|
|
1005
|
+
function registerAnalyzeTools(server, client, cfg) {
|
|
895
1006
|
server.registerTool(
|
|
896
1007
|
"analyze_media",
|
|
897
1008
|
{
|
|
@@ -899,16 +1010,19 @@ function registerAnalyzeTools(server, client, _cfg) {
|
|
|
899
1010
|
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
1011
|
inputSchema: {
|
|
901
1012
|
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().
|
|
1013
|
+
media_urls: z5.array(z5.string().min(1)).max(50).optional().describe(
|
|
1014
|
+
"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)."
|
|
1015
|
+
),
|
|
903
1016
|
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
1017
|
}
|
|
905
1018
|
},
|
|
906
1019
|
async (args) => {
|
|
907
1020
|
try {
|
|
1021
|
+
const media_urls = await resolveRefs(cfg, args.media_urls);
|
|
908
1022
|
const res = await client.analyzeMedia(
|
|
909
1023
|
compact({
|
|
910
1024
|
prompt: args.prompt,
|
|
911
|
-
media_urls
|
|
1025
|
+
media_urls,
|
|
912
1026
|
model: args.model
|
|
913
1027
|
})
|
|
914
1028
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "beplus-mcp",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.1",
|
|
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": {
|