From 230a51d15a4bac033ad8942f0fbf0d0d105de206 Mon Sep 17 00:00:00 2001 From: Fanyang Meng Date: Tue, 11 Feb 2025 23:54:23 -0500 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Support=20update=20users?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + src/ghost_mcp/api.py | 2 +- src/ghost_mcp/server.py | 1 + src/ghost_mcp/tools.py | 4 +- src/ghost_mcp/tools/__init__.py | 3 +- src/ghost_mcp/tools/users.py | 143 ++++++++++++++++++++++++++++++++ 6 files changed, 151 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7277a51..8e22700 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ GHOST_API_URL=your_ghost_api_url GHOST_STAFF_API_KEY=your_staff_api_key npx @mod ### Users Management - `list_roles`: List all available roles - `create_invite`: Create a new user invitation email and role_id +- `update_user`: Update an existing user's information **(Please note: Ghost has not implemented the ability to update user roles via the API even though they include it in the API documentation)** - `list_users`: List all users with detailed role information - `read_user`: Get comprehensive details of a specific user diff --git a/src/ghost_mcp/api.py b/src/ghost_mcp/api.py index 194d213..c4bd5ad 100644 --- a/src/ghost_mcp/api.py +++ b/src/ghost_mcp/api.py @@ -56,7 +56,7 @@ async def get_auth_headers(staff_api_key: str) -> Dict[str, str]: token = await generate_token(staff_api_key) return { "Authorization": f"Ghost {token}", - "Accept-Version": "v5" + "Accept-Version": "v5.109" } async def make_ghost_request( diff --git a/src/ghost_mcp/server.py b/src/ghost_mcp/server.py index 399460d..d03701a 100644 --- a/src/ghost_mcp/server.py +++ b/src/ghost_mcp/server.py @@ -49,6 +49,7 @@ def create_server() -> FastMCP: mcp.tool()(tools.delete_post) mcp.tool()(tools.list_users) mcp.tool()(tools.read_user) + mcp.tool()(tools.update_user) mcp.tool()(tools.list_members) mcp.tool()(tools.read_member) mcp.tool()(tools.create_member) diff --git a/src/ghost_mcp/tools.py b/src/ghost_mcp/tools.py index 603ca32..3c356c6 100644 --- a/src/ghost_mcp/tools.py +++ b/src/ghost_mcp/tools.py @@ -11,7 +11,8 @@ from .tools.posts import ( ) from .tools.users import ( list_users, - read_user + read_user, + update_user ) from .tools.members import ( list_members, @@ -49,6 +50,7 @@ __all__ = [ 'delete_post', 'list_users', 'read_user', + 'update_user', 'list_members', 'read_member', 'create_member', diff --git a/src/ghost_mcp/tools/__init__.py b/src/ghost_mcp/tools/__init__.py index 432a146..716617d 100644 --- a/src/ghost_mcp/tools/__init__.py +++ b/src/ghost_mcp/tools/__init__.py @@ -1,7 +1,7 @@ """Ghost MCP tools package.""" 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 .users import list_users, read_user, update_user from .members import list_members, read_member, create_member, update_member from .tiers import list_tiers, read_tier, create_tier, update_tier from .offers import list_offers, read_offer, create_offer, update_offer @@ -18,6 +18,7 @@ __all__ = [ 'delete_post', 'list_users', 'read_user', + 'update_user', 'list_members', 'read_member', 'create_member', diff --git a/src/ghost_mcp/tools/users.py b/src/ghost_mcp/tools/users.py index acf53e6..25b2425 100644 --- a/src/ghost_mcp/tools/users.py +++ b/src/ghost_mcp/tools/users.py @@ -72,6 +72,149 @@ ID: {user.get('id', 'Unknown')} ctx.error(f"Failed to list users: {str(e)}") return str(e) +async def update_user( + user_id: str, + name: str = None, + slug: str = None, + email: str = None, + profile_image: str = None, + cover_image: str = None, + bio: str = None, + website: str = None, + location: str = None, + facebook: str = None, + twitter: str = None, + meta_title: str = None, + meta_description: str = None, + accessibility: str = None, + comment_notifications: bool = None, + free_member_signup_notification: bool = None, + paid_subscription_started_notification: bool = None, + paid_subscription_canceled_notification: bool = None, + mention_notifications: bool = None, + milestone_notifications: bool = None, + ctx: Context = None +) -> str: + """Update an existing user in Ghost. + + Args: + user_id: ID of the user to update (required) + name: User's full name (optional) + slug: User's slug (optional) + email: User's email address (optional) + profile_image: URL for profile image (optional) + cover_image: URL for cover image (optional) + bio: User's bio (optional) + website: User's website URL (optional) + location: User's location (optional) + facebook: Facebook username (optional) + twitter: Twitter username (optional) + meta_title: Meta title for SEO (optional) + meta_description: Meta description for SEO (optional) + accessibility: Accessibility settings (optional) + comment_notifications: Enable comment notifications (optional) + free_member_signup_notification: Enable free member signup notifications (optional) + paid_subscription_started_notification: Enable paid subscription started notifications (optional) + paid_subscription_canceled_notification: Enable paid subscription canceled notifications (optional) + mention_notifications: Enable mention notifications (optional) + milestone_notifications: Enable milestone notifications (optional) + ctx: Optional context for logging + + Returns: + String representation of the updated user + + Raises: + GhostError: If the Ghost API request fails + ValueError: If no fields to update are provided + """ + # Check if at least one field to update is provided + update_fields = { + 'name': name, + 'slug': slug, + 'email': email, + 'profile_image': profile_image, + 'cover_image': cover_image, + 'bio': bio, + 'website': website, + 'location': location, + 'facebook': facebook, + 'twitter': twitter, + 'meta_title': meta_title, + 'meta_description': meta_description, + 'accessibility': accessibility, + 'comment_notifications': comment_notifications, + 'free_member_signup_notification': free_member_signup_notification, + 'paid_subscription_started_notification': paid_subscription_started_notification, + 'paid_subscription_canceled_notification': paid_subscription_canceled_notification, + 'mention_notifications': mention_notifications, + 'milestone_notifications': milestone_notifications + } + + if not any(v is not None for v in update_fields.values()): + raise ValueError("At least one field must be provided to update") + + if ctx: + ctx.info(f"Updating user with ID: {user_id}") + + # Construct update data with only provided fields + update_data = {"users": [{}]} + user_updates = update_data["users"][0] + + for field, value in update_fields.items(): + if value is not None: + user_updates[field] = value + + 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 user {user_id}") + response = await make_ghost_request( + f"users/{user_id}/", + headers, + ctx, + http_method="PUT", + json_data=update_data + ) + + if ctx: + ctx.debug("Processing updated user response") + + user = response.get("users", [{}])[0] + roles = [role.get('name') for role in user.get('roles', [])] + + return f""" +User updated successfully: +Name: {user.get('name', 'Unknown')} +Email: {user.get('email', 'Unknown')} +Slug: {user.get('slug', 'Unknown')} +Status: {user.get('status', 'Unknown')} +Roles: {', '.join(roles)} +Location: {user.get('location', 'Not specified')} +Website: {user.get('website', 'None')} +Bio: {user.get('bio', 'No bio')} +Profile Image: {user.get('profile_image', 'None')} +Cover Image: {user.get('cover_image', 'None')} +Facebook: {user.get('facebook', 'None')} +Twitter: {user.get('twitter', 'None')} +Created: {user.get('created_at', 'Unknown')} +Updated: {user.get('updated_at', 'Unknown')} +Last Seen: {user.get('last_seen', 'Never')} +Notifications: +- Comments: {user.get('comment_notifications', False)} +- Free Member Signup: {user.get('free_member_signup_notification', False)} +- Paid Subscription Started: {user.get('paid_subscription_started_notification', False)} +- Paid Subscription Canceled: {user.get('paid_subscription_canceled_notification', False)} +- Mentions: {user.get('mention_notifications', False)} +- Milestones: {user.get('milestone_notifications', False)} +""" + except Exception as e: + if ctx: + ctx.error(f"Failed to update user: {str(e)}") + raise + async def read_user(user_id: str, ctx: Context = None) -> str: """Get the details of a specific user.