hanzo-mcp 0.5.0__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.
- hanzo_mcp/__init__.py +1 -1
- hanzo_mcp/config/settings.py +61 -0
- hanzo_mcp/tools/__init__.py +158 -12
- hanzo_mcp/tools/common/base.py +7 -2
- hanzo_mcp/tools/common/config_tool.py +396 -0
- hanzo_mcp/tools/common/stats.py +261 -0
- hanzo_mcp/tools/common/tool_disable.py +144 -0
- hanzo_mcp/tools/common/tool_enable.py +182 -0
- hanzo_mcp/tools/common/tool_list.py +263 -0
- hanzo_mcp/tools/database/__init__.py +71 -0
- hanzo_mcp/tools/database/database_manager.py +246 -0
- hanzo_mcp/tools/database/graph_add.py +257 -0
- hanzo_mcp/tools/database/graph_query.py +536 -0
- hanzo_mcp/tools/database/graph_remove.py +267 -0
- hanzo_mcp/tools/database/graph_search.py +348 -0
- hanzo_mcp/tools/database/graph_stats.py +345 -0
- hanzo_mcp/tools/database/sql_query.py +229 -0
- hanzo_mcp/tools/database/sql_search.py +296 -0
- hanzo_mcp/tools/database/sql_stats.py +254 -0
- hanzo_mcp/tools/editor/__init__.py +11 -0
- hanzo_mcp/tools/editor/neovim_command.py +272 -0
- hanzo_mcp/tools/editor/neovim_edit.py +290 -0
- hanzo_mcp/tools/editor/neovim_session.py +356 -0
- hanzo_mcp/tools/filesystem/__init__.py +20 -1
- hanzo_mcp/tools/filesystem/batch_search.py +812 -0
- hanzo_mcp/tools/filesystem/find_files.py +348 -0
- hanzo_mcp/tools/filesystem/git_search.py +505 -0
- hanzo_mcp/tools/llm/__init__.py +27 -0
- hanzo_mcp/tools/llm/consensus_tool.py +351 -0
- hanzo_mcp/tools/llm/llm_manage.py +413 -0
- hanzo_mcp/tools/llm/llm_tool.py +346 -0
- hanzo_mcp/tools/llm/provider_tools.py +412 -0
- hanzo_mcp/tools/mcp/__init__.py +11 -0
- hanzo_mcp/tools/mcp/mcp_add.py +263 -0
- hanzo_mcp/tools/mcp/mcp_remove.py +127 -0
- hanzo_mcp/tools/mcp/mcp_stats.py +165 -0
- hanzo_mcp/tools/shell/__init__.py +27 -7
- hanzo_mcp/tools/shell/logs.py +265 -0
- hanzo_mcp/tools/shell/npx.py +194 -0
- hanzo_mcp/tools/shell/npx_background.py +254 -0
- hanzo_mcp/tools/shell/pkill.py +262 -0
- hanzo_mcp/tools/shell/processes.py +279 -0
- hanzo_mcp/tools/shell/run_background.py +326 -0
- hanzo_mcp/tools/shell/uvx.py +187 -0
- hanzo_mcp/tools/shell/uvx_background.py +249 -0
- hanzo_mcp/tools/vector/__init__.py +21 -12
- hanzo_mcp/tools/vector/ast_analyzer.py +459 -0
- hanzo_mcp/tools/vector/git_ingester.py +485 -0
- hanzo_mcp/tools/vector/index_tool.py +358 -0
- hanzo_mcp/tools/vector/infinity_store.py +465 -1
- hanzo_mcp/tools/vector/mock_infinity.py +162 -0
- hanzo_mcp/tools/vector/vector_index.py +7 -6
- hanzo_mcp/tools/vector/vector_search.py +22 -7
- {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.dist-info}/METADATA +68 -20
- hanzo_mcp-0.5.2.dist-info/RECORD +106 -0
- hanzo_mcp-0.5.0.dist-info/RECORD +0 -63
- {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.5.0.dist-info → hanzo_mcp-0.5.2.dist-info}/licenses/LICENSE +0 -0
- {hanzo_mcp-0.5.0.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
|