Skip to main content
Glama
blog-tools.tsโ€ข17.7 kB
/** * MCP Blog Tools for GoHighLevel Integration * Exposes blog management capabilities to ChatGPT */ import { Tool } from '@modelcontextprotocol/sdk/types.js'; import { GHLApiClient } from '../clients/ghl-api-client.js'; import { MCPCreateBlogPostParams, MCPUpdateBlogPostParams, MCPGetBlogPostsParams, MCPGetBlogSitesParams, MCPGetBlogAuthorsParams, MCPGetBlogCategoriesParams, MCPCheckUrlSlugParams, GHLBlogPostStatus, GHLBlogPost, GHLBlogSite, GHLBlogAuthor, GHLBlogCategory } from '../types/ghl-types.js'; /** * Blog Tools Class * Implements MCP tools for blog management */ export class BlogTools { constructor(private ghlClient: GHLApiClient) {} /** * Get all blog tool definitions for MCP server */ getToolDefinitions(): Tool[] { return [ // 1. Create Blog Post { name: 'create_blog_post', description: 'Create a new blog post in GoHighLevel. Requires blog ID, author ID, and category IDs which can be obtained from other blog tools.', inputSchema: { type: 'object', properties: { title: { type: 'string', description: 'Blog post title' }, blogId: { type: 'string', description: 'Blog site ID (use get_blog_sites to find available blogs)' }, content: { type: 'string', description: 'Full HTML content of the blog post' }, description: { type: 'string', description: 'Short description/excerpt of the blog post' }, imageUrl: { type: 'string', description: 'URL of the featured image for the blog post' }, imageAltText: { type: 'string', description: 'Alt text for the featured image (for SEO and accessibility)' }, urlSlug: { type: 'string', description: 'URL slug for the blog post (use check_url_slug to verify availability)' }, author: { type: 'string', description: 'Author ID (use get_blog_authors to find available authors)' }, categories: { type: 'array', items: { type: 'string' }, description: 'Array of category IDs (use get_blog_categories to find available categories)' }, tags: { type: 'array', items: { type: 'string' }, description: 'Optional array of tags for the blog post' }, status: { type: 'string', enum: ['DRAFT', 'PUBLISHED', 'SCHEDULED', 'ARCHIVED'], description: 'Publication status of the blog post', default: 'DRAFT' }, canonicalLink: { type: 'string', description: 'Optional canonical URL for SEO' }, publishedAt: { type: 'string', description: 'Optional ISO timestamp for publication date (defaults to now for PUBLISHED status)' } }, required: ['title', 'blogId', 'content', 'description', 'imageUrl', 'imageAltText', 'urlSlug', 'author', 'categories'] } }, // 2. Update Blog Post { name: 'update_blog_post', description: 'Update an existing blog post in GoHighLevel. All fields except postId and blogId are optional.', inputSchema: { type: 'object', properties: { postId: { type: 'string', description: 'Blog post ID to update' }, blogId: { type: 'string', description: 'Blog site ID that contains the post' }, title: { type: 'string', description: 'Updated blog post title' }, content: { type: 'string', description: 'Updated HTML content of the blog post' }, description: { type: 'string', description: 'Updated description/excerpt of the blog post' }, imageUrl: { type: 'string', description: 'Updated featured image URL' }, imageAltText: { type: 'string', description: 'Updated alt text for the featured image' }, urlSlug: { type: 'string', description: 'Updated URL slug (use check_url_slug to verify availability)' }, author: { type: 'string', description: 'Updated author ID' }, categories: { type: 'array', items: { type: 'string' }, description: 'Updated array of category IDs' }, tags: { type: 'array', items: { type: 'string' }, description: 'Updated array of tags' }, status: { type: 'string', enum: ['DRAFT', 'PUBLISHED', 'SCHEDULED', 'ARCHIVED'], description: 'Updated publication status' }, canonicalLink: { type: 'string', description: 'Updated canonical URL' }, publishedAt: { type: 'string', description: 'Updated ISO timestamp for publication date' } }, required: ['postId', 'blogId'] } }, // 3. Get Blog Posts { name: 'get_blog_posts', description: 'Get blog posts from a specific blog site. Use this to list and search existing blog posts.', inputSchema: { type: 'object', properties: { blogId: { type: 'string', description: 'Blog site ID to get posts from (use get_blog_sites to find available blogs)' }, limit: { type: 'number', description: 'Number of posts to retrieve (default: 10, max recommended: 50)', default: 10 }, offset: { type: 'number', description: 'Number of posts to skip for pagination (default: 0)', default: 0 }, searchTerm: { type: 'string', description: 'Optional search term to filter posts by title or content' }, status: { type: 'string', enum: ['DRAFT', 'PUBLISHED', 'SCHEDULED', 'ARCHIVED'], description: 'Optional filter by publication status' } }, required: ['blogId'] } }, // 4. Get Blog Sites { name: 'get_blog_sites', description: 'Get all blog sites for the current location. Use this to find available blogs before creating or managing posts.', inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Number of blogs to retrieve (default: 10)', default: 10 }, skip: { type: 'number', description: 'Number of blogs to skip for pagination (default: 0)', default: 0 }, searchTerm: { type: 'string', description: 'Optional search term to filter blogs by name' } } } }, // 5. Get Blog Authors { name: 'get_blog_authors', description: 'Get all available blog authors for the current location. Use this to find author IDs for creating blog posts.', inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Number of authors to retrieve (default: 10)', default: 10 }, offset: { type: 'number', description: 'Number of authors to skip for pagination (default: 0)', default: 0 } } } }, // 6. Get Blog Categories { name: 'get_blog_categories', description: 'Get all available blog categories for the current location. Use this to find category IDs for creating blog posts.', inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Number of categories to retrieve (default: 10)', default: 10 }, offset: { type: 'number', description: 'Number of categories to skip for pagination (default: 0)', default: 0 } } } }, // 7. Check URL Slug { name: 'check_url_slug', description: 'Check if a URL slug is available for use. Use this before creating or updating blog posts to ensure unique URLs.', inputSchema: { type: 'object', properties: { urlSlug: { type: 'string', description: 'URL slug to check for availability' }, postId: { type: 'string', description: 'Optional post ID when updating an existing post (to exclude itself from the check)' } }, required: ['urlSlug'] } } ]; } /** * Execute blog tool based on tool name and arguments */ async executeTool(name: string, args: any): Promise<any> { switch (name) { case 'create_blog_post': return this.createBlogPost(args as MCPCreateBlogPostParams); case 'update_blog_post': return this.updateBlogPost(args as MCPUpdateBlogPostParams); case 'get_blog_posts': return this.getBlogPosts(args as MCPGetBlogPostsParams); case 'get_blog_sites': return this.getBlogSites(args as MCPGetBlogSitesParams); case 'get_blog_authors': return this.getBlogAuthors(args as MCPGetBlogAuthorsParams); case 'get_blog_categories': return this.getBlogCategories(args as MCPGetBlogCategoriesParams); case 'check_url_slug': return this.checkUrlSlug(args as MCPCheckUrlSlugParams); default: throw new Error(`Unknown tool: ${name}`); } } /** * CREATE BLOG POST */ private async createBlogPost(params: MCPCreateBlogPostParams): Promise<{ success: boolean; blogPost: GHLBlogPost; message: string }> { try { // Set default publishedAt if status is PUBLISHED and no date provided let publishedAt = params.publishedAt; if (!publishedAt && params.status === 'PUBLISHED') { publishedAt = new Date().toISOString(); } else if (!publishedAt) { publishedAt = new Date().toISOString(); // Always provide a date } const blogPostData = { title: params.title, locationId: this.ghlClient.getConfig().locationId, blogId: params.blogId, imageUrl: params.imageUrl, description: params.description, rawHTML: params.content, status: (params.status as GHLBlogPostStatus) || 'DRAFT', imageAltText: params.imageAltText, categories: params.categories, tags: params.tags || [], author: params.author, urlSlug: params.urlSlug, canonicalLink: params.canonicalLink, publishedAt: publishedAt }; const result = await this.ghlClient.createBlogPost(blogPostData); if (result.success && result.data) { return { success: true, blogPost: result.data.data, message: `Blog post "${params.title}" created successfully with ID: ${result.data.data._id}` }; } else { throw new Error('Failed to create blog post - no data returned'); } } catch (error) { throw new Error(`Failed to create blog post: ${error}`); } } /** * UPDATE BLOG POST */ private async updateBlogPost(params: MCPUpdateBlogPostParams): Promise<{ success: boolean; blogPost: GHLBlogPost; message: string }> { try { const updateData: any = { locationId: this.ghlClient.getConfig().locationId, blogId: params.blogId }; // Only include fields that are provided if (params.title) updateData.title = params.title; if (params.content) updateData.rawHTML = params.content; if (params.description) updateData.description = params.description; if (params.imageUrl) updateData.imageUrl = params.imageUrl; if (params.imageAltText) updateData.imageAltText = params.imageAltText; if (params.urlSlug) updateData.urlSlug = params.urlSlug; if (params.author) updateData.author = params.author; if (params.categories) updateData.categories = params.categories; if (params.tags) updateData.tags = params.tags; if (params.status) updateData.status = params.status; if (params.canonicalLink) updateData.canonicalLink = params.canonicalLink; if (params.publishedAt) updateData.publishedAt = params.publishedAt; const result = await this.ghlClient.updateBlogPost(params.postId, updateData); if (result.success && result.data) { return { success: true, blogPost: result.data.updatedBlogPost, message: `Blog post updated successfully` }; } else { throw new Error('Failed to update blog post - no data returned'); } } catch (error) { throw new Error(`Failed to update blog post: ${error}`); } } /** * GET BLOG POSTS */ private async getBlogPosts(params: MCPGetBlogPostsParams): Promise<{ success: boolean; posts: GHLBlogPost[]; count: number; message: string }> { try { const searchParams = { locationId: this.ghlClient.getConfig().locationId, blogId: params.blogId, limit: params.limit || 10, offset: params.offset || 0, searchTerm: params.searchTerm, status: params.status }; const result = await this.ghlClient.getBlogPosts(searchParams); if (result.success && result.data) { const posts = result.data.blogs || []; return { success: true, posts: posts, count: posts.length, message: `Retrieved ${posts.length} blog posts from blog ${params.blogId}` }; } else { throw new Error('Failed to get blog posts - no data returned'); } } catch (error) { throw new Error(`Failed to get blog posts: ${error}`); } } /** * GET BLOG SITES */ private async getBlogSites(params: MCPGetBlogSitesParams): Promise<{ success: boolean; sites: GHLBlogSite[]; count: number; message: string }> { try { const searchParams = { locationId: this.ghlClient.getConfig().locationId, skip: params.skip || 0, limit: params.limit || 10, searchTerm: params.searchTerm }; const result = await this.ghlClient.getBlogSites(searchParams); if (result.success && result.data) { const sites = result.data.data || []; return { success: true, sites: sites, count: sites.length, message: `Retrieved ${sites.length} blog sites` }; } else { throw new Error('Failed to get blog sites - no data returned'); } } catch (error) { throw new Error(`Failed to get blog sites: ${error}`); } } /** * GET BLOG AUTHORS */ private async getBlogAuthors(params: MCPGetBlogAuthorsParams): Promise<{ success: boolean; authors: GHLBlogAuthor[]; count: number; message: string }> { try { const searchParams = { locationId: this.ghlClient.getConfig().locationId, limit: params.limit || 10, offset: params.offset || 0 }; const result = await this.ghlClient.getBlogAuthors(searchParams); if (result.success && result.data) { const authors = result.data.authors || []; return { success: true, authors: authors, count: authors.length, message: `Retrieved ${authors.length} blog authors` }; } else { throw new Error('Failed to get blog authors - no data returned'); } } catch (error) { throw new Error(`Failed to get blog authors: ${error}`); } } /** * GET BLOG CATEGORIES */ private async getBlogCategories(params: MCPGetBlogCategoriesParams): Promise<{ success: boolean; categories: GHLBlogCategory[]; count: number; message: string }> { try { const searchParams = { locationId: this.ghlClient.getConfig().locationId, limit: params.limit || 10, offset: params.offset || 0 }; const result = await this.ghlClient.getBlogCategories(searchParams); if (result.success && result.data) { const categories = result.data.categories || []; return { success: true, categories: categories, count: categories.length, message: `Retrieved ${categories.length} blog categories` }; } else { throw new Error('Failed to get blog categories - no data returned'); } } catch (error) { throw new Error(`Failed to get blog categories: ${error}`); } } /** * CHECK URL SLUG */ private async checkUrlSlug(params: MCPCheckUrlSlugParams): Promise<{ success: boolean; urlSlug: string; exists: boolean; available: boolean; message: string }> { try { const checkParams = { locationId: this.ghlClient.getConfig().locationId, urlSlug: params.urlSlug, postId: params.postId }; const result = await this.ghlClient.checkUrlSlugExists(checkParams); if (result.success && result.data !== undefined) { const exists = result.data.exists; return { success: true, urlSlug: params.urlSlug, exists: exists, available: !exists, message: exists ? `URL slug "${params.urlSlug}" is already in use` : `URL slug "${params.urlSlug}" is available` }; } else { throw new Error('Failed to check URL slug - no data returned'); } } catch (error) { throw new Error(`Failed to check URL slug: ${error}`); } } }

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/clickmediapropy/gohighlevel-mcp-server'

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