Skip to main content
Glama

contentful-mcp

index.ts12.2 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js" import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js" import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from "@modelcontextprotocol/sdk/types.js" import { CONTENTFUL_PROMPTS } from "./prompts/contentful-prompts.js" export { CONTENTFUL_PROMPTS } import { handlePrompt } from "./prompts/handlers.js" import { entryHandlers } from "./handlers/entry-handlers.js" import { assetHandlers } from "./handlers/asset-handlers.js" import { spaceHandlers } from "./handlers/space-handlers.js" import { contentTypeHandlers } from "./handlers/content-type-handlers.js" import { bulkActionHandlers } from "./handlers/bulk-action-handlers.js" import { aiActionHandlers } from "./handlers/ai-action-handlers.js" import { commentHandlers } from "./handlers/comment-handlers.js" import { getTools } from "./types/tools.js" import { validateEnvironment } from "./utils/validation.js" import { AiActionToolContext } from "./utils/ai-action-tool-generator.js" import type { AiActionInvocation } from "./types/ai-actions.js" import { StreamableHttpServer } from "./transports/streamable-http.js" // Validate environment variables validateEnvironment() // Create AI Action tool context const aiActionToolContext = new AiActionToolContext( process.env.SPACE_ID || "", process.env.ENVIRONMENT_ID || "master", ) // Function to get all tools including dynamic AI Action tools export function getAllTools() { const staticTools = getTools() // Add dynamically generated tools for AI Actions const dynamicTools = aiActionToolContext.generateAllToolSchemas() return { ...staticTools, ...dynamicTools.reduce( (acc, tool) => { acc[tool.name] = tool return acc }, {} as Record<string, unknown>, ), } } // Create MCP server const server = new Server( { name: "contentful-mcp-server", version: "1.15.0", }, { capabilities: { tools: getAllTools(), prompts: CONTENTFUL_PROMPTS, }, }, ) // Set up request handlers server.setRequestHandler(ListToolsRequestSchema, async () => { // Return both static and dynamic tools return { tools: Object.values(getAllTools()), } }) // Set up request handlers server.setRequestHandler(ListPromptsRequestSchema, async () => ({ prompts: Object.values(CONTENTFUL_PROMPTS), })) server.setRequestHandler(GetPromptRequestSchema, async (request) => { const { name, arguments: args } = request.params return handlePrompt(name, args) }) // Type-safe handler // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unused-vars server.setRequestHandler(CallToolRequestSchema, async (request, _extra): Promise<any> => { try { const { name, arguments: args } = request.params const handler = getHandler(name) if (!handler) { throw new Error(`Unknown tool: ${name}`) } const result = await handler(args || {}) // For AI Action responses, format them appropriately if (result && typeof result === "object") { // Check if this is an AI Action invocation result if ( "sys" in result && typeof result.sys === "object" && result.sys && "type" in result.sys && result.sys.type === "AiActionInvocation" ) { const invocationResult = result as AiActionInvocation // Format AI Action result as text content if available if (invocationResult.result && invocationResult.result.content) { return { content: [ { type: "text", text: typeof invocationResult.result.content === "string" ? invocationResult.result.content : JSON.stringify(invocationResult.result.content), }, ], } } } // Check for error response if ("isError" in result && result.isError === true) { // Format error response return { content: [ { type: "text", text: "message" in result ? String(result.message) : "Unknown error", }, ], isError: true, } } } // Return the result as is for regular handlers return result } catch (error) { return { content: [ { type: "text", text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, } } }) // Helper function to map tool names to handlers // eslint-disable-next-line @typescript-eslint/no-explicit-any function getHandler(name: string): ((args: any) => Promise<any>) | undefined { // Check if this is a dynamic AI Action tool if (name.startsWith("ai_action_")) { const actionId = name.replace("ai_action_", "") return (args: Record<string, unknown>) => handleAiActionInvocation(actionId, args) } const handlers = { // Entry operations create_entry: entryHandlers.createEntry, get_entry: entryHandlers.getEntry, update_entry: entryHandlers.updateEntry, delete_entry: entryHandlers.deleteEntry, publish_entry: entryHandlers.publishEntry, unpublish_entry: entryHandlers.unpublishEntry, search_entries: entryHandlers.searchEntries, // Bulk operations bulk_publish: bulkActionHandlers.bulkPublish, bulk_unpublish: bulkActionHandlers.bulkUnpublish, bulk_validate: bulkActionHandlers.bulkValidate, // Asset operations upload_asset: assetHandlers.uploadAsset, get_asset: assetHandlers.getAsset, update_asset: assetHandlers.updateAsset, delete_asset: assetHandlers.deleteAsset, publish_asset: assetHandlers.publishAsset, unpublish_asset: assetHandlers.unpublishAsset, list_assets: assetHandlers.listAssets, // Space & Environment operations list_spaces: spaceHandlers.listSpaces, get_space: spaceHandlers.getSpace, list_environments: spaceHandlers.listEnvironments, create_environment: spaceHandlers.createEnvironment, delete_environment: spaceHandlers.deleteEnvironment, // Content Type operations list_content_types: contentTypeHandlers.listContentTypes, get_content_type: contentTypeHandlers.getContentType, create_content_type: contentTypeHandlers.createContentType, update_content_type: contentTypeHandlers.updateContentType, delete_content_type: contentTypeHandlers.deleteContentType, publish_content_type: contentTypeHandlers.publishContentType, // AI Action operations list_ai_actions: aiActionHandlers.listAiActions, get_ai_action: aiActionHandlers.getAiAction, create_ai_action: aiActionHandlers.createAiAction, update_ai_action: aiActionHandlers.updateAiAction, delete_ai_action: aiActionHandlers.deleteAiAction, publish_ai_action: aiActionHandlers.publishAiAction, unpublish_ai_action: aiActionHandlers.unpublishAiAction, invoke_ai_action: aiActionHandlers.invokeAiAction, get_ai_action_invocation: aiActionHandlers.getAiActionInvocation, // Comment operations get_comments: commentHandlers.getComments, create_comment: commentHandlers.createComment, get_single_comment: commentHandlers.getSingleComment, delete_comment: commentHandlers.deleteComment, update_comment: commentHandlers.updateComment, } return handlers[name as keyof typeof handlers] } // Handler for dynamic AI Action tools async function handleAiActionInvocation(actionId: string, args: Record<string, unknown>) { try { console.error(`Handling AI Action invocation for ${actionId} with args:`, JSON.stringify(args)) // Get the parameters using the updated getInvocationParams const params = aiActionToolContext.getInvocationParams(actionId, args) // Directly use the variables property from getInvocationParams const invocationParams = { spaceId: params.spaceId, environmentId: params.environmentId, aiActionId: params.aiActionId, outputFormat: params.outputFormat, waitForCompletion: params.waitForCompletion, // Use the correctly formatted variables array directly rawVariables: params.variables, } console.error(`Invoking AI Action with params:`, JSON.stringify(invocationParams)) // Invoke the AI Action return aiActionHandlers.invokeAiAction(invocationParams) } catch (error) { console.error(`Error invoking AI Action:`, error) return { isError: true, message: error instanceof Error ? error.message : String(error), } } } // Functions to initialize and refresh AI Actions async function loadAiActions() { try { // First, clear the cache to avoid duplicates aiActionToolContext.clearCache() // Only load AI Actions if we have required space and environment if (!process.env.SPACE_ID) { return } // Fetch published AI Actions const response = await aiActionHandlers.listAiActions({ spaceId: process.env.SPACE_ID, environmentId: process.env.ENVIRONMENT_ID || "master", status: "published", }) // Check for errors or undefined response if (!response) { console.error("Error loading AI Actions: No response received") return } if (typeof response === "object" && "isError" in response) { console.error(`Error loading AI Actions: ${response.message}`) return } // Add each AI Action to the context for (const action of response.items) { aiActionToolContext.addAiAction(action) // Log variable mappings for debugging if (action.instruction.variables && action.instruction.variables.length > 0) { // Log ID mappings const idMappings = aiActionToolContext.getIdMappings(action.sys.id) if (idMappings && idMappings.size > 0) { const mappingLog = Array.from(idMappings.entries()) .map(([friendly, original]) => `${friendly} -> ${original}`) .join(", ") console.error(`AI Action ${action.name} - Parameter mappings: ${mappingLog}`) } // Log path mappings const pathMappings = aiActionToolContext.getPathMappings(action.sys.id) if (pathMappings && pathMappings.size > 0) { const pathMappingLog = Array.from(pathMappings.entries()) .map(([friendly, original]) => `${friendly} -> ${original}`) .join(", ") console.error(`AI Action ${action.name} - Path parameter mappings: ${pathMappingLog}`) } } } console.error(`Loaded ${response.items.length} AI Actions`) } catch (error) { console.error("Error loading AI Actions:", error) } } // Start the server async function runServer() { // Determine if HTTP server mode is enabled const enableHttp = process.env.ENABLE_HTTP_SERVER === "true" const httpPort = process.env.HTTP_PORT ? parseInt(process.env.HTTP_PORT) : 3000 // Load AI Actions before connecting await loadAiActions() if (enableHttp) { // Start StreamableHTTP server for MCP over HTTP const httpServer = new StreamableHttpServer({ port: httpPort, host: process.env.HTTP_HOST || "localhost", }) await httpServer.start() console.error( `Contentful MCP Server running with StreamableHTTP on port ${httpPort} using contentful host ${process.env.CONTENTFUL_HOST}`, ) // Keep the process running process.on("SIGINT", async () => { console.error("Shutting down HTTP server...") await httpServer.stop() process.exit(0) }) } else { // Traditional stdio mode const transport = new StdioServerTransport() // Connect to the server await server.connect(transport) console.error( `Contentful MCP Server running on stdio using contentful host ${process.env.CONTENTFUL_HOST}`, ) } // Set up periodic refresh of AI Actions (every 5 minutes) setInterval(loadAiActions, 5 * 60 * 1000) } runServer().catch((error) => { console.error("Fatal error running server:", error) process.exit(1) })

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ivo-toby/contentful-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server