strapi-plugin-mcp-chat 0.3.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -45,20 +45,24 @@ export function createContentTools(strapi: any) {
45
45
 
46
46
  // Populate profundo: components (simples/repetíveis), dynamic zones (com `on`
47
47
  // por componente) e mídia/relações. `seen` evita recursão infinita.
48
- const buildPopulate = (attributes: Record<string, any>, seen = new Set<string>()): any => {
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 {};
49
53
  const populate: any = {};
50
54
  for (const [name, a] of Object.entries(attributes) as any[]) {
51
55
  if (a.type === 'component' && a.component) {
52
56
  const sub = seen.has(a.component)
53
57
  ? {}
54
- : buildPopulate(attrsOf(a.component), new Set(seen).add(a.component));
58
+ : buildPopulate(attrsOf(a.component), new Set(seen).add(a.component), depth + 1);
55
59
  populate[name] = Object.keys(sub).length ? { populate: sub } : true;
56
60
  } else if (a.type === 'dynamiczone') {
57
61
  const on: any = {};
58
62
  for (const comp of a.components || []) {
59
63
  const sub = seen.has(comp)
60
64
  ? {}
61
- : buildPopulate(attrsOf(comp), new Set(seen).add(comp));
65
+ : buildPopulate(attrsOf(comp), new Set(seen).add(comp), depth + 1);
62
66
  on[comp] = Object.keys(sub).length ? { populate: sub } : true;
63
67
  }
64
68
  populate[name] = { on };
@@ -77,6 +81,7 @@ export function createContentTools(strapi: any) {
77
81
  collect: (path: (string | number)[], campo: string, valor: string) => void
78
82
  ) => {
79
83
  if (!node || typeof node !== 'object') return;
84
+ if (basePath.length > 24) return; // teto rígido contra recursão patológica
80
85
  for (const [name, a] of Object.entries(attributes) as any[]) {
81
86
  const v = node[name];
82
87
  if (v == null) continue;
@@ -102,11 +107,13 @@ export function createContentTools(strapi: any) {
102
107
  }
103
108
  };
104
109
 
110
+ const MAX_MATCHES = 100;
105
111
  const buscarTexto = async (termo: string) => {
106
112
  const needle = String(termo || '').toLowerCase().trim();
107
113
  if (!needle) return { erro: 'termo vazio' };
108
114
  const matches: any[] = [];
109
115
  for (const ct of apiContentTypes() as any[]) {
116
+ if (matches.length >= MAX_MATCHES) break; // teto: não varre além do necessário
110
117
  const attributes = ct.attributes || {};
111
118
  const populate = buildPopulate(attributes);
112
119
  let entries: any[] = [];
@@ -135,7 +142,12 @@ export function createContentTools(strapi: any) {
135
142
  });
136
143
  }
137
144
  }
138
- return { total: matches.length, resultados: matches };
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
+ };
139
151
  };
140
152
 
141
153
  // Converte um nó populado de volta a forma gravável: preserva `id` (p/ Strapi
@@ -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
- const n = path.normalize(target);
30
- return n === base || n.startsWith(base + path.sep);
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. */
@@ -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
- stopFrontend();
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;
@@ -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). Estrutura modular inspirada no padrão do exemplo do Paul
4
- * Bratslavsky: cada tool é um módulo em ./tools/*, agregado em ./tools/index.ts,
5
- * e aqui passamos por um loop chamando `tool.register(registerTool, strapi)`.
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
- const { registerTool } = mcp;
22
- for (const tool of tools) tool.register(registerTool, strapi);
23
- strapi.log.info(`[mcp-chat] ${tools.length} tools registradas no MCP nativo (mcp_chat_*).`);
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 type { StrapiMcpToolModule } from '../types';
2
+ import { defineTool } from '../define';
3
3
  import { createContentTools } from '../../content-tools';
4
4
 
5
- const tool: StrapiMcpToolModule = {
6
- register(registerTool) {
7
- registerTool({
8
- name: 'mcp_chat_buscar_texto',
9
- title: 'Search text across content (deep)',
10
- description:
11
- '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.',
12
- resolveInputSchema: () => z.object({ termo: z.string() }),
13
- resolveOutputSchema: () =>
14
- z.object({
15
- total: z.number().optional(),
16
- resultados: z.array(z.any()).optional(),
17
- erro: z.string().optional(),
18
- }),
19
- auth: { policies: [{ action: 'plugin::content-manager.explorer.read' }] },
20
- createHandler: (strapi: any) => async ({ args }: any) => {
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 type { StrapiMcpToolModule } from '../types';
2
+ import { defineTool } from '../define';
3
3
  import { createContentTools } from '../../content-tools';
4
4
 
5
- const tool: StrapiMcpToolModule = {
6
- register(registerTool) {
7
- registerTool({
8
- name: 'mcp_chat_criar_locale',
9
- title: 'Create an i18n locale',
10
- description:
11
- 'Create a locale (language). `code` must be a valid ISO code (e.g. "pt-BR", "es"). Idempotent: returns ok if it already exists.',
12
- resolveInputSchema: () => z.object({ code: z.string(), name: z.string().optional() }),
13
- resolveOutputSchema: () =>
14
- z.object({
15
- ok: z.boolean().optional(),
16
- code: z.string().optional(),
17
- name: z.string().optional(),
18
- existed: z.boolean().optional(),
19
- erro: z.string().optional(),
20
- }),
21
- auth: { policies: [{ action: 'plugin::i18n.locale.create' }] },
22
- createHandler: (strapi: any) => async ({ args }: any) => {
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 type { StrapiMcpToolModule } from '../types';
2
+ import { defineTool } from '../define';
3
3
  import { createContentTools } from '../../content-tools';
4
4
 
5
- const tool: StrapiMcpToolModule = {
6
- register(registerTool) {
7
- registerTool({
8
- name: 'mcp_chat_editar_campo',
9
- title: 'Edit a (possibly nested) field',
10
- description:
11
- '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`.',
12
- resolveInputSchema: () =>
13
- z.object({
14
- uid: z.string(),
15
- documentId: z.string(),
16
- path: z.array(z.union([z.string(), z.number()])).optional(),
17
- campo: z.string().optional(),
18
- novo_valor: z.string(),
19
- locale: z.string().optional(),
20
- }),
21
- resolveOutputSchema: () =>
22
- z.object({
23
- ok: z.boolean().optional(),
24
- uid: z.string().optional(),
25
- documentId: z.string().optional(),
26
- path: z.array(z.any()).optional(),
27
- novo_valor: z.string().optional(),
28
- erro: z.string().optional(),
29
- }),
30
- auth: { policies: [{ action: 'plugin::content-manager.explorer.update' }] },
31
- createHandler: (strapi: any) => async ({ args }: any) => {
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 type { StrapiMcpToolModule } from '../types';
2
+ import { defineTool } from '../define';
3
3
  import { enableI18n } from '../../provision/enable-i18n';
4
4
 
5
- const tool: StrapiMcpToolModule = {
6
- register(registerTool) {
7
- registerTool({
8
- name: 'mcp_chat_habilitar_i18n',
9
- title: 'Enable i18n on a content-type',
10
- description:
11
- '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.',
12
- resolveInputSchema: () =>
13
- z.object({ uid: z.string().optional(), campos: z.array(z.string()).optional() }),
14
- resolveOutputSchema: () =>
15
- z.object({
16
- ok: z.boolean().optional(),
17
- uid: z.string().optional(),
18
- campos: z.array(z.string()).optional(),
19
- contentTypes: z.array(z.any()).optional(),
20
- total: z.number().optional(),
21
- restart: z.boolean().optional(),
22
- erro: z.string().optional(),
23
- }),
24
- auth: { policies: [{ action: 'plugin::content-type-builder.read' }] },
25
- createHandler: (strapi: any) => async ({ args }: any) => {
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 type { StrapiMcpToolModule } from '../types';
2
+ import { defineTool } from '../define';
3
3
  import { createContentTools } from '../../content-tools';
4
4
 
5
- const tool: StrapiMcpToolModule = {
6
- register(registerTool) {
7
- registerTool({
8
- name: 'mcp_chat_listar_locales',
9
- title: 'List i18n locales',
10
- description: 'List the configured locales (languages) and which one is the default.',
11
- resolveInputSchema: () => z.object({}),
12
- resolveOutputSchema: () =>
13
- z.object({
14
- default: z.string().optional(),
15
- locales: z.array(z.any()).optional(),
16
- erro: z.string().optional(),
17
- }),
18
- auth: { policies: [{ action: 'plugin::content-manager.explorer.read' }] },
19
- createHandler: (strapi: any) => async () => {
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 type { StrapiMcpToolModule } from '../types';
2
+ import { defineTool } from '../define';
3
3
  import { createContentTools } from '../../content-tools';
4
4
 
5
- const tool: StrapiMcpToolModule = {
6
- register(registerTool) {
7
- registerTool({
8
- name: 'mcp_chat_publicar',
9
- title: 'Publish an entry',
10
- description:
11
- 'Publish an entry by uid + documentId, making the change visible on the site. Pass `locale` to publish a specific language, or "*" for all.',
12
- resolveInputSchema: () =>
13
- z.object({ uid: z.string(), documentId: z.string(), locale: z.string().optional() }),
14
- resolveOutputSchema: () =>
15
- z.object({
16
- ok: z.boolean().optional(),
17
- uid: z.string().optional(),
18
- documentId: z.string().optional(),
19
- status: z.string().optional(),
20
- locale: z.string().optional(),
21
- }),
22
- auth: { policies: [{ action: 'plugin::content-manager.explorer.publish' }] },
23
- createHandler: (strapi: any) => async ({ args }: any) => {
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
+ });
@@ -1,36 +1,30 @@
1
1
  import { z } from '@strapi/utils';
2
- import type { StrapiMcpToolModule } from '../types';
2
+ import { defineTool } from '../define';
3
3
  import { createContentTools } from '../../content-tools';
4
4
 
5
- const tool: StrapiMcpToolModule = {
6
- register(registerTool) {
7
- registerTool({
8
- name: 'mcp_chat_traduzir',
9
- title: 'Translate localized content',
10
- description:
11
- '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.',
12
- resolveInputSchema: () =>
13
- z.object({
14
- target_locales: z.array(z.string()).min(1),
15
- source_locale: z.string().optional(),
16
- uid: z.string().optional(),
17
- documentId: z.string().optional(),
18
- publish: z.boolean().optional(),
19
- }),
20
- resolveOutputSchema: () =>
21
- z.object({
22
- ok: z.boolean().optional(),
23
- source: z.string().optional(),
24
- por_locale: z.array(z.any()).optional(),
25
- erro: z.string().optional(),
26
- }),
27
- auth: { policies: [{ action: 'plugin::content-manager.explorer.update' }] },
28
- createHandler: (strapi: any) => async ({ args }: any) => {
29
- const r = await createContentTools(strapi).traduzir(args);
30
- return { content: [{ type: 'text', text: JSON.stringify(r) }], structuredContent: r };
31
- },
32
- });
5
+ export default defineTool({
6
+ name: 'mcp_chat_traduzir',
7
+ title: 'Translate localized content',
8
+ description:
9
+ '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.',
10
+ resolveInputSchema: () =>
11
+ z.object({
12
+ target_locales: z.array(z.string()).min(1),
13
+ source_locale: z.string().optional(),
14
+ uid: z.string().optional(),
15
+ documentId: z.string().optional(),
16
+ publish: z.boolean().optional(),
17
+ }),
18
+ resolveOutputSchema: () =>
19
+ z.object({
20
+ ok: z.boolean().optional(),
21
+ source: z.string().optional(),
22
+ por_locale: z.array(z.any()).optional(),
23
+ erro: z.string().optional(),
24
+ }),
25
+ auth: { policies: [{ action: 'plugin::content-manager.explorer.update' }] },
26
+ createHandler: (strapi: any) => async ({ args }) => {
27
+ const r = await createContentTools(strapi).traduzir(args);
28
+ return { content: [{ type: 'text', text: JSON.stringify(r) }], structuredContent: r };
33
29
  },
34
- };
35
-
36
- export default tool;
30
+ });
@@ -1,11 +1,14 @@
1
1
  /**
2
- * Tipos para os módulos de tool do MCP (padrão inspirado no exemplo do Paul
3
- * Bratslavsky: github.com/PaulBratslavsky/strapi-mcp-demo-and-tool-extension).
4
- * Cada tool é um módulo com um `register(registerTool, strapi)`.
2
+ * Tipos do MCP do plugin. As tools agora são DEFINIÇÕES puras criadas com
3
+ * `defineTool` (ver ./define.ts) e registradas a partir de um array — alinhado
4
+ * à direção do PR #26603 (`ai.mcp.defineTool`). Re-exportamos os tipos de
5
+ * `./define` aqui por conveniência/compatibilidade.
5
6
  */
6
-
7
- export type RegisterTool = (toolDef: Record<string, any>) => void;
8
-
9
- export type StrapiMcpToolModule = {
10
- register: (registerTool: RegisterTool, strapi: any) => void;
11
- };
7
+ export type {
8
+ McpToolDef,
9
+ McpResourceDef,
10
+ McpPromptDef,
11
+ McpToolResult,
12
+ McpAuth,
13
+ RegisterTool,
14
+ } from './define';