From 457245c6ed49587cbe86fdc6e0a051ec2a45eb4c Mon Sep 17 00:00:00 2001 From: Fanyang Meng Date: Wed, 12 Mar 2025 10:28:25 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20Simplify=20Tools=20Structure?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +- src/ghost_mcp/server.py | 20 ++++---- src/ghost_mcp/tools/__init__.py | 25 +++++---- src/ghost_mcp/tools/ghost.py | 91 +++++++++++++++++++++++++++++++++ 4 files changed, 118 insertions(+), 21 deletions(-) create mode 100644 src/ghost_mcp/tools/ghost.py diff --git a/.gitignore b/.gitignore index 3f5737a..e09000b 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ wheels/ .venv .vscode ghost-admin-api.md -mcp-python-sdk.md \ No newline at end of file +mcp-python-sdk.md +.qodo diff --git a/src/ghost_mcp/server.py b/src/ghost_mcp/server.py index 0dc1fe5..a5c55bc 100644 --- a/src/ghost_mcp/server.py +++ b/src/ghost_mcp/server.py @@ -26,12 +26,9 @@ def register_resources(mcp: FastMCP) -> None: mcp.resource(uri_template)(handler) def register_tools(mcp: FastMCP) -> None: - """Register all available tools.""" - # Get all tool functions from __all__ - for tool_name in tools.__all__: - tool_func = getattr(tools, tool_name) - if inspect.isfunction(tool_func): - mcp.tool()(tool_func) + """Register only the main ghost tool (which provides access to all functionality).""" + # Register only the main ghost tool + mcp.tool()(tools.ghost) def register_prompts(mcp: FastMCP) -> None: """Register all prompt templates.""" @@ -39,9 +36,9 @@ def register_prompts(mcp: FastMCP) -> None: def search_blog() -> str: """Prompt template for searching blog posts""" return """I want to help you search the blog posts. You can: -1. Search by title with: search_posts_by_title("your search term") -2. List all posts with: list_posts() -3. Read a specific post with: read_post("post_id") +1. Search by title with: ghost(action="search_posts_by_title", params={"query": "your search term"}) +2. List all posts with: ghost(action="list_posts") +3. Read a specific post with: ghost(action="read_post", params={"post_id": "post_id"}) What would you like to search for?""" @@ -52,7 +49,10 @@ What would you like to search for?""" Resource: post://{post_id} -Key points to include: +Alternatively, you can also get the post content with: +ghost(action="read_post", params={{"post_id": "{post_id}"}}) + +Key points to include in your summary: 1. Main topic/theme 2. Key arguments or insights 3. Important conclusions diff --git a/src/ghost_mcp/tools/__init__.py b/src/ghost_mcp/tools/__init__.py index d0488c3..a120f44 100644 --- a/src/ghost_mcp/tools/__init__.py +++ b/src/ghost_mcp/tools/__init__.py @@ -16,29 +16,31 @@ from .tags import browse_tags, read_tag, create_tag, update_tag, delete_tag from .tiers import list_tiers, read_tier, create_tier, update_tier from .users import list_users, read_user, delete_user from .webhooks import create_webhook, update_webhook, delete_webhook +from .ghost import ghost -__all__ = [ +# Hidden tools - these are accessible through the ghost meta-tool but not exposed directly +_all_tools = [ # Invites "create_invite", - + # Members "list_members", "read_member", "create_member", "update_member", - + # Newsletters "list_newsletters", "read_newsletter", "create_newsletter", "update_newsletter", - + # Offers "list_offers", "read_offer", "create_offer", "update_offer", - + # Posts "list_posts", "search_posts_by_title", @@ -47,30 +49,33 @@ __all__ = [ "update_post", "delete_post", "batchly_update_posts", - + # Roles "list_roles", - + # Tags "browse_tags", "read_tag", "create_tag", "update_tag", "delete_tag", - + # Tiers "list_tiers", "read_tier", "create_tier", "update_tier", - + # Users "list_users", "read_user", "delete_user", - + # Webhooks "create_webhook", "update_webhook", "delete_webhook", ] + +# Only expose the ghost meta-tool publicly +__all__ = ["ghost"] diff --git a/src/ghost_mcp/tools/ghost.py b/src/ghost_mcp/tools/ghost.py new file mode 100644 index 0000000..c8518cc --- /dev/null +++ b/src/ghost_mcp/tools/ghost.py @@ -0,0 +1,91 @@ +"""Main Ghost meta-tool that provides access to all Ghost functionality.""" + +import inspect +from mcp.server.fastmcp import Context +from typing import Any, Dict, Optional, List + +from .. import tools +from ..exceptions import GhostError + +async def ghost( + action: str, + params: Optional[Dict[str, Any]] = None, + ctx: Optional[Context] = None +) -> str: + """Central Ghost tool that provides access to all Ghost CMS functionality. + + Args: + action: The specific Ghost action to perform. + Available actions: + - Posts: list_posts, search_posts_by_title, read_post, create_post, update_post, delete_post, batchly_update_posts + - Users: list_users, read_user, delete_user, list_roles + - Members: list_members, read_member, create_member, update_member + - Tags: browse_tags, read_tag, create_tag, update_tag, delete_tag + - Tiers: list_tiers, read_tier, create_tier, update_tier + - Offers: list_offers, read_offer, create_offer, update_offer + - Newsletters: list_newsletters, read_newsletter, create_newsletter, update_newsletter + - Webhooks: create_webhook, update_webhook, delete_webhook + - Invites: create_invite + + params: Dictionary of parameters specific to the chosen action. + Required parameters vary by action. + ctx: Optional context for logging + + Returns: + Response from the specified Ghost action + + Raises: + GhostError: If there is an error processing the request + """ + if ctx: + ctx.info(f"Ghost tool called with action: {action}, params: {params}") + + # Validate action + if action not in tools._all_tools: + valid_actions = ", ".join(tools._all_tools) + return f"Invalid action '{action}'. Valid actions are: {valid_actions}" + + # Get the function for the specified action + tool_func = getattr(tools, action) + if not inspect.isfunction(tool_func): + return f"Invalid action '{action}'. This is not a valid function." + + # Prepare parameters for the function call + if params is None: + params = {} + + # Add context to params if the function expects it + sig = inspect.signature(tool_func) + call_params = params.copy() + if 'ctx' in sig.parameters: + call_params['ctx'] = ctx + + try: + # Call the function with the appropriate parameters + result = await tool_func(**call_params) + return result + except GhostError as e: + if ctx: + ctx.error(f"Ghost tool error for action '{action}': {str(e)}") + return f"Error executing '{action}': {str(e)}" + except TypeError as e: + # This usually happens when the wrong parameters are provided + if ctx: + ctx.error(f"Parameter error for action '{action}': {str(e)}") + + # Get the function parameters to provide better error messages + params_info = [] + for name, param in sig.parameters.items(): + if name == 'ctx': + continue + + param_type = param.annotation.__name__ if param.annotation != inspect.Parameter.empty else "any" + default = f"(default: {param.default})" if param.default != inspect.Parameter.empty else "(required)" + params_info.append(f"- {name}: {param_type} {default}") + + params_help = "\n".join(params_info) + return f"Error: {str(e)}\n\nExpected parameters for '{action}':\n{params_help}" + except Exception as e: + if ctx: + ctx.error(f"Unexpected error for action '{action}': {str(e)}") + return f"Unexpected error executing '{action}': {str(e)}"