strapi-plugin-mcp-chat 0.3.1 → 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.
@@ -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)) {
@@ -2088,8 +2181,11 @@ async function integrateFrontend(strapi, opts) {
2088
2181
  // server/src/controllers/frontend.ts
2089
2182
  var MANIFEST_NAME = "strapi.manifest.json";
2090
2183
  function ensureInside2(base, target) {
2091
- const n = import_node_path7.default.normalize(target);
2092
- 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);
2093
2189
  }
2094
2190
  function toKebab(input) {
2095
2191
  const s = (input || "frontend").toLowerCase().replace(/\.zip$/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/^[^a-z]+/, "");
@@ -2311,6 +2407,15 @@ var baseHeaders = {
2311
2407
  "Content-Type": "application/json",
2312
2408
  Accept: "application/json, text/event-stream"
2313
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
+ };
2314
2419
  var parseSse = (text) => {
2315
2420
  const dataLines = text.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trim()).filter(Boolean);
2316
2421
  const last = dataLines[dataLines.length - 1];
@@ -2334,7 +2439,7 @@ var McpClient = class {
2334
2439
  return h;
2335
2440
  }
2336
2441
  async init() {
2337
- const res = await fetch(this.url, {
2442
+ const res = await fetchT(this.url, {
2338
2443
  method: "POST",
2339
2444
  headers: this.headers(),
2340
2445
  body: JSON.stringify({
@@ -2350,14 +2455,14 @@ var McpClient = class {
2350
2455
  });
2351
2456
  this.sessionId = res.headers.get("mcp-session-id") || void 0;
2352
2457
  await res.text();
2353
- await fetch(this.url, {
2458
+ await fetchT(this.url, {
2354
2459
  method: "POST",
2355
2460
  headers: this.headers(),
2356
2461
  body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })
2357
2462
  });
2358
2463
  }
2359
2464
  async rpc(method, params, id) {
2360
- const res = await fetch(this.url, {
2465
+ const res = await fetchT(this.url, {
2361
2466
  method: "POST",
2362
2467
  headers: this.headers(),
2363
2468
  body: JSON.stringify({ jsonrpc: "2.0", id, method, params })
@@ -2473,16 +2578,18 @@ function createContentTools(strapi) {
2473
2578
  );
2474
2579
  const attrsOf = (uid) => strapi.contentTypes?.[uid]?.attributes || strapi.components?.[uid]?.attributes || {};
2475
2580
  const hasDraftAndPublish = (uid) => strapi.contentTypes?.[uid]?.options?.draftAndPublish === true;
2476
- const buildPopulate = (attributes, seen = /* @__PURE__ */ new Set()) => {
2581
+ const MAX_DEPTH = 8;
2582
+ const buildPopulate = (attributes, seen = /* @__PURE__ */ new Set(), depth = 0) => {
2583
+ if (depth >= MAX_DEPTH) return {};
2477
2584
  const populate = {};
2478
2585
  for (const [name, a] of Object.entries(attributes)) {
2479
2586
  if (a.type === "component" && a.component) {
2480
- 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);
2481
2588
  populate[name] = Object.keys(sub).length ? { populate: sub } : true;
2482
2589
  } else if (a.type === "dynamiczone") {
2483
2590
  const on = {};
2484
2591
  for (const comp of a.components || []) {
2485
- 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);
2486
2593
  on[comp] = Object.keys(sub).length ? { populate: sub } : true;
2487
2594
  }
2488
2595
  populate[name] = { on };
@@ -2494,6 +2601,7 @@ function createContentTools(strapi) {
2494
2601
  };
2495
2602
  const walkFind = (node, attributes, basePath, needle, collect) => {
2496
2603
  if (!node || typeof node !== "object") return;
2604
+ if (basePath.length > 24) return;
2497
2605
  for (const [name, a] of Object.entries(attributes)) {
2498
2606
  const v = node[name];
2499
2607
  if (v == null) continue;
@@ -2518,11 +2626,13 @@ function createContentTools(strapi) {
2518
2626
  }
2519
2627
  }
2520
2628
  };
2629
+ const MAX_MATCHES = 100;
2521
2630
  const buscarTexto = async (termo) => {
2522
2631
  const needle = String(termo || "").toLowerCase().trim();
2523
2632
  if (!needle) return { erro: "termo vazio" };
2524
2633
  const matches = [];
2525
2634
  for (const ct of apiContentTypes()) {
2635
+ if (matches.length >= MAX_MATCHES) break;
2526
2636
  const attributes = ct.attributes || {};
2527
2637
  const populate = buildPopulate(attributes);
2528
2638
  let entries = [];
@@ -2549,7 +2659,12 @@ function createContentTools(strapi) {
2549
2659
  });
2550
2660
  }
2551
2661
  }
2552
- 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
+ };
2553
2668
  };
2554
2669
  const sanitizeNode = (node, attributes) => {
2555
2670
  if (node == null) return node;
@@ -3061,7 +3176,10 @@ var chat_default2 = ({ strapi }) => ({
3061
3176
  if (process.env.PLAYWRIGHT_MCP_URL) {
3062
3177
  try {
3063
3178
  const client = new McpClient(process.env.PLAYWRIGHT_MCP_URL, "playwright");
3064
- await client.init();
3179
+ await Promise.race([
3180
+ client.init(),
3181
+ new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), 4e3))
3182
+ ]);
3065
3183
  const list = await client.listTools();
3066
3184
  for (const t of list) {
3067
3185
  if (mcpByTool[t.name]) continue;
@@ -3133,11 +3251,22 @@ PUBLISH POLICY: DRAFT MODE (auto-publish OFF). Do NOT call publicar unless the u
3133
3251
  }
3134
3252
  });
3135
3253
  const callOpenAI = async (body) => {
3136
- const res = await fetch(OPENAI_URL4, {
3137
- method: "POST",
3138
- headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
3139
- body: JSON.stringify(body)
3140
- });
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
+ }
3141
3270
  if (!res.ok) throw new Error(`OpenAI chat: ${await res.text()}`);
3142
3271
  return res.json();
3143
3272
  };
@@ -3176,6 +3305,11 @@ PUBLISH POLICY: DRAFT MODE (auto-publish OFF). Do NOT call publicar unless the u
3176
3305
  } catch (e) {
3177
3306
  content = `Erro ao chamar a tool ${name}: ${e?.message || e}`;
3178
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
+ }
3179
3313
  convo.push({ role: "tool", tool_call_id: call.id, content });
3180
3314
  }
3181
3315
  continue;
@@ -3299,196 +3433,166 @@ var routes_default = {
3299
3433
 
3300
3434
  // server/src/mcp/tools/buscar-texto.ts
3301
3435
  var import_utils2 = require("@strapi/utils");
3302
- var tool = {
3303
- register(registerTool) {
3304
- registerTool({
3305
- name: "mcp_chat_buscar_texto",
3306
- title: "Search text across content (deep)",
3307
- 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.',
3308
- resolveInputSchema: () => import_utils2.z.object({ termo: import_utils2.z.string() }),
3309
- resolveOutputSchema: () => import_utils2.z.object({
3310
- total: import_utils2.z.number().optional(),
3311
- resultados: import_utils2.z.array(import_utils2.z.any()).optional(),
3312
- erro: import_utils2.z.string().optional()
3313
- }),
3314
- auth: { policies: [{ action: "plugin::content-manager.explorer.read" }] },
3315
- createHandler: (strapi) => async ({ args }) => {
3316
- const r = await createContentTools(strapi).buscarTexto(args?.termo);
3317
- return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
3318
- }
3319
- });
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 };
3320
3455
  }
3321
- };
3322
- var buscar_texto_default = tool;
3456
+ });
3323
3457
 
3324
3458
  // server/src/mcp/tools/editar-campo.ts
3325
3459
  var import_utils3 = require("@strapi/utils");
3326
- var tool2 = {
3327
- register(registerTool) {
3328
- registerTool({
3329
- name: "mcp_chat_editar_campo",
3330
- title: "Edit a (possibly nested) field",
3331
- 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`.",
3332
- resolveInputSchema: () => import_utils3.z.object({
3333
- uid: import_utils3.z.string(),
3334
- documentId: import_utils3.z.string(),
3335
- path: import_utils3.z.array(import_utils3.z.union([import_utils3.z.string(), import_utils3.z.number()])).optional(),
3336
- campo: import_utils3.z.string().optional(),
3337
- novo_valor: import_utils3.z.string(),
3338
- locale: import_utils3.z.string().optional()
3339
- }),
3340
- resolveOutputSchema: () => import_utils3.z.object({
3341
- ok: import_utils3.z.boolean().optional(),
3342
- uid: import_utils3.z.string().optional(),
3343
- documentId: import_utils3.z.string().optional(),
3344
- path: import_utils3.z.array(import_utils3.z.any()).optional(),
3345
- novo_valor: import_utils3.z.string().optional(),
3346
- erro: import_utils3.z.string().optional()
3347
- }),
3348
- auth: { policies: [{ action: "plugin::content-manager.explorer.update" }] },
3349
- createHandler: (strapi) => async ({ args }) => {
3350
- const r = await createContentTools(strapi).editarCampo(args);
3351
- return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
3352
- }
3353
- });
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 };
3354
3484
  }
3355
- };
3356
- var editar_campo_default = tool2;
3485
+ });
3357
3486
 
3358
3487
  // server/src/mcp/tools/publicar.ts
3359
3488
  var import_utils4 = require("@strapi/utils");
3360
- var tool3 = {
3361
- register(registerTool) {
3362
- registerTool({
3363
- name: "mcp_chat_publicar",
3364
- title: "Publish an entry",
3365
- description: 'Publish an entry by uid + documentId, making the change visible on the site. Pass `locale` to publish a specific language, or "*" for all.',
3366
- resolveInputSchema: () => import_utils4.z.object({ uid: import_utils4.z.string(), documentId: import_utils4.z.string(), locale: import_utils4.z.string().optional() }),
3367
- resolveOutputSchema: () => import_utils4.z.object({
3368
- ok: import_utils4.z.boolean().optional(),
3369
- uid: import_utils4.z.string().optional(),
3370
- documentId: import_utils4.z.string().optional(),
3371
- status: import_utils4.z.string().optional(),
3372
- locale: import_utils4.z.string().optional()
3373
- }),
3374
- auth: { policies: [{ action: "plugin::content-manager.explorer.publish" }] },
3375
- createHandler: (strapi) => async ({ args }) => {
3376
- const r = await createContentTools(strapi).publicar(args);
3377
- return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
3378
- }
3379
- });
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 };
3380
3505
  }
3381
- };
3382
- var publicar_default = tool3;
3506
+ });
3383
3507
 
3384
3508
  // server/src/mcp/tools/listar-locales.ts
3385
3509
  var import_utils5 = require("@strapi/utils");
3386
- var tool4 = {
3387
- register(registerTool) {
3388
- registerTool({
3389
- name: "mcp_chat_listar_locales",
3390
- title: "List i18n locales",
3391
- description: "List the configured locales (languages) and which one is the default.",
3392
- resolveInputSchema: () => import_utils5.z.object({}),
3393
- resolveOutputSchema: () => import_utils5.z.object({
3394
- default: import_utils5.z.string().optional(),
3395
- locales: import_utils5.z.array(import_utils5.z.any()).optional(),
3396
- erro: import_utils5.z.string().optional()
3397
- }),
3398
- auth: { policies: [{ action: "plugin::content-manager.explorer.read" }] },
3399
- createHandler: (strapi) => async () => {
3400
- const r = await createContentTools(strapi).listarLocales();
3401
- return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
3402
- }
3403
- });
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 };
3404
3524
  }
3405
- };
3406
- var listar_locales_default = tool4;
3525
+ });
3407
3526
 
3408
3527
  // server/src/mcp/tools/criar-locale.ts
3409
3528
  var import_utils6 = require("@strapi/utils");
3410
- var tool5 = {
3411
- register(registerTool) {
3412
- registerTool({
3413
- name: "mcp_chat_criar_locale",
3414
- title: "Create an i18n locale",
3415
- description: 'Create a locale (language). `code` must be a valid ISO code (e.g. "pt-BR", "es"). Idempotent: returns ok if it already exists.',
3416
- resolveInputSchema: () => import_utils6.z.object({ code: import_utils6.z.string(), name: import_utils6.z.string().optional() }),
3417
- resolveOutputSchema: () => import_utils6.z.object({
3418
- ok: import_utils6.z.boolean().optional(),
3419
- code: import_utils6.z.string().optional(),
3420
- name: import_utils6.z.string().optional(),
3421
- existed: import_utils6.z.boolean().optional(),
3422
- erro: import_utils6.z.string().optional()
3423
- }),
3424
- auth: { policies: [{ action: "plugin::i18n.locale.create" }] },
3425
- createHandler: (strapi) => async ({ args }) => {
3426
- const r = await createContentTools(strapi).criarLocale(args);
3427
- return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
3428
- }
3429
- });
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 };
3430
3545
  }
3431
- };
3432
- var criar_locale_default = tool5;
3546
+ });
3433
3547
 
3434
3548
  // server/src/mcp/tools/traduzir.ts
3435
3549
  var import_utils7 = require("@strapi/utils");
3436
- var tool6 = {
3437
- register(registerTool) {
3438
- registerTool({
3439
- name: "mcp_chat_traduzir",
3440
- title: "Translate localized content",
3441
- 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.",
3442
- resolveInputSchema: () => import_utils7.z.object({
3443
- target_locales: import_utils7.z.array(import_utils7.z.string()).min(1),
3444
- source_locale: import_utils7.z.string().optional(),
3445
- uid: import_utils7.z.string().optional(),
3446
- documentId: import_utils7.z.string().optional(),
3447
- publish: import_utils7.z.boolean().optional()
3448
- }),
3449
- resolveOutputSchema: () => import_utils7.z.object({
3450
- ok: import_utils7.z.boolean().optional(),
3451
- source: import_utils7.z.string().optional(),
3452
- por_locale: import_utils7.z.array(import_utils7.z.any()).optional(),
3453
- erro: import_utils7.z.string().optional()
3454
- }),
3455
- auth: { policies: [{ action: "plugin::content-manager.explorer.update" }] },
3456
- createHandler: (strapi) => async ({ args }) => {
3457
- const r = await createContentTools(strapi).traduzir(args);
3458
- return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
3459
- }
3460
- });
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 };
3461
3571
  }
3462
- };
3463
- var traduzir_default = tool6;
3572
+ });
3464
3573
 
3465
3574
  // server/src/mcp/tools/habilitar-i18n.ts
3466
3575
  var import_utils8 = require("@strapi/utils");
3467
- var tool7 = {
3468
- register(registerTool) {
3469
- registerTool({
3470
- name: "mcp_chat_habilitar_i18n",
3471
- title: "Enable i18n on a content-type",
3472
- 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.',
3473
- resolveInputSchema: () => import_utils8.z.object({ uid: import_utils8.z.string().optional(), campos: import_utils8.z.array(import_utils8.z.string()).optional() }),
3474
- resolveOutputSchema: () => import_utils8.z.object({
3475
- ok: import_utils8.z.boolean().optional(),
3476
- uid: import_utils8.z.string().optional(),
3477
- campos: import_utils8.z.array(import_utils8.z.string()).optional(),
3478
- contentTypes: import_utils8.z.array(import_utils8.z.any()).optional(),
3479
- total: import_utils8.z.number().optional(),
3480
- restart: import_utils8.z.boolean().optional(),
3481
- erro: import_utils8.z.string().optional()
3482
- }),
3483
- auth: { policies: [{ action: "plugin::content-type-builder.read" }] },
3484
- createHandler: (strapi) => async ({ args }) => {
3485
- const r = enableI18n({ strapi, uid: args?.uid, campos: args?.campos });
3486
- return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
3487
- }
3488
- });
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 };
3489
3594
  }
3490
- };
3491
- var habilitar_i18n_default = tool7;
3595
+ });
3492
3596
 
3493
3597
  // server/src/mcp/tools/index.ts
3494
3598
  var tools = [
@@ -3511,9 +3615,16 @@ var registerMcpTools = (strapi) => {
3511
3615
  );
3512
3616
  return;
3513
3617
  }
3514
- const { registerTool } = mcp;
3515
- for (const tool8 of tools) tool8.register(registerTool, strapi);
3516
- 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_*).`);
3517
3628
  };
3518
3629
 
3519
3630
  // server/src/register.ts
@@ -3542,7 +3653,10 @@ var index_default = {
3542
3653
  }
3543
3654
  },
3544
3655
  destroy() {
3545
- stopFrontend();
3656
+ try {
3657
+ stopFrontend();
3658
+ } catch {
3659
+ }
3546
3660
  },
3547
3661
  config: {
3548
3662
  default: {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strapi-plugin-mcp-chat",
3
- "version": "0.3.1",
3
+ "version": "0.5.0",
4
4
  "description": "AI chat inside the Strapi 5 admin that reads and edits your content (incl. components & dynamic zones) via MCP, with voice and a side-by-side live preview.",
5
5
  "keywords": [
6
6
  "strapi",