strapi-plugin-mcp-chat 0.3.1 → 0.6.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, autoPublish } = ctx.request.body || {};
39
+ const { messages, image, lang, previewUrl, previewStatus, 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, autoPublish });
44
+ const result = await strapi.plugin("mcp-chat").service("chat").chat({ messages, image, lang, previewUrl, previewStatus, autoPublish });
45
45
  ctx.body = result;
46
46
  } catch (e) {
47
47
  strapi.log.error(`[mcp-chat] ${e?.message || e}`);
@@ -88,8 +88,8 @@ var import_node_path7 = __toESM(require("node:path"));
88
88
  var import_jszip = __toESM(require("jszip"));
89
89
 
90
90
  // server/src/provision/orchestrate.ts
91
- var import_node_fs3 = __toESM(require("node:fs"));
92
- var import_node_path3 = __toESM(require("node:path"));
91
+ var import_node_fs4 = __toESM(require("node:fs"));
92
+ var import_node_path4 = __toESM(require("node:path"));
93
93
 
94
94
  // server/src/provision/manifest.ts
95
95
  var import_utils = require("@strapi/utils");
@@ -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)) {
@@ -540,8 +633,8 @@ async function seedContent(strapi, manifest) {
540
633
  }
541
634
 
542
635
  // server/src/provision/link.ts
543
- var import_node_fs2 = __toESM(require("node:fs"));
544
- var import_node_path2 = __toESM(require("node:path"));
636
+ var import_node_fs3 = __toESM(require("node:fs"));
637
+ var import_node_path3 = __toESM(require("node:path"));
545
638
 
546
639
  // server/src/provision/adapters.ts
547
640
  var nextAdapter = {
@@ -677,6 +770,179 @@ ${interfaces}
677
770
  `;
678
771
  }
679
772
 
773
+ // server/src/provision/runner.ts
774
+ var import_node_child_process = require("node:child_process");
775
+ var import_node_net = __toESM(require("node:net"));
776
+ var import_node_fs2 = __toESM(require("node:fs"));
777
+ var import_node_path2 = __toESM(require("node:path"));
778
+ var info = { state: "idle", dir: null, url: null, pm: null, error: null, log: [] };
779
+ var child = null;
780
+ var pollTimer = null;
781
+ var appRootForPid = null;
782
+ function pidFilePath(appRoot) {
783
+ return import_node_path2.default.join(appRoot, ".mcp-chat", "frontend.pid");
784
+ }
785
+ function writePid(pid) {
786
+ try {
787
+ if (!appRootForPid) return;
788
+ const pf = pidFilePath(appRootForPid);
789
+ import_node_fs2.default.mkdirSync(import_node_path2.default.dirname(pf), { recursive: true });
790
+ import_node_fs2.default.writeFileSync(pf, String(pid), "utf8");
791
+ } catch {
792
+ }
793
+ }
794
+ function clearPid() {
795
+ try {
796
+ if (appRootForPid) import_node_fs2.default.unlinkSync(pidFilePath(appRootForPid));
797
+ } catch {
798
+ }
799
+ }
800
+ function isAlive(pid) {
801
+ try {
802
+ process.kill(pid, 0);
803
+ return true;
804
+ } catch {
805
+ return false;
806
+ }
807
+ }
808
+ function cleanupStaleFrontend(appRoot) {
809
+ appRootForPid = appRoot;
810
+ try {
811
+ const pf = pidFilePath(appRoot);
812
+ if (!import_node_fs2.default.existsSync(pf)) return;
813
+ const pid = parseInt(import_node_fs2.default.readFileSync(pf, "utf8").trim(), 10);
814
+ if (pid && isAlive(pid)) {
815
+ try {
816
+ process.kill(pid, "SIGTERM");
817
+ } catch {
818
+ }
819
+ }
820
+ import_node_fs2.default.unlinkSync(pf);
821
+ } catch {
822
+ }
823
+ }
824
+ function detectPM(dir) {
825
+ if (import_node_fs2.default.existsSync(import_node_path2.default.join(dir, "bun.lockb")) || import_node_fs2.default.existsSync(import_node_path2.default.join(dir, "bun.lock"))) return "bun";
826
+ if (import_node_fs2.default.existsSync(import_node_path2.default.join(dir, "pnpm-lock.yaml"))) return "pnpm";
827
+ if (import_node_fs2.default.existsSync(import_node_path2.default.join(dir, "yarn.lock"))) return "yarn";
828
+ return "npm";
829
+ }
830
+ var has = (dir, ...names) => names.some((n) => import_node_fs2.default.existsSync(import_node_path2.default.join(dir, n)));
831
+ function detectFramework(dir) {
832
+ if (has(dir, "next.config.js", "next.config.ts", "next.config.mjs")) return "next";
833
+ if (has(dir, "vite.config.js", "vite.config.ts", "vite.config.mjs")) return "vite";
834
+ return "other";
835
+ }
836
+ var FRONTEND_BASE_PORT = 4321;
837
+ function findFreePort(start) {
838
+ return new Promise((resolve) => {
839
+ const tryPort = (p) => {
840
+ if (p > start + 200) return resolve(start);
841
+ const srv = import_node_net.default.createServer();
842
+ srv.once("error", () => tryPort(p + 1));
843
+ srv.once("listening", () => srv.close(() => resolve(p)));
844
+ srv.listen(p, "0.0.0.0");
845
+ };
846
+ tryPort(start);
847
+ });
848
+ }
849
+ function pushLog(s) {
850
+ for (const line of String(s).split("\n")) {
851
+ const t = line.trim();
852
+ if (t) info.log.push(t);
853
+ }
854
+ if (info.log.length > 60) info.log = info.log.slice(-60);
855
+ }
856
+ async function urlUp(url) {
857
+ try {
858
+ const res = await fetch(url, { method: "GET" });
859
+ return res.status >= 200 && res.status < 400;
860
+ } catch {
861
+ return false;
862
+ }
863
+ }
864
+ function getRunStatus() {
865
+ return { ...info, log: info.log.slice(-15) };
866
+ }
867
+ function stopFrontend() {
868
+ if (pollTimer) {
869
+ clearInterval(pollTimer);
870
+ pollTimer = null;
871
+ }
872
+ if (child) {
873
+ try {
874
+ child.kill("SIGTERM");
875
+ } catch {
876
+ }
877
+ child = null;
878
+ }
879
+ clearPid();
880
+ if (info.state !== "error") info.state = "idle";
881
+ }
882
+ async function startFrontend(_strapi, opts) {
883
+ const { dir } = opts;
884
+ if (_strapi?.dirs?.app?.root) appRootForPid = _strapi.dirs.app.root;
885
+ if (child && info.dir === dir && ["installing", "starting", "running"].includes(info.state)) {
886
+ return getRunStatus();
887
+ }
888
+ stopFrontend();
889
+ const pm = detectPM(dir);
890
+ const framework = detectFramework(dir);
891
+ const port = await findFreePort(FRONTEND_BASE_PORT);
892
+ const url = `http://127.0.0.1:${port}`;
893
+ info = { state: "installing", dir, url, pm, error: null, log: [] };
894
+ const spawnIn = (cmd, args) => (0, import_node_child_process.spawn)(cmd, args, { cwd: dir, env: { ...process.env }, stdio: ["ignore", "pipe", "pipe"] });
895
+ const fwArgs = framework === "next" ? ["-H", "127.0.0.1", "-p", String(port)] : framework === "vite" ? ["--host", "127.0.0.1", "--port", String(port), "--strictPort"] : ["--port", String(port)];
896
+ const devArgs = pm === "yarn" ? ["dev", ...fwArgs] : ["run", "dev", "--", ...fwArgs];
897
+ const startDev = () => {
898
+ info.state = "starting";
899
+ child = spawnIn(pm, devArgs);
900
+ if (child.pid) writePid(child.pid);
901
+ child.stdout?.on("data", (d) => pushLog(d));
902
+ child.stderr?.on("data", (d) => pushLog(d));
903
+ child.on("exit", (code) => {
904
+ child = null;
905
+ clearPid();
906
+ if (pollTimer) {
907
+ clearInterval(pollTimer);
908
+ pollTimer = null;
909
+ }
910
+ if (info.state === "running") info.state = "idle";
911
+ else {
912
+ info.state = "error";
913
+ info.error = `dev encerrou (c\xF3digo ${code}). Veja o log.`;
914
+ }
915
+ });
916
+ pollTimer = setInterval(async () => {
917
+ if (await urlUp(url)) {
918
+ info.state = "running";
919
+ if (pollTimer) {
920
+ clearInterval(pollTimer);
921
+ pollTimer = null;
922
+ }
923
+ }
924
+ }, 1500);
925
+ };
926
+ const needInstall = !import_node_fs2.default.existsSync(import_node_path2.default.join(dir, "node_modules"));
927
+ if (needInstall) {
928
+ pushLog(`Instalando depend\xEAncias com ${pm}\u2026`);
929
+ const installArgs = pm === "npm" ? ["install", "--no-audit", "--no-fund"] : ["install"];
930
+ const inst = spawnIn(pm, installArgs);
931
+ inst.stdout?.on("data", (d) => pushLog(d));
932
+ inst.stderr?.on("data", (d) => pushLog(d));
933
+ inst.on("exit", (code) => {
934
+ if (code === 0) startDev();
935
+ else {
936
+ info.state = "error";
937
+ info.error = `instala\xE7\xE3o falhou (c\xF3digo ${code}). Veja o log.`;
938
+ }
939
+ });
940
+ } else {
941
+ startDev();
942
+ }
943
+ return getRunStatus();
944
+ }
945
+
680
946
  // server/src/provision/link.ts
681
947
  function parseEnv(content) {
682
948
  const out = {};
@@ -706,48 +972,120 @@ function mergeEnv(existing, next) {
706
972
  }
707
973
  return { content, added, preserved };
708
974
  }
709
- function buildPreviewConfig(manifest) {
975
+ var PREVIEW_MARKER = "mcp-chat:preview-merged";
976
+ var PREVIEW_MODULE = "mcp-chat-preview";
977
+ function buildPreviewConfig(manifest, framework = manifest.framework) {
710
978
  const routes = {};
711
979
  for (const ct of manifest.contentTypes) {
712
- if (ct.preview?.route) routes[apiUid(ct.singularName)] = ct.preview.route;
980
+ routes[apiUid(ct.singularName)] = ct.preview?.route ?? "/";
713
981
  }
714
982
  const routesJson = JSON.stringify(routes, null, 2);
715
- return `// Preview gerado pelo mcp-chat a partir do strapi.manifest.json.
983
+ const isNext = framework === "next";
984
+ const urlBranch = isNext ? ` // Next.js: rota de draft mode que seta o cookie e redireciona p/ \`path\`.
985
+ const qs = new URLSearchParams({ secret, status: status ?? 'draft', path: pathname });
986
+ return \`\${clientUrl}/api/preview?\${qs.toString()}\`;` : ` // SPA (Vite/TanStack): abre a p\xE1gina direta; o front l\xEA ?preview/status.
987
+ const qs = new URLSearchParams({ preview: '1', status: status ?? 'draft', secret });
988
+ return \`\${clientUrl}\${pathname}?\${qs.toString()}\`;`;
989
+ return `// Preview gerado pelo mcp-chat a partir do strapi.manifest.json (framework: ${framework}).
716
990
  // Mapa uid -> rota do frontend (placeholders :campo s\xE3o preenchidos pelo doc).
991
+ // Este arquivo \xE9 mesclado em config/admin.ts \u2014 n\xE3o precisa edit\xE1-lo \xE0 m\xE3o.
717
992
  const PREVIEW_ROUTES: Record<string, string> = ${routesJson};
718
993
 
719
- export default ({ env }) => ({
720
- auth: {
721
- secret: env('ADMIN_JWT_SECRET'),
722
- },
723
- apiToken: { salt: env('API_TOKEN_SALT') },
724
- transfer: { token: { salt: env('TRANSFER_TOKEN_SALT') } },
994
+ export default ({ env }: { env: any }) => ({
725
995
  preview: {
726
996
  enabled: true,
727
997
  config: {
728
998
  allowedOrigins: [env('CLIENT_URL', 'http://localhost:3000')],
729
999
  async handler(uid: string, { documentId, locale, status }: any) {
730
- const route = PREVIEW_ROUTES[uid];
731
- if (!route) return null;
732
- const doc = await strapi.documents(uid as any).findOne({ documentId, locale });
733
- if (!doc) return null;
734
- // substitui :campo pelos valores do documento (ex.: :slug)
735
- const pathname = route.replace(/:([a-zA-Z0-9_]+)/g, (_m, f) =>
736
- encodeURIComponent(String((doc as any)[f] ?? ''))
737
- );
1000
+ const route = PREVIEW_ROUTES[uid] ?? '/';
738
1001
  const clientUrl = env('CLIENT_URL', 'http://localhost:3000');
739
1002
  const secret = env('PREVIEW_SECRET', '');
740
- const qs = new URLSearchParams({ secret, status: status ?? 'draft', path: pathname });
741
- return \`\${clientUrl}/api/preview?\${qs.toString()}\`;
1003
+
1004
+ // s\xF3 busca o doc se a rota tiver placeholders (ex.: :slug) a preencher.
1005
+ let pathname = route;
1006
+ if (pathname.includes(':')) {
1007
+ const doc = await strapi.documents(uid as any).findOne({ documentId, locale });
1008
+ if (!doc) return null;
1009
+ pathname = pathname.replace(/:([a-zA-Z0-9_]+)/g, (_m, f) =>
1010
+ encodeURIComponent(String((doc as any)[f] ?? ''))
1011
+ );
1012
+ }
1013
+
1014
+ ${urlBranch}
742
1015
  },
743
1016
  },
744
1017
  },
745
1018
  });
746
1019
  `;
747
1020
  }
1021
+ function buildStandaloneAdmin() {
1022
+ return `// ${PREVIEW_MARKER} \u2014 admin.ts gerado pelo mcp-chat (preview inclu\xEDdo).
1023
+ import previewConfig from './${PREVIEW_MODULE}';
1024
+
1025
+ export default ({ env }: { env: any }) => ({
1026
+ auth: { secret: env('ADMIN_JWT_SECRET') },
1027
+ apiToken: { salt: env('API_TOKEN_SALT') },
1028
+ transfer: { token: { salt: env('TRANSFER_TOKEN_SALT') } },
1029
+ secrets: { encryptionKey: env('ENCRYPTION_KEY') },
1030
+ flags: {
1031
+ nps: env.bool('FLAG_NPS', true),
1032
+ promoteEE: env.bool('FLAG_PROMOTE_EE', true),
1033
+ },
1034
+ ...previewConfig({ env }),
1035
+ });
1036
+ `;
1037
+ }
1038
+ function buildAdminWrapper() {
1039
+ return `// ${PREVIEW_MARKER} \u2014 preview do mcp-chat mesclado sobre o admin original.
1040
+ // Sua config original est\xE1 preservada em ./admin.base \u2014 edite l\xE1, n\xE3o aqui.
1041
+ import base from './admin.base';
1042
+ import previewConfig from './${PREVIEW_MODULE}';
1043
+
1044
+ export default (ctx: any) => {
1045
+ const b = typeof base === 'function' ? (base as any)(ctx) : (base ?? {});
1046
+ return { ...b, ...previewConfig(ctx) };
1047
+ };
1048
+ `;
1049
+ }
1050
+ var CSP_MARKER = "mcp-chat:csp-frame";
1051
+ function securityBlock() {
1052
+ return ` // ${CSP_MARKER} \u2014 libera o frame-src p/ o admin embutir o preview do frontend
1053
+ // (dev server local em qualquer porta). Sem isto a CSP padr\xE3o (default-src 'self')
1054
+ // bloqueia o iframe e o preview fica em branco.
1055
+ {
1056
+ name: 'strapi::security',
1057
+ config: {
1058
+ contentSecurityPolicy: {
1059
+ useDefaults: true,
1060
+ directives: {
1061
+ 'connect-src': ["'self'", 'https:', 'http:'],
1062
+ 'frame-src': ["'self'", 'http://localhost:*', 'http://127.0.0.1:*'],
1063
+ 'img-src': ["'self'", 'data:', 'blob:', 'market-assets.strapi.io'],
1064
+ 'media-src': ["'self'", 'data:', 'blob:'],
1065
+ upgradeInsecureRequests: null,
1066
+ },
1067
+ },
1068
+ },
1069
+ },`;
1070
+ }
1071
+ function patchSecurityMiddleware(strapiAppDir, dryRun) {
1072
+ const configDir = import_node_path3.default.join(strapiAppDir, "config");
1073
+ const file = ["middlewares.ts", "middlewares.js"].find(
1074
+ (f) => import_node_fs3.default.existsSync(import_node_path3.default.join(configDir, f))
1075
+ );
1076
+ if (!file) return "skipped";
1077
+ const p = import_node_path3.default.join(configDir, file);
1078
+ const content = import_node_fs3.default.readFileSync(p, "utf8");
1079
+ if (content.includes(CSP_MARKER) || content.includes("'frame-src'")) return "already";
1080
+ const m = content.match(/^[ \t]*['"]strapi::security['"]\s*,/m);
1081
+ if (!m) return "manual";
1082
+ const next = content.replace(m[0], securityBlock());
1083
+ if (!dryRun) import_node_fs3.default.writeFileSync(p, next, "utf8");
1084
+ return "patched";
1085
+ }
748
1086
  function ensureInside(base, target) {
749
- const n = import_node_path2.default.normalize(target);
750
- return n === base || n.startsWith(base + import_node_path2.default.sep);
1087
+ const n = import_node_path3.default.normalize(target);
1088
+ return n === base || n.startsWith(base + import_node_path3.default.sep);
751
1089
  }
752
1090
  function linkFrontend(manifest, opts) {
753
1091
  const adapter = adapterForManifest(manifest);
@@ -759,49 +1097,90 @@ function linkFrontend(manifest, opts) {
759
1097
  typesFile: "strapi-types.ts",
760
1098
  previewFile: "config/admin.ts",
761
1099
  previewAction: "skipped",
1100
+ backendEnvAdded: [],
1101
+ cspAction: "skipped",
762
1102
  errors: []
763
1103
  };
764
- if (!import_node_path2.default.isAbsolute(opts.frontendDir) || !import_node_path2.default.isAbsolute(opts.strapiAppDir)) {
1104
+ if (!import_node_path3.default.isAbsolute(opts.frontendDir) || !import_node_path3.default.isAbsolute(opts.strapiAppDir)) {
765
1105
  result.errors.push("frontendDir e strapiAppDir devem ser absolutos");
766
1106
  return result;
767
1107
  }
768
1108
  try {
769
- const envPath = import_node_path2.default.join(opts.frontendDir, adapter.envFileName);
1109
+ const envPath = import_node_path3.default.join(opts.frontendDir, adapter.envFileName);
770
1110
  if (!ensureInside(opts.frontendDir, envPath)) throw new Error("env fora do frontendDir");
771
- const existing = import_node_fs2.default.existsSync(envPath) ? import_node_fs2.default.readFileSync(envPath, "utf8") : "";
1111
+ const existing = import_node_fs3.default.existsSync(envPath) ? import_node_fs3.default.readFileSync(envPath, "utf8") : "";
772
1112
  const vars = adapter.buildEnv(opts.context);
773
1113
  const { content, added, preserved } = mergeEnv(existing, vars);
774
1114
  result.envAdded = added;
775
1115
  result.envPreserved = preserved;
776
- if (!opts.dryRun && added.length) import_node_fs2.default.writeFileSync(envPath, content, "utf8");
1116
+ if (!opts.dryRun && added.length) import_node_fs3.default.writeFileSync(envPath, content, "utf8");
777
1117
  } catch (e) {
778
1118
  result.errors.push(`env: ${e?.message ?? e}`);
779
1119
  }
780
1120
  try {
781
- const typesPath = import_node_path2.default.join(opts.frontendDir, result.typesFile);
1121
+ const typesPath = import_node_path3.default.join(opts.frontendDir, result.typesFile);
782
1122
  if (!ensureInside(opts.frontendDir, typesPath)) throw new Error("types fora do frontendDir");
783
- if (!opts.dryRun) import_node_fs2.default.writeFileSync(typesPath, generateTypes(manifest), "utf8");
1123
+ if (!opts.dryRun) import_node_fs3.default.writeFileSync(typesPath, generateTypes(manifest), "utf8");
784
1124
  } catch (e) {
785
1125
  result.errors.push(`types: ${e?.message ?? e}`);
786
1126
  }
787
1127
  try {
788
- const adminPath = import_node_path2.default.join(opts.strapiAppDir, "config", "admin.ts");
789
- const content = buildPreviewConfig(manifest);
790
- if (import_node_fs2.default.existsSync(adminPath)) {
791
- result.previewFile = "config/admin.mcp-chat-preview.ts";
792
- const sidecar = import_node_path2.default.join(opts.strapiAppDir, "config", "admin.mcp-chat-preview.ts");
793
- if (!opts.dryRun) import_node_fs2.default.writeFileSync(sidecar, content, "utf8");
794
- result.previewAction = "sidecar";
795
- } else {
796
- if (!opts.dryRun) {
797
- import_node_fs2.default.mkdirSync(import_node_path2.default.dirname(adminPath), { recursive: true });
798
- import_node_fs2.default.writeFileSync(adminPath, content, "utf8");
1128
+ const configDir = import_node_path3.default.join(opts.strapiAppDir, "config");
1129
+ if (!ensureInside(opts.strapiAppDir, configDir)) throw new Error("config fora do strapiAppDir");
1130
+ const adminPath = import_node_path3.default.join(configDir, "admin.ts");
1131
+ const adminBasePath = import_node_path3.default.join(configDir, "admin.base.ts");
1132
+ const modulePath = import_node_path3.default.join(configDir, `${PREVIEW_MODULE}.ts`);
1133
+ if (!opts.dryRun) {
1134
+ import_node_fs3.default.mkdirSync(configDir, { recursive: true });
1135
+ import_node_fs3.default.writeFileSync(modulePath, buildPreviewConfig(manifest, adapter.framework), "utf8");
1136
+ try {
1137
+ import_node_fs3.default.unlinkSync(import_node_path3.default.join(configDir, "admin.mcp-chat-preview.ts"));
1138
+ } catch {
799
1139
  }
1140
+ }
1141
+ result.previewFile = `config/${PREVIEW_MODULE}.ts`;
1142
+ if (!import_node_fs3.default.existsSync(adminPath)) {
1143
+ if (!opts.dryRun) import_node_fs3.default.writeFileSync(adminPath, buildStandaloneAdmin(), "utf8");
800
1144
  result.previewAction = "created";
1145
+ } else {
1146
+ const adminContent = import_node_fs3.default.readFileSync(adminPath, "utf8");
1147
+ if (!adminContent.includes(PREVIEW_MARKER)) {
1148
+ if (!opts.dryRun) {
1149
+ if (!import_node_fs3.default.existsSync(adminBasePath)) {
1150
+ import_node_fs3.default.writeFileSync(adminBasePath, adminContent, "utf8");
1151
+ }
1152
+ import_node_fs3.default.writeFileSync(adminPath, buildAdminWrapper(), "utf8");
1153
+ }
1154
+ }
1155
+ result.previewAction = "merged";
801
1156
  }
802
1157
  } catch (e) {
803
1158
  result.errors.push(`preview: ${e?.message ?? e}`);
804
1159
  }
1160
+ try {
1161
+ const clientUrl = opts.context.frontendUrl || `http://localhost:${FRONTEND_BASE_PORT}`;
1162
+ const backendVars = { CLIENT_URL: clientUrl };
1163
+ if (opts.context.previewSecret) backendVars.PREVIEW_SECRET = opts.context.previewSecret;
1164
+ const backendEnvPath = import_node_path3.default.join(opts.strapiAppDir, ".env");
1165
+ if (ensureInside(opts.strapiAppDir, backendEnvPath)) {
1166
+ const existing = import_node_fs3.default.existsSync(backendEnvPath) ? import_node_fs3.default.readFileSync(backendEnvPath, "utf8") : "";
1167
+ const { content, added } = mergeEnv(existing, backendVars);
1168
+ result.backendEnvAdded = added;
1169
+ if (!opts.dryRun && added.length) import_node_fs3.default.writeFileSync(backendEnvPath, content, "utf8");
1170
+ }
1171
+ } catch (e) {
1172
+ result.errors.push(`backend env: ${e?.message ?? e}`);
1173
+ }
1174
+ try {
1175
+ result.cspAction = patchSecurityMiddleware(opts.strapiAppDir, opts.dryRun);
1176
+ if (result.cspAction === "manual") {
1177
+ result.errors.push(
1178
+ "CSP: config/middlewares j\xE1 tem strapi::security customizado \u2014 adicione manualmente frame-src 'self' http://localhost:* http://127.0.0.1:* para o preview embutir o frontend."
1179
+ );
1180
+ }
1181
+ } catch (e) {
1182
+ result.errors.push(`csp: ${e?.message ?? e}`);
1183
+ }
805
1184
  result.ok = result.errors.length === 0;
806
1185
  return result;
807
1186
  }
@@ -844,17 +1223,17 @@ var MARKER_DIR = ".mcp-chat";
844
1223
  var MARKER_FILE = "pending-provision.json";
845
1224
  var DONE_FILE = "last-provision.json";
846
1225
  function markerPath(strapiAppDir) {
847
- return import_node_path3.default.join(strapiAppDir, MARKER_DIR, MARKER_FILE);
1226
+ return import_node_path4.default.join(strapiAppDir, MARKER_DIR, MARKER_FILE);
848
1227
  }
849
1228
  function donePath(strapiAppDir) {
850
- return import_node_path3.default.join(strapiAppDir, MARKER_DIR, DONE_FILE);
1229
+ return import_node_path4.default.join(strapiAppDir, MARKER_DIR, DONE_FILE);
851
1230
  }
852
1231
  function getProvisionStatus(strapiAppDir) {
853
- const pending = import_node_fs3.default.existsSync(markerPath(strapiAppDir));
1232
+ const pending = import_node_fs4.default.existsSync(markerPath(strapiAppDir));
854
1233
  let done = null;
855
1234
  try {
856
1235
  const dp = donePath(strapiAppDir);
857
- if (import_node_fs3.default.existsSync(dp)) done = JSON.parse(import_node_fs3.default.readFileSync(dp, "utf8"));
1236
+ if (import_node_fs4.default.existsSync(dp)) done = JSON.parse(import_node_fs4.default.readFileSync(dp, "utf8"));
858
1237
  } catch {
859
1238
  }
860
1239
  return { pending, done };
@@ -890,10 +1269,10 @@ function stageProvision(strapi, input) {
890
1269
  if (!input.dryRun) {
891
1270
  try {
892
1271
  const mp = markerPath(input.strapiAppDir);
893
- import_node_fs3.default.mkdirSync(import_node_path3.default.dirname(mp), { recursive: true });
894
- import_node_fs3.default.writeFileSync(mp, JSON.stringify(marker, null, 2), "utf8");
1272
+ import_node_fs4.default.mkdirSync(import_node_path4.default.dirname(mp), { recursive: true });
1273
+ import_node_fs4.default.writeFileSync(mp, JSON.stringify(marker, null, 2), "utf8");
895
1274
  try {
896
- import_node_fs3.default.unlinkSync(donePath(input.strapiAppDir));
1275
+ import_node_fs4.default.unlinkSync(donePath(input.strapiAppDir));
897
1276
  } catch {
898
1277
  }
899
1278
  result.staged = true;
@@ -909,10 +1288,10 @@ function stageProvision(strapi, input) {
909
1288
  async function runPendingProvision(strapi, strapiAppDir) {
910
1289
  const result = { ran: false, errors: [] };
911
1290
  const mp = markerPath(strapiAppDir);
912
- if (!import_node_fs3.default.existsSync(mp)) return result;
1291
+ if (!import_node_fs4.default.existsSync(mp)) return result;
913
1292
  let marker;
914
1293
  try {
915
- marker = JSON.parse(import_node_fs3.default.readFileSync(mp, "utf8"));
1294
+ marker = JSON.parse(import_node_fs4.default.readFileSync(mp, "utf8"));
916
1295
  } catch (e) {
917
1296
  result.errors.push(`marcador ileg\xEDvel: ${e?.message ?? e}`);
918
1297
  return result;
@@ -941,8 +1320,7 @@ async function runPendingProvision(strapi, strapiAppDir) {
941
1320
  result.errors.push(`link: ${e?.message ?? e}`);
942
1321
  }
943
1322
  try {
944
- const adapter = adapterForManifest(marker.manifest);
945
- const previewUrl = marker.context.frontendUrl || `http://localhost:${adapter.defaultPort}`;
1323
+ const previewUrl = marker.context.frontendUrl || `http://localhost:${FRONTEND_BASE_PORT}`;
946
1324
  const done = {
947
1325
  name: marker.manifest.name,
948
1326
  framework: marker.manifest.framework,
@@ -954,21 +1332,21 @@ async function runPendingProvision(strapi, strapiAppDir) {
954
1332
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
955
1333
  };
956
1334
  const dp = donePath(strapiAppDir);
957
- import_node_fs3.default.mkdirSync(import_node_path3.default.dirname(dp), { recursive: true });
958
- import_node_fs3.default.writeFileSync(dp, JSON.stringify(done, null, 2), "utf8");
1335
+ import_node_fs4.default.mkdirSync(import_node_path4.default.dirname(dp), { recursive: true });
1336
+ import_node_fs4.default.writeFileSync(dp, JSON.stringify(done, null, 2), "utf8");
959
1337
  } catch (e) {
960
1338
  result.errors.push(`resumo: ${e?.message ?? e}`);
961
1339
  }
962
1340
  try {
963
- import_node_fs3.default.unlinkSync(mp);
1341
+ import_node_fs4.default.unlinkSync(mp);
964
1342
  } catch {
965
1343
  }
966
1344
  return result;
967
1345
  }
968
1346
 
969
1347
  // server/src/provision/infer.ts
970
- var import_node_fs4 = __toESM(require("node:fs"));
971
- var import_node_path4 = __toESM(require("node:path"));
1348
+ var import_node_fs5 = __toESM(require("node:fs"));
1349
+ var import_node_path5 = __toESM(require("node:path"));
972
1350
  var OPENAI_URL = "https://api.openai.com/v1/chat/completions";
973
1351
  var MODEL = process.env.OPENAI_CHAT_MODEL || "gpt-4o";
974
1352
  var SKIP_DIRS = /* @__PURE__ */ new Set([
@@ -1001,38 +1379,53 @@ function score(rel) {
1001
1379
  function walk(dir, base, out) {
1002
1380
  let entries;
1003
1381
  try {
1004
- entries = import_node_fs4.default.readdirSync(dir, { withFileTypes: true });
1382
+ entries = import_node_fs5.default.readdirSync(dir, { withFileTypes: true });
1005
1383
  } catch {
1006
1384
  return;
1007
1385
  }
1008
1386
  for (const e of entries) {
1009
1387
  if (e.name.startsWith(".") && e.name !== ".") continue;
1010
- const full = import_node_path4.default.join(dir, e.name);
1388
+ const full = import_node_path5.default.join(dir, e.name);
1011
1389
  if (e.isDirectory()) {
1012
1390
  if (SKIP_DIRS.has(e.name)) continue;
1013
1391
  walk(full, base, out);
1014
- } else if (CODE_EXT.has(import_node_path4.default.extname(e.name))) {
1015
- out.push(import_node_path4.default.relative(base, full));
1392
+ } else if (CODE_EXT.has(import_node_path5.default.extname(e.name))) {
1393
+ out.push(import_node_path5.default.relative(base, full));
1016
1394
  }
1017
1395
  }
1018
1396
  }
1397
+ function hasInlineDataArray(content) {
1398
+ return /(?:export\s+)?const\s+\w+\s*(?::[^=\n]+)?=\s*\[\s*\{/.test(content);
1399
+ }
1019
1400
  function collectFiles(frontendDir) {
1020
1401
  const all = [];
1021
1402
  walk(frontendDir, frontendDir, all);
1022
1403
  const tree = all.slice().sort();
1023
- const ranked = all.map((rel) => ({ rel, s: score(rel) })).filter((x) => x.s > 0).sort((a, b) => b.s - a.s);
1024
- const files = [];
1025
- let total = 0;
1026
- for (const { rel } of ranked) {
1027
- if (files.length >= MAX_FILES || total >= MAX_TOTAL_CHARS) break;
1404
+ const scored = [];
1405
+ for (const rel of all) {
1406
+ let content;
1028
1407
  try {
1029
- let content = import_node_fs4.default.readFileSync(import_node_path4.default.join(frontendDir, rel), "utf8");
1030
- if (!/export\s+(const|default|type|interface)/.test(content)) continue;
1031
- if (content.length > MAX_FILE_CHARS) content = content.slice(0, MAX_FILE_CHARS) + "\n/* \u2026truncado\u2026 */";
1032
- files.push({ rel, content });
1033
- total += content.length;
1408
+ content = import_node_fs5.default.readFileSync(import_node_path5.default.join(frontendDir, rel), "utf8");
1034
1409
  } catch {
1410
+ continue;
1035
1411
  }
1412
+ const dataArray = hasInlineDataArray(content);
1413
+ const hasExport = /export\s+(const|default|type|interface)/.test(content);
1414
+ if (!hasExport && !dataArray) continue;
1415
+ let s = score(rel);
1416
+ if (dataArray) s += 8;
1417
+ if (s <= 0) continue;
1418
+ scored.push({ rel, content, s });
1419
+ }
1420
+ scored.sort((a, b) => b.s - a.s);
1421
+ const files = [];
1422
+ let total = 0;
1423
+ for (const it of scored) {
1424
+ if (files.length >= MAX_FILES || total >= MAX_TOTAL_CHARS) break;
1425
+ let content = it.content;
1426
+ if (content.length > MAX_FILE_CHARS) content = content.slice(0, MAX_FILE_CHARS) + "\n/* \u2026truncado\u2026 */";
1427
+ files.push({ rel: it.rel, content });
1428
+ total += content.length;
1036
1429
  }
1037
1430
  return { files, tree };
1038
1431
  }
@@ -1067,7 +1460,7 @@ function collectPageTexts(frontendDir) {
1067
1460
  for (const rel of ranked) {
1068
1461
  if (out.length >= MAX_TEXT_FILES) break;
1069
1462
  try {
1070
- const texts = extractTexts(import_node_fs4.default.readFileSync(import_node_path4.default.join(frontendDir, rel), "utf8"));
1463
+ const texts = extractTexts(import_node_fs5.default.readFileSync(import_node_path5.default.join(frontendDir, rel), "utf8"));
1071
1464
  if (texts.length) out.push({ rel, texts });
1072
1465
  } catch {
1073
1466
  }
@@ -1155,9 +1548,9 @@ function buildPageContentTypes(pageTexts, budget) {
1155
1548
  }
1156
1549
  return { contentTypes, seed };
1157
1550
  }
1158
- function detectFramework(frontendDir) {
1551
+ function detectFramework2(frontendDir) {
1159
1552
  try {
1160
- const pkg = JSON.parse(import_node_fs4.default.readFileSync(import_node_path4.default.join(frontendDir, "package.json"), "utf8"));
1553
+ const pkg = JSON.parse(import_node_fs5.default.readFileSync(import_node_path5.default.join(frontendDir, "package.json"), "utf8"));
1161
1554
  const deps = { ...pkg.dependencies || {}, ...pkg.devDependencies || {} };
1162
1555
  if (deps.next) return "next";
1163
1556
  if (deps["@tanstack/react-start"]) return "tanstack";
@@ -1201,13 +1594,15 @@ Gere um JSON "strapi.manifest.json" com ESTE formato:
1201
1594
 
1202
1595
  REGRAS:
1203
1596
  - Crie uma content-type para cada COLE\xC7\xC3O de dados (arrays de objetos). Use os MESMOS nomes de campo do c\xF3digo.
1597
+ - IMPORTANTE: muitos frontends guardam os dados em arrays declarados INLINE dentro de componentes (.tsx/.jsx), ex.: \`const services = [{ name, price, desc }]\`, \`const reviews = [...]\`, \`const hours = [...]\`. TRATE esses arrays como cole\xE7\xF5es e modele cada um como um collectionType, mesmo que estejam dentro de um componente de UI.
1598
+ - Ao modelar um array desses, inclua SOMENTE os campos que s\xE3o dados/texto (ex.: name, price, desc, label, href, value). IGNORE props que s\xE3o c\xF3digo/apresenta\xE7\xE3o: componentes de \xEDcone (ex.: \`icon: Scissors\`), elementos React, fun\xE7\xF5es, classes CSS, imports de imagem.
1204
1599
  - Dados de "configura\xE7\xE3o do site" (objeto \xFAnico: nome, telefone, etc.) \u2192 singleType.
1205
1600
  - Campos string longos/descri\xE7\xF5es \u2192 "text" ou "richtext". Listas de strings \u2192 "json".
1206
1601
  - Use "date"/"datetime" SOMENTE para datas ISO completas (YYYY-MM-DD). Datas parciais como "2025-04" ou textos livres \u2192 use "string" (sen\xE3o o seed falha).
1207
1602
  - Imagens (imports de assets ou caminhos) \u2192 "media" (N\xC3O coloque o valor da imagem no seed; omita o campo no seed).
1208
- - Em "seed", extraia o conte\xFAdo REAL hardcoded no c\xF3digo, omitindo campos de m\xEDdia e rela\xE7\xF5es.
1603
+ - Em "seed", copie o conte\xFAdo REAL hardcoded no c\xF3digo, VERBATIM (exatamente como est\xE1, sem reescrever, traduzir ou inventar), omitindo campos de m\xEDdia e rela\xE7\xF5es. Todo valor de seed TEM que existir literalmente no c\xF3digo fornecido.
1209
1604
  - Foque APENAS em cole\xE7\xF5es/objetos de dados \u2014 N\xC3O precisa modelar textos soltos de UI (isso \xE9 tratado \xE0 parte).
1210
- - N\xC3O invente. singularName kebab-case, sem repetir. Rela\xE7\xF5es s\xF3 apontam para types definidos por voc\xEA.
1605
+ - N\xC3O invente NADA. Se n\xE3o tiver certeza de um valor, omita-o. singularName kebab-case, sem repetir. Rela\xE7\xF5es s\xF3 apontam para types definidos por voc\xEA.
1211
1606
  - Se n\xE3o houver cole\xE7\xF5es de dados, devolva contentTypes: [] e seed: [].
1212
1607
  - Responda APENAS com o JSON, nada de markdown.
1213
1608
 
@@ -1218,7 +1613,7 @@ Arquivos de dados (cole\xE7\xF5es):
1218
1613
  ${filesBlock}`;
1219
1614
  }
1220
1615
  async function inferManifest(strapi, frontendDir, opts) {
1221
- const framework = detectFramework(frontendDir);
1616
+ const framework = detectFramework2(frontendDir);
1222
1617
  const result = {
1223
1618
  ok: false,
1224
1619
  inferred: true,
@@ -1227,10 +1622,10 @@ async function inferManifest(strapi, frontendDir, opts) {
1227
1622
  warnings: [],
1228
1623
  errors: []
1229
1624
  };
1230
- const existing = import_node_path4.default.join(frontendDir, "strapi.manifest.json");
1231
- if (import_node_fs4.default.existsSync(existing)) {
1625
+ const existing = import_node_path5.default.join(frontendDir, "strapi.manifest.json");
1626
+ if (import_node_fs5.default.existsSync(existing)) {
1232
1627
  try {
1233
- const raw = JSON.parse(import_node_fs4.default.readFileSync(existing, "utf8"));
1628
+ const raw = JSON.parse(import_node_fs5.default.readFileSync(existing, "utf8"));
1234
1629
  const v2 = validateManifest(raw);
1235
1630
  result.inferred = false;
1236
1631
  result.rawManifest = raw;
@@ -1313,6 +1708,48 @@ async function inferManifest(strapi, frontendDir, opts) {
1313
1708
  } else if (!apiKey) {
1314
1709
  result.warnings.push("Sem OPENAI_API_KEY: modelando os TEXTOS (determin\xEDstico); cole\xE7\xF5es de dados n\xE3o inferidas.");
1315
1710
  }
1711
+ if (dataCts.length && dataSeed.length) {
1712
+ const parts = [];
1713
+ for (const f of collected.files) {
1714
+ try {
1715
+ parts.push(import_node_fs5.default.readFileSync(import_node_path5.default.join(frontendDir, f.rel), "utf8"));
1716
+ } catch {
1717
+ parts.push(f.content);
1718
+ }
1719
+ }
1720
+ const norm = (x) => String(x).toLowerCase().replace(/[^a-z0-9]+/g, "");
1721
+ const hay = norm(parts.join("\n"));
1722
+ const present = (v2) => {
1723
+ if (typeof v2 !== "string") return false;
1724
+ const n = norm(v2);
1725
+ return n.length >= 4 && hay.includes(n);
1726
+ };
1727
+ const keep = /* @__PURE__ */ new Set();
1728
+ const verifiedSeed = [];
1729
+ let droppedEntries = 0;
1730
+ for (const grp of dataSeed) {
1731
+ const entries = (grp.entries ?? []).filter((e) => {
1732
+ const ok = Object.values(e).some(present);
1733
+ if (!ok) droppedEntries++;
1734
+ return ok;
1735
+ });
1736
+ if (entries.length) {
1737
+ verifiedSeed.push({ ...grp, entries });
1738
+ keep.add(grp.singularName);
1739
+ }
1740
+ }
1741
+ const verifiedCts = dataCts.filter(
1742
+ (ct) => ct.kind === "singleType" || keep.has(ct.singularName)
1743
+ );
1744
+ const droppedCts = dataCts.length - verifiedCts.length;
1745
+ if (droppedEntries || droppedCts) {
1746
+ result.warnings.push(
1747
+ `Anti-alucina\xE7\xE3o: descartei ${droppedEntries} entrada(s) e ${droppedCts} content-type(s) cujos valores n\xE3o batiam com o c\xF3digo.`
1748
+ );
1749
+ }
1750
+ dataCts = verifiedCts;
1751
+ dataSeed = verifiedSeed;
1752
+ }
1316
1753
  const budget = 60 - dataCts.length - 1;
1317
1754
  const page = buildPageContentTypes(pageTexts, budget);
1318
1755
  const finalManifest = {
@@ -1341,132 +1778,6 @@ async function inferManifest(strapi, frontendDir, opts) {
1341
1778
  return result;
1342
1779
  }
1343
1780
 
1344
- // server/src/provision/runner.ts
1345
- var import_node_child_process = require("node:child_process");
1346
- var import_node_net = __toESM(require("node:net"));
1347
- var import_node_fs5 = __toESM(require("node:fs"));
1348
- var import_node_path5 = __toESM(require("node:path"));
1349
- var info = { state: "idle", dir: null, url: null, pm: null, error: null, log: [] };
1350
- var child = null;
1351
- var pollTimer = null;
1352
- function detectPM(dir) {
1353
- if (import_node_fs5.default.existsSync(import_node_path5.default.join(dir, "bun.lockb")) || import_node_fs5.default.existsSync(import_node_path5.default.join(dir, "bun.lock"))) return "bun";
1354
- if (import_node_fs5.default.existsSync(import_node_path5.default.join(dir, "pnpm-lock.yaml"))) return "pnpm";
1355
- if (import_node_fs5.default.existsSync(import_node_path5.default.join(dir, "yarn.lock"))) return "yarn";
1356
- return "npm";
1357
- }
1358
- var has = (dir, ...names) => names.some((n) => import_node_fs5.default.existsSync(import_node_path5.default.join(dir, n)));
1359
- function detectFramework2(dir) {
1360
- if (has(dir, "next.config.js", "next.config.ts", "next.config.mjs")) return "next";
1361
- if (has(dir, "vite.config.js", "vite.config.ts", "vite.config.mjs")) return "vite";
1362
- return "other";
1363
- }
1364
- var FRONTEND_BASE_PORT = 4321;
1365
- function findFreePort(start) {
1366
- return new Promise((resolve) => {
1367
- const tryPort = (p) => {
1368
- if (p > start + 200) return resolve(start);
1369
- const srv = import_node_net.default.createServer();
1370
- srv.once("error", () => tryPort(p + 1));
1371
- srv.once("listening", () => srv.close(() => resolve(p)));
1372
- srv.listen(p, "0.0.0.0");
1373
- };
1374
- tryPort(start);
1375
- });
1376
- }
1377
- function pushLog(s) {
1378
- for (const line of String(s).split("\n")) {
1379
- const t = line.trim();
1380
- if (t) info.log.push(t);
1381
- }
1382
- if (info.log.length > 60) info.log = info.log.slice(-60);
1383
- }
1384
- async function urlUp(url) {
1385
- try {
1386
- const res = await fetch(url, { method: "GET" });
1387
- return res.status >= 200 && res.status < 400;
1388
- } catch {
1389
- return false;
1390
- }
1391
- }
1392
- function getRunStatus() {
1393
- return { ...info, log: info.log.slice(-15) };
1394
- }
1395
- function stopFrontend() {
1396
- if (pollTimer) {
1397
- clearInterval(pollTimer);
1398
- pollTimer = null;
1399
- }
1400
- if (child) {
1401
- try {
1402
- child.kill("SIGTERM");
1403
- } catch {
1404
- }
1405
- child = null;
1406
- }
1407
- if (info.state !== "error") info.state = "idle";
1408
- }
1409
- async function startFrontend(_strapi, opts) {
1410
- const { dir } = opts;
1411
- if (child && info.dir === dir && ["installing", "starting", "running"].includes(info.state)) {
1412
- return getRunStatus();
1413
- }
1414
- stopFrontend();
1415
- const pm = detectPM(dir);
1416
- const framework = detectFramework2(dir);
1417
- const port = await findFreePort(FRONTEND_BASE_PORT);
1418
- const url = `http://127.0.0.1:${port}`;
1419
- info = { state: "installing", dir, url, pm, error: null, log: [] };
1420
- const spawnIn = (cmd, args) => (0, import_node_child_process.spawn)(cmd, args, { cwd: dir, env: { ...process.env }, stdio: ["ignore", "pipe", "pipe"] });
1421
- const fwArgs = framework === "next" ? ["-H", "127.0.0.1", "-p", String(port)] : framework === "vite" ? ["--host", "127.0.0.1", "--port", String(port), "--strictPort"] : ["--port", String(port)];
1422
- const devArgs = pm === "yarn" ? ["dev", ...fwArgs] : ["run", "dev", "--", ...fwArgs];
1423
- const startDev = () => {
1424
- info.state = "starting";
1425
- child = spawnIn(pm, devArgs);
1426
- child.stdout?.on("data", (d) => pushLog(d));
1427
- child.stderr?.on("data", (d) => pushLog(d));
1428
- child.on("exit", (code) => {
1429
- child = null;
1430
- if (pollTimer) {
1431
- clearInterval(pollTimer);
1432
- pollTimer = null;
1433
- }
1434
- if (info.state === "running") info.state = "idle";
1435
- else {
1436
- info.state = "error";
1437
- info.error = `dev encerrou (c\xF3digo ${code}). Veja o log.`;
1438
- }
1439
- });
1440
- pollTimer = setInterval(async () => {
1441
- if (await urlUp(url)) {
1442
- info.state = "running";
1443
- if (pollTimer) {
1444
- clearInterval(pollTimer);
1445
- pollTimer = null;
1446
- }
1447
- }
1448
- }, 1500);
1449
- };
1450
- const needInstall = !import_node_fs5.default.existsSync(import_node_path5.default.join(dir, "node_modules"));
1451
- if (needInstall) {
1452
- pushLog(`Instalando depend\xEAncias com ${pm}\u2026`);
1453
- const installArgs = pm === "npm" ? ["install", "--no-audit", "--no-fund"] : ["install"];
1454
- const inst = spawnIn(pm, installArgs);
1455
- inst.stdout?.on("data", (d) => pushLog(d));
1456
- inst.stderr?.on("data", (d) => pushLog(d));
1457
- inst.on("exit", (code) => {
1458
- if (code === 0) startDev();
1459
- else {
1460
- info.state = "error";
1461
- info.error = `instala\xE7\xE3o falhou (c\xF3digo ${code}). Veja o log.`;
1462
- }
1463
- });
1464
- } else {
1465
- startDev();
1466
- }
1467
- return getRunStatus();
1468
- }
1469
-
1470
1781
  // server/src/provision/integrate.ts
1471
1782
  var import_node_fs6 = __toESM(require("node:fs"));
1472
1783
  var import_node_path6 = __toESM(require("node:path"));
@@ -2088,8 +2399,11 @@ async function integrateFrontend(strapi, opts) {
2088
2399
  // server/src/controllers/frontend.ts
2089
2400
  var MANIFEST_NAME = "strapi.manifest.json";
2090
2401
  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);
2402
+ const b = import_node_path7.default.resolve(base);
2403
+ const t = import_node_path7.default.resolve(target);
2404
+ if (t === b) return true;
2405
+ const rel = import_node_path7.default.relative(b, t);
2406
+ return !!rel && !rel.startsWith("..") && !import_node_path7.default.isAbsolute(rel);
2093
2407
  }
2094
2408
  function toKebab(input) {
2095
2409
  const s = (input || "frontend").toLowerCase().replace(/\.zip$/, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").replace(/^[^a-z]+/, "");
@@ -2311,6 +2625,15 @@ var baseHeaders = {
2311
2625
  "Content-Type": "application/json",
2312
2626
  Accept: "application/json, text/event-stream"
2313
2627
  };
2628
+ var fetchT = async (url, opts, timeoutMs = 8e3) => {
2629
+ const ctrl = new AbortController();
2630
+ const t = setTimeout(() => ctrl.abort(), timeoutMs);
2631
+ try {
2632
+ return await fetch(url, { ...opts, signal: ctrl.signal });
2633
+ } finally {
2634
+ clearTimeout(t);
2635
+ }
2636
+ };
2314
2637
  var parseSse = (text) => {
2315
2638
  const dataLines = text.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trim()).filter(Boolean);
2316
2639
  const last = dataLines[dataLines.length - 1];
@@ -2334,7 +2657,7 @@ var McpClient = class {
2334
2657
  return h;
2335
2658
  }
2336
2659
  async init() {
2337
- const res = await fetch(this.url, {
2660
+ const res = await fetchT(this.url, {
2338
2661
  method: "POST",
2339
2662
  headers: this.headers(),
2340
2663
  body: JSON.stringify({
@@ -2350,14 +2673,14 @@ var McpClient = class {
2350
2673
  });
2351
2674
  this.sessionId = res.headers.get("mcp-session-id") || void 0;
2352
2675
  await res.text();
2353
- await fetch(this.url, {
2676
+ await fetchT(this.url, {
2354
2677
  method: "POST",
2355
2678
  headers: this.headers(),
2356
2679
  body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })
2357
2680
  });
2358
2681
  }
2359
2682
  async rpc(method, params, id) {
2360
- const res = await fetch(this.url, {
2683
+ const res = await fetchT(this.url, {
2361
2684
  method: "POST",
2362
2685
  headers: this.headers(),
2363
2686
  body: JSON.stringify({ jsonrpc: "2.0", id, method, params })
@@ -2473,16 +2796,18 @@ function createContentTools(strapi) {
2473
2796
  );
2474
2797
  const attrsOf = (uid) => strapi.contentTypes?.[uid]?.attributes || strapi.components?.[uid]?.attributes || {};
2475
2798
  const hasDraftAndPublish = (uid) => strapi.contentTypes?.[uid]?.options?.draftAndPublish === true;
2476
- const buildPopulate = (attributes, seen = /* @__PURE__ */ new Set()) => {
2799
+ const MAX_DEPTH = 8;
2800
+ const buildPopulate = (attributes, seen = /* @__PURE__ */ new Set(), depth = 0) => {
2801
+ if (depth >= MAX_DEPTH) return {};
2477
2802
  const populate = {};
2478
2803
  for (const [name, a] of Object.entries(attributes)) {
2479
2804
  if (a.type === "component" && a.component) {
2480
- const sub = seen.has(a.component) ? {} : buildPopulate(attrsOf(a.component), new Set(seen).add(a.component));
2805
+ const sub = seen.has(a.component) ? {} : buildPopulate(attrsOf(a.component), new Set(seen).add(a.component), depth + 1);
2481
2806
  populate[name] = Object.keys(sub).length ? { populate: sub } : true;
2482
2807
  } else if (a.type === "dynamiczone") {
2483
2808
  const on = {};
2484
2809
  for (const comp of a.components || []) {
2485
- const sub = seen.has(comp) ? {} : buildPopulate(attrsOf(comp), new Set(seen).add(comp));
2810
+ const sub = seen.has(comp) ? {} : buildPopulate(attrsOf(comp), new Set(seen).add(comp), depth + 1);
2486
2811
  on[comp] = Object.keys(sub).length ? { populate: sub } : true;
2487
2812
  }
2488
2813
  populate[name] = { on };
@@ -2494,6 +2819,7 @@ function createContentTools(strapi) {
2494
2819
  };
2495
2820
  const walkFind = (node, attributes, basePath, needle, collect) => {
2496
2821
  if (!node || typeof node !== "object") return;
2822
+ if (basePath.length > 24) return;
2497
2823
  for (const [name, a] of Object.entries(attributes)) {
2498
2824
  const v = node[name];
2499
2825
  if (v == null) continue;
@@ -2518,16 +2844,19 @@ function createContentTools(strapi) {
2518
2844
  }
2519
2845
  }
2520
2846
  };
2521
- const buscarTexto = async (termo) => {
2847
+ const MAX_MATCHES = 100;
2848
+ const buscarTexto = async (termo, status = "draft") => {
2522
2849
  const needle = String(termo || "").toLowerCase().trim();
2523
2850
  if (!needle) return { erro: "termo vazio" };
2851
+ const st = status === "published" ? "published" : "draft";
2524
2852
  const matches = [];
2525
2853
  for (const ct of apiContentTypes()) {
2854
+ if (matches.length >= MAX_MATCHES) break;
2526
2855
  const attributes = ct.attributes || {};
2527
2856
  const populate = buildPopulate(attributes);
2528
2857
  let entries = [];
2529
2858
  try {
2530
- const res = await strapi.documents(ct.uid).findMany({ status: "draft", populate, limit: 200 });
2859
+ const res = await strapi.documents(ct.uid).findMany({ status: st, populate, limit: 200 });
2531
2860
  entries = Array.isArray(res) ? res : res ? [res] : [];
2532
2861
  } catch {
2533
2862
  continue;
@@ -2549,7 +2878,12 @@ function createContentTools(strapi) {
2549
2878
  });
2550
2879
  }
2551
2880
  }
2552
- return { total: matches.length, resultados: matches };
2881
+ const truncated = matches.length > MAX_MATCHES;
2882
+ return {
2883
+ total: matches.length,
2884
+ resultados: matches.slice(0, MAX_MATCHES),
2885
+ ...truncated ? { truncado: true, nota: `mostrando ${MAX_MATCHES} de ${matches.length} resultados; refine o termo` } : {}
2886
+ };
2553
2887
  };
2554
2888
  const sanitizeNode = (node, attributes) => {
2555
2889
  if (node == null) return node;
@@ -3037,7 +3371,7 @@ If the user shares their screen, an image is attached to the last message \u2014
3037
3371
  Be concise and actionable. ALWAYS answer in English.`
3038
3372
  };
3039
3373
  var chat_default2 = ({ strapi }) => ({
3040
- async chat({ messages, image, lang = "pt", previewUrl, autoPublish = false }) {
3374
+ async chat({ messages, image, lang = "pt", previewUrl, previewStatus = "draft", autoPublish = false }) {
3041
3375
  const apiKey = process.env.OPENAI_API_KEY;
3042
3376
  if (!apiKey) {
3043
3377
  throw new Error(
@@ -3047,7 +3381,7 @@ var chat_default2 = ({ strapi }) => ({
3047
3381
  const language = lang === "en" ? "en" : "pt";
3048
3382
  const { buscarTexto, editarCampo, publicar, listarLocales, criarLocale, traduzir } = createContentTools(strapi);
3049
3383
  const LOCAL_TOOLS = {
3050
- buscar_texto: (a) => buscarTexto(a?.termo),
3384
+ buscar_texto: (a) => buscarTexto(a?.termo, previewStatus),
3051
3385
  editar_campo: (a) => editarCampo(a),
3052
3386
  publicar: (a) => publicar(a),
3053
3387
  listar_locales: () => listarLocales(),
@@ -3061,7 +3395,10 @@ var chat_default2 = ({ strapi }) => ({
3061
3395
  if (process.env.PLAYWRIGHT_MCP_URL) {
3062
3396
  try {
3063
3397
  const client = new McpClient(process.env.PLAYWRIGHT_MCP_URL, "playwright");
3064
- await client.init();
3398
+ await Promise.race([
3399
+ client.init(),
3400
+ new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), 4e3))
3401
+ ]);
3065
3402
  const list = await client.listTools();
3066
3403
  for (const t of list) {
3067
3404
  if (mcpByTool[t.name]) continue;
@@ -3133,11 +3470,22 @@ PUBLISH POLICY: DRAFT MODE (auto-publish OFF). Do NOT call publicar unless the u
3133
3470
  }
3134
3471
  });
3135
3472
  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
- });
3473
+ const ctrl = new AbortController();
3474
+ const timer = setTimeout(() => ctrl.abort(), 6e4);
3475
+ let res;
3476
+ try {
3477
+ res = await fetch(OPENAI_URL4, {
3478
+ method: "POST",
3479
+ headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
3480
+ body: JSON.stringify(body),
3481
+ signal: ctrl.signal
3482
+ });
3483
+ } catch (e) {
3484
+ if (e?.name === "AbortError") throw new Error("OpenAI chat: tempo limite excedido (60s).");
3485
+ throw e;
3486
+ } finally {
3487
+ clearTimeout(timer);
3488
+ }
3141
3489
  if (!res.ok) throw new Error(`OpenAI chat: ${await res.text()}`);
3142
3490
  return res.json();
3143
3491
  };
@@ -3176,6 +3524,11 @@ PUBLISH POLICY: DRAFT MODE (auto-publish OFF). Do NOT call publicar unless the u
3176
3524
  } catch (e) {
3177
3525
  content = `Erro ao chamar a tool ${name}: ${e?.message || e}`;
3178
3526
  }
3527
+ const MAX_TOOL_CHARS = 12e3;
3528
+ if (content.length > MAX_TOOL_CHARS) {
3529
+ content = content.slice(0, MAX_TOOL_CHARS) + `
3530
+ \u2026[resultado truncado: ${content.length} chars]`;
3531
+ }
3179
3532
  convo.push({ role: "tool", tool_call_id: call.id, content });
3180
3533
  }
3181
3534
  continue;
@@ -3299,196 +3652,166 @@ var routes_default = {
3299
3652
 
3300
3653
  // server/src/mcp/tools/buscar-texto.ts
3301
3654
  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
- });
3655
+
3656
+ // server/src/mcp/define.ts
3657
+ var defineTool = (def) => def;
3658
+
3659
+ // server/src/mcp/tools/buscar-texto.ts
3660
+ var buscar_texto_default = defineTool({
3661
+ name: "mcp_chat_buscar_texto",
3662
+ title: "Search text across content (deep)",
3663
+ 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.',
3664
+ resolveInputSchema: () => import_utils2.z.object({ termo: import_utils2.z.string() }),
3665
+ resolveOutputSchema: () => import_utils2.z.object({
3666
+ total: import_utils2.z.number().optional(),
3667
+ resultados: import_utils2.z.array(import_utils2.z.any()).optional(),
3668
+ erro: import_utils2.z.string().optional()
3669
+ }),
3670
+ auth: { policies: [{ action: "plugin::content-manager.explorer.read" }] },
3671
+ createHandler: (strapi) => async ({ args }) => {
3672
+ const r = await createContentTools(strapi).buscarTexto(args.termo);
3673
+ return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
3320
3674
  }
3321
- };
3322
- var buscar_texto_default = tool;
3675
+ });
3323
3676
 
3324
3677
  // server/src/mcp/tools/editar-campo.ts
3325
3678
  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
- });
3679
+ var editar_campo_default = defineTool({
3680
+ name: "mcp_chat_editar_campo",
3681
+ title: "Edit a (possibly nested) field",
3682
+ 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`.",
3683
+ resolveInputSchema: () => import_utils3.z.object({
3684
+ uid: import_utils3.z.string(),
3685
+ documentId: import_utils3.z.string(),
3686
+ path: import_utils3.z.array(import_utils3.z.union([import_utils3.z.string(), import_utils3.z.number()])).optional(),
3687
+ campo: import_utils3.z.string().optional(),
3688
+ novo_valor: import_utils3.z.string(),
3689
+ locale: import_utils3.z.string().optional()
3690
+ }),
3691
+ resolveOutputSchema: () => import_utils3.z.object({
3692
+ ok: import_utils3.z.boolean().optional(),
3693
+ uid: import_utils3.z.string().optional(),
3694
+ documentId: import_utils3.z.string().optional(),
3695
+ path: import_utils3.z.array(import_utils3.z.any()).optional(),
3696
+ novo_valor: import_utils3.z.string().optional(),
3697
+ erro: import_utils3.z.string().optional()
3698
+ }),
3699
+ auth: { policies: [{ action: "plugin::content-manager.explorer.update" }] },
3700
+ createHandler: (strapi) => async ({ args }) => {
3701
+ const r = await createContentTools(strapi).editarCampo(args);
3702
+ return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
3354
3703
  }
3355
- };
3356
- var editar_campo_default = tool2;
3704
+ });
3357
3705
 
3358
3706
  // server/src/mcp/tools/publicar.ts
3359
3707
  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
- });
3708
+ var publicar_default = defineTool({
3709
+ name: "mcp_chat_publicar",
3710
+ title: "Publish an entry",
3711
+ 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.',
3712
+ resolveInputSchema: () => import_utils4.z.object({ uid: import_utils4.z.string(), documentId: import_utils4.z.string(), locale: import_utils4.z.string().optional() }),
3713
+ resolveOutputSchema: () => import_utils4.z.object({
3714
+ ok: import_utils4.z.boolean().optional(),
3715
+ uid: import_utils4.z.string().optional(),
3716
+ documentId: import_utils4.z.string().optional(),
3717
+ status: import_utils4.z.string().optional(),
3718
+ locale: import_utils4.z.string().optional()
3719
+ }),
3720
+ auth: { policies: [{ action: "plugin::content-manager.explorer.publish" }] },
3721
+ createHandler: (strapi) => async ({ args }) => {
3722
+ const r = await createContentTools(strapi).publicar(args);
3723
+ return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
3380
3724
  }
3381
- };
3382
- var publicar_default = tool3;
3725
+ });
3383
3726
 
3384
3727
  // server/src/mcp/tools/listar-locales.ts
3385
3728
  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
- });
3729
+ var listar_locales_default = defineTool({
3730
+ name: "mcp_chat_listar_locales",
3731
+ title: "List i18n locales",
3732
+ description: "List the configured locales (languages) and which one is the default.",
3733
+ resolveInputSchema: () => import_utils5.z.object({}),
3734
+ resolveOutputSchema: () => import_utils5.z.object({
3735
+ default: import_utils5.z.string().optional(),
3736
+ locales: import_utils5.z.array(import_utils5.z.any()).optional(),
3737
+ erro: import_utils5.z.string().optional()
3738
+ }),
3739
+ auth: { policies: [{ action: "plugin::content-manager.explorer.read" }] },
3740
+ createHandler: (strapi) => async () => {
3741
+ const r = await createContentTools(strapi).listarLocales();
3742
+ return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
3404
3743
  }
3405
- };
3406
- var listar_locales_default = tool4;
3744
+ });
3407
3745
 
3408
3746
  // server/src/mcp/tools/criar-locale.ts
3409
3747
  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
- });
3748
+ var criar_locale_default = defineTool({
3749
+ name: "mcp_chat_criar_locale",
3750
+ title: "Create an i18n locale",
3751
+ description: 'Create a locale (language). `code` must be a valid ISO code (e.g. "pt-BR", "es"). Idempotent: returns ok if it already exists.',
3752
+ resolveInputSchema: () => import_utils6.z.object({ code: import_utils6.z.string(), name: import_utils6.z.string().optional() }),
3753
+ resolveOutputSchema: () => import_utils6.z.object({
3754
+ ok: import_utils6.z.boolean().optional(),
3755
+ code: import_utils6.z.string().optional(),
3756
+ name: import_utils6.z.string().optional(),
3757
+ existed: import_utils6.z.boolean().optional(),
3758
+ erro: import_utils6.z.string().optional()
3759
+ }),
3760
+ auth: { policies: [{ action: "plugin::i18n.locale.create" }] },
3761
+ createHandler: (strapi) => async ({ args }) => {
3762
+ const r = await createContentTools(strapi).criarLocale(args);
3763
+ return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
3430
3764
  }
3431
- };
3432
- var criar_locale_default = tool5;
3765
+ });
3433
3766
 
3434
3767
  // server/src/mcp/tools/traduzir.ts
3435
3768
  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
- });
3769
+ var traduzir_default = defineTool({
3770
+ name: "mcp_chat_traduzir",
3771
+ title: "Translate localized content",
3772
+ 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.",
3773
+ resolveInputSchema: () => import_utils7.z.object({
3774
+ target_locales: import_utils7.z.array(import_utils7.z.string()).min(1),
3775
+ source_locale: import_utils7.z.string().optional(),
3776
+ uid: import_utils7.z.string().optional(),
3777
+ documentId: import_utils7.z.string().optional(),
3778
+ publish: import_utils7.z.boolean().optional()
3779
+ }),
3780
+ resolveOutputSchema: () => import_utils7.z.object({
3781
+ ok: import_utils7.z.boolean().optional(),
3782
+ source: import_utils7.z.string().optional(),
3783
+ por_locale: import_utils7.z.array(import_utils7.z.any()).optional(),
3784
+ erro: import_utils7.z.string().optional()
3785
+ }),
3786
+ auth: { policies: [{ action: "plugin::content-manager.explorer.update" }] },
3787
+ createHandler: (strapi) => async ({ args }) => {
3788
+ const r = await createContentTools(strapi).traduzir(args);
3789
+ return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
3461
3790
  }
3462
- };
3463
- var traduzir_default = tool6;
3791
+ });
3464
3792
 
3465
3793
  // server/src/mcp/tools/habilitar-i18n.ts
3466
3794
  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
- });
3795
+ var habilitar_i18n_default = defineTool({
3796
+ name: "mcp_chat_habilitar_i18n",
3797
+ title: "Enable i18n on a content-type",
3798
+ 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.',
3799
+ resolveInputSchema: () => import_utils8.z.object({ uid: import_utils8.z.string().optional(), campos: import_utils8.z.array(import_utils8.z.string()).optional() }),
3800
+ resolveOutputSchema: () => import_utils8.z.object({
3801
+ ok: import_utils8.z.boolean().optional(),
3802
+ uid: import_utils8.z.string().optional(),
3803
+ campos: import_utils8.z.array(import_utils8.z.string()).optional(),
3804
+ contentTypes: import_utils8.z.array(import_utils8.z.any()).optional(),
3805
+ total: import_utils8.z.number().optional(),
3806
+ restart: import_utils8.z.boolean().optional(),
3807
+ erro: import_utils8.z.string().optional()
3808
+ }),
3809
+ auth: { policies: [{ action: "plugin::content-type-builder.read" }] },
3810
+ createHandler: (strapi) => async ({ args }) => {
3811
+ const r = enableI18n({ strapi, uid: args.uid, campos: args.campos });
3812
+ return { content: [{ type: "text", text: JSON.stringify(r) }], structuredContent: r };
3489
3813
  }
3490
- };
3491
- var habilitar_i18n_default = tool7;
3814
+ });
3492
3815
 
3493
3816
  // server/src/mcp/tools/index.ts
3494
3817
  var tools = [
@@ -3511,9 +3834,16 @@ var registerMcpTools = (strapi) => {
3511
3834
  );
3512
3835
  return;
3513
3836
  }
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_*).`);
3837
+ let registered = 0;
3838
+ for (const tool of tools) {
3839
+ try {
3840
+ mcp.registerTool(tool);
3841
+ registered += 1;
3842
+ } catch (e) {
3843
+ strapi.log.warn(`[mcp-chat] tool "${tool?.name}" falhou ao registrar: ${e?.message ?? e}`);
3844
+ }
3845
+ }
3846
+ strapi.log.info(`[mcp-chat] ${registered}/${tools.length} tools registradas no MCP nativo (mcp_chat_*).`);
3517
3847
  };
3518
3848
 
3519
3849
  // server/src/register.ts
@@ -3529,6 +3859,10 @@ var register_default = ({ strapi }) => {
3529
3859
  var index_default = {
3530
3860
  register: register_default,
3531
3861
  async bootstrap({ strapi }) {
3862
+ try {
3863
+ cleanupStaleFrontend(strapi.dirs.app.root);
3864
+ } catch {
3865
+ }
3532
3866
  try {
3533
3867
  const r = await runPendingProvision(strapi, strapi.dirs.app.root);
3534
3868
  if (r.ran) {
@@ -3542,7 +3876,10 @@ var index_default = {
3542
3876
  }
3543
3877
  },
3544
3878
  destroy() {
3545
- stopFrontend();
3879
+ try {
3880
+ stopFrontend();
3881
+ } catch {
3882
+ }
3546
3883
  },
3547
3884
  config: {
3548
3885
  default: {},