mirror of
https://github.com/jlengrand/ghost-mcp.git
synced 2026-03-10 08:21:19 +00:00
✨ Support webhooks management
This commit is contained in:
@@ -111,6 +111,11 @@ GHOST_API_URL=your_ghost_api_url GHOST_STAFF_API_KEY=your_staff_api_key npx @mod
|
|||||||
- `create_newsletter`: Create a new newsletter with specified details
|
- `create_newsletter`: Create a new newsletter with specified details
|
||||||
- `update_newsletter`: Update an existing newsletter with new information
|
- `update_newsletter`: Update an existing newsletter with new information
|
||||||
|
|
||||||
|
### Webhooks Management
|
||||||
|
- `create_webhook`: Create a new webhook with specified details
|
||||||
|
- `update_webhook`: Update an existing webhook with new information
|
||||||
|
- `delete_webhook`: Delete a specific webhook
|
||||||
|
|
||||||
## Available Resources
|
## Available Resources
|
||||||
|
|
||||||
All resources follow the URI pattern: `[type]://[id]`
|
All resources follow the URI pattern: `[type]://[id]`
|
||||||
|
|||||||
@@ -69,6 +69,9 @@ def create_server() -> FastMCP:
|
|||||||
mcp.tool()(tools.update_newsletter)
|
mcp.tool()(tools.update_newsletter)
|
||||||
mcp.tool()(tools.list_roles)
|
mcp.tool()(tools.list_roles)
|
||||||
mcp.tool()(tools.create_invite)
|
mcp.tool()(tools.create_invite)
|
||||||
|
mcp.tool()(tools.create_webhook)
|
||||||
|
mcp.tool()(tools.update_webhook)
|
||||||
|
mcp.tool()(tools.delete_webhook)
|
||||||
|
|
||||||
# Register prompts
|
# Register prompts
|
||||||
@mcp.prompt()
|
@mcp.prompt()
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ from .tools.newsletters import (
|
|||||||
)
|
)
|
||||||
from .tools.roles import list_roles
|
from .tools.roles import list_roles
|
||||||
from .tools.invites import create_invite
|
from .tools.invites import create_invite
|
||||||
|
from .tools.webhooks import create_webhook, update_webhook, delete_webhook
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'search_posts_by_title',
|
'search_posts_by_title',
|
||||||
@@ -69,5 +70,8 @@ __all__ = [
|
|||||||
'create_newsletter',
|
'create_newsletter',
|
||||||
'update_newsletter',
|
'update_newsletter',
|
||||||
'list_roles',
|
'list_roles',
|
||||||
'create_invite'
|
'create_invite',
|
||||||
|
'create_webhook',
|
||||||
|
'update_webhook',
|
||||||
|
'delete_webhook'
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from .offers import list_offers, read_offer, create_offer, update_offer
|
|||||||
from .newsletters import list_newsletters, read_newsletter, create_newsletter, update_newsletter
|
from .newsletters import list_newsletters, read_newsletter, create_newsletter, update_newsletter
|
||||||
from .roles import list_roles
|
from .roles import list_roles
|
||||||
from .invites import create_invite
|
from .invites import create_invite
|
||||||
|
from .webhooks import create_webhook, update_webhook, delete_webhook
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'search_posts_by_title',
|
'search_posts_by_title',
|
||||||
@@ -37,5 +38,8 @@ __all__ = [
|
|||||||
'create_newsletter',
|
'create_newsletter',
|
||||||
'update_newsletter',
|
'update_newsletter',
|
||||||
'list_roles',
|
'list_roles',
|
||||||
'create_invite'
|
'create_invite',
|
||||||
|
'create_webhook',
|
||||||
|
'update_webhook',
|
||||||
|
'delete_webhook'
|
||||||
]
|
]
|
||||||
|
|||||||
344
src/ghost_mcp/tools/webhooks.py
Normal file
344
src/ghost_mcp/tools/webhooks.py
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
"""Webhook-related MCP tools for Ghost API."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from typing import Optional
|
||||||
|
from mcp.server.fastmcp import Context
|
||||||
|
|
||||||
|
from ..api import make_ghost_request, get_auth_headers
|
||||||
|
from ..config import STAFF_API_KEY
|
||||||
|
from ..exceptions import GhostError
|
||||||
|
|
||||||
|
async def create_webhook(
|
||||||
|
event: str,
|
||||||
|
target_url: str,
|
||||||
|
integration_id: Optional[str] = None,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
secret: Optional[str] = None,
|
||||||
|
api_version: Optional[str] = None,
|
||||||
|
ctx: Context = None
|
||||||
|
) -> str:
|
||||||
|
"""Create a new webhook in Ghost.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
event: Event to trigger the webhook (required)
|
||||||
|
target_url: URL to send the webhook to (required)
|
||||||
|
integration_id: ID of the integration (optional - only needed for user authentication)
|
||||||
|
name: Name of the webhook (optional)
|
||||||
|
secret: Secret for the webhook (optional)
|
||||||
|
api_version: API version for the webhook (optional)
|
||||||
|
ctx: Optional context for logging
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
String representation of the created webhook
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
GhostError: If the Ghost API request fails
|
||||||
|
ValueError: If required parameters are missing or invalid
|
||||||
|
"""
|
||||||
|
# List of valid webhook events from Ghost documentation
|
||||||
|
valid_events = [
|
||||||
|
'site.changed',
|
||||||
|
'post.added',
|
||||||
|
'post.deleted',
|
||||||
|
'post.edited',
|
||||||
|
'post.published',
|
||||||
|
'post.published.edited',
|
||||||
|
'post.unpublished',
|
||||||
|
'post.scheduled',
|
||||||
|
'post.unscheduled',
|
||||||
|
'post.rescheduled',
|
||||||
|
'page.added',
|
||||||
|
'page.deleted',
|
||||||
|
'page.edited',
|
||||||
|
'page.published',
|
||||||
|
'page.published.edited',
|
||||||
|
'page.unpublished',
|
||||||
|
'page.scheduled',
|
||||||
|
'page.unscheduled',
|
||||||
|
'page.rescheduled',
|
||||||
|
'tag.added',
|
||||||
|
'tag.edited',
|
||||||
|
'tag.deleted',
|
||||||
|
'post.tag.attached',
|
||||||
|
'post.tag.detached',
|
||||||
|
'page.tag.attached',
|
||||||
|
'page.tag.detached',
|
||||||
|
'member.added',
|
||||||
|
'member.edited',
|
||||||
|
'member.deleted'
|
||||||
|
]
|
||||||
|
|
||||||
|
if not all([event, target_url]):
|
||||||
|
raise ValueError("event and target_url are required")
|
||||||
|
|
||||||
|
if event not in valid_events:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid event. Must be one of: {', '.join(valid_events)}\n"
|
||||||
|
"See Ghost documentation for event descriptions."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure target_url has a trailing slash and is a valid URL
|
||||||
|
if not target_url.endswith('/'):
|
||||||
|
target_url = f"{target_url}/"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Validate URL format
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
parsed = urlparse(target_url)
|
||||||
|
if not all([parsed.scheme, parsed.netloc]):
|
||||||
|
raise ValueError
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(
|
||||||
|
"target_url must be a valid URL in the format 'https://example.com/hook/'"
|
||||||
|
)
|
||||||
|
|
||||||
|
if ctx:
|
||||||
|
ctx.info(f"Creating webhook for event: {event} targeting: {target_url}")
|
||||||
|
|
||||||
|
# Construct webhook data
|
||||||
|
webhook_data = {
|
||||||
|
"webhooks": [{
|
||||||
|
"event": event,
|
||||||
|
"target_url": target_url
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add optional fields if provided
|
||||||
|
webhook = webhook_data["webhooks"][0]
|
||||||
|
if integration_id:
|
||||||
|
webhook["integration_id"] = integration_id
|
||||||
|
if name:
|
||||||
|
webhook["name"] = name
|
||||||
|
if secret:
|
||||||
|
webhook["secret"] = secret
|
||||||
|
if api_version:
|
||||||
|
webhook["api_version"] = api_version
|
||||||
|
|
||||||
|
try:
|
||||||
|
if ctx:
|
||||||
|
ctx.debug("Getting auth headers")
|
||||||
|
headers = await get_auth_headers(STAFF_API_KEY)
|
||||||
|
|
||||||
|
if ctx:
|
||||||
|
ctx.debug("Making API request to create webhook")
|
||||||
|
response = await make_ghost_request(
|
||||||
|
"webhooks/",
|
||||||
|
headers,
|
||||||
|
ctx,
|
||||||
|
http_method="POST",
|
||||||
|
json_data=webhook_data
|
||||||
|
)
|
||||||
|
|
||||||
|
if ctx:
|
||||||
|
ctx.debug("Processing created webhook response")
|
||||||
|
|
||||||
|
webhook = response.get("webhooks", [{}])[0]
|
||||||
|
|
||||||
|
return f"""
|
||||||
|
Webhook created successfully:
|
||||||
|
Event: {webhook.get('event')}
|
||||||
|
Target URL: {webhook.get('target_url')}
|
||||||
|
Name: {webhook.get('name', 'None')}
|
||||||
|
API Version: {webhook.get('api_version', 'v5')}
|
||||||
|
Status: {webhook.get('status', 'available')}
|
||||||
|
Integration ID: {webhook.get('integration_id', 'None')}
|
||||||
|
Created: {webhook.get('created_at', 'Unknown')}
|
||||||
|
Last Triggered: {webhook.get('last_triggered_at', 'Never')}
|
||||||
|
Last Status: {webhook.get('last_triggered_status', 'N/A')}
|
||||||
|
Last Error: {webhook.get('last_triggered_error', 'None')}
|
||||||
|
ID: {webhook.get('id')}
|
||||||
|
"""
|
||||||
|
except Exception as e:
|
||||||
|
if ctx:
|
||||||
|
ctx.error(f"Failed to create webhook: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def delete_webhook(
|
||||||
|
webhook_id: str,
|
||||||
|
ctx: Context = None
|
||||||
|
) -> str:
|
||||||
|
"""Delete a webhook from Ghost.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
webhook_id: ID of the webhook to delete (required)
|
||||||
|
ctx: Optional context for logging
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Success message if deletion was successful
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
GhostError: If the Ghost API request fails
|
||||||
|
ValueError: If webhook_id is not provided
|
||||||
|
"""
|
||||||
|
if not webhook_id:
|
||||||
|
raise ValueError("webhook_id is required")
|
||||||
|
|
||||||
|
if ctx:
|
||||||
|
ctx.info(f"Attempting to delete webhook with ID: {webhook_id}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if ctx:
|
||||||
|
ctx.debug("Getting auth headers")
|
||||||
|
headers = await get_auth_headers(STAFF_API_KEY)
|
||||||
|
|
||||||
|
if ctx:
|
||||||
|
ctx.debug(f"Making API request to delete webhook {webhook_id}")
|
||||||
|
response = await make_ghost_request(
|
||||||
|
f"webhooks/{webhook_id}/",
|
||||||
|
headers,
|
||||||
|
ctx,
|
||||||
|
http_method="DELETE"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check for 204 status code
|
||||||
|
if response == {}:
|
||||||
|
return f"Webhook with ID {webhook_id} has been successfully deleted."
|
||||||
|
else:
|
||||||
|
raise GhostError("Unexpected response from Ghost API")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if ctx:
|
||||||
|
ctx.error(f"Failed to delete webhook: {str(e)}")
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def update_webhook(
|
||||||
|
webhook_id: str,
|
||||||
|
event: Optional[str] = None,
|
||||||
|
target_url: Optional[str] = None,
|
||||||
|
name: Optional[str] = None,
|
||||||
|
api_version: Optional[str] = None,
|
||||||
|
ctx: Context = None
|
||||||
|
) -> str:
|
||||||
|
"""Update an existing webhook in Ghost.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
webhook_id: ID of the webhook to update (required)
|
||||||
|
event: New event to trigger the webhook (optional)
|
||||||
|
target_url: New URL to send the webhook to (optional)
|
||||||
|
name: New name of the webhook (optional)
|
||||||
|
api_version: New API version for the webhook (optional)
|
||||||
|
ctx: Optional context for logging
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
String representation of the updated webhook
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
GhostError: If the Ghost API request fails
|
||||||
|
ValueError: If no fields to update are provided or if the event is invalid
|
||||||
|
"""
|
||||||
|
# List of valid webhook events from Ghost documentation
|
||||||
|
valid_events = [
|
||||||
|
'site.changed',
|
||||||
|
'post.added',
|
||||||
|
'post.deleted',
|
||||||
|
'post.edited',
|
||||||
|
'post.published',
|
||||||
|
'post.published.edited',
|
||||||
|
'post.unpublished',
|
||||||
|
'post.scheduled',
|
||||||
|
'post.unscheduled',
|
||||||
|
'post.rescheduled',
|
||||||
|
'page.added',
|
||||||
|
'page.deleted',
|
||||||
|
'page.edited',
|
||||||
|
'page.published',
|
||||||
|
'page.published.edited',
|
||||||
|
'page.unpublished',
|
||||||
|
'page.scheduled',
|
||||||
|
'page.unscheduled',
|
||||||
|
'page.rescheduled',
|
||||||
|
'tag.added',
|
||||||
|
'tag.edited',
|
||||||
|
'tag.deleted',
|
||||||
|
'post.tag.attached',
|
||||||
|
'post.tag.detached',
|
||||||
|
'page.tag.attached',
|
||||||
|
'page.tag.detached',
|
||||||
|
'member.added',
|
||||||
|
'member.edited',
|
||||||
|
'member.deleted'
|
||||||
|
]
|
||||||
|
|
||||||
|
if not any([event, target_url, name, api_version]):
|
||||||
|
raise ValueError("At least one field must be provided to update")
|
||||||
|
|
||||||
|
if event and event not in valid_events:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid event. Must be one of: {', '.join(valid_events)}\n"
|
||||||
|
"See Ghost documentation for event descriptions."
|
||||||
|
)
|
||||||
|
|
||||||
|
if target_url:
|
||||||
|
# Ensure target_url has a trailing slash and is a valid URL
|
||||||
|
if not target_url.endswith('/'):
|
||||||
|
target_url = f"{target_url}/"
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Validate URL format
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
parsed = urlparse(target_url)
|
||||||
|
if not all([parsed.scheme, parsed.netloc]):
|
||||||
|
raise ValueError
|
||||||
|
except ValueError:
|
||||||
|
raise ValueError(
|
||||||
|
"target_url must be a valid URL in the format 'https://example.com/hook/'"
|
||||||
|
)
|
||||||
|
|
||||||
|
if ctx:
|
||||||
|
ctx.info(f"Updating webhook with ID: {webhook_id}")
|
||||||
|
|
||||||
|
# Construct webhook data
|
||||||
|
webhook_data = {
|
||||||
|
"webhooks": [{}]
|
||||||
|
}
|
||||||
|
webhook = webhook_data["webhooks"][0]
|
||||||
|
|
||||||
|
# Add fields to update
|
||||||
|
if event:
|
||||||
|
webhook["event"] = event
|
||||||
|
if target_url:
|
||||||
|
webhook["target_url"] = target_url
|
||||||
|
if name:
|
||||||
|
webhook["name"] = name
|
||||||
|
if api_version:
|
||||||
|
webhook["api_version"] = api_version
|
||||||
|
|
||||||
|
try:
|
||||||
|
if ctx:
|
||||||
|
ctx.debug("Getting auth headers")
|
||||||
|
headers = await get_auth_headers(STAFF_API_KEY)
|
||||||
|
|
||||||
|
if ctx:
|
||||||
|
ctx.debug(f"Making API request to update webhook {webhook_id}")
|
||||||
|
response = await make_ghost_request(
|
||||||
|
f"webhooks/{webhook_id}/",
|
||||||
|
headers,
|
||||||
|
ctx,
|
||||||
|
http_method="PUT",
|
||||||
|
json_data=webhook_data
|
||||||
|
)
|
||||||
|
|
||||||
|
if ctx:
|
||||||
|
ctx.debug("Processing updated webhook response")
|
||||||
|
|
||||||
|
webhook = response.get("webhooks", [{}])[0]
|
||||||
|
|
||||||
|
return f"""
|
||||||
|
Webhook updated successfully:
|
||||||
|
ID: {webhook.get('id')}
|
||||||
|
Event: {webhook.get('event')}
|
||||||
|
Target URL: {webhook.get('target_url')}
|
||||||
|
Name: {webhook.get('name', 'None')}
|
||||||
|
API Version: {webhook.get('api_version', 'v5')}
|
||||||
|
Status: {webhook.get('status', 'available')}
|
||||||
|
Integration ID: {webhook.get('integration_id', 'None')}
|
||||||
|
Created: {webhook.get('created_at', 'Unknown')}
|
||||||
|
Updated: {webhook.get('updated_at', 'Unknown')}
|
||||||
|
Last Triggered: {webhook.get('last_triggered_at', 'Never')}
|
||||||
|
Last Status: {webhook.get('last_triggered_status', 'N/A')}
|
||||||
|
Last Error: {webhook.get('last_triggered_error', 'None')}
|
||||||
|
"""
|
||||||
|
except Exception as e:
|
||||||
|
if ctx:
|
||||||
|
ctx.error(f"Failed to update webhook: {str(e)}")
|
||||||
|
raise
|
||||||
Reference in New Issue
Block a user