UNPKG

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
#!/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':