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.

Files changed (117) hide show
  1. hanzo_mcp/__init__.py +2 -2
  2. hanzo_mcp/analytics/__init__.py +5 -0
  3. hanzo_mcp/analytics/posthog_analytics.py +364 -0
  4. hanzo_mcp/cli.py +5 -5
  5. hanzo_mcp/cli_enhanced.py +7 -7
  6. hanzo_mcp/cli_plugin.py +91 -0
  7. hanzo_mcp/config/__init__.py +1 -1
  8. hanzo_mcp/config/settings.py +70 -7
  9. hanzo_mcp/config/tool_config.py +20 -6
  10. hanzo_mcp/dev_server.py +3 -3
  11. hanzo_mcp/prompts/project_system.py +1 -1
  12. hanzo_mcp/server.py +40 -3
  13. hanzo_mcp/server_enhanced.py +69 -0
  14. hanzo_mcp/tools/__init__.py +140 -31
  15. hanzo_mcp/tools/agent/__init__.py +85 -4
  16. hanzo_mcp/tools/agent/agent_tool.py +104 -6
  17. hanzo_mcp/tools/agent/agent_tool_v2.py +459 -0
  18. hanzo_mcp/tools/agent/clarification_protocol.py +220 -0
  19. hanzo_mcp/tools/agent/clarification_tool.py +68 -0
  20. hanzo_mcp/tools/agent/claude_cli_tool.py +125 -0
  21. hanzo_mcp/tools/agent/claude_desktop_auth.py +508 -0
  22. hanzo_mcp/tools/agent/cli_agent_base.py +191 -0
  23. hanzo_mcp/tools/agent/code_auth.py +436 -0
  24. hanzo_mcp/tools/agent/code_auth_tool.py +194 -0
  25. hanzo_mcp/tools/agent/codex_cli_tool.py +123 -0
  26. hanzo_mcp/tools/agent/critic_tool.py +376 -0
  27. hanzo_mcp/tools/agent/gemini_cli_tool.py +128 -0
  28. hanzo_mcp/tools/agent/grok_cli_tool.py +128 -0
  29. hanzo_mcp/tools/agent/iching_tool.py +380 -0
  30. hanzo_mcp/tools/agent/network_tool.py +273 -0
  31. hanzo_mcp/tools/agent/prompt.py +62 -20
  32. hanzo_mcp/tools/agent/review_tool.py +433 -0
  33. hanzo_mcp/tools/agent/swarm_tool.py +535 -0
  34. hanzo_mcp/tools/agent/swarm_tool_v2.py +594 -0
  35. hanzo_mcp/tools/common/__init__.py +15 -1
  36. hanzo_mcp/tools/common/base.py +5 -4
  37. hanzo_mcp/tools/common/batch_tool.py +103 -11
  38. hanzo_mcp/tools/common/config_tool.py +2 -2
  39. hanzo_mcp/tools/common/context.py +2 -2
  40. hanzo_mcp/tools/common/context_fix.py +26 -0
  41. hanzo_mcp/tools/common/critic_tool.py +196 -0
  42. hanzo_mcp/tools/common/decorators.py +208 -0
  43. hanzo_mcp/tools/common/enhanced_base.py +106 -0
  44. hanzo_mcp/tools/common/fastmcp_pagination.py +369 -0
  45. hanzo_mcp/tools/common/forgiving_edit.py +243 -0
  46. hanzo_mcp/tools/common/mode.py +116 -0
  47. hanzo_mcp/tools/common/mode_loader.py +105 -0
  48. hanzo_mcp/tools/common/paginated_base.py +230 -0
  49. hanzo_mcp/tools/common/paginated_response.py +307 -0
  50. hanzo_mcp/tools/common/pagination.py +226 -0
  51. hanzo_mcp/tools/common/permissions.py +1 -1
  52. hanzo_mcp/tools/common/personality.py +936 -0
  53. hanzo_mcp/tools/common/plugin_loader.py +287 -0
  54. hanzo_mcp/tools/common/stats.py +4 -4
  55. hanzo_mcp/tools/common/tool_list.py +4 -1
  56. hanzo_mcp/tools/common/truncate.py +101 -0
  57. hanzo_mcp/tools/common/validation.py +1 -1
  58. hanzo_mcp/tools/config/__init__.py +3 -1
  59. hanzo_mcp/tools/config/config_tool.py +1 -1
  60. hanzo_mcp/tools/config/mode_tool.py +209 -0
  61. hanzo_mcp/tools/database/__init__.py +1 -1
  62. hanzo_mcp/tools/editor/__init__.py +1 -1
  63. hanzo_mcp/tools/filesystem/__init__.py +48 -14
  64. hanzo_mcp/tools/filesystem/ast_multi_edit.py +562 -0
  65. hanzo_mcp/tools/filesystem/batch_search.py +3 -3
  66. hanzo_mcp/tools/filesystem/diff.py +2 -2
  67. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +338 -0
  68. hanzo_mcp/tools/filesystem/rules_tool.py +235 -0
  69. hanzo_mcp/tools/filesystem/{unified_search.py → search_tool.py} +12 -12
  70. hanzo_mcp/tools/filesystem/{symbols_unified.py → symbols_tool.py} +104 -5
  71. hanzo_mcp/tools/filesystem/watch.py +3 -2
  72. hanzo_mcp/tools/jupyter/__init__.py +2 -2
  73. hanzo_mcp/tools/jupyter/jupyter.py +1 -1
  74. hanzo_mcp/tools/llm/__init__.py +3 -3
  75. hanzo_mcp/tools/llm/llm_tool.py +648 -143
  76. hanzo_mcp/tools/lsp/__init__.py +5 -0
  77. hanzo_mcp/tools/lsp/lsp_tool.py +512 -0
  78. hanzo_mcp/tools/mcp/__init__.py +2 -2
  79. hanzo_mcp/tools/mcp/{mcp_unified.py → mcp_tool.py} +3 -3
  80. hanzo_mcp/tools/memory/__init__.py +76 -0
  81. hanzo_mcp/tools/memory/knowledge_tools.py +518 -0
  82. hanzo_mcp/tools/memory/memory_tools.py +456 -0
  83. hanzo_mcp/tools/search/__init__.py +6 -0
  84. hanzo_mcp/tools/search/find_tool.py +581 -0
  85. hanzo_mcp/tools/search/unified_search.py +953 -0
  86. hanzo_mcp/tools/shell/__init__.py +11 -6
  87. hanzo_mcp/tools/shell/auto_background.py +203 -0
  88. hanzo_mcp/tools/shell/base_process.py +57 -29
  89. hanzo_mcp/tools/shell/bash_session_executor.py +1 -1
  90. hanzo_mcp/tools/shell/{bash_unified.py → bash_tool.py} +18 -34
  91. hanzo_mcp/tools/shell/command_executor.py +2 -2
  92. hanzo_mcp/tools/shell/{npx_unified.py → npx_tool.py} +16 -33
  93. hanzo_mcp/tools/shell/open.py +2 -2
  94. hanzo_mcp/tools/shell/{process_unified.py → process_tool.py} +1 -1
  95. hanzo_mcp/tools/shell/run_command_windows.py +1 -1
  96. hanzo_mcp/tools/shell/streaming_command.py +594 -0
  97. hanzo_mcp/tools/shell/uvx.py +47 -2
  98. hanzo_mcp/tools/shell/uvx_background.py +47 -2
  99. hanzo_mcp/tools/shell/{uvx_unified.py → uvx_tool.py} +16 -33
  100. hanzo_mcp/tools/todo/__init__.py +14 -19
  101. hanzo_mcp/tools/todo/todo.py +22 -1
  102. hanzo_mcp/tools/vector/__init__.py +1 -1
  103. hanzo_mcp/tools/vector/infinity_store.py +2 -2
  104. hanzo_mcp/tools/vector/project_manager.py +1 -1
  105. hanzo_mcp/types.py +23 -0
  106. hanzo_mcp-0.7.0.dist-info/METADATA +516 -0
  107. hanzo_mcp-0.7.0.dist-info/RECORD +180 -0
  108. {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.7.0.dist-info}/entry_points.txt +1 -0
  109. hanzo_mcp/tools/common/palette.py +0 -344
  110. hanzo_mcp/tools/common/palette_loader.py +0 -108
  111. hanzo_mcp/tools/config/palette_tool.py +0 -179
  112. hanzo_mcp/tools/llm/llm_unified.py +0 -851
  113. hanzo_mcp-0.6.12.dist-info/METADATA +0 -339
  114. hanzo_mcp-0.6.12.dist-info/RECORD +0 -135
  115. hanzo_mcp-0.6.12.dist-info/licenses/LICENSE +0 -21
  116. {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.7.0.dist-info}/WHEEL +0 -0
  117. {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.7.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,307 @@
1
+ """Automatic pagination response wrapper for MCP tools.
2
+
3
+ This module provides utilities to automatically paginate tool responses
4
+ when they exceed token limits.
5
+ """
6
+
7
+ import json
8
+ from typing import Any, Dict, List, Optional, Union
9
+ from hanzo_mcp.tools.common.truncate import estimate_tokens
10
+ from hanzo_mcp.tools.common.pagination import CursorManager
11
+
12
+
13
+ class AutoPaginatedResponse:
14
+ """Automatically paginate responses that exceed token limits."""
15
+
16
+ # MCP token limit with safety buffer
17
+ MAX_TOKENS = 20000 # Leave 5k buffer from 25k limit
18
+
19
+ @staticmethod
20
+ def create_response(
21
+ content: Union[str, Dict[str, Any], List[Any]],
22
+ cursor: Optional[str] = None,
23
+ max_tokens: int = MAX_TOKENS
24
+ ) -> Dict[str, Any]:
25
+ """Create a response that automatically paginates if too large.
26
+
27
+ Args:
28
+ content: The content to return
29
+ cursor: Optional cursor from request
30
+ max_tokens: Maximum tokens allowed in response
31
+
32
+ Returns:
33
+ Dict with content and optional nextCursor
34
+ """
35
+ # Handle different content types
36
+ if isinstance(content, str):
37
+ return AutoPaginatedResponse._handle_string_response(
38
+ content, cursor, max_tokens
39
+ )
40
+ elif isinstance(content, list):
41
+ return AutoPaginatedResponse._handle_list_response(
42
+ content, cursor, max_tokens
43
+ )
44
+ elif isinstance(content, dict):
45
+ # If dict already has pagination info, return as-is
46
+ if "nextCursor" in content or "cursor" in content:
47
+ return content
48
+ # Otherwise treat as single item
49
+ return AutoPaginatedResponse._handle_dict_response(
50
+ content, cursor, max_tokens
51
+ )
52
+ else:
53
+ # Convert to string for other types
54
+ return AutoPaginatedResponse._handle_string_response(
55
+ str(content), cursor, max_tokens
56
+ )
57
+
58
+ @staticmethod
59
+ def _handle_string_response(
60
+ content: str,
61
+ cursor: Optional[str],
62
+ max_tokens: int
63
+ ) -> Dict[str, Any]:
64
+ """Handle pagination for string responses."""
65
+ # Parse cursor to get offset
66
+ offset = 0
67
+ if cursor:
68
+ cursor_data = CursorManager.parse_cursor(cursor)
69
+ if cursor_data and "offset" in cursor_data:
70
+ offset = cursor_data["offset"]
71
+
72
+ # For strings, paginate by lines
73
+ lines = content.split('\n')
74
+
75
+ if offset >= len(lines):
76
+ return {"content": "", "message": "No more content"}
77
+
78
+ # Build response line by line, checking tokens
79
+ result_lines = []
80
+ current_tokens = 0
81
+ line_index = offset
82
+
83
+ # Add header if this is a continuation
84
+ if offset > 0:
85
+ header = f"[Continued from line {offset + 1}]\n"
86
+ current_tokens = estimate_tokens(header)
87
+ result_lines.append(header)
88
+
89
+ while line_index < len(lines):
90
+ line = lines[line_index]
91
+ line_tokens = estimate_tokens(line + "\n")
92
+
93
+ # Check if adding this line would exceed limit
94
+ if current_tokens + line_tokens > max_tokens:
95
+ # Need to paginate
96
+ if not result_lines:
97
+ # Single line too long, truncate it
98
+ truncated_line = line[:1000] + "... [line truncated]"
99
+ result_lines.append(truncated_line)
100
+ line_index += 1
101
+ break
102
+
103
+ result_lines.append(line)
104
+ current_tokens += line_tokens
105
+ line_index += 1
106
+
107
+ # Build response
108
+ response = {
109
+ "content": '\n'.join(result_lines)
110
+ }
111
+
112
+ # Add pagination info
113
+ if line_index < len(lines):
114
+ response["nextCursor"] = CursorManager.create_offset_cursor(line_index)
115
+ response["pagination_info"] = {
116
+ "current_lines": f"{offset + 1}-{line_index}",
117
+ "total_lines": len(lines),
118
+ "has_more": True
119
+ }
120
+ else:
121
+ response["pagination_info"] = {
122
+ "current_lines": f"{offset + 1}-{len(lines)}",
123
+ "total_lines": len(lines),
124
+ "has_more": False
125
+ }
126
+
127
+ return response
128
+
129
+ @staticmethod
130
+ def _handle_list_response(
131
+ items: List[Any],
132
+ cursor: Optional[str],
133
+ max_tokens: int
134
+ ) -> Dict[str, Any]:
135
+ """Handle pagination for list responses."""
136
+ # Parse cursor to get offset
137
+ offset = 0
138
+ if cursor:
139
+ cursor_data = CursorManager.parse_cursor(cursor)
140
+ if cursor_data and "offset" in cursor_data:
141
+ offset = cursor_data["offset"]
142
+
143
+ if offset >= len(items):
144
+ return {"items": [], "message": "No more items"}
145
+
146
+ # Build response item by item, checking tokens
147
+ result_items = []
148
+ current_tokens = 100 # Base overhead
149
+ item_index = offset
150
+
151
+ # Add header if continuation
152
+ header_obj = {}
153
+ if offset > 0:
154
+ header_obj["continuation_from"] = offset
155
+ current_tokens += 50
156
+
157
+ while item_index < len(items):
158
+ item = items[item_index]
159
+
160
+ # Estimate tokens for this item
161
+ item_str = json.dumps(item) if not isinstance(item, str) else item
162
+ item_tokens = estimate_tokens(item_str)
163
+
164
+ # Check if adding this item would exceed limit
165
+ if current_tokens + item_tokens > max_tokens:
166
+ if not result_items:
167
+ # Single item too large, truncate it
168
+ if isinstance(item, str):
169
+ truncated = item[:5000] + "... [truncated]"
170
+ result_items.append(truncated)
171
+ else:
172
+ result_items.append({"error": "Item too large", "index": item_index})
173
+ item_index += 1
174
+ break
175
+
176
+ result_items.append(item)
177
+ current_tokens += item_tokens
178
+ item_index += 1
179
+
180
+ # Build response
181
+ response = {
182
+ "items": result_items
183
+ }
184
+
185
+ if header_obj:
186
+ response.update(header_obj)
187
+
188
+ # Add pagination info
189
+ if item_index < len(items):
190
+ response["nextCursor"] = CursorManager.create_offset_cursor(item_index)
191
+ response["pagination_info"] = {
192
+ "returned_items": len(result_items),
193
+ "total_items": len(items),
194
+ "has_more": True,
195
+ "next_index": item_index
196
+ }
197
+ else:
198
+ response["pagination_info"] = {
199
+ "returned_items": len(result_items),
200
+ "total_items": len(items),
201
+ "has_more": False
202
+ }
203
+
204
+ return response
205
+
206
+ @staticmethod
207
+ def _handle_dict_response(
208
+ content: Dict[str, Any],
209
+ cursor: Optional[str],
210
+ max_tokens: int
211
+ ) -> Dict[str, Any]:
212
+ """Handle pagination for dict responses."""
213
+ # For dicts, check if it's too large as-is
214
+ content_str = json.dumps(content, indent=2)
215
+ content_tokens = estimate_tokens(content_str)
216
+
217
+ if content_tokens <= max_tokens:
218
+ # Fits within limit
219
+ return content
220
+
221
+ # Too large - need to paginate
222
+ # Strategy: Convert to key-value pairs and paginate
223
+ items = list(content.items())
224
+ offset = 0
225
+
226
+ if cursor:
227
+ cursor_data = CursorManager.parse_cursor(cursor)
228
+ if cursor_data and "offset" in cursor_data:
229
+ offset = cursor_data["offset"]
230
+
231
+ if offset >= len(items):
232
+ return {"content": {}, "message": "No more content"}
233
+
234
+ # Build paginated dict
235
+ result = {}
236
+ current_tokens = 100 # Base overhead
237
+
238
+ for i in range(offset, len(items)):
239
+ key, value = items[i]
240
+
241
+ # Estimate tokens for this entry
242
+ entry_str = json.dumps({key: value})
243
+ entry_tokens = estimate_tokens(entry_str)
244
+
245
+ if current_tokens + entry_tokens > max_tokens:
246
+ if not result:
247
+ # Single entry too large
248
+ result[key] = "[Value too large - use specific key access]"
249
+ break
250
+
251
+ result[key] = value
252
+ current_tokens += entry_tokens
253
+
254
+ # Wrap in response
255
+ response = {
256
+ "content": result
257
+ }
258
+
259
+ # Add pagination info
260
+ processed = offset + len(result)
261
+ if processed < len(items):
262
+ response["nextCursor"] = CursorManager.create_offset_cursor(processed)
263
+ response["pagination_info"] = {
264
+ "keys_returned": len(result),
265
+ "total_keys": len(items),
266
+ "has_more": True
267
+ }
268
+ else:
269
+ response["pagination_info"] = {
270
+ "keys_returned": len(result),
271
+ "total_keys": len(items),
272
+ "has_more": False
273
+ }
274
+
275
+ return response
276
+
277
+
278
+ def paginate_if_needed(
279
+ response: Any,
280
+ cursor: Optional[str] = None,
281
+ force_pagination: bool = False
282
+ ) -> Union[str, Dict[str, Any]]:
283
+ """Wrap a response with automatic pagination if needed.
284
+
285
+ Args:
286
+ response: The response to potentially paginate
287
+ cursor: Optional cursor from request
288
+ force_pagination: Force pagination even for small responses
289
+
290
+ Returns:
291
+ Original response if small enough, otherwise paginated dict
292
+ """
293
+ # Quick check - if response is already paginated, return as-is
294
+ if isinstance(response, dict) and ("nextCursor" in response or "pagination_info" in response):
295
+ return response
296
+
297
+ # For small responses, don't paginate unless forced
298
+ if not force_pagination:
299
+ try:
300
+ response_str = json.dumps(response) if not isinstance(response, str) else response
301
+ if len(response_str) < 10000: # Quick heuristic
302
+ return response
303
+ except:
304
+ pass
305
+
306
+ # Create paginated response
307
+ return AutoPaginatedResponse.create_response(response, cursor)
@@ -0,0 +1,226 @@
1
+ """Pagination utilities for MCP tools.
2
+
3
+ This module provides utilities for implementing cursor-based pagination
4
+ according to the MCP pagination protocol.
5
+ """
6
+
7
+ import base64
8
+ import json
9
+ from typing import Any, Dict, List, Optional, Tuple, TypeVar, Generic
10
+ from dataclasses import dataclass
11
+
12
+
13
+ T = TypeVar('T')
14
+
15
+
16
+ @dataclass
17
+ class PaginationParams:
18
+ """Parameters for pagination."""
19
+ cursor: Optional[str] = None
20
+ page_size: int = 100 # Default page size
21
+
22
+
23
+ @dataclass
24
+ class PaginatedResponse(Generic[T]):
25
+ """A paginated response containing items and optional next cursor."""
26
+ items: List[T]
27
+ next_cursor: Optional[str] = None
28
+
29
+ def to_dict(self, items_key: str = "items") -> Dict[str, Any]:
30
+ """Convert to dictionary format for MCP response.
31
+
32
+ Args:
33
+ items_key: The key to use for items in the response
34
+
35
+ Returns:
36
+ Dictionary with items and optional nextCursor
37
+ """
38
+ result = {items_key: self.items}
39
+ if self.next_cursor:
40
+ result["nextCursor"] = self.next_cursor
41
+ return result
42
+
43
+
44
+ class CursorManager:
45
+ """Manages cursor creation and parsing for pagination."""
46
+
47
+ @staticmethod
48
+ def create_cursor(data: Dict[str, Any]) -> str:
49
+ """Create an opaque cursor from data.
50
+
51
+ Args:
52
+ data: Data to encode in the cursor
53
+
54
+ Returns:
55
+ Base64-encoded cursor string
56
+ """
57
+ json_data = json.dumps(data, separators=(',', ':'))
58
+ return base64.b64encode(json_data.encode()).decode()
59
+
60
+ @staticmethod
61
+ def parse_cursor(cursor: str) -> Optional[Dict[str, Any]]:
62
+ """Parse a cursor string back to data.
63
+
64
+ Args:
65
+ cursor: Base64-encoded cursor string
66
+
67
+ Returns:
68
+ Decoded data or None if invalid
69
+ """
70
+ try:
71
+ decoded = base64.b64decode(cursor.encode()).decode()
72
+ return json.loads(decoded)
73
+ except (ValueError, json.JSONDecodeError):
74
+ return None
75
+
76
+ @staticmethod
77
+ def create_offset_cursor(offset: int) -> str:
78
+ """Create a cursor for offset-based pagination.
79
+
80
+ Args:
81
+ offset: The offset for the next page
82
+
83
+ Returns:
84
+ Cursor string
85
+ """
86
+ return CursorManager.create_cursor({"offset": offset})
87
+
88
+ @staticmethod
89
+ def parse_offset_cursor(cursor: Optional[str]) -> int:
90
+ """Parse an offset cursor.
91
+
92
+ Args:
93
+ cursor: Cursor string or None
94
+
95
+ Returns:
96
+ Offset value (0 if cursor is None or invalid)
97
+ """
98
+ if not cursor:
99
+ return 0
100
+
101
+ data = CursorManager.parse_cursor(cursor)
102
+ if not data or "offset" not in data:
103
+ return 0
104
+
105
+ return int(data["offset"])
106
+
107
+
108
+ class Paginator(Generic[T]):
109
+ """Generic paginator for any list of items."""
110
+
111
+ def __init__(self, items: List[T], page_size: int = 100):
112
+ """Initialize paginator.
113
+
114
+ Args:
115
+ items: List of items to paginate
116
+ page_size: Number of items per page
117
+ """
118
+ self.items = items
119
+ self.page_size = page_size
120
+
121
+ def get_page(self, cursor: Optional[str] = None) -> PaginatedResponse[T]:
122
+ """Get a page of results.
123
+
124
+ Args:
125
+ cursor: Optional cursor for the page
126
+
127
+ Returns:
128
+ Paginated response with items and next cursor
129
+ """
130
+ offset = CursorManager.parse_offset_cursor(cursor)
131
+
132
+ # Get the page of items
133
+ start = offset
134
+ end = min(start + self.page_size, len(self.items))
135
+ page_items = self.items[start:end]
136
+
137
+ # Create next cursor if there are more items
138
+ next_cursor = None
139
+ if end < len(self.items):
140
+ next_cursor = CursorManager.create_offset_cursor(end)
141
+
142
+ return PaginatedResponse(items=page_items, next_cursor=next_cursor)
143
+
144
+
145
+ class StreamPaginator(Generic[T]):
146
+ """Paginator for streaming/generator-based results."""
147
+
148
+ def __init__(self, page_size: int = 100):
149
+ """Initialize stream paginator.
150
+
151
+ Args:
152
+ page_size: Number of items per page
153
+ """
154
+ self.page_size = page_size
155
+
156
+ def paginate_stream(
157
+ self,
158
+ stream_generator,
159
+ cursor: Optional[str] = None
160
+ ) -> PaginatedResponse[T]:
161
+ """Paginate results from a stream/generator.
162
+
163
+ Args:
164
+ stream_generator: Generator function that yields items
165
+ cursor: Optional cursor for resuming
166
+
167
+ Returns:
168
+ Paginated response
169
+ """
170
+ items = []
171
+ skip_count = 0
172
+
173
+ # Parse cursor to get skip count
174
+ if cursor:
175
+ cursor_data = CursorManager.parse_cursor(cursor)
176
+ if cursor_data and "skip" in cursor_data:
177
+ skip_count = cursor_data["skip"]
178
+
179
+ # Skip items based on cursor
180
+ item_count = 0
181
+ for item in stream_generator():
182
+ if item_count < skip_count:
183
+ item_count += 1
184
+ continue
185
+
186
+ items.append(item)
187
+ if len(items) >= self.page_size:
188
+ # We have a full page, create cursor for next page
189
+ next_cursor = CursorManager.create_cursor({
190
+ "skip": skip_count + len(items)
191
+ })
192
+ return PaginatedResponse(items=items, next_cursor=next_cursor)
193
+
194
+ # No more items
195
+ return PaginatedResponse(items=items, next_cursor=None)
196
+
197
+
198
+ def paginate_list(
199
+ items: List[T],
200
+ cursor: Optional[str] = None,
201
+ page_size: int = 100
202
+ ) -> PaginatedResponse[T]:
203
+ """Convenience function to paginate a list.
204
+
205
+ Args:
206
+ items: List to paginate
207
+ cursor: Optional cursor
208
+ page_size: Items per page
209
+
210
+ Returns:
211
+ Paginated response
212
+ """
213
+ paginator = Paginator(items, page_size)
214
+ return paginator.get_page(cursor)
215
+
216
+
217
+ def validate_cursor(cursor: str) -> bool:
218
+ """Validate that a cursor is properly formatted.
219
+
220
+ Args:
221
+ cursor: Cursor string to validate
222
+
223
+ Returns:
224
+ True if valid, False otherwise
225
+ """
226
+ return CursorManager.parse_cursor(cursor) is not None
@@ -1,4 +1,4 @@
1
- """Permission system for the Hanzo MCP server."""
1
+ """Permission system for the Hanzo AI server."""
2
2
 
3
3
  import json
4
4
  import os