hanzo-mcp 0.3.8__py3-none-any.whl → 0.5.1__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 (93) hide show
  1. hanzo_mcp/__init__.py +1 -1
  2. hanzo_mcp/cli.py +118 -170
  3. hanzo_mcp/cli_enhanced.py +438 -0
  4. hanzo_mcp/config/__init__.py +19 -0
  5. hanzo_mcp/config/settings.py +449 -0
  6. hanzo_mcp/config/tool_config.py +197 -0
  7. hanzo_mcp/prompts/__init__.py +117 -0
  8. hanzo_mcp/prompts/compact_conversation.py +77 -0
  9. hanzo_mcp/prompts/create_release.py +38 -0
  10. hanzo_mcp/prompts/project_system.py +120 -0
  11. hanzo_mcp/prompts/project_todo_reminder.py +111 -0
  12. hanzo_mcp/prompts/utils.py +286 -0
  13. hanzo_mcp/server.py +117 -99
  14. hanzo_mcp/tools/__init__.py +121 -33
  15. hanzo_mcp/tools/agent/__init__.py +8 -11
  16. hanzo_mcp/tools/agent/agent_tool.py +290 -224
  17. hanzo_mcp/tools/agent/prompt.py +16 -13
  18. hanzo_mcp/tools/agent/tool_adapter.py +9 -9
  19. hanzo_mcp/tools/common/__init__.py +17 -16
  20. hanzo_mcp/tools/common/base.py +79 -110
  21. hanzo_mcp/tools/common/batch_tool.py +330 -0
  22. hanzo_mcp/tools/common/config_tool.py +396 -0
  23. hanzo_mcp/tools/common/context.py +26 -292
  24. hanzo_mcp/tools/common/permissions.py +12 -12
  25. hanzo_mcp/tools/common/thinking_tool.py +153 -0
  26. hanzo_mcp/tools/common/validation.py +1 -63
  27. hanzo_mcp/tools/filesystem/__init__.py +97 -57
  28. hanzo_mcp/tools/filesystem/base.py +32 -24
  29. hanzo_mcp/tools/filesystem/content_replace.py +114 -107
  30. hanzo_mcp/tools/filesystem/directory_tree.py +129 -105
  31. hanzo_mcp/tools/filesystem/edit.py +279 -0
  32. hanzo_mcp/tools/filesystem/grep.py +458 -0
  33. hanzo_mcp/tools/filesystem/grep_ast_tool.py +250 -0
  34. hanzo_mcp/tools/filesystem/multi_edit.py +362 -0
  35. hanzo_mcp/tools/filesystem/read.py +255 -0
  36. hanzo_mcp/tools/filesystem/unified_search.py +689 -0
  37. hanzo_mcp/tools/filesystem/write.py +156 -0
  38. hanzo_mcp/tools/jupyter/__init__.py +41 -29
  39. hanzo_mcp/tools/jupyter/base.py +66 -57
  40. hanzo_mcp/tools/jupyter/{edit_notebook.py → notebook_edit.py} +162 -139
  41. hanzo_mcp/tools/jupyter/notebook_read.py +152 -0
  42. hanzo_mcp/tools/shell/__init__.py +29 -20
  43. hanzo_mcp/tools/shell/base.py +87 -45
  44. hanzo_mcp/tools/shell/bash_session.py +731 -0
  45. hanzo_mcp/tools/shell/bash_session_executor.py +295 -0
  46. hanzo_mcp/tools/shell/command_executor.py +435 -384
  47. hanzo_mcp/tools/shell/run_command.py +284 -131
  48. hanzo_mcp/tools/shell/run_command_windows.py +328 -0
  49. hanzo_mcp/tools/shell/session_manager.py +196 -0
  50. hanzo_mcp/tools/shell/session_storage.py +325 -0
  51. hanzo_mcp/tools/todo/__init__.py +66 -0
  52. hanzo_mcp/tools/todo/base.py +319 -0
  53. hanzo_mcp/tools/todo/todo_read.py +148 -0
  54. hanzo_mcp/tools/todo/todo_write.py +378 -0
  55. hanzo_mcp/tools/vector/__init__.py +99 -0
  56. hanzo_mcp/tools/vector/ast_analyzer.py +459 -0
  57. hanzo_mcp/tools/vector/git_ingester.py +482 -0
  58. hanzo_mcp/tools/vector/infinity_store.py +731 -0
  59. hanzo_mcp/tools/vector/mock_infinity.py +162 -0
  60. hanzo_mcp/tools/vector/project_manager.py +361 -0
  61. hanzo_mcp/tools/vector/vector_index.py +116 -0
  62. hanzo_mcp/tools/vector/vector_search.py +225 -0
  63. hanzo_mcp-0.5.1.dist-info/METADATA +276 -0
  64. hanzo_mcp-0.5.1.dist-info/RECORD +68 -0
  65. {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.1.dist-info}/WHEEL +1 -1
  66. hanzo_mcp/tools/agent/base_provider.py +0 -73
  67. hanzo_mcp/tools/agent/litellm_provider.py +0 -45
  68. hanzo_mcp/tools/agent/lmstudio_agent.py +0 -385
  69. hanzo_mcp/tools/agent/lmstudio_provider.py +0 -219
  70. hanzo_mcp/tools/agent/provider_registry.py +0 -120
  71. hanzo_mcp/tools/common/error_handling.py +0 -86
  72. hanzo_mcp/tools/common/logging_config.py +0 -115
  73. hanzo_mcp/tools/common/session.py +0 -91
  74. hanzo_mcp/tools/common/think_tool.py +0 -123
  75. hanzo_mcp/tools/common/version_tool.py +0 -120
  76. hanzo_mcp/tools/filesystem/edit_file.py +0 -287
  77. hanzo_mcp/tools/filesystem/get_file_info.py +0 -170
  78. hanzo_mcp/tools/filesystem/read_files.py +0 -199
  79. hanzo_mcp/tools/filesystem/search_content.py +0 -275
  80. hanzo_mcp/tools/filesystem/write_file.py +0 -162
  81. hanzo_mcp/tools/jupyter/notebook_operations.py +0 -514
  82. hanzo_mcp/tools/jupyter/read_notebook.py +0 -165
  83. hanzo_mcp/tools/project/__init__.py +0 -64
  84. hanzo_mcp/tools/project/analysis.py +0 -886
  85. hanzo_mcp/tools/project/base.py +0 -66
  86. hanzo_mcp/tools/project/project_analyze.py +0 -173
  87. hanzo_mcp/tools/shell/run_script.py +0 -215
  88. hanzo_mcp/tools/shell/script_tool.py +0 -244
  89. hanzo_mcp-0.3.8.dist-info/METADATA +0 -196
  90. hanzo_mcp-0.3.8.dist-info/RECORD +0 -53
  91. {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.1.dist-info}/entry_points.txt +0 -0
  92. {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.1.dist-info}/licenses/LICENSE +0 -0
  93. {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.1.dist-info}/top_level.txt +0 -0
@@ -11,7 +11,7 @@ from collections.abc import Iterable
11
11
  from pathlib import Path
12
12
  from typing import Any, ClassVar, final
13
13
 
14
- from mcp.server.fastmcp import Context as MCPContext
14
+ from fastmcp import Context as MCPContext
15
15
  from mcp.server.lowlevel.helper_types import ReadResourceContents
16
16
 
17
17
 
@@ -87,7 +87,11 @@ class ToolContext:
87
87
  Args:
88
88
  message: The message to log
89
89
  """
90
- await self._mcp_context.info(self._format_message(message))
90
+ try:
91
+ await self._mcp_context.info(self._format_message(message))
92
+ except Exception:
93
+ # Silently ignore errors when client has disconnected
94
+ pass
91
95
 
92
96
  async def debug(self, message: str) -> None:
93
97
  """Log a debug message.
@@ -95,7 +99,11 @@ class ToolContext:
95
99
  Args:
96
100
  message: The message to log
97
101
  """
98
- await self._mcp_context.debug(self._format_message(message))
102
+ try:
103
+ await self._mcp_context.debug(self._format_message(message))
104
+ except Exception:
105
+ # Silently ignore errors when client has disconnected
106
+ pass
99
107
 
100
108
  async def warning(self, message: str) -> None:
101
109
  """Log a warning message.
@@ -103,7 +111,11 @@ class ToolContext:
103
111
  Args:
104
112
  message: The message to log
105
113
  """
106
- await self._mcp_context.warning(self._format_message(message))
114
+ try:
115
+ await self._mcp_context.warning(self._format_message(message))
116
+ except Exception:
117
+ # Silently ignore errors when client has disconnected
118
+ pass
107
119
 
108
120
  async def error(self, message: str) -> None:
109
121
  """Log an error message.
@@ -111,7 +123,11 @@ class ToolContext:
111
123
  Args:
112
124
  message: The message to log
113
125
  """
114
- await self._mcp_context.error(self._format_message(message))
126
+ try:
127
+ await self._mcp_context.error(self._format_message(message))
128
+ except Exception:
129
+ # Silently ignore errors when client has disconnected
130
+ pass
115
131
 
116
132
  def _format_message(self, message: str) -> str:
117
133
  """Format a message with tool information if available.
@@ -135,7 +151,11 @@ class ToolContext:
135
151
  current: Current progress value
136
152
  total: Total progress value
137
153
  """
138
- await self._mcp_context.report_progress(current, total)
154
+ try:
155
+ await self._mcp_context.report_progress(current, total)
156
+ except Exception:
157
+ # Silently ignore errors when client has disconnected
158
+ pass
139
159
 
140
160
  async def read_resource(self, uri: str) -> Iterable[ReadResourceContents]:
141
161
  """Read a resource via the MCP protocol.
@@ -160,289 +180,3 @@ def create_tool_context(mcp_context: MCPContext) -> ToolContext:
160
180
  A new ToolContext
161
181
  """
162
182
  return ToolContext(mcp_context)
163
-
164
-
165
- @final
166
- class DocumentContext:
167
- """Manages document context and codebase understanding."""
168
-
169
- def __init__(self) -> None:
170
- """Initialize the document context."""
171
- self.documents: dict[str, str] = {}
172
- self.document_metadata: dict[str, dict[str, Any]] = {}
173
- self.modified_times: dict[str, float] = {}
174
- self.allowed_paths: set[Path] = set()
175
-
176
- def add_allowed_path(self, path: str) -> None:
177
- """Add a path to the allowed paths.
178
-
179
- Args:
180
- path: The path to allow
181
- """
182
- # Expand user path (e.g., ~/ or $HOME)
183
- expanded_path = os.path.expanduser(path)
184
- resolved_path: Path = Path(expanded_path).resolve()
185
- self.allowed_paths.add(resolved_path)
186
-
187
- def is_path_allowed(self, path: str) -> bool:
188
- """Check if a path is allowed.
189
-
190
- Args:
191
- path: The path to check
192
-
193
- Returns:
194
- True if the path is allowed, False otherwise
195
- """
196
- # Expand user path (e.g., ~/ or $HOME)
197
- expanded_path = os.path.expanduser(path)
198
- resolved_path: Path = Path(expanded_path).resolve()
199
-
200
- # Check if the path is within any allowed path
201
- for allowed_path in self.allowed_paths:
202
- try:
203
- _ = resolved_path.relative_to(allowed_path)
204
- return True
205
- except ValueError:
206
- continue
207
-
208
- return False
209
-
210
- def add_document(
211
- self, path: str, content: str, metadata: dict[str, Any] | None = None
212
- ) -> None:
213
- """Add a document to the context.
214
-
215
- Args:
216
- path: The path of the document
217
- content: The content of the document
218
- metadata: Optional metadata about the document
219
- """
220
- self.documents[path] = content
221
- self.modified_times[path] = time.time()
222
-
223
- if metadata:
224
- self.document_metadata[path] = metadata
225
- else:
226
- # Try to infer metadata
227
- self.document_metadata[path] = self._infer_metadata(path, content)
228
-
229
- def get_document(self, path: str) -> str | None:
230
- """Get a document from the context.
231
-
232
- Args:
233
- path: The path of the document
234
-
235
- Returns:
236
- The document content, or None if not found
237
- """
238
- return self.documents.get(path)
239
-
240
- def get_document_metadata(self, path: str) -> dict[str, Any] | None:
241
- """Get document metadata.
242
-
243
- Args:
244
- path: The path of the document
245
-
246
- Returns:
247
- The document metadata, or None if not found
248
- """
249
- return self.document_metadata.get(path)
250
-
251
- def update_document(self, path: str, content: str) -> None:
252
- """Update a document in the context.
253
-
254
- Args:
255
- path: The path of the document
256
- content: The new content of the document
257
- """
258
- self.documents[path] = content
259
- self.modified_times[path] = time.time()
260
-
261
- # Update metadata
262
- self.document_metadata[path] = self._infer_metadata(path, content)
263
-
264
- def remove_document(self, path: str) -> None:
265
- """Remove a document from the context.
266
-
267
- Args:
268
- path: The path of the document
269
- """
270
- if path in self.documents:
271
- del self.documents[path]
272
-
273
- if path in self.document_metadata:
274
- del self.document_metadata[path]
275
-
276
- if path in self.modified_times:
277
- del self.modified_times[path]
278
-
279
- def _infer_metadata(self, path: str, content: str) -> dict[str, Any]:
280
- """Infer metadata about a document.
281
-
282
- Args:
283
- path: The path of the document
284
- content: The content of the document
285
-
286
- Returns:
287
- Inferred metadata
288
- """
289
- extension: str = Path(path).suffix.lower()
290
-
291
- metadata: dict[str, Any] = {
292
- "extension": extension,
293
- "size": len(content),
294
- "line_count": content.count("\n") + 1,
295
- }
296
-
297
- # Infer language based on extension
298
- language_map: dict[str, list[str]] = {
299
- "python": [".py"],
300
- "javascript": [".js", ".jsx"],
301
- "typescript": [".ts", ".tsx"],
302
- "java": [".java"],
303
- "c++": [".c", ".cpp", ".h", ".hpp"],
304
- "go": [".go"],
305
- "rust": [".rs"],
306
- "ruby": [".rb"],
307
- "php": [".php"],
308
- "html": [".html", ".htm"],
309
- "css": [".css"],
310
- "markdown": [".md"],
311
- "json": [".json"],
312
- "yaml": [".yaml", ".yml"],
313
- "xml": [".xml"],
314
- "sql": [".sql"],
315
- "shell": [".sh", ".bash"],
316
- }
317
-
318
- # Find matching language
319
- for language, extensions in language_map.items():
320
- if extension in extensions:
321
- metadata["language"] = language
322
- break
323
- else:
324
- metadata["language"] = "text"
325
-
326
- return metadata
327
-
328
- def load_directory(
329
- self,
330
- directory: str,
331
- recursive: bool = True,
332
- exclude_patterns: list[str] | None = None,
333
- ) -> None:
334
- """Load all files in a directory into the context.
335
-
336
- Args:
337
- directory: The directory to load
338
- recursive: Whether to load subdirectories
339
- exclude_patterns: Patterns to exclude
340
- """
341
- if not self.is_path_allowed(directory):
342
- raise ValueError(f"Directory not allowed: {directory}")
343
-
344
- dir_path: Path = Path(directory)
345
-
346
- if not dir_path.exists() or not dir_path.is_dir():
347
- raise ValueError(f"Not a valid directory: {directory}")
348
-
349
- if exclude_patterns is None:
350
- exclude_patterns = []
351
-
352
- # Common directories and files to exclude
353
- default_excludes: list[str] = [
354
- "__pycache__",
355
- ".git",
356
- ".github",
357
- ".ssh",
358
- ".gnupg",
359
- ".config",
360
- "node_modules",
361
- "__pycache__",
362
- ".venv",
363
- "venv",
364
- "env",
365
- ".idea",
366
- ".vscode",
367
- ".DS_Store",
368
- ]
369
-
370
- exclude_patterns.extend(default_excludes)
371
-
372
- def should_exclude(path: Path) -> bool:
373
- """Check if a path should be excluded.
374
-
375
- Args:
376
- path: The path to check
377
-
378
- Returns:
379
- True if the path should be excluded, False otherwise
380
- """
381
- for pattern in exclude_patterns:
382
- if pattern.startswith("*"):
383
- if path.name.endswith(pattern[1:]):
384
- return True
385
- elif pattern in str(path):
386
- return True
387
- return False
388
-
389
- # Walk the directory
390
- for root, dirs, files in os.walk(dir_path):
391
- # Skip excluded directories
392
- dirs[:] = [d for d in dirs if not should_exclude(Path(root) / d)]
393
-
394
- # Process files
395
- for file in files:
396
- file_path: Path = Path(root) / file
397
-
398
- if should_exclude(file_path):
399
- continue
400
-
401
- try:
402
- with open(file_path, "r", encoding="utf-8") as f:
403
- content: str = f.read()
404
-
405
- # Add to context
406
- self.add_document(str(file_path), content)
407
- except UnicodeDecodeError:
408
- # Skip binary files
409
- continue
410
-
411
- # Stop if not recursive
412
- if not recursive:
413
- break
414
-
415
- def to_json(self) -> str:
416
- """Convert the context to a JSON string.
417
-
418
- Returns:
419
- A JSON string representation of the context
420
- """
421
- data: dict[str, Any] = {
422
- "documents": self.documents,
423
- "metadata": self.document_metadata,
424
- "modified_times": self.modified_times,
425
- "allowed_paths": [str(p) for p in self.allowed_paths],
426
- }
427
-
428
- return json.dumps(data)
429
-
430
- @classmethod
431
- def from_json(cls, json_str: str) -> "DocumentContext":
432
- """Create a context from a JSON string.
433
-
434
- Args:
435
- json_str: The JSON string
436
-
437
- Returns:
438
- A new DocumentContext instance
439
- """
440
- data: dict[str, Any] = json.loads(json_str)
441
-
442
- context = cls()
443
- context.documents = data.get("documents", {})
444
- context.document_metadata = data.get("metadata", {})
445
- context.modified_times = data.get("modified_times", {})
446
- context.allowed_paths = set(Path(p) for p in data.get("allowed_paths", []))
447
-
448
- return context
@@ -2,6 +2,8 @@
2
2
 
3
3
  import json
4
4
  import os
5
+ import sys
6
+ import tempfile
5
7
  from collections.abc import Awaitable, Callable
6
8
  from pathlib import Path
7
9
  from typing import Any, TypeVar, final
@@ -18,9 +20,14 @@ class PermissionManager:
18
20
  def __init__(self) -> None:
19
21
  """Initialize the permission manager."""
20
22
  # Allowed paths
21
- self.allowed_paths: set[Path] = set(
22
- [Path("/tmp").resolve(), Path("/var").resolve()]
23
- )
23
+ self.allowed_paths: set[Path] = set()
24
+
25
+ # Allowed paths based on platform
26
+ if sys.platform == "win32": # Windows
27
+ self.allowed_paths.add(Path(tempfile.gettempdir()).resolve())
28
+ else: # Unix/Linux/Mac
29
+ self.allowed_paths.add(Path("/tmp").resolve())
30
+ self.allowed_paths.add(Path("/var").resolve())
24
31
 
25
32
  # Excluded paths
26
33
  self.excluded_paths: set[Path] = set()
@@ -33,17 +40,14 @@ class PermissionManager:
33
40
  """Add default exclusions for sensitive files and directories."""
34
41
  # Sensitive directories
35
42
  sensitive_dirs: list[str] = [
36
- # ".git" is now allowed by default
37
43
  ".ssh",
38
44
  ".gnupg",
39
- ".config",
40
45
  "node_modules",
41
46
  "__pycache__",
42
47
  ".venv",
43
48
  "venv",
44
49
  "env",
45
50
  ".idea",
46
- ".vscode",
47
51
  ".DS_Store",
48
52
  ]
49
53
  self.excluded_patterns.extend(sensitive_dirs)
@@ -69,9 +73,7 @@ class PermissionManager:
69
73
  Args:
70
74
  path: The path to allow
71
75
  """
72
- # Expand user path (e.g., ~/ or $HOME)
73
- expanded_path = os.path.expanduser(path)
74
- resolved_path: Path = Path(expanded_path).resolve()
76
+ resolved_path: Path = Path(path).resolve()
75
77
  self.allowed_paths.add(resolved_path)
76
78
 
77
79
  def remove_allowed_path(self, path: str) -> None:
@@ -110,9 +112,7 @@ class PermissionManager:
110
112
  Returns:
111
113
  True if the path is allowed, False otherwise
112
114
  """
113
- # Expand user path (e.g., ~/ or $HOME)
114
- expanded_path = os.path.expanduser(path)
115
- resolved_path: Path = Path(expanded_path).resolve()
115
+ resolved_path: Path = Path(path).resolve()
116
116
 
117
117
  # Check exclusions first
118
118
  if self._is_path_excluded(resolved_path):
@@ -0,0 +1,153 @@
1
+ """Thinking tool implementation.
2
+
3
+ This module provides the ThinkingTool for Claude to engage in structured thinking.
4
+ """
5
+
6
+ from typing import Annotated, TypedDict, Unpack, final, override
7
+
8
+ from fastmcp import Context as MCPContext
9
+ from fastmcp import FastMCP
10
+ from fastmcp.server.dependencies import get_context
11
+ from pydantic import Field
12
+
13
+ from hanzo_mcp.tools.common.base import BaseTool
14
+ from hanzo_mcp.tools.common.context import create_tool_context
15
+
16
+
17
+ Thought = Annotated[
18
+ str,
19
+ Field(
20
+ description="The detailed thought process to record",
21
+ min_length=1,
22
+ ),
23
+ ]
24
+
25
+
26
+ class ThinkingToolParams(TypedDict):
27
+ """Parameters for the ThinkingTool.
28
+
29
+ Attributes:
30
+ thought: The detailed thought process to record
31
+ """
32
+
33
+ thought: Thought
34
+
35
+
36
+ @final
37
+ class ThinkingTool(BaseTool):
38
+ """Tool for Claude to engage in structured thinking."""
39
+
40
+ @property
41
+ @override
42
+ def name(self) -> str:
43
+ """Get the tool name.
44
+
45
+ Returns:
46
+ Tool name
47
+ """
48
+ return "think"
49
+
50
+ @property
51
+ @override
52
+ def description(self) -> str:
53
+ """Get the tool description.
54
+
55
+ Returns:
56
+ Tool description
57
+ """
58
+ return """Use the tool to think about something. It will not obtain new information or make any changes to the repository, but just log the thought. Use it when complex reasoning or brainstorming is needed.
59
+ Ensure thinking content is concise and accurate, without needing to include code details
60
+
61
+ Common use cases:
62
+ 1. When exploring a repository and discovering the source of a bug, call this tool to brainstorm several unique ways of fixing the bug, and assess which change(s) are likely to be simplest and most effective
63
+ 2. After receiving test results, use this tool to brainstorm ways to fix failing tests
64
+ 3. When planning a complex refactoring, use this tool to outline different approaches and their tradeoffs
65
+ 4. When designing a new feature, use this tool to think through architecture decisions and implementation details
66
+ 5. When debugging a complex issue, use this tool to organize your thoughts and hypotheses
67
+ 6. When considering changes to the plan or shifts in thinking that the user has not previously mentioned, consider whether it is necessary to confirm with the user.
68
+
69
+ <think_example>
70
+ Feature Implementation Planning
71
+ - New code search feature requirements:
72
+ * Search for code patterns across multiple files
73
+ * Identify function usages and references
74
+ * Analyze import relationships
75
+ * Generate summary of matching patterns
76
+ - Implementation considerations:
77
+ * Need to leverage existing search mechanisms
78
+ * Should use regex for pattern matching
79
+ * Results need consistent format with other search methods
80
+ * Must handle large codebases efficiently
81
+ - Design approach:
82
+ 1. Create new CodeSearcher class that follows existing search patterns
83
+ 2. Implement core pattern matching algorithm
84
+ 3. Add result formatting methods
85
+ 4. Integrate with file traversal system
86
+ 5. Add caching for performance optimization
87
+ - Testing strategy:
88
+ * Unit tests for search accuracy
89
+ * Integration tests with existing components
90
+ * Performance tests with large codebases
91
+ </think_example>"""
92
+
93
+ def __init__(self) -> None:
94
+ """Initialize the thinking tool."""
95
+ pass
96
+
97
+ @override
98
+ async def call(
99
+ self,
100
+ ctx: MCPContext,
101
+ **params: Unpack[ThinkingToolParams],
102
+ ) -> str:
103
+ """Execute the tool with the given parameters.
104
+
105
+ Args:
106
+ ctx: MCP context
107
+ **params: Tool parameters
108
+
109
+ Returns:
110
+ Tool result
111
+ """
112
+ tool_ctx = create_tool_context(ctx)
113
+ tool_ctx.set_tool_info(self.name)
114
+
115
+ # Extract parameters
116
+ thought = params.get("thought")
117
+
118
+ # Validate required thought parameter
119
+ if not thought:
120
+ await tool_ctx.error(
121
+ "Parameter 'thought' is required but was None or empty"
122
+ )
123
+ return "Error: Parameter 'thought' is required but was None or empty"
124
+
125
+ if thought.strip() == "":
126
+ await tool_ctx.error("Parameter 'thought' cannot be empty")
127
+ return "Error: Parameter 'thought' cannot be empty"
128
+
129
+ # Log the thought but don't take action
130
+ await tool_ctx.info("Thinking process recorded")
131
+
132
+ # Return confirmation
133
+ return "I've recorded your thinking process. You can continue with your next action based on this analysis."
134
+
135
+ @override
136
+ def register(self, mcp_server: FastMCP) -> None:
137
+ """Register this thinking tool with the MCP server.
138
+
139
+ Creates a wrapper function with explicitly defined parameters that match
140
+ the tool's parameter schema and registers it with the MCP server.
141
+
142
+ Args:
143
+ mcp_server: The FastMCP server instance
144
+ """
145
+ tool_self = self # Create a reference to self for use in the closure
146
+
147
+ @mcp_server.tool(name=self.name, description=self.description)
148
+ async def think(
149
+ ctx: MCPContext,
150
+ thought: Thought,
151
+ ) -> str:
152
+ ctx = get_context()
153
+ return await tool_self.call(ctx, thought=thought)
@@ -3,7 +3,7 @@
3
3
  This module provides utilities for validating parameters in tool functions.
4
4
  """
5
5
 
6
- from typing import Any, TypeVar, final
6
+ from typing import TypeVar, final
7
7
 
8
8
  T = TypeVar("T")
9
9
 
@@ -32,48 +32,6 @@ class ValidationResult:
32
32
  return not self.is_valid
33
33
 
34
34
 
35
- def validate_parameter(
36
- parameter: Any, parameter_name: str, allow_empty: bool = False
37
- ) -> ValidationResult:
38
- """Validate a single parameter.
39
-
40
- Args:
41
- parameter: The parameter value to validate
42
- parameter_name: The name of the parameter (for error messages)
43
- allow_empty: Whether to allow empty strings, lists, etc.
44
-
45
- Returns:
46
- A ValidationResult indicating whether the parameter is valid
47
- """
48
- # Check for None
49
- if parameter is None:
50
- return ValidationResult(
51
- is_valid=False,
52
- error_message=f"Parameter '{parameter_name}' is required but was None",
53
- )
54
-
55
- # Check for empty strings
56
- if isinstance(parameter, str) and not allow_empty and parameter.strip() == "":
57
- return ValidationResult(
58
- is_valid=False,
59
- error_message=f"Parameter '{parameter_name}' is required but was empty string",
60
- )
61
-
62
- # Check for empty collections
63
- if (
64
- isinstance(parameter, (list, tuple, dict, set))
65
- and not allow_empty
66
- and len(parameter) == 0
67
- ):
68
- return ValidationResult(
69
- is_valid=False,
70
- error_message=f"Parameter '{parameter_name}' is required but was empty {type(parameter).__name__}",
71
- )
72
-
73
- # Parameter is valid
74
- return ValidationResult(is_valid=True)
75
-
76
-
77
35
  def validate_path_parameter(
78
36
  path: str | None, parameter_name: str = "path"
79
37
  ) -> ValidationResult:
@@ -102,23 +60,3 @@ def validate_path_parameter(
102
60
 
103
61
  # Path is valid
104
62
  return ValidationResult(is_valid=True)
105
-
106
-
107
- def validate_parameters(**kwargs: Any) -> ValidationResult:
108
- """Validate multiple parameters.
109
-
110
- Accepts keyword arguments where the key is the parameter name and the value is the parameter value.
111
-
112
- Args:
113
- **kwargs: Parameters to validate as name=value pairs
114
-
115
- Returns:
116
- A ValidationResult for the first invalid parameter, or a valid result if all are valid
117
- """
118
- for name, value in kwargs.items():
119
- result = validate_parameter(value, name)
120
- if result.is_error:
121
- return result
122
-
123
- # All parameters are valid
124
- return ValidationResult(is_valid=True)