Strapi MCP Server
by l33tdawg
- src
#!/usr/bin/env node
/**
* Strapi MCP Server
*
* This MCP server integrates with any Strapi CMS instance to provide:
* - Access to Strapi content types as resources
* - Tools to create and update content types in Strapi
* - Tools to manage content entries (create, read, update, delete)
* - Support for Strapi in development mode
*
* This server is designed to be generic and work with any Strapi instance,
* regardless of the content types defined in that instance.
*/
import { Server } from "@modelcontextprotocol/sdk/server/index";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio";
import {
CallToolRequestSchema,
ListResourcesRequestSchema,
ListToolsRequestSchema,
ReadResourceRequestSchema,
ErrorCode,
McpError,
ReadResourceRequest,
CallToolRequest,
} from "@modelcontextprotocol/sdk/types";
import axios from "axios";
// Configuration from environment variables
const STRAPI_URL = process.env.STRAPI_URL || "http://localhost:1337";
const STRAPI_API_TOKEN = process.env.STRAPI_API_TOKEN;
const STRAPI_DEV_MODE = process.env.STRAPI_DEV_MODE === "true";
// Validate required environment variables
if (!STRAPI_API_TOKEN) {
console.error("[Error] Missing STRAPI_API_TOKEN environment variable");
process.exit(1);
}
console.error(`[Setup] Connecting to Strapi at ${STRAPI_URL}`);
console.error(`[Setup] Development mode: ${STRAPI_DEV_MODE ? "enabled" : "disabled"}`);
// Axios instance for Strapi API
const strapiClient = axios.create({
baseURL: STRAPI_URL,
headers: {
Authorization: `Bearer ${STRAPI_API_TOKEN}`,
"Content-Type": "application/json",
},
});
// Cache for content types
let contentTypesCache: any[] = [];
/**
* Create an MCP server with capabilities for resources and tools
*/
const server = new Server(
{
name: "strapi-mcp",
version: "0.1.0",
},
{
capabilities: {
resources: {},
tools: {},
},
}
);
/**
* Fetch all content types from Strapi
*/
async function fetchContentTypes(): Promise<any[]> {
try {
console.error("[API] Fetching content types from Strapi");
// If we have cached content types, return them
if (contentTypesCache.length > 0) {
return contentTypesCache;
}
let endpoint = "/api/content-types";
// If in development mode, use the content-type-builder API
if (STRAPI_DEV_MODE) {
endpoint = "/content-type-builder/content-types";
}
// Get the list of content types from Strapi
const response = await strapiClient.get(endpoint);
// Filter out internal content types
const contentTypes = response.data.data.filter((ct: any) =>
!ct.uid.startsWith("admin::") &&
!ct.uid.startsWith("plugin::")
);
// Cache the content types
contentTypesCache = contentTypes;
return contentTypes;
} catch (error) {
console.error("[Error] Failed to fetch content types:", error);
throw new McpError(
ErrorCode.InternalError,
`Failed to fetch content types: ${error instanceof Error ? error.message : String(error)}`
);
}
}
/**
* Interface for query parameters
*/
interface QueryParams {
filters?: Record<string, any>;
pagination?: {
page?: number;
pageSize?: number;
};
sort?: string[];
populate?: string | string[] | Record<string, any>;
}
/**
* Fetch entries for a specific content type with optional filtering, pagination, and sorting
*/
async function fetchEntries(contentType: string, queryParams?: QueryParams): Promise<any> {
try {
console.error(`[API] Fetching entries for content type: ${contentType}`);
// Extract the collection name from the content type UID
const collection = contentType.split(".")[1];
// Build query parameters
const params: Record<string, any> = {};
if (queryParams?.filters) {
params.filters = queryParams.filters;
}
if (queryParams?.pagination) {
params.pagination = queryParams.pagination;
}
if (queryParams?.sort) {
params.sort = queryParams.sort;
}
if (queryParams?.populate) {
params.populate = queryParams.populate;
}
// Get the entries from Strapi
const response = await strapiClient.get(`/api/${collection}`, {
params: params
});
// Return both data and pagination info
return {
data: response.data.data || [],
meta: response.data.meta || {}
};
} catch (error) {
console.error(`[Error] Failed to fetch entries for ${contentType}:`, error);
throw new McpError(
ErrorCode.InternalError,
`Failed to fetch entries for ${contentType}: ${error instanceof Error ? error.message : String(error)}`
);
}
}
/**
* Fetch a specific entry by ID
*/
async function fetchEntry(contentType: string, id: string): Promise<any> {
try {
console.error(`[API] Fetching entry ${id} for content type: ${contentType}`);
// Extract the collection name from the content type UID
const collection = contentType.split(".")[1];
// Get the entry from Strapi
const response = await strapiClient.get(`/api/${collection}/${id}`);
return response.data.data;
} catch (error) {
console.error(`[Error] Failed to fetch entry ${id} for ${contentType}:`, error);
throw new McpError(
ErrorCode.InternalError,
`Failed to fetch entry ${id} for ${contentType}: ${error instanceof Error ? error.message : String(error)}`
);
}
}
/**
* Create a new entry
*/
async function createEntry(contentType: string, data: any): Promise<any> {
try {
console.error(`[API] Creating new entry for content type: ${contentType}`);
// Extract the collection name from the content type UID
const collection = contentType.split(".")[1];
// Create the entry in Strapi
const response = await strapiClient.post(`/api/${collection}`, {
data: data
});
return response.data.data;
} catch (error) {
console.error(`[Error] Failed to create entry for ${contentType}:`, error);
throw new McpError(
ErrorCode.InternalError,
`Failed to create entry for ${contentType}: ${error instanceof Error ? error.message : String(error)}`
);
}
}
/**
* Update an existing entry
*/
async function updateEntry(contentType: string, id: string, data: any): Promise<any> {
try {
console.error(`[API] Updating entry ${id} for content type: ${contentType}`);
// Extract the collection name from the content type UID
const collection = contentType.split(".")[1];
// Update the entry in Strapi
const response = await strapiClient.put(`/api/${collection}/${id}`, {
data: data
});
return response.data.data;
} catch (error) {
console.error(`[Error] Failed to update entry ${id} for ${contentType}:`, error);
throw new McpError(
ErrorCode.InternalError,
`Failed to update entry ${id} for ${contentType}: ${error instanceof Error ? error.message : String(error)}`
);
}
}
/**
* Delete an entry
*/
async function deleteEntry(contentType: string, id: string): Promise<void> {
try {
console.error(`[API] Deleting entry ${id} for content type: ${contentType}`);
// Extract the collection name from the content type UID
const collection = contentType.split(".")[1];
// Delete the entry from Strapi
await strapiClient.delete(`/api/${collection}/${id}`);
} catch (error) {
console.error(`[Error] Failed to delete entry ${id} for ${contentType}:`, error);
throw new McpError(
ErrorCode.InternalError,
`Failed to delete entry ${id} for ${contentType}: ${error instanceof Error ? error.message : String(error)}`
);
}
}
/**
* Upload a media file to Strapi
*/
async function uploadMedia(fileData: string, fileName: string, fileType: string): Promise<any> {
try {
console.error(`[API] Uploading media file: ${fileName}`);
// Convert base64 data to buffer
const base64Data = fileData.replace(/^data:([A-Za-z-+/]+);base64,/, '');
const buffer = Buffer.from(base64Data, 'base64');
// Create form data
// Note: We need to dynamically import form-data since it's a CommonJS module
const { default: FormData } = await import('form-data');
const form = new FormData();
form.append('files', buffer, {
filename: fileName,
contentType: fileType,
});
// Upload the file to Strapi
const response = await axios.post(`${STRAPI_URL}/api/upload`, form, {
headers: {
...form.getHeaders(),
Authorization: `Bearer ${STRAPI_API_TOKEN}`,
},
});
return response.data[0];
} catch (error) {
console.error(`[Error] Failed to upload media:`, error);
throw new McpError(
ErrorCode.InternalError,
`Failed to upload media: ${error instanceof Error ? error.message : String(error)}`
);
}
}
/**
* Handler for listing available Strapi content as resources.
* Each content type and entry is exposed as a resource with:
* - A strapi:// URI scheme
* - JSON MIME type
* - Human readable name and description
*/
server.setRequestHandler(ListResourcesRequestSchema, async () => {
try {
// Fetch all content types
const contentTypes = await fetchContentTypes();
// Create a resource for each content type
const contentTypeResources = contentTypes.map(ct => ({
uri: `strapi://content-type/${ct.uid}`,
mimeType: "application/json",
name: ct.info.displayName,
description: `Strapi content type: ${ct.info.displayName}`
}));
// Return the resources
return {
resources: contentTypeResources
};
} catch (error) {
console.error("[Error] Failed to list resources:", error);
throw new McpError(
ErrorCode.InternalError,
`Failed to list resources: ${error instanceof Error ? error.message : String(error)}`
);
}
});
/**
* Handler for reading the contents of a specific resource.
* Takes a strapi:// URI and returns the content as JSON.
*
* Supports URIs in the following formats:
* - strapi://content-type/[contentTypeUid] - Get all entries for a content type
* - strapi://content-type/[contentTypeUid]/[entryId] - Get a specific entry
* - strapi://content-type/[contentTypeUid]?[queryParams] - Get filtered entries
*/
server.setRequestHandler(ReadResourceRequestSchema, async (request: ReadResourceRequest) => {
try {
const uri = request.params.uri;
// Parse the URI for content type
const contentTypeMatch = uri.match(/^strapi:\/\/content-type\/([^\/\?]+)(?:\/([^\/\?]+))?(?:\?(.+))?$/);
if (!contentTypeMatch) {
throw new McpError(
ErrorCode.InvalidRequest,
`Invalid URI format: ${uri}`
);
}
const contentTypeUid = contentTypeMatch[1];
const entryId = contentTypeMatch[2];
const queryString = contentTypeMatch[3];
// Parse query parameters if present
let queryParams: QueryParams = {};
if (queryString) {
try {
// Parse the query string into an object
const parsedParams = new URLSearchParams(queryString);
// Extract filters
const filtersParam = parsedParams.get('filters');
if (filtersParam) {
queryParams.filters = JSON.parse(filtersParam);
}
// Extract pagination
const pageParam = parsedParams.get('page');
const pageSizeParam = parsedParams.get('pageSize');
if (pageParam || pageSizeParam) {
queryParams.pagination = {};
if (pageParam) queryParams.pagination.page = parseInt(pageParam, 10);
if (pageSizeParam) queryParams.pagination.pageSize = parseInt(pageSizeParam, 10);
}
// Extract sort
const sortParam = parsedParams.get('sort');
if (sortParam) {
queryParams.sort = sortParam.split(',');
}
// Extract populate
const populateParam = parsedParams.get('populate');
if (populateParam) {
try {
// Try to parse as JSON
queryParams.populate = JSON.parse(populateParam);
} catch {
// If not valid JSON, treat as comma-separated string
queryParams.populate = populateParam.split(',');
}
}
} catch (parseError) {
console.error("[Error] Failed to parse query parameters:", parseError);
throw new McpError(
ErrorCode.InvalidRequest,
`Invalid query parameters: ${parseError instanceof Error ? parseError.message : String(parseError)}`
);
}
}
// If an entry ID is provided, fetch that specific entry
if (entryId) {
const entry = await fetchEntry(contentTypeUid, entryId);
return {
contents: [{
uri: request.params.uri,
mimeType: "application/json",
text: JSON.stringify(entry, null, 2)
}]
};
}
// Otherwise, fetch entries with query parameters
const entries = await fetchEntries(contentTypeUid, queryParams);
// Return the entries as JSON
return {
contents: [{
uri: request.params.uri,
mimeType: "application/json",
text: JSON.stringify(entries, null, 2)
}]
};
} catch (error) {
console.error("[Error] Failed to read resource:", error);
throw new McpError(
ErrorCode.InternalError,
`Failed to read resource: ${error instanceof Error ? error.message : String(error)}`
);
}
});
/**
* Handler that lists available tools.
* Exposes tools for working with Strapi content.
*/
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "list_content_types",
description: "List all available content types in Strapi",
inputSchema: {
type: "object",
properties: {}
}
},
{
name: "get_entries",
description: "Get entries for a specific content type with optional filtering, pagination, sorting, and population of relations",
inputSchema: {
type: "object",
properties: {
contentType: {
type: "string",
description: "The content type UID (e.g., 'api::article.article')"
},
filters: {
type: "object",
description: "Filters to apply to the query (e.g., { title: { $contains: 'hello' } })"
},
pagination: {
type: "object",
properties: {
page: {
type: "number",
description: "Page number"
},
pageSize: {
type: "number",
description: "Number of items per page"
}
},
description: "Pagination options"
},
sort: {
type: "array",
items: {
type: "string"
},
description: "Sorting options (e.g., ['title:asc', 'createdAt:desc'])"
},
populate: {
oneOf: [
{
type: "string",
description: "Relation to populate (e.g., 'author')"
},
{
type: "array",
items: {
type: "string"
},
description: "Relations to populate (e.g., ['author', 'categories'])"
},
{
type: "object",
description: "Complex populate configuration"
}
],
description: "Relations to populate"
}
},
required: ["contentType"]
}
},
{
name: "get_entry",
description: "Get a specific entry by ID",
inputSchema: {
type: "object",
properties: {
contentType: {
type: "string",
description: "The content type UID (e.g., 'api::article.article')"
},
id: {
type: "string",
description: "The ID of the entry"
}
},
required: ["contentType", "id"]
}
},
{
name: "create_entry",
description: "Create a new entry for a content type",
inputSchema: {
type: "object",
properties: {
contentType: {
type: "string",
description: "The content type UID (e.g., 'api::article.article')"
},
data: {
type: "object",
description: "The data for the new entry"
}
},
required: ["contentType", "data"]
}
},
{
name: "update_entry",
description: "Update an existing entry",
inputSchema: {
type: "object",
properties: {
contentType: {
type: "string",
description: "The content type UID (e.g., 'api::article.article')"
},
id: {
type: "string",
description: "The ID of the entry to update"
},
data: {
type: "object",
description: "The updated data for the entry"
}
},
required: ["contentType", "id", "data"]
}
},
{
name: "delete_entry",
description: "Delete an entry",
inputSchema: {
type: "object",
properties: {
contentType: {
type: "string",
description: "The content type UID (e.g., 'api::article.article')"
},
id: {
type: "string",
description: "The ID of the entry to delete"
}
},
required: ["contentType", "id"]
}
},
{
name: "upload_media",
description: "Upload a media file to Strapi",
inputSchema: {
type: "object",
properties: {
fileData: {
type: "string",
description: "Base64-encoded file data, optionally with data URL prefix (e.g., 'data:image/jpeg;base64,...')"
},
fileName: {
type: "string",
description: "Name of the file (e.g., 'image.jpg')"
},
fileType: {
type: "string",
description: "MIME type of the file (e.g., 'image/jpeg')"
}
},
required: ["fileData", "fileName", "fileType"]
}
}
]
};
});
/**
* Handler for tool calls.
* Implements various tools for working with Strapi content.
*/
server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) => {
try {
switch (request.params.name) {
case "list_content_types": {
const contentTypes = await fetchContentTypes();
return {
content: [{
type: "text",
text: JSON.stringify(contentTypes.map(ct => ({
uid: ct.uid,
displayName: ct.info.displayName,
description: ct.info.description
})), null, 2)
}]
};
}
case "get_entries": {
const contentType = String(request.params.arguments?.contentType);
if (!contentType) {
throw new McpError(
ErrorCode.InvalidParams,
"Content type is required"
);
}
// Extract query parameters from the request
const queryParams: QueryParams = {};
if (request.params.arguments?.filters) {
queryParams.filters = request.params.arguments.filters;
}
if (request.params.arguments?.pagination) {
queryParams.pagination = request.params.arguments.pagination;
}
if (request.params.arguments?.sort) {
queryParams.sort = request.params.arguments.sort;
}
if (request.params.arguments?.populate) {
queryParams.populate = request.params.arguments.populate;
}
// Fetch entries with query parameters
const entries = await fetchEntries(contentType, queryParams);
return {
content: [{
type: "text",
text: JSON.stringify(entries, null, 2)
}]
};
}
case "get_entry": {
const contentType = String(request.params.arguments?.contentType);
const id = String(request.params.arguments?.id);
if (!contentType || !id) {
throw new McpError(
ErrorCode.InvalidParams,
"Content type and ID are required"
);
}
const entry = await fetchEntry(contentType, id);
return {
content: [{
type: "text",
text: JSON.stringify(entry, null, 2)
}]
};
}
case "create_entry": {
const contentType = String(request.params.arguments?.contentType);
const data = request.params.arguments?.data;
if (!contentType || !data) {
throw new McpError(
ErrorCode.InvalidParams,
"Content type and data are required"
);
}
const entry = await createEntry(contentType, data);
return {
content: [{
type: "text",
text: JSON.stringify(entry, null, 2)
}]
};
}
case "update_entry": {
const contentType = String(request.params.arguments?.contentType);
const id = String(request.params.arguments?.id);
const data = request.params.arguments?.data;
if (!contentType || !id || !data) {
throw new McpError(
ErrorCode.InvalidParams,
"Content type, ID, and data are required"
);
}
const entry = await updateEntry(contentType, id, data);
return {
content: [{
type: "text",
text: JSON.stringify(entry, null, 2)
}]
};
}
case "delete_entry": {
const contentType = String(request.params.arguments?.contentType);
const id = String(request.params.arguments?.id);
if (!contentType || !id) {
throw new McpError(
ErrorCode.InvalidParams,
"Content type and ID are required"
);
}
await deleteEntry(contentType, id);
return {
content: [{
type: "text",
text: `Successfully deleted entry ${id} from ${contentType}`
}]
};
}
case "upload_media": {
const fileData = String(request.params.arguments?.fileData);
const fileName = String(request.params.arguments?.fileName);
const fileType = String(request.params.arguments?.fileType);
if (!fileData || !fileName || !fileType) {
throw new McpError(
ErrorCode.InvalidParams,
"File data, file name, and file type are required"
);
}
const media = await uploadMedia(fileData, fileName, fileType);
return {
content: [{
type: "text",
text: JSON.stringify(media, null, 2)
}]
};
}
default:
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${request.params.name}`
);
}
} catch (error) {
console.error(`[Error] Tool execution failed: ${error instanceof Error ? error.message : String(error)}`);
if (error instanceof McpError) {
throw error;
}
return {
content: [{
type: "text",
text: `Error: ${error instanceof Error ? error.message : String(error)}`
}],
isError: true
};
}
});
/**
* Start the server using stdio transport.
*/
async function main() {
console.error("[Setup] Starting Strapi MCP server");
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("[Setup] Strapi MCP server running");
}
main().catch((error) => {
console.error("[Error] Server error:", error);
process.exit(1);
});