contentful-mcp
by ivo-toby
- contentful-mcp
- src
- config
import { getContentfulClient } from "./client"
import type {
AiActionEntity,
AiActionEntityCollection,
AiActionInvocation,
AiActionInvocationType,
AiActionSchemaParsed,
StatusFilter,
} from "../types/ai-actions"
// Alpha header required for AI Actions (temporary - will be removed in 2-3 weeks)
// TODO: Remove alpha header after 2-3 weeks (around May 2025) when it's no longer required
const ALPHA_HEADER_NAME = "X-Contentful-Enable-Alpha-Feature"
const ALPHA_HEADER_VALUE = "ai-service"
/**
* Add alpha header to request options
* @param options Request options
* @returns Options with alpha header added
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function withAlphaHeader(options: any = {}): any {
const headers = options.headers || {}
return {
...options,
headers: {
...headers,
[ALPHA_HEADER_NAME]: ALPHA_HEADER_VALUE,
},
}
}
/**
* Extract data from response, handling both direct response and response.data formats
* @param response API response from contentful-management client
* @returns Extracted data
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function extractResponseData<T>(response: any): T {
// If we have a response but no data property, check if the response itself is the data
if (response && !response.data && typeof response === "object") {
// For collections (AI Action listing)
if ("items" in response && "sys" in response && response.sys.type === "Array") {
return response as T
}
// For single entities (AI Action retrieval)
if ("sys" in response) {
return response as T
}
}
// Default to the data property
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (response as any).data as T
}
/**
* Parameters for AI Action API operations
*/
// List AI Actions
export interface ListAiActionsParams {
spaceId: string
environmentId?: string
limit?: number
skip?: number
status?: StatusFilter
}
// Get AI Action
export interface GetAiActionParams {
spaceId: string
environmentId?: string
aiActionId: string
}
// Create AI Action
export interface CreateAiActionParams {
spaceId: string
environmentId?: string
actionData: AiActionSchemaParsed
}
// Update AI Action
export interface UpdateAiActionParams {
spaceId: string
environmentId?: string
aiActionId: string
version: number
actionData: AiActionSchemaParsed
}
// Delete AI Action
export interface DeleteAiActionParams {
spaceId: string
environmentId?: string
aiActionId: string
version: number
}
// Publish AI Action
export interface PublishAiActionParams {
spaceId: string
environmentId?: string
aiActionId: string
version: number
}
// Unpublish AI Action
export interface UnpublishAiActionParams {
spaceId: string
environmentId?: string
aiActionId: string
}
// Invoke AI Action
export interface InvokeAiActionParams {
spaceId: string
environmentId?: string
aiActionId: string
invocationData: AiActionInvocationType
}
// Get AI Action invocation
export interface GetAiActionInvocationParams {
spaceId: string
environmentId?: string
aiActionId: string
invocationId: string
}
/**
* AI Actions client for interacting with the Contentful API
*/
export const aiActionsClient = {
/**
* List AI Actions in a space
*/
async listAiActions({
spaceId,
environmentId = "master",
limit = 100,
skip = 0,
status,
}: ListAiActionsParams): Promise<AiActionEntityCollection> {
const client = await getContentfulClient()
// Build the URL for listing AI actions
let url = `/spaces/${spaceId}`
// Add environment if specified (API uses space-level endpoint for AI Actions)
if (environmentId) {
url += `/environments/${environmentId}`
}
url += "/ai/actions"
const queryParams = new URLSearchParams()
queryParams.append("limit", limit.toString())
queryParams.append("skip", skip.toString())
if (status) {
queryParams.append("status", status)
}
const queryString = queryParams.toString()
if (queryString) {
url += `?${queryString}`
}
const response = await client.raw.get(url, withAlphaHeader())
return extractResponseData<AiActionEntityCollection>(response)
},
/**
* Get a specific AI Action
*/
async getAiAction({
spaceId,
environmentId = "master",
aiActionId,
}: GetAiActionParams): Promise<AiActionEntity> {
const client = await getContentfulClient()
let url = `/spaces/${spaceId}`
// Add environment if specified
if (environmentId) {
url += `/environments/${environmentId}`
}
url += `/ai/actions/${aiActionId}`
const response = await client.raw.get(url, withAlphaHeader())
return extractResponseData<AiActionEntity>(response)
},
/**
* Create a new AI Action
*/
async createAiAction({
spaceId,
environmentId = "master",
actionData,
}: CreateAiActionParams): Promise<AiActionEntity> {
const client = await getContentfulClient()
let url = `/spaces/${spaceId}`
// Add environment if specified
if (environmentId) {
url += `/environments/${environmentId}`
}
url += `/ai/actions`
const response = await client.raw.post(url, actionData, withAlphaHeader())
return extractResponseData<AiActionEntity>(response)
},
/**
* Update an existing AI Action
*/
async updateAiAction({
spaceId,
environmentId = "master",
aiActionId,
version,
actionData,
}: UpdateAiActionParams): Promise<AiActionEntity> {
const client = await getContentfulClient()
let url = `/spaces/${spaceId}`
// Add environment if specified
if (environmentId) {
url += `/environments/${environmentId}`
}
url += `/ai/actions/${aiActionId}`
const headers = {
"X-Contentful-Version": version.toString(),
}
const response = await client.raw.put(url, actionData, withAlphaHeader({ headers }))
return extractResponseData<AiActionEntity>(response)
},
/**
* Delete an AI Action
*/
async deleteAiAction({
spaceId,
environmentId = "master",
aiActionId,
version,
}: DeleteAiActionParams): Promise<void> {
const client = await getContentfulClient()
let url = `/spaces/${spaceId}`
// Add environment if specified
if (environmentId) {
url += `/environments/${environmentId}`
}
url += `/ai/actions/${aiActionId}`
const headers = {
"X-Contentful-Version": version.toString(),
}
await client.raw.delete(url, withAlphaHeader({ headers }))
},
/**
* Publish an AI Action
*/
async publishAiAction({
spaceId,
environmentId = "master",
aiActionId,
version,
}: PublishAiActionParams): Promise<AiActionEntity> {
const client = await getContentfulClient()
let url = `/spaces/${spaceId}`
// Add environment if specified
if (environmentId) {
url += `/environments/${environmentId}`
}
url += `/ai/actions/${aiActionId}/published`
const headers = {
"X-Contentful-Version": version.toString(),
}
const response = await client.raw.put(url, {}, withAlphaHeader({ headers }))
return extractResponseData<AiActionEntity>(response)
},
/**
* Unpublish an AI Action
*/
async unpublishAiAction({
spaceId,
environmentId = "master",
aiActionId,
}: UnpublishAiActionParams): Promise<AiActionEntity> {
const client = await getContentfulClient()
let url = `/spaces/${spaceId}`
// Add environment if specified
if (environmentId) {
url += `/environments/${environmentId}`
}
url += `/ai/actions/${aiActionId}/published`
const response = await client.raw.delete(url, withAlphaHeader())
return extractResponseData<AiActionEntity>(response)
},
/**
* Invoke an AI Action
*/
async invokeAiAction({
spaceId,
environmentId = "master",
aiActionId,
invocationData,
}: InvokeAiActionParams): Promise<AiActionInvocation> {
const client = await getContentfulClient()
let url = `/spaces/${spaceId}`
if (environmentId) {
url += `/environments/${environmentId}`
}
url += `/ai/actions/${aiActionId}/invoke`
const headers = {
"X-Contentful-Include-Invocation-Metadata": "true",
}
// Debug log the invocation data before sending
console.error(`AI Action invocation request to ${url}:`, JSON.stringify(invocationData))
const response = await client.raw.post(url, invocationData, withAlphaHeader({ headers }))
return extractResponseData<AiActionInvocation>(response)
},
/**
* Get an AI Action invocation result
*/
async getAiActionInvocation({
spaceId,
environmentId = "master",
aiActionId,
invocationId,
}: GetAiActionInvocationParams): Promise<AiActionInvocation> {
const client = await getContentfulClient()
let url = `/spaces/${spaceId}`
if (environmentId) {
url += `/environments/${environmentId}`
}
url += `/ai/actions/${aiActionId}/invocations/${invocationId}`
const headers = {
"X-Contentful-Include-Invocation-Metadata": "true",
}
const response = await client.raw.get(url, withAlphaHeader({ headers }))
return extractResponseData<AiActionInvocation>(response)
},
/**
* Poll an AI Action invocation until it is complete or fails
* @param params The invocation parameters
* @param maxAttempts Maximum number of polling attempts
* @param initialDelay Initial delay in ms between polling attempts
* @param maxDelay Maximum delay in ms between polling attempts
*/
async pollInvocation(
params: GetAiActionInvocationParams,
maxAttempts = 10,
initialDelay = 1000,
maxDelay = 5000,
): Promise<AiActionInvocation> {
let attempts = 0
let delay = initialDelay
while (attempts < maxAttempts) {
const invocation = await this.getAiActionInvocation(params)
if (
invocation.sys.status === "COMPLETED" ||
invocation.sys.status === "FAILED" ||
invocation.sys.status === "CANCELLED"
) {
return invocation
}
// Wait with exponential backoff
await new Promise((resolve) => setTimeout(resolve, delay))
delay = Math.min(delay * 1.5, maxDelay)
attempts++
}
throw new Error(`AI Action invocation polling exceeded maximum attempts (${maxAttempts})`)
},
}