hanzo-mcp 0.5.1__py3-none-any.whl → 0.5.2__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 (54) hide show
  1. hanzo_mcp/__init__.py +1 -1
  2. hanzo_mcp/tools/__init__.py +135 -4
  3. hanzo_mcp/tools/common/base.py +7 -2
  4. hanzo_mcp/tools/common/stats.py +261 -0
  5. hanzo_mcp/tools/common/tool_disable.py +144 -0
  6. hanzo_mcp/tools/common/tool_enable.py +182 -0
  7. hanzo_mcp/tools/common/tool_list.py +263 -0
  8. hanzo_mcp/tools/database/__init__.py +71 -0
  9. hanzo_mcp/tools/database/database_manager.py +246 -0
  10. hanzo_mcp/tools/database/graph_add.py +257 -0
  11. hanzo_mcp/tools/database/graph_query.py +536 -0
  12. hanzo_mcp/tools/database/graph_remove.py +267 -0
  13. hanzo_mcp/tools/database/graph_search.py +348 -0
  14. hanzo_mcp/tools/database/graph_stats.py +345 -0
  15. hanzo_mcp/tools/database/sql_query.py +229 -0
  16. hanzo_mcp/tools/database/sql_search.py +296 -0
  17. hanzo_mcp/tools/database/sql_stats.py +254 -0
  18. hanzo_mcp/tools/editor/__init__.py +11 -0
  19. hanzo_mcp/tools/editor/neovim_command.py +272 -0
  20. hanzo_mcp/tools/editor/neovim_edit.py +290 -0
  21. hanzo_mcp/tools/editor/neovim_session.py +356 -0
  22. hanzo_mcp/tools/filesystem/__init__.py +15 -5
  23. hanzo_mcp/tools/filesystem/{unified_search.py → batch_search.py} +254 -131
  24. hanzo_mcp/tools/filesystem/find_files.py +348 -0
  25. hanzo_mcp/tools/filesystem/git_search.py +505 -0
  26. hanzo_mcp/tools/llm/__init__.py +27 -0
  27. hanzo_mcp/tools/llm/consensus_tool.py +351 -0
  28. hanzo_mcp/tools/llm/llm_manage.py +413 -0
  29. hanzo_mcp/tools/llm/llm_tool.py +346 -0
  30. hanzo_mcp/tools/llm/provider_tools.py +412 -0
  31. hanzo_mcp/tools/mcp/__init__.py +11 -0
  32. hanzo_mcp/tools/mcp/mcp_add.py +263 -0
  33. hanzo_mcp/tools/mcp/mcp_remove.py +127 -0
  34. hanzo_mcp/tools/mcp/mcp_stats.py +165 -0
  35. hanzo_mcp/tools/shell/__init__.py +27 -7
  36. hanzo_mcp/tools/shell/logs.py +265 -0
  37. hanzo_mcp/tools/shell/npx.py +194 -0
  38. hanzo_mcp/tools/shell/npx_background.py +254 -0
  39. hanzo_mcp/tools/shell/pkill.py +262 -0
  40. hanzo_mcp/tools/shell/processes.py +279 -0
  41. hanzo_mcp/tools/shell/run_background.py +326 -0
  42. hanzo_mcp/tools/shell/uvx.py +187 -0
  43. hanzo_mcp/tools/shell/uvx_background.py +249 -0
  44. hanzo_mcp/tools/vector/__init__.py +5 -0
  45. hanzo_mcp/tools/vector/git_ingester.py +3 -0
  46. hanzo_mcp/tools/vector/index_tool.py +358 -0
  47. hanzo_mcp/tools/vector/infinity_store.py +98 -0
  48. hanzo_mcp/tools/vector/vector_search.py +11 -6
  49. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.5.2.dist-info}/METADATA +1 -1
  50. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.5.2.dist-info}/RECORD +54 -16
  51. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.5.2.dist-info}/WHEEL +0 -0
  52. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.5.2.dist-info}/entry_points.txt +0 -0
  53. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.5.2.dist-info}/licenses/LICENSE +0 -0
  54. {hanzo_mcp-0.5.1.dist-info → hanzo_mcp-0.5.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,267 @@
1
+ """Graph remove tool for removing nodes and edges from the graph database."""
2
+
3
+ import json
4
+ from typing import Annotated, Optional, TypedDict, Unpack, final, override
5
+
6
+ from fastmcp import Context as MCPContext
7
+ from pydantic import Field
8
+
9
+ from hanzo_mcp.tools.common.base import BaseTool
10
+ from hanzo_mcp.tools.common.context import create_tool_context
11
+ from hanzo_mcp.tools.common.permissions import PermissionManager
12
+ from hanzo_mcp.tools.database.database_manager import DatabaseManager
13
+
14
+
15
+ NodeId = Annotated[
16
+ Optional[str],
17
+ Field(
18
+ description="Node ID to remove",
19
+ default=None,
20
+ ),
21
+ ]
22
+
23
+ Source = Annotated[
24
+ Optional[str],
25
+ Field(
26
+ description="Source node ID (for edge removal)",
27
+ default=None,
28
+ ),
29
+ ]
30
+
31
+ Target = Annotated[
32
+ Optional[str],
33
+ Field(
34
+ description="Target node ID (for edge removal)",
35
+ default=None,
36
+ ),
37
+ ]
38
+
39
+ Relationship = Annotated[
40
+ Optional[str],
41
+ Field(
42
+ description="Edge relationship type (for edge removal)",
43
+ default=None,
44
+ ),
45
+ ]
46
+
47
+ Cascade = Annotated[
48
+ bool,
49
+ Field(
50
+ description="Cascade delete - remove all connected edges when removing a node",
51
+ default=True,
52
+ ),
53
+ ]
54
+
55
+ ProjectPath = Annotated[
56
+ Optional[str],
57
+ Field(
58
+ description="Project path (defaults to current directory)",
59
+ default=None,
60
+ ),
61
+ ]
62
+
63
+
64
+ class GraphRemoveParams(TypedDict, total=False):
65
+ """Parameters for graph remove tool."""
66
+
67
+ node_id: Optional[str]
68
+ source: Optional[str]
69
+ target: Optional[str]
70
+ relationship: Optional[str]
71
+ cascade: bool
72
+ project_path: Optional[str]
73
+
74
+
75
+ @final
76
+ class GraphRemoveTool(BaseTool):
77
+ """Tool for removing nodes and edges from graph database."""
78
+
79
+ def __init__(self, permission_manager: PermissionManager, db_manager: DatabaseManager):
80
+ """Initialize the graph remove tool.
81
+
82
+ Args:
83
+ permission_manager: Permission manager for access control
84
+ db_manager: Database manager instance
85
+ """
86
+ self.permission_manager = permission_manager
87
+ self.db_manager = db_manager
88
+
89
+ @property
90
+ @override
91
+ def name(self) -> str:
92
+ """Get the tool name."""
93
+ return "graph_remove"
94
+
95
+ @property
96
+ @override
97
+ def description(self) -> str:
98
+ """Get the tool description."""
99
+ return """Remove nodes and edges from the project's graph database.
100
+
101
+ To remove a node:
102
+ - Provide node_id
103
+ - Use --cascade (default true) to remove connected edges
104
+ - Use --no-cascade to keep edges (may leave orphaned edges)
105
+
106
+ To remove an edge:
107
+ - Provide source, target, and relationship
108
+ - Removes only the specific edge
109
+
110
+ To remove all edges between two nodes:
111
+ - Provide source and target (no relationship)
112
+
113
+ Examples:
114
+ - graph_remove --node-id "main.py" # Remove node and its edges
115
+ - graph_remove --node-id "MyClass" --no-cascade # Remove node only
116
+ - graph_remove --source "main.py" --target "utils.py" --relationship "imports"
117
+ - graph_remove --source "func1" --target "func2" # Remove all edges
118
+ """
119
+
120
+ @override
121
+ async def call(
122
+ self,
123
+ ctx: MCPContext,
124
+ **params: Unpack[GraphRemoveParams],
125
+ ) -> str:
126
+ """Remove nodes or edges from graph.
127
+
128
+ Args:
129
+ ctx: MCP context
130
+ **params: Tool parameters
131
+
132
+ Returns:
133
+ Result of remove operation
134
+ """
135
+ tool_ctx = create_tool_context(ctx)
136
+ await tool_ctx.set_tool_info(self.name)
137
+
138
+ # Extract parameters
139
+ node_id = params.get("node_id")
140
+ source = params.get("source")
141
+ target = params.get("target")
142
+ relationship = params.get("relationship")
143
+ cascade = params.get("cascade", True)
144
+ project_path = params.get("project_path")
145
+
146
+ # Determine if removing node or edge
147
+ is_node = node_id is not None
148
+ is_edge = source is not None and target is not None
149
+
150
+ if not is_node and not is_edge:
151
+ return "Error: Must provide either node_id for a node, or (source, target) for edges"
152
+
153
+ if is_node and is_edge:
154
+ return "Error: Cannot remove both node and edge in one operation"
155
+
156
+ # Get project database
157
+ try:
158
+ if project_path:
159
+ project_db = self.db_manager.get_project_db(project_path)
160
+ else:
161
+ import os
162
+ project_db = self.db_manager.get_project_for_path(os.getcwd())
163
+
164
+ if not project_db:
165
+ return "Error: Could not find project database"
166
+
167
+ except PermissionError as e:
168
+ return str(e)
169
+ except Exception as e:
170
+ return f"Error accessing project database: {str(e)}"
171
+
172
+ # Get graph connection
173
+ graph_conn = project_db.get_graph_connection()
174
+
175
+ try:
176
+ if is_node:
177
+ # Remove node
178
+ await tool_ctx.info(f"Removing node: {node_id}")
179
+
180
+ # Check if node exists
181
+ cursor = graph_conn.cursor()
182
+ cursor.execute("SELECT id FROM nodes WHERE id = ?", (node_id,))
183
+ if not cursor.fetchone():
184
+ return f"Error: Node '{node_id}' does not exist"
185
+
186
+ if cascade:
187
+ # Count edges that will be removed
188
+ cursor.execute(
189
+ "SELECT COUNT(*) FROM edges WHERE source = ? OR target = ?",
190
+ (node_id, node_id)
191
+ )
192
+ edge_count = cursor.fetchone()[0]
193
+
194
+ # Remove connected edges
195
+ graph_conn.execute(
196
+ "DELETE FROM edges WHERE source = ? OR target = ?",
197
+ (node_id, node_id)
198
+ )
199
+
200
+ # Remove node
201
+ graph_conn.execute("DELETE FROM nodes WHERE id = ?", (node_id,))
202
+ graph_conn.commit()
203
+
204
+ # Save to disk
205
+ project_db._save_graph_to_disk()
206
+
207
+ return f"Successfully removed node '{node_id}' and {edge_count} connected edge(s)"
208
+ else:
209
+ # Remove node only
210
+ graph_conn.execute("DELETE FROM nodes WHERE id = ?", (node_id,))
211
+ graph_conn.commit()
212
+
213
+ # Save to disk
214
+ project_db._save_graph_to_disk()
215
+
216
+ return f"Successfully removed node '{node_id}' (edges preserved)"
217
+
218
+ else:
219
+ # Remove edge(s)
220
+ if relationship:
221
+ # Remove specific edge
222
+ await tool_ctx.info(f"Removing edge: {source} --[{relationship}]--> {target}")
223
+
224
+ cursor = graph_conn.cursor()
225
+ cursor.execute(
226
+ "DELETE FROM edges WHERE source = ? AND target = ? AND relationship = ?",
227
+ (source, target, relationship)
228
+ )
229
+
230
+ removed = cursor.rowcount
231
+ graph_conn.commit()
232
+
233
+ if removed == 0:
234
+ return f"No edge found: {source} --[{relationship}]--> {target}"
235
+
236
+ # Save to disk
237
+ project_db._save_graph_to_disk()
238
+
239
+ return f"Successfully removed edge: {source} --[{relationship}]--> {target}"
240
+ else:
241
+ # Remove all edges between nodes
242
+ await tool_ctx.info(f"Removing all edges between {source} and {target}")
243
+
244
+ cursor = graph_conn.cursor()
245
+ cursor.execute(
246
+ "DELETE FROM edges WHERE source = ? AND target = ?",
247
+ (source, target)
248
+ )
249
+
250
+ removed = cursor.rowcount
251
+ graph_conn.commit()
252
+
253
+ if removed == 0:
254
+ return f"No edges found between '{source}' and '{target}'"
255
+
256
+ # Save to disk
257
+ project_db._save_graph_to_disk()
258
+
259
+ return f"Successfully removed {removed} edge(s) between '{source}' and '{target}'"
260
+
261
+ except Exception as e:
262
+ await tool_ctx.error(f"Failed to remove from graph: {str(e)}")
263
+ return f"Error removing from graph: {str(e)}"
264
+
265
+ def register(self, mcp_server) -> None:
266
+ """Register this tool with the MCP server."""
267
+ pass
@@ -0,0 +1,348 @@
1
+ """Graph search tool for searching nodes and edges in the graph database."""
2
+
3
+ import json
4
+ import sqlite3
5
+ from typing import Annotated, Optional, TypedDict, Unpack, final, override
6
+
7
+ from fastmcp import Context as MCPContext
8
+ from pydantic import Field
9
+
10
+ from hanzo_mcp.tools.common.base import BaseTool
11
+ from hanzo_mcp.tools.common.context import create_tool_context
12
+ from hanzo_mcp.tools.common.permissions import PermissionManager
13
+ from hanzo_mcp.tools.database.database_manager import DatabaseManager
14
+
15
+
16
+ Pattern = Annotated[
17
+ str,
18
+ Field(
19
+ description="Search pattern (SQL LIKE syntax, % for wildcard)",
20
+ min_length=1,
21
+ ),
22
+ ]
23
+
24
+ SearchType = Annotated[
25
+ str,
26
+ Field(
27
+ description="What to search: nodes, edges, properties, all",
28
+ default="all",
29
+ ),
30
+ ]
31
+
32
+ NodeType = Annotated[
33
+ Optional[str],
34
+ Field(
35
+ description="Filter by node type",
36
+ default=None,
37
+ ),
38
+ ]
39
+
40
+ Relationship = Annotated[
41
+ Optional[str],
42
+ Field(
43
+ description="Filter by relationship type",
44
+ default=None,
45
+ ),
46
+ ]
47
+
48
+ ProjectPath = Annotated[
49
+ Optional[str],
50
+ Field(
51
+ description="Project path (defaults to current directory)",
52
+ default=None,
53
+ ),
54
+ ]
55
+
56
+ MaxResults = Annotated[
57
+ int,
58
+ Field(
59
+ description="Maximum number of results",
60
+ default=50,
61
+ ),
62
+ ]
63
+
64
+
65
+ class GraphSearchParams(TypedDict, total=False):
66
+ """Parameters for graph search tool."""
67
+
68
+ pattern: str
69
+ search_type: str
70
+ node_type: Optional[str]
71
+ relationship: Optional[str]
72
+ project_path: Optional[str]
73
+ max_results: int
74
+
75
+
76
+ @final
77
+ class GraphSearchTool(BaseTool):
78
+ """Tool for searching nodes and edges in graph database."""
79
+
80
+ def __init__(self, permission_manager: PermissionManager, db_manager: DatabaseManager):
81
+ """Initialize the graph search tool.
82
+
83
+ Args:
84
+ permission_manager: Permission manager for access control
85
+ db_manager: Database manager instance
86
+ """
87
+ self.permission_manager = permission_manager
88
+ self.db_manager = db_manager
89
+
90
+ @property
91
+ @override
92
+ def name(self) -> str:
93
+ """Get the tool name."""
94
+ return "graph_search"
95
+
96
+ @property
97
+ @override
98
+ def description(self) -> str:
99
+ """Get the tool description."""
100
+ return """Search for nodes and edges in the project's graph database.
101
+
102
+ Search types:
103
+ - nodes: Search in node IDs
104
+ - edges: Search in edge relationships
105
+ - properties: Search in node/edge properties
106
+ - all: Search everywhere (default)
107
+
108
+ Supports SQL LIKE pattern matching:
109
+ - % matches any sequence of characters
110
+ - _ matches any single character
111
+
112
+ Examples:
113
+ - graph_search --pattern "%test%" # Find anything with 'test'
114
+ - graph_search --pattern "%.py" --search-type nodes # Find Python files
115
+ - graph_search --pattern "%import%" --search-type edges
116
+ - graph_search --pattern "%TODO%" --search-type properties
117
+ - graph_search --pattern "MyClass%" --node-type "class"
118
+ """
119
+
120
+ @override
121
+ async def call(
122
+ self,
123
+ ctx: MCPContext,
124
+ **params: Unpack[GraphSearchParams],
125
+ ) -> str:
126
+ """Execute graph search.
127
+
128
+ Args:
129
+ ctx: MCP context
130
+ **params: Tool parameters
131
+
132
+ Returns:
133
+ Search results
134
+ """
135
+ tool_ctx = create_tool_context(ctx)
136
+ await tool_ctx.set_tool_info(self.name)
137
+
138
+ # Extract parameters
139
+ pattern = params.get("pattern")
140
+ if not pattern:
141
+ return "Error: pattern is required"
142
+
143
+ search_type = params.get("search_type", "all")
144
+ node_type = params.get("node_type")
145
+ relationship = params.get("relationship")
146
+ project_path = params.get("project_path")
147
+ max_results = params.get("max_results", 50)
148
+
149
+ # Validate search type
150
+ valid_types = ["nodes", "edges", "properties", "all"]
151
+ if search_type not in valid_types:
152
+ return f"Error: Invalid search_type '{search_type}'. Must be one of: {', '.join(valid_types)}"
153
+
154
+ # Get project database
155
+ try:
156
+ if project_path:
157
+ project_db = self.db_manager.get_project_db(project_path)
158
+ else:
159
+ import os
160
+ project_db = self.db_manager.get_project_for_path(os.getcwd())
161
+
162
+ if not project_db:
163
+ return "Error: Could not find project database"
164
+
165
+ except PermissionError as e:
166
+ return str(e)
167
+ except Exception as e:
168
+ return f"Error accessing project database: {str(e)}"
169
+
170
+ await tool_ctx.info(f"Searching graph for pattern: {pattern}")
171
+
172
+ # Get graph connection
173
+ graph_conn = project_db.get_graph_connection()
174
+ results = []
175
+
176
+ try:
177
+ cursor = graph_conn.cursor()
178
+
179
+ # Search nodes
180
+ if search_type in ["nodes", "all"]:
181
+ query = "SELECT id, type, properties FROM nodes WHERE id LIKE ?"
182
+ params_list = [pattern]
183
+
184
+ if node_type:
185
+ query += " AND type = ?"
186
+ params_list.append(node_type)
187
+
188
+ if search_type == "nodes":
189
+ query += f" LIMIT {max_results}"
190
+
191
+ cursor.execute(query, params_list)
192
+
193
+ for row in cursor.fetchall():
194
+ results.append({
195
+ "type": "node",
196
+ "id": row[0],
197
+ "node_type": row[1],
198
+ "properties": json.loads(row[2]) if row[2] else {},
199
+ "match_field": "id"
200
+ })
201
+
202
+ # Search edges
203
+ if search_type in ["edges", "all"]:
204
+ query = """SELECT source, target, relationship, weight, properties
205
+ FROM edges WHERE relationship LIKE ?"""
206
+ params_list = [pattern]
207
+
208
+ if relationship:
209
+ query += " AND relationship = ?"
210
+ params_list.append(relationship)
211
+
212
+ if search_type == "edges":
213
+ query += f" LIMIT {max_results}"
214
+
215
+ cursor.execute(query, params_list)
216
+
217
+ for row in cursor.fetchall():
218
+ results.append({
219
+ "type": "edge",
220
+ "source": row[0],
221
+ "target": row[1],
222
+ "relationship": row[2],
223
+ "weight": row[3],
224
+ "properties": json.loads(row[4]) if row[4] else {},
225
+ "match_field": "relationship"
226
+ })
227
+
228
+ # Search in properties
229
+ if search_type in ["properties", "all"]:
230
+ # Search node properties
231
+ query = """SELECT id, type, properties FROM nodes
232
+ WHERE properties IS NOT NULL AND properties LIKE ?"""
233
+ params_list = [f"%{pattern}%"]
234
+
235
+ if node_type:
236
+ query += " AND type = ?"
237
+ params_list.append(node_type)
238
+
239
+ cursor.execute(query, params_list)
240
+
241
+ for row in cursor.fetchall():
242
+ props = json.loads(row[2]) if row[2] else {}
243
+ # Check which property matches
244
+ matching_props = {}
245
+ for key, value in props.items():
246
+ if pattern.replace('%', '').lower() in str(value).lower():
247
+ matching_props[key] = value
248
+
249
+ if matching_props:
250
+ results.append({
251
+ "type": "node",
252
+ "id": row[0],
253
+ "node_type": row[1],
254
+ "properties": props,
255
+ "match_field": "properties",
256
+ "matching_properties": matching_props
257
+ })
258
+
259
+ # Search edge properties
260
+ query = """SELECT source, target, relationship, weight, properties
261
+ FROM edges WHERE properties IS NOT NULL AND properties LIKE ?"""
262
+ params_list = [f"%{pattern}%"]
263
+
264
+ if relationship:
265
+ query += " AND relationship = ?"
266
+ params_list.append(relationship)
267
+
268
+ cursor.execute(query, params_list)
269
+
270
+ for row in cursor.fetchall():
271
+ props = json.loads(row[4]) if row[4] else {}
272
+ # Check which property matches
273
+ matching_props = {}
274
+ for key, value in props.items():
275
+ if pattern.replace('%', '').lower() in str(value).lower():
276
+ matching_props[key] = value
277
+
278
+ if matching_props:
279
+ results.append({
280
+ "type": "edge",
281
+ "source": row[0],
282
+ "target": row[1],
283
+ "relationship": row[2],
284
+ "weight": row[3],
285
+ "properties": props,
286
+ "match_field": "properties",
287
+ "matching_properties": matching_props
288
+ })
289
+
290
+ # Limit total results if searching all
291
+ if search_type == "all" and len(results) > max_results:
292
+ results = results[:max_results]
293
+
294
+ if not results:
295
+ return f"No results found for pattern '{pattern}'"
296
+
297
+ # Format results
298
+ output = [f"Found {len(results)} result(s) for pattern '{pattern}':\n"]
299
+
300
+ # Group by type
301
+ nodes = [r for r in results if r["type"] == "node"]
302
+ edges = [r for r in results if r["type"] == "edge"]
303
+
304
+ if nodes:
305
+ output.append(f"Nodes ({len(nodes)}):")
306
+ for node in nodes[:20]: # Show first 20
307
+ output.append(f" {node['id']} ({node['node_type']})")
308
+ if node["match_field"] == "properties" and "matching_properties" in node:
309
+ output.append(f" Matched in: {list(node['matching_properties'].keys())}")
310
+ if node["properties"] and node["match_field"] != "properties":
311
+ props_str = json.dumps(node["properties"], indent=6)[:100]
312
+ if len(props_str) == 100:
313
+ props_str += "..."
314
+ output.append(f" Properties: {props_str}")
315
+
316
+ if len(nodes) > 20:
317
+ output.append(f" ... and {len(nodes) - 20} more nodes")
318
+ output.append("")
319
+
320
+ if edges:
321
+ output.append(f"Edges ({len(edges)}):")
322
+ for edge in edges[:20]: # Show first 20
323
+ output.append(f" {edge['source']} --[{edge['relationship']}]--> {edge['target']}")
324
+ if edge["match_field"] == "properties" and "matching_properties" in edge:
325
+ output.append(f" Matched in: {list(edge['matching_properties'].keys())}")
326
+ if edge["weight"] != 1.0:
327
+ output.append(f" Weight: {edge['weight']}")
328
+ if edge["properties"]:
329
+ props_str = json.dumps(edge["properties"], indent=6)[:100]
330
+ if len(props_str) == 100:
331
+ props_str += "..."
332
+ output.append(f" Properties: {props_str}")
333
+
334
+ if len(edges) > 20:
335
+ output.append(f" ... and {len(edges) - 20} more edges")
336
+
337
+ return "\n".join(output)
338
+
339
+ except sqlite3.Error as e:
340
+ await tool_ctx.error(f"SQL error: {str(e)}")
341
+ return f"SQL error: {str(e)}"
342
+ except Exception as e:
343
+ await tool_ctx.error(f"Unexpected error: {str(e)}")
344
+ return f"Error executing search: {str(e)}"
345
+
346
+ def register(self, mcp_server) -> None:
347
+ """Register this tool with the MCP server."""
348
+ pass