Skip to main content
Glama
content-operations.js11.9 kB
/** * Generic CRUD operations factory for Ghost content types (posts, pages) * * This module provides factory functions that generate handlers for * create, read, update, delete, and list operations on Ghost content. */ import { createApiClient, buildApiUrl, formatErrorResponse, formatSuccessResponse } from './api-client.js'; /** * Create a validation error response */ function validationError(message) { return { content: [{ type: 'text', text: JSON.stringify({ error: message }, null, 2), }], isError: true }; } /** * Factory to create a "create content" handler * * @param {string} contentType - 'posts' or 'pages' * @param {object} options - Configuration options * @param {boolean} options.supportsTags - Whether this content type supports tags */ export function createContentHandler(contentType, options = {}) { const { supportsTags = false } = options; const singularType = contentType.slice(0, -1); // posts -> post, pages -> page return async function(server, args) { // Validate required parameters if (!args.title || !args.content) { return validationError("Title and content are required"); } const status = args.status || "draft"; const featured = args.featured !== undefined ? args.featured : false; try { const { client, apiUrl } = createApiClient(); const url = buildApiUrl(apiUrl, contentType, { source: 'html' }); const payload = { [contentType]: [{ title: args.title, html: args.content, status, featured }] }; if (supportsTags && args.tags) { payload[contentType][0].tags = args.tags; } console.log(`Creating ${singularType} at: ${url}`); const response = await client.post(url, payload, { retry: 3, retryDelay: 1000 }); return formatSuccessResponse(response.data); } catch (error) { return formatErrorResponse(error); } }; } /** * Factory to create a "list content" handler * * @param {string} contentType - 'posts' or 'pages' */ export function listContentHandler(contentType) { return async function(server, args) { const page = args.page || 1; const limit = args.limit || 15; const status = args.status || null; const include = args.include || "tags,authors"; try { const { client, apiUrl } = createApiClient(); const url = buildApiUrl(apiUrl, contentType); const params = { page, limit, include }; if (status) { params.filter = `status:${status}`; } console.log(`Listing ${contentType} from: ${url}`); const response = await client.get(url, { params, retry: 3, retryDelay: 1000 }); return formatSuccessResponse(response.data); } catch (error) { return formatErrorResponse(error); } }; } /** * Factory to create an "update content" handler * * @param {string} contentType - 'posts' or 'pages' * @param {string} idField - Name of the ID field in args (e.g., 'post_id', 'page_id') * @param {object} options - Configuration options * @param {boolean} options.supportsTags - Whether this content type supports tags */ export function updateContentHandler(contentType, idField, options = {}) { const { supportsTags = false } = options; const singularType = contentType.slice(0, -1); return async function(server, args) { const id = args[idField]; if (!id) { return validationError(`${singularType.charAt(0).toUpperCase() + singularType.slice(1)} ID is required`); } try { const { client, apiUrl } = createApiClient(); // Fetch current content to get updated_at for collision detection const getUrl = buildApiUrl(apiUrl, `${contentType}/${id}`); console.log(`Fetching current ${singularType} from: ${getUrl}`); const getResponse = await client.get(getUrl, { retry: 3, retryDelay: 1000 }); const currentContent = getResponse.data[contentType]?.[0]; if (!currentContent) { return validationError(`${singularType.charAt(0).toUpperCase() + singularType.slice(1)} ${id} not found`); } // Build update payload const updateUrl = buildApiUrl(apiUrl, `${contentType}/${id}`, { source: 'html' }); const payload = { [contentType]: [{ updated_at: currentContent.updated_at }] }; // Add optional fields const fieldMappings = { title: 'title', content: 'html', status: 'status', featured: 'featured' }; if (supportsTags) { fieldMappings.tags = 'tags'; } for (const [argField, payloadField] of Object.entries(fieldMappings)) { if (args[argField] !== undefined) { payload[contentType][0][payloadField] = args[argField]; } } console.log(`Updating ${singularType} at: ${updateUrl}`); const response = await client.put(updateUrl, payload, { retry: 3, retryDelay: 1000 }); return formatSuccessResponse(response.data); } catch (error) { return formatErrorResponse(error); } }; } /** * Factory to create a "delete content" handler * * @param {string} contentType - 'posts' or 'pages' * @param {string} idField - Name of the ID field in args */ export function deleteContentHandler(contentType, idField) { const singularType = contentType.slice(0, -1); return async function(server, args) { const id = args[idField]; if (!id) { return validationError(`${singularType.charAt(0).toUpperCase() + singularType.slice(1)} ID is required`); } try { const { client, apiUrl } = createApiClient(); const url = buildApiUrl(apiUrl, `${contentType}/${id}`); console.log(`Deleting ${singularType} at: ${url}`); const response = await client.delete(url, { retry: 3, retryDelay: 1000 }); // Ghost returns 204 No Content for successful deletion if (response.status === 204) { return formatSuccessResponse({ success: true, message: `${singularType.charAt(0).toUpperCase() + singularType.slice(1)} ${id} deleted successfully` }); } return formatSuccessResponse(response.data); } catch (error) { return formatErrorResponse(error); } }; } /** * Generate tool definition for create operations */ export function createToolDefinition(contentType, options = {}) { const singularType = contentType.slice(0, -1); const { supportsTags = false } = options; const properties = { title: { type: 'string', description: `The title of the ${singularType}`, }, content: { type: 'string', description: `The content/body of the ${singularType} (HTML)`, }, status: { type: 'string', description: `${singularType.charAt(0).toUpperCase() + singularType.slice(1)} status (draft, published, scheduled)`, default: 'draft' }, featured: { type: 'boolean', description: `Whether this is a featured ${singularType}`, default: false }, }; if (supportsTags) { properties.tags = { type: 'array', items: { type: 'object' }, description: "List of tag objects (each with 'name')", }; } return { name: `create_ghost_${singularType}`, description: `Creates a new ${singularType} in Ghost blog`, inputSchema: { type: 'object', properties, required: ['title', 'content'], }, }; } /** * Generate tool definition for list operations */ export function listToolDefinition(contentType) { const singularType = contentType.slice(0, -1); return { name: `list_ghost_${contentType}`, description: `Lists ${contentType} from Ghost blog with pagination and filtering options`, inputSchema: { type: 'object', properties: { page: { type: 'integer', description: 'Page number for pagination', default: 1 }, limit: { type: 'integer', description: `Number of ${contentType} per page`, default: 15 }, status: { type: 'string', description: `Filter by ${singularType} status (draft, published, scheduled)`, }, include: { type: 'string', description: 'Related data to include (comma-separated: tags,authors)', default: 'tags,authors' }, }, required: [], }, }; } /** * Generate tool definition for update operations */ export function updateToolDefinition(contentType, options = {}) { const singularType = contentType.slice(0, -1); const { supportsTags = false } = options; const properties = { [`${singularType}_id`]: { type: 'string', description: `The ID of the ${singularType} to update`, }, title: { type: 'string', description: `New title for the ${singularType}`, }, content: { type: 'string', description: `New content/body for the ${singularType} in HTML`, }, status: { type: 'string', description: `New status for the ${singularType} (draft, published, scheduled)`, }, featured: { type: 'boolean', description: `Whether this should be a featured ${singularType}`, }, }; if (supportsTags) { properties.tags = { type: 'array', items: { type: 'object' }, description: "List of tag objects (each with 'name')", }; } return { name: `update_ghost_${singularType}`, description: `Updates an existing ${singularType} in Ghost blog`, inputSchema: { type: 'object', properties, required: [`${singularType}_id`], }, }; } /** * Generate tool definition for delete operations */ export function deleteToolDefinition(contentType) { const singularType = contentType.slice(0, -1); return { name: `delete_ghost_${singularType}`, description: `Deletes a ${singularType} from Ghost blog`, inputSchema: { type: 'object', properties: { [`${singularType}_id`]: { type: 'string', description: `The ID of the ${singularType} to delete`, } }, required: [`${singularType}_id`], }, }; }

Latest Blog Posts

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/oculairmedia/Ghost-MCP'

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