mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-27 04:20:09 +00:00
241 lines
8.5 KiB
Python
241 lines
8.5 KiB
Python
"""Enhanced base class with automatic pagination support.
|
|
|
|
This module provides a base class that automatically handles pagination
|
|
for all tool responses that exceed MCP token limits.
|
|
"""
|
|
|
|
from abc import abstractmethod
|
|
from typing import Any, Dict, Union
|
|
|
|
from mcp.server.fastmcp import Context as MCPContext
|
|
|
|
from hanzo_mcp.tools.common.auto_timeout import auto_timeout
|
|
from hanzo_mcp.tools.common.base import BaseTool, handle_connection_errors
|
|
from hanzo_mcp.tools.common.paginated_response import paginate_if_needed
|
|
from hanzo_mcp.tools.common.pagination import CursorManager
|
|
|
|
|
|
class PaginatedBaseTool(BaseTool):
|
|
"""Base class for tools with automatic pagination support.
|
|
|
|
This base class automatically handles pagination for responses that
|
|
exceed MCP token limits, making all tools pagination-aware by default.
|
|
"""
|
|
|
|
def __init__(self):
|
|
"""Initialize the paginated base tool."""
|
|
super().__init__()
|
|
self._supports_pagination = True
|
|
|
|
@abstractmethod
|
|
async def execute(self, ctx: MCPContext, **params: Any) -> Any:
|
|
"""Execute the tool logic and return raw results.
|
|
|
|
This method should be implemented by subclasses to perform the
|
|
actual tool logic. The base class will handle pagination of
|
|
the returned results automatically.
|
|
|
|
Args:
|
|
ctx: MCP context
|
|
**params: Tool parameters
|
|
|
|
Returns:
|
|
Raw tool results (will be paginated if needed)
|
|
"""
|
|
pass
|
|
|
|
@handle_connection_errors
|
|
@auto_timeout("paginated_base")
|
|
async def call(self, ctx: MCPContext, **params: Any) -> Union[str, Dict[str, Any]]:
|
|
"""Execute the tool with automatic pagination support.
|
|
|
|
This method wraps the execute() method and automatically handles
|
|
pagination if the response exceeds token limits.
|
|
|
|
Args:
|
|
ctx: MCP context
|
|
**params: Tool parameters including optional 'cursor'
|
|
|
|
Returns:
|
|
Tool result, potentially paginated
|
|
"""
|
|
# Extract cursor if provided
|
|
cursor = params.pop("cursor", None)
|
|
|
|
# Validate cursor if provided
|
|
if cursor and not CursorManager.parse_cursor(cursor):
|
|
return {"error": "Invalid cursor provided", "code": -32602}
|
|
|
|
# Check if this is a continuation request
|
|
if cursor:
|
|
# For continuation, check if we have cached results
|
|
cursor_data = CursorManager.parse_cursor(cursor)
|
|
if (
|
|
cursor_data
|
|
and "tool" in cursor_data
|
|
and cursor_data["tool"] != self.name
|
|
):
|
|
return {"error": "Cursor is for a different tool", "code": -32602}
|
|
|
|
# Execute the tool
|
|
try:
|
|
result = await self.execute(ctx, **params)
|
|
except Exception as e:
|
|
# Format errors consistently
|
|
return {"error": str(e), "type": type(e).__name__}
|
|
|
|
# Handle pagination automatically
|
|
if self._supports_pagination:
|
|
paginated_result = paginate_if_needed(result, cursor)
|
|
|
|
# If pagination occurred, add tool info to help with continuation
|
|
if isinstance(paginated_result, dict) and "nextCursor" in paginated_result:
|
|
# Enhance the cursor with tool information
|
|
if "nextCursor" in paginated_result:
|
|
cursor_data = CursorManager.parse_cursor(
|
|
paginated_result["nextCursor"]
|
|
)
|
|
if cursor_data:
|
|
cursor_data["tool"] = self.name
|
|
cursor_data["params"] = params # Store params for continuation
|
|
paginated_result["nextCursor"] = CursorManager.create_cursor(
|
|
cursor_data
|
|
)
|
|
|
|
return paginated_result
|
|
else:
|
|
# Return raw result if pagination is disabled
|
|
return result
|
|
|
|
def disable_pagination(self):
|
|
"""Disable automatic pagination for this tool.
|
|
|
|
Some tools may want to handle their own pagination logic.
|
|
"""
|
|
self._supports_pagination = False
|
|
|
|
def enable_pagination(self):
|
|
"""Re-enable automatic pagination for this tool."""
|
|
self._supports_pagination = True
|
|
|
|
|
|
class PaginatedFileSystemTool(PaginatedBaseTool):
|
|
"""Base class for filesystem tools with pagination support."""
|
|
|
|
def __init__(self, permission_manager):
|
|
"""Initialize filesystem tool with pagination.
|
|
|
|
Args:
|
|
permission_manager: Permission manager for access control
|
|
"""
|
|
super().__init__()
|
|
self.permission_manager = permission_manager
|
|
|
|
def is_path_allowed(self, path: str) -> bool:
|
|
"""Check if a path is allowed according to permission settings.
|
|
|
|
Args:
|
|
path: Path to check
|
|
|
|
Returns:
|
|
True if the path is allowed, False otherwise
|
|
"""
|
|
return self.permission_manager.is_path_allowed(path)
|
|
|
|
|
|
def migrate_tool_to_paginated(tool_class):
|
|
"""Decorator to migrate existing tools to use pagination.
|
|
|
|
This decorator can be applied to existing tool classes to add
|
|
automatic pagination support without modifying their code.
|
|
|
|
Usage:
|
|
@migrate_tool_to_paginated
|
|
class MyTool(BaseTool):
|
|
...
|
|
"""
|
|
|
|
class PaginatedWrapper(PaginatedBaseTool):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__()
|
|
self._wrapped_tool = tool_class(*args, **kwargs)
|
|
|
|
@property
|
|
def name(self):
|
|
return self._wrapped_tool.name
|
|
|
|
@property
|
|
def description(self):
|
|
# Add pagination info to description
|
|
desc = self._wrapped_tool.description
|
|
if "pagination" not in desc.lower():
|
|
desc += "\n\nThis tool supports automatic pagination. If the response is too large, it will be split across multiple requests. Use the returned cursor to continue."
|
|
return desc
|
|
|
|
async def execute(self, ctx: MCPContext, **params: Any) -> Any:
|
|
# Call the wrapped tool's call method
|
|
return await self._wrapped_tool.call(ctx, **params)
|
|
|
|
def register(self, mcp_server):
|
|
# Need to create a new registration that includes cursor parameter
|
|
tool_self = self
|
|
|
|
# Get the original registration function
|
|
original_register = self._wrapped_tool.register
|
|
|
|
# Create a new registration that adds cursor support
|
|
def register_with_pagination(server):
|
|
# First register the original tool
|
|
original_register(server)
|
|
|
|
# Then override with pagination support
|
|
import inspect
|
|
|
|
# Get the registered function
|
|
tool_func = None
|
|
for name, func in server._tools.items():
|
|
if name == self.name:
|
|
tool_func = func
|
|
break
|
|
|
|
if tool_func:
|
|
# Get original signature
|
|
sig = inspect.signature(tool_func)
|
|
params = list(sig.parameters.values())
|
|
|
|
# Add cursor parameter if not present
|
|
has_cursor = any(p.name == "cursor" for p in params)
|
|
if not has_cursor:
|
|
import inspect
|
|
from typing import Optional
|
|
|
|
# Create new parameter with cursor
|
|
cursor_param = inspect.Parameter(
|
|
"cursor",
|
|
inspect.Parameter.KEYWORD_ONLY,
|
|
default=None,
|
|
annotation=Optional[str],
|
|
)
|
|
|
|
# Insert before ctx parameter
|
|
new_params = []
|
|
for p in params:
|
|
if p.name == "ctx":
|
|
new_params.append(cursor_param)
|
|
new_params.append(p)
|
|
|
|
# Create wrapper function
|
|
async def paginated_wrapper(**kwargs):
|
|
return await tool_self.call(kwargs.get("ctx"), **kwargs)
|
|
|
|
# Update registration
|
|
server._tools[self.name] = paginated_wrapper
|
|
|
|
register_with_pagination(mcp_server)
|
|
|
|
# Set the class name
|
|
PaginatedWrapper.__name__ = f"Paginated{tool_class.__name__}"
|
|
PaginatedWrapper.__qualname__ = f"Paginated{tool_class.__qualname__}"
|
|
|
|
return PaginatedWrapper
|