strapi-plugin-mcp-chat 0.1.0 → 0.5.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.
@@ -36,12 +36,12 @@ module.exports = __toCommonJS(index_exports);
36
36
  // server/src/controllers/chat.ts
37
37
  var chat_default = ({ strapi }) => ({
38
38
  async message(ctx) {
39
- const { messages, image, lang, previewUrl } = ctx.request.body || {};
39
+ const { messages, image, lang, previewUrl, autoPublish } = ctx.request.body || {};
40
40
  if (!Array.isArray(messages) || messages.length === 0) {
41
41
  return ctx.badRequest('Campo "messages" (array) \xE9 obrigat\xF3rio.');
42
42
  }
43
43
  try {
44
- const result = await strapi.plugin("mcp-chat").service("chat").chat({ messages, image, lang, previewUrl });
44
+ const result = await strapi.plugin("mcp-chat").service("chat").chat({ messages, image, lang, previewUrl, autoPublish });
45
45
  ctx.body = result;
46
46
  } catch (e) {
47
47
  strapi.log.error(`[mcp-chat] ${e?.message || e}`);
@@ -412,6 +412,68 @@ var import_node_path = __toESM(require("node:path"));
412
412
  function isDev() {
413
413
  return process.env.NODE_ENV === "development";
414
414
  }
415
+ var KNOWN_ATTR_TYPES = /* @__PURE__ */ new Set([
416
+ "string",
417
+ "text",
418
+ "richtext",
419
+ "blocks",
420
+ "email",
421
+ "password",
422
+ "uid",
423
+ "enumeration",
424
+ "json",
425
+ "integer",
426
+ "biginteger",
427
+ "decimal",
428
+ "float",
429
+ "date",
430
+ "time",
431
+ "datetime",
432
+ "timestamp",
433
+ "boolean",
434
+ "media",
435
+ "relation",
436
+ "component",
437
+ "dynamiczone"
438
+ ]);
439
+ function validateApi(api) {
440
+ const errs = [];
441
+ const rel = Object.keys(api.files).find((r) => r.endsWith("schema.json"));
442
+ if (!rel) {
443
+ errs.push(`${api.singularName}: schema.json ausente nos arquivos gerados`);
444
+ return errs;
445
+ }
446
+ let schema;
447
+ try {
448
+ schema = JSON.parse(api.files[rel]);
449
+ } catch (e) {
450
+ errs.push(`${api.singularName}: schema.json n\xE3o \xE9 JSON v\xE1lido (${e?.message ?? e})`);
451
+ return errs;
452
+ }
453
+ if (schema?.kind !== "collectionType" && schema?.kind !== "singleType") {
454
+ errs.push(`${api.singularName}: "kind" inv\xE1lido (${schema?.kind})`);
455
+ }
456
+ if (!schema?.info?.singularName || !schema?.info?.pluralName) {
457
+ errs.push(`${api.singularName}: info.singularName/pluralName obrigat\xF3rios`);
458
+ }
459
+ const attrs = schema?.attributes;
460
+ if (!attrs || typeof attrs !== "object") {
461
+ errs.push(`${api.singularName}: "attributes" ausente ou inv\xE1lido`);
462
+ } else {
463
+ for (const [name, a] of Object.entries(attrs)) {
464
+ if (!a || typeof a !== "object" || !a.type) {
465
+ errs.push(`${api.singularName}.${name}: atributo sem "type"`);
466
+ } else if (!KNOWN_ATTR_TYPES.has(a.type)) {
467
+ errs.push(`${api.singularName}.${name}: type desconhecido "${a.type}"`);
468
+ } else if (a.type === "relation" && !a.target) {
469
+ errs.push(`${api.singularName}.${name}: relation sem "target"`);
470
+ } else if (a.type === "component" && !a.component) {
471
+ errs.push(`${api.singularName}.${name}: component sem "component"`);
472
+ }
473
+ }
474
+ }
475
+ return errs;
476
+ }
415
477
  function writeApis(apis, opts) {
416
478
  const result = {
417
479
  ok: false,
@@ -431,6 +493,37 @@ function writeApis(apis, opts) {
431
493
  result.errors.push(`apiRoot deve ser um caminho absoluto: ${opts.apiRoot}`);
432
494
  return result;
433
495
  }
496
+ const toWrite = apis.filter((api) => !import_node_fs.default.existsSync(import_node_path.default.join(opts.apiRoot, api.singularName)));
497
+ const knownSingulars = /* @__PURE__ */ new Set([
498
+ ...apis.map((a) => a.singularName),
499
+ ...import_node_fs.default.existsSync(opts.apiRoot) ? import_node_fs.default.readdirSync(opts.apiRoot, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name) : []
500
+ ]);
501
+ const validationErrors = [];
502
+ for (const api of toWrite) {
503
+ validationErrors.push(...validateApi(api));
504
+ const rel = Object.keys(api.files).find((r) => r.endsWith("schema.json"));
505
+ if (rel) {
506
+ try {
507
+ const attrs = JSON.parse(api.files[rel])?.attributes || {};
508
+ for (const [name, a] of Object.entries(attrs)) {
509
+ if (a?.type === "relation" && typeof a.target === "string") {
510
+ const tgt = a.target.split("::")[1]?.split(".")[0];
511
+ if (tgt && !knownSingulars.has(tgt)) {
512
+ validationErrors.push(`${api.singularName}.${name}: relation aponta para "${a.target}" inexistente`);
513
+ }
514
+ }
515
+ }
516
+ } catch {
517
+ }
518
+ }
519
+ }
520
+ if (validationErrors.length) {
521
+ result.errors.push(
522
+ "Schema gerado inv\xE1lido \u2014 nada foi escrito (provis\xE3o abortada com seguran\xE7a):",
523
+ ...validationErrors
524
+ );
525
+ return result;
526
+ }
434
527
  for (const api of apis) {
435
528
  const apiDir = import_node_path.default.join(opts.apiRoot, api.singularName);
436
529
  if (import_node_fs.default.existsSync(apiDir)) {
@@ -1712,6 +1805,18 @@ export function __getLocale(): string {
1712
1805
  } catch {}
1713
1806
  return __defaultLocale;
1714
1807
  }
1808
+ /** Status ativo: ?preview=1 ou ?status=draft na URL \u2192 rascunho (preview do
1809
+ * mcp-chat em modo Draft). Caso contr\xE1rio, publicado. S\xF3 no cliente; no SSR
1810
+ * cai para "published" (ver a nota de draft preview no README). */
1811
+ export function __getStatus(): "draft" | "published" {
1812
+ try {
1813
+ if (typeof window !== "undefined") {
1814
+ const sp = new URL(window.location.href).searchParams;
1815
+ if (sp.get("preview") === "1" || sp.get("status") === "draft") return "draft";
1816
+ }
1817
+ } catch {}
1818
+ return "published";
1819
+ }
1715
1820
 
1716
1821
  ${mapperCode}
1717
1822
 
@@ -1719,11 +1824,13 @@ const __store: Record<string, any> = {};
1719
1824
  export function hydrate(d: any) { if (d) for (const k of Object.keys(d)) __store[k] = d[k]; }
1720
1825
 
1721
1826
  export async function loadAllData(opts: { locale?: string; status?: "draft" | "published" } = {}) {
1827
+ // Sem status expl\xEDcito, herda do flag de preview na URL (?preview=1 \u2192 draft).
1828
+ const __opts = { locale: opts.locale, status: opts.status || __getStatus() };
1722
1829
  const raw: Record<string, any> = {};
1723
1830
  await Promise.all(
1724
1831
  __cts.map(async (c: any) => {
1725
1832
  try {
1726
- raw[c.s] = c.k === "singleType" ? await fetchSingle(c.s, opts) : await fetchCollection(c.p, opts);
1833
+ raw[c.s] = c.k === "singleType" ? await fetchSingle(c.s, __opts) : await fetchCollection(c.p, __opts);
1727
1834
  } catch {
1728
1835
  raw[c.s] = c.k === "singleType" ? null : [];
1729
1836
  }
@@ -2074,8 +2181,11 @@ async function integrateFrontend(strapi, opts) {
2074
2181
  // server/src/controllers/frontend.ts
2075
2182
  var MANIFEST_NAME = "strapi.manifest.json";
2076
2183
  function ensureInside2(base, target) {
2077
- const n = import_node_path7.default.normalize(target);
2078
- return n === base || n.startsWith(base + import_node_path7.default.sep);
2184
+ const b = import_node_path7.default.resolve(base);
2185
+ const t = import_node_path7.default.resolve(target);
2186
+ if (t === b) return true;
2187
+ const rel = import_node_path7.default.relative(b, t);
2188
+ return !!rel && !rel.startsWith("..") && !import_node_path7.default.isAbsolute(rel);
2079
2189
  }
2080
2190
  function toKebab(input) {
2081
2191
  const s = (input || "frontend").toLowerCase().replace(/\.zip$/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/^[^a-z]+/, "");
@@ -2297,6 +2407,15 @@ var baseHeaders = {
2297
2407
  "Content-Type": "application/json",
2298
2408
  Accept: "application/json, text/event-stream"
2299
2409
  };
2410
+ var fetchT = async (url, opts, timeoutMs = 8e3) => {
2411
+ const ctrl = new AbortController();
2412
+ const t = setTimeout(() => ctrl.abort(), timeoutMs);
2413
+ try {
2414
+ return await fetch(url, { ...opts, signal: ctrl.signal });
2415
+ } finally {
2416
+ clearTimeout(t);
2417
+ }
2418
+ };
2300
2419
  var parseSse = (text) => {
2301
2420
  const dataLines = text.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trim()).filter(Boolean);
2302
2421
  const last = dataLines[dataLines.length - 1];
@@ -2320,7 +2439,7 @@ var McpClient = class {
2320
2439
  return h;
2321
2440
  }
2322
2441
  async init() {
2323
- const res = await fetch(this.url, {
2442
+ const res = await fetchT(this.url, {
2324
2443
  method: "POST",
2325
2444
  headers: this.headers(),
2326
2445
  body: JSON.stringify({
@@ -2336,14 +2455,14 @@ var McpClient = class {
2336
2455
  });
2337
2456
  this.sessionId = res.headers.get("mcp-session-id") || void 0;
2338
2457
  await res.text();
2339
- await fetch(this.url, {
2458
+ await fetchT(this.url, {
2340
2459
  method: "POST",
2341
2460
  headers: this.headers(),
2342
2461
  body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })
2343
2462
  });
2344
2463
  }
2345
2464
  async rpc(method, params, id) {
2346
- const res = await fetch(this.url, {
2465
+ const res = await fetchT(this.url, {
2347
2466
  method: "POST",
2348
2467
  headers: this.headers(),
2349
2468
  body: JSON.stringify({ jsonrpc: "2.0", id, method, params })
@@ -2458,16 +2577,19 @@ function createContentTools(strapi) {
2458
2577
  (ct) => ct.uid?.startsWith("api::")
2459
2578
  );
2460
2579
  const attrsOf = (uid) => strapi.contentTypes?.[uid]?.attributes || strapi.components?.[uid]?.attributes || {};
2461
- const buildPopulate = (attributes, seen = /* @__PURE__ */ new Set()) => {
2580
+ const hasDraftAndPublish = (uid) => strapi.contentTypes?.[uid]?.options?.draftAndPublish === true;
2581
+ const MAX_DEPTH = 8;
2582
+ const buildPopulate = (attributes, seen = /* @__PURE__ */ new Set(), depth = 0) => {
2583
+ if (depth >= MAX_DEPTH) return {};
2462
2584
  const populate = {};
2463
2585
  for (const [name, a] of Object.entries(attributes)) {
2464
2586
  if (a.type === "component" && a.component) {
2465
- const sub = seen.has(a.component) ? {} : buildPopulate(attrsOf(a.component), new Set(seen).add(a.component));
2587
+ const sub = seen.has(a.component) ? {} : buildPopulate(attrsOf(a.component), new Set(seen).add(a.component), depth + 1);
2466
2588
  populate[name] = Object.keys(sub).length ? { populate: sub } : true;
2467
2589
  } else if (a.type === "dynamiczone") {
2468
2590
  const on = {};
2469
2591
  for (const comp of a.components || []) {
2470
- const sub = seen.has(comp) ? {} : buildPopulate(attrsOf(comp), new Set(seen).add(comp));
2592
+ const sub = seen.has(comp) ? {} : buildPopulate(attrsOf(comp), new Set(seen).add(comp), depth + 1);
2471
2593
  on[comp] = Object.keys(sub).length ? { populate: sub } : true;
2472
2594
  }
2473
2595
  populate[name] = { on };
@@ -2479,6 +2601,7 @@ function createContentTools(strapi) {
2479
2601
  };
2480
2602
  const walkFind = (node, attributes, basePath, needle, collect) => {
2481
2603
  if (!node || typeof node !== "object") return;
2604
+ if (basePath.length > 24) return;
2482
2605
  for (const [name, a] of Object.entries(attributes)) {
2483
2606
  const v = node[name];
2484
2607
  if (v == null) continue;
@@ -2503,11 +2626,13 @@ function createContentTools(strapi) {
2503
2626
  }
2504
2627
  }
2505
2628
  };
2629
+ const MAX_MATCHES = 100;
2506
2630
  const buscarTexto = async (termo) => {
2507
2631
  const needle = String(termo || "").toLowerCase().trim();
2508
2632
  if (!needle) return { erro: "termo vazio" };
2509
2633
  const matches = [];
2510
2634
  for (const ct of apiContentTypes()) {
2635
+ if (matches.length >= MAX_MATCHES) break;
2511
2636
  const attributes = ct.attributes || {};
2512
2637
  const populate = buildPopulate(attributes);
2513
2638
  let entries = [];
@@ -2517,6 +2642,7 @@ function createContentTools(strapi) {
2517
2642
  } catch {
2518
2643
  continue;
2519
2644
  }
2645
+ const dp = hasDraftAndPublish(ct.uid);
2520
2646
  for (const e of entries) {
2521
2647
  walkFind(e, attributes, [], needle, (path9, campo, valor) => {
2522
2648
  matches.push({
@@ -2525,12 +2651,20 @@ function createContentTools(strapi) {
2525
2651
  documentId: e.documentId,
2526
2652
  path: path9,
2527
2653
  campo,
2528
- valor_atual: valor.length > 300 ? valor.slice(0, 300) + "\u2026" : valor
2654
+ valor_atual: valor.length > 300 ? valor.slice(0, 300) + "\u2026" : valor,
2655
+ // draftAndPublish=false → não há rascunho; a edição já é o conteúdo
2656
+ // vivo e não há o que publicar (a IA deve avisar o usuário).
2657
+ draftAndPublish: dp
2529
2658
  });
2530
2659
  });
2531
2660
  }
2532
2661
  }
2533
- return { total: matches.length, resultados: matches };
2662
+ const truncated = matches.length > MAX_MATCHES;
2663
+ return {
2664
+ total: matches.length,
2665
+ resultados: matches.slice(0, MAX_MATCHES),
2666
+ ...truncated ? { truncado: true, nota: `mostrando ${MAX_MATCHES} de ${matches.length} resultados; refine o termo` } : {}
2667
+ };
2534
2668
  };
2535
2669
  const sanitizeNode = (node, attributes) => {
2536
2670
  if (node == null) return node;
@@ -2572,7 +2706,7 @@ function createContentTools(strapi) {
2572
2706
  const loc = locale ? { locale } : {};
2573
2707
  if (p.length === 1 && ad && TEXTUAL.includes(ad.type)) {
2574
2708
  const updated2 = await strapi.documents(uid).update({ documentId, ...loc, data: { [topAttr]: novo_valor } });
2575
- return { ok: true, uid, documentId: updated2?.documentId || documentId, path: p, novo_valor, locale };
2709
+ return { ok: true, uid, documentId: updated2?.documentId || documentId, path: p, novo_valor, locale, draftAndPublish: hasDraftAndPublish(uid) };
2576
2710
  }
2577
2711
  const populate = buildPopulate(attributes);
2578
2712
  const entry = await strapi.documents(uid).findOne({ documentId, status: "draft", ...loc, populate });
@@ -2586,13 +2720,22 @@ function createContentTools(strapi) {
2586
2720
  cur[p[p.length - 1]] = novo_valor;
2587
2721
  const data = { [topAttr]: sanitizeAttr(entry[topAttr], ad) };
2588
2722
  const updated = await strapi.documents(uid).update({ documentId, ...loc, data });
2589
- return { ok: true, uid, documentId: updated?.documentId || documentId, path: p, novo_valor, locale };
2723
+ return { ok: true, uid, documentId: updated?.documentId || documentId, path: p, novo_valor, locale, draftAndPublish: hasDraftAndPublish(uid) };
2590
2724
  };
2591
2725
  const publicar = async ({
2592
2726
  uid,
2593
2727
  documentId,
2594
2728
  locale
2595
2729
  }) => {
2730
+ if (!hasDraftAndPublish(uid)) {
2731
+ return {
2732
+ ok: true,
2733
+ uid,
2734
+ documentId,
2735
+ status: "no-draft-publish",
2736
+ nota: "Esta content-type n\xE3o tem Draft & Publish; n\xE3o h\xE1 rascunho a publicar \u2014 a altera\xE7\xE3o j\xE1 est\xE1 no ar."
2737
+ };
2738
+ }
2596
2739
  await strapi.documents(uid).publish({ documentId, ...locale ? { locale } : {} });
2597
2740
  return { ok: true, uid, documentId, status: "published", locale };
2598
2741
  };
@@ -2722,7 +2865,7 @@ function createContentTools(strapi) {
2722
2865
  if (!Object.keys(data).length) continue;
2723
2866
  await strapi.documents(ct.uid).update({ documentId: e.documentId, locale: tgt, data });
2724
2867
  documentos += 1;
2725
- if (publish) {
2868
+ if (publish && hasDraftAndPublish(ct.uid)) {
2726
2869
  await strapi.documents(ct.uid).publish({ documentId: e.documentId, locale: tgt });
2727
2870
  publicados += 1;
2728
2871
  }
@@ -2779,7 +2922,7 @@ var openAiToolSpecs = [
2779
2922
  type: "function",
2780
2923
  function: {
2781
2924
  name: "publicar",
2782
- description: 'Publica a entrada (torna a altera\xE7\xE3o vis\xEDvel no site p\xFAblico). Passe "locale" para publicar um idioma espec\xEDfico, ou "*" para todos.',
2925
+ description: 'Publica a entrada (torna a altera\xE7\xE3o vis\xEDvel no site p\xFAblico). Passe "locale" para publicar um idioma espec\xEDfico, ou "*" para todos. Se a content-type N\xC3O tiver Draft & Publish, n\xE3o h\xE1 o que publicar: retorna status "no-draft-publish" (a edi\xE7\xE3o j\xE1 est\xE1 no ar) \u2014 avise o usu\xE1rio em vez de tentar publicar de novo.',
2783
2926
  parameters: {
2784
2927
  type: "object",
2785
2928
  properties: {
@@ -2955,9 +3098,9 @@ Ferramentas de conte\xFAdo:
2955
3098
  Fluxo padr\xE3o quando o usu\xE1rio pede uma mudan\xE7a no site (por texto, voz ou mostrando a tela):
2956
3099
  1. Use buscar_texto com um trecho distintivo do texto a alterar (sem r\xF3tulos de status).
2957
3100
  2. Se houver mais de um resultado, escolha o mais prov\xE1vel pelo contexto (e diga qual escolheu); se amb\xEDguo de verdade, pergunte.
2958
- 3. editar_campo passando o mesmo uid, documentId e path do resultado, com o novo valor.
2959
- 4. publicar a entrada.
2960
- 5. Confirme em 1 frase o que foi alterado e publicado (content-type, campo, antes \u2192 depois).
3101
+ 3. editar_campo passando o mesmo uid, documentId e path do resultado, com o novo valor. Isso salva como RASCUNHO (n\xE3o publica).
3102
+ 4. Decida se publica ou n\xE3o conforme a POL\xCDTICA DE PUBLICA\xC7\xC3O indicada mais abaixo.
3103
+ 5. Confirme em 1 frase o que foi alterado (content-type, campo, antes \u2192 depois) e se ficou como rascunho ou foi publicado.
2961
3104
 
2962
3105
  Ferramentas de tradu\xE7\xE3o / idiomas (i18n):
2963
3106
  - listar_locales(): mostra os idiomas configurados e o default.
@@ -2971,6 +3114,8 @@ Fluxo quando o usu\xE1rio pede tradu\xE7\xE3o (ex.: "quero o site todo em pt-BR"
2971
3114
  3. Ap\xF3s o restart, ao repetir, traduzir funciona e localiza tudo.
2972
3115
  4. Confirme em 1 frase: idiomas, quantos documentos e campos foram traduzidos/publicados (use o resumo retornado, n\xE3o despeje o conte\xFAdo).
2973
3116
 
3117
+ Draft & Publish: cada resultado de buscar_texto traz "draftAndPublish". Se for false, aquele tipo N\xC3O tem rascunho no Strapi \u2014 a edi\xE7\xE3o j\xE1 \xE9 o conte\xFAdo vivo e N\xC3O h\xE1 o que publicar; nesse caso, ao confirmar, avise que "esse conte\xFAdo n\xE3o tem rascunho, a altera\xE7\xE3o j\xE1 est\xE1 no ar" e N\xC3O chame publicar.
3118
+
2974
3119
  Se o usu\xE1rio compartilhar a tela, uma imagem \xE9 anexada \xE0 \xFAltima mensagem \u2014 use-a para entender exatamente o que ele est\xE1 vendo e qual texto quer trocar.
2975
3120
 
2976
3121
  Seja objetivo e acion\xE1vel. Responda SEMPRE em portugu\xEAs.`,
@@ -2984,9 +3129,9 @@ Content tools:
2984
3129
  Default flow when the user asks for a site change (by text, voice or by showing their screen):
2985
3130
  1. Use buscar_texto with a distinctive snippet of the text to change (no status labels).
2986
3131
  2. If there is more than one result, pick the most likely from context (and say which); if truly ambiguous, ask.
2987
- 3. editar_campo passing the same uid, documentId and path from the result, with the new value.
2988
- 4. publicar the entry.
2989
- 5. Confirm in one sentence what was changed and published (content-type, field, before \u2192 after).
3132
+ 3. editar_campo passing the same uid, documentId and path from the result, with the new value. This saves a DRAFT (does not publish).
3133
+ 4. Decide whether to publish based on the PUBLISH POLICY stated below.
3134
+ 5. Confirm in one sentence what was changed (content-type, field, before \u2192 after) and whether it stayed a draft or was published.
2990
3135
 
2991
3136
  Translation / language tools (i18n):
2992
3137
  - listar_locales(): shows configured languages and the default.
@@ -3000,12 +3145,14 @@ Flow when the user asks for translation (e.g. "I want the whole site in pt-BR"):
3000
3145
  3. After the restart, repeating the request makes traduzir localize everything.
3001
3146
  4. Confirm in one sentence: languages, how many documents and fields were translated/published (use the returned summary, don't dump the content).
3002
3147
 
3148
+ Draft & Publish: each buscar_texto result includes "draftAndPublish". If it is false, that type has NO draft in Strapi \u2014 the edit IS the live content and there is nothing to publish; in that case, when confirming, warn that "this content has no draft, the change is already live" and do NOT call publicar.
3149
+
3003
3150
  If the user shares their screen, an image is attached to the last message \u2014 use it to understand exactly what they see and which text they want to change.
3004
3151
 
3005
3152
  Be concise and actionable. ALWAYS answer in English.`
3006
3153
  };
3007
3154
  var chat_default2 = ({ strapi }) => ({
3008
- async chat({ messages, image, lang = "pt", previewUrl }) {
3155
+ async chat({ messages, image, lang = "pt", previewUrl, autoPublish = false }) {
3009
3156
  const apiKey = process.env.OPENAI_API_KEY;
3010
3157
  if (!apiKey) {
3011
3158
  throw new Error(
@@ -3029,7 +3176,10 @@ var chat_default2 = ({ strapi }) => ({
3029
3176
  if (process.env.PLAYWRIGHT_MCP_URL) {
3030
3177
  try {
3031
3178
  const client = new McpClient(process.env.PLAYWRIGHT_MCP_URL, "playwright");
3032
- await client.init();
3179
+ await Promise.race([
3180
+ client.init(),
3181
+ new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), 4e3))
3182
+ ]);
3033
3183
  const list = await client.listTools();
3034
3184
  for (const t of list) {
3035
3185
  if (mcpByTool[t.name]) continue;
@@ -3062,7 +3212,19 @@ Voc\xEA tamb\xE9m controla um navegador real via ferramentas browser_* (Playwrig
3062
3212
 
3063
3213
  You also control a real browser via browser_* tools (Playwright), pointed at the STRAPI ADMIN at ${adminBase} (the backend \u2014 this is where content actually changes, NOT the public site). You can navigate (browser_navigate), click, type, scroll, take your own screenshots (browser_take_screenshot) and inspect console/errors. Always prefer your direct tools (buscar_texto/editar_campo/publicar) to change content; use the browser to VERIFY in the admin that the edit/publish landed, or for admin UI flows the direct tools don't cover.`
3064
3214
  };
3065
- const systemContent = SYSTEM[language] + (hasBrowser ? BROWSER_NOTE[language] : "");
3215
+ const PUBLISH_POLICY = {
3216
+ pt: autoPublish ? `
3217
+
3218
+ POL\xCDTICA DE PUBLICA\xC7\xC3O: AUTO-PUBLICAR est\xE1 LIGADO. Depois de editar_campo, chame publicar para deixar a mudan\xE7a no ar. Em traduzir, use publish:true (default).` : `
3219
+
3220
+ POL\xCDTICA DE PUBLICA\xC7\xC3O: MODO RASCUNHO (auto-publicar DESLIGADO). N\xC3O chame publicar a menos que o usu\xE1rio pe\xE7a explicitamente ("publica", "p\xF5e no ar", "publish"). Depois de editar_campo, PARE e avise que a altera\xE7\xE3o foi salva como RASCUNHO para revis\xE3o (ela j\xE1 aparece no preview em modo rascunho, mas ainda n\xE3o no site p\xFAblico). Em traduzir, passe publish:false. Se o usu\xE1rio pedir para publicar, a\xED sim use publicar (ou traduzir com publish:true).`,
3221
+ en: autoPublish ? `
3222
+
3223
+ PUBLISH POLICY: AUTO-PUBLISH is ON. After editar_campo, call publicar to make the change live. For traduzir, use publish:true (default).` : `
3224
+
3225
+ PUBLISH POLICY: DRAFT MODE (auto-publish OFF). Do NOT call publicar unless the user explicitly asks ("publish", "make it live", "publica"). After editar_campo, STOP and tell them the change was saved as a DRAFT for review (it already shows in the preview when in draft mode, but not on the public site yet). For traduzir, pass publish:false. If the user asks to publish, then use publicar (or traduzir with publish:true).`
3226
+ };
3227
+ const systemContent = SYSTEM[language] + (hasBrowser ? BROWSER_NOTE[language] : "") + PUBLISH_POLICY[language];
3066
3228
  const convo = [{ role: "system", content: systemContent }];
3067
3229
  const pageNote = previewUrl ? language === "en" ? `
3068
3230
 
@@ -3089,11 +3251,22 @@ You also control a real browser via browser_* tools (Playwright), pointed at the
3089
3251
  }
3090
3252
  });
3091
3253
  const callOpenAI = async (body) => {
3092
- const res = await fetch(OPENAI_URL4, {
3093
- method: "POST",
3094
- headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
3095
- body: JSON.stringify(body)
3096
- });
3254
+ const ctrl = new AbortController();
3255
+ const timer = setTimeout(() => ctrl.abort(), 6e4);
3256
+ let res;
3257
+ try {
3258
+ res = await fetch(OPENAI_URL4, {
3259
+ method: "POST",
3260
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
3261
+ body: JSON.stringify(body),
3262
+ signal: ctrl.signal
3263
+ });
3264
+ } catch (e) {
3265
+ if (e?.name === "AbortError") throw new Error("OpenAI chat: tempo limite excedido (60s).");
3266
+ throw e;
3267
+ } finally {
3268
+ clearTimeout(timer);
3269
+ }
3097
3270
  if (!res.ok) throw new Error(`OpenAI chat: ${await res.text()}`);
3098
3271
  return res.json();
3099
3272
  };
@@ -3132,6 +3305,11 @@ You also control a real browser via browser_* tools (Playwright), pointed at the
3132
3305
  } catch (e) {
3133
3306
  content = `Erro ao chamar a tool ${name}: ${e?.message || e}`;
3134
3307
  }
3308
+ const MAX_TOOL_CHARS = 12e3;
3309
+ if (content.length > MAX_TOOL_CHARS) {
3310
+ content = content.slice(0, MAX_TOOL_CHARS) + `
3311
+ \u2026[resultado truncado: ${content.length} chars]`;
3312
+ }
3135
3313
  convo.push({ role: "tool", tool_call_id: call.id, content });
3136
3314
  }
3137
3315
  continue;
@@ -3255,196 +3433,166 @@ var routes_default = {
3255
3433
 
3256
3434
  // server/src/mcp/tools/buscar-texto.ts
3257
3435
  var import_utils2 = require("@strapi/utils");
3258
- var tool = {
3259
- register(registerTool) {
3260
- registerTool({
3261
- name: "mcp_chat_buscar_texto",
3262
- title: "Search text across content (deep)",
3263
- description: 'Search a phrase across ALL content-types, single types, components and dynamic zones (recursive, substring). Returns matches with a `path` (e.g. ["dynamic_zone",2,"heading"]) to pass to mcp_chat_editar_campo.',
3264
- resolveInputSchema: () => import_utils2.z.object({ termo: import_utils2.z.string() }),
3265
- resolveOutputSchema: () => import_utils2.z.object({
3266
- total: import_utils2.z.number().optional(),
3267
- resultados: import_utils2.z.array(import_utils2.z.any()).optional(),
3268
- erro: import_utils2.z.string().optional()
3269
- }),
3270
- auth: { policies: [{ action: "plugin::content-manager.explorer.read" }] },
3271
- createHandler: (strapi) => async ({ args }) => {
3272
- const r = await createContentTools(strapi).buscarTexto(args?.termo);
3273
- return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
3274
- }
3275
- });
3436
+
3437
+ // server/src/mcp/define.ts
3438
+ var defineTool = (def) => def;
3439
+
3440
+ // server/src/mcp/tools/buscar-texto.ts
3441
+ var buscar_texto_default = defineTool({
3442
+ name: "mcp_chat_buscar_texto",
3443
+ title: "Search text across content (deep)",
3444
+ description: 'Search a phrase across ALL content-types, single types, components and dynamic zones (recursive, substring). Returns matches with a `path` (e.g. ["dynamic_zone",2,"heading"]) to pass to mcp_chat_editar_campo.',
3445
+ resolveInputSchema: () => import_utils2.z.object({ termo: import_utils2.z.string() }),
3446
+ resolveOutputSchema: () => import_utils2.z.object({
3447
+ total: import_utils2.z.number().optional(),
3448
+ resultados: import_utils2.z.array(import_utils2.z.any()).optional(),
3449
+ erro: import_utils2.z.string().optional()
3450
+ }),
3451
+ auth: { policies: [{ action: "plugin::content-manager.explorer.read" }] },
3452
+ createHandler: (strapi) => async ({ args }) => {
3453
+ const r = await createContentTools(strapi).buscarTexto(args.termo);
3454
+ return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
3276
3455
  }
3277
- };
3278
- var buscar_texto_default = tool;
3456
+ });
3279
3457
 
3280
3458
  // server/src/mcp/tools/editar-campo.ts
3281
3459
  var import_utils3 = require("@strapi/utils");
3282
- var tool2 = {
3283
- register(registerTool) {
3284
- registerTool({
3285
- name: "mcp_chat_editar_campo",
3286
- title: "Edit a (possibly nested) field",
3287
- description: "Edit a field value (saved as draft), including text nested in components/dynamic zones. Pass the `path` exactly as returned by mcp_chat_buscar_texto; for a simple top-level field you may use `campo`.",
3288
- resolveInputSchema: () => import_utils3.z.object({
3289
- uid: import_utils3.z.string(),
3290
- documentId: import_utils3.z.string(),
3291
- path: import_utils3.z.array(import_utils3.z.union([import_utils3.z.string(), import_utils3.z.number()])).optional(),
3292
- campo: import_utils3.z.string().optional(),
3293
- novo_valor: import_utils3.z.string(),
3294
- locale: import_utils3.z.string().optional()
3295
- }),
3296
- resolveOutputSchema: () => import_utils3.z.object({
3297
- ok: import_utils3.z.boolean().optional(),
3298
- uid: import_utils3.z.string().optional(),
3299
- documentId: import_utils3.z.string().optional(),
3300
- path: import_utils3.z.array(import_utils3.z.any()).optional(),
3301
- novo_valor: import_utils3.z.string().optional(),
3302
- erro: import_utils3.z.string().optional()
3303
- }),
3304
- auth: { policies: [{ action: "plugin::content-manager.explorer.update" }] },
3305
- createHandler: (strapi) => async ({ args }) => {
3306
- const r = await createContentTools(strapi).editarCampo(args);
3307
- return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
3308
- }
3309
- });
3460
+ var editar_campo_default = defineTool({
3461
+ name: "mcp_chat_editar_campo",
3462
+ title: "Edit a (possibly nested) field",
3463
+ description: "Edit a field value (saved as draft), including text nested in components/dynamic zones. Pass the `path` exactly as returned by mcp_chat_buscar_texto; for a simple top-level field you may use `campo`.",
3464
+ resolveInputSchema: () => import_utils3.z.object({
3465
+ uid: import_utils3.z.string(),
3466
+ documentId: import_utils3.z.string(),
3467
+ path: import_utils3.z.array(import_utils3.z.union([import_utils3.z.string(), import_utils3.z.number()])).optional(),
3468
+ campo: import_utils3.z.string().optional(),
3469
+ novo_valor: import_utils3.z.string(),
3470
+ locale: import_utils3.z.string().optional()
3471
+ }),
3472
+ resolveOutputSchema: () => import_utils3.z.object({
3473
+ ok: import_utils3.z.boolean().optional(),
3474
+ uid: import_utils3.z.string().optional(),
3475
+ documentId: import_utils3.z.string().optional(),
3476
+ path: import_utils3.z.array(import_utils3.z.any()).optional(),
3477
+ novo_valor: import_utils3.z.string().optional(),
3478
+ erro: import_utils3.z.string().optional()
3479
+ }),
3480
+ auth: { policies: [{ action: "plugin::content-manager.explorer.update" }] },
3481
+ createHandler: (strapi) => async ({ args }) => {
3482
+ const r = await createContentTools(strapi).editarCampo(args);
3483
+ return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
3310
3484
  }
3311
- };
3312
- var editar_campo_default = tool2;
3485
+ });
3313
3486
 
3314
3487
  // server/src/mcp/tools/publicar.ts
3315
3488
  var import_utils4 = require("@strapi/utils");
3316
- var tool3 = {
3317
- register(registerTool) {
3318
- registerTool({
3319
- name: "mcp_chat_publicar",
3320
- title: "Publish an entry",
3321
- description: 'Publish an entry by uid + documentId, making the change visible on the site. Pass `locale` to publish a specific language, or "*" for all.',
3322
- resolveInputSchema: () => import_utils4.z.object({ uid: import_utils4.z.string(), documentId: import_utils4.z.string(), locale: import_utils4.z.string().optional() }),
3323
- resolveOutputSchema: () => import_utils4.z.object({
3324
- ok: import_utils4.z.boolean().optional(),
3325
- uid: import_utils4.z.string().optional(),
3326
- documentId: import_utils4.z.string().optional(),
3327
- status: import_utils4.z.string().optional(),
3328
- locale: import_utils4.z.string().optional()
3329
- }),
3330
- auth: { policies: [{ action: "plugin::content-manager.explorer.publish" }] },
3331
- createHandler: (strapi) => async ({ args }) => {
3332
- const r = await createContentTools(strapi).publicar(args);
3333
- return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
3334
- }
3335
- });
3489
+ var publicar_default = defineTool({
3490
+ name: "mcp_chat_publicar",
3491
+ title: "Publish an entry",
3492
+ description: 'Publish an entry by uid + documentId, making the change visible on the site. Pass `locale` to publish a specific language, or "*" for all. For content-types without Draft & Publish there is nothing to publish (returns status "no-draft-publish") \u2014 the edit is already live.',
3493
+ resolveInputSchema: () => import_utils4.z.object({ uid: import_utils4.z.string(), documentId: import_utils4.z.string(), locale: import_utils4.z.string().optional() }),
3494
+ resolveOutputSchema: () => import_utils4.z.object({
3495
+ ok: import_utils4.z.boolean().optional(),
3496
+ uid: import_utils4.z.string().optional(),
3497
+ documentId: import_utils4.z.string().optional(),
3498
+ status: import_utils4.z.string().optional(),
3499
+ locale: import_utils4.z.string().optional()
3500
+ }),
3501
+ auth: { policies: [{ action: "plugin::content-manager.explorer.publish" }] },
3502
+ createHandler: (strapi) => async ({ args }) => {
3503
+ const r = await createContentTools(strapi).publicar(args);
3504
+ return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
3336
3505
  }
3337
- };
3338
- var publicar_default = tool3;
3506
+ });
3339
3507
 
3340
3508
  // server/src/mcp/tools/listar-locales.ts
3341
3509
  var import_utils5 = require("@strapi/utils");
3342
- var tool4 = {
3343
- register(registerTool) {
3344
- registerTool({
3345
- name: "mcp_chat_listar_locales",
3346
- title: "List i18n locales",
3347
- description: "List the configured locales (languages) and which one is the default.",
3348
- resolveInputSchema: () => import_utils5.z.object({}),
3349
- resolveOutputSchema: () => import_utils5.z.object({
3350
- default: import_utils5.z.string().optional(),
3351
- locales: import_utils5.z.array(import_utils5.z.any()).optional(),
3352
- erro: import_utils5.z.string().optional()
3353
- }),
3354
- auth: { policies: [{ action: "plugin::content-manager.explorer.read" }] },
3355
- createHandler: (strapi) => async () => {
3356
- const r = await createContentTools(strapi).listarLocales();
3357
- return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
3358
- }
3359
- });
3510
+ var listar_locales_default = defineTool({
3511
+ name: "mcp_chat_listar_locales",
3512
+ title: "List i18n locales",
3513
+ description: "List the configured locales (languages) and which one is the default.",
3514
+ resolveInputSchema: () => import_utils5.z.object({}),
3515
+ resolveOutputSchema: () => import_utils5.z.object({
3516
+ default: import_utils5.z.string().optional(),
3517
+ locales: import_utils5.z.array(import_utils5.z.any()).optional(),
3518
+ erro: import_utils5.z.string().optional()
3519
+ }),
3520
+ auth: { policies: [{ action: "plugin::content-manager.explorer.read" }] },
3521
+ createHandler: (strapi) => async () => {
3522
+ const r = await createContentTools(strapi).listarLocales();
3523
+ return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
3360
3524
  }
3361
- };
3362
- var listar_locales_default = tool4;
3525
+ });
3363
3526
 
3364
3527
  // server/src/mcp/tools/criar-locale.ts
3365
3528
  var import_utils6 = require("@strapi/utils");
3366
- var tool5 = {
3367
- register(registerTool) {
3368
- registerTool({
3369
- name: "mcp_chat_criar_locale",
3370
- title: "Create an i18n locale",
3371
- description: 'Create a locale (language). `code` must be a valid ISO code (e.g. "pt-BR", "es"). Idempotent: returns ok if it already exists.',
3372
- resolveInputSchema: () => import_utils6.z.object({ code: import_utils6.z.string(), name: import_utils6.z.string().optional() }),
3373
- resolveOutputSchema: () => import_utils6.z.object({
3374
- ok: import_utils6.z.boolean().optional(),
3375
- code: import_utils6.z.string().optional(),
3376
- name: import_utils6.z.string().optional(),
3377
- existed: import_utils6.z.boolean().optional(),
3378
- erro: import_utils6.z.string().optional()
3379
- }),
3380
- auth: { policies: [{ action: "plugin::i18n.locale.create" }] },
3381
- createHandler: (strapi) => async ({ args }) => {
3382
- const r = await createContentTools(strapi).criarLocale(args);
3383
- return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
3384
- }
3385
- });
3529
+ var criar_locale_default = defineTool({
3530
+ name: "mcp_chat_criar_locale",
3531
+ title: "Create an i18n locale",
3532
+ description: 'Create a locale (language). `code` must be a valid ISO code (e.g. "pt-BR", "es"). Idempotent: returns ok if it already exists.',
3533
+ resolveInputSchema: () => import_utils6.z.object({ code: import_utils6.z.string(), name: import_utils6.z.string().optional() }),
3534
+ resolveOutputSchema: () => import_utils6.z.object({
3535
+ ok: import_utils6.z.boolean().optional(),
3536
+ code: import_utils6.z.string().optional(),
3537
+ name: import_utils6.z.string().optional(),
3538
+ existed: import_utils6.z.boolean().optional(),
3539
+ erro: import_utils6.z.string().optional()
3540
+ }),
3541
+ auth: { policies: [{ action: "plugin::i18n.locale.create" }] },
3542
+ createHandler: (strapi) => async ({ args }) => {
3543
+ const r = await createContentTools(strapi).criarLocale(args);
3544
+ return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
3386
3545
  }
3387
- };
3388
- var criar_locale_default = tool5;
3546
+ });
3389
3547
 
3390
3548
  // server/src/mcp/tools/traduzir.ts
3391
3549
  var import_utils7 = require("@strapi/utils");
3392
- var tool6 = {
3393
- register(registerTool) {
3394
- registerTool({
3395
- name: "mcp_chat_traduzir",
3396
- title: "Translate localized content",
3397
- description: "Translate localized content into one or more languages. Creates missing locales, translates field by field (long text is split and reassembled, never overflows) and publishes. Without uid/documentId, translates ALL localized content-types. Handles many locales at once.",
3398
- resolveInputSchema: () => import_utils7.z.object({
3399
- target_locales: import_utils7.z.array(import_utils7.z.string()).min(1),
3400
- source_locale: import_utils7.z.string().optional(),
3401
- uid: import_utils7.z.string().optional(),
3402
- documentId: import_utils7.z.string().optional(),
3403
- publish: import_utils7.z.boolean().optional()
3404
- }),
3405
- resolveOutputSchema: () => import_utils7.z.object({
3406
- ok: import_utils7.z.boolean().optional(),
3407
- source: import_utils7.z.string().optional(),
3408
- por_locale: import_utils7.z.array(import_utils7.z.any()).optional(),
3409
- erro: import_utils7.z.string().optional()
3410
- }),
3411
- auth: { policies: [{ action: "plugin::content-manager.explorer.update" }] },
3412
- createHandler: (strapi) => async ({ args }) => {
3413
- const r = await createContentTools(strapi).traduzir(args);
3414
- return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
3415
- }
3416
- });
3550
+ var traduzir_default = defineTool({
3551
+ name: "mcp_chat_traduzir",
3552
+ title: "Translate localized content",
3553
+ description: "Translate localized content into one or more languages. Creates missing locales, translates field by field (long text is split and reassembled, never overflows) and publishes (only on content-types with Draft & Publish). Without uid/documentId, translates ALL localized content-types. Handles many locales at once.",
3554
+ resolveInputSchema: () => import_utils7.z.object({
3555
+ target_locales: import_utils7.z.array(import_utils7.z.string()).min(1),
3556
+ source_locale: import_utils7.z.string().optional(),
3557
+ uid: import_utils7.z.string().optional(),
3558
+ documentId: import_utils7.z.string().optional(),
3559
+ publish: import_utils7.z.boolean().optional()
3560
+ }),
3561
+ resolveOutputSchema: () => import_utils7.z.object({
3562
+ ok: import_utils7.z.boolean().optional(),
3563
+ source: import_utils7.z.string().optional(),
3564
+ por_locale: import_utils7.z.array(import_utils7.z.any()).optional(),
3565
+ erro: import_utils7.z.string().optional()
3566
+ }),
3567
+ auth: { policies: [{ action: "plugin::content-manager.explorer.update" }] },
3568
+ createHandler: (strapi) => async ({ args }) => {
3569
+ const r = await createContentTools(strapi).traduzir(args);
3570
+ return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
3417
3571
  }
3418
- };
3419
- var traduzir_default = tool6;
3572
+ });
3420
3573
 
3421
3574
  // server/src/mcp/tools/habilitar-i18n.ts
3422
3575
  var import_utils8 = require("@strapi/utils");
3423
- var tool7 = {
3424
- register(registerTool) {
3425
- registerTool({
3426
- name: "mcp_chat_habilitar_i18n",
3427
- title: "Enable i18n on a content-type",
3428
- description: 'Enable translation on content-types not localized yet: marks the content-type and its textual fields/components as localized. Required before translating content provisioned without i18n. Omit `uid` (or pass "*") to enable ALL content-types at once. Edits the schema (dev-only); Strapi restarts.',
3429
- resolveInputSchema: () => import_utils8.z.object({ uid: import_utils8.z.string().optional(), campos: import_utils8.z.array(import_utils8.z.string()).optional() }),
3430
- resolveOutputSchema: () => import_utils8.z.object({
3431
- ok: import_utils8.z.boolean().optional(),
3432
- uid: import_utils8.z.string().optional(),
3433
- campos: import_utils8.z.array(import_utils8.z.string()).optional(),
3434
- contentTypes: import_utils8.z.array(import_utils8.z.any()).optional(),
3435
- total: import_utils8.z.number().optional(),
3436
- restart: import_utils8.z.boolean().optional(),
3437
- erro: import_utils8.z.string().optional()
3438
- }),
3439
- auth: { policies: [{ action: "plugin::content-type-builder.read" }] },
3440
- createHandler: (strapi) => async ({ args }) => {
3441
- const r = enableI18n({ strapi, uid: args?.uid, campos: args?.campos });
3442
- return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
3443
- }
3444
- });
3576
+ var habilitar_i18n_default = defineTool({
3577
+ name: "mcp_chat_habilitar_i18n",
3578
+ title: "Enable i18n on a content-type",
3579
+ description: 'Enable translation on content-types not localized yet: marks the content-type and its textual fields/components as localized. Required before translating content provisioned without i18n. Omit `uid` (or pass "*") to enable ALL content-types at once. Edits the schema (dev-only); Strapi restarts.',
3580
+ resolveInputSchema: () => import_utils8.z.object({ uid: import_utils8.z.string().optional(), campos: import_utils8.z.array(import_utils8.z.string()).optional() }),
3581
+ resolveOutputSchema: () => import_utils8.z.object({
3582
+ ok: import_utils8.z.boolean().optional(),
3583
+ uid: import_utils8.z.string().optional(),
3584
+ campos: import_utils8.z.array(import_utils8.z.string()).optional(),
3585
+ contentTypes: import_utils8.z.array(import_utils8.z.any()).optional(),
3586
+ total: import_utils8.z.number().optional(),
3587
+ restart: import_utils8.z.boolean().optional(),
3588
+ erro: import_utils8.z.string().optional()
3589
+ }),
3590
+ auth: { policies: [{ action: "plugin::content-type-builder.read" }] },
3591
+ createHandler: (strapi) => async ({ args }) => {
3592
+ const r = enableI18n({ strapi, uid: args.uid, campos: args.campos });
3593
+ return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
3445
3594
  }
3446
- };
3447
- var habilitar_i18n_default = tool7;
3595
+ });
3448
3596
 
3449
3597
  // server/src/mcp/tools/index.ts
3450
3598
  var tools = [
@@ -3467,9 +3615,16 @@ var registerMcpTools = (strapi) => {
3467
3615
  );
3468
3616
  return;
3469
3617
  }
3470
- const { registerTool } = mcp;
3471
- for (const tool8 of tools) tool8.register(registerTool, strapi);
3472
- strapi.log.info(`[mcp-chat] ${tools.length} tools registradas no MCP nativo (mcp_chat_*).`);
3618
+ let registered = 0;
3619
+ for (const tool of tools) {
3620
+ try {
3621
+ mcp.registerTool(tool);
3622
+ registered += 1;
3623
+ } catch (e) {
3624
+ strapi.log.warn(`[mcp-chat] tool "${tool?.name}" falhou ao registrar: ${e?.message ?? e}`);
3625
+ }
3626
+ }
3627
+ strapi.log.info(`[mcp-chat] ${registered}/${tools.length} tools registradas no MCP nativo (mcp_chat_*).`);
3473
3628
  };
3474
3629
 
3475
3630
  // server/src/register.ts
@@ -3498,7 +3653,10 @@ var index_default = {
3498
3653
  }
3499
3654
  },
3500
3655
  destroy() {
3501
- stopFrontend();
3656
+ try {
3657
+ stopFrontend();
3658
+ } catch {
3659
+ }
3502
3660
  },
3503
3661
  config: {
3504
3662
  default: {},