
Skill
strapi-mcp-capabilities
create custom MCP capabilities for Strapi
Description
Create custom MCP capabilities (tools, prompts, resources) in a Strapi 5 plugin via the strapi.ai.mcp service. Use when adding a custom MCP tool/prompt/resource to Strapi, extending the Strapi MCP server.
SKILL.md
Strapi MCP Capabilities
Register custom MCP tools/prompts/resources so AI clients can trigger plugin-specific actions on a running Strapi instance. Strapi 5.47.0+. API source of truth: @strapi/types, search mcp.
Hard rules
- Enable the server:
config/server.tsmust setmcp: { enabled: true }. Endpoint isPOST /mcp. - Register only in the
register()phase. The capability list is locked once the MCP server starts (during bootstrap). Registering later throws. - Use Zod from
@strapi/utils(import { z } from '@strapi/utils'), not standalonezod. - Never use global
strapiat module top-level. Either register inline (havestrapi) or useai.mcp.defineTool(...)to define at module scope, then register with the injectedstrapi.
Two registration patterns
Inline — when strapi is in scope (a function called from register()):
// mcp/tools/get-store-kpis.ts
import type { Core } from '@strapi/strapi';
import { z } from '@strapi/utils';
export const registerGetStoreKpisTool = (strapi: Core.Strapi) => {
strapi.ai.mcp.registerTool({ /* definition */ });
};
Builder — define at module scope (no strapi), register later. defineTool is an identity helper that narrows the access variant for type inference:
// mcp/tools/get-top-products.ts
import { ai } from '@strapi/strapi';
import { z } from '@strapi/utils';
export const getTopProductsTool = ai.mcp.defineTool({ /* definition */ });
// register.ts
strapi.ai.mcp.registerTool(getTopProductsTool);
Wire both from the plugin register():
// register.ts
export const register = ({ strapi }: { strapi: Core.Strapi }) => {
registerGetStoreKpisTool(strapi); // inline
strapi.ai.mcp.registerTool(getTopProductsTool); // builder
};
Tool definition (the core unit)
strapi.ai.mcp.registerTool({
name: 'get_store_kpis', // snake_case, unique
title: 'Get Store KPIs',
description: 'What it returns + when to use it. The AI reads this to choose the tool.',
auth: { policies: [{ action: 'plugin::store-analytics.read' }] }, // OR devModeOnly: true
resolveInputSchema: () => inputSchema, // optional; omit if no args
resolveOutputSchema: () => outputSchema, // required; a z.object
createHandler: (strapi, context) => async ({ args, extra }) => {
const result = await strapi
.plugin('store-analytics')
.service('analytics')
.getStoreKpis(args);
return {
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
structuredContent: result, // must match resolveOutputSchema
};
},
});
Key points:
createHandler(strapi, context) => handler. The handler receives a single object{ args, extra }.argsisz.infer<inputSchema>(typed); if noresolveInputSchema, omitargs.context: { userAbility, user }— useuserAbilityfor entity/field permission checks anduser(token owner) forsetCreatorFieldson writes.- Keep handlers thin: delegate to a plugin service; the tool is just the MCP surface.
name/title/description/resolveOutputSchemaare required.descriptionquality drives AI tool selection — state what it returns and when to use it.
Schemas
- Define
z.objectinput/output schemas; reuse shared field schemas (e.g. anisoDateSchemawith.describe(...)). .describe()every non-obvious field — descriptions reach the AI client.structuredContentreturned by the handler must satisfyresolveOutputSchema.
Auth (choose exactly one access variant)
auth: { policies: [{ action, subject? }] }— CASL check; session passes if the token's ability satisfies any policy.actionis typically a registered admin permission, e.g.plugin::<plugin-id>.<permission>.devModeOnly: true— no auth; only available whenautoReloadis on (dev). Use for debug tools.
For auth to be grantable to a token, register the permission action in bootstrap():
await strapi.service('admin::permission').actionProvider.registerMany([
{ section: 'plugins', displayName: 'Access store analytics',
uid: 'read', pluginName: 'store-analytics' },
]);
// → action becomes 'plugin::store-analytics.read'
Handler return shape
// success
return { content: [{ type: 'text', text }], structuredContent: result };
// error
return { content: [{ type: 'text', text: 'message' }], isError: true };
structuredContent and isError are mutually exclusive.
Folder convention
server/src/
├── register.ts # calls registrations (register phase only)
├── bootstrap.ts # registerMany permission actions
├── mcp/
│ ├── index.ts # registerAllMcpTools(strapi)
│ ├── schemas.ts # shared zod field schemas
│ └── tools/<tool>.ts # one capability per file
└── services/ # business logic the handlers call
Verify
mcp: { enabled: true }inconfig/server.ts; restart Strapi.- Connect a client to
http://localhost:1337/mcpwithAuthorization: Bearer <admin-token>whose permissions include the tool'sauthaction. - Confirm the tool appears and returns
structuredContentmatching its output schema.
Prompts & resources
Same lifecycle/auth rules, different builders: ai.mcp.definePrompt / strapi.ai.mcp.registerPrompt and ai.mcp.defineResource / strapi.ai.mcp.registerResource. See reference.md for their field tables and signatures.
Docs
- MCP server (config, permission boundaries, stateless architecture, limitations)
- API types:
@strapi/types