hanzo-mcp 0.5.2__py3-none-any.whl → 0.6.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/cli.py +32 -0
- hanzo_mcp/dev_server.py +246 -0
- hanzo_mcp/prompts/__init__.py +1 -1
- hanzo_mcp/prompts/project_system.py +43 -7
- hanzo_mcp/server.py +5 -1
- hanzo_mcp/tools/__init__.py +66 -35
- hanzo_mcp/tools/agent/__init__.py +1 -1
- hanzo_mcp/tools/agent/agent.py +401 -0
- hanzo_mcp/tools/agent/agent_tool.py +3 -4
- hanzo_mcp/tools/common/__init__.py +1 -1
- hanzo_mcp/tools/common/base.py +2 -2
- hanzo_mcp/tools/common/batch_tool.py +3 -5
- hanzo_mcp/tools/common/config_tool.py +1 -1
- hanzo_mcp/tools/common/context.py +1 -1
- hanzo_mcp/tools/common/palette.py +344 -0
- hanzo_mcp/tools/common/palette_loader.py +108 -0
- hanzo_mcp/tools/common/stats.py +1 -1
- hanzo_mcp/tools/common/thinking_tool.py +3 -5
- hanzo_mcp/tools/common/tool_disable.py +1 -1
- hanzo_mcp/tools/common/tool_enable.py +1 -1
- hanzo_mcp/tools/common/tool_list.py +49 -52
- hanzo_mcp/tools/config/__init__.py +10 -0
- hanzo_mcp/tools/config/config_tool.py +212 -0
- hanzo_mcp/tools/config/index_config.py +176 -0
- hanzo_mcp/tools/config/palette_tool.py +166 -0
- hanzo_mcp/tools/database/__init__.py +1 -1
- hanzo_mcp/tools/database/graph.py +482 -0
- hanzo_mcp/tools/database/graph_add.py +1 -1
- hanzo_mcp/tools/database/graph_query.py +1 -1
- hanzo_mcp/tools/database/graph_remove.py +1 -1
- hanzo_mcp/tools/database/graph_search.py +1 -1
- hanzo_mcp/tools/database/graph_stats.py +1 -1
- hanzo_mcp/tools/database/sql.py +411 -0
- hanzo_mcp/tools/database/sql_query.py +1 -1
- hanzo_mcp/tools/database/sql_search.py +1 -1
- hanzo_mcp/tools/database/sql_stats.py +1 -1
- hanzo_mcp/tools/editor/neovim_command.py +1 -1
- hanzo_mcp/tools/editor/neovim_edit.py +1 -1
- hanzo_mcp/tools/editor/neovim_session.py +1 -1
- hanzo_mcp/tools/filesystem/__init__.py +42 -13
- hanzo_mcp/tools/filesystem/base.py +1 -1
- hanzo_mcp/tools/filesystem/batch_search.py +4 -4
- hanzo_mcp/tools/filesystem/content_replace.py +3 -5
- hanzo_mcp/tools/filesystem/diff.py +193 -0
- hanzo_mcp/tools/filesystem/directory_tree.py +3 -5
- hanzo_mcp/tools/filesystem/edit.py +3 -5
- hanzo_mcp/tools/filesystem/find.py +443 -0
- hanzo_mcp/tools/filesystem/find_files.py +1 -1
- hanzo_mcp/tools/filesystem/git_search.py +1 -1
- hanzo_mcp/tools/filesystem/grep.py +2 -2
- hanzo_mcp/tools/filesystem/multi_edit.py +3 -5
- hanzo_mcp/tools/filesystem/read.py +17 -5
- hanzo_mcp/tools/filesystem/{grep_ast_tool.py → symbols.py} +17 -27
- hanzo_mcp/tools/filesystem/symbols_unified.py +376 -0
- hanzo_mcp/tools/filesystem/tree.py +268 -0
- hanzo_mcp/tools/filesystem/unified_search.py +711 -0
- hanzo_mcp/tools/filesystem/unix_aliases.py +99 -0
- hanzo_mcp/tools/filesystem/watch.py +174 -0
- hanzo_mcp/tools/filesystem/write.py +3 -5
- hanzo_mcp/tools/jupyter/__init__.py +9 -12
- hanzo_mcp/tools/jupyter/base.py +1 -1
- hanzo_mcp/tools/jupyter/jupyter.py +326 -0
- hanzo_mcp/tools/jupyter/notebook_edit.py +3 -4
- hanzo_mcp/tools/jupyter/notebook_read.py +3 -5
- hanzo_mcp/tools/llm/__init__.py +4 -0
- hanzo_mcp/tools/llm/consensus_tool.py +1 -1
- hanzo_mcp/tools/llm/llm_manage.py +1 -1
- hanzo_mcp/tools/llm/llm_tool.py +1 -1
- hanzo_mcp/tools/llm/llm_unified.py +851 -0
- hanzo_mcp/tools/llm/provider_tools.py +1 -1
- hanzo_mcp/tools/mcp/__init__.py +4 -0
- hanzo_mcp/tools/mcp/mcp_add.py +1 -1
- hanzo_mcp/tools/mcp/mcp_remove.py +1 -1
- hanzo_mcp/tools/mcp/mcp_stats.py +1 -1
- hanzo_mcp/tools/mcp/mcp_unified.py +503 -0
- hanzo_mcp/tools/shell/__init__.py +20 -42
- hanzo_mcp/tools/shell/base.py +1 -1
- hanzo_mcp/tools/shell/base_process.py +303 -0
- hanzo_mcp/tools/shell/bash_unified.py +134 -0
- hanzo_mcp/tools/shell/logs.py +1 -1
- hanzo_mcp/tools/shell/npx.py +1 -1
- hanzo_mcp/tools/shell/npx_background.py +1 -1
- hanzo_mcp/tools/shell/npx_unified.py +101 -0
- hanzo_mcp/tools/shell/open.py +107 -0
- hanzo_mcp/tools/shell/pkill.py +1 -1
- hanzo_mcp/tools/shell/process_unified.py +131 -0
- hanzo_mcp/tools/shell/processes.py +1 -1
- hanzo_mcp/tools/shell/run_background.py +1 -1
- hanzo_mcp/tools/shell/run_command.py +3 -4
- hanzo_mcp/tools/shell/run_command_windows.py +3 -4
- hanzo_mcp/tools/shell/uvx.py +1 -1
- hanzo_mcp/tools/shell/uvx_background.py +1 -1
- hanzo_mcp/tools/shell/uvx_unified.py +101 -0
- hanzo_mcp/tools/todo/__init__.py +1 -1
- hanzo_mcp/tools/todo/base.py +1 -1
- hanzo_mcp/tools/todo/todo.py +265 -0
- hanzo_mcp/tools/todo/todo_read.py +3 -5
- hanzo_mcp/tools/todo/todo_write.py +3 -5
- hanzo_mcp/tools/vector/__init__.py +1 -1
- hanzo_mcp/tools/vector/index_tool.py +1 -1
- hanzo_mcp/tools/vector/project_manager.py +27 -5
- hanzo_mcp/tools/vector/vector.py +311 -0
- hanzo_mcp/tools/vector/vector_index.py +1 -1
- hanzo_mcp/tools/vector/vector_search.py +1 -1
- hanzo_mcp-0.6.2.dist-info/METADATA +336 -0
- hanzo_mcp-0.6.2.dist-info/RECORD +134 -0
- hanzo_mcp-0.6.2.dist-info/entry_points.txt +3 -0
- hanzo_mcp-0.5.2.dist-info/METADATA +0 -276
- hanzo_mcp-0.5.2.dist-info/RECORD +0 -106
- hanzo_mcp-0.5.2.dist-info/entry_points.txt +0 -2
- {hanzo_mcp-0.5.2.dist-info → hanzo_mcp-0.6.2.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.5.2.dist-info → hanzo_mcp-0.6.2.dist-info}/licenses/LICENSE +0 -0
- {hanzo_mcp-0.5.2.dist-info → hanzo_mcp-0.6.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
"""Unified graph database tool."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated, TypedDict, Unpack, final, override, Optional, List, Dict, Any
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from mcp.server.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.permissions import PermissionManager
|
|
12
|
+
from hanzo_mcp.tools.database.database_manager import DatabaseManager
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Parameter types
|
|
16
|
+
Action = Annotated[
|
|
17
|
+
str,
|
|
18
|
+
Field(
|
|
19
|
+
description="Action: query (default), add, remove, search, stats",
|
|
20
|
+
default="query",
|
|
21
|
+
),
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
NodeId = Annotated[
|
|
25
|
+
Optional[str],
|
|
26
|
+
Field(
|
|
27
|
+
description="Node ID",
|
|
28
|
+
default=None,
|
|
29
|
+
),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
NodeType = Annotated[
|
|
33
|
+
Optional[str],
|
|
34
|
+
Field(
|
|
35
|
+
description="Node type/label",
|
|
36
|
+
default=None,
|
|
37
|
+
),
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
EdgeType = Annotated[
|
|
41
|
+
Optional[str],
|
|
42
|
+
Field(
|
|
43
|
+
description="Edge type/relationship",
|
|
44
|
+
default=None,
|
|
45
|
+
),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
FromNode = Annotated[
|
|
49
|
+
Optional[str],
|
|
50
|
+
Field(
|
|
51
|
+
description="Source node ID for edges",
|
|
52
|
+
default=None,
|
|
53
|
+
),
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
ToNode = Annotated[
|
|
57
|
+
Optional[str],
|
|
58
|
+
Field(
|
|
59
|
+
description="Target node ID for edges",
|
|
60
|
+
default=None,
|
|
61
|
+
),
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
Properties = Annotated[
|
|
65
|
+
Optional[Dict[str, Any]],
|
|
66
|
+
Field(
|
|
67
|
+
description="Node/edge properties as JSON",
|
|
68
|
+
default=None,
|
|
69
|
+
),
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
Pattern = Annotated[
|
|
73
|
+
Optional[str],
|
|
74
|
+
Field(
|
|
75
|
+
description="Search pattern for properties",
|
|
76
|
+
default=None,
|
|
77
|
+
),
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
Depth = Annotated[
|
|
81
|
+
int,
|
|
82
|
+
Field(
|
|
83
|
+
description="Max traversal depth for queries",
|
|
84
|
+
default=2,
|
|
85
|
+
),
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
Limit = Annotated[
|
|
89
|
+
int,
|
|
90
|
+
Field(
|
|
91
|
+
description="Maximum results to return",
|
|
92
|
+
default=50,
|
|
93
|
+
),
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class GraphParams(TypedDict, total=False):
|
|
98
|
+
"""Parameters for graph tool."""
|
|
99
|
+
action: str
|
|
100
|
+
node_id: Optional[str]
|
|
101
|
+
node_type: Optional[str]
|
|
102
|
+
edge_type: Optional[str]
|
|
103
|
+
from_node: Optional[str]
|
|
104
|
+
to_node: Optional[str]
|
|
105
|
+
properties: Optional[Dict[str, Any]]
|
|
106
|
+
pattern: Optional[str]
|
|
107
|
+
depth: int
|
|
108
|
+
limit: int
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@final
|
|
112
|
+
class GraphTool(BaseTool):
|
|
113
|
+
"""Unified graph database tool."""
|
|
114
|
+
|
|
115
|
+
def __init__(self, permission_manager: PermissionManager, db_manager: DatabaseManager):
|
|
116
|
+
"""Initialize the graph tool."""
|
|
117
|
+
super().__init__(permission_manager)
|
|
118
|
+
self.db_manager = db_manager
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
@override
|
|
122
|
+
def name(self) -> str:
|
|
123
|
+
"""Get the tool name."""
|
|
124
|
+
return "graph"
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
@override
|
|
128
|
+
def description(self) -> str:
|
|
129
|
+
"""Get the tool description."""
|
|
130
|
+
return """Graph database. Actions: query (default), add, remove, search, stats.
|
|
131
|
+
|
|
132
|
+
Usage:
|
|
133
|
+
graph --node-id user123
|
|
134
|
+
graph --action add --node-id user123 --node-type User --properties '{"name": "John"}'
|
|
135
|
+
graph --action add --from-node user123 --to-node post456 --edge-type CREATED
|
|
136
|
+
graph --action query --node-id user123 --depth 3
|
|
137
|
+
graph --action search --pattern "John" --node-type User
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
@override
|
|
141
|
+
async def call(
|
|
142
|
+
self,
|
|
143
|
+
ctx: MCPContext,
|
|
144
|
+
**params: Unpack[GraphParams],
|
|
145
|
+
) -> str:
|
|
146
|
+
"""Execute graph operation."""
|
|
147
|
+
tool_ctx = self.create_tool_context(ctx)
|
|
148
|
+
|
|
149
|
+
# Get current project database
|
|
150
|
+
project_db = self.db_manager.get_current_project_db()
|
|
151
|
+
if not project_db:
|
|
152
|
+
return "Error: No project database found. Are you in a project directory?"
|
|
153
|
+
|
|
154
|
+
# Extract action
|
|
155
|
+
action = params.get("action", "query")
|
|
156
|
+
|
|
157
|
+
# Route to appropriate handler
|
|
158
|
+
if action == "query":
|
|
159
|
+
return await self._handle_query(project_db, params, tool_ctx)
|
|
160
|
+
elif action == "add":
|
|
161
|
+
return await self._handle_add(project_db, params, tool_ctx)
|
|
162
|
+
elif action == "remove":
|
|
163
|
+
return await self._handle_remove(project_db, params, tool_ctx)
|
|
164
|
+
elif action == "search":
|
|
165
|
+
return await self._handle_search(project_db, params, tool_ctx)
|
|
166
|
+
elif action == "stats":
|
|
167
|
+
return await self._handle_stats(project_db, tool_ctx)
|
|
168
|
+
else:
|
|
169
|
+
return f"Error: Unknown action '{action}'. Valid actions: query, add, remove, search, stats"
|
|
170
|
+
|
|
171
|
+
async def _handle_query(self, project_db, params: Dict[str, Any], tool_ctx) -> str:
|
|
172
|
+
"""Query graph relationships."""
|
|
173
|
+
node_id = params.get("node_id")
|
|
174
|
+
node_type = params.get("node_type")
|
|
175
|
+
depth = params.get("depth", 2)
|
|
176
|
+
limit = params.get("limit", 50)
|
|
177
|
+
|
|
178
|
+
if not node_id and not node_type:
|
|
179
|
+
return "Error: node_id or node_type required for query"
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
with project_db.get_graph_connection() as conn:
|
|
183
|
+
results = []
|
|
184
|
+
|
|
185
|
+
if node_id:
|
|
186
|
+
# Query specific node and its relationships
|
|
187
|
+
cursor = conn.execute("""
|
|
188
|
+
WITH RECURSIVE
|
|
189
|
+
node_tree(id, type, properties, depth, path) AS (
|
|
190
|
+
SELECT id, type, properties, 0, id
|
|
191
|
+
FROM nodes
|
|
192
|
+
WHERE id = ?
|
|
193
|
+
|
|
194
|
+
UNION ALL
|
|
195
|
+
|
|
196
|
+
SELECT n.id, n.type, n.properties, nt.depth + 1,
|
|
197
|
+
nt.path || ' -> ' || n.id
|
|
198
|
+
FROM nodes n
|
|
199
|
+
JOIN edges e ON (e.to_node = n.id OR e.from_node = n.id)
|
|
200
|
+
JOIN node_tree nt ON (
|
|
201
|
+
(e.from_node = nt.id AND e.to_node = n.id) OR
|
|
202
|
+
(e.to_node = nt.id AND e.from_node = n.id)
|
|
203
|
+
)
|
|
204
|
+
WHERE nt.depth < ?
|
|
205
|
+
)
|
|
206
|
+
SELECT DISTINCT * FROM node_tree
|
|
207
|
+
ORDER BY depth, id
|
|
208
|
+
LIMIT ?
|
|
209
|
+
""", (node_id, depth, limit))
|
|
210
|
+
|
|
211
|
+
nodes = cursor.fetchall()
|
|
212
|
+
|
|
213
|
+
# Get edges
|
|
214
|
+
cursor = conn.execute("""
|
|
215
|
+
SELECT from_node, to_node, type, properties
|
|
216
|
+
FROM edges
|
|
217
|
+
WHERE from_node IN (SELECT id FROM node_tree)
|
|
218
|
+
OR to_node IN (SELECT id FROM node_tree)
|
|
219
|
+
""")
|
|
220
|
+
|
|
221
|
+
edges = cursor.fetchall()
|
|
222
|
+
|
|
223
|
+
else:
|
|
224
|
+
# Query by type
|
|
225
|
+
cursor = conn.execute("""
|
|
226
|
+
SELECT id, type, properties
|
|
227
|
+
FROM nodes
|
|
228
|
+
WHERE type = ?
|
|
229
|
+
LIMIT ?
|
|
230
|
+
""", (node_type, limit))
|
|
231
|
+
|
|
232
|
+
nodes = cursor.fetchall()
|
|
233
|
+
edges = []
|
|
234
|
+
|
|
235
|
+
# Format results
|
|
236
|
+
output = ["=== Graph Query Results ==="]
|
|
237
|
+
|
|
238
|
+
if nodes:
|
|
239
|
+
output.append(f"\nNodes ({len(nodes)}):")
|
|
240
|
+
for node in nodes:
|
|
241
|
+
props = json.loads(node[2]) if node[2] else {}
|
|
242
|
+
output.append(f" {node[0]} [{node[1]}] {props}")
|
|
243
|
+
|
|
244
|
+
if edges:
|
|
245
|
+
output.append(f"\nEdges ({len(edges)}):")
|
|
246
|
+
for edge in edges:
|
|
247
|
+
props = json.loads(edge[3]) if edge[3] else {}
|
|
248
|
+
output.append(f" {edge[0]} --[{edge[2]}]--> {edge[1]} {props}")
|
|
249
|
+
|
|
250
|
+
if not nodes and not edges:
|
|
251
|
+
output.append("No results found")
|
|
252
|
+
|
|
253
|
+
return "\n".join(output)
|
|
254
|
+
|
|
255
|
+
except Exception as e:
|
|
256
|
+
await tool_ctx.error(f"Query failed: {str(e)}")
|
|
257
|
+
return f"Error during query: {str(e)}"
|
|
258
|
+
|
|
259
|
+
async def _handle_add(self, project_db, params: Dict[str, Any], tool_ctx) -> str:
|
|
260
|
+
"""Add nodes or edges."""
|
|
261
|
+
node_id = params.get("node_id")
|
|
262
|
+
from_node = params.get("from_node")
|
|
263
|
+
to_node = params.get("to_node")
|
|
264
|
+
|
|
265
|
+
if node_id:
|
|
266
|
+
# Add node
|
|
267
|
+
node_type = params.get("node_type")
|
|
268
|
+
if not node_type:
|
|
269
|
+
return "Error: node_type required when adding node"
|
|
270
|
+
|
|
271
|
+
properties = params.get("properties", {})
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
with project_db.get_graph_connection() as conn:
|
|
275
|
+
conn.execute("""
|
|
276
|
+
INSERT OR REPLACE INTO nodes (id, type, properties)
|
|
277
|
+
VALUES (?, ?, ?)
|
|
278
|
+
""", (node_id, node_type, json.dumps(properties)))
|
|
279
|
+
conn.commit()
|
|
280
|
+
|
|
281
|
+
await tool_ctx.info(f"Added node: {node_id}")
|
|
282
|
+
return f"Added node {node_id} [{node_type}]"
|
|
283
|
+
|
|
284
|
+
except Exception as e:
|
|
285
|
+
await tool_ctx.error(f"Failed to add node: {str(e)}")
|
|
286
|
+
return f"Error adding node: {str(e)}"
|
|
287
|
+
|
|
288
|
+
elif from_node and to_node:
|
|
289
|
+
# Add edge
|
|
290
|
+
edge_type = params.get("edge_type", "RELATED")
|
|
291
|
+
properties = params.get("properties", {})
|
|
292
|
+
|
|
293
|
+
try:
|
|
294
|
+
with project_db.get_graph_connection() as conn:
|
|
295
|
+
conn.execute("""
|
|
296
|
+
INSERT OR REPLACE INTO edges (from_node, to_node, type, properties)
|
|
297
|
+
VALUES (?, ?, ?, ?)
|
|
298
|
+
""", (from_node, to_node, edge_type, json.dumps(properties)))
|
|
299
|
+
conn.commit()
|
|
300
|
+
|
|
301
|
+
await tool_ctx.info(f"Added edge: {from_node} -> {to_node}")
|
|
302
|
+
return f"Added edge {from_node} --[{edge_type}]--> {to_node}"
|
|
303
|
+
|
|
304
|
+
except Exception as e:
|
|
305
|
+
await tool_ctx.error(f"Failed to add edge: {str(e)}")
|
|
306
|
+
return f"Error adding edge: {str(e)}"
|
|
307
|
+
|
|
308
|
+
else:
|
|
309
|
+
return "Error: Either node_id (for node) or from_node + to_node (for edge) required"
|
|
310
|
+
|
|
311
|
+
async def _handle_remove(self, project_db, params: Dict[str, Any], tool_ctx) -> str:
|
|
312
|
+
"""Remove nodes or edges."""
|
|
313
|
+
node_id = params.get("node_id")
|
|
314
|
+
from_node = params.get("from_node")
|
|
315
|
+
to_node = params.get("to_node")
|
|
316
|
+
|
|
317
|
+
if node_id:
|
|
318
|
+
# Remove node and its edges
|
|
319
|
+
try:
|
|
320
|
+
with project_db.get_graph_connection() as conn:
|
|
321
|
+
# Delete edges first
|
|
322
|
+
cursor = conn.execute("""
|
|
323
|
+
DELETE FROM edges
|
|
324
|
+
WHERE from_node = ? OR to_node = ?
|
|
325
|
+
""", (node_id, node_id))
|
|
326
|
+
|
|
327
|
+
edges_deleted = cursor.rowcount
|
|
328
|
+
|
|
329
|
+
# Delete node
|
|
330
|
+
cursor = conn.execute("""
|
|
331
|
+
DELETE FROM nodes WHERE id = ?
|
|
332
|
+
""", (node_id,))
|
|
333
|
+
|
|
334
|
+
if cursor.rowcount == 0:
|
|
335
|
+
return f"Node {node_id} not found"
|
|
336
|
+
|
|
337
|
+
conn.commit()
|
|
338
|
+
|
|
339
|
+
msg = f"Removed node {node_id}"
|
|
340
|
+
if edges_deleted > 0:
|
|
341
|
+
msg += f" and {edges_deleted} connected edges"
|
|
342
|
+
|
|
343
|
+
await tool_ctx.info(msg)
|
|
344
|
+
return msg
|
|
345
|
+
|
|
346
|
+
except Exception as e:
|
|
347
|
+
await tool_ctx.error(f"Failed to remove node: {str(e)}")
|
|
348
|
+
return f"Error removing node: {str(e)}"
|
|
349
|
+
|
|
350
|
+
elif from_node and to_node:
|
|
351
|
+
# Remove specific edge
|
|
352
|
+
edge_type = params.get("edge_type")
|
|
353
|
+
|
|
354
|
+
try:
|
|
355
|
+
with project_db.get_graph_connection() as conn:
|
|
356
|
+
if edge_type:
|
|
357
|
+
cursor = conn.execute("""
|
|
358
|
+
DELETE FROM edges
|
|
359
|
+
WHERE from_node = ? AND to_node = ? AND type = ?
|
|
360
|
+
""", (from_node, to_node, edge_type))
|
|
361
|
+
else:
|
|
362
|
+
cursor = conn.execute("""
|
|
363
|
+
DELETE FROM edges
|
|
364
|
+
WHERE from_node = ? AND to_node = ?
|
|
365
|
+
""", (from_node, to_node))
|
|
366
|
+
|
|
367
|
+
if cursor.rowcount == 0:
|
|
368
|
+
return f"Edge not found"
|
|
369
|
+
|
|
370
|
+
conn.commit()
|
|
371
|
+
|
|
372
|
+
return f"Removed edge {from_node} --> {to_node}"
|
|
373
|
+
|
|
374
|
+
except Exception as e:
|
|
375
|
+
await tool_ctx.error(f"Failed to remove edge: {str(e)}")
|
|
376
|
+
return f"Error removing edge: {str(e)}"
|
|
377
|
+
|
|
378
|
+
else:
|
|
379
|
+
return "Error: Either node_id or from_node + to_node required for remove"
|
|
380
|
+
|
|
381
|
+
async def _handle_search(self, project_db, params: Dict[str, Any], tool_ctx) -> str:
|
|
382
|
+
"""Search graph by pattern."""
|
|
383
|
+
pattern = params.get("pattern")
|
|
384
|
+
if not pattern:
|
|
385
|
+
return "Error: pattern required for search"
|
|
386
|
+
|
|
387
|
+
node_type = params.get("node_type")
|
|
388
|
+
limit = params.get("limit", 50)
|
|
389
|
+
|
|
390
|
+
try:
|
|
391
|
+
with project_db.get_graph_connection() as conn:
|
|
392
|
+
# Search in properties
|
|
393
|
+
if node_type:
|
|
394
|
+
cursor = conn.execute("""
|
|
395
|
+
SELECT id, type, properties
|
|
396
|
+
FROM nodes
|
|
397
|
+
WHERE type = ? AND properties LIKE ?
|
|
398
|
+
LIMIT ?
|
|
399
|
+
""", (node_type, f"%{pattern}%", limit))
|
|
400
|
+
else:
|
|
401
|
+
cursor = conn.execute("""
|
|
402
|
+
SELECT id, type, properties
|
|
403
|
+
FROM nodes
|
|
404
|
+
WHERE properties LIKE ?
|
|
405
|
+
LIMIT ?
|
|
406
|
+
""", (f"%{pattern}%", limit))
|
|
407
|
+
|
|
408
|
+
results = cursor.fetchall()
|
|
409
|
+
|
|
410
|
+
if not results:
|
|
411
|
+
return f"No nodes found matching '{pattern}'"
|
|
412
|
+
|
|
413
|
+
# Format results
|
|
414
|
+
output = [f"=== Graph Search Results for '{pattern}' ==="]
|
|
415
|
+
output.append(f"Found {len(results)} nodes\n")
|
|
416
|
+
|
|
417
|
+
for node in results:
|
|
418
|
+
props = json.loads(node[2]) if node[2] else {}
|
|
419
|
+
output.append(f"{node[0]} [{node[1]}] {props}")
|
|
420
|
+
|
|
421
|
+
return "\n".join(output)
|
|
422
|
+
|
|
423
|
+
except Exception as e:
|
|
424
|
+
await tool_ctx.error(f"Search failed: {str(e)}")
|
|
425
|
+
return f"Error during search: {str(e)}"
|
|
426
|
+
|
|
427
|
+
async def _handle_stats(self, project_db, tool_ctx) -> str:
|
|
428
|
+
"""Get graph statistics."""
|
|
429
|
+
try:
|
|
430
|
+
with project_db.get_graph_connection() as conn:
|
|
431
|
+
# Node stats
|
|
432
|
+
cursor = conn.execute("""
|
|
433
|
+
SELECT type, COUNT(*) as count
|
|
434
|
+
FROM nodes
|
|
435
|
+
GROUP BY type
|
|
436
|
+
ORDER BY count DESC
|
|
437
|
+
""")
|
|
438
|
+
|
|
439
|
+
node_stats = cursor.fetchall()
|
|
440
|
+
|
|
441
|
+
# Edge stats
|
|
442
|
+
cursor = conn.execute("""
|
|
443
|
+
SELECT type, COUNT(*) as count
|
|
444
|
+
FROM edges
|
|
445
|
+
GROUP BY type
|
|
446
|
+
ORDER BY count DESC
|
|
447
|
+
""")
|
|
448
|
+
|
|
449
|
+
edge_stats = cursor.fetchall()
|
|
450
|
+
|
|
451
|
+
# Total counts
|
|
452
|
+
cursor = conn.execute("SELECT COUNT(*) FROM nodes")
|
|
453
|
+
total_nodes = cursor.fetchone()[0]
|
|
454
|
+
|
|
455
|
+
cursor = conn.execute("SELECT COUNT(*) FROM edges")
|
|
456
|
+
total_edges = cursor.fetchone()[0]
|
|
457
|
+
|
|
458
|
+
# Format output
|
|
459
|
+
output = [f"=== Graph Database Statistics ==="]
|
|
460
|
+
output.append(f"Project: {project_db.project_path}")
|
|
461
|
+
output.append(f"\nTotal nodes: {total_nodes}")
|
|
462
|
+
output.append(f"Total edges: {total_edges}")
|
|
463
|
+
|
|
464
|
+
if node_stats:
|
|
465
|
+
output.append("\nNodes by type:")
|
|
466
|
+
for node_type, count in node_stats:
|
|
467
|
+
output.append(f" {node_type}: {count}")
|
|
468
|
+
|
|
469
|
+
if edge_stats:
|
|
470
|
+
output.append("\nEdges by type:")
|
|
471
|
+
for edge_type, count in edge_stats:
|
|
472
|
+
output.append(f" {edge_type}: {count}")
|
|
473
|
+
|
|
474
|
+
return "\n".join(output)
|
|
475
|
+
|
|
476
|
+
except Exception as e:
|
|
477
|
+
await tool_ctx.error(f"Failed to get stats: {str(e)}")
|
|
478
|
+
return f"Error getting stats: {str(e)}"
|
|
479
|
+
|
|
480
|
+
def register(self, mcp_server) -> None:
|
|
481
|
+
"""Register this tool with the MCP server."""
|
|
482
|
+
pass
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import json
|
|
4
4
|
from typing import Annotated, Optional, TypedDict, Unpack, final, override
|
|
5
5
|
|
|
6
|
-
from fastmcp import Context as MCPContext
|
|
6
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
7
7
|
from pydantic import Field
|
|
8
8
|
|
|
9
9
|
from hanzo_mcp.tools.common.base import BaseTool
|
|
@@ -5,7 +5,7 @@ import sqlite3
|
|
|
5
5
|
from typing import Annotated, Optional, TypedDict, Unpack, final, override, List, Dict, Any
|
|
6
6
|
from collections import deque
|
|
7
7
|
|
|
8
|
-
from fastmcp import Context as MCPContext
|
|
8
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
9
9
|
from pydantic import Field
|
|
10
10
|
|
|
11
11
|
from hanzo_mcp.tools.common.base import BaseTool
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import json
|
|
4
4
|
from typing import Annotated, Optional, TypedDict, Unpack, final, override
|
|
5
5
|
|
|
6
|
-
from fastmcp import Context as MCPContext
|
|
6
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
7
7
|
from pydantic import Field
|
|
8
8
|
|
|
9
9
|
from hanzo_mcp.tools.common.base import BaseTool
|
|
@@ -4,7 +4,7 @@ import json
|
|
|
4
4
|
import sqlite3
|
|
5
5
|
from typing import Annotated, Optional, TypedDict, Unpack, final, override
|
|
6
6
|
|
|
7
|
-
from fastmcp import Context as MCPContext
|
|
7
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
8
8
|
from pydantic import Field
|
|
9
9
|
|
|
10
10
|
from hanzo_mcp.tools.common.base import BaseTool
|
|
@@ -5,7 +5,7 @@ import sqlite3
|
|
|
5
5
|
from typing import Annotated, Optional, TypedDict, Unpack, final, override
|
|
6
6
|
from collections import Counter, defaultdict
|
|
7
7
|
|
|
8
|
-
from fastmcp import Context as MCPContext
|
|
8
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
9
9
|
from pydantic import Field
|
|
10
10
|
|
|
11
11
|
from hanzo_mcp.tools.common.base import BaseTool
|