hanzo-mcp 0.7.7__py3-none-any.whl → 0.8.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 +6 -0
- hanzo_mcp/__main__.py +1 -1
- hanzo_mcp/analytics/__init__.py +2 -2
- hanzo_mcp/analytics/posthog_analytics.py +76 -82
- hanzo_mcp/cli.py +31 -36
- hanzo_mcp/cli_enhanced.py +94 -72
- hanzo_mcp/cli_plugin.py +27 -17
- hanzo_mcp/config/__init__.py +2 -2
- hanzo_mcp/config/settings.py +112 -88
- hanzo_mcp/config/tool_config.py +32 -34
- hanzo_mcp/dev_server.py +66 -67
- hanzo_mcp/prompts/__init__.py +94 -12
- hanzo_mcp/prompts/enhanced_prompts.py +809 -0
- hanzo_mcp/prompts/example_custom_prompt.py +6 -5
- hanzo_mcp/prompts/project_todo_reminder.py +0 -1
- hanzo_mcp/prompts/tool_explorer.py +10 -7
- hanzo_mcp/server.py +17 -21
- hanzo_mcp/server_enhanced.py +15 -22
- hanzo_mcp/tools/__init__.py +56 -28
- hanzo_mcp/tools/agent/__init__.py +16 -19
- hanzo_mcp/tools/agent/agent.py +82 -65
- hanzo_mcp/tools/agent/agent_tool.py +152 -122
- hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +66 -62
- hanzo_mcp/tools/agent/clarification_protocol.py +55 -50
- hanzo_mcp/tools/agent/clarification_tool.py +11 -10
- hanzo_mcp/tools/agent/claude_cli_tool.py +21 -20
- hanzo_mcp/tools/agent/claude_desktop_auth.py +130 -144
- hanzo_mcp/tools/agent/cli_agent_base.py +59 -53
- hanzo_mcp/tools/agent/code_auth.py +102 -107
- hanzo_mcp/tools/agent/code_auth_tool.py +28 -27
- hanzo_mcp/tools/agent/codex_cli_tool.py +20 -19
- hanzo_mcp/tools/agent/critic_tool.py +86 -73
- hanzo_mcp/tools/agent/gemini_cli_tool.py +21 -20
- hanzo_mcp/tools/agent/grok_cli_tool.py +21 -20
- hanzo_mcp/tools/agent/iching_tool.py +404 -139
- hanzo_mcp/tools/agent/network_tool.py +89 -73
- hanzo_mcp/tools/agent/prompt.py +2 -1
- hanzo_mcp/tools/agent/review_tool.py +101 -98
- hanzo_mcp/tools/agent/swarm_alias.py +87 -0
- hanzo_mcp/tools/agent/swarm_tool.py +246 -161
- hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +134 -92
- hanzo_mcp/tools/agent/tool_adapter.py +21 -11
- hanzo_mcp/tools/common/__init__.py +1 -1
- hanzo_mcp/tools/common/base.py +3 -5
- hanzo_mcp/tools/common/batch_tool.py +46 -39
- hanzo_mcp/tools/common/config_tool.py +120 -84
- hanzo_mcp/tools/common/context.py +1 -5
- hanzo_mcp/tools/common/context_fix.py +5 -3
- hanzo_mcp/tools/common/critic_tool.py +4 -8
- hanzo_mcp/tools/common/decorators.py +58 -56
- hanzo_mcp/tools/common/enhanced_base.py +29 -32
- hanzo_mcp/tools/common/fastmcp_pagination.py +91 -94
- hanzo_mcp/tools/common/forgiving_edit.py +91 -87
- hanzo_mcp/tools/common/mode.py +15 -17
- hanzo_mcp/tools/common/mode_loader.py +27 -24
- hanzo_mcp/tools/common/paginated_base.py +61 -53
- hanzo_mcp/tools/common/paginated_response.py +72 -79
- hanzo_mcp/tools/common/pagination.py +50 -53
- hanzo_mcp/tools/common/permissions.py +4 -4
- hanzo_mcp/tools/common/personality.py +186 -138
- hanzo_mcp/tools/common/plugin_loader.py +54 -54
- hanzo_mcp/tools/common/stats.py +65 -47
- hanzo_mcp/tools/common/test_helpers.py +31 -0
- hanzo_mcp/tools/common/thinking_tool.py +4 -8
- hanzo_mcp/tools/common/tool_disable.py +17 -12
- hanzo_mcp/tools/common/tool_enable.py +13 -14
- hanzo_mcp/tools/common/tool_list.py +36 -28
- hanzo_mcp/tools/common/truncate.py +23 -23
- hanzo_mcp/tools/config/__init__.py +4 -4
- hanzo_mcp/tools/config/config_tool.py +42 -29
- hanzo_mcp/tools/config/index_config.py +37 -34
- hanzo_mcp/tools/config/mode_tool.py +175 -55
- hanzo_mcp/tools/database/__init__.py +15 -12
- hanzo_mcp/tools/database/database_manager.py +77 -75
- hanzo_mcp/tools/database/graph.py +137 -91
- hanzo_mcp/tools/database/graph_add.py +30 -18
- hanzo_mcp/tools/database/graph_query.py +178 -102
- hanzo_mcp/tools/database/graph_remove.py +33 -28
- hanzo_mcp/tools/database/graph_search.py +97 -75
- hanzo_mcp/tools/database/graph_stats.py +91 -59
- hanzo_mcp/tools/database/sql.py +107 -79
- hanzo_mcp/tools/database/sql_query.py +30 -24
- hanzo_mcp/tools/database/sql_search.py +29 -25
- hanzo_mcp/tools/database/sql_stats.py +47 -35
- hanzo_mcp/tools/editor/neovim_command.py +25 -28
- hanzo_mcp/tools/editor/neovim_edit.py +21 -23
- hanzo_mcp/tools/editor/neovim_session.py +60 -54
- hanzo_mcp/tools/filesystem/__init__.py +31 -30
- hanzo_mcp/tools/filesystem/ast_multi_edit.py +329 -249
- hanzo_mcp/tools/filesystem/ast_tool.py +4 -4
- hanzo_mcp/tools/filesystem/base.py +1 -1
- hanzo_mcp/tools/filesystem/batch_search.py +316 -224
- hanzo_mcp/tools/filesystem/content_replace.py +4 -4
- hanzo_mcp/tools/filesystem/diff.py +71 -59
- hanzo_mcp/tools/filesystem/directory_tree.py +7 -7
- hanzo_mcp/tools/filesystem/directory_tree_paginated.py +49 -37
- hanzo_mcp/tools/filesystem/edit.py +4 -4
- hanzo_mcp/tools/filesystem/find.py +173 -80
- hanzo_mcp/tools/filesystem/find_files.py +73 -52
- hanzo_mcp/tools/filesystem/git_search.py +157 -104
- hanzo_mcp/tools/filesystem/grep.py +8 -8
- hanzo_mcp/tools/filesystem/multi_edit.py +4 -8
- hanzo_mcp/tools/filesystem/read.py +12 -10
- hanzo_mcp/tools/filesystem/rules_tool.py +59 -43
- hanzo_mcp/tools/filesystem/search_tool.py +263 -207
- hanzo_mcp/tools/filesystem/symbols_tool.py +94 -54
- hanzo_mcp/tools/filesystem/tree.py +35 -33
- hanzo_mcp/tools/filesystem/unix_aliases.py +13 -18
- hanzo_mcp/tools/filesystem/watch.py +37 -36
- hanzo_mcp/tools/filesystem/write.py +4 -8
- hanzo_mcp/tools/jupyter/__init__.py +4 -4
- hanzo_mcp/tools/jupyter/base.py +4 -5
- hanzo_mcp/tools/jupyter/jupyter.py +67 -47
- hanzo_mcp/tools/jupyter/notebook_edit.py +4 -4
- hanzo_mcp/tools/jupyter/notebook_read.py +4 -7
- hanzo_mcp/tools/llm/__init__.py +5 -7
- hanzo_mcp/tools/llm/consensus_tool.py +72 -52
- hanzo_mcp/tools/llm/llm_manage.py +101 -60
- hanzo_mcp/tools/llm/llm_tool.py +226 -166
- hanzo_mcp/tools/llm/provider_tools.py +25 -26
- hanzo_mcp/tools/lsp/__init__.py +1 -1
- hanzo_mcp/tools/lsp/lsp_tool.py +228 -143
- hanzo_mcp/tools/mcp/__init__.py +2 -3
- hanzo_mcp/tools/mcp/mcp_add.py +27 -25
- hanzo_mcp/tools/mcp/mcp_remove.py +7 -8
- hanzo_mcp/tools/mcp/mcp_stats.py +23 -22
- hanzo_mcp/tools/mcp/mcp_tool.py +129 -98
- hanzo_mcp/tools/memory/__init__.py +39 -21
- hanzo_mcp/tools/memory/knowledge_tools.py +124 -99
- hanzo_mcp/tools/memory/memory_tools.py +90 -108
- hanzo_mcp/tools/search/__init__.py +7 -2
- hanzo_mcp/tools/search/find_tool.py +297 -212
- hanzo_mcp/tools/search/unified_search.py +366 -314
- hanzo_mcp/tools/shell/__init__.py +8 -7
- hanzo_mcp/tools/shell/auto_background.py +56 -49
- hanzo_mcp/tools/shell/base.py +1 -1
- hanzo_mcp/tools/shell/base_process.py +75 -75
- hanzo_mcp/tools/shell/bash_session.py +2 -2
- hanzo_mcp/tools/shell/bash_session_executor.py +4 -4
- hanzo_mcp/tools/shell/bash_tool.py +24 -31
- hanzo_mcp/tools/shell/command_executor.py +12 -12
- hanzo_mcp/tools/shell/logs.py +43 -33
- hanzo_mcp/tools/shell/npx.py +13 -13
- hanzo_mcp/tools/shell/npx_background.py +24 -21
- hanzo_mcp/tools/shell/npx_tool.py +18 -22
- hanzo_mcp/tools/shell/open.py +19 -21
- hanzo_mcp/tools/shell/pkill.py +31 -26
- hanzo_mcp/tools/shell/process_tool.py +32 -32
- hanzo_mcp/tools/shell/processes.py +57 -58
- hanzo_mcp/tools/shell/run_background.py +24 -25
- hanzo_mcp/tools/shell/run_command.py +5 -5
- hanzo_mcp/tools/shell/run_command_windows.py +5 -5
- hanzo_mcp/tools/shell/session_storage.py +3 -3
- hanzo_mcp/tools/shell/streaming_command.py +141 -126
- hanzo_mcp/tools/shell/uvx.py +24 -25
- hanzo_mcp/tools/shell/uvx_background.py +35 -33
- hanzo_mcp/tools/shell/uvx_tool.py +18 -22
- hanzo_mcp/tools/todo/__init__.py +6 -2
- hanzo_mcp/tools/todo/todo.py +50 -37
- hanzo_mcp/tools/todo/todo_read.py +5 -8
- hanzo_mcp/tools/todo/todo_write.py +5 -7
- hanzo_mcp/tools/vector/__init__.py +40 -28
- hanzo_mcp/tools/vector/ast_analyzer.py +176 -143
- hanzo_mcp/tools/vector/git_ingester.py +170 -179
- hanzo_mcp/tools/vector/index_tool.py +96 -44
- hanzo_mcp/tools/vector/infinity_store.py +283 -228
- hanzo_mcp/tools/vector/mock_infinity.py +39 -40
- hanzo_mcp/tools/vector/project_manager.py +88 -78
- hanzo_mcp/tools/vector/vector.py +59 -42
- hanzo_mcp/tools/vector/vector_index.py +30 -27
- hanzo_mcp/tools/vector/vector_search.py +64 -45
- hanzo_mcp/types.py +6 -4
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.0.dist-info}/METADATA +1 -1
- hanzo_mcp-0.8.0.dist-info/RECORD +185 -0
- hanzo_mcp-0.7.7.dist-info/RECORD +0 -182
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.0.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.0.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.0.dist-info}/top_level.txt +0 -0
|
@@ -3,35 +3,35 @@
|
|
|
3
3
|
This module provides pagination utilities optimized for FastMCP with minimal latency.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
import base64
|
|
7
6
|
import json
|
|
8
7
|
import time
|
|
9
|
-
|
|
10
|
-
from dataclasses import dataclass, field
|
|
11
|
-
from datetime import datetime
|
|
8
|
+
import base64
|
|
12
9
|
import hashlib
|
|
10
|
+
from typing import Any, Dict, List, Union, Generic, TypeVar, Optional
|
|
11
|
+
from dataclasses import field, dataclass
|
|
13
12
|
|
|
14
|
-
T = TypeVar(
|
|
13
|
+
T = TypeVar("T")
|
|
15
14
|
|
|
16
15
|
|
|
17
16
|
@dataclass
|
|
18
17
|
class CursorData:
|
|
19
18
|
"""Cursor data structure for efficient pagination."""
|
|
19
|
+
|
|
20
20
|
# Primary cursor fields (indexed)
|
|
21
21
|
last_id: Optional[str] = None
|
|
22
22
|
last_timestamp: Optional[float] = None
|
|
23
23
|
offset: int = 0
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
# Metadata for validation and optimization
|
|
26
26
|
page_size: int = 100
|
|
27
27
|
sort_field: str = "id"
|
|
28
28
|
sort_order: str = "asc"
|
|
29
|
-
|
|
29
|
+
|
|
30
30
|
# Security and validation
|
|
31
31
|
created_at: float = field(default_factory=time.time)
|
|
32
32
|
expires_at: Optional[float] = None
|
|
33
33
|
checksum: Optional[str] = None
|
|
34
|
-
|
|
34
|
+
|
|
35
35
|
def to_cursor(self) -> str:
|
|
36
36
|
"""Convert to opaque cursor string."""
|
|
37
37
|
data = {
|
|
@@ -45,41 +45,44 @@ class CursorData:
|
|
|
45
45
|
}
|
|
46
46
|
if self.expires_at:
|
|
47
47
|
data["ea"] = self.expires_at
|
|
48
|
-
|
|
48
|
+
|
|
49
49
|
# Add checksum for integrity
|
|
50
|
-
data_str = json.dumps(data, sort_keys=True, separators=(
|
|
50
|
+
data_str = json.dumps(data, sort_keys=True, separators=(",", ":"))
|
|
51
51
|
data["cs"] = hashlib.md5(data_str.encode()).hexdigest()[:8]
|
|
52
|
-
|
|
52
|
+
|
|
53
53
|
# Encode as base64
|
|
54
|
-
final_str = json.dumps(data, separators=(
|
|
55
|
-
return base64.urlsafe_b64encode(final_str.encode()).decode().rstrip(
|
|
56
|
-
|
|
54
|
+
final_str = json.dumps(data, separators=(",", ":"))
|
|
55
|
+
return base64.urlsafe_b64encode(final_str.encode()).decode().rstrip("=")
|
|
56
|
+
|
|
57
57
|
@classmethod
|
|
58
|
-
def from_cursor(cls, cursor: str) -> Optional[
|
|
58
|
+
def from_cursor(cls, cursor: str) -> Optional["CursorData"]:
|
|
59
59
|
"""Parse cursor string back to CursorData."""
|
|
60
60
|
try:
|
|
61
61
|
# Add padding if needed
|
|
62
62
|
padding = 4 - (len(cursor) % 4)
|
|
63
63
|
if padding != 4:
|
|
64
|
-
cursor +=
|
|
65
|
-
|
|
64
|
+
cursor += "=" * padding
|
|
65
|
+
|
|
66
66
|
decoded = base64.urlsafe_b64decode(cursor.encode())
|
|
67
67
|
data = json.loads(decoded)
|
|
68
|
-
|
|
68
|
+
|
|
69
69
|
# Validate checksum
|
|
70
70
|
checksum = data.pop("cs", None)
|
|
71
71
|
if checksum:
|
|
72
|
-
data_str = json.dumps(
|
|
73
|
-
|
|
72
|
+
data_str = json.dumps(
|
|
73
|
+
{k: v for k, v in data.items() if k != "cs"},
|
|
74
|
+
sort_keys=True,
|
|
75
|
+
separators=(",", ":"),
|
|
76
|
+
)
|
|
74
77
|
expected = hashlib.md5(data_str.encode()).hexdigest()[:8]
|
|
75
78
|
if checksum != expected:
|
|
76
79
|
return None
|
|
77
|
-
|
|
80
|
+
|
|
78
81
|
# Check expiration
|
|
79
82
|
expires_at = data.get("ea")
|
|
80
83
|
if expires_at and time.time() > expires_at:
|
|
81
84
|
return None
|
|
82
|
-
|
|
85
|
+
|
|
83
86
|
return cls(
|
|
84
87
|
last_id=data.get("id"),
|
|
85
88
|
last_timestamp=data.get("ts"),
|
|
@@ -88,7 +91,7 @@ class CursorData:
|
|
|
88
91
|
sort_field=data.get("sf", "id"),
|
|
89
92
|
sort_order=data.get("so", "asc"),
|
|
90
93
|
created_at=data.get("ca", time.time()),
|
|
91
|
-
expires_at=expires_at
|
|
94
|
+
expires_at=expires_at,
|
|
92
95
|
)
|
|
93
96
|
except Exception:
|
|
94
97
|
return None
|
|
@@ -96,16 +99,16 @@ class CursorData:
|
|
|
96
99
|
|
|
97
100
|
class FastMCPPaginator(Generic[T]):
|
|
98
101
|
"""High-performance paginator for FastMCP responses."""
|
|
99
|
-
|
|
102
|
+
|
|
100
103
|
def __init__(
|
|
101
104
|
self,
|
|
102
105
|
page_size: int = 100,
|
|
103
106
|
max_page_size: int = 1000,
|
|
104
107
|
cursor_ttl: int = 3600, # 1 hour
|
|
105
|
-
enable_prefetch: bool = False
|
|
108
|
+
enable_prefetch: bool = False,
|
|
106
109
|
):
|
|
107
110
|
"""Initialize the paginator.
|
|
108
|
-
|
|
111
|
+
|
|
109
112
|
Args:
|
|
110
113
|
page_size: Default page size
|
|
111
114
|
max_page_size: Maximum allowed page size
|
|
@@ -117,45 +120,44 @@ class FastMCPPaginator(Generic[T]):
|
|
|
117
120
|
self.cursor_ttl = cursor_ttl
|
|
118
121
|
self.enable_prefetch = enable_prefetch
|
|
119
122
|
self._cache: Dict[str, Any] = {}
|
|
120
|
-
|
|
123
|
+
|
|
121
124
|
def paginate_list(
|
|
122
125
|
self,
|
|
123
126
|
items: List[T],
|
|
124
127
|
cursor: Optional[str] = None,
|
|
125
128
|
page_size: Optional[int] = None,
|
|
126
|
-
sort_key: Optional[str] = None
|
|
129
|
+
sort_key: Optional[str] = None,
|
|
127
130
|
) -> Dict[str, Any]:
|
|
128
131
|
"""Paginate a list with optimal performance.
|
|
129
|
-
|
|
132
|
+
|
|
130
133
|
Args:
|
|
131
134
|
items: List to paginate
|
|
132
135
|
cursor: Optional cursor from previous request
|
|
133
136
|
page_size: Override default page size
|
|
134
137
|
sort_key: Sort field for consistent ordering
|
|
135
|
-
|
|
138
|
+
|
|
136
139
|
Returns:
|
|
137
140
|
Dict with items and optional nextCursor
|
|
138
141
|
"""
|
|
139
142
|
# Parse cursor or create new
|
|
140
143
|
cursor_data = CursorData.from_cursor(cursor) if cursor else CursorData()
|
|
141
|
-
|
|
144
|
+
|
|
142
145
|
# Use provided page size or default
|
|
143
146
|
actual_page_size = min(
|
|
144
|
-
page_size or cursor_data.page_size or self.page_size,
|
|
145
|
-
self.max_page_size
|
|
147
|
+
page_size or cursor_data.page_size or self.page_size, self.max_page_size
|
|
146
148
|
)
|
|
147
|
-
|
|
149
|
+
|
|
148
150
|
# Get starting position
|
|
149
151
|
start_idx = cursor_data.offset
|
|
150
|
-
|
|
152
|
+
|
|
151
153
|
# Validate bounds
|
|
152
154
|
if start_idx >= len(items):
|
|
153
155
|
return {"items": [], "hasMore": False}
|
|
154
|
-
|
|
156
|
+
|
|
155
157
|
# Slice the page
|
|
156
158
|
end_idx = min(start_idx + actual_page_size, len(items))
|
|
157
159
|
page_items = items[start_idx:end_idx]
|
|
158
|
-
|
|
160
|
+
|
|
159
161
|
# Build response
|
|
160
162
|
response = {
|
|
161
163
|
"items": page_items,
|
|
@@ -163,161 +165,156 @@ class FastMCPPaginator(Generic[T]):
|
|
|
163
165
|
"startIndex": start_idx,
|
|
164
166
|
"endIndex": end_idx,
|
|
165
167
|
"pageSize": len(page_items),
|
|
166
|
-
"totalItems": len(items)
|
|
167
|
-
}
|
|
168
|
+
"totalItems": len(items),
|
|
169
|
+
},
|
|
168
170
|
}
|
|
169
|
-
|
|
171
|
+
|
|
170
172
|
# Create next cursor if more items exist
|
|
171
173
|
if end_idx < len(items):
|
|
172
174
|
next_cursor_data = CursorData(
|
|
173
175
|
offset=end_idx,
|
|
174
176
|
page_size=actual_page_size,
|
|
175
|
-
expires_at=time.time() + self.cursor_ttl if self.cursor_ttl else None
|
|
177
|
+
expires_at=time.time() + self.cursor_ttl if self.cursor_ttl else None,
|
|
176
178
|
)
|
|
177
179
|
response["nextCursor"] = next_cursor_data.to_cursor()
|
|
178
180
|
response["hasMore"] = True
|
|
179
181
|
else:
|
|
180
182
|
response["hasMore"] = False
|
|
181
|
-
|
|
183
|
+
|
|
182
184
|
return response
|
|
183
|
-
|
|
185
|
+
|
|
184
186
|
def paginate_query(
|
|
185
187
|
self,
|
|
186
188
|
query_func,
|
|
187
189
|
cursor: Optional[str] = None,
|
|
188
190
|
page_size: Optional[int] = None,
|
|
189
|
-
**query_params
|
|
191
|
+
**query_params,
|
|
190
192
|
) -> Dict[str, Any]:
|
|
191
193
|
"""Paginate results from a query function.
|
|
192
|
-
|
|
194
|
+
|
|
193
195
|
This is optimized for database queries using indexed fields.
|
|
194
|
-
|
|
196
|
+
|
|
195
197
|
Args:
|
|
196
198
|
query_func: Function that accepts (last_id, last_timestamp, limit, **params)
|
|
197
199
|
cursor: Optional cursor
|
|
198
200
|
page_size: Override page size
|
|
199
201
|
**query_params: Additional query parameters
|
|
200
|
-
|
|
202
|
+
|
|
201
203
|
Returns:
|
|
202
204
|
Paginated response
|
|
203
205
|
"""
|
|
204
206
|
# Parse cursor
|
|
205
207
|
cursor_data = CursorData.from_cursor(cursor) if cursor else CursorData()
|
|
206
|
-
|
|
208
|
+
|
|
207
209
|
# Determine page size
|
|
208
210
|
limit = min(
|
|
209
|
-
page_size or cursor_data.page_size or self.page_size,
|
|
210
|
-
self.max_page_size
|
|
211
|
+
page_size or cursor_data.page_size or self.page_size, self.max_page_size
|
|
211
212
|
)
|
|
212
|
-
|
|
213
|
+
|
|
213
214
|
# Execute query with cursor position
|
|
214
215
|
results = query_func(
|
|
215
216
|
last_id=cursor_data.last_id,
|
|
216
217
|
last_timestamp=cursor_data.last_timestamp,
|
|
217
218
|
limit=limit + 1, # Fetch one extra to detect more
|
|
218
|
-
**query_params
|
|
219
|
+
**query_params,
|
|
219
220
|
)
|
|
220
|
-
|
|
221
|
+
|
|
221
222
|
# Check if there are more results
|
|
222
223
|
has_more = len(results) > limit
|
|
223
224
|
if has_more:
|
|
224
225
|
results = results[:limit] # Remove the extra item
|
|
225
|
-
|
|
226
|
+
|
|
226
227
|
# Build response
|
|
227
228
|
response = {
|
|
228
229
|
"items": results,
|
|
229
|
-
"pageInfo": {
|
|
230
|
-
"pageSize": len(results),
|
|
231
|
-
"hasMore": has_more
|
|
232
|
-
}
|
|
230
|
+
"pageInfo": {"pageSize": len(results), "hasMore": has_more},
|
|
233
231
|
}
|
|
234
|
-
|
|
232
|
+
|
|
235
233
|
# Create next cursor if needed
|
|
236
234
|
if has_more and results:
|
|
237
235
|
last_item = results[-1]
|
|
238
236
|
next_cursor_data = CursorData(
|
|
239
|
-
last_id=getattr(last_item,
|
|
240
|
-
last_timestamp=getattr(last_item,
|
|
237
|
+
last_id=getattr(last_item, "id", None),
|
|
238
|
+
last_timestamp=getattr(last_item, "timestamp", None),
|
|
241
239
|
page_size=limit,
|
|
242
240
|
sort_field=cursor_data.sort_field,
|
|
243
241
|
sort_order=cursor_data.sort_order,
|
|
244
|
-
expires_at=time.time() + self.cursor_ttl if self.cursor_ttl else None
|
|
242
|
+
expires_at=time.time() + self.cursor_ttl if self.cursor_ttl else None,
|
|
245
243
|
)
|
|
246
244
|
response["nextCursor"] = next_cursor_data.to_cursor()
|
|
247
|
-
|
|
245
|
+
|
|
248
246
|
return response
|
|
249
247
|
|
|
250
248
|
|
|
251
249
|
class TokenAwarePaginator:
|
|
252
250
|
"""Paginator that respects token limits for LLM responses."""
|
|
253
|
-
|
|
251
|
+
|
|
254
252
|
def __init__(self, max_tokens: int = 20000):
|
|
255
253
|
"""Initialize token-aware paginator.
|
|
256
|
-
|
|
254
|
+
|
|
257
255
|
Args:
|
|
258
256
|
max_tokens: Maximum tokens per response
|
|
259
257
|
"""
|
|
260
258
|
self.max_tokens = max_tokens
|
|
261
259
|
self.paginator = FastMCPPaginator()
|
|
262
|
-
|
|
260
|
+
|
|
263
261
|
def paginate_by_tokens(
|
|
264
|
-
self,
|
|
265
|
-
items: List[Any],
|
|
266
|
-
cursor: Optional[str] = None,
|
|
267
|
-
estimate_func=None
|
|
262
|
+
self, items: List[Any], cursor: Optional[str] = None, estimate_func=None
|
|
268
263
|
) -> Dict[str, Any]:
|
|
269
264
|
"""Paginate items based on token count.
|
|
270
|
-
|
|
265
|
+
|
|
271
266
|
Args:
|
|
272
267
|
items: Items to paginate
|
|
273
268
|
cursor: Optional cursor
|
|
274
269
|
estimate_func: Function to estimate tokens for an item
|
|
275
|
-
|
|
270
|
+
|
|
276
271
|
Returns:
|
|
277
272
|
Paginated response
|
|
278
273
|
"""
|
|
279
274
|
from hanzo_mcp.tools.common.truncate import estimate_tokens
|
|
280
|
-
|
|
275
|
+
|
|
281
276
|
# Default token estimation
|
|
282
277
|
if not estimate_func:
|
|
283
|
-
estimate_func = lambda x: estimate_tokens(
|
|
284
|
-
|
|
278
|
+
estimate_func = lambda x: estimate_tokens(
|
|
279
|
+
json.dumps(x) if not isinstance(x, str) else x
|
|
280
|
+
)
|
|
281
|
+
|
|
285
282
|
# Parse cursor
|
|
286
283
|
cursor_data = CursorData.from_cursor(cursor) if cursor else CursorData()
|
|
287
284
|
start_idx = cursor_data.offset
|
|
288
|
-
|
|
285
|
+
|
|
289
286
|
# Build page respecting token limit
|
|
290
287
|
page_items = []
|
|
291
288
|
current_tokens = 100 # Base overhead
|
|
292
289
|
current_idx = start_idx
|
|
293
|
-
|
|
290
|
+
|
|
294
291
|
while current_idx < len(items) and current_tokens < self.max_tokens:
|
|
295
292
|
item = items[current_idx]
|
|
296
293
|
item_tokens = estimate_func(item)
|
|
297
|
-
|
|
294
|
+
|
|
298
295
|
# Check if adding this item would exceed limit
|
|
299
296
|
if current_tokens + item_tokens > self.max_tokens and page_items:
|
|
300
297
|
break
|
|
301
|
-
|
|
298
|
+
|
|
302
299
|
page_items.append(item)
|
|
303
300
|
current_tokens += item_tokens
|
|
304
301
|
current_idx += 1
|
|
305
|
-
|
|
302
|
+
|
|
306
303
|
# Build response
|
|
307
304
|
response = {
|
|
308
305
|
"items": page_items,
|
|
309
306
|
"pageInfo": {
|
|
310
307
|
"itemCount": len(page_items),
|
|
311
308
|
"estimatedTokens": current_tokens,
|
|
312
|
-
"hasMore": current_idx < len(items)
|
|
313
|
-
}
|
|
309
|
+
"hasMore": current_idx < len(items),
|
|
310
|
+
},
|
|
314
311
|
}
|
|
315
|
-
|
|
312
|
+
|
|
316
313
|
# Add next cursor if needed
|
|
317
314
|
if current_idx < len(items):
|
|
318
315
|
next_cursor_data = CursorData(offset=current_idx)
|
|
319
316
|
response["nextCursor"] = next_cursor_data.to_cursor()
|
|
320
|
-
|
|
317
|
+
|
|
321
318
|
return response
|
|
322
319
|
|
|
323
320
|
|
|
@@ -326,44 +323,44 @@ def create_paginated_response(
|
|
|
326
323
|
items: Union[List[Any], Dict[str, Any], str],
|
|
327
324
|
cursor: Optional[str] = None,
|
|
328
325
|
page_size: int = 100,
|
|
329
|
-
use_token_limit: bool = True
|
|
326
|
+
use_token_limit: bool = True,
|
|
330
327
|
) -> Dict[str, Any]:
|
|
331
328
|
"""Create a paginated response compatible with FastMCP.
|
|
332
|
-
|
|
329
|
+
|
|
333
330
|
Args:
|
|
334
331
|
items: The items to paginate
|
|
335
332
|
cursor: Optional cursor from request
|
|
336
333
|
page_size: Items per page
|
|
337
334
|
use_token_limit: Whether to use token-based pagination
|
|
338
|
-
|
|
335
|
+
|
|
339
336
|
Returns:
|
|
340
337
|
FastMCP-compatible paginated response
|
|
341
338
|
"""
|
|
342
339
|
if use_token_limit:
|
|
343
340
|
paginator = TokenAwarePaginator()
|
|
344
|
-
|
|
341
|
+
|
|
345
342
|
# Convert different types to list
|
|
346
343
|
if isinstance(items, str):
|
|
347
344
|
# Split string by lines for pagination
|
|
348
|
-
items = items.split(
|
|
345
|
+
items = items.split("\n")
|
|
349
346
|
elif isinstance(items, dict):
|
|
350
347
|
# Convert dict to list of key-value pairs
|
|
351
348
|
items = [{"key": k, "value": v} for k, v in items.items()]
|
|
352
|
-
|
|
349
|
+
|
|
353
350
|
return paginator.paginate_by_tokens(items, cursor)
|
|
354
351
|
else:
|
|
355
352
|
paginator = FastMCPPaginator(page_size=page_size)
|
|
356
|
-
|
|
353
|
+
|
|
357
354
|
# Handle different input types
|
|
358
355
|
if isinstance(items, list):
|
|
359
356
|
return paginator.paginate_list(items, cursor, page_size)
|
|
360
357
|
else:
|
|
361
358
|
# Convert to list first
|
|
362
359
|
if isinstance(items, str):
|
|
363
|
-
items = items.split(
|
|
360
|
+
items = items.split("\n")
|
|
364
361
|
elif isinstance(items, dict):
|
|
365
362
|
items = [{"key": k, "value": v} for k, v in items.items()]
|
|
366
363
|
else:
|
|
367
364
|
items = [items]
|
|
368
|
-
|
|
369
|
-
return paginator.paginate_list(items, cursor, page_size)
|
|
365
|
+
|
|
366
|
+
return paginator.paginate_list(items, cursor, page_size)
|