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,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)