strapi-plugin-mcp-chat 0.1.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -9
- package/admin/src/components/AdminOverlays.tsx +35 -1
- package/admin/src/components/ErrorBoundary.tsx +29 -0
- package/admin/src/components/FloatingChat.tsx +22 -0
- package/admin/src/components/LangSwitcher.tsx +24 -0
- package/admin/src/components/Onboarding.tsx +192 -0
- package/admin/src/components/PreviewPanel.tsx +22 -1
- package/admin/src/components/StackLogos.tsx +60 -0
- package/admin/src/i18n.ts +214 -0
- package/admin/src/index.tsx +39 -5
- package/admin/src/pages/HomePage.tsx +55 -24
- package/admin/src/pages/ProvisionPage.tsx +54 -59
- package/dist/server/index.js +358 -200
- package/package.json +1 -1
- package/server/src/content-tools.ts +42 -8
- package/server/src/controllers/chat.ts +2 -2
- package/server/src/controllers/frontend.ts +7 -2
- package/server/src/index.ts +6 -1
- package/server/src/mcp/define.ts +72 -0
- package/server/src/mcp/index.ts +19 -6
- package/server/src/mcp/tools/buscar-texto.ts +18 -24
- package/server/src/mcp/tools/criar-locale.ts +20 -26
- package/server/src/mcp/tools/editar-campo.ts +29 -35
- package/server/src/mcp/tools/habilitar-i18n.ts +23 -29
- package/server/src/mcp/tools/listar-locales.ts +17 -23
- package/server/src/mcp/tools/publicar.ts +21 -27
- package/server/src/mcp/tools/traduzir.ts +26 -32
- package/server/src/mcp/types.ts +12 -9
- package/server/src/mcp-client.ts +15 -3
- package/server/src/provision/integrate.ts +15 -1
- package/server/src/provision/write.ts +92 -0
- package/server/src/services/chat.ts +56 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "strapi-plugin-mcp-chat",
|
|
3
|
-
"version": "0.
|
|
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",
|
|
@@ -37,22 +37,32 @@ export function createContentTools(strapi: any) {
|
|
|
37
37
|
strapi.components?.[uid]?.attributes ||
|
|
38
38
|
{}) as Record<string, any>;
|
|
39
39
|
|
|
40
|
+
// Draft & Publish é por content-type e vem DESLIGADO por padrão (docs Strapi 5).
|
|
41
|
+
// `publish()`/`unpublish()` SÓ existem quando está ligado — chamar sem D&P
|
|
42
|
+
// lança erro. Por isso checamos antes de publicar.
|
|
43
|
+
const hasDraftAndPublish = (uid: string): boolean =>
|
|
44
|
+
strapi.contentTypes?.[uid]?.options?.draftAndPublish === true;
|
|
45
|
+
|
|
40
46
|
// Populate profundo: components (simples/repetíveis), dynamic zones (com `on`
|
|
41
47
|
// por componente) e mídia/relações. `seen` evita recursão infinita.
|
|
42
|
-
|
|
48
|
+
// `seen` evita ciclos (componente que se referencia) e `depth` é um teto rígido
|
|
49
|
+
// contra schemas patológicos (recursão profunda → stack/memória).
|
|
50
|
+
const MAX_DEPTH = 8;
|
|
51
|
+
const buildPopulate = (attributes: Record<string, any>, seen = new Set<string>(), depth = 0): any => {
|
|
52
|
+
if (depth >= MAX_DEPTH) return {};
|
|
43
53
|
const populate: any = {};
|
|
44
54
|
for (const [name, a] of Object.entries(attributes) as any[]) {
|
|
45
55
|
if (a.type === 'component' && a.component) {
|
|
46
56
|
const sub = seen.has(a.component)
|
|
47
57
|
? {}
|
|
48
|
-
: buildPopulate(attrsOf(a.component), new Set(seen).add(a.component));
|
|
58
|
+
: buildPopulate(attrsOf(a.component), new Set(seen).add(a.component), depth + 1);
|
|
49
59
|
populate[name] = Object.keys(sub).length ? { populate: sub } : true;
|
|
50
60
|
} else if (a.type === 'dynamiczone') {
|
|
51
61
|
const on: any = {};
|
|
52
62
|
for (const comp of a.components || []) {
|
|
53
63
|
const sub = seen.has(comp)
|
|
54
64
|
? {}
|
|
55
|
-
: buildPopulate(attrsOf(comp), new Set(seen).add(comp));
|
|
65
|
+
: buildPopulate(attrsOf(comp), new Set(seen).add(comp), depth + 1);
|
|
56
66
|
on[comp] = Object.keys(sub).length ? { populate: sub } : true;
|
|
57
67
|
}
|
|
58
68
|
populate[name] = { on };
|
|
@@ -71,6 +81,7 @@ export function createContentTools(strapi: any) {
|
|
|
71
81
|
collect: (path: (string | number)[], campo: string, valor: string) => void
|
|
72
82
|
) => {
|
|
73
83
|
if (!node || typeof node !== 'object') return;
|
|
84
|
+
if (basePath.length > 24) return; // teto rígido contra recursão patológica
|
|
74
85
|
for (const [name, a] of Object.entries(attributes) as any[]) {
|
|
75
86
|
const v = node[name];
|
|
76
87
|
if (v == null) continue;
|
|
@@ -96,11 +107,13 @@ export function createContentTools(strapi: any) {
|
|
|
96
107
|
}
|
|
97
108
|
};
|
|
98
109
|
|
|
110
|
+
const MAX_MATCHES = 100;
|
|
99
111
|
const buscarTexto = async (termo: string) => {
|
|
100
112
|
const needle = String(termo || '').toLowerCase().trim();
|
|
101
113
|
if (!needle) return { erro: 'termo vazio' };
|
|
102
114
|
const matches: any[] = [];
|
|
103
115
|
for (const ct of apiContentTypes() as any[]) {
|
|
116
|
+
if (matches.length >= MAX_MATCHES) break; // teto: não varre além do necessário
|
|
104
117
|
const attributes = ct.attributes || {};
|
|
105
118
|
const populate = buildPopulate(attributes);
|
|
106
119
|
let entries: any[] = [];
|
|
@@ -112,6 +125,7 @@ export function createContentTools(strapi: any) {
|
|
|
112
125
|
} catch {
|
|
113
126
|
continue;
|
|
114
127
|
}
|
|
128
|
+
const dp = hasDraftAndPublish(ct.uid);
|
|
115
129
|
for (const e of entries) {
|
|
116
130
|
walkFind(e, attributes, [], needle, (path, campo, valor) => {
|
|
117
131
|
matches.push({
|
|
@@ -121,11 +135,19 @@ export function createContentTools(strapi: any) {
|
|
|
121
135
|
path,
|
|
122
136
|
campo,
|
|
123
137
|
valor_atual: valor.length > 300 ? valor.slice(0, 300) + '…' : valor,
|
|
138
|
+
// draftAndPublish=false → não há rascunho; a edição já é o conteúdo
|
|
139
|
+
// vivo e não há o que publicar (a IA deve avisar o usuário).
|
|
140
|
+
draftAndPublish: dp,
|
|
124
141
|
});
|
|
125
142
|
});
|
|
126
143
|
}
|
|
127
144
|
}
|
|
128
|
-
|
|
145
|
+
const truncated = matches.length > MAX_MATCHES;
|
|
146
|
+
return {
|
|
147
|
+
total: matches.length,
|
|
148
|
+
resultados: matches.slice(0, MAX_MATCHES),
|
|
149
|
+
...(truncated ? { truncado: true, nota: `mostrando ${MAX_MATCHES} de ${matches.length} resultados; refine o termo` } : {}),
|
|
150
|
+
};
|
|
129
151
|
};
|
|
130
152
|
|
|
131
153
|
// Converte um nó populado de volta a forma gravável: preserva `id` (p/ Strapi
|
|
@@ -179,7 +201,7 @@ export function createContentTools(strapi: any) {
|
|
|
179
201
|
const updated = await strapi
|
|
180
202
|
.documents(uid)
|
|
181
203
|
.update({ documentId, ...loc, data: { [topAttr]: novo_valor } });
|
|
182
|
-
return { ok: true, uid, documentId: updated?.documentId || documentId, path: p, novo_valor, locale };
|
|
204
|
+
return { ok: true, uid, documentId: updated?.documentId || documentId, path: p, novo_valor, locale, draftAndPublish: hasDraftAndPublish(uid) };
|
|
183
205
|
}
|
|
184
206
|
|
|
185
207
|
// Campo aninhado → busca profunda, muta no caminho, sanitiza e regrava o
|
|
@@ -196,7 +218,7 @@ export function createContentTools(strapi: any) {
|
|
|
196
218
|
cur[p[p.length - 1] as any] = novo_valor;
|
|
197
219
|
const data = { [topAttr]: sanitizeAttr(entry[topAttr], ad) };
|
|
198
220
|
const updated = await strapi.documents(uid).update({ documentId, ...loc, data });
|
|
199
|
-
return { ok: true, uid, documentId: updated?.documentId || documentId, path: p, novo_valor, locale };
|
|
221
|
+
return { ok: true, uid, documentId: updated?.documentId || documentId, path: p, novo_valor, locale, draftAndPublish: hasDraftAndPublish(uid) };
|
|
200
222
|
};
|
|
201
223
|
|
|
202
224
|
const publicar = async ({
|
|
@@ -209,6 +231,17 @@ export function createContentTools(strapi: any) {
|
|
|
209
231
|
/** Locale a publicar; "*" publica todos os locales disponíveis. */
|
|
210
232
|
locale?: string;
|
|
211
233
|
}) => {
|
|
234
|
+
// Best practice Strapi 5: publish() só existe com Draft & Publish ligado;
|
|
235
|
+
// chamar sem D&P lança erro. Sem D&P não há rascunho — a edição já é o vivo.
|
|
236
|
+
if (!hasDraftAndPublish(uid)) {
|
|
237
|
+
return {
|
|
238
|
+
ok: true,
|
|
239
|
+
uid,
|
|
240
|
+
documentId,
|
|
241
|
+
status: 'no-draft-publish',
|
|
242
|
+
nota: 'Esta content-type não tem Draft & Publish; não há rascunho a publicar — a alteração já está no ar.',
|
|
243
|
+
};
|
|
244
|
+
}
|
|
212
245
|
await strapi.documents(uid).publish({ documentId, ...(locale ? { locale } : {}) });
|
|
213
246
|
return { ok: true, uid, documentId, status: 'published', locale };
|
|
214
247
|
};
|
|
@@ -374,7 +407,8 @@ export function createContentTools(strapi: any) {
|
|
|
374
407
|
// upsert idempotente da versão do locale
|
|
375
408
|
await strapi.documents(ct.uid).update({ documentId: e.documentId, locale: tgt, data });
|
|
376
409
|
documentos += 1;
|
|
377
|
-
|
|
410
|
+
// Só publica se a CT tiver Draft & Publish (senão publish() lança erro).
|
|
411
|
+
if (publish && hasDraftAndPublish(ct.uid)) {
|
|
378
412
|
await strapi.documents(ct.uid).publish({ documentId: e.documentId, locale: tgt });
|
|
379
413
|
publicados += 1;
|
|
380
414
|
}
|
|
@@ -439,7 +473,7 @@ export const openAiToolSpecs = [
|
|
|
439
473
|
type: 'function',
|
|
440
474
|
function: {
|
|
441
475
|
name: 'publicar',
|
|
442
|
-
description: 'Publica a entrada (torna a alteração visível no site público). Passe "locale" para publicar um idioma específico, ou "*" para todos.',
|
|
476
|
+
description: 'Publica a entrada (torna a alteração visível no site público). Passe "locale" para publicar um idioma específico, ou "*" para todos. Se a content-type NÃO tiver Draft & Publish, não há o que publicar: retorna status "no-draft-publish" (a edição já está no ar) — avise o usuário em vez de tentar publicar de novo.',
|
|
443
477
|
parameters: {
|
|
444
478
|
type: 'object',
|
|
445
479
|
properties: {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
export default ({ strapi }: { strapi: any }) => ({
|
|
6
6
|
async message(ctx: any) {
|
|
7
|
-
const { messages, image, lang, previewUrl } = ctx.request.body || {};
|
|
7
|
+
const { messages, image, lang, previewUrl, autoPublish } = ctx.request.body || {};
|
|
8
8
|
if (!Array.isArray(messages) || messages.length === 0) {
|
|
9
9
|
return ctx.badRequest('Campo "messages" (array) é obrigatório.');
|
|
10
10
|
}
|
|
@@ -12,7 +12,7 @@ export default ({ strapi }: { strapi: any }) => ({
|
|
|
12
12
|
const result = await strapi
|
|
13
13
|
.plugin('mcp-chat')
|
|
14
14
|
.service('chat')
|
|
15
|
-
.chat({ messages, image, lang, previewUrl });
|
|
15
|
+
.chat({ messages, image, lang, previewUrl, autoPublish });
|
|
16
16
|
ctx.body = result;
|
|
17
17
|
} catch (e: any) {
|
|
18
18
|
strapi.log.error(`[mcp-chat] ${e?.message || e}`);
|
|
@@ -26,8 +26,13 @@ import type { LinkContext } from '../provision/adapters';
|
|
|
26
26
|
const MANIFEST_NAME = 'strapi.manifest.json';
|
|
27
27
|
|
|
28
28
|
function ensureInside(base: string, target: string): boolean {
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
// Resolve ambos para absoluto e compara via path.relative — robusto contra
|
|
30
|
+
// "..", separadores mistos e zip-slip (entrada do zip não pode escapar da pasta).
|
|
31
|
+
const b = path.resolve(base);
|
|
32
|
+
const t = path.resolve(target);
|
|
33
|
+
if (t === b) return true;
|
|
34
|
+
const rel = path.relative(b, t);
|
|
35
|
+
return !!rel && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
/** normaliza um nome livre para o formato kebab exigido pelo manifest. */
|
package/server/src/index.ts
CHANGED
|
@@ -31,7 +31,12 @@ export default {
|
|
|
31
31
|
},
|
|
32
32
|
destroy() {
|
|
33
33
|
// encerra o dev server do frontend (se foi iniciado pelo preview).
|
|
34
|
-
|
|
34
|
+
// Nunca deixa o shutdown do Strapi falhar por causa disto.
|
|
35
|
+
try {
|
|
36
|
+
stopFrontend();
|
|
37
|
+
} catch {
|
|
38
|
+
/* shutdown best-effort */
|
|
39
|
+
}
|
|
35
40
|
},
|
|
36
41
|
config: {
|
|
37
42
|
default: {},
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity functions para DEFINIR capacidades MCP (tools/resources/prompts) com
|
|
3
|
+
* inferência de tipos, desacopladas do registro — espelhando a direção do PR
|
|
4
|
+
* #26603 do Strapi (`ai.mcp.defineTool` / `defineResource` / `definePrompt` +
|
|
5
|
+
* o namespace `import { ai } from "@strapi/strapi"`).
|
|
6
|
+
*
|
|
7
|
+
* Hoje o MCP nativo (Strapi >= 5.47) expõe `strapi.ai.mcp.registerTool`. Estes
|
|
8
|
+
* wrappers locais dão a inferência dos `args` do handler a partir do schema de
|
|
9
|
+
* input AGORA (acaba com o `any`), e mantêm cada tool como uma DEFINIÇÃO pura,
|
|
10
|
+
* registrada a partir de um array (sem side-effects na definição). Quando o
|
|
11
|
+
* `ai.mcp.defineTool` estável sair, migrar é trocar o import destes helpers por
|
|
12
|
+
* `import { ai } from "@strapi/strapi"` — as definições não mudam.
|
|
13
|
+
*
|
|
14
|
+
* Nota (decisão consciente): NÃO dependemos do build experimental do PR (a API
|
|
15
|
+
* ainda está em debate — namespace global vs DI, `@strapi/ai` como pacote
|
|
16
|
+
* próprio, etc.). Ficamos prontos-pra-migrar em vez de acoplados ao instável.
|
|
17
|
+
*/
|
|
18
|
+
import type { z } from '@strapi/utils';
|
|
19
|
+
|
|
20
|
+
/** Resultado padrão de um handler de tool MCP. */
|
|
21
|
+
export type McpToolResult = {
|
|
22
|
+
content: Array<{ type: string; text?: string; [k: string]: any }>;
|
|
23
|
+
structuredContent?: any;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Autorização declarativa (mesma forma aceita pelo MCP nativo). */
|
|
27
|
+
export type McpAuth = { policies?: Array<{ action: string; [k: string]: any }> };
|
|
28
|
+
|
|
29
|
+
/** Definição de uma tool MCP, com `args` do handler inferidos do input schema. */
|
|
30
|
+
export type McpToolDef<S extends z.ZodTypeAny = z.ZodTypeAny> = {
|
|
31
|
+
name: string;
|
|
32
|
+
title?: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
resolveInputSchema: () => S;
|
|
35
|
+
resolveOutputSchema?: () => z.ZodTypeAny;
|
|
36
|
+
auth?: McpAuth;
|
|
37
|
+
createHandler: (
|
|
38
|
+
strapi: any
|
|
39
|
+
) => (ctx: { args: z.infer<S> }) => Promise<McpToolResult> | McpToolResult;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Identity function: devolve a definição inalterada, mas infere o genérico `S`
|
|
44
|
+
* do `resolveInputSchema`, tipando `ctx.args` no handler (sem `any`).
|
|
45
|
+
*/
|
|
46
|
+
export const defineTool = <S extends z.ZodTypeAny>(def: McpToolDef<S>): McpToolDef<S> => def;
|
|
47
|
+
|
|
48
|
+
// ── Resources / Prompts (alinhamento completo com o PR; ainda não usados) ──────
|
|
49
|
+
export type McpResourceDef = {
|
|
50
|
+
name: string;
|
|
51
|
+
uri: string;
|
|
52
|
+
title?: string;
|
|
53
|
+
description?: string;
|
|
54
|
+
mimeType?: string;
|
|
55
|
+
devModeOnly?: boolean;
|
|
56
|
+
auth?: McpAuth;
|
|
57
|
+
createHandler: (strapi: any) => (ctx: any) => Promise<any> | any;
|
|
58
|
+
};
|
|
59
|
+
export const defineResource = (def: McpResourceDef): McpResourceDef => def;
|
|
60
|
+
|
|
61
|
+
export type McpPromptDef<S extends z.ZodTypeAny = z.ZodTypeAny> = {
|
|
62
|
+
name: string;
|
|
63
|
+
title?: string;
|
|
64
|
+
description?: string;
|
|
65
|
+
resolveArgsSchema?: () => S;
|
|
66
|
+
devModeOnly?: boolean;
|
|
67
|
+
createHandler: (strapi: any) => (ctx: { args: z.infer<S> }) => Promise<any> | any;
|
|
68
|
+
};
|
|
69
|
+
export const definePrompt = <S extends z.ZodTypeAny>(def: McpPromptDef<S>): McpPromptDef<S> => def;
|
|
70
|
+
|
|
71
|
+
/** Assinatura do `registerTool` do MCP nativo (aceita uma McpToolDef). */
|
|
72
|
+
export type RegisterTool = (def: McpToolDef<any>) => void;
|
package/server/src/mcp/index.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Registra as tools de conteúdo do plugin no MCP server NATIVO da Strapi
|
|
3
|
-
* (>= 5.47.0).
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* (>= 5.47.0). Cada tool é uma DEFINIÇÃO pura (`defineTool`, em ./tools/*),
|
|
4
|
+
* agregada em ./tools/index.ts; aqui só percorremos o array chamando
|
|
5
|
+
* `mcp.registerTool(def)` (padrão "define + register-from-array").
|
|
6
|
+
*
|
|
7
|
+
* Alinhado à direção do PR #26603 (`ai.mcp.defineTool`): quando o helper
|
|
8
|
+
* estável sair, troca-se o import de `./define` por `import { ai } from
|
|
9
|
+
* "@strapi/strapi"` — as definições não mudam. Ver server/src/mcp/define.ts.
|
|
6
10
|
*
|
|
7
11
|
* Deve rodar no `register()` do plugin, ANTES de o MCP server iniciar.
|
|
8
12
|
*/
|
|
@@ -18,7 +22,16 @@ export const registerMcpTools = (strapi: any) => {
|
|
|
18
22
|
);
|
|
19
23
|
return;
|
|
20
24
|
}
|
|
21
|
-
|
|
22
|
-
for (const tool of tools)
|
|
23
|
-
|
|
25
|
+
let registered = 0;
|
|
26
|
+
for (const tool of tools) {
|
|
27
|
+
// Isola cada tool: uma definição com problema NUNCA aborta o registro das
|
|
28
|
+
// demais nem o boot do Strapi.
|
|
29
|
+
try {
|
|
30
|
+
mcp.registerTool(tool);
|
|
31
|
+
registered += 1;
|
|
32
|
+
} catch (e: any) {
|
|
33
|
+
strapi.log.warn(`[mcp-chat] tool "${tool?.name}" falhou ao registrar: ${e?.message ?? e}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
strapi.log.info(`[mcp-chat] ${registered}/${tools.length} tools registradas no MCP nativo (mcp_chat_*).`);
|
|
24
37
|
};
|
|
@@ -1,28 +1,22 @@
|
|
|
1
1
|
import { z } from '@strapi/utils';
|
|
2
|
-
import
|
|
2
|
+
import { defineTool } from '../define';
|
|
3
3
|
import { createContentTools } from '../../content-tools';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const r = await createContentTools(strapi).buscarTexto(args?.termo);
|
|
22
|
-
return { content: [{ type: 'text', text: JSON.stringify(r) }], structuredContent: r };
|
|
23
|
-
},
|
|
24
|
-
});
|
|
5
|
+
export default defineTool({
|
|
6
|
+
name: 'mcp_chat_buscar_texto',
|
|
7
|
+
title: 'Search text across content (deep)',
|
|
8
|
+
description:
|
|
9
|
+
'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.',
|
|
10
|
+
resolveInputSchema: () => z.object({ termo: z.string() }),
|
|
11
|
+
resolveOutputSchema: () =>
|
|
12
|
+
z.object({
|
|
13
|
+
total: z.number().optional(),
|
|
14
|
+
resultados: z.array(z.any()).optional(),
|
|
15
|
+
erro: z.string().optional(),
|
|
16
|
+
}),
|
|
17
|
+
auth: { policies: [{ action: 'plugin::content-manager.explorer.read' }] },
|
|
18
|
+
createHandler: (strapi: any) => async ({ args }) => {
|
|
19
|
+
const r = await createContentTools(strapi).buscarTexto(args.termo);
|
|
20
|
+
return { content: [{ type: 'text', text: JSON.stringify(r) }], structuredContent: r };
|
|
25
21
|
},
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export default tool;
|
|
22
|
+
});
|
|
@@ -1,30 +1,24 @@
|
|
|
1
1
|
import { z } from '@strapi/utils';
|
|
2
|
-
import
|
|
2
|
+
import { defineTool } from '../define';
|
|
3
3
|
import { createContentTools } from '../../content-tools';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const r = await createContentTools(strapi).criarLocale(args);
|
|
24
|
-
return { content: [{ type: 'text', text: JSON.stringify(r) }], structuredContent: r };
|
|
25
|
-
},
|
|
26
|
-
});
|
|
5
|
+
export default defineTool({
|
|
6
|
+
name: 'mcp_chat_criar_locale',
|
|
7
|
+
title: 'Create an i18n locale',
|
|
8
|
+
description:
|
|
9
|
+
'Create a locale (language). `code` must be a valid ISO code (e.g. "pt-BR", "es"). Idempotent: returns ok if it already exists.',
|
|
10
|
+
resolveInputSchema: () => z.object({ code: z.string(), name: z.string().optional() }),
|
|
11
|
+
resolveOutputSchema: () =>
|
|
12
|
+
z.object({
|
|
13
|
+
ok: z.boolean().optional(),
|
|
14
|
+
code: z.string().optional(),
|
|
15
|
+
name: z.string().optional(),
|
|
16
|
+
existed: z.boolean().optional(),
|
|
17
|
+
erro: z.string().optional(),
|
|
18
|
+
}),
|
|
19
|
+
auth: { policies: [{ action: 'plugin::i18n.locale.create' }] },
|
|
20
|
+
createHandler: (strapi: any) => async ({ args }) => {
|
|
21
|
+
const r = await createContentTools(strapi).criarLocale(args);
|
|
22
|
+
return { content: [{ type: 'text', text: JSON.stringify(r) }], structuredContent: r };
|
|
27
23
|
},
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export default tool;
|
|
24
|
+
});
|
|
@@ -1,39 +1,33 @@
|
|
|
1
1
|
import { z } from '@strapi/utils';
|
|
2
|
-
import
|
|
2
|
+
import { defineTool } from '../define';
|
|
3
3
|
import { createContentTools } from '../../content-tools';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const r = await createContentTools(strapi).editarCampo(args);
|
|
33
|
-
return { content: [{ type: 'text', text: JSON.stringify(r) }], structuredContent: r };
|
|
34
|
-
},
|
|
35
|
-
});
|
|
5
|
+
export default defineTool({
|
|
6
|
+
name: 'mcp_chat_editar_campo',
|
|
7
|
+
title: 'Edit a (possibly nested) field',
|
|
8
|
+
description:
|
|
9
|
+
'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`.',
|
|
10
|
+
resolveInputSchema: () =>
|
|
11
|
+
z.object({
|
|
12
|
+
uid: z.string(),
|
|
13
|
+
documentId: z.string(),
|
|
14
|
+
path: z.array(z.union([z.string(), z.number()])).optional(),
|
|
15
|
+
campo: z.string().optional(),
|
|
16
|
+
novo_valor: z.string(),
|
|
17
|
+
locale: z.string().optional(),
|
|
18
|
+
}),
|
|
19
|
+
resolveOutputSchema: () =>
|
|
20
|
+
z.object({
|
|
21
|
+
ok: z.boolean().optional(),
|
|
22
|
+
uid: z.string().optional(),
|
|
23
|
+
documentId: z.string().optional(),
|
|
24
|
+
path: z.array(z.any()).optional(),
|
|
25
|
+
novo_valor: z.string().optional(),
|
|
26
|
+
erro: z.string().optional(),
|
|
27
|
+
}),
|
|
28
|
+
auth: { policies: [{ action: 'plugin::content-manager.explorer.update' }] },
|
|
29
|
+
createHandler: (strapi: any) => async ({ args }) => {
|
|
30
|
+
const r = await createContentTools(strapi).editarCampo(args);
|
|
31
|
+
return { content: [{ type: 'text', text: JSON.stringify(r) }], structuredContent: r };
|
|
36
32
|
},
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export default tool;
|
|
33
|
+
});
|
|
@@ -1,33 +1,27 @@
|
|
|
1
1
|
import { z } from '@strapi/utils';
|
|
2
|
-
import
|
|
2
|
+
import { defineTool } from '../define';
|
|
3
3
|
import { enableI18n } from '../../provision/enable-i18n';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const r = enableI18n({ strapi, uid: args?.uid, campos: args?.campos });
|
|
27
|
-
return { content: [{ type: 'text', text: JSON.stringify(r) }], structuredContent: r };
|
|
28
|
-
},
|
|
29
|
-
});
|
|
5
|
+
export default defineTool({
|
|
6
|
+
name: 'mcp_chat_habilitar_i18n',
|
|
7
|
+
title: 'Enable i18n on a content-type',
|
|
8
|
+
description:
|
|
9
|
+
'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.',
|
|
10
|
+
resolveInputSchema: () =>
|
|
11
|
+
z.object({ uid: z.string().optional(), campos: z.array(z.string()).optional() }),
|
|
12
|
+
resolveOutputSchema: () =>
|
|
13
|
+
z.object({
|
|
14
|
+
ok: z.boolean().optional(),
|
|
15
|
+
uid: z.string().optional(),
|
|
16
|
+
campos: z.array(z.string()).optional(),
|
|
17
|
+
contentTypes: z.array(z.any()).optional(),
|
|
18
|
+
total: z.number().optional(),
|
|
19
|
+
restart: z.boolean().optional(),
|
|
20
|
+
erro: z.string().optional(),
|
|
21
|
+
}),
|
|
22
|
+
auth: { policies: [{ action: 'plugin::content-type-builder.read' }] },
|
|
23
|
+
createHandler: (strapi: any) => async ({ args }) => {
|
|
24
|
+
const r = enableI18n({ strapi, uid: args.uid, campos: args.campos });
|
|
25
|
+
return { content: [{ type: 'text', text: JSON.stringify(r) }], structuredContent: r };
|
|
30
26
|
},
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export default tool;
|
|
27
|
+
});
|
|
@@ -1,27 +1,21 @@
|
|
|
1
1
|
import { z } from '@strapi/utils';
|
|
2
|
-
import
|
|
2
|
+
import { defineTool } from '../define';
|
|
3
3
|
import { createContentTools } from '../../content-tools';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const r = await createContentTools(strapi).listarLocales();
|
|
21
|
-
return { content: [{ type: 'text', text: JSON.stringify(r) }], structuredContent: r };
|
|
22
|
-
},
|
|
23
|
-
});
|
|
5
|
+
export default defineTool({
|
|
6
|
+
name: 'mcp_chat_listar_locales',
|
|
7
|
+
title: 'List i18n locales',
|
|
8
|
+
description: 'List the configured locales (languages) and which one is the default.',
|
|
9
|
+
resolveInputSchema: () => z.object({}),
|
|
10
|
+
resolveOutputSchema: () =>
|
|
11
|
+
z.object({
|
|
12
|
+
default: z.string().optional(),
|
|
13
|
+
locales: z.array(z.any()).optional(),
|
|
14
|
+
erro: z.string().optional(),
|
|
15
|
+
}),
|
|
16
|
+
auth: { policies: [{ action: 'plugin::content-manager.explorer.read' }] },
|
|
17
|
+
createHandler: (strapi: any) => async () => {
|
|
18
|
+
const r = await createContentTools(strapi).listarLocales();
|
|
19
|
+
return { content: [{ type: 'text', text: JSON.stringify(r) }], structuredContent: r };
|
|
24
20
|
},
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
export default tool;
|
|
21
|
+
});
|
|
@@ -1,31 +1,25 @@
|
|
|
1
1
|
import { z } from '@strapi/utils';
|
|
2
|
-
import
|
|
2
|
+
import { defineTool } from '../define';
|
|
3
3
|
import { createContentTools } from '../../content-tools';
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const r = await createContentTools(strapi).publicar(args);
|
|
25
|
-
return { content: [{ type: 'text', text: JSON.stringify(r) }], structuredContent: r };
|
|
26
|
-
},
|
|
27
|
-
});
|
|
5
|
+
export default defineTool({
|
|
6
|
+
name: 'mcp_chat_publicar',
|
|
7
|
+
title: 'Publish an entry',
|
|
8
|
+
description:
|
|
9
|
+
'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") — the edit is already live.',
|
|
10
|
+
resolveInputSchema: () =>
|
|
11
|
+
z.object({ uid: z.string(), documentId: z.string(), locale: z.string().optional() }),
|
|
12
|
+
resolveOutputSchema: () =>
|
|
13
|
+
z.object({
|
|
14
|
+
ok: z.boolean().optional(),
|
|
15
|
+
uid: z.string().optional(),
|
|
16
|
+
documentId: z.string().optional(),
|
|
17
|
+
status: z.string().optional(),
|
|
18
|
+
locale: z.string().optional(),
|
|
19
|
+
}),
|
|
20
|
+
auth: { policies: [{ action: 'plugin::content-manager.explorer.publish' }] },
|
|
21
|
+
createHandler: (strapi: any) => async ({ args }) => {
|
|
22
|
+
const r = await createContentTools(strapi).publicar(args);
|
|
23
|
+
return { content: [{ type: 'text', text: JSON.stringify(r) }], structuredContent: r };
|
|
28
24
|
},
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export default tool;
|
|
25
|
+
});
|