hanzo-mcp 0.6.12__py3-none-any.whl → 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of hanzo-mcp might be problematic. Click here for more details.
- hanzo_mcp/__init__.py +2 -2
- hanzo_mcp/analytics/__init__.py +5 -0
- hanzo_mcp/analytics/posthog_analytics.py +364 -0
- hanzo_mcp/cli.py +5 -5
- hanzo_mcp/cli_enhanced.py +7 -7
- hanzo_mcp/cli_plugin.py +91 -0
- hanzo_mcp/config/__init__.py +1 -1
- hanzo_mcp/config/settings.py +70 -7
- hanzo_mcp/config/tool_config.py +20 -6
- hanzo_mcp/dev_server.py +3 -3
- hanzo_mcp/prompts/project_system.py +1 -1
- hanzo_mcp/server.py +40 -3
- hanzo_mcp/server_enhanced.py +69 -0
- hanzo_mcp/tools/__init__.py +140 -31
- hanzo_mcp/tools/agent/__init__.py +85 -4
- hanzo_mcp/tools/agent/agent_tool.py +104 -6
- hanzo_mcp/tools/agent/agent_tool_v2.py +459 -0
- hanzo_mcp/tools/agent/clarification_protocol.py +220 -0
- hanzo_mcp/tools/agent/clarification_tool.py +68 -0
- hanzo_mcp/tools/agent/claude_cli_tool.py +125 -0
- hanzo_mcp/tools/agent/claude_desktop_auth.py +508 -0
- hanzo_mcp/tools/agent/cli_agent_base.py +191 -0
- hanzo_mcp/tools/agent/code_auth.py +436 -0
- hanzo_mcp/tools/agent/code_auth_tool.py +194 -0
- hanzo_mcp/tools/agent/codex_cli_tool.py +123 -0
- hanzo_mcp/tools/agent/critic_tool.py +376 -0
- hanzo_mcp/tools/agent/gemini_cli_tool.py +128 -0
- hanzo_mcp/tools/agent/grok_cli_tool.py +128 -0
- hanzo_mcp/tools/agent/iching_tool.py +380 -0
- hanzo_mcp/tools/agent/network_tool.py +273 -0
- hanzo_mcp/tools/agent/prompt.py +62 -20
- hanzo_mcp/tools/agent/review_tool.py +433 -0
- hanzo_mcp/tools/agent/swarm_tool.py +535 -0
- hanzo_mcp/tools/agent/swarm_tool_v2.py +594 -0
- hanzo_mcp/tools/common/__init__.py +15 -1
- hanzo_mcp/tools/common/base.py +5 -4
- hanzo_mcp/tools/common/batch_tool.py +103 -11
- hanzo_mcp/tools/common/config_tool.py +2 -2
- hanzo_mcp/tools/common/context.py +2 -2
- hanzo_mcp/tools/common/context_fix.py +26 -0
- hanzo_mcp/tools/common/critic_tool.py +196 -0
- hanzo_mcp/tools/common/decorators.py +208 -0
- hanzo_mcp/tools/common/enhanced_base.py +106 -0
- hanzo_mcp/tools/common/fastmcp_pagination.py +369 -0
- hanzo_mcp/tools/common/forgiving_edit.py +243 -0
- hanzo_mcp/tools/common/mode.py +116 -0
- hanzo_mcp/tools/common/mode_loader.py +105 -0
- hanzo_mcp/tools/common/paginated_base.py +230 -0
- hanzo_mcp/tools/common/paginated_response.py +307 -0
- hanzo_mcp/tools/common/pagination.py +226 -0
- hanzo_mcp/tools/common/permissions.py +1 -1
- hanzo_mcp/tools/common/personality.py +936 -0
- hanzo_mcp/tools/common/plugin_loader.py +287 -0
- hanzo_mcp/tools/common/stats.py +4 -4
- hanzo_mcp/tools/common/tool_list.py +4 -1
- hanzo_mcp/tools/common/truncate.py +101 -0
- hanzo_mcp/tools/common/validation.py +1 -1
- hanzo_mcp/tools/config/__init__.py +3 -1
- hanzo_mcp/tools/config/config_tool.py +1 -1
- hanzo_mcp/tools/config/mode_tool.py +209 -0
- hanzo_mcp/tools/database/__init__.py +1 -1
- hanzo_mcp/tools/editor/__init__.py +1 -1
- hanzo_mcp/tools/filesystem/__init__.py +48 -14
- hanzo_mcp/tools/filesystem/ast_multi_edit.py +562 -0
- hanzo_mcp/tools/filesystem/batch_search.py +3 -3
- hanzo_mcp/tools/filesystem/diff.py +2 -2
- hanzo_mcp/tools/filesystem/directory_tree_paginated.py +338 -0
- hanzo_mcp/tools/filesystem/rules_tool.py +235 -0
- hanzo_mcp/tools/filesystem/{unified_search.py → search_tool.py} +12 -12
- hanzo_mcp/tools/filesystem/{symbols_unified.py → symbols_tool.py} +104 -5
- hanzo_mcp/tools/filesystem/watch.py +3 -2
- hanzo_mcp/tools/jupyter/__init__.py +2 -2
- hanzo_mcp/tools/jupyter/jupyter.py +1 -1
- hanzo_mcp/tools/llm/__init__.py +3 -3
- hanzo_mcp/tools/llm/llm_tool.py +648 -143
- hanzo_mcp/tools/lsp/__init__.py +5 -0
- hanzo_mcp/tools/lsp/lsp_tool.py +512 -0
- hanzo_mcp/tools/mcp/__init__.py +2 -2
- hanzo_mcp/tools/mcp/{mcp_unified.py → mcp_tool.py} +3 -3
- hanzo_mcp/tools/memory/__init__.py +76 -0
- hanzo_mcp/tools/memory/knowledge_tools.py +518 -0
- hanzo_mcp/tools/memory/memory_tools.py +456 -0
- hanzo_mcp/tools/search/__init__.py +6 -0
- hanzo_mcp/tools/search/find_tool.py +581 -0
- hanzo_mcp/tools/search/unified_search.py +953 -0
- hanzo_mcp/tools/shell/__init__.py +11 -6
- hanzo_mcp/tools/shell/auto_background.py +203 -0
- hanzo_mcp/tools/shell/base_process.py +57 -29
- hanzo_mcp/tools/shell/bash_session_executor.py +1 -1
- hanzo_mcp/tools/shell/{bash_unified.py → bash_tool.py} +18 -34
- hanzo_mcp/tools/shell/command_executor.py +2 -2
- hanzo_mcp/tools/shell/{npx_unified.py → npx_tool.py} +16 -33
- hanzo_mcp/tools/shell/open.py +2 -2
- hanzo_mcp/tools/shell/{process_unified.py → process_tool.py} +1 -1
- hanzo_mcp/tools/shell/run_command_windows.py +1 -1
- hanzo_mcp/tools/shell/streaming_command.py +594 -0
- hanzo_mcp/tools/shell/uvx.py +47 -2
- hanzo_mcp/tools/shell/uvx_background.py +47 -2
- hanzo_mcp/tools/shell/{uvx_unified.py → uvx_tool.py} +16 -33
- hanzo_mcp/tools/todo/__init__.py +14 -19
- hanzo_mcp/tools/todo/todo.py +22 -1
- hanzo_mcp/tools/vector/__init__.py +1 -1
- hanzo_mcp/tools/vector/infinity_store.py +2 -2
- hanzo_mcp/tools/vector/project_manager.py +1 -1
- hanzo_mcp/types.py +23 -0
- hanzo_mcp-0.7.0.dist-info/METADATA +516 -0
- hanzo_mcp-0.7.0.dist-info/RECORD +180 -0
- {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.7.0.dist-info}/entry_points.txt +1 -0
- hanzo_mcp/tools/common/palette.py +0 -344
- hanzo_mcp/tools/common/palette_loader.py +0 -108
- hanzo_mcp/tools/config/palette_tool.py +0 -179
- hanzo_mcp/tools/llm/llm_unified.py +0 -851
- hanzo_mcp-0.6.12.dist-info/METADATA +0 -339
- hanzo_mcp-0.6.12.dist-info/RECORD +0 -135
- hanzo_mcp-0.6.12.dist-info/licenses/LICENSE +0 -21
- {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.7.0.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.7.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Enhanced base classes for MCP tools with automatic context handling.
|
|
2
|
+
|
|
3
|
+
This module provides enhanced base classes that automatically handle
|
|
4
|
+
context normalization and other cross-cutting concerns, ensuring
|
|
5
|
+
consistent behavior across all tools.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from typing import Any, get_type_hints, get_args, get_origin
|
|
10
|
+
import inspect
|
|
11
|
+
|
|
12
|
+
from mcp.server import FastMCP
|
|
13
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
14
|
+
|
|
15
|
+
from hanzo_mcp.tools.common.base import BaseTool
|
|
16
|
+
from hanzo_mcp.tools.common.decorators import with_context_normalization
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class EnhancedBaseTool(BaseTool, ABC):
|
|
20
|
+
"""Enhanced base class for MCP tools with automatic context normalization.
|
|
21
|
+
|
|
22
|
+
This base class automatically wraps the tool registration to include
|
|
23
|
+
context normalization, ensuring that all tools handle external calls
|
|
24
|
+
properly without requiring manual decoration or copy-pasted code.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def register(self, mcp_server: FastMCP) -> None:
|
|
28
|
+
"""Register this tool with automatic context normalization.
|
|
29
|
+
|
|
30
|
+
This method automatically applies context normalization to the tool
|
|
31
|
+
handler, ensuring it works properly when called externally.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
mcp_server: The FastMCP server instance
|
|
35
|
+
"""
|
|
36
|
+
# Get the tool method from the subclass
|
|
37
|
+
tool_method = self._create_tool_handler()
|
|
38
|
+
|
|
39
|
+
# Apply context normalization decorator
|
|
40
|
+
normalized_method = with_context_normalization(tool_method)
|
|
41
|
+
|
|
42
|
+
# Register with the server
|
|
43
|
+
mcp_server.tool(
|
|
44
|
+
name=self.name,
|
|
45
|
+
description=self.description
|
|
46
|
+
)(normalized_method)
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def _create_tool_handler(self) -> Any:
|
|
50
|
+
"""Create the tool handler function.
|
|
51
|
+
|
|
52
|
+
Subclasses must implement this to return an async function
|
|
53
|
+
that will be registered as the tool handler.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
An async function that handles tool calls
|
|
57
|
+
"""
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class AutoRegisterTool(BaseTool, ABC):
|
|
62
|
+
"""Base class that automatically generates tool handlers from the call method.
|
|
63
|
+
|
|
64
|
+
This base class inspects the call method signature and automatically
|
|
65
|
+
creates a properly typed tool handler with context normalization.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def register(self, mcp_server: FastMCP) -> None:
|
|
69
|
+
"""Register this tool with automatic handler generation.
|
|
70
|
+
|
|
71
|
+
This method inspects the call method signature and automatically
|
|
72
|
+
creates a tool handler with the correct parameters and types.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
mcp_server: The FastMCP server instance
|
|
76
|
+
"""
|
|
77
|
+
# Get the call method signature
|
|
78
|
+
call_method = self.call
|
|
79
|
+
sig = inspect.signature(call_method)
|
|
80
|
+
|
|
81
|
+
# Get type hints for proper typing
|
|
82
|
+
hints = get_type_hints(call_method)
|
|
83
|
+
|
|
84
|
+
# Create a dynamic handler function
|
|
85
|
+
tool_self = self
|
|
86
|
+
|
|
87
|
+
# Build the handler dynamically based on the call signature
|
|
88
|
+
params = list(sig.parameters.items())
|
|
89
|
+
|
|
90
|
+
# Skip 'self' and 'ctx' parameters
|
|
91
|
+
tool_params = [(name, param) for name, param in params
|
|
92
|
+
if name not in ('self', 'ctx')]
|
|
93
|
+
|
|
94
|
+
# Create the handler function dynamically
|
|
95
|
+
async def handler(ctx: MCPContext, **kwargs: Any) -> Any:
|
|
96
|
+
# Call the tool's call method with the context and parameters
|
|
97
|
+
return await tool_self.call(ctx, **kwargs)
|
|
98
|
+
|
|
99
|
+
# Apply context normalization
|
|
100
|
+
normalized_handler = with_context_normalization(handler)
|
|
101
|
+
|
|
102
|
+
# Register with the server
|
|
103
|
+
mcp_server.tool(
|
|
104
|
+
name=self.name,
|
|
105
|
+
description=self.description
|
|
106
|
+
)(normalized_handler)
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"""FastMCP-compatible pagination implementation.
|
|
2
|
+
|
|
3
|
+
This module provides pagination utilities optimized for FastMCP with minimal latency.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import base64
|
|
7
|
+
import json
|
|
8
|
+
import time
|
|
9
|
+
from typing import Any, Dict, List, Optional, Tuple, TypeVar, Generic, Union
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
import hashlib
|
|
13
|
+
|
|
14
|
+
T = TypeVar('T')
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class CursorData:
|
|
19
|
+
"""Cursor data structure for efficient pagination."""
|
|
20
|
+
# Primary cursor fields (indexed)
|
|
21
|
+
last_id: Optional[str] = None
|
|
22
|
+
last_timestamp: Optional[float] = None
|
|
23
|
+
offset: int = 0
|
|
24
|
+
|
|
25
|
+
# Metadata for validation and optimization
|
|
26
|
+
page_size: int = 100
|
|
27
|
+
sort_field: str = "id"
|
|
28
|
+
sort_order: str = "asc"
|
|
29
|
+
|
|
30
|
+
# Security and validation
|
|
31
|
+
created_at: float = field(default_factory=time.time)
|
|
32
|
+
expires_at: Optional[float] = None
|
|
33
|
+
checksum: Optional[str] = None
|
|
34
|
+
|
|
35
|
+
def to_cursor(self) -> str:
|
|
36
|
+
"""Convert to opaque cursor string."""
|
|
37
|
+
data = {
|
|
38
|
+
"id": self.last_id,
|
|
39
|
+
"ts": self.last_timestamp,
|
|
40
|
+
"o": self.offset,
|
|
41
|
+
"ps": self.page_size,
|
|
42
|
+
"sf": self.sort_field,
|
|
43
|
+
"so": self.sort_order,
|
|
44
|
+
"ca": self.created_at,
|
|
45
|
+
}
|
|
46
|
+
if self.expires_at:
|
|
47
|
+
data["ea"] = self.expires_at
|
|
48
|
+
|
|
49
|
+
# Add checksum for integrity
|
|
50
|
+
data_str = json.dumps(data, sort_keys=True, separators=(',', ':'))
|
|
51
|
+
data["cs"] = hashlib.md5(data_str.encode()).hexdigest()[:8]
|
|
52
|
+
|
|
53
|
+
# Encode as base64
|
|
54
|
+
final_str = json.dumps(data, separators=(',', ':'))
|
|
55
|
+
return base64.urlsafe_b64encode(final_str.encode()).decode().rstrip('=')
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def from_cursor(cls, cursor: str) -> Optional['CursorData']:
|
|
59
|
+
"""Parse cursor string back to CursorData."""
|
|
60
|
+
try:
|
|
61
|
+
# Add padding if needed
|
|
62
|
+
padding = 4 - (len(cursor) % 4)
|
|
63
|
+
if padding != 4:
|
|
64
|
+
cursor += '=' * padding
|
|
65
|
+
|
|
66
|
+
decoded = base64.urlsafe_b64decode(cursor.encode())
|
|
67
|
+
data = json.loads(decoded)
|
|
68
|
+
|
|
69
|
+
# Validate checksum
|
|
70
|
+
checksum = data.pop("cs", None)
|
|
71
|
+
if checksum:
|
|
72
|
+
data_str = json.dumps({k: v for k, v in data.items() if k != "cs"},
|
|
73
|
+
sort_keys=True, separators=(',', ':'))
|
|
74
|
+
expected = hashlib.md5(data_str.encode()).hexdigest()[:8]
|
|
75
|
+
if checksum != expected:
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
# Check expiration
|
|
79
|
+
expires_at = data.get("ea")
|
|
80
|
+
if expires_at and time.time() > expires_at:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
return cls(
|
|
84
|
+
last_id=data.get("id"),
|
|
85
|
+
last_timestamp=data.get("ts"),
|
|
86
|
+
offset=data.get("o", 0),
|
|
87
|
+
page_size=data.get("ps", 100),
|
|
88
|
+
sort_field=data.get("sf", "id"),
|
|
89
|
+
sort_order=data.get("so", "asc"),
|
|
90
|
+
created_at=data.get("ca", time.time()),
|
|
91
|
+
expires_at=expires_at
|
|
92
|
+
)
|
|
93
|
+
except Exception:
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class FastMCPPaginator(Generic[T]):
|
|
98
|
+
"""High-performance paginator for FastMCP responses."""
|
|
99
|
+
|
|
100
|
+
def __init__(
|
|
101
|
+
self,
|
|
102
|
+
page_size: int = 100,
|
|
103
|
+
max_page_size: int = 1000,
|
|
104
|
+
cursor_ttl: int = 3600, # 1 hour
|
|
105
|
+
enable_prefetch: bool = False
|
|
106
|
+
):
|
|
107
|
+
"""Initialize the paginator.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
page_size: Default page size
|
|
111
|
+
max_page_size: Maximum allowed page size
|
|
112
|
+
cursor_ttl: Cursor time-to-live in seconds
|
|
113
|
+
enable_prefetch: Enable prefetching for next page
|
|
114
|
+
"""
|
|
115
|
+
self.page_size = page_size
|
|
116
|
+
self.max_page_size = max_page_size
|
|
117
|
+
self.cursor_ttl = cursor_ttl
|
|
118
|
+
self.enable_prefetch = enable_prefetch
|
|
119
|
+
self._cache: Dict[str, Any] = {}
|
|
120
|
+
|
|
121
|
+
def paginate_list(
|
|
122
|
+
self,
|
|
123
|
+
items: List[T],
|
|
124
|
+
cursor: Optional[str] = None,
|
|
125
|
+
page_size: Optional[int] = None,
|
|
126
|
+
sort_key: Optional[str] = None
|
|
127
|
+
) -> Dict[str, Any]:
|
|
128
|
+
"""Paginate a list with optimal performance.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
items: List to paginate
|
|
132
|
+
cursor: Optional cursor from previous request
|
|
133
|
+
page_size: Override default page size
|
|
134
|
+
sort_key: Sort field for consistent ordering
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Dict with items and optional nextCursor
|
|
138
|
+
"""
|
|
139
|
+
# Parse cursor or create new
|
|
140
|
+
cursor_data = CursorData.from_cursor(cursor) if cursor else CursorData()
|
|
141
|
+
|
|
142
|
+
# Use provided page size or default
|
|
143
|
+
actual_page_size = min(
|
|
144
|
+
page_size or cursor_data.page_size or self.page_size,
|
|
145
|
+
self.max_page_size
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Get starting position
|
|
149
|
+
start_idx = cursor_data.offset
|
|
150
|
+
|
|
151
|
+
# Validate bounds
|
|
152
|
+
if start_idx >= len(items):
|
|
153
|
+
return {"items": [], "hasMore": False}
|
|
154
|
+
|
|
155
|
+
# Slice the page
|
|
156
|
+
end_idx = min(start_idx + actual_page_size, len(items))
|
|
157
|
+
page_items = items[start_idx:end_idx]
|
|
158
|
+
|
|
159
|
+
# Build response
|
|
160
|
+
response = {
|
|
161
|
+
"items": page_items,
|
|
162
|
+
"pageInfo": {
|
|
163
|
+
"startIndex": start_idx,
|
|
164
|
+
"endIndex": end_idx,
|
|
165
|
+
"pageSize": len(page_items),
|
|
166
|
+
"totalItems": len(items)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
# Create next cursor if more items exist
|
|
171
|
+
if end_idx < len(items):
|
|
172
|
+
next_cursor_data = CursorData(
|
|
173
|
+
offset=end_idx,
|
|
174
|
+
page_size=actual_page_size,
|
|
175
|
+
expires_at=time.time() + self.cursor_ttl if self.cursor_ttl else None
|
|
176
|
+
)
|
|
177
|
+
response["nextCursor"] = next_cursor_data.to_cursor()
|
|
178
|
+
response["hasMore"] = True
|
|
179
|
+
else:
|
|
180
|
+
response["hasMore"] = False
|
|
181
|
+
|
|
182
|
+
return response
|
|
183
|
+
|
|
184
|
+
def paginate_query(
|
|
185
|
+
self,
|
|
186
|
+
query_func,
|
|
187
|
+
cursor: Optional[str] = None,
|
|
188
|
+
page_size: Optional[int] = None,
|
|
189
|
+
**query_params
|
|
190
|
+
) -> Dict[str, Any]:
|
|
191
|
+
"""Paginate results from a query function.
|
|
192
|
+
|
|
193
|
+
This is optimized for database queries using indexed fields.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
query_func: Function that accepts (last_id, last_timestamp, limit, **params)
|
|
197
|
+
cursor: Optional cursor
|
|
198
|
+
page_size: Override page size
|
|
199
|
+
**query_params: Additional query parameters
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Paginated response
|
|
203
|
+
"""
|
|
204
|
+
# Parse cursor
|
|
205
|
+
cursor_data = CursorData.from_cursor(cursor) if cursor else CursorData()
|
|
206
|
+
|
|
207
|
+
# Determine page size
|
|
208
|
+
limit = min(
|
|
209
|
+
page_size or cursor_data.page_size or self.page_size,
|
|
210
|
+
self.max_page_size
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Execute query with cursor position
|
|
214
|
+
results = query_func(
|
|
215
|
+
last_id=cursor_data.last_id,
|
|
216
|
+
last_timestamp=cursor_data.last_timestamp,
|
|
217
|
+
limit=limit + 1, # Fetch one extra to detect more
|
|
218
|
+
**query_params
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
# Check if there are more results
|
|
222
|
+
has_more = len(results) > limit
|
|
223
|
+
if has_more:
|
|
224
|
+
results = results[:limit] # Remove the extra item
|
|
225
|
+
|
|
226
|
+
# Build response
|
|
227
|
+
response = {
|
|
228
|
+
"items": results,
|
|
229
|
+
"pageInfo": {
|
|
230
|
+
"pageSize": len(results),
|
|
231
|
+
"hasMore": has_more
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
# Create next cursor if needed
|
|
236
|
+
if has_more and results:
|
|
237
|
+
last_item = results[-1]
|
|
238
|
+
next_cursor_data = CursorData(
|
|
239
|
+
last_id=getattr(last_item, 'id', None),
|
|
240
|
+
last_timestamp=getattr(last_item, 'timestamp', None),
|
|
241
|
+
page_size=limit,
|
|
242
|
+
sort_field=cursor_data.sort_field,
|
|
243
|
+
sort_order=cursor_data.sort_order,
|
|
244
|
+
expires_at=time.time() + self.cursor_ttl if self.cursor_ttl else None
|
|
245
|
+
)
|
|
246
|
+
response["nextCursor"] = next_cursor_data.to_cursor()
|
|
247
|
+
|
|
248
|
+
return response
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class TokenAwarePaginator:
|
|
252
|
+
"""Paginator that respects token limits for LLM responses."""
|
|
253
|
+
|
|
254
|
+
def __init__(self, max_tokens: int = 20000):
|
|
255
|
+
"""Initialize token-aware paginator.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
max_tokens: Maximum tokens per response
|
|
259
|
+
"""
|
|
260
|
+
self.max_tokens = max_tokens
|
|
261
|
+
self.paginator = FastMCPPaginator()
|
|
262
|
+
|
|
263
|
+
def paginate_by_tokens(
|
|
264
|
+
self,
|
|
265
|
+
items: List[Any],
|
|
266
|
+
cursor: Optional[str] = None,
|
|
267
|
+
estimate_func=None
|
|
268
|
+
) -> Dict[str, Any]:
|
|
269
|
+
"""Paginate items based on token count.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
items: Items to paginate
|
|
273
|
+
cursor: Optional cursor
|
|
274
|
+
estimate_func: Function to estimate tokens for an item
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
Paginated response
|
|
278
|
+
"""
|
|
279
|
+
from hanzo_mcp.tools.common.truncate import estimate_tokens
|
|
280
|
+
|
|
281
|
+
# Default token estimation
|
|
282
|
+
if not estimate_func:
|
|
283
|
+
estimate_func = lambda x: estimate_tokens(json.dumps(x) if not isinstance(x, str) else x)
|
|
284
|
+
|
|
285
|
+
# Parse cursor
|
|
286
|
+
cursor_data = CursorData.from_cursor(cursor) if cursor else CursorData()
|
|
287
|
+
start_idx = cursor_data.offset
|
|
288
|
+
|
|
289
|
+
# Build page respecting token limit
|
|
290
|
+
page_items = []
|
|
291
|
+
current_tokens = 100 # Base overhead
|
|
292
|
+
current_idx = start_idx
|
|
293
|
+
|
|
294
|
+
while current_idx < len(items) and current_tokens < self.max_tokens:
|
|
295
|
+
item = items[current_idx]
|
|
296
|
+
item_tokens = estimate_func(item)
|
|
297
|
+
|
|
298
|
+
# Check if adding this item would exceed limit
|
|
299
|
+
if current_tokens + item_tokens > self.max_tokens and page_items:
|
|
300
|
+
break
|
|
301
|
+
|
|
302
|
+
page_items.append(item)
|
|
303
|
+
current_tokens += item_tokens
|
|
304
|
+
current_idx += 1
|
|
305
|
+
|
|
306
|
+
# Build response
|
|
307
|
+
response = {
|
|
308
|
+
"items": page_items,
|
|
309
|
+
"pageInfo": {
|
|
310
|
+
"itemCount": len(page_items),
|
|
311
|
+
"estimatedTokens": current_tokens,
|
|
312
|
+
"hasMore": current_idx < len(items)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
# Add next cursor if needed
|
|
317
|
+
if current_idx < len(items):
|
|
318
|
+
next_cursor_data = CursorData(offset=current_idx)
|
|
319
|
+
response["nextCursor"] = next_cursor_data.to_cursor()
|
|
320
|
+
|
|
321
|
+
return response
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# FastMCP integration helpers
|
|
325
|
+
def create_paginated_response(
|
|
326
|
+
items: Union[List[Any], Dict[str, Any], str],
|
|
327
|
+
cursor: Optional[str] = None,
|
|
328
|
+
page_size: int = 100,
|
|
329
|
+
use_token_limit: bool = True
|
|
330
|
+
) -> Dict[str, Any]:
|
|
331
|
+
"""Create a paginated response compatible with FastMCP.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
items: The items to paginate
|
|
335
|
+
cursor: Optional cursor from request
|
|
336
|
+
page_size: Items per page
|
|
337
|
+
use_token_limit: Whether to use token-based pagination
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
FastMCP-compatible paginated response
|
|
341
|
+
"""
|
|
342
|
+
if use_token_limit:
|
|
343
|
+
paginator = TokenAwarePaginator()
|
|
344
|
+
|
|
345
|
+
# Convert different types to list
|
|
346
|
+
if isinstance(items, str):
|
|
347
|
+
# Split string by lines for pagination
|
|
348
|
+
items = items.split('\n')
|
|
349
|
+
elif isinstance(items, dict):
|
|
350
|
+
# Convert dict to list of key-value pairs
|
|
351
|
+
items = [{"key": k, "value": v} for k, v in items.items()]
|
|
352
|
+
|
|
353
|
+
return paginator.paginate_by_tokens(items, cursor)
|
|
354
|
+
else:
|
|
355
|
+
paginator = FastMCPPaginator(page_size=page_size)
|
|
356
|
+
|
|
357
|
+
# Handle different input types
|
|
358
|
+
if isinstance(items, list):
|
|
359
|
+
return paginator.paginate_list(items, cursor, page_size)
|
|
360
|
+
else:
|
|
361
|
+
# Convert to list first
|
|
362
|
+
if isinstance(items, str):
|
|
363
|
+
items = items.split('\n')
|
|
364
|
+
elif isinstance(items, dict):
|
|
365
|
+
items = [{"key": k, "value": v} for k, v in items.items()]
|
|
366
|
+
else:
|
|
367
|
+
items = [items]
|
|
368
|
+
|
|
369
|
+
return paginator.paginate_list(items, cursor, page_size)
|