Skip to main content
Glama
api-client.js6.13 kB
/** * Shared Ghost API Client Module * * Provides centralized API client creation with dependency injection for testing. * All credentials are loaded from environment variables. */ import axios from 'axios'; import crypto from 'crypto'; /** * Configuration loaded from environment variables */ export function getConfig() { return { apiUrl: process.env.GHOST_API_URL || 'https://blog.emmanuelu.com', adminKey: process.env.GHOST_ADMIN_KEY || '', }; } /** * Parse Admin API key into ID and Secret * Format: {id}:{secret} */ export function parseAdminKey(adminKey) { if (!adminKey) { throw new Error('GHOST_ADMIN_KEY environment variable is required'); } const parts = adminKey.split(':'); if (parts.length !== 2) { throw new Error('GHOST_ADMIN_KEY must be in format {id}:{secret}'); } return { id: parts[0], secret: parts[1] }; } /** * Create a JWT token for Ghost Admin API * * @param {string} keyId - The Admin API key ID * @param {string} keySecret - The Admin API key secret (hex encoded) * @returns {string} - JWT token */ export function createGhostToken(keyId, keySecret) { const header = { alg: "HS256", typ: "JWT", kid: keyId }; const iat = Math.floor(Date.now() / 1000); const payload = { iat: iat, exp: iat + 300, // Token expires in 5 minutes aud: "/admin/" }; // Base64url encode function const b64encode = (obj) => { return Buffer.from(JSON.stringify(obj)) .toString('base64') .replace(/=+$/, '') .replace(/\+/g, '-') .replace(/\//g, '_'); }; const headerEncoded = b64encode(header); const payloadEncoded = b64encode(payload); const message = `${headerEncoded}.${payloadEncoded}`; const key = Buffer.from(keySecret, 'hex'); const signature = crypto.createHmac('sha256', key) .update(message) .digest('base64') .replace(/=+$/, '') .replace(/\+/g, '-') .replace(/\//g, '_'); return `${headerEncoded}.${payloadEncoded}.${signature}`; } /** * Add retry interceptor to axios instance * * @param {object} axiosInstance - Axios instance to add retry logic to * @returns {object} - The same axios instance with retry logic added */ export function addRetryInterceptor(axiosInstance) { axiosInstance.interceptors.response.use(null, async (error) => { const { config } = error; if (!config || !config.retry) { return Promise.reject(error); } config.retryCount = config.retryCount || 0; if (config.retryCount >= config.retry) { return Promise.reject(error); } config.retryCount += 1; const backoff = config.retryDelay || 1000; await new Promise(resolve => setTimeout(resolve, backoff)); return axiosInstance(config); }); return axiosInstance; } /** * Create a Ghost API client with authentication and retry logic * * @param {object} options - Configuration options * @param {object} options.axiosInstance - Optional axios instance for dependency injection (testing) * @param {string} options.token - Optional pre-generated token * @param {string} options.adminKey - Optional admin key (overrides env var) * @param {string} options.apiUrl - Optional API URL (overrides env var) * @returns {object} - Object containing { client, apiUrl, token } */ export function createApiClient(options = {}) { const config = getConfig(); const apiUrl = options.apiUrl || config.apiUrl; const adminKey = options.adminKey || config.adminKey; // Parse admin key if not providing a pre-made token let token = options.token; if (!token) { const { id, secret } = parseAdminKey(adminKey); token = createGhostToken(id, secret); } // Use injected axios instance or create new one let client; if (options.axiosInstance) { client = options.axiosInstance; } else { client = axios.create({ timeout: 30000, headers: { 'Authorization': `Ghost ${token}`, 'Accept': 'application/json', 'Content-Type': 'application/json' } }); addRetryInterceptor(client); } return { client, apiUrl: apiUrl.replace(/\/$/, ''), // Remove trailing slash token }; } /** * Build Ghost Admin API URL for a specific endpoint * * @param {string} baseUrl - Base API URL * @param {string} endpoint - API endpoint (e.g., 'posts', 'pages', 'tags') * @param {object} params - Optional query parameters * @returns {string} - Full URL */ export function buildApiUrl(baseUrl, endpoint, params = {}) { let url = `${baseUrl}/ghost/api/admin/${endpoint}/`; const queryParams = new URLSearchParams(); for (const [key, value] of Object.entries(params)) { if (value !== undefined && value !== null) { queryParams.append(key, value); } } const queryString = queryParams.toString(); if (queryString) { url += `?${queryString}`; } return url; } /** * Format error response for MCP tools * * @param {Error} error - Error object * @returns {object} - Formatted error response */ export function formatErrorResponse(error) { let errorMsg = error.message; if (error.response) { errorMsg += `\nResponse: ${JSON.stringify(error.response.data)}`; } return { content: [{ type: 'text', text: JSON.stringify({ error: errorMsg }, null, 2), }], isError: true }; } /** * Format success response for MCP tools * * @param {object} data - Response data * @returns {object} - Formatted success response */ export function formatSuccessResponse(data) { return { content: [{ type: 'text', text: JSON.stringify(data, null, 2), }], }; }

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