/**
* 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}`);
}
}
}