strapi-mcp
Version:
An MCP server for your Strapi CMS that provides access to content types and entries through the MCP protocol
976 lines • 132 kB
JavaScript
#!/usr/bin/env node
process.on('unhandledRejection', (reason, promise) => {
console.error('[FATAL] Unhandled Rejection at:', promise, 'reason:', reason);
});
process.on('uncaughtException', (error) => {
console.error('[FATAL] Uncaught Exception:', error);
process.exit(1); // Mandatory exit after uncaught exception
});
/**
* 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.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, ErrorCode, McpError, } from "@modelcontextprotocol/sdk/types.js";
import axios from "axios";
import dotenv from 'dotenv';
// Load environment variables from .env file
dotenv.config();
// Extended error codes to include additional ones we need
var ExtendedErrorCode;
(function (ExtendedErrorCode) {
// Original error codes from SDK
ExtendedErrorCode["InvalidRequest"] = "InvalidRequest";
ExtendedErrorCode["MethodNotFound"] = "MethodNotFound";
ExtendedErrorCode["InvalidParams"] = "InvalidParams";
ExtendedErrorCode["InternalError"] = "InternalError";
// Additional error codes
ExtendedErrorCode["ResourceNotFound"] = "ResourceNotFound";
ExtendedErrorCode["AccessDenied"] = "AccessDenied";
})(ExtendedErrorCode || (ExtendedErrorCode = {}));
// Custom error class extending McpError to support our extended error codes
class ExtendedMcpError extends McpError {
extendedCode;
constructor(code, message) {
// Map our extended codes to standard MCP error codes when needed
let mcpCode;
// Map custom error codes to standard MCP error codes
switch (code) {
case ExtendedErrorCode.ResourceNotFound:
case ExtendedErrorCode.AccessDenied:
// Map custom codes to InternalError for SDK compatibility
mcpCode = ErrorCode.InternalError;
break;
case ExtendedErrorCode.InvalidRequest:
mcpCode = ErrorCode.InvalidRequest;
break;
case ExtendedErrorCode.MethodNotFound:
mcpCode = ErrorCode.MethodNotFound;
break;
case ExtendedErrorCode.InvalidParams:
mcpCode = ErrorCode.InvalidParams;
break;
case ExtendedErrorCode.InternalError:
default:
mcpCode = ErrorCode.InternalError;
break;
}
// Call super before accessing 'this'
super(mcpCode, message);
// Store the extended code for reference
this.extendedCode = code;
}
}
// 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";
const STRAPI_ADMIN_EMAIL = process.env.STRAPI_ADMIN_EMAIL;
const STRAPI_ADMIN_PASSWORD = process.env.STRAPI_ADMIN_PASSWORD;
// Validate required environment variables
if (!STRAPI_API_TOKEN && !(STRAPI_ADMIN_EMAIL && STRAPI_ADMIN_PASSWORD)) {
console.error("[Error] Missing required authentication. Please provide either STRAPI_API_TOKEN or both STRAPI_ADMIN_EMAIL and STRAPI_ADMIN_PASSWORD environment variables");
process.exit(1);
}
// Only validate API token format if we don't have admin credentials (since admin creds take priority)
if (!STRAPI_ADMIN_EMAIL || !STRAPI_ADMIN_PASSWORD) {
// If no admin credentials, validate that API token is not a placeholder
if (STRAPI_API_TOKEN && (STRAPI_API_TOKEN === "strapi_token" || STRAPI_API_TOKEN === "your-api-token-here" || STRAPI_API_TOKEN.includes("placeholder"))) {
console.error("[Error] STRAPI_API_TOKEN appears to be a placeholder value. Please provide a real API token from your Strapi admin panel or use admin credentials instead.");
process.exit(1);
}
}
console.error(`[Setup] Connecting to Strapi at ${STRAPI_URL}`);
console.error(`[Setup] Development mode: ${STRAPI_DEV_MODE ? "enabled" : "disabled"}`);
// Determine authentication method priority
if (STRAPI_ADMIN_EMAIL && STRAPI_ADMIN_PASSWORD) {
console.error(`[Setup] Authentication: Using admin credentials (priority)`);
if (STRAPI_API_TOKEN && STRAPI_API_TOKEN !== "strapi_token" && !STRAPI_API_TOKEN.includes("placeholder")) {
console.error(`[Setup] API token also available as fallback`);
}
}
else if (STRAPI_API_TOKEN) {
console.error(`[Setup] Authentication: Using API token`);
}
else {
console.error(`[Setup] Authentication: ERROR - No valid authentication method available`);
}
// Axios instance for Strapi API
const strapiClient = axios.create({
baseURL: STRAPI_URL,
headers: {
"Content-Type": "application/json",
},
validateStatus: function (status) {
// Consider only 5xx as errors - for more robust error handling
return status < 500;
}
});
// If API token is provided, use it
if (STRAPI_API_TOKEN) {
strapiClient.defaults.headers.common['Authorization'] = `Bearer ${STRAPI_API_TOKEN}`;
}
// Store admin JWT token if we log in
let adminJwtToken = null;
/**
* Log in to the Strapi admin API using provided credentials
*/
async function loginToStrapiAdmin() {
// Use process.env directly here to ensure latest values are used
const email = process.env.STRAPI_ADMIN_EMAIL;
const password = process.env.STRAPI_ADMIN_PASSWORD;
if (!email || !password) {
console.error("[Auth] No admin credentials found in process.env, skipping admin login");
return false;
}
try {
// Log the authentication attempt with more detail
console.error(`[Auth] Attempting login to Strapi admin at ${STRAPI_URL}/admin/login as ${email}`);
console.error(`[Auth] Full URL being used: ${STRAPI_URL}/admin/login`);
// Make the request with more detailed logging
console.error(`[Auth] Sending POST request with email and password`);
const response = await axios.post(`${STRAPI_URL}/admin/login`, {
email,
password
});
console.error(`[Auth] Response status: ${response.status}`);
console.error(`[Auth] Response headers:`, JSON.stringify(response.headers));
// Check if we got back valid data
if (response.data && response.data.data && response.data.data.token) {
adminJwtToken = response.data.data.token;
console.error("[Auth] Successfully logged in to Strapi admin");
console.error(`[Auth] Token received (first 20 chars): ${adminJwtToken?.substring(0, 20)}...`);
return true;
}
else {
console.error("[Auth] Login response missing token");
console.error(`[Auth] Response data:`, JSON.stringify(response.data));
return false;
}
}
catch (error) {
console.error("[Auth] Failed to log in to Strapi admin:");
if (axios.isAxiosError(error)) {
console.error(`[Auth] Status: ${error.response?.status}`);
console.error(`[Auth] Response data:`, error.response?.data);
console.error(`[Auth] Request URL: ${error.config?.url}`);
console.error(`[Auth] Request method: ${error.config?.method}`);
}
else {
console.error(error);
}
return false;
}
}
/**
* Make a request to the admin API using the admin JWT token
*/
async function makeAdminApiRequest(endpoint, method = 'get', data, params) {
if (!adminJwtToken) {
// Try to log in first
console.error(`[Admin API] No token available, attempting login...`);
const success = await loginToStrapiAdmin();
if (!success) {
console.error(`[Admin API] Login failed. Cannot authenticate for admin API access.`);
throw new Error("Not authenticated for admin API access");
}
console.error(`[Admin API] Login successful, proceeding with request.`);
}
const fullUrl = `${STRAPI_URL}${endpoint}`;
console.error(`[Admin API] Making ${method.toUpperCase()} request to: ${fullUrl}`);
if (data) {
console.error(`[Admin API] Request payload: ${JSON.stringify(data, null, 2)}`);
}
try {
console.error(`[Admin API] Sending request with Authorization header using token: ${adminJwtToken?.substring(0, 20)}...`);
const response = await axios({
method,
url: fullUrl,
headers: {
'Authorization': `Bearer ${adminJwtToken}`,
'Content-Type': 'application/json'
},
data, // Used for POST, PUT, etc.
params // Used for GET requests query parameters
});
console.error(`[Admin API] Response status: ${response.status}`);
if (response.data) {
console.error(`[Admin API] Response received successfully`);
}
return response.data;
}
catch (error) {
console.error(`[Admin API] Request to ${endpoint} failed:`);
if (axios.isAxiosError(error)) {
console.error(`[Admin API] Status: ${error.response?.status}`);
console.error(`[Admin API] Error data: ${JSON.stringify(error.response?.data)}`);
console.error(`[Admin API] Error headers: ${JSON.stringify(error.response?.headers)}`);
// Check if it's an auth error (e.g., token expired)
if (error.response?.status === 401 && adminJwtToken) {
console.error("[Admin API] Admin token might be expired. Attempting re-login...");
adminJwtToken = null; // Clear expired token
const loginSuccess = await loginToStrapiAdmin();
if (loginSuccess) {
console.error("[Admin API] Re-login successful. Retrying original request...");
// Retry the request once after successful re-login
try {
const retryResponse = await axios({
method,
url: fullUrl,
headers: {
'Authorization': `Bearer ${adminJwtToken}`,
'Content-Type': 'application/json'
},
data,
params
});
console.error(`[Admin API] Retry successful, status: ${retryResponse.status}`);
return retryResponse.data;
}
catch (retryError) {
console.error(`[Admin API] Retry failed:`, retryError);
throw retryError;
}
}
else {
console.error("[Admin API] Re-login failed. Throwing original error.");
throw new Error("Admin re-authentication failed after token expiry.");
}
}
}
else {
console.error(`[Admin API] Non-Axios error:`, error);
}
// If not a 401 or re-login failed, throw the original error
throw error;
}
}
// Cache for content types
let contentTypesCache = [];
/**
* Create an MCP server with capabilities for resources and tools
*/
const server = new Server({
name: "strapi-mcp",
version: "0.2.0",
}, {
capabilities: {
resources: {},
tools: {},
},
});
/**
* Fetch all content types from Strapi
*/
async function fetchContentTypes() {
try {
// Validate connection before attempting to fetch
await validateStrapiConnection();
console.error("[API] Fetching content types from Strapi");
// If we have cached content types, return them
// --- DEBUG: Temporarily disable cache ---
// if (contentTypesCache.length > 0) {
// console.error("[API] Returning cached content types");
// return contentTypesCache;
// }
// --- END DEBUG ---
// Helper function to process and cache content types
const processAndCacheContentTypes = (data, source) => {
console.error(`[API] Successfully fetched collection types from ${source}`);
const contentTypes = data.map((item) => {
const uid = item.uid;
const apiID = uid.split('.').pop() || '';
return {
uid: uid,
apiID: apiID,
info: {
displayName: item.info?.displayName || apiID.charAt(0).toUpperCase() + apiID.slice(1).replace(/-/g, ' '),
description: item.info?.description || `${apiID} content type`,
},
attributes: item.attributes || {}
};
});
// Filter out internal types
const filteredTypes = contentTypes.filter((ct) => !ct.uid.startsWith("admin::") &&
!ct.uid.startsWith("plugin::"));
console.error(`[API] Found ${filteredTypes.length} content types via ${source}`);
contentTypesCache = filteredTypes; // Update cache
return filteredTypes;
};
// --- Attempt 1: Use Admin Credentials if available ---
console.error(`[DEBUG] Checking admin creds: EMAIL=${Boolean(STRAPI_ADMIN_EMAIL)}, PASSWORD=${Boolean(STRAPI_ADMIN_PASSWORD)}`);
if (STRAPI_ADMIN_EMAIL && STRAPI_ADMIN_PASSWORD) {
console.error("[API] Attempting to fetch content types using admin credentials");
try {
// Use makeAdminApiRequest which handles login
// Try the content-type-builder endpoint first, as it's more common for schema listing
console.error("[API] Trying admin endpoint: /content-type-builder/content-types");
const adminResponse = await makeAdminApiRequest('/content-type-builder/content-types');
console.error("[API] Admin response structure:", Object.keys(adminResponse || {}));
// Strapi's admin API often wraps data, check common structures
let adminData = null;
if (adminResponse && adminResponse.data && Array.isArray(adminResponse.data)) {
adminData = adminResponse.data; // Direct array in response.data
}
else if (adminResponse && Array.isArray(adminResponse)) {
adminData = adminResponse; // Direct array response
}
if (adminData && adminData.length > 0) {
return processAndCacheContentTypes(adminData, "Admin API (/content-type-builder/content-types)");
}
else {
console.error("[API] Admin API response did not contain expected data array or was empty.", adminResponse);
}
}
catch (adminError) {
console.error(`[API] Failed to fetch content types using admin credentials:`, adminError);
if (axios.isAxiosError(adminError)) {
console.error(`[API] Admin API Error Status: ${adminError.response?.status}`);
console.error(`[API] Admin API Error Data:`, adminError.response?.data);
}
// Don't throw, proceed to next method
}
}
else {
console.error("[API] Admin credentials not provided, skipping admin API attempt.");
}
// --- Attempt 2: Try different admin endpoints ---
if (STRAPI_ADMIN_EMAIL && STRAPI_ADMIN_PASSWORD) {
console.error("[API] Trying alternative admin endpoint: /content-manager/content-types");
try {
const adminResponse2 = await makeAdminApiRequest('/content-manager/content-types');
console.error("[API] Admin response 2 structure:", Object.keys(adminResponse2 || {}));
let adminData2 = null;
if (adminResponse2 && adminResponse2.data && Array.isArray(adminResponse2.data)) {
adminData2 = adminResponse2.data;
}
else if (adminResponse2 && Array.isArray(adminResponse2)) {
adminData2 = adminResponse2;
}
if (adminData2 && adminData2.length > 0) {
return processAndCacheContentTypes(adminData2, "Admin API (/content-manager/content-types)");
}
}
catch (adminError2) {
console.error(`[API] Alternative admin endpoint also failed:`, adminError2);
}
}
// --- Attempt 3: Use API Token via strapiClient (Original Primary Method) ---
console.error("[API] Attempting to fetch content types using API token (strapiClient)");
try {
// This is the most reliable way *if* the token has permissions
const response = await strapiClient.get('/content-manager/collection-types');
if (response.data && Array.isArray(response.data)) {
// Note: This path might require admin permissions, often fails with API token
return processAndCacheContentTypes(response.data, "Content Manager API (/content-manager/collection-types)");
}
}
catch (apiError) {
console.error(`[API] Failed to fetch from content manager API:`, apiError);
if (axios.isAxiosError(apiError)) {
console.error(`[API] API Error Status: ${apiError.response?.status}`);
console.error(`[API] API Error Data:`, apiError.response?.data);
}
}
// --- Attempt 4: Discovery via exploring known endpoints ---
console.error(`[API] Trying content type discovery via known patterns...`);
// Try to discover by checking common content types
const commonTypes = ['article', 'page', 'post', 'user', 'category'];
const discoveredTypes = [];
for (const type of commonTypes) {
try {
const testResponse = await strapiClient.get(`/api/${type}?pagination[limit]=1`);
if (testResponse.status === 200) {
console.error(`[API] Discovered content type: api::${type}.${type}`);
discoveredTypes.push({
uid: `api::${type}.${type}`,
apiID: type,
info: {
displayName: type.charAt(0).toUpperCase() + type.slice(1),
description: `${type} content type (discovered)`,
},
attributes: {}
});
}
}
catch (e) {
// Ignore 404s and continue
}
}
if (discoveredTypes.length > 0) {
console.error(`[API] Found ${discoveredTypes.length} content types via discovery`);
contentTypesCache = discoveredTypes;
return discoveredTypes;
}
// Final attempt: Try to discover content types by checking for common endpoint patterns
// If all proper API methods failed, provide a helpful error message instead of silent failure
let errorMessage = "Unable to fetch content types from Strapi. This could be due to:\n";
errorMessage += "1. Strapi server not running or unreachable\n";
errorMessage += "2. Invalid API token or insufficient permissions\n";
errorMessage += "3. Admin credentials not working\n";
errorMessage += "4. Database connectivity issues\n";
errorMessage += "5. Strapi instance configuration problems\n\n";
errorMessage += "Please check:\n";
errorMessage += `- Strapi is running at ${STRAPI_URL}\n`;
errorMessage += "- Your API token has proper permissions\n";
errorMessage += "- Admin credentials are correct\n";
errorMessage += "- Database is accessible and running\n";
errorMessage += "- Try creating a test content type in your Strapi admin panel";
throw new ExtendedMcpError(ExtendedErrorCode.InternalError, errorMessage);
}
catch (error) {
console.error("[Error] Failed to fetch content types:", error);
let errorMessage = "Failed to fetch content types";
let errorCode = ExtendedErrorCode.InternalError;
if (axios.isAxiosError(error)) {
errorMessage += `: ${error.response?.status} ${error.response?.statusText}`;
if (error.response?.status === 403) {
errorCode = ExtendedErrorCode.AccessDenied;
errorMessage += ` (Permission denied - check API token permissions)`;
}
else if (error.response?.status === 401) {
errorCode = ExtendedErrorCode.AccessDenied;
errorMessage += ` (Unauthorized - API token may be invalid or expired)`;
}
}
else if (error instanceof Error) {
errorMessage += `: ${error.message}`;
}
else {
errorMessage += `: ${String(error)}`;
}
throw new ExtendedMcpError(errorCode, errorMessage);
}
}
/**
* Fetch entries for a specific content type with optional filtering, pagination, and sorting
*/
async function fetchEntries(contentType, queryParams) {
// Validate connection before attempting to fetch
await validateStrapiConnection();
let response;
let success = false;
let fetchedData = [];
let fetchedMeta = {};
const collection = contentType.split(".")[1]; // Keep this for potential path variations if needed
// --- Attempt 1: Use Admin Credentials via makeAdminApiRequest ---
// Only attempt if admin credentials are provided
if (STRAPI_ADMIN_EMAIL && STRAPI_ADMIN_PASSWORD) {
console.error(`[API] Attempt 1: Fetching entries for ${contentType} using makeAdminApiRequest (Admin Credentials)`);
try {
// Use the full content type UID for the content-manager endpoint
const adminEndpoint = `/content-manager/collection-types/${contentType}`;
// Prepare query params for admin request (might need adjustment based on API)
// Let's assume makeAdminApiRequest handles params correctly
const adminParams = {};
// Convert nested Strapi v4 params to flat query params if needed, or pass as is
// Example: filters[field][$eq]=value, pagination[page]=1, sort=field:asc, populate=*, fields=field1,field2
// For simplicity, let's pass the original structure first and modify makeAdminApiRequest
if (queryParams?.filters)
adminParams.filters = queryParams.filters;
if (queryParams?.pagination)
adminParams.pagination = queryParams.pagination;
if (queryParams?.sort)
adminParams.sort = queryParams.sort;
if (queryParams?.populate)
adminParams.populate = queryParams.populate;
if (queryParams?.fields)
adminParams.fields = queryParams.fields;
// Make the request using admin credentials (modify makeAdminApiRequest to handle params)
const adminResponse = await makeAdminApiRequest(adminEndpoint, 'get', undefined, adminParams); // Pass params here
// Process admin response (structure might differ, e.g., response.data.results)
if (adminResponse && adminResponse.results && Array.isArray(adminResponse.results)) {
console.error(`[API] Successfully fetched data via admin credentials for ${contentType}`);
// Admin API often returns data in 'results' and pagination info separately
fetchedData = adminResponse.results;
fetchedMeta = adminResponse.pagination || {}; // Adjust based on actual admin API response structure
// Filter out potential errors within items
fetchedData = fetchedData.filter((item) => !item?.error);
if (fetchedData.length > 0) {
console.error(`[API] Returning data fetched via admin credentials for ${contentType}`);
return { data: fetchedData, meta: fetchedMeta };
}
else {
console.error(`[API] Admin fetch succeeded for ${contentType} but returned no entries. Trying API token.`);
}
}
else {
console.error(`[API] Admin fetch for ${contentType} did not return expected 'results' array. Response:`, adminResponse);
console.error(`[API] Falling back to API token.`);
}
}
catch (adminError) {
console.error(`[API] Failed to fetch entries using admin credentials for ${contentType}:`, adminError);
console.error(`[API] Falling back to API token.`);
}
}
else {
console.error("[API] Admin credentials not provided, using API token instead.");
}
// --- Attempt 2: Use API Token via strapiClient (as fallback) ---
console.error(`[API] Attempt 2: Fetching entries for ${contentType} using strapiClient (API Token)`);
try {
const params = {};
// ... build params from queryParams ... (existing code)
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;
if (queryParams?.fields)
params.fields = queryParams.fields;
// Try multiple possible API paths (keep this flexibility)
const possiblePaths = [
`/api/${collection}`,
`/api/${collection.toLowerCase()}`,
// Add more variations if necessary
];
for (const path of possiblePaths) {
try {
console.error(`[API] Trying path with strapiClient: ${path}`);
response = await strapiClient.get(path, { params });
if (response.data && response.data.error) {
console.error(`[API] Path ${path} returned an error:`, response.data.error);
continue; // Try next path
}
console.error(`[API] Successfully fetched data from: ${path} using strapiClient`);
success = true;
// Process response data
if (response.data.data) {
fetchedData = Array.isArray(response.data.data) ? response.data.data : [response.data.data];
fetchedMeta = response.data.meta || {};
}
else if (Array.isArray(response.data)) {
fetchedData = response.data;
fetchedMeta = { pagination: { page: 1, pageSize: fetchedData.length, pageCount: 1, total: fetchedData.length } };
}
else {
// Handle unexpected format, maybe log it
console.warn(`[API] Unexpected response format from ${path} using strapiClient:`, response.data);
fetchedData = response.data ? [response.data] : []; // Wrap if not null/undefined
fetchedMeta = {};
}
// Filter out potential errors within items if any structure allows it
fetchedData = fetchedData.filter((item) => !item?.error);
break; // Exit loop on success
}
catch (err) {
if (axios.isAxiosError(err) && (err.response?.status === 404 || err.response?.status === 403 || err.response?.status === 401)) {
// 404: Try next path. 403/401: Permissions issue
console.error(`[API] Path ${path} failed with ${err.response?.status}, trying next path...`);
continue;
}
// For other errors, rethrow to be caught by the outer try-catch
console.error(`[API] Unexpected error on path ${path} with strapiClient:`, err);
throw err;
}
}
// If strapiClient succeeded AND returned data, return it
if (success && fetchedData.length > 0) {
console.error(`[API] Returning data fetched via strapiClient for ${contentType}`);
return { data: fetchedData, meta: fetchedMeta };
}
else if (success && fetchedData.length === 0) {
console.error(`[API] Content type ${contentType} exists but has no entries (empty collection)`);
// Return empty result for legitimate empty collections (not an error)
return { data: [], meta: fetchedMeta };
}
else {
console.error(`[API] strapiClient failed to fetch entries for ${contentType}.`);
}
}
catch (error) {
// Catch errors from the strapiClient attempts (excluding 404/403/401 handled above)
console.error(`[API] Error during strapiClient fetch for ${contentType}:`, error);
}
// --- All attempts failed: Provide helpful error instead of silent empty return ---
console.error(`[API] All attempts failed to fetch entries for ${contentType}`);
let errorMessage = `Failed to fetch entries for content type ${contentType}. This could be due to:\n`;
errorMessage += "1. Content type doesn't exist in your Strapi instance\n";
errorMessage += "2. API token lacks permissions to access this content type\n";
errorMessage += "3. Admin credentials don't have access to this content type\n";
errorMessage += "4. Content type exists but has no published entries\n";
errorMessage += "5. Database connectivity issues\n\n";
errorMessage += "Troubleshooting:\n";
errorMessage += `- Verify ${contentType} exists in your Strapi admin panel\n`;
errorMessage += "- Check your API token permissions\n";
errorMessage += "- Ensure the content type has published entries\n";
errorMessage += "- Verify admin credentials if using admin authentication";
throw new ExtendedMcpError(ExtendedErrorCode.ResourceNotFound, errorMessage);
}
/**
* Fetch a specific entry by ID
*/
async function fetchEntry(contentType, id, queryParams) {
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];
// --- Attempt 1: Use Admin Credentials ---
if (STRAPI_ADMIN_EMAIL && STRAPI_ADMIN_PASSWORD) {
console.error(`[API] Attempt 1: Fetching entry ${id} for ${contentType} using admin credentials`);
try {
// Admin API for content management uses a different path structure
const adminEndpoint = `/content-manager/collection-types/${contentType}/${id}`;
// Prepare admin params
const adminParams = {};
if (queryParams?.populate)
adminParams.populate = queryParams.populate;
if (queryParams?.fields)
adminParams.fields = queryParams.fields;
// Make the request
const adminResponse = await makeAdminApiRequest(adminEndpoint, 'get', undefined, adminParams);
if (adminResponse) {
console.error(`[API] Successfully fetched entry ${id} via admin credentials`);
return adminResponse;
}
}
catch (adminError) {
console.error(`[API] Failed to fetch entry ${id} using admin credentials:`, adminError);
console.error(`[API] Falling back to API token...`);
}
}
else {
console.error(`[API] Admin credentials not provided, falling back to API token`);
}
// --- Attempt 2: Use API Token as fallback ---
// Build query parameters only for populate and fields
const params = {};
if (queryParams?.populate) {
params.populate = queryParams.populate;
}
if (queryParams?.fields) {
params.fields = queryParams.fields;
}
console.error(`[API] Attempt 2: Fetching entry ${id} for ${contentType} using API token`);
// Get the entry from Strapi
const response = await strapiClient.get(`/api/${collection}/${id}`, { params });
return response.data.data;
}
catch (error) {
console.error(`[Error] Failed to fetch entry ${id} for ${contentType}:`, error);
let errorMessage = `Failed to fetch entry ${id} for ${contentType}`;
let errorCode = ExtendedErrorCode.InternalError;
if (axios.isAxiosError(error)) {
errorMessage += `: ${error.response?.status} ${error.response?.statusText}`;
if (error.response?.status === 404) {
errorCode = ExtendedErrorCode.ResourceNotFound;
errorMessage += ` (Entry not found)`;
}
else if (error.response?.status === 403) {
errorCode = ExtendedErrorCode.AccessDenied;
errorMessage += ` (Permission denied - check API token permissions)`;
}
else if (error.response?.status === 401) {
errorCode = ExtendedErrorCode.AccessDenied;
errorMessage += ` (Unauthorized - API token may be invalid or expired)`;
}
}
else if (error instanceof Error) {
errorMessage += `: ${error.message}`;
}
else {
errorMessage += `: ${String(error)}`;
}
throw new ExtendedMcpError(errorCode, errorMessage);
}
}
/**
* Create a new entry
*/
async function createEntry(contentType, data) {
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];
// --- Attempt 1: Use Admin Credentials via makeAdminApiRequest ---
if (STRAPI_ADMIN_EMAIL && STRAPI_ADMIN_PASSWORD) {
console.error(`[API] Attempt 1: Creating entry for ${contentType} using makeAdminApiRequest`);
try {
// Admin API for content management often uses a different path structure
const adminEndpoint = `/content-manager/collection-types/${contentType}`;
console.error(`[API] Trying admin create endpoint: ${adminEndpoint}`);
// Admin API might need the data directly, not nested under 'data'
const adminResponse = await makeAdminApiRequest(adminEndpoint, 'post', data);
// Check response from admin API (structure might differ)
if (adminResponse) {
console.error(`[API] Successfully created entry via makeAdminApiRequest.`);
// Admin API might return the created entry directly or nested under 'data'
return adminResponse.data || adminResponse;
}
else {
// Should not happen if makeAdminApiRequest resolves, but handle defensively
console.warn(`[API] Admin create completed but returned no data.`);
// Return a success indicator even without data, as the operation likely succeeded
return { message: "Create via admin succeeded, no data returned." };
}
}
catch (adminError) {
console.error(`[API] Failed to create entry using admin credentials:`, adminError);
// Only try API token if admin credentials fail
console.error(`[API] Admin credentials failed, attempting to use API token as fallback.`);
}
}
else {
console.error("[API] Admin credentials not provided, falling back to API token.");
}
// --- Attempt 2: Use API Token via strapiClient (as fallback) ---
console.error(`[API] Attempt 2: Creating entry for ${contentType} using strapiClient`);
try {
// Create the entry in Strapi
const response = await strapiClient.post(`/api/${collection}`, {
data: data
});
if (response.data && response.data.data) {
console.error(`[API] Successfully created entry via strapiClient.`);
return response.data.data;
}
else {
console.warn(`[API] Create via strapiClient completed, but no data returned.`);
throw new McpError(ErrorCode.InternalError, `Failed to create entry for ${contentType}: No data returned from API`);
}
}
catch (error) {
console.error(`[API] Failed to create entry via strapiClient:`, error);
throw new McpError(ErrorCode.InternalError, `Failed to create entry for ${contentType} via strapiClient: ${error instanceof Error ? error.message : String(error)}`);
}
}
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, id, data) {
const collection = contentType.split(".")[1];
const apiPath = `/api/${collection}/${id}`;
let responseData = null;
// --- Attempt 1: Use Admin Credentials via makeAdminApiRequest ---
if (STRAPI_ADMIN_EMAIL && STRAPI_ADMIN_PASSWORD) {
console.error(`[API] Attempt 1: Updating entry ${id} for ${contentType} using makeAdminApiRequest`);
try {
// Admin API for content management often uses a different path structure
const adminEndpoint = `/content-manager/collection-types/${contentType}/${id}`;
console.error(`[API] Trying admin update endpoint: ${adminEndpoint}`);
// Admin API PUT might just need the data directly, not nested under 'data'
const adminResponse = await makeAdminApiRequest(adminEndpoint, 'put', data); // Send 'data' directly
// Check response from admin API (structure might differ)
if (adminResponse) {
console.error(`[API] Successfully updated entry ${id} via makeAdminApiRequest.`);
// Admin API might return the updated entry directly or nested under 'data'
return adminResponse.data || adminResponse;
}
else {
// Should not happen if makeAdminApiRequest resolves, but handle defensively
console.warn(`[API] Admin update for ${id} completed but returned no data.`);
// Return a success indicator even without data, as the operation likely succeeded
return { id: id, message: "Update via admin succeeded, no data returned." };
}
}
catch (adminError) {
console.error(`[API] Failed to update entry ${id} using admin credentials:`, adminError);
console.error(`[API] Admin credentials failed, attempting to use API token as fallback.`);
}
}
else {
console.error("[API] Admin credentials not provided, falling back to API token.");
}
// --- Attempt 2: Use API Token via strapiClient (as fallback) ---
console.error(`[API] Attempt 2: Updating entry ${id} for ${contentType} using strapiClient`);
try {
const response = await strapiClient.put(apiPath, { data: data });
// Check if data was returned
if (response.data && response.data.data) {
console.error(`[API] Successfully updated entry ${id} via strapiClient.`);
return response.data.data; // Success with data returned
}
else {
// Update might have succeeded but didn't return data
console.warn(`[API] Update via strapiClient for ${id} completed, but no updated data returned.`);
// Return a success indicator even without data, as the operation likely succeeded
return { id: id, message: "Update via API token succeeded, no data returned." };
}
}
catch (error) {
console.error(`[API] Failed to update entry ${id} via strapiClient:`, error);
throw new McpError(ErrorCode.InternalError, `Failed to update entry ${id} for ${contentType} via strapiClient: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Delete an entry
*/
async function deleteEntry(contentType, id) {
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];
// --- Attempt 1: Use Admin Credentials via makeAdminApiRequest ---
if (STRAPI_ADMIN_EMAIL && STRAPI_ADMIN_PASSWORD) {
console.error(`[API] Attempt 1: Deleting entry ${id} for ${contentType} using makeAdminApiRequest`);
try {
// Admin API for content management often uses a different path structure
const adminEndpoint = `/content-manager/collection-types/${contentType}/${id}`;
console.error(`[API] Trying admin delete endpoint: ${adminEndpoint}`);
// Admin API DELETE request
const adminResponse = await makeAdminApiRequest(adminEndpoint, 'delete');
// Check response from admin API
console.error(`[API] Successfully deleted entry ${id} via makeAdminApiRequest.`);
return; // Success - exit early
}
catch (adminError) {
console.error(`[API] Failed to delete entry ${id} using admin credentials:`, adminError);
console.error(`[API] Admin credentials failed, attempting to use API token as fallback.`);
}
}
else {
console.error("[API] Admin credentials not provided, falling back to API token.");
}
// --- Attempt 2: Use API Token via strapiClient (as fallback) ---
console.error(`[API] Attempt 2: Deleting entry ${id} for ${contentType} using strapiClient`);
try {
// Delete the entry from Strapi using API token
const response = await strapiClient.delete(`/api/${collection}/${id}`);
if (response.status >= 200 && response.status < 300) {
console.error(`[API] Successfully deleted entry ${id} via strapiClient.`);
return; // Success
}
else {
throw new Error(`Unexpected response status: ${response.status}`);
}
}
catch (error) {
console.error(`[API] Failed to delete entry ${id} via strapiClient:`, error);
throw new McpError(ErrorCode.InternalError, `Failed to delete entry ${id} for ${contentType} via strapiClient: ${error instanceof Error ? error.message : String(error)}`);
}
}
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 media file to Strapi
*/
async function uploadMedia(fileData, fileName, fileType) {
try {
console.error(`[API] Uploading media file: ${fileName} (type: ${fileType})`);
// Calculate base64 size and warn about large files
const base64Size = fileData.length;
const estimatedFileSize = Math.round((base64Size * 3) / 4); // Rough file size estimate
const estimatedFileSizeMB = (estimatedFileSize / (1024 * 1024)).toFixed(2);
console.error(`[API] File size: ~${estimatedFileSizeMB}MB (base64 length: ${base64Size})`);
// Add size limits to prevent context window overflow
const MAX_BASE64_SIZE = 1024 * 1024; // 1MB of base64 text (~750KB file)
if (base64Size > MAX_BASE64_SIZE) {
const maxFileSizeMB = ((MAX_BASE64_SIZE * 3) / 4 / (1024 * 1024)).toFixed(2);
throw new Error(`File too large. Base64 data is ${base64Size} characters (~${estimatedFileSizeMB}MB file). Maximum allowed is ${MAX_BASE64_SIZE} characters (~${maxFileSizeMB}MB file). Large files cause context window overflow. Consider using smaller files or implementing chunked upload.`);
}
// Warn about large files that might cause issues
if (base64Size > 100000) { // 100KB of base64 text
console.error(`[API] Warning: Large file detected (~${estimatedFileSizeMB}MB). This may cause context window issues.`);
}
const buffer = Buffer.from(fileData, 'base64');
// --- Attempt 1: Use Admin Credentials if available ---
if (STRAPI_ADMIN_EMAIL && STRAPI_ADMIN_PASSWORD) {
console.error(`[API] Attempt 1: Uploading media file using admin credentials`);
try {
// For admin uploads, we need to use axios directly with admin token
// since makeAdminApiRequest doesn't handle FormData well
if (!adminJwtToken) {
await loginToStrapiAdmin();
}
const formData = new FormData();
const blob = new Blob([buffer], { type: fileType });
formData.append('files', blob, fileName);
const adminResponse = await axios.post(`${STRAPI_URL}/api/upload`, formData, {
headers: {
'Authorization': `Bearer ${adminJwtToken}`,
// Don't set Content-Type, let axios handle it for FormData
}
});
const cleanResponse = filterBase64FromResponse(adminResponse.data);
console.error(`[API] Successfully uploaded media file via admin credentials`);
return cleanResponse;
}
catch (adminError) {
console.error(`[API] Failed to upload media file using admin credentials:`, adminError);
console.error(`[API] Falling back to API token...`);
}
}
else {
console.error("[API] Admin credentials not provided, using API token.");
}
// --- Attempt 2: Use API Token via strapiClient (as fallback) ---
console.error(`[API] Attempt 2: Uploading media file using API token`);
// Use FormData for file upload
const formData = new FormData();
// Convert Buffer to Blob with the correct content type
const blob = new Blob([buffer], { type: fileType });
formData.append('files', blob, fileName);
const response = await strapiClient.post('/api/upload', formData, {
headers: {
// Let axios set the correct multipart/form-data content-type with boundary
'Content-Type': 'multipart/form-data'
}
});
// Filter out any base64 data from the response to prevent context overflow
const cleanResponse = filterBase64FromResponse(response.data);
return cleanResponse;
}
catch (error) {
console.error(`[Error] Failed to upload media file ${fileName}:`, error);
throw new McpError(ErrorCode.InternalError, `Failed to upload media file ${fileName}: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Upload media file from file path (alternative to base64)
*/
async function uploadMediaFromPath(filePath, fileName, fileType) {
try {
const fs = await import('fs');
const path = await import('path');
// Check if file exists
if (!fs.existsSync(filePath)) {
throw new Error(`File not found: ${filePath}`);
}
// Get file stats
const stats = fs.statSync(filePath);
const fileSizeMB = (stats.size / (1024 * 1024)).toFixed(2);
console.error(`[API] Uploading media file from path: ${filePath} (${fileSizeMB}MB)`);
// Add size limits
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
if (stats.size > MAX_FILE_SIZE) {
throw new Error(`File too large: ${fileSizeMB}MB. Maximum allowed is 10MB.`);
}
// Auto-detect fileName and fileType if not provided
const actualFileName = fileName || path.basename(filePath);
const extension = path.extname(filePath).toLowerCase();
let actualFileType = fileType;
if (!actualFileType) {
// Basic MIME type detection
const mimeTypes = {
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif',
'.webp': 'image/webp',
'.pdf': 'application/pdf',
'.txt': 'text/plain',
'.json': 'application/json',
'.mp4': 'video/mp4',
'.avi': 'video/avi',
'.mov':