From 146c3ffcfd2ed0aa91df4f38f2c439bc889f037a Mon Sep 17 00:00:00 2001 From: Fanyang Meng Date: Tue, 11 Feb 2025 22:26:51 -0500 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Support=20tier=20creating=20and=20u?= =?UTF-8?q?pdating?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 2 + src/ghost_mcp/server.py | 2 + src/ghost_mcp/tools.py | 6 +- src/ghost_mcp/tools/__init__.py | 4 +- src/ghost_mcp/tools/tiers.py | 203 ++++++++++++++++++++++++++++++++ 5 files changed, 215 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c455e84..90b71db 100644 --- a/README.md +++ b/README.md @@ -90,6 +90,8 @@ GHOST_API_URL=your_ghost_api_url GHOST_STAFF_API_KEY=your_staff_api_key npx @mod ### Tiers Management - `list_tiers`: List all available membership tiers - `read_tier`: Retrieve detailed information about a specific tier, including benefits and pricing +- `create_tier`: Create a new membership tier with specified details +- `update_tier`: Update an existing tier with new information ### Offers Management - `list_offers`: List promotional offers with relevant details diff --git a/src/ghost_mcp/server.py b/src/ghost_mcp/server.py index 93a0617..8c2ba34 100644 --- a/src/ghost_mcp/server.py +++ b/src/ghost_mcp/server.py @@ -53,6 +53,8 @@ def create_server() -> FastMCP: mcp.tool()(tools.read_member) mcp.tool()(tools.list_tiers) mcp.tool()(tools.read_tier) + mcp.tool()(tools.create_tier) + mcp.tool()(tools.update_tier) mcp.tool()(tools.list_offers) mcp.tool()(tools.read_offer) mcp.tool()(tools.list_newsletters) diff --git a/src/ghost_mcp/tools.py b/src/ghost_mcp/tools.py index 0b3e144..dde152c 100644 --- a/src/ghost_mcp/tools.py +++ b/src/ghost_mcp/tools.py @@ -19,7 +19,9 @@ from .tools.members import ( ) from .tools.tiers import ( list_tiers, - read_tier + read_tier, + create_tier, + update_tier ) from .tools.offers import ( list_offers, @@ -43,6 +45,8 @@ __all__ = [ 'read_member', 'list_tiers', 'read_tier', + 'create_tier', + 'update_tier', 'list_offers', 'read_offer', 'list_newsletters', diff --git a/src/ghost_mcp/tools/__init__.py b/src/ghost_mcp/tools/__init__.py index 210b445..7154901 100644 --- a/src/ghost_mcp/tools/__init__.py +++ b/src/ghost_mcp/tools/__init__.py @@ -3,7 +3,7 @@ from .posts import search_posts_by_title, list_posts, read_post, create_post, update_post, delete_post from .users import list_users, read_user from .members import list_members, read_member -from .tiers import list_tiers, read_tier +from .tiers import list_tiers, read_tier, create_tier, update_tier from .offers import list_offers, read_offer from .newsletters import list_newsletters, read_newsletter @@ -20,6 +20,8 @@ __all__ = [ 'read_member', 'list_tiers', 'read_tier', + 'create_tier', + 'update_tier', 'list_offers', 'read_offer', 'list_newsletters', diff --git a/src/ghost_mcp/tools/tiers.py b/src/ghost_mcp/tools/tiers.py index 500c192..41922d5 100644 --- a/src/ghost_mcp/tools/tiers.py +++ b/src/ghost_mcp/tools/tiers.py @@ -1,6 +1,11 @@ """Tier-related MCP tools for Ghost API.""" import json +from typing import Optional, List +from mcp.server.fastmcp import Context + +from ..api import make_ghost_request, get_auth_headers +from ..config import STAFF_API_KEY from mcp.server.fastmcp import Context from ..api import make_ghost_request, get_auth_headers @@ -125,3 +130,201 @@ Benefits: if ctx: ctx.error(f"Failed to read tier: {str(e)}") return str(e) + +async def create_tier( + name: str, + monthly_price: Optional[int] = None, + yearly_price: Optional[int] = None, + description: Optional[str] = None, + benefits: Optional[List[str]] = None, + welcome_page_url: Optional[str] = None, + visibility: str = "public", + currency: str = "usd", + ctx: Context = None +) -> str: + """Create a new tier in Ghost. + + Args: + name: Name of the tier (required) + monthly_price: Optional monthly price in cents (e.g. 500 for $5.00) + yearly_price: Optional yearly price in cents (e.g. 5000 for $50.00) + description: Optional description of the tier + benefits: Optional list of benefits for the tier + welcome_page_url: Optional URL for the welcome page + visibility: Visibility of tier, either "public" or "none" (default: "public") + currency: Currency for prices (default: "usd") + ctx: Optional context for logging + + Returns: + String representation of the created tier + + Raises: + GhostError: If the Ghost API request fails + """ + if not name: + raise ValueError("Name is required for creating a tier") + + if ctx: + ctx.info(f"Creating new tier: {name}") + + # Construct tier data + tier_data = { + "tiers": [{ + "name": name, + "description": description, + "type": "paid" if (monthly_price or yearly_price) else "free", + "active": True, + "visibility": visibility, + "welcome_page_url": welcome_page_url, + "benefits": benefits or [], + "currency": currency + }] + } + + # Add pricing if provided + if monthly_price is not None: + tier_data["tiers"][0]["monthly_price"] = monthly_price + if yearly_price is not None: + tier_data["tiers"][0]["yearly_price"] = yearly_price + + 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 tier") + response = await make_ghost_request( + "tiers/", + headers, + ctx, + http_method="POST", + json_data=tier_data + ) + + if ctx: + ctx.debug("Processing created tier response") + + tier = response.get("tiers", [{}])[0] + + # Format response + benefits_text = "\n- ".join(tier.get('benefits', [])) if tier.get('benefits') else "None" + return f""" +Tier created successfully: +Name: {tier.get('name')} +Type: {tier.get('type')} +Description: {tier.get('description', 'No description')} +Active: {tier.get('active', False)} +Visibility: {tier.get('visibility', 'public')} +Monthly Price: {tier.get('monthly_price', 'N/A')} {tier.get('currency', 'usd').upper()} +Yearly Price: {tier.get('yearly_price', 'N/A')} {tier.get('currency', 'usd').upper()} +Currency: {tier.get('currency', 'usd').upper()} +Benefits: +- {benefits_text} +ID: {tier.get('id', 'Unknown')} +""" + except Exception as e: + if ctx: + ctx.error(f"Failed to create tier: {str(e)}") + raise + +async def update_tier( + tier_id: str, + name: Optional[str] = None, + description: Optional[str] = None, + monthly_price: Optional[int] = None, + yearly_price: Optional[int] = None, + benefits: Optional[List[str]] = None, + welcome_page_url: Optional[str] = None, + visibility: Optional[str] = None, + currency: Optional[str] = None, + active: Optional[bool] = None, + ctx: Context = None +) -> str: + """Update an existing tier in Ghost. + + Args: + tier_id: ID of the tier to update (required) + name: New name for the tier + description: New description for the tier + monthly_price: New monthly price in cents (e.g. 500 for $5.00) + yearly_price: New yearly price in cents (e.g. 5000 for $50.00) + benefits: New list of benefits for the tier + welcome_page_url: New URL for the welcome page + visibility: New visibility setting ("public" or "none") + currency: New currency for prices + active: New active status + ctx: Optional context for logging + + Returns: + String representation of the updated tier + + Raises: + GhostError: If the Ghost API request fails + """ + if ctx: + ctx.info(f"Updating tier with ID: {tier_id}") + + # Construct update data with only provided fields + update_data = {"tiers": [{}]} + tier_updates = update_data["tiers"][0] + + if name is not None: + tier_updates["name"] = name + if description is not None: + tier_updates["description"] = description + if monthly_price is not None: + tier_updates["monthly_price"] = monthly_price + if yearly_price is not None: + tier_updates["yearly_price"] = yearly_price + if benefits is not None: + tier_updates["benefits"] = benefits + if welcome_page_url is not None: + tier_updates["welcome_page_url"] = welcome_page_url + if visibility is not None: + tier_updates["visibility"] = visibility + if currency is not None: + tier_updates["currency"] = currency + if active is not None: + tier_updates["active"] = active + + 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 tier {tier_id}") + response = await make_ghost_request( + f"tiers/{tier_id}/", + headers, + ctx, + http_method="PUT", + json_data=update_data + ) + + if ctx: + ctx.debug("Processing updated tier response") + + tier = response.get("tiers", [{}])[0] + + # Format response + benefits_text = "\n- ".join(tier.get('benefits', [])) if tier.get('benefits') else "None" + return f""" +Tier updated successfully: +Name: {tier.get('name')} +Type: {tier.get('type')} +Description: {tier.get('description', 'No description')} +Active: {tier.get('active', False)} +Visibility: {tier.get('visibility', 'public')} +Monthly Price: {tier.get('monthly_price', 'N/A')} {tier.get('currency', 'usd').upper()} +Yearly Price: {tier.get('yearly_price', 'N/A')} {tier.get('currency', 'usd').upper()} +Currency: {tier.get('currency', 'usd').upper()} +Benefits: +- {benefits_text} +ID: {tier.get('id')} +""" + except Exception as e: + if ctx: + ctx.error(f"Failed to update tier: {str(e)}") + raise \ No newline at end of file