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
|
@@ -4,107 +4,114 @@ This module provides a base class that automatically handles pagination
|
|
|
4
4
|
for all tool responses that exceed MCP token limits.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
import json
|
|
8
7
|
from abc import abstractmethod
|
|
9
|
-
from typing import Any, Dict,
|
|
8
|
+
from typing import Any, Dict, Union
|
|
10
9
|
|
|
11
10
|
from mcp.server.fastmcp import Context as MCPContext
|
|
12
11
|
|
|
13
12
|
from hanzo_mcp.tools.common.base import BaseTool, handle_connection_errors
|
|
14
|
-
from hanzo_mcp.tools.common.paginated_response import paginate_if_needed
|
|
15
13
|
from hanzo_mcp.tools.common.pagination import CursorManager
|
|
14
|
+
from hanzo_mcp.tools.common.paginated_response import paginate_if_needed
|
|
16
15
|
|
|
17
16
|
|
|
18
17
|
class PaginatedBaseTool(BaseTool):
|
|
19
18
|
"""Base class for tools with automatic pagination support.
|
|
20
|
-
|
|
19
|
+
|
|
21
20
|
This base class automatically handles pagination for responses that
|
|
22
21
|
exceed MCP token limits, making all tools pagination-aware by default.
|
|
23
22
|
"""
|
|
24
|
-
|
|
23
|
+
|
|
25
24
|
def __init__(self):
|
|
26
25
|
"""Initialize the paginated base tool."""
|
|
27
26
|
super().__init__()
|
|
28
27
|
self._supports_pagination = True
|
|
29
|
-
|
|
28
|
+
|
|
30
29
|
@abstractmethod
|
|
31
30
|
async def execute(self, ctx: MCPContext, **params: Any) -> Any:
|
|
32
31
|
"""Execute the tool logic and return raw results.
|
|
33
|
-
|
|
32
|
+
|
|
34
33
|
This method should be implemented by subclasses to perform the
|
|
35
34
|
actual tool logic. The base class will handle pagination of
|
|
36
35
|
the returned results automatically.
|
|
37
|
-
|
|
36
|
+
|
|
38
37
|
Args:
|
|
39
38
|
ctx: MCP context
|
|
40
39
|
**params: Tool parameters
|
|
41
|
-
|
|
40
|
+
|
|
42
41
|
Returns:
|
|
43
42
|
Raw tool results (will be paginated if needed)
|
|
44
43
|
"""
|
|
45
44
|
pass
|
|
46
|
-
|
|
45
|
+
|
|
47
46
|
@handle_connection_errors
|
|
48
47
|
async def call(self, ctx: MCPContext, **params: Any) -> Union[str, Dict[str, Any]]:
|
|
49
48
|
"""Execute the tool with automatic pagination support.
|
|
50
|
-
|
|
49
|
+
|
|
51
50
|
This method wraps the execute() method and automatically handles
|
|
52
51
|
pagination if the response exceeds token limits.
|
|
53
|
-
|
|
52
|
+
|
|
54
53
|
Args:
|
|
55
54
|
ctx: MCP context
|
|
56
55
|
**params: Tool parameters including optional 'cursor'
|
|
57
|
-
|
|
56
|
+
|
|
58
57
|
Returns:
|
|
59
58
|
Tool result, potentially paginated
|
|
60
59
|
"""
|
|
61
60
|
# Extract cursor if provided
|
|
62
61
|
cursor = params.pop("cursor", None)
|
|
63
|
-
|
|
62
|
+
|
|
64
63
|
# Validate cursor if provided
|
|
65
64
|
if cursor and not CursorManager.parse_cursor(cursor):
|
|
66
65
|
return {"error": "Invalid cursor provided", "code": -32602}
|
|
67
|
-
|
|
66
|
+
|
|
68
67
|
# Check if this is a continuation request
|
|
69
68
|
if cursor:
|
|
70
69
|
# For continuation, check if we have cached results
|
|
71
70
|
cursor_data = CursorManager.parse_cursor(cursor)
|
|
72
|
-
if
|
|
71
|
+
if (
|
|
72
|
+
cursor_data
|
|
73
|
+
and "tool" in cursor_data
|
|
74
|
+
and cursor_data["tool"] != self.name
|
|
75
|
+
):
|
|
73
76
|
return {"error": "Cursor is for a different tool", "code": -32602}
|
|
74
|
-
|
|
77
|
+
|
|
75
78
|
# Execute the tool
|
|
76
79
|
try:
|
|
77
80
|
result = await self.execute(ctx, **params)
|
|
78
81
|
except Exception as e:
|
|
79
82
|
# Format errors consistently
|
|
80
83
|
return {"error": str(e), "type": type(e).__name__}
|
|
81
|
-
|
|
84
|
+
|
|
82
85
|
# Handle pagination automatically
|
|
83
86
|
if self._supports_pagination:
|
|
84
87
|
paginated_result = paginate_if_needed(result, cursor)
|
|
85
|
-
|
|
88
|
+
|
|
86
89
|
# If pagination occurred, add tool info to help with continuation
|
|
87
90
|
if isinstance(paginated_result, dict) and "nextCursor" in paginated_result:
|
|
88
91
|
# Enhance the cursor with tool information
|
|
89
92
|
if "nextCursor" in paginated_result:
|
|
90
|
-
cursor_data = CursorManager.parse_cursor(
|
|
93
|
+
cursor_data = CursorManager.parse_cursor(
|
|
94
|
+
paginated_result["nextCursor"]
|
|
95
|
+
)
|
|
91
96
|
if cursor_data:
|
|
92
97
|
cursor_data["tool"] = self.name
|
|
93
98
|
cursor_data["params"] = params # Store params for continuation
|
|
94
|
-
paginated_result["nextCursor"] = CursorManager.create_cursor(
|
|
95
|
-
|
|
99
|
+
paginated_result["nextCursor"] = CursorManager.create_cursor(
|
|
100
|
+
cursor_data
|
|
101
|
+
)
|
|
102
|
+
|
|
96
103
|
return paginated_result
|
|
97
104
|
else:
|
|
98
105
|
# Return raw result if pagination is disabled
|
|
99
106
|
return result
|
|
100
|
-
|
|
107
|
+
|
|
101
108
|
def disable_pagination(self):
|
|
102
109
|
"""Disable automatic pagination for this tool.
|
|
103
|
-
|
|
110
|
+
|
|
104
111
|
Some tools may want to handle their own pagination logic.
|
|
105
112
|
"""
|
|
106
113
|
self._supports_pagination = False
|
|
107
|
-
|
|
114
|
+
|
|
108
115
|
def enable_pagination(self):
|
|
109
116
|
"""Re-enable automatic pagination for this tool."""
|
|
110
117
|
self._supports_pagination = True
|
|
@@ -112,22 +119,22 @@ class PaginatedBaseTool(BaseTool):
|
|
|
112
119
|
|
|
113
120
|
class PaginatedFileSystemTool(PaginatedBaseTool):
|
|
114
121
|
"""Base class for filesystem tools with pagination support."""
|
|
115
|
-
|
|
122
|
+
|
|
116
123
|
def __init__(self, permission_manager):
|
|
117
124
|
"""Initialize filesystem tool with pagination.
|
|
118
|
-
|
|
125
|
+
|
|
119
126
|
Args:
|
|
120
127
|
permission_manager: Permission manager for access control
|
|
121
128
|
"""
|
|
122
129
|
super().__init__()
|
|
123
130
|
self.permission_manager = permission_manager
|
|
124
|
-
|
|
131
|
+
|
|
125
132
|
def is_path_allowed(self, path: str) -> bool:
|
|
126
133
|
"""Check if a path is allowed according to permission settings.
|
|
127
|
-
|
|
134
|
+
|
|
128
135
|
Args:
|
|
129
136
|
path: Path to check
|
|
130
|
-
|
|
137
|
+
|
|
131
138
|
Returns:
|
|
132
139
|
True if the path is allowed, False otherwise
|
|
133
140
|
"""
|
|
@@ -136,95 +143,96 @@ class PaginatedFileSystemTool(PaginatedBaseTool):
|
|
|
136
143
|
|
|
137
144
|
def migrate_tool_to_paginated(tool_class):
|
|
138
145
|
"""Decorator to migrate existing tools to use pagination.
|
|
139
|
-
|
|
146
|
+
|
|
140
147
|
This decorator can be applied to existing tool classes to add
|
|
141
148
|
automatic pagination support without modifying their code.
|
|
142
|
-
|
|
149
|
+
|
|
143
150
|
Usage:
|
|
144
151
|
@migrate_tool_to_paginated
|
|
145
152
|
class MyTool(BaseTool):
|
|
146
153
|
...
|
|
147
154
|
"""
|
|
155
|
+
|
|
148
156
|
class PaginatedWrapper(PaginatedBaseTool):
|
|
149
157
|
def __init__(self, *args, **kwargs):
|
|
150
158
|
super().__init__()
|
|
151
159
|
self._wrapped_tool = tool_class(*args, **kwargs)
|
|
152
|
-
|
|
160
|
+
|
|
153
161
|
@property
|
|
154
162
|
def name(self):
|
|
155
163
|
return self._wrapped_tool.name
|
|
156
|
-
|
|
157
|
-
@property
|
|
164
|
+
|
|
165
|
+
@property
|
|
158
166
|
def description(self):
|
|
159
167
|
# Add pagination info to description
|
|
160
168
|
desc = self._wrapped_tool.description
|
|
161
169
|
if not "pagination" in desc.lower():
|
|
162
170
|
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."
|
|
163
171
|
return desc
|
|
164
|
-
|
|
172
|
+
|
|
165
173
|
async def execute(self, ctx: MCPContext, **params: Any) -> Any:
|
|
166
174
|
# Call the wrapped tool's call method
|
|
167
175
|
return await self._wrapped_tool.call(ctx, **params)
|
|
168
|
-
|
|
176
|
+
|
|
169
177
|
def register(self, mcp_server):
|
|
170
178
|
# Need to create a new registration that includes cursor parameter
|
|
171
179
|
tool_self = self
|
|
172
|
-
|
|
180
|
+
|
|
173
181
|
# Get the original registration function
|
|
174
182
|
original_register = self._wrapped_tool.register
|
|
175
|
-
|
|
183
|
+
|
|
176
184
|
# Create a new registration that adds cursor support
|
|
177
185
|
def register_with_pagination(server):
|
|
178
186
|
# First register the original tool
|
|
179
187
|
original_register(server)
|
|
180
|
-
|
|
188
|
+
|
|
181
189
|
# Then override with pagination support
|
|
182
190
|
import inspect
|
|
183
|
-
|
|
191
|
+
|
|
184
192
|
# Get the registered function
|
|
185
193
|
tool_func = None
|
|
186
194
|
for name, func in server._tools.items():
|
|
187
195
|
if name == self.name:
|
|
188
196
|
tool_func = func
|
|
189
197
|
break
|
|
190
|
-
|
|
198
|
+
|
|
191
199
|
if tool_func:
|
|
192
200
|
# Get original signature
|
|
193
201
|
sig = inspect.signature(tool_func)
|
|
194
202
|
params = list(sig.parameters.values())
|
|
195
|
-
|
|
203
|
+
|
|
196
204
|
# Add cursor parameter if not present
|
|
197
205
|
has_cursor = any(p.name == "cursor" for p in params)
|
|
198
206
|
if not has_cursor:
|
|
199
|
-
from typing import Optional
|
|
200
207
|
import inspect
|
|
201
|
-
|
|
208
|
+
from typing import Optional
|
|
209
|
+
|
|
202
210
|
# Create new parameter with cursor
|
|
203
211
|
cursor_param = inspect.Parameter(
|
|
204
212
|
"cursor",
|
|
205
213
|
inspect.Parameter.KEYWORD_ONLY,
|
|
206
214
|
default=None,
|
|
207
|
-
annotation=Optional[str]
|
|
215
|
+
annotation=Optional[str],
|
|
208
216
|
)
|
|
209
|
-
|
|
217
|
+
|
|
210
218
|
# Insert before ctx parameter
|
|
211
219
|
new_params = []
|
|
212
220
|
for p in params:
|
|
213
221
|
if p.name == "ctx":
|
|
214
222
|
new_params.append(cursor_param)
|
|
215
223
|
new_params.append(p)
|
|
216
|
-
|
|
224
|
+
|
|
217
225
|
# Create wrapper function
|
|
218
226
|
async def paginated_wrapper(**kwargs):
|
|
219
227
|
return await tool_self.call(kwargs.get("ctx"), **kwargs)
|
|
220
|
-
|
|
228
|
+
|
|
221
229
|
# Update registration
|
|
222
230
|
server._tools[self.name] = paginated_wrapper
|
|
223
|
-
|
|
231
|
+
|
|
224
232
|
register_with_pagination(mcp_server)
|
|
225
|
-
|
|
233
|
+
|
|
226
234
|
# Set the class name
|
|
227
235
|
PaginatedWrapper.__name__ = f"Paginated{tool_class.__name__}"
|
|
228
236
|
PaginatedWrapper.__qualname__ = f"Paginated{tool_class.__qualname__}"
|
|
229
|
-
|
|
230
|
-
return PaginatedWrapper
|
|
237
|
+
|
|
238
|
+
return PaginatedWrapper
|
|
@@ -5,30 +5,31 @@ when they exceed token limits.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import json
|
|
8
|
-
from typing import Any, Dict, List,
|
|
8
|
+
from typing import Any, Dict, List, Union, Optional
|
|
9
|
+
|
|
9
10
|
from hanzo_mcp.tools.common.truncate import estimate_tokens
|
|
10
11
|
from hanzo_mcp.tools.common.pagination import CursorManager
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
class AutoPaginatedResponse:
|
|
14
15
|
"""Automatically paginate responses that exceed token limits."""
|
|
15
|
-
|
|
16
|
+
|
|
16
17
|
# MCP token limit with safety buffer
|
|
17
18
|
MAX_TOKENS = 20000 # Leave 5k buffer from 25k limit
|
|
18
|
-
|
|
19
|
+
|
|
19
20
|
@staticmethod
|
|
20
21
|
def create_response(
|
|
21
22
|
content: Union[str, Dict[str, Any], List[Any]],
|
|
22
23
|
cursor: Optional[str] = None,
|
|
23
|
-
max_tokens: int = MAX_TOKENS
|
|
24
|
+
max_tokens: int = MAX_TOKENS,
|
|
24
25
|
) -> Dict[str, Any]:
|
|
25
26
|
"""Create a response that automatically paginates if too large.
|
|
26
|
-
|
|
27
|
+
|
|
27
28
|
Args:
|
|
28
29
|
content: The content to return
|
|
29
30
|
cursor: Optional cursor from request
|
|
30
31
|
max_tokens: Maximum tokens allowed in response
|
|
31
|
-
|
|
32
|
+
|
|
32
33
|
Returns:
|
|
33
34
|
Dict with content and optional nextCursor
|
|
34
35
|
"""
|
|
@@ -54,12 +55,10 @@ class AutoPaginatedResponse:
|
|
|
54
55
|
return AutoPaginatedResponse._handle_string_response(
|
|
55
56
|
str(content), cursor, max_tokens
|
|
56
57
|
)
|
|
57
|
-
|
|
58
|
+
|
|
58
59
|
@staticmethod
|
|
59
60
|
def _handle_string_response(
|
|
60
|
-
content: str,
|
|
61
|
-
cursor: Optional[str],
|
|
62
|
-
max_tokens: int
|
|
61
|
+
content: str, cursor: Optional[str], max_tokens: int
|
|
63
62
|
) -> Dict[str, Any]:
|
|
64
63
|
"""Handle pagination for string responses."""
|
|
65
64
|
# Parse cursor to get offset
|
|
@@ -68,28 +67,28 @@ class AutoPaginatedResponse:
|
|
|
68
67
|
cursor_data = CursorManager.parse_cursor(cursor)
|
|
69
68
|
if cursor_data and "offset" in cursor_data:
|
|
70
69
|
offset = cursor_data["offset"]
|
|
71
|
-
|
|
70
|
+
|
|
72
71
|
# For strings, paginate by lines
|
|
73
|
-
lines = content.split(
|
|
74
|
-
|
|
72
|
+
lines = content.split("\n")
|
|
73
|
+
|
|
75
74
|
if offset >= len(lines):
|
|
76
75
|
return {"content": "", "message": "No more content"}
|
|
77
|
-
|
|
76
|
+
|
|
78
77
|
# Build response line by line, checking tokens
|
|
79
78
|
result_lines = []
|
|
80
79
|
current_tokens = 0
|
|
81
80
|
line_index = offset
|
|
82
|
-
|
|
81
|
+
|
|
83
82
|
# Add header if this is a continuation
|
|
84
83
|
if offset > 0:
|
|
85
84
|
header = f"[Continued from line {offset + 1}]\n"
|
|
86
85
|
current_tokens = estimate_tokens(header)
|
|
87
86
|
result_lines.append(header)
|
|
88
|
-
|
|
87
|
+
|
|
89
88
|
while line_index < len(lines):
|
|
90
89
|
line = lines[line_index]
|
|
91
90
|
line_tokens = estimate_tokens(line + "\n")
|
|
92
|
-
|
|
91
|
+
|
|
93
92
|
# Check if adding this line would exceed limit
|
|
94
93
|
if current_tokens + line_tokens > max_tokens:
|
|
95
94
|
# Need to paginate
|
|
@@ -99,38 +98,34 @@ class AutoPaginatedResponse:
|
|
|
99
98
|
result_lines.append(truncated_line)
|
|
100
99
|
line_index += 1
|
|
101
100
|
break
|
|
102
|
-
|
|
101
|
+
|
|
103
102
|
result_lines.append(line)
|
|
104
103
|
current_tokens += line_tokens
|
|
105
104
|
line_index += 1
|
|
106
|
-
|
|
105
|
+
|
|
107
106
|
# Build response
|
|
108
|
-
response = {
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
107
|
+
response = {"content": "\n".join(result_lines)}
|
|
108
|
+
|
|
112
109
|
# Add pagination info
|
|
113
110
|
if line_index < len(lines):
|
|
114
111
|
response["nextCursor"] = CursorManager.create_offset_cursor(line_index)
|
|
115
112
|
response["pagination_info"] = {
|
|
116
113
|
"current_lines": f"{offset + 1}-{line_index}",
|
|
117
114
|
"total_lines": len(lines),
|
|
118
|
-
"has_more": True
|
|
115
|
+
"has_more": True,
|
|
119
116
|
}
|
|
120
117
|
else:
|
|
121
118
|
response["pagination_info"] = {
|
|
122
119
|
"current_lines": f"{offset + 1}-{len(lines)}",
|
|
123
120
|
"total_lines": len(lines),
|
|
124
|
-
"has_more": False
|
|
121
|
+
"has_more": False,
|
|
125
122
|
}
|
|
126
|
-
|
|
123
|
+
|
|
127
124
|
return response
|
|
128
|
-
|
|
125
|
+
|
|
129
126
|
@staticmethod
|
|
130
127
|
def _handle_list_response(
|
|
131
|
-
items: List[Any],
|
|
132
|
-
cursor: Optional[str],
|
|
133
|
-
max_tokens: int
|
|
128
|
+
items: List[Any], cursor: Optional[str], max_tokens: int
|
|
134
129
|
) -> Dict[str, Any]:
|
|
135
130
|
"""Handle pagination for list responses."""
|
|
136
131
|
# Parse cursor to get offset
|
|
@@ -139,28 +134,28 @@ class AutoPaginatedResponse:
|
|
|
139
134
|
cursor_data = CursorManager.parse_cursor(cursor)
|
|
140
135
|
if cursor_data and "offset" in cursor_data:
|
|
141
136
|
offset = cursor_data["offset"]
|
|
142
|
-
|
|
137
|
+
|
|
143
138
|
if offset >= len(items):
|
|
144
139
|
return {"items": [], "message": "No more items"}
|
|
145
|
-
|
|
140
|
+
|
|
146
141
|
# Build response item by item, checking tokens
|
|
147
142
|
result_items = []
|
|
148
143
|
current_tokens = 100 # Base overhead
|
|
149
144
|
item_index = offset
|
|
150
|
-
|
|
145
|
+
|
|
151
146
|
# Add header if continuation
|
|
152
147
|
header_obj = {}
|
|
153
148
|
if offset > 0:
|
|
154
149
|
header_obj["continuation_from"] = offset
|
|
155
150
|
current_tokens += 50
|
|
156
|
-
|
|
151
|
+
|
|
157
152
|
while item_index < len(items):
|
|
158
153
|
item = items[item_index]
|
|
159
|
-
|
|
154
|
+
|
|
160
155
|
# Estimate tokens for this item
|
|
161
156
|
item_str = json.dumps(item) if not isinstance(item, str) else item
|
|
162
157
|
item_tokens = estimate_tokens(item_str)
|
|
163
|
-
|
|
158
|
+
|
|
164
159
|
# Check if adding this item would exceed limit
|
|
165
160
|
if current_tokens + item_tokens > max_tokens:
|
|
166
161
|
if not result_items:
|
|
@@ -169,22 +164,22 @@ class AutoPaginatedResponse:
|
|
|
169
164
|
truncated = item[:5000] + "... [truncated]"
|
|
170
165
|
result_items.append(truncated)
|
|
171
166
|
else:
|
|
172
|
-
result_items.append(
|
|
167
|
+
result_items.append(
|
|
168
|
+
{"error": "Item too large", "index": item_index}
|
|
169
|
+
)
|
|
173
170
|
item_index += 1
|
|
174
171
|
break
|
|
175
|
-
|
|
172
|
+
|
|
176
173
|
result_items.append(item)
|
|
177
174
|
current_tokens += item_tokens
|
|
178
175
|
item_index += 1
|
|
179
|
-
|
|
176
|
+
|
|
180
177
|
# Build response
|
|
181
|
-
response = {
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
|
|
178
|
+
response = {"items": result_items}
|
|
179
|
+
|
|
185
180
|
if header_obj:
|
|
186
181
|
response.update(header_obj)
|
|
187
|
-
|
|
182
|
+
|
|
188
183
|
# Add pagination info
|
|
189
184
|
if item_index < len(items):
|
|
190
185
|
response["nextCursor"] = CursorManager.create_offset_cursor(item_index)
|
|
@@ -192,70 +187,66 @@ class AutoPaginatedResponse:
|
|
|
192
187
|
"returned_items": len(result_items),
|
|
193
188
|
"total_items": len(items),
|
|
194
189
|
"has_more": True,
|
|
195
|
-
"next_index": item_index
|
|
190
|
+
"next_index": item_index,
|
|
196
191
|
}
|
|
197
192
|
else:
|
|
198
193
|
response["pagination_info"] = {
|
|
199
194
|
"returned_items": len(result_items),
|
|
200
195
|
"total_items": len(items),
|
|
201
|
-
"has_more": False
|
|
196
|
+
"has_more": False,
|
|
202
197
|
}
|
|
203
|
-
|
|
198
|
+
|
|
204
199
|
return response
|
|
205
|
-
|
|
200
|
+
|
|
206
201
|
@staticmethod
|
|
207
202
|
def _handle_dict_response(
|
|
208
|
-
content: Dict[str, Any],
|
|
209
|
-
cursor: Optional[str],
|
|
210
|
-
max_tokens: int
|
|
203
|
+
content: Dict[str, Any], cursor: Optional[str], max_tokens: int
|
|
211
204
|
) -> Dict[str, Any]:
|
|
212
205
|
"""Handle pagination for dict responses."""
|
|
213
206
|
# For dicts, check if it's too large as-is
|
|
214
207
|
content_str = json.dumps(content, indent=2)
|
|
215
208
|
content_tokens = estimate_tokens(content_str)
|
|
216
|
-
|
|
209
|
+
|
|
217
210
|
if content_tokens <= max_tokens:
|
|
218
211
|
# Fits within limit
|
|
219
212
|
return content
|
|
220
|
-
|
|
213
|
+
|
|
221
214
|
# Too large - need to paginate
|
|
222
215
|
# Strategy: Convert to key-value pairs and paginate
|
|
223
216
|
items = list(content.items())
|
|
224
217
|
offset = 0
|
|
225
|
-
|
|
218
|
+
|
|
226
219
|
if cursor:
|
|
227
220
|
cursor_data = CursorManager.parse_cursor(cursor)
|
|
228
221
|
if cursor_data and "offset" in cursor_data:
|
|
229
222
|
offset = cursor_data["offset"]
|
|
230
|
-
|
|
223
|
+
|
|
231
224
|
if offset >= len(items):
|
|
232
225
|
return {"content": {}, "message": "No more content"}
|
|
233
|
-
|
|
226
|
+
|
|
234
227
|
# Build paginated dict
|
|
235
228
|
result = {}
|
|
236
229
|
current_tokens = 100 # Base overhead
|
|
237
|
-
|
|
230
|
+
|
|
238
231
|
for i in range(offset, len(items)):
|
|
239
232
|
key, value = items[i]
|
|
240
|
-
|
|
233
|
+
|
|
241
234
|
# Estimate tokens for this entry
|
|
242
235
|
entry_str = json.dumps({key: value})
|
|
243
236
|
entry_tokens = estimate_tokens(entry_str)
|
|
244
|
-
|
|
237
|
+
|
|
245
238
|
if current_tokens + entry_tokens > max_tokens:
|
|
246
239
|
if not result:
|
|
247
240
|
# Single entry too large
|
|
248
241
|
result[key] = "[Value too large - use specific key access]"
|
|
249
242
|
break
|
|
250
|
-
|
|
243
|
+
|
|
251
244
|
result[key] = value
|
|
252
245
|
current_tokens += entry_tokens
|
|
253
|
-
|
|
246
|
+
|
|
254
247
|
# Wrap in response
|
|
255
|
-
response = {
|
|
256
|
-
|
|
257
|
-
}
|
|
258
|
-
|
|
248
|
+
response = {"content": result}
|
|
249
|
+
|
|
259
250
|
# Add pagination info
|
|
260
251
|
processed = offset + len(result)
|
|
261
252
|
if processed < len(items):
|
|
@@ -263,45 +254,47 @@ class AutoPaginatedResponse:
|
|
|
263
254
|
response["pagination_info"] = {
|
|
264
255
|
"keys_returned": len(result),
|
|
265
256
|
"total_keys": len(items),
|
|
266
|
-
"has_more": True
|
|
257
|
+
"has_more": True,
|
|
267
258
|
}
|
|
268
259
|
else:
|
|
269
260
|
response["pagination_info"] = {
|
|
270
261
|
"keys_returned": len(result),
|
|
271
262
|
"total_keys": len(items),
|
|
272
|
-
"has_more": False
|
|
263
|
+
"has_more": False,
|
|
273
264
|
}
|
|
274
|
-
|
|
265
|
+
|
|
275
266
|
return response
|
|
276
267
|
|
|
277
268
|
|
|
278
269
|
def paginate_if_needed(
|
|
279
|
-
response: Any,
|
|
280
|
-
cursor: Optional[str] = None,
|
|
281
|
-
force_pagination: bool = False
|
|
270
|
+
response: Any, cursor: Optional[str] = None, force_pagination: bool = False
|
|
282
271
|
) -> Union[str, Dict[str, Any]]:
|
|
283
272
|
"""Wrap a response with automatic pagination if needed.
|
|
284
|
-
|
|
273
|
+
|
|
285
274
|
Args:
|
|
286
275
|
response: The response to potentially paginate
|
|
287
276
|
cursor: Optional cursor from request
|
|
288
277
|
force_pagination: Force pagination even for small responses
|
|
289
|
-
|
|
278
|
+
|
|
290
279
|
Returns:
|
|
291
280
|
Original response if small enough, otherwise paginated dict
|
|
292
281
|
"""
|
|
293
282
|
# Quick check - if response is already paginated, return as-is
|
|
294
|
-
if isinstance(response, dict) and (
|
|
283
|
+
if isinstance(response, dict) and (
|
|
284
|
+
"nextCursor" in response or "pagination_info" in response
|
|
285
|
+
):
|
|
295
286
|
return response
|
|
296
|
-
|
|
287
|
+
|
|
297
288
|
# For small responses, don't paginate unless forced
|
|
298
289
|
if not force_pagination:
|
|
299
290
|
try:
|
|
300
|
-
response_str =
|
|
291
|
+
response_str = (
|
|
292
|
+
json.dumps(response) if not isinstance(response, str) else response
|
|
293
|
+
)
|
|
301
294
|
if len(response_str) < 10000: # Quick heuristic
|
|
302
295
|
return response
|
|
303
|
-
except:
|
|
296
|
+
except Exception:
|
|
304
297
|
pass
|
|
305
|
-
|
|
298
|
+
|
|
306
299
|
# Create paginated response
|
|
307
|
-
return AutoPaginatedResponse.create_response(response, cursor)
|
|
300
|
+
return AutoPaginatedResponse.create_response(response, cursor)
|