beplus-mcp 0.25.0 → 0.26.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 +17 -0
- package/dist/index.js +1053 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -13050,6 +13050,7 @@ function loadConfig() {
|
|
|
13050
13050
|
return {
|
|
13051
13051
|
baseUrl,
|
|
13052
13052
|
apiBase: `${baseUrl}/api/v1/mcp`,
|
|
13053
|
+
canvasBase: `${baseUrl}/api/v1/canvas`,
|
|
13053
13054
|
token,
|
|
13054
13055
|
inlineImages,
|
|
13055
13056
|
inlineImageMax: 4,
|
|
@@ -21200,8 +21201,9 @@ var BeplusClient = class {
|
|
|
21200
21201
|
this.cfg = cfg;
|
|
21201
21202
|
}
|
|
21202
21203
|
cfg;
|
|
21204
|
+
/** Público para os módulos de rota própria (ex.: Canvas) reaproveitarem auth, timeout e erros. */
|
|
21203
21205
|
async request(method, path, opts = {}) {
|
|
21204
|
-
const url = new URL(`${this.cfg.apiBase}${path}`);
|
|
21206
|
+
const url = new URL(`${opts.base ?? this.cfg.apiBase}${path}`);
|
|
21205
21207
|
if (opts.query) {
|
|
21206
21208
|
for (const [k, v] of Object.entries(opts.query)) {
|
|
21207
21209
|
if (v !== void 0 && v !== null && v !== "") url.searchParams.set(k, String(v));
|
|
@@ -23466,11 +23468,1052 @@ ${calls.map(callLine).join("\n\n")}`);
|
|
|
23466
23468
|
);
|
|
23467
23469
|
}
|
|
23468
23470
|
|
|
23471
|
+
// src/canvas/api.ts
|
|
23472
|
+
var PINNED_CORE_VERSION = "ca7b3b406184c7f679f555c3a008013197182b23";
|
|
23473
|
+
function errorRaw(err) {
|
|
23474
|
+
if (!(err instanceof ApiError)) return void 0;
|
|
23475
|
+
const raw = err.body?.error?.raw;
|
|
23476
|
+
return raw && typeof raw === "object" ? raw : void 0;
|
|
23477
|
+
}
|
|
23478
|
+
var CanvasApi = class {
|
|
23479
|
+
constructor(client, cfg) {
|
|
23480
|
+
this.client = client;
|
|
23481
|
+
this.cfg = cfg;
|
|
23482
|
+
}
|
|
23483
|
+
client;
|
|
23484
|
+
cfg;
|
|
23485
|
+
coreVersion = PINNED_CORE_VERSION;
|
|
23486
|
+
userId = null;
|
|
23487
|
+
req(method, path, opts = {}) {
|
|
23488
|
+
return this.client.request(method, path, { ...opts, base: this.cfg.canvasBase });
|
|
23489
|
+
}
|
|
23490
|
+
/** Token do `beplus-mcp login` (OAuth) não abre `/api/v1/canvas`: só PAT. */
|
|
23491
|
+
get usesPat() {
|
|
23492
|
+
return this.cfg.token.startsWith("pat_");
|
|
23493
|
+
}
|
|
23494
|
+
/** `createdBy` dos nós que o MCP cria. Nulo seria "motor de execução" no Canvas. */
|
|
23495
|
+
async me() {
|
|
23496
|
+
if (!this.userId) this.userId = (await this.client.me()).userId;
|
|
23497
|
+
return this.userId;
|
|
23498
|
+
}
|
|
23499
|
+
listBoards(kind = "board") {
|
|
23500
|
+
return this.req("GET", "/boards", { query: { kind } });
|
|
23501
|
+
}
|
|
23502
|
+
createBoard(body) {
|
|
23503
|
+
return this.req("POST", "/boards", { body });
|
|
23504
|
+
}
|
|
23505
|
+
snapshot(boardId2) {
|
|
23506
|
+
return this.req("GET", `/boards/${boardId2}`);
|
|
23507
|
+
}
|
|
23508
|
+
/** Replay depois de `after`. `null` quando o servidor manda reler a foto (410). */
|
|
23509
|
+
async opsAfter(boardId2, after) {
|
|
23510
|
+
try {
|
|
23511
|
+
const { envelopes } = await this.req("GET", `/boards/${boardId2}/ops`, {
|
|
23512
|
+
query: { after }
|
|
23513
|
+
});
|
|
23514
|
+
return envelopes;
|
|
23515
|
+
} catch (e) {
|
|
23516
|
+
if (e instanceof ApiError && e.httpStatus === 410) return null;
|
|
23517
|
+
throw e;
|
|
23518
|
+
}
|
|
23519
|
+
}
|
|
23520
|
+
async commit(boardId2, ops) {
|
|
23521
|
+
const send = () => this.req("POST", `/boards/${boardId2}/ops`, {
|
|
23522
|
+
body: { coreVersion: this.coreVersion, ops }
|
|
23523
|
+
});
|
|
23524
|
+
try {
|
|
23525
|
+
return await send();
|
|
23526
|
+
} catch (e) {
|
|
23527
|
+
const raw = errorRaw(e);
|
|
23528
|
+
if (!(e instanceof ApiError) || e.httpStatus !== 409 || raw?.reason !== "core-mismatch") throw e;
|
|
23529
|
+
const serverVersion = typeof raw.coreVersion === "string" ? raw.coreVersion : "";
|
|
23530
|
+
if (!serverVersion || serverVersion === this.coreVersion) throw new CoreMismatchError(serverVersion);
|
|
23531
|
+
this.coreVersion = serverVersion;
|
|
23532
|
+
try {
|
|
23533
|
+
return await send();
|
|
23534
|
+
} catch (again) {
|
|
23535
|
+
if (again instanceof ApiError && again.httpStatus === 409 && errorRaw(again)?.reason === "core-mismatch") {
|
|
23536
|
+
throw new CoreMismatchError(String(errorRaw(again)?.coreVersion ?? ""));
|
|
23537
|
+
}
|
|
23538
|
+
throw again;
|
|
23539
|
+
}
|
|
23540
|
+
}
|
|
23541
|
+
}
|
|
23542
|
+
members(boardId2) {
|
|
23543
|
+
return this.req("GET", `/boards/${boardId2}/members`);
|
|
23544
|
+
}
|
|
23545
|
+
putMember(boardId2, userId, role) {
|
|
23546
|
+
return this.req("PUT", `/boards/${boardId2}/members/${userId}`, { body: { role } });
|
|
23547
|
+
}
|
|
23548
|
+
removeMember(boardId2, userId) {
|
|
23549
|
+
return this.req("DELETE", `/boards/${boardId2}/members/${userId}`);
|
|
23550
|
+
}
|
|
23551
|
+
/** Só para os testes conferirem o aprendizado. */
|
|
23552
|
+
get currentCoreVersion() {
|
|
23553
|
+
return this.coreVersion;
|
|
23554
|
+
}
|
|
23555
|
+
};
|
|
23556
|
+
var CoreMismatchError = class extends Error {
|
|
23557
|
+
constructor(serverVersion) {
|
|
23558
|
+
super(
|
|
23559
|
+
"O servidor do Canvas recusou a vers\xE3o do canvas-core deste MCP (409 core-mismatch)" + (serverVersion ? ` e segue exigindo ${serverVersion.slice(0, 12)}` : " sem informar a vers\xE3o esperada") + ". Nada foi aplicado. Atualize o beplus-mcp (npx -y beplus-mcp@latest) ou avise o time."
|
|
23560
|
+
);
|
|
23561
|
+
this.serverVersion = serverVersion;
|
|
23562
|
+
this.name = "CoreMismatchError";
|
|
23563
|
+
}
|
|
23564
|
+
serverVersion;
|
|
23565
|
+
};
|
|
23566
|
+
|
|
23567
|
+
// src/canvas/catalog.ts
|
|
23568
|
+
var WORKFLOW_NODE_TYPE = "workflow";
|
|
23569
|
+
var KIND_FIELD = "kind";
|
|
23570
|
+
var KINDS = {
|
|
23571
|
+
text: { title: "Texto", size: { w: 300, h: 160 }, data: { text: "" }, inputs: [], outputs: ["text"], main: "text" },
|
|
23572
|
+
block: { title: "Bloco", size: { w: 260, h: 140 }, data: { text: "" }, inputs: [], outputs: ["text"], main: "text" },
|
|
23573
|
+
prompt: {
|
|
23574
|
+
title: "Prompt",
|
|
23575
|
+
size: { w: 340, h: 200 },
|
|
23576
|
+
data: { template: "" },
|
|
23577
|
+
inputs: ["refs"],
|
|
23578
|
+
outputs: ["text", "refs"],
|
|
23579
|
+
dynamic: "uma entrada de texto por {{var}} do template",
|
|
23580
|
+
main: "template"
|
|
23581
|
+
},
|
|
23582
|
+
llm: {
|
|
23583
|
+
title: "Redator",
|
|
23584
|
+
size: { w: 340, h: 300 },
|
|
23585
|
+
data: { model: "gemini-flash", system: "", instruction: "", outputMode: "text" },
|
|
23586
|
+
inputs: ["instruction", "media"],
|
|
23587
|
+
outputs: ["text"],
|
|
23588
|
+
main: "instruction"
|
|
23589
|
+
},
|
|
23590
|
+
brief: {
|
|
23591
|
+
title: "Diretor",
|
|
23592
|
+
size: { w: 420, h: 360 },
|
|
23593
|
+
data: {
|
|
23594
|
+
model: "gemini-flash",
|
|
23595
|
+
sceneCount: 4,
|
|
23596
|
+
aspect_ratio: "16:9",
|
|
23597
|
+
secondsPerScene: 5,
|
|
23598
|
+
tone: "Natural e pr\xF3ximo, como quem conta para um amigo",
|
|
23599
|
+
language: "pt-BR"
|
|
23600
|
+
},
|
|
23601
|
+
inputs: ["briefing", "refs"],
|
|
23602
|
+
outputs: ["script", "narration", "music", "scenes", "motion"]
|
|
23603
|
+
},
|
|
23604
|
+
narration: { title: "Narra\xE7\xE3o", size: { w: 360, h: 360 }, data: { blocks: [] }, inputs: [], outputs: ["text", "blocks"], main: "blocks" },
|
|
23605
|
+
shotlist: {
|
|
23606
|
+
title: "Roteiro",
|
|
23607
|
+
size: { w: 780, h: 410 },
|
|
23608
|
+
data: { rows: [], orphanedRows: [] },
|
|
23609
|
+
inputs: ["blocks"],
|
|
23610
|
+
outputs: ["shotlist"],
|
|
23611
|
+
dynamic: "uma sa\xEDda shot:<ideaId> por ideia",
|
|
23612
|
+
main: "rows"
|
|
23613
|
+
},
|
|
23614
|
+
context: { title: "Contexto", size: { w: 320, h: 220 }, data: { kitId: null }, inputs: [], outputs: ["context"] },
|
|
23615
|
+
copy: {
|
|
23616
|
+
title: "Textos da pe\xE7a",
|
|
23617
|
+
size: { w: 340, h: 320 },
|
|
23618
|
+
data: { title: "", body: "", cta: "", legal: "" },
|
|
23619
|
+
inputs: [],
|
|
23620
|
+
outputs: ["title", "body", "cta", "legal"],
|
|
23621
|
+
main: "title"
|
|
23622
|
+
},
|
|
23623
|
+
media: { title: "M\xEDdia", size: { w: 320, h: 280 }, data: { items: [] }, inputs: [], outputs: ["image"], dynamic: "sa\xEDda por tipo de item (image, video, audio, file)", main: "items" },
|
|
23624
|
+
image: {
|
|
23625
|
+
title: "Gerador de imagem",
|
|
23626
|
+
size: { w: 320, h: 380 },
|
|
23627
|
+
data: { model: "google/nano-banana-2", aspect_ratio: "1:1", count: 1 },
|
|
23628
|
+
inputs: ["prompt"],
|
|
23629
|
+
outputs: ["image"],
|
|
23630
|
+
dynamic: "refs e negative conforme o modelo"
|
|
23631
|
+
},
|
|
23632
|
+
video: {
|
|
23633
|
+
title: "Gerador de v\xEDdeo",
|
|
23634
|
+
size: { w: 360, h: 320 },
|
|
23635
|
+
data: { model: "bytedance/seedance-2.0-mini", aspect_ratio: "16:9", duration: 5, generateAudio: true },
|
|
23636
|
+
inputs: ["prompt"],
|
|
23637
|
+
outputs: ["video"],
|
|
23638
|
+
dynamic: "frame, refs e negative conforme o modelo"
|
|
23639
|
+
},
|
|
23640
|
+
tts: {
|
|
23641
|
+
title: "Locu\xE7\xE3o",
|
|
23642
|
+
size: { w: 320, h: 212 },
|
|
23643
|
+
data: { model: "gemini-3.1-flash-tts-preview", voice: "Algieba", speed: "normal" },
|
|
23644
|
+
inputs: ["text"],
|
|
23645
|
+
outputs: ["audio", "approved"],
|
|
23646
|
+
dynamic: "style conforme o modelo"
|
|
23647
|
+
},
|
|
23648
|
+
music: {
|
|
23649
|
+
title: "Trilha",
|
|
23650
|
+
size: { w: 320, h: 212 },
|
|
23651
|
+
data: { model: "suno/v5-5", instrumental: true, negativeTags: ["vocals", "choir", "whistling", "marching band", "epic orchestral"] },
|
|
23652
|
+
inputs: ["prompt", "negative"],
|
|
23653
|
+
outputs: ["audio", "approved"],
|
|
23654
|
+
dynamic: "lyrics quando n\xE3o \xE9 instrumental"
|
|
23655
|
+
},
|
|
23656
|
+
review: {
|
|
23657
|
+
title: "Revisor",
|
|
23658
|
+
size: { w: 400, h: 420 },
|
|
23659
|
+
data: {
|
|
23660
|
+
model: "gemini-flash",
|
|
23661
|
+
strictness: "normal",
|
|
23662
|
+
criteria: "",
|
|
23663
|
+
checks: { inventedText: true, inventedLogo: true, productFidelity: true, composition: true, artifacts: true }
|
|
23664
|
+
},
|
|
23665
|
+
inputs: ["items", "prompt", "refs"],
|
|
23666
|
+
outputs: ["reviewed"],
|
|
23667
|
+
main: "criteria"
|
|
23668
|
+
},
|
|
23669
|
+
approval: { title: "Aprova\xE7\xE3o", size: { w: 660, h: 580 }, data: {}, inputs: ["items"], outputs: ["approved"] },
|
|
23670
|
+
"audio-demo": { title: "Demo de \xE1udio", size: { w: 460, h: 500 }, data: { chosenKey: null }, inputs: ["voice", "music"], outputs: ["voice", "music"] },
|
|
23671
|
+
compose: {
|
|
23672
|
+
title: "Composi\xE7\xE3o",
|
|
23673
|
+
size: { w: 440, h: 320 },
|
|
23674
|
+
data: { formatIds: [], imageType: "jpg", jpgQuality: 0.92 },
|
|
23675
|
+
inputs: ["background", "title", "body", "cta", "legal", "logo"],
|
|
23676
|
+
outputs: ["pieces"]
|
|
23677
|
+
},
|
|
23678
|
+
montage: {
|
|
23679
|
+
title: "Montagem",
|
|
23680
|
+
size: { w: 460, h: 560 },
|
|
23681
|
+
data: {
|
|
23682
|
+
order: "chegada",
|
|
23683
|
+
musicGainDb: -14,
|
|
23684
|
+
duckingDb: -8,
|
|
23685
|
+
fadeMs: 1500,
|
|
23686
|
+
title: "",
|
|
23687
|
+
renderFilm: true,
|
|
23688
|
+
filmFps: 30,
|
|
23689
|
+
clipAudioGainDb: -12
|
|
23690
|
+
},
|
|
23691
|
+
inputs: ["clips", "voice", "music"],
|
|
23692
|
+
outputs: ["film", "cut", "clips", "voice", "music"],
|
|
23693
|
+
main: "title"
|
|
23694
|
+
},
|
|
23695
|
+
export: {
|
|
23696
|
+
title: "Exporta\xE7\xE3o",
|
|
23697
|
+
size: { w: 380, h: 420 },
|
|
23698
|
+
data: { client: "", campaign: "", version: 1, imageType: "png", jpgQuality: 90, manifest: true, flat: false },
|
|
23699
|
+
inputs: ["items"],
|
|
23700
|
+
outputs: ["package"]
|
|
23701
|
+
},
|
|
23702
|
+
note: { title: "Nota", size: { w: 220, h: 220 }, data: { text: "", color: "yellow", fontSize: "auto" }, inputs: [], outputs: [], annotation: true, main: "text" },
|
|
23703
|
+
label: {
|
|
23704
|
+
title: "Texto livre",
|
|
23705
|
+
size: { w: 320, h: 56 },
|
|
23706
|
+
data: { text: "", size: "titulo", weight: "bold", color: "neutral", align: "left" },
|
|
23707
|
+
inputs: [],
|
|
23708
|
+
outputs: [],
|
|
23709
|
+
annotation: true,
|
|
23710
|
+
main: "text"
|
|
23711
|
+
},
|
|
23712
|
+
section: { title: "Se\xE7\xE3o", size: { w: 880, h: 560 }, data: { title: "Se\xE7\xE3o", color: "violet" }, inputs: [], outputs: [], annotation: true, main: "title" },
|
|
23713
|
+
shape: {
|
|
23714
|
+
title: "Forma",
|
|
23715
|
+
size: { w: 200, h: 140 },
|
|
23716
|
+
data: { shape: "rounded", fill: "blue", stroke: "blue", strokeWidth: 2, text: "" },
|
|
23717
|
+
inputs: [],
|
|
23718
|
+
outputs: [],
|
|
23719
|
+
annotation: true,
|
|
23720
|
+
main: "text"
|
|
23721
|
+
}
|
|
23722
|
+
};
|
|
23723
|
+
function kindList() {
|
|
23724
|
+
return Object.entries(KINDS).map(([k, s]) => `${k} (${s.title})`).join(", ");
|
|
23725
|
+
}
|
|
23726
|
+
function knownPort(kind, side, port) {
|
|
23727
|
+
const spec = kind ? KINDS[kind] : void 0;
|
|
23728
|
+
if (!spec) return void 0;
|
|
23729
|
+
const list = side === "in" ? spec.inputs : spec.outputs;
|
|
23730
|
+
if (list.includes(port)) return true;
|
|
23731
|
+
if (spec.dynamic) {
|
|
23732
|
+
return void 0;
|
|
23733
|
+
}
|
|
23734
|
+
return false;
|
|
23735
|
+
}
|
|
23736
|
+
|
|
23737
|
+
// src/canvas/core.ts
|
|
23738
|
+
import { randomBytes, randomUUID } from "crypto";
|
|
23739
|
+
var SCHEMA_VERSION = 1;
|
|
23740
|
+
var MAX_OP_BYTES = 64 * 1024;
|
|
23741
|
+
var MAX_DATA_URL_BYTES = 8 * 1024;
|
|
23742
|
+
var CLIENT_ID = `mcp-${randomBytes(6).toString("hex")}`;
|
|
23743
|
+
var NODE_KEYS = ["id", "type", "position", "size", "zIndex", "parentId", "data", "createdBy", "createdAt", "updatedAt"];
|
|
23744
|
+
var EDGE_KEYS = ["id", "source", "sourcePort", "target", "targetPort", "data", "createdBy", "createdAt"];
|
|
23745
|
+
var FORBIDDEN_SEGMENTS = ["__proto__", "constructor", "prototype"];
|
|
23746
|
+
var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
23747
|
+
var isFiniteNumber = (v) => typeof v === "number" && Number.isFinite(v);
|
|
23748
|
+
var isStringOrNull = (v) => v === null || typeof v === "string";
|
|
23749
|
+
var hasOnlyKeys = (r, keys) => Object.keys(r).every((k) => keys.includes(k));
|
|
23750
|
+
function isJson(value) {
|
|
23751
|
+
if (value === null) return true;
|
|
23752
|
+
const kind = typeof value;
|
|
23753
|
+
if (kind === "boolean" || kind === "string") return true;
|
|
23754
|
+
if (kind === "number") return Number.isFinite(value);
|
|
23755
|
+
if (Array.isArray(value)) return value.every(isJson);
|
|
23756
|
+
if (isRecord(value)) return Object.values(value).every(isJson);
|
|
23757
|
+
return false;
|
|
23758
|
+
}
|
|
23759
|
+
var isJsonBag = (v) => isRecord(v) && Object.values(v).every(isJson);
|
|
23760
|
+
var isPoint = (v) => isRecord(v) && hasOnlyKeys(v, ["x", "y"]) && isFiniteNumber(v.x) && isFiniteNumber(v.y);
|
|
23761
|
+
var isSize = (v) => isRecord(v) && hasOnlyKeys(v, ["w", "h"]) && isFiniteNumber(v.w) && isFiniteNumber(v.h);
|
|
23762
|
+
function isValidNode(key, v) {
|
|
23763
|
+
return isRecord(v) && hasOnlyKeys(v, NODE_KEYS) && v.id === key && typeof v.type === "string" && isPoint(v.position) && isSize(v.size) && isFiniteNumber(v.zIndex) && isStringOrNull(v.parentId) && isJsonBag(v.data) && isStringOrNull(v.createdBy) && isFiniteNumber(v.createdAt) && isFiniteNumber(v.updatedAt);
|
|
23764
|
+
}
|
|
23765
|
+
function isValidEdge(key, v) {
|
|
23766
|
+
return isRecord(v) && hasOnlyKeys(v, EDGE_KEYS) && v.id === key && typeof v.source === "string" && typeof v.sourcePort === "string" && typeof v.target === "string" && typeof v.targetPort === "string" && isJsonBag(v.data) && isStringOrNull(v.createdBy) && isFiniteNumber(v.createdAt);
|
|
23767
|
+
}
|
|
23768
|
+
function checkNodePath(path) {
|
|
23769
|
+
const segments = path.split(".");
|
|
23770
|
+
if (segments.some((s) => s.length === 0)) return `caminho inv\xE1lido "${path}"`;
|
|
23771
|
+
if (segments.some((s) => FORBIDDEN_SEGMENTS.includes(s))) return `segmento proibido em "${path}"`;
|
|
23772
|
+
const [root, ...rest] = segments;
|
|
23773
|
+
if (root === "data" && rest.length >= 1) return null;
|
|
23774
|
+
if (root === "position" && rest.length === 1 && ["x", "y"].includes(rest[0]) || root === "size" && rest.length === 1 && ["w", "h"].includes(rest[0])) return null;
|
|
23775
|
+
return `s\xF3 d\xE1 para editar data.*, position.x/y e size.w/h (recebido "${path}")`;
|
|
23776
|
+
}
|
|
23777
|
+
function hasLargeDataUrl(value) {
|
|
23778
|
+
const stack = [value];
|
|
23779
|
+
while (stack.length) {
|
|
23780
|
+
const cur = stack.pop();
|
|
23781
|
+
if (typeof cur === "string") {
|
|
23782
|
+
if (cur.length > MAX_DATA_URL_BYTES && cur.startsWith("data:")) return true;
|
|
23783
|
+
} else if (Array.isArray(cur)) stack.push(...cur);
|
|
23784
|
+
else if (isRecord(cur)) stack.push(...Object.values(cur));
|
|
23785
|
+
}
|
|
23786
|
+
return false;
|
|
23787
|
+
}
|
|
23788
|
+
function checkOp(op) {
|
|
23789
|
+
switch (op.type) {
|
|
23790
|
+
case "node.add":
|
|
23791
|
+
if (op.node.id !== op.nodeId || !isValidNode(op.nodeId, op.node)) return `n\xF3 ${op.nodeId} fora do formato do Canvas`;
|
|
23792
|
+
return null;
|
|
23793
|
+
case "edge.add":
|
|
23794
|
+
if (op.edge.id !== op.edgeId || !isValidEdge(op.edgeId, op.edge)) return `liga\xE7\xE3o ${op.edgeId} fora do formato do Canvas`;
|
|
23795
|
+
return null;
|
|
23796
|
+
case "node.set": {
|
|
23797
|
+
const bad = checkNodePath(op.path);
|
|
23798
|
+
if (bad) return bad;
|
|
23799
|
+
if (op.value.kind === "value") {
|
|
23800
|
+
if (!isJson(op.value.value)) return `valor de ${op.path} n\xE3o \xE9 JSON`;
|
|
23801
|
+
if (!op.path.startsWith("data.") && !isFiniteNumber(op.value.value)) return `${op.path} precisa ser n\xFAmero`;
|
|
23802
|
+
} else if (!op.path.startsWith("data.")) return `${op.path} n\xE3o pode ser apagado`;
|
|
23803
|
+
return null;
|
|
23804
|
+
}
|
|
23805
|
+
case "board.set":
|
|
23806
|
+
if (op.field === "title" && (op.value.kind !== "value" || typeof op.value.value !== "string" || !op.value.value.trim()))
|
|
23807
|
+
return "t\xEDtulo vazio";
|
|
23808
|
+
return null;
|
|
23809
|
+
case "batch":
|
|
23810
|
+
for (const inner of op.ops) {
|
|
23811
|
+
const bad = checkOp(inner);
|
|
23812
|
+
if (bad) return bad;
|
|
23813
|
+
}
|
|
23814
|
+
return null;
|
|
23815
|
+
default:
|
|
23816
|
+
return null;
|
|
23817
|
+
}
|
|
23818
|
+
}
|
|
23819
|
+
function envelope(boardId2, op, actorId) {
|
|
23820
|
+
return { id: randomUUID(), boardId: boardId2, clientId: CLIENT_ID, actorId, seq: null, ts: Date.now(), op };
|
|
23821
|
+
}
|
|
23822
|
+
function checkEnvelope(env) {
|
|
23823
|
+
const bad = checkOp(env.op);
|
|
23824
|
+
if (bad) return bad;
|
|
23825
|
+
if (Buffer.byteLength(JSON.stringify(env)) > MAX_OP_BYTES)
|
|
23826
|
+
return "opera\xE7\xE3o maior que 64 KB: divida em partes menores";
|
|
23827
|
+
if (hasLargeDataUrl(env.op)) return "m\xEDdia embutida (data:) no board: suba o arquivo e use a URL";
|
|
23828
|
+
return null;
|
|
23829
|
+
}
|
|
23830
|
+
function emptyBoard(title, now = Date.now()) {
|
|
23831
|
+
return {
|
|
23832
|
+
id: randomUUID(),
|
|
23833
|
+
schemaVersion: SCHEMA_VERSION,
|
|
23834
|
+
title,
|
|
23835
|
+
ownerId: "",
|
|
23836
|
+
folderId: null,
|
|
23837
|
+
version: 0,
|
|
23838
|
+
nodes: {},
|
|
23839
|
+
edges: {},
|
|
23840
|
+
results: {},
|
|
23841
|
+
nodeHistory: {},
|
|
23842
|
+
createdAt: now,
|
|
23843
|
+
updatedAt: now
|
|
23844
|
+
};
|
|
23845
|
+
}
|
|
23846
|
+
function makeNode(init) {
|
|
23847
|
+
const now = init.now ?? Date.now();
|
|
23848
|
+
return {
|
|
23849
|
+
id: init.id ?? randomUUID(),
|
|
23850
|
+
type: init.type,
|
|
23851
|
+
position: { x: init.position.x, y: init.position.y },
|
|
23852
|
+
size: { w: init.size.w, h: init.size.h },
|
|
23853
|
+
zIndex: init.zIndex,
|
|
23854
|
+
parentId: null,
|
|
23855
|
+
data: init.data,
|
|
23856
|
+
createdBy: init.createdBy,
|
|
23857
|
+
createdAt: now,
|
|
23858
|
+
updatedAt: now
|
|
23859
|
+
};
|
|
23860
|
+
}
|
|
23861
|
+
function makeEdge(init) {
|
|
23862
|
+
return {
|
|
23863
|
+
id: randomUUID(),
|
|
23864
|
+
source: init.source,
|
|
23865
|
+
sourcePort: init.sourcePort,
|
|
23866
|
+
target: init.target,
|
|
23867
|
+
targetPort: init.targetPort,
|
|
23868
|
+
data: {},
|
|
23869
|
+
createdBy: init.createdBy,
|
|
23870
|
+
createdAt: init.now ?? Date.now()
|
|
23871
|
+
};
|
|
23872
|
+
}
|
|
23873
|
+
var setValue = (value) => ({ kind: "value", value });
|
|
23874
|
+
var unsetValue = () => ({ kind: "unset" });
|
|
23875
|
+
function removeNodeOps(doc, nodeId) {
|
|
23876
|
+
const ops = [];
|
|
23877
|
+
const edgeIds = Object.keys(doc.edges).filter((id) => doc.edges[id].source === nodeId || doc.edges[id].target === nodeId).sort();
|
|
23878
|
+
for (const edgeId of edgeIds) ops.push({ type: "edge.remove", edgeId });
|
|
23879
|
+
const history = Object.hasOwn(doc.nodeHistory, nodeId) ? doc.nodeHistory[nodeId] : void 0;
|
|
23880
|
+
if (history) for (const resultId of [...history].reverse()) ops.push({ type: "history.remove", nodeId, resultId });
|
|
23881
|
+
ops.push({ type: "node.remove", nodeId });
|
|
23882
|
+
return ops;
|
|
23883
|
+
}
|
|
23884
|
+
function batchOf(ops) {
|
|
23885
|
+
return ops.length === 1 ? ops[0] : { type: "batch", ops };
|
|
23886
|
+
}
|
|
23887
|
+
|
|
23888
|
+
// src/canvas/summary.ts
|
|
23889
|
+
var MAX_TEXT = 160;
|
|
23890
|
+
var MAX_BLOCKS = 40;
|
|
23891
|
+
var HEAVY_KEYS = /* @__PURE__ */ new Set([
|
|
23892
|
+
"history",
|
|
23893
|
+
"pinned",
|
|
23894
|
+
"main",
|
|
23895
|
+
"lastFrame",
|
|
23896
|
+
"sequence",
|
|
23897
|
+
"points",
|
|
23898
|
+
"extra",
|
|
23899
|
+
"mentions",
|
|
23900
|
+
"mediaMentions",
|
|
23901
|
+
"refs",
|
|
23902
|
+
"fields",
|
|
23903
|
+
"orphanedRows",
|
|
23904
|
+
"holds",
|
|
23905
|
+
"appearance",
|
|
23906
|
+
"settings",
|
|
23907
|
+
"enabledKeys",
|
|
23908
|
+
"checks",
|
|
23909
|
+
"versions"
|
|
23910
|
+
]);
|
|
23911
|
+
var NAME_KEYS = /* @__PURE__ */ new Set([KIND_FIELD, "label", "nodeTitle"]);
|
|
23912
|
+
function oneLine(text, max = MAX_TEXT) {
|
|
23913
|
+
const flat = text.replace(/\s+/g, " ").trim();
|
|
23914
|
+
return flat.length > max ? `${flat.slice(0, max - 1)}\u2026` : flat;
|
|
23915
|
+
}
|
|
23916
|
+
function nodeKind(node) {
|
|
23917
|
+
const k = node.data[KIND_FIELD];
|
|
23918
|
+
return typeof k === "string" && k ? k : null;
|
|
23919
|
+
}
|
|
23920
|
+
function nodeName(node) {
|
|
23921
|
+
const { label, nodeTitle } = node.data;
|
|
23922
|
+
if (typeof label === "string" && label.trim()) return label.trim();
|
|
23923
|
+
if (typeof nodeTitle === "string" && nodeTitle.trim()) return nodeTitle.trim();
|
|
23924
|
+
const kind = nodeKind(node);
|
|
23925
|
+
return kind && KINDS[kind]?.title || kind || node.type;
|
|
23926
|
+
}
|
|
23927
|
+
function scalar(v) {
|
|
23928
|
+
if (typeof v === "string") {
|
|
23929
|
+
if (v.startsWith("data:")) return "[data-url]";
|
|
23930
|
+
if (/^https?:\/\//.test(v)) return "[url]";
|
|
23931
|
+
return JSON.stringify(oneLine(v, 60));
|
|
23932
|
+
}
|
|
23933
|
+
if (typeof v === "number" || typeof v === "boolean" || v === null) return String(v);
|
|
23934
|
+
return null;
|
|
23935
|
+
}
|
|
23936
|
+
function blocksLines(blocks) {
|
|
23937
|
+
if (!Array.isArray(blocks)) return [];
|
|
23938
|
+
const out = blocks.slice(0, MAX_BLOCKS).map((b) => {
|
|
23939
|
+
const rec = b ?? {};
|
|
23940
|
+
return ` \xB7 ${String(rec.id ?? "?")}: ${oneLine(String(rec.text ?? ""), 120)}`;
|
|
23941
|
+
});
|
|
23942
|
+
if (blocks.length > MAX_BLOCKS) out.push(` \xB7 \u2026 +${blocks.length - MAX_BLOCKS} blocos`);
|
|
23943
|
+
return out;
|
|
23944
|
+
}
|
|
23945
|
+
function rowsLines(rows) {
|
|
23946
|
+
if (!Array.isArray(rows)) return [];
|
|
23947
|
+
const out = [];
|
|
23948
|
+
for (const r of rows.slice(0, MAX_BLOCKS)) {
|
|
23949
|
+
const rec = r ?? {};
|
|
23950
|
+
const ideas = Array.isArray(rec.ideas) ? rec.ideas : [];
|
|
23951
|
+
const ideaText = ideas.slice(0, 6).map((i) => {
|
|
23952
|
+
const idea = i ?? {};
|
|
23953
|
+
return `${String(idea.id ?? "?")}="${oneLine(String(idea.text ?? ""), 60)}"`;
|
|
23954
|
+
}).join("; ");
|
|
23955
|
+
out.push(` \xB7 linha ${String(rec.id ?? "?")} (bloco ${String(rec.narrationBlockId ?? "?")}): ${ideas.length} ideia(s) ${ideaText}`);
|
|
23956
|
+
}
|
|
23957
|
+
if (rows.length > MAX_BLOCKS) out.push(` \xB7 \u2026 +${rows.length - MAX_BLOCKS} linhas`);
|
|
23958
|
+
return out;
|
|
23959
|
+
}
|
|
23960
|
+
function nodeLines(node, run) {
|
|
23961
|
+
const kind = nodeKind(node);
|
|
23962
|
+
const spec = kind ? KINDS[kind] : void 0;
|
|
23963
|
+
const head = `- ${node.id} \xB7 ${kind ?? node.type} \xB7 "${oneLine(nodeName(node), 60)}" @(${Math.round(node.position.x)},${Math.round(node.position.y)})`;
|
|
23964
|
+
const facts = [];
|
|
23965
|
+
const extra = [];
|
|
23966
|
+
const d = node.data;
|
|
23967
|
+
if (d.archived) facts.push("ARQUIVADO");
|
|
23968
|
+
const approval = d.approval?.state;
|
|
23969
|
+
if (typeof approval === "string") facts.push(`aprova\xE7\xE3o: ${approval}`);
|
|
23970
|
+
if (Array.isArray(d.history)) facts.push(`${d.history.length} gera\xE7\xE3o(\xF5es)`);
|
|
23971
|
+
if (Array.isArray(d.items)) {
|
|
23972
|
+
const names = d.items.slice(0, 5).map((i) => String(i?.name ?? "?"));
|
|
23973
|
+
facts.push(`${d.items.length} arquivo(s)${names.length ? `: ${names.join(", ")}` : ""}`);
|
|
23974
|
+
}
|
|
23975
|
+
if (Array.isArray(d.blocks)) {
|
|
23976
|
+
facts.push(`${d.blocks.length} bloco(s)`);
|
|
23977
|
+
extra.push(...blocksLines(d.blocks));
|
|
23978
|
+
}
|
|
23979
|
+
if (Array.isArray(d.rows)) {
|
|
23980
|
+
facts.push(`${d.rows.length} linha(s)`);
|
|
23981
|
+
extra.push(...rowsLines(d.rows));
|
|
23982
|
+
}
|
|
23983
|
+
const main2 = spec?.main;
|
|
23984
|
+
if (main2 && typeof d[main2] === "string" && d[main2]) facts.push(`${main2}: "${oneLine(d[main2])}"`);
|
|
23985
|
+
for (const [key, value] of Object.entries(d)) {
|
|
23986
|
+
if (NAME_KEYS.has(key) || HEAVY_KEYS.has(key) || key === main2) continue;
|
|
23987
|
+
if (["blocks", "rows", "items", "archived", "approval"].includes(key)) continue;
|
|
23988
|
+
const s = scalar(value);
|
|
23989
|
+
if (s !== null) facts.push(`${key}=${s}`);
|
|
23990
|
+
else if (Array.isArray(value)) facts.push(`${key}=[${value.length}]`);
|
|
23991
|
+
else if (value && typeof value === "object") facts.push(`${key}={\u2026}`);
|
|
23992
|
+
if (facts.length > 12) break;
|
|
23993
|
+
}
|
|
23994
|
+
if (run?.claimedBy) facts.push("rodando agora");
|
|
23995
|
+
return [facts.length ? `${head} \xB7 ${facts.join(" \xB7 ")}` : head, ...extra];
|
|
23996
|
+
}
|
|
23997
|
+
function boardSummary(snap) {
|
|
23998
|
+
const doc = snap.doc;
|
|
23999
|
+
const nodes = Object.values(doc.nodes).sort((a, b) => a.position.x - b.position.x || a.position.y - b.position.y);
|
|
24000
|
+
const edges = Object.values(doc.edges);
|
|
24001
|
+
const runs = new Map(snap.runs.map((r) => [r.nodeId, r]));
|
|
24002
|
+
const lines = [
|
|
24003
|
+
`Board "${snap.title}" \xB7 id ${snap.id} \xB7 seq ${snap.seq} \xB7 seu papel: ${snap.role} \xB7 ${nodes.length} n\xF3(s) \xB7 ${edges.length} liga\xE7\xE3o(\xF5es)`
|
|
24004
|
+
];
|
|
24005
|
+
const ialab = doc.meta?.ialab;
|
|
24006
|
+
if (ialab === null) lines.push("IA Lab: sem v\xEDnculo (gera\xE7\xF5es saem dos diamantes de quem roda).");
|
|
24007
|
+
else if (ialab && typeof ialab === "object") {
|
|
24008
|
+
const label = ["code", "name", "projectCode", "projectName"].map((k) => ialab[k]).find((v) => typeof v === "string");
|
|
24009
|
+
lines.push(`IA Lab: vinculado${label ? ` a ${String(label)}` : ""}.`);
|
|
24010
|
+
}
|
|
24011
|
+
if (doc.meta?.guide) lines.push(`Guia: ${oneLine(doc.meta.guide)}`);
|
|
24012
|
+
lines.push("", "N\xF3s:");
|
|
24013
|
+
if (!nodes.length) lines.push("(vazio)");
|
|
24014
|
+
for (const n of nodes) lines.push(...nodeLines(n, runs.get(n.id)));
|
|
24015
|
+
lines.push("", "Liga\xE7\xF5es:");
|
|
24016
|
+
if (!edges.length) lines.push("(nenhuma)");
|
|
24017
|
+
for (const e of edges) lines.push(`- ${e.id}: ${e.source}.${e.sourcePort} \u2192 ${e.target}.${e.targetPort}`);
|
|
24018
|
+
return lines.join("\n");
|
|
24019
|
+
}
|
|
24020
|
+
function nodeDetail(node) {
|
|
24021
|
+
const strip = (v, depth) => {
|
|
24022
|
+
if (typeof v === "string") {
|
|
24023
|
+
if (v.startsWith("data:")) return `[data-url ${v.length}b]`;
|
|
24024
|
+
return v.length > 2e3 ? `${v.slice(0, 2e3)}\u2026 [+${v.length - 2e3}]` : v;
|
|
24025
|
+
}
|
|
24026
|
+
if (Array.isArray(v)) {
|
|
24027
|
+
if (depth > 4) return `[${v.length} itens]`;
|
|
24028
|
+
const head = v.slice(0, 60).map((x) => strip(x, depth + 1));
|
|
24029
|
+
return v.length > 60 ? [...head, `\u2026 +${v.length - 60}`] : head;
|
|
24030
|
+
}
|
|
24031
|
+
if (v && typeof v === "object") {
|
|
24032
|
+
if (depth > 4) return "{\u2026}";
|
|
24033
|
+
const out = {};
|
|
24034
|
+
for (const [k, x] of Object.entries(v)) {
|
|
24035
|
+
out[k] = k === "points" || k === "sequence" ? "[omitido]" : strip(x, depth + 1);
|
|
24036
|
+
}
|
|
24037
|
+
return out;
|
|
24038
|
+
}
|
|
24039
|
+
return v;
|
|
24040
|
+
};
|
|
24041
|
+
return `${node.id} \xB7 ${nodeKind(node) ?? node.type} \xB7 "${nodeName(node)}"
|
|
24042
|
+
${JSON.stringify(strip(node.data, 0))}`;
|
|
24043
|
+
}
|
|
24044
|
+
function changesSince(envelopes, doc) {
|
|
24045
|
+
const count = /* @__PURE__ */ new Map();
|
|
24046
|
+
for (const e of envelopes) {
|
|
24047
|
+
const key = e.op.type;
|
|
24048
|
+
count.set(key, (count.get(key) ?? 0) + 1);
|
|
24049
|
+
}
|
|
24050
|
+
const parts = [...count].map(([t, n]) => `${n}\xD7 ${t}`);
|
|
24051
|
+
const names = [...touchedNodes(envelopes)].map((id) => doc.nodes[id] ? `"${oneLine(nodeName(doc.nodes[id]), 40)}"` : `${id} (apagado)`).slice(0, 8);
|
|
24052
|
+
return `${envelopes.length} opera\xE7\xE3o(\xF5es) de outras pessoas desde a sua leitura (${parts.join(", ")})` + (names.length ? `; n\xF3s tocados: ${names.join(", ")}` : "");
|
|
24053
|
+
}
|
|
24054
|
+
function touchedNodes(envelopes) {
|
|
24055
|
+
const out = /* @__PURE__ */ new Set();
|
|
24056
|
+
const visit = (op) => {
|
|
24057
|
+
if (op.type === "batch" && Array.isArray(op.ops)) {
|
|
24058
|
+
for (const inner of op.ops) visit(inner);
|
|
24059
|
+
return;
|
|
24060
|
+
}
|
|
24061
|
+
if (typeof op.nodeId === "string") out.add(op.nodeId);
|
|
24062
|
+
const edge = op.edge;
|
|
24063
|
+
if (edge && typeof edge.source === "string") out.add(edge.source);
|
|
24064
|
+
if (edge && typeof edge.target === "string") out.add(edge.target);
|
|
24065
|
+
};
|
|
24066
|
+
for (const e of envelopes) visit(e.op);
|
|
24067
|
+
return out;
|
|
24068
|
+
}
|
|
24069
|
+
|
|
24070
|
+
// src/tools/canvas.ts
|
|
24071
|
+
var REJECT_TEXT = {
|
|
24072
|
+
"node-missing": "o n\xF3 n\xE3o existe mais (algu\xE9m apagou)",
|
|
24073
|
+
"edge-endpoint-missing": "uma ponta da liga\xE7\xE3o n\xE3o existe mais",
|
|
24074
|
+
"invalid-op": "opera\xE7\xE3o inv\xE1lida ou repetida",
|
|
24075
|
+
"too-large": "opera\xE7\xE3o grande demais (64 KB) ou com m\xEDdia embutida"
|
|
24076
|
+
};
|
|
24077
|
+
var SECTION_Z = -1e3;
|
|
24078
|
+
var GAP = 80;
|
|
24079
|
+
var CanvasToolError = class extends Error {
|
|
24080
|
+
};
|
|
24081
|
+
var boardId = external_exports.string().uuid().describe("Id (uuid) do board, como sai em canvas_list_boards.");
|
|
24082
|
+
var nodeIdArg = external_exports.string().min(1).max(128);
|
|
24083
|
+
var point = external_exports.object({ x: external_exports.number().finite(), y: external_exports.number().finite() });
|
|
24084
|
+
function registerCanvasTools(server, client, cfg) {
|
|
24085
|
+
const api = new CanvasApi(client, cfg);
|
|
24086
|
+
const seen = /* @__PURE__ */ new Map();
|
|
24087
|
+
function fail(e) {
|
|
24088
|
+
if (e instanceof CanvasToolError || e instanceof CoreMismatchError) return errorResult(e.message);
|
|
24089
|
+
if (e instanceof ApiError && e.httpStatus === 401 && !api.usesPat) {
|
|
24090
|
+
return errorResult(
|
|
24091
|
+
"O acesso pelo navegador expirou ou foi revogado. Rode `beplus-mcp login` de novo e autorize."
|
|
24092
|
+
);
|
|
24093
|
+
}
|
|
24094
|
+
if (e instanceof ApiError && e.httpStatus === 404) {
|
|
24095
|
+
return errorResult("Board n\xE3o encontrado, ou voc\xEA n\xE3o \xE9 membro dele. Confira o id em canvas_list_boards.");
|
|
24096
|
+
}
|
|
24097
|
+
return errorResult(apiErrorToUserMessage(e));
|
|
24098
|
+
}
|
|
24099
|
+
async function run(fn) {
|
|
24100
|
+
try {
|
|
24101
|
+
return textResult(await fn());
|
|
24102
|
+
} catch (e) {
|
|
24103
|
+
return fail(e);
|
|
24104
|
+
}
|
|
24105
|
+
}
|
|
24106
|
+
async function edit(id, build) {
|
|
24107
|
+
const last = seen.get(id);
|
|
24108
|
+
if (last === void 0) {
|
|
24109
|
+
throw new CanvasToolError("Leia o board com canvas_read_board antes de editar (a leitura vale para a sess\xE3o).");
|
|
24110
|
+
}
|
|
24111
|
+
const snap = await api.snapshot(id);
|
|
24112
|
+
if (snap.role === "viewer") throw new CanvasToolError("Voc\xEA \xE9 s\xF3 leitor neste board: pe\xE7a ao dono para te dar papel de editor.");
|
|
24113
|
+
const doc = snap.doc;
|
|
24114
|
+
const { ops, done, targets } = await build(doc, snap);
|
|
24115
|
+
if (!ops.length) return `Nada a mudar. ${done}`;
|
|
24116
|
+
let note = "";
|
|
24117
|
+
if (snap.seq > last) {
|
|
24118
|
+
const since = await api.opsAfter(id, last);
|
|
24119
|
+
const theirs = since?.filter((e) => e.clientId !== CLIENT_ID) ?? null;
|
|
24120
|
+
if (theirs === null) {
|
|
24121
|
+
seen.delete(id);
|
|
24122
|
+
throw new CanvasToolError(
|
|
24123
|
+
`O board mudou muito desde a sua leitura (seq ${last} \u2192 ${snap.seq}). Releia com canvas_read_board antes de editar.`
|
|
24124
|
+
);
|
|
24125
|
+
}
|
|
24126
|
+
const clash = [...touchedNodes(theirs)].filter((n) => targets.includes(n));
|
|
24127
|
+
if (clash.length) {
|
|
24128
|
+
seen.delete(id);
|
|
24129
|
+
const names = clash.map((n) => doc.nodes[n] ? `"${oneLine(nodeName(doc.nodes[n]), 40)}"` : n).join(", ");
|
|
24130
|
+
throw new CanvasToolError(
|
|
24131
|
+
`Outra pessoa mexeu em ${names} depois da sua leitura (seq ${last} \u2192 ${snap.seq}). Nada foi aplicado: releia com canvas_read_board e refa\xE7a a edi\xE7\xE3o em cima do estado atual.`
|
|
24132
|
+
);
|
|
24133
|
+
}
|
|
24134
|
+
if (theirs.length) note = `
|
|
24135
|
+
Aviso: ${changesSince(theirs, doc)}.`;
|
|
24136
|
+
}
|
|
24137
|
+
const env = envelope(id, batchOf(ops), await api.me());
|
|
24138
|
+
const bad = checkEnvelope(env);
|
|
24139
|
+
if (bad) throw new CanvasToolError(`Edi\xE7\xE3o recusada antes de enviar: ${bad}.`);
|
|
24140
|
+
const res = await api.commit(id, [env]);
|
|
24141
|
+
const outcome = res.outcomes.find((o) => o.opId === env.id);
|
|
24142
|
+
if (!outcome || "rejected" in outcome) {
|
|
24143
|
+
seen.delete(id);
|
|
24144
|
+
const why = outcome && "rejected" in outcome ? REJECT_TEXT[outcome.rejected] ?? outcome.rejected : "sem resposta do servidor";
|
|
24145
|
+
throw new CanvasToolError(`O servidor recusou a edi\xE7\xE3o: ${why}. Nada foi aplicado. Releia com canvas_read_board.`);
|
|
24146
|
+
}
|
|
24147
|
+
seen.set(id, res.seq);
|
|
24148
|
+
return `\u2705 ${done} (seq ${res.seq}).${note}`;
|
|
24149
|
+
}
|
|
24150
|
+
function needNode(doc, nodeId) {
|
|
24151
|
+
const node = doc.nodes[nodeId];
|
|
24152
|
+
if (!node) throw new CanvasToolError(`N\xF3 ${nodeId} n\xE3o existe no board. Releia com canvas_read_board.`);
|
|
24153
|
+
return node;
|
|
24154
|
+
}
|
|
24155
|
+
function portWarning(doc, source, sourcePort, target, targetPort) {
|
|
24156
|
+
const warn = [];
|
|
24157
|
+
const s = knownPort(nodeKind(doc.nodes[source]), "out", sourcePort);
|
|
24158
|
+
const t = knownPort(nodeKind(doc.nodes[target]), "in", targetPort);
|
|
24159
|
+
if (s === false) warn.push(`sa\xEDda "${sourcePort}" n\xE3o \xE9 porta conhecida de ${nodeKind(doc.nodes[source])}`);
|
|
24160
|
+
if (t === false) warn.push(`entrada "${targetPort}" n\xE3o \xE9 porta conhecida de ${nodeKind(doc.nodes[target])}`);
|
|
24161
|
+
return warn.length ? ` Aten\xE7\xE3o: ${warn.join("; ")}.` : "";
|
|
24162
|
+
}
|
|
24163
|
+
function edgeOps(doc, links, createdBy, pending) {
|
|
24164
|
+
const ops = [];
|
|
24165
|
+
let warn = "";
|
|
24166
|
+
for (const l of links) {
|
|
24167
|
+
for (const end of [l.source, l.target]) {
|
|
24168
|
+
if (!doc.nodes[end] && !pending.has(end)) throw new CanvasToolError(`N\xF3 ${end} n\xE3o existe no board. Releia com canvas_read_board.`);
|
|
24169
|
+
}
|
|
24170
|
+
const dup = Object.values(doc.edges).some(
|
|
24171
|
+
(e) => e.source === l.source && e.sourcePort === l.sourcePort && e.target === l.target && e.targetPort === l.targetPort
|
|
24172
|
+
);
|
|
24173
|
+
if (dup) continue;
|
|
24174
|
+
if (doc.nodes[l.source] && doc.nodes[l.target]) warn += portWarning(doc, l.source, l.sourcePort, l.target, l.targetPort);
|
|
24175
|
+
const edge = makeEdge({ ...l, createdBy });
|
|
24176
|
+
ops.push({ type: "edge.add", edgeId: edge.id, edge });
|
|
24177
|
+
}
|
|
24178
|
+
return { ops, warn };
|
|
24179
|
+
}
|
|
24180
|
+
function placeFor(doc, size, near, side = "right") {
|
|
24181
|
+
if (near) {
|
|
24182
|
+
const x = side === "right" ? near.position.x + near.size.w + GAP : near.position.x - size.w - GAP;
|
|
24183
|
+
let y = near.position.y;
|
|
24184
|
+
const nodes2 = Object.values(doc.nodes);
|
|
24185
|
+
const hits = (yy) => nodes2.some((n) => x < n.position.x + n.size.w && x + size.w > n.position.x && yy < n.position.y + n.size.h && yy + size.h > n.position.y);
|
|
24186
|
+
for (let i = 0; i < 50 && hits(y); i++) y += GAP;
|
|
24187
|
+
return { x, y };
|
|
24188
|
+
}
|
|
24189
|
+
const nodes = Object.values(doc.nodes).filter((n) => nodeKind(n) !== "section");
|
|
24190
|
+
if (!nodes.length) return { x: 0, y: 0 };
|
|
24191
|
+
return {
|
|
24192
|
+
x: Math.max(...nodes.map((n) => n.position.x + n.size.w)) + GAP,
|
|
24193
|
+
y: Math.min(...nodes.map((n) => n.position.y))
|
|
24194
|
+
};
|
|
24195
|
+
}
|
|
24196
|
+
function parseData(raw) {
|
|
24197
|
+
const out = {};
|
|
24198
|
+
for (const [k, v] of Object.entries(raw ?? {})) {
|
|
24199
|
+
if (k === KIND_FIELD) throw new CanvasToolError("data.kind n\xE3o muda: apague o n\xF3 e crie outro do tipo certo.");
|
|
24200
|
+
if (!isJson(v)) throw new CanvasToolError(`data.${k} n\xE3o \xE9 JSON v\xE1lido.`);
|
|
24201
|
+
out[k] = v;
|
|
24202
|
+
}
|
|
24203
|
+
return out;
|
|
24204
|
+
}
|
|
24205
|
+
server.registerTool(
|
|
24206
|
+
"canvas_list_boards",
|
|
24207
|
+
{
|
|
24208
|
+
title: "Canvas: listar boards",
|
|
24209
|
+
description: "Lista os boards do Canvas online de que voc\xEA \xE9 membro (id, t\xEDtulo, seu papel, seq, \xFAltima mudan\xE7a). S\xF3 leitura.",
|
|
24210
|
+
inputSchema: { kind: external_exports.enum(["board", "template"]).optional().describe("board (padr\xE3o) ou template (moldes).") },
|
|
24211
|
+
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
24212
|
+
},
|
|
24213
|
+
async ({ kind }) => run(async () => {
|
|
24214
|
+
const boards = await api.listBoards(kind ?? "board");
|
|
24215
|
+
if (!boards.length) return "Nenhum board. Crie um com canvas_create_board.";
|
|
24216
|
+
return boards.map((b) => `- ${b.id} \xB7 "${b.title}" \xB7 ${b.role} \xB7 seq ${b.seq} \xB7 ${String(b.updatedAt).slice(0, 16).replace("T", " ")}`).join("\n");
|
|
24217
|
+
})
|
|
24218
|
+
);
|
|
24219
|
+
server.registerTool(
|
|
24220
|
+
"canvas_read_board",
|
|
24221
|
+
{
|
|
24222
|
+
title: "Canvas: ler board",
|
|
24223
|
+
description: "L\xEA um board: n\xF3s (id, tipo, nome, campos principais), liga\xE7\xF5es e v\xEDnculo com o IA Lab, sem m\xEDdia. Obrigat\xF3rio antes de editar (vale para a sess\xE3o). node_ids devolve o data completo desses n\xF3s.",
|
|
24224
|
+
inputSchema: {
|
|
24225
|
+
board_id: boardId,
|
|
24226
|
+
node_ids: external_exports.array(nodeIdArg).max(20).optional().describe("N\xF3s para ver o data inteiro (sem blobs).")
|
|
24227
|
+
},
|
|
24228
|
+
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
24229
|
+
},
|
|
24230
|
+
async ({ board_id, node_ids }) => run(async () => {
|
|
24231
|
+
const snap = await api.snapshot(board_id);
|
|
24232
|
+
seen.set(board_id, snap.seq);
|
|
24233
|
+
const parts = [boardSummary(snap)];
|
|
24234
|
+
if (node_ids?.length) {
|
|
24235
|
+
parts.push("", "Detalhe:");
|
|
24236
|
+
for (const n of node_ids) parts.push(snap.doc.nodes[n] ? nodeDetail(snap.doc.nodes[n]) : `${n}: n\xE3o existe`);
|
|
24237
|
+
}
|
|
24238
|
+
return parts.join("\n");
|
|
24239
|
+
})
|
|
24240
|
+
);
|
|
24241
|
+
server.registerTool(
|
|
24242
|
+
"canvas_create_board",
|
|
24243
|
+
{
|
|
24244
|
+
title: "Canvas: criar board",
|
|
24245
|
+
description: 'Cria um board online (voc\xEA vira dono). template "narration" j\xE1 cria Contexto, Narra\xE7\xE3o, Roteiro e Locu\xE7\xE3o ligados (Narra\xE7\xE3o.blocks \u2192 Roteiro.blocks, Narra\xE7\xE3o.text \u2192 Locu\xE7\xE3o.text). O kit do Contexto a pessoa escolhe no Canvas. N\xE3o gera m\xEDdia.',
|
|
24246
|
+
inputSchema: {
|
|
24247
|
+
title: external_exports.string().trim().min(1).max(200),
|
|
24248
|
+
template: external_exports.enum(["blank", "narration"]).optional().describe("blank (padr\xE3o) ou narration.")
|
|
24249
|
+
},
|
|
24250
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
|
|
24251
|
+
},
|
|
24252
|
+
async ({ title, template }) => run(async () => {
|
|
24253
|
+
const doc = emptyBoard(title);
|
|
24254
|
+
const ids = {};
|
|
24255
|
+
if (template === "narration") {
|
|
24256
|
+
const me = await api.me();
|
|
24257
|
+
let zIndex = 1;
|
|
24258
|
+
const add = (kind, x, y) => {
|
|
24259
|
+
const spec = KINDS[kind];
|
|
24260
|
+
const node = makeNode({
|
|
24261
|
+
type: WORKFLOW_NODE_TYPE,
|
|
24262
|
+
data: { ...spec.data, [KIND_FIELD]: kind },
|
|
24263
|
+
position: { x, y },
|
|
24264
|
+
size: spec.size,
|
|
24265
|
+
zIndex: zIndex++,
|
|
24266
|
+
createdBy: me
|
|
24267
|
+
});
|
|
24268
|
+
doc.nodes[node.id] = node;
|
|
24269
|
+
ids[kind] = node.id;
|
|
24270
|
+
return node.id;
|
|
24271
|
+
};
|
|
24272
|
+
add("context", 200, 200);
|
|
24273
|
+
const narration = add("narration", 650, 200);
|
|
24274
|
+
const shotlist = add("shotlist", 1150, 200);
|
|
24275
|
+
const tts = add("tts", 650, 750);
|
|
24276
|
+
for (const l of [
|
|
24277
|
+
{ source: narration, sourcePort: "blocks", target: shotlist, targetPort: "blocks" },
|
|
24278
|
+
{ source: narration, sourcePort: "text", target: tts, targetPort: "text" }
|
|
24279
|
+
]) {
|
|
24280
|
+
const edge = makeEdge({ ...l, createdBy: me });
|
|
24281
|
+
doc.edges[edge.id] = edge;
|
|
24282
|
+
}
|
|
24283
|
+
}
|
|
24284
|
+
const created = await api.createBoard({ title, kind: "board", doc });
|
|
24285
|
+
seen.set(created.id, created.seq);
|
|
24286
|
+
const idLine = Object.keys(ids).length ? `
|
|
24287
|
+
N\xF3s: ${Object.entries(ids).map(([k, v]) => `${k}=${v}`).join(" \xB7 ")}` : "";
|
|
24288
|
+
return `\u2705 Board criado: "${created.title}" \xB7 id ${created.id} \xB7 seq ${created.seq}.${idLine}`;
|
|
24289
|
+
})
|
|
24290
|
+
);
|
|
24291
|
+
server.registerTool(
|
|
24292
|
+
"canvas_add_node",
|
|
24293
|
+
{
|
|
24294
|
+
title: "Canvas: adicionar n\xF3",
|
|
24295
|
+
description: `Adiciona um n\xF3 (n\xE3o executa). kind: ${kindList()}. data completa o padr\xE3o do tipo; label d\xE1 o nome. Sem position, nasce ao lado do n\xF3 ligado ou \xE0 direita do board. inputs/outputs j\xE1 ligam o n\xF3 no mesmo passo.`,
|
|
24296
|
+
inputSchema: {
|
|
24297
|
+
board_id: boardId,
|
|
24298
|
+
kind: external_exports.string().min(1).max(40),
|
|
24299
|
+
label: external_exports.string().trim().min(1).max(120).optional().describe("Nome do cart\xE3o (data.label)."),
|
|
24300
|
+
data: external_exports.record(external_exports.unknown()).optional().describe("Campos do data. Ex.: narration {blocks:[{id,text}]}, text {text}, prompt {template}."),
|
|
24301
|
+
position: point.optional(),
|
|
24302
|
+
inputs: external_exports.array(external_exports.object({ from: nodeIdArg, from_port: external_exports.string().min(1), to_port: external_exports.string().min(1) })).max(20).optional().describe("Liga\xE7\xF5es que CHEGAM no n\xF3 novo: from.from_port \u2192 novo.to_port."),
|
|
24303
|
+
outputs: external_exports.array(external_exports.object({ from_port: external_exports.string().min(1), to: nodeIdArg, to_port: external_exports.string().min(1) })).max(20).optional().describe("Liga\xE7\xF5es que SAEM do n\xF3 novo: novo.from_port \u2192 to.to_port.")
|
|
24304
|
+
},
|
|
24305
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
|
|
24306
|
+
},
|
|
24307
|
+
async (args) => run(
|
|
24308
|
+
() => edit(args.board_id, async (doc) => {
|
|
24309
|
+
const spec = KINDS[args.kind];
|
|
24310
|
+
if (!spec) throw new CanvasToolError(`Tipo "${args.kind}" desconhecido. Tipos: ${kindList()}.`);
|
|
24311
|
+
const me = await api.me();
|
|
24312
|
+
const data = { ...spec.data, ...parseData(args.data), [KIND_FIELD]: args.kind };
|
|
24313
|
+
if (args.label) data.label = args.label;
|
|
24314
|
+
const near = args.inputs?.[0] ? doc.nodes[args.inputs[0].from] : args.outputs?.[0] ? doc.nodes[args.outputs[0].to] : void 0;
|
|
24315
|
+
const position = args.position ?? placeFor(doc, spec.size, near, args.inputs?.[0] ? "right" : "left");
|
|
24316
|
+
const zIndex = args.kind === "section" ? SECTION_Z : Object.values(doc.nodes).reduce((t, n) => Math.max(t, n.zIndex), 0) + 1;
|
|
24317
|
+
const node = makeNode({ type: WORKFLOW_NODE_TYPE, data, position, size: spec.size, zIndex, createdBy: me });
|
|
24318
|
+
const links = [
|
|
24319
|
+
...(args.inputs ?? []).map((l) => ({ source: l.from, sourcePort: l.from_port, target: node.id, targetPort: l.to_port })),
|
|
24320
|
+
...(args.outputs ?? []).map((l) => ({ source: node.id, sourcePort: l.from_port, target: l.to, targetPort: l.to_port }))
|
|
24321
|
+
];
|
|
24322
|
+
for (const l of links) {
|
|
24323
|
+
const warn = knownPort(args.kind, l.source === node.id ? "out" : "in", l.source === node.id ? l.sourcePort : l.targetPort);
|
|
24324
|
+
if (warn === false) {
|
|
24325
|
+
throw new CanvasToolError(
|
|
24326
|
+
`Porta "${l.source === node.id ? l.sourcePort : l.targetPort}" n\xE3o existe em ${args.kind}. Entradas: ${spec.inputs.join(", ") || "\u2014"}; sa\xEDdas: ${spec.outputs.join(", ") || "\u2014"}.`
|
|
24327
|
+
);
|
|
24328
|
+
}
|
|
24329
|
+
}
|
|
24330
|
+
const edges = edgeOps(doc, links, me, /* @__PURE__ */ new Set([node.id]));
|
|
24331
|
+
return {
|
|
24332
|
+
ops: [{ type: "node.add", nodeId: node.id, node }, ...edges.ops],
|
|
24333
|
+
targets: links.map((l) => l.source === node.id ? l.target : l.source),
|
|
24334
|
+
done: `N\xF3 ${args.kind} "${nodeName(node)}" criado: ${node.id}${edges.ops.length ? ` com ${edges.ops.length} liga\xE7\xE3o(\xF5es)` : ""}.${edges.warn}`
|
|
24335
|
+
};
|
|
24336
|
+
})
|
|
24337
|
+
)
|
|
24338
|
+
);
|
|
24339
|
+
server.registerTool(
|
|
24340
|
+
"canvas_update_node",
|
|
24341
|
+
{
|
|
24342
|
+
title: "Canvas: editar n\xF3",
|
|
24343
|
+
description: "Muda campos do data de um n\xF3, chave a chave (null apaga a chave), sem executar. label renomeia. N\xE3o troca o tipo. Leia o n\xF3 com node_ids antes para n\xE3o sobrescrever o que outra pessoa escreveu.",
|
|
24344
|
+
inputSchema: {
|
|
24345
|
+
board_id: boardId,
|
|
24346
|
+
node_id: nodeIdArg,
|
|
24347
|
+
data: external_exports.record(external_exports.unknown()).optional().describe("Chaves de data a gravar. Valor null apaga a chave."),
|
|
24348
|
+
label: external_exports.string().trim().min(1).max(120).optional(),
|
|
24349
|
+
position: point.optional()
|
|
24350
|
+
},
|
|
24351
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
|
|
24352
|
+
},
|
|
24353
|
+
async (args) => run(
|
|
24354
|
+
() => edit(args.board_id, async (doc) => {
|
|
24355
|
+
const node = needNode(doc, args.node_id);
|
|
24356
|
+
const ops = [];
|
|
24357
|
+
const patch = parseData(args.data);
|
|
24358
|
+
if (args.label) patch.label = args.label;
|
|
24359
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
24360
|
+
const path = `data.${k}`;
|
|
24361
|
+
if (v === null) {
|
|
24362
|
+
if (Object.hasOwn(node.data, k)) ops.push({ type: "node.set", nodeId: node.id, path, value: unsetValue() });
|
|
24363
|
+
} else if (JSON.stringify(node.data[k]) !== JSON.stringify(v)) {
|
|
24364
|
+
ops.push({ type: "node.set", nodeId: node.id, path, value: setValue(v) });
|
|
24365
|
+
}
|
|
24366
|
+
}
|
|
24367
|
+
if (args.position) {
|
|
24368
|
+
ops.push({ type: "node.set", nodeId: node.id, path: "position.x", value: setValue(args.position.x) });
|
|
24369
|
+
ops.push({ type: "node.set", nodeId: node.id, path: "position.y", value: setValue(args.position.y) });
|
|
24370
|
+
}
|
|
24371
|
+
const keys = ops.map((o) => o.type === "node.set" ? o.path.replace(/^data\./, "") : "").join(", ");
|
|
24372
|
+
return { ops, targets: [node.id], done: `N\xF3 "${nodeName(node)}" atualizado: ${keys || "nada mudou"}` };
|
|
24373
|
+
})
|
|
24374
|
+
)
|
|
24375
|
+
);
|
|
24376
|
+
server.registerTool(
|
|
24377
|
+
"canvas_connect",
|
|
24378
|
+
{
|
|
24379
|
+
title: "Canvas: ligar portas",
|
|
24380
|
+
description: "Liga a sa\xEDda de um n\xF3 \xE0 entrada de outro (ids e portas de canvas_read_board). Liga\xE7\xE3o repetida \xE9 ignorada.",
|
|
24381
|
+
inputSchema: {
|
|
24382
|
+
board_id: boardId,
|
|
24383
|
+
source: nodeIdArg,
|
|
24384
|
+
source_port: external_exports.string().min(1).max(80),
|
|
24385
|
+
target: nodeIdArg,
|
|
24386
|
+
target_port: external_exports.string().min(1).max(80)
|
|
24387
|
+
},
|
|
24388
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
|
|
24389
|
+
},
|
|
24390
|
+
async (args) => run(
|
|
24391
|
+
() => edit(args.board_id, async (doc) => {
|
|
24392
|
+
if (args.source === args.target) throw new CanvasToolError("Um n\xF3 n\xE3o se liga nele mesmo.");
|
|
24393
|
+
const link = { source: args.source, sourcePort: args.source_port, target: args.target, targetPort: args.target_port };
|
|
24394
|
+
const { ops, warn } = edgeOps(doc, [link], await api.me(), /* @__PURE__ */ new Set());
|
|
24395
|
+
return {
|
|
24396
|
+
ops,
|
|
24397
|
+
targets: [args.source, args.target],
|
|
24398
|
+
done: ops.length ? `Ligado ${nodeName(doc.nodes[args.source])}.${args.source_port} \u2192 ${nodeName(doc.nodes[args.target])}.${args.target_port}.${warn}` : "A liga\xE7\xE3o j\xE1 existia."
|
|
24399
|
+
};
|
|
24400
|
+
})
|
|
24401
|
+
)
|
|
24402
|
+
);
|
|
24403
|
+
server.registerTool(
|
|
24404
|
+
"canvas_delete",
|
|
24405
|
+
{
|
|
24406
|
+
title: "Canvas: apagar n\xF3s/liga\xE7\xF5es",
|
|
24407
|
+
description: "Apaga n\xF3s (com as liga\xE7\xF5es e varia\xE7\xF5es deles) e/ou liga\xE7\xF5es, num passo s\xF3. Irrevers\xEDvel pelo MCP: confirme com o usu\xE1rio antes. N\xE3o apaga o board.",
|
|
24408
|
+
inputSchema: {
|
|
24409
|
+
board_id: boardId,
|
|
24410
|
+
node_ids: external_exports.array(nodeIdArg).max(200).optional(),
|
|
24411
|
+
edge_ids: external_exports.array(nodeIdArg).max(200).optional()
|
|
24412
|
+
},
|
|
24413
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
|
|
24414
|
+
},
|
|
24415
|
+
async (args) => run(
|
|
24416
|
+
() => edit(args.board_id, async (doc) => {
|
|
24417
|
+
const nodeIds = [...new Set(args.node_ids ?? [])];
|
|
24418
|
+
const edgeIds = [...new Set(args.edge_ids ?? [])];
|
|
24419
|
+
if (!nodeIds.length && !edgeIds.length) throw new CanvasToolError("Informe node_ids e/ou edge_ids.");
|
|
24420
|
+
const missing = [...nodeIds.filter((n) => !doc.nodes[n]), ...edgeIds.filter((e) => !doc.edges[e])];
|
|
24421
|
+
if (missing.length) throw new CanvasToolError(`N\xE3o existem no board: ${missing.join(", ")}. Releia com canvas_read_board.`);
|
|
24422
|
+
const ops = [];
|
|
24423
|
+
const gone = /* @__PURE__ */ new Set();
|
|
24424
|
+
for (const e of edgeIds) {
|
|
24425
|
+
const edge = doc.edges[e];
|
|
24426
|
+
if (nodeIds.includes(edge.source) || nodeIds.includes(edge.target)) continue;
|
|
24427
|
+
ops.push({ type: "edge.remove", edgeId: e });
|
|
24428
|
+
gone.add(e);
|
|
24429
|
+
}
|
|
24430
|
+
for (const n of nodeIds) {
|
|
24431
|
+
for (const op of removeNodeOps(doc, n)) {
|
|
24432
|
+
if (op.type === "edge.remove") {
|
|
24433
|
+
if (gone.has(op.edgeId)) continue;
|
|
24434
|
+
gone.add(op.edgeId);
|
|
24435
|
+
}
|
|
24436
|
+
ops.push(op);
|
|
24437
|
+
}
|
|
24438
|
+
}
|
|
24439
|
+
const names = nodeIds.map((n) => `"${oneLine(nodeName(doc.nodes[n]), 40)}"`);
|
|
24440
|
+
return {
|
|
24441
|
+
ops,
|
|
24442
|
+
targets: nodeIds,
|
|
24443
|
+
done: `Apagado: ${nodeIds.length} n\xF3(s)${names.length ? ` (${names.join(", ")})` : ""} e ${gone.size} liga\xE7\xE3o(\xF5es)`
|
|
24444
|
+
};
|
|
24445
|
+
})
|
|
24446
|
+
)
|
|
24447
|
+
);
|
|
24448
|
+
server.registerTool(
|
|
24449
|
+
"canvas_rename_board",
|
|
24450
|
+
{
|
|
24451
|
+
title: "Canvas: renomear board",
|
|
24452
|
+
description: "Troca o t\xEDtulo do board (board.set). Precisa de papel de editor ou dono.",
|
|
24453
|
+
inputSchema: { board_id: boardId, title: external_exports.string().trim().min(1).max(200) },
|
|
24454
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
|
|
24455
|
+
},
|
|
24456
|
+
async ({ board_id, title }) => run(
|
|
24457
|
+
() => edit(board_id, async (doc) => ({
|
|
24458
|
+
ops: doc.title === title ? [] : [{ type: "board.set", field: "title", value: setValue(title) }],
|
|
24459
|
+
targets: [],
|
|
24460
|
+
done: `Board renomeado: "${doc.title}" \u2192 "${title}"`
|
|
24461
|
+
}))
|
|
24462
|
+
)
|
|
24463
|
+
);
|
|
24464
|
+
server.registerTool(
|
|
24465
|
+
"canvas_list_members",
|
|
24466
|
+
{
|
|
24467
|
+
title: "Canvas: membros do board",
|
|
24468
|
+
description: "Lista quem tem acesso ao board (userId, nome, papel). S\xF3 leitura.",
|
|
24469
|
+
inputSchema: { board_id: boardId },
|
|
24470
|
+
annotations: { readOnlyHint: true, openWorldHint: false }
|
|
24471
|
+
},
|
|
24472
|
+
async ({ board_id }) => run(async () => {
|
|
24473
|
+
const members = await api.members(board_id);
|
|
24474
|
+
return members.map((m) => {
|
|
24475
|
+
const name = [m.firstName, m.lastName].filter(Boolean).join(" ") || m.nickname || "(sem nome)";
|
|
24476
|
+
return `- ${m.userId} \xB7 ${name} \xB7 ${m.role}`;
|
|
24477
|
+
}).join("\n");
|
|
24478
|
+
})
|
|
24479
|
+
);
|
|
24480
|
+
server.registerTool(
|
|
24481
|
+
"canvas_set_member",
|
|
24482
|
+
{
|
|
24483
|
+
title: "Canvas: adicionar membro",
|
|
24484
|
+
description: "D\xE1 acesso ao board (ou troca o papel) para um usu\xE1rio pelo userId (uuid). S\xF3 o dono pode. Ainda n\xE3o h\xE1 busca por e-mail: pe\xE7a o userId ou copie de canvas_list_members de outro board.",
|
|
24485
|
+
inputSchema: {
|
|
24486
|
+
board_id: boardId,
|
|
24487
|
+
user_id: external_exports.string().uuid(),
|
|
24488
|
+
role: external_exports.enum(["editor", "viewer"]).describe("editor edita; viewer s\xF3 v\xEA.")
|
|
24489
|
+
},
|
|
24490
|
+
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: false }
|
|
24491
|
+
},
|
|
24492
|
+
async ({ board_id, user_id, role }) => run(async () => {
|
|
24493
|
+
const r = await api.putMember(board_id, user_id, role);
|
|
24494
|
+
return `\u2705 ${r.userId} agora \xE9 ${r.role} no board.`;
|
|
24495
|
+
})
|
|
24496
|
+
);
|
|
24497
|
+
server.registerTool(
|
|
24498
|
+
"canvas_remove_member",
|
|
24499
|
+
{
|
|
24500
|
+
title: "Canvas: remover membro",
|
|
24501
|
+
description: "Tira o acesso de algu\xE9m ao board (o dono tira qualquer um; quem n\xE3o \xE9 dono s\xF3 sai sozinho). O dono n\xE3o sai.",
|
|
24502
|
+
inputSchema: { board_id: boardId, user_id: external_exports.string().uuid() },
|
|
24503
|
+
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: false }
|
|
24504
|
+
},
|
|
24505
|
+
async ({ board_id, user_id }) => run(async () => {
|
|
24506
|
+
await api.removeMember(board_id, user_id);
|
|
24507
|
+
return `\u2705 ${user_id} n\xE3o tem mais acesso ao board.`;
|
|
24508
|
+
})
|
|
24509
|
+
);
|
|
24510
|
+
}
|
|
24511
|
+
|
|
23469
24512
|
// src/version.ts
|
|
23470
24513
|
import { createRequire } from "module";
|
|
23471
24514
|
var require2 = createRequire(import.meta.url);
|
|
23472
24515
|
function readVersion() {
|
|
23473
|
-
if ("0.
|
|
24516
|
+
if ("0.26.0") return "0.26.0";
|
|
23474
24517
|
try {
|
|
23475
24518
|
const pkg = require2("../package.json");
|
|
23476
24519
|
return pkg.version ?? "0.0.0-unknown";
|
|
@@ -23481,8 +24524,14 @@ function readVersion() {
|
|
|
23481
24524
|
var VERSION = readVersion();
|
|
23482
24525
|
|
|
23483
24526
|
// src/server.ts
|
|
24527
|
+
var SERVER_INSTRUCTIONS = [
|
|
24528
|
+
"BePlus: tudo roda pela conta do usu\xE1rio (diamantes, limites e hist\xF3rico na plataforma).",
|
|
24529
|
+
'M\xEDdia: generate_* gasta diamantes; confira com estimate_cost antes de lotes grandes. Sem project, a gera\xE7\xE3o cai no projeto ativo; billing:"personal" cobra do usu\xE1rio.',
|
|
24530
|
+
"Calls: search_calls acha, get_call traz a transcri\xE7\xE3o.",
|
|
24531
|
+
"Canvas online (canvas_*): leia com canvas_read_board antes de editar e siga o estado lido. Cada edi\xE7\xE3o vai num passo s\xF3; se outra pessoa mexeu nos mesmos n\xF3s, a ferramenta recusa e pede releitura. As ferramentas canvas_* n\xE3o geram m\xEDdia nem gastam diamantes: quem roda os n\xF3s \xE9 o usu\xE1rio no Canvas. D\xEA nome (label) aos n\xF3s, n\xE3o invente tipos nem portas, e confirme antes de canvas_delete."
|
|
24532
|
+
].join("\n");
|
|
23484
24533
|
function buildServer(cfg) {
|
|
23485
|
-
const server = new McpServer({ name: "beplus", version: VERSION });
|
|
24534
|
+
const server = new McpServer({ name: "beplus", version: VERSION }, { instructions: SERVER_INSTRUCTIONS });
|
|
23486
24535
|
const client = new BeplusClient(cfg);
|
|
23487
24536
|
registerGenerationTools(server, client, cfg);
|
|
23488
24537
|
registerUpscaleTools(server, client, cfg);
|
|
@@ -23493,6 +24542,7 @@ function buildServer(cfg) {
|
|
|
23493
24542
|
registerProjectTools(server, client, cfg);
|
|
23494
24543
|
registerClientTools(server, client, cfg);
|
|
23495
24544
|
registerCallTools(server, client, cfg);
|
|
24545
|
+
registerCanvasTools(server, client, cfg);
|
|
23496
24546
|
return { server, client };
|
|
23497
24547
|
}
|
|
23498
24548
|
|