hanzo-mcp 0.7.7__py3-none-any.whl → 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of hanzo-mcp might be problematic. Click here for more details.
- hanzo_mcp/__init__.py +6 -0
- hanzo_mcp/__main__.py +1 -1
- hanzo_mcp/analytics/__init__.py +2 -2
- hanzo_mcp/analytics/posthog_analytics.py +76 -82
- hanzo_mcp/cli.py +31 -36
- hanzo_mcp/cli_enhanced.py +94 -72
- hanzo_mcp/cli_plugin.py +27 -17
- hanzo_mcp/config/__init__.py +2 -2
- hanzo_mcp/config/settings.py +112 -88
- hanzo_mcp/config/tool_config.py +32 -34
- hanzo_mcp/dev_server.py +66 -67
- hanzo_mcp/prompts/__init__.py +94 -12
- hanzo_mcp/prompts/enhanced_prompts.py +809 -0
- hanzo_mcp/prompts/example_custom_prompt.py +6 -5
- hanzo_mcp/prompts/project_todo_reminder.py +0 -1
- hanzo_mcp/prompts/tool_explorer.py +10 -7
- hanzo_mcp/server.py +17 -21
- hanzo_mcp/server_enhanced.py +15 -22
- hanzo_mcp/tools/__init__.py +56 -28
- hanzo_mcp/tools/agent/__init__.py +16 -19
- hanzo_mcp/tools/agent/agent.py +82 -65
- hanzo_mcp/tools/agent/agent_tool.py +152 -122
- hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +66 -62
- hanzo_mcp/tools/agent/clarification_protocol.py +55 -50
- hanzo_mcp/tools/agent/clarification_tool.py +11 -10
- hanzo_mcp/tools/agent/claude_cli_tool.py +21 -20
- hanzo_mcp/tools/agent/claude_desktop_auth.py +130 -144
- hanzo_mcp/tools/agent/cli_agent_base.py +59 -53
- hanzo_mcp/tools/agent/code_auth.py +102 -107
- hanzo_mcp/tools/agent/code_auth_tool.py +28 -27
- hanzo_mcp/tools/agent/codex_cli_tool.py +20 -19
- hanzo_mcp/tools/agent/critic_tool.py +86 -73
- hanzo_mcp/tools/agent/gemini_cli_tool.py +21 -20
- hanzo_mcp/tools/agent/grok_cli_tool.py +21 -20
- hanzo_mcp/tools/agent/iching_tool.py +404 -139
- hanzo_mcp/tools/agent/network_tool.py +89 -73
- hanzo_mcp/tools/agent/prompt.py +2 -1
- hanzo_mcp/tools/agent/review_tool.py +101 -98
- hanzo_mcp/tools/agent/swarm_alias.py +87 -0
- hanzo_mcp/tools/agent/swarm_tool.py +246 -161
- hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +134 -92
- hanzo_mcp/tools/agent/tool_adapter.py +21 -11
- hanzo_mcp/tools/common/__init__.py +1 -1
- hanzo_mcp/tools/common/base.py +3 -5
- hanzo_mcp/tools/common/batch_tool.py +46 -39
- hanzo_mcp/tools/common/config_tool.py +120 -84
- hanzo_mcp/tools/common/context.py +1 -5
- hanzo_mcp/tools/common/context_fix.py +5 -3
- hanzo_mcp/tools/common/critic_tool.py +4 -8
- hanzo_mcp/tools/common/decorators.py +58 -56
- hanzo_mcp/tools/common/enhanced_base.py +29 -32
- hanzo_mcp/tools/common/fastmcp_pagination.py +91 -94
- hanzo_mcp/tools/common/forgiving_edit.py +91 -87
- hanzo_mcp/tools/common/mode.py +15 -17
- hanzo_mcp/tools/common/mode_loader.py +27 -24
- hanzo_mcp/tools/common/paginated_base.py +61 -53
- hanzo_mcp/tools/common/paginated_response.py +72 -79
- hanzo_mcp/tools/common/pagination.py +50 -53
- hanzo_mcp/tools/common/permissions.py +4 -4
- hanzo_mcp/tools/common/personality.py +186 -138
- hanzo_mcp/tools/common/plugin_loader.py +54 -54
- hanzo_mcp/tools/common/stats.py +65 -47
- hanzo_mcp/tools/common/test_helpers.py +31 -0
- hanzo_mcp/tools/common/thinking_tool.py +4 -8
- hanzo_mcp/tools/common/tool_disable.py +17 -12
- hanzo_mcp/tools/common/tool_enable.py +13 -14
- hanzo_mcp/tools/common/tool_list.py +36 -28
- hanzo_mcp/tools/common/truncate.py +23 -23
- hanzo_mcp/tools/config/__init__.py +4 -4
- hanzo_mcp/tools/config/config_tool.py +42 -29
- hanzo_mcp/tools/config/index_config.py +37 -34
- hanzo_mcp/tools/config/mode_tool.py +175 -55
- hanzo_mcp/tools/database/__init__.py +15 -12
- hanzo_mcp/tools/database/database_manager.py +77 -75
- hanzo_mcp/tools/database/graph.py +137 -91
- hanzo_mcp/tools/database/graph_add.py +30 -18
- hanzo_mcp/tools/database/graph_query.py +178 -102
- hanzo_mcp/tools/database/graph_remove.py +33 -28
- hanzo_mcp/tools/database/graph_search.py +97 -75
- hanzo_mcp/tools/database/graph_stats.py +91 -59
- hanzo_mcp/tools/database/sql.py +107 -79
- hanzo_mcp/tools/database/sql_query.py +30 -24
- hanzo_mcp/tools/database/sql_search.py +29 -25
- hanzo_mcp/tools/database/sql_stats.py +47 -35
- hanzo_mcp/tools/editor/neovim_command.py +25 -28
- hanzo_mcp/tools/editor/neovim_edit.py +21 -23
- hanzo_mcp/tools/editor/neovim_session.py +60 -54
- hanzo_mcp/tools/filesystem/__init__.py +31 -30
- hanzo_mcp/tools/filesystem/ast_multi_edit.py +329 -249
- hanzo_mcp/tools/filesystem/ast_tool.py +4 -4
- hanzo_mcp/tools/filesystem/base.py +1 -1
- hanzo_mcp/tools/filesystem/batch_search.py +316 -224
- hanzo_mcp/tools/filesystem/content_replace.py +4 -4
- hanzo_mcp/tools/filesystem/diff.py +71 -59
- hanzo_mcp/tools/filesystem/directory_tree.py +7 -7
- hanzo_mcp/tools/filesystem/directory_tree_paginated.py +49 -37
- hanzo_mcp/tools/filesystem/edit.py +4 -4
- hanzo_mcp/tools/filesystem/find.py +173 -80
- hanzo_mcp/tools/filesystem/find_files.py +73 -52
- hanzo_mcp/tools/filesystem/git_search.py +157 -104
- hanzo_mcp/tools/filesystem/grep.py +8 -8
- hanzo_mcp/tools/filesystem/multi_edit.py +4 -8
- hanzo_mcp/tools/filesystem/read.py +12 -10
- hanzo_mcp/tools/filesystem/rules_tool.py +59 -43
- hanzo_mcp/tools/filesystem/search_tool.py +263 -207
- hanzo_mcp/tools/filesystem/symbols_tool.py +94 -54
- hanzo_mcp/tools/filesystem/tree.py +35 -33
- hanzo_mcp/tools/filesystem/unix_aliases.py +13 -18
- hanzo_mcp/tools/filesystem/watch.py +37 -36
- hanzo_mcp/tools/filesystem/write.py +4 -8
- hanzo_mcp/tools/jupyter/__init__.py +4 -4
- hanzo_mcp/tools/jupyter/base.py +4 -5
- hanzo_mcp/tools/jupyter/jupyter.py +67 -47
- hanzo_mcp/tools/jupyter/notebook_edit.py +4 -4
- hanzo_mcp/tools/jupyter/notebook_read.py +4 -7
- hanzo_mcp/tools/llm/__init__.py +5 -7
- hanzo_mcp/tools/llm/consensus_tool.py +72 -52
- hanzo_mcp/tools/llm/llm_manage.py +101 -60
- hanzo_mcp/tools/llm/llm_tool.py +226 -166
- hanzo_mcp/tools/llm/provider_tools.py +25 -26
- hanzo_mcp/tools/lsp/__init__.py +1 -1
- hanzo_mcp/tools/lsp/lsp_tool.py +228 -143
- hanzo_mcp/tools/mcp/__init__.py +2 -3
- hanzo_mcp/tools/mcp/mcp_add.py +27 -25
- hanzo_mcp/tools/mcp/mcp_remove.py +7 -8
- hanzo_mcp/tools/mcp/mcp_stats.py +23 -22
- hanzo_mcp/tools/mcp/mcp_tool.py +129 -98
- hanzo_mcp/tools/memory/__init__.py +39 -21
- hanzo_mcp/tools/memory/knowledge_tools.py +124 -99
- hanzo_mcp/tools/memory/memory_tools.py +90 -108
- hanzo_mcp/tools/search/__init__.py +7 -2
- hanzo_mcp/tools/search/find_tool.py +297 -212
- hanzo_mcp/tools/search/unified_search.py +366 -314
- hanzo_mcp/tools/shell/__init__.py +8 -7
- hanzo_mcp/tools/shell/auto_background.py +56 -49
- hanzo_mcp/tools/shell/base.py +1 -1
- hanzo_mcp/tools/shell/base_process.py +75 -75
- hanzo_mcp/tools/shell/bash_session.py +2 -2
- hanzo_mcp/tools/shell/bash_session_executor.py +4 -4
- hanzo_mcp/tools/shell/bash_tool.py +24 -31
- hanzo_mcp/tools/shell/command_executor.py +12 -12
- hanzo_mcp/tools/shell/logs.py +43 -33
- hanzo_mcp/tools/shell/npx.py +13 -13
- hanzo_mcp/tools/shell/npx_background.py +24 -21
- hanzo_mcp/tools/shell/npx_tool.py +18 -22
- hanzo_mcp/tools/shell/open.py +19 -21
- hanzo_mcp/tools/shell/pkill.py +31 -26
- hanzo_mcp/tools/shell/process_tool.py +32 -32
- hanzo_mcp/tools/shell/processes.py +57 -58
- hanzo_mcp/tools/shell/run_background.py +24 -25
- hanzo_mcp/tools/shell/run_command.py +5 -5
- hanzo_mcp/tools/shell/run_command_windows.py +5 -5
- hanzo_mcp/tools/shell/session_storage.py +3 -3
- hanzo_mcp/tools/shell/streaming_command.py +141 -126
- hanzo_mcp/tools/shell/uvx.py +24 -25
- hanzo_mcp/tools/shell/uvx_background.py +35 -33
- hanzo_mcp/tools/shell/uvx_tool.py +18 -22
- hanzo_mcp/tools/todo/__init__.py +6 -2
- hanzo_mcp/tools/todo/todo.py +50 -37
- hanzo_mcp/tools/todo/todo_read.py +5 -8
- hanzo_mcp/tools/todo/todo_write.py +5 -7
- hanzo_mcp/tools/vector/__init__.py +40 -28
- hanzo_mcp/tools/vector/ast_analyzer.py +176 -143
- hanzo_mcp/tools/vector/git_ingester.py +170 -179
- hanzo_mcp/tools/vector/index_tool.py +96 -44
- hanzo_mcp/tools/vector/infinity_store.py +283 -228
- hanzo_mcp/tools/vector/mock_infinity.py +39 -40
- hanzo_mcp/tools/vector/project_manager.py +88 -78
- hanzo_mcp/tools/vector/vector.py +59 -42
- hanzo_mcp/tools/vector/vector_index.py +30 -27
- hanzo_mcp/tools/vector/vector_search.py +64 -45
- hanzo_mcp/types.py +6 -4
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.0.dist-info}/METADATA +1 -1
- hanzo_mcp-0.8.0.dist-info/RECORD +185 -0
- hanzo_mcp-0.7.7.dist-info/RECORD +0 -182
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.0.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.0.dist-info}/entry_points.txt +0 -0
- {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.0.dist-info}/top_level.txt +0 -0
|
@@ -2,18 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import sqlite3
|
|
5
|
-
from typing import
|
|
5
|
+
from typing import (
|
|
6
|
+
Unpack,
|
|
7
|
+
Optional,
|
|
8
|
+
Annotated,
|
|
9
|
+
TypedDict,
|
|
10
|
+
final,
|
|
11
|
+
override,
|
|
12
|
+
)
|
|
6
13
|
from collections import deque
|
|
7
14
|
|
|
8
|
-
from mcp.server.fastmcp import Context as MCPContext
|
|
9
15
|
from pydantic import Field
|
|
16
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
10
17
|
|
|
11
18
|
from hanzo_mcp.tools.common.base import BaseTool
|
|
12
19
|
from hanzo_mcp.tools.common.context import create_tool_context
|
|
13
20
|
from hanzo_mcp.tools.common.permissions import PermissionManager
|
|
14
21
|
from hanzo_mcp.tools.database.database_manager import DatabaseManager
|
|
15
22
|
|
|
16
|
-
|
|
17
23
|
Query = Annotated[
|
|
18
24
|
str,
|
|
19
25
|
Field(
|
|
@@ -96,7 +102,9 @@ class GraphQueryParams(TypedDict, total=False):
|
|
|
96
102
|
class GraphQueryTool(BaseTool):
|
|
97
103
|
"""Tool for querying the graph database."""
|
|
98
104
|
|
|
99
|
-
def __init__(
|
|
105
|
+
def __init__(
|
|
106
|
+
self, permission_manager: PermissionManager, db_manager: DatabaseManager
|
|
107
|
+
):
|
|
100
108
|
"""Initialize the graph query tool.
|
|
101
109
|
|
|
102
110
|
Args:
|
|
@@ -172,12 +180,22 @@ Examples:
|
|
|
172
180
|
project_path = params.get("project_path")
|
|
173
181
|
|
|
174
182
|
# Validate query type
|
|
175
|
-
valid_queries = [
|
|
183
|
+
valid_queries = [
|
|
184
|
+
"neighbors",
|
|
185
|
+
"path",
|
|
186
|
+
"subgraph",
|
|
187
|
+
"connected",
|
|
188
|
+
"ancestors",
|
|
189
|
+
"descendants",
|
|
190
|
+
]
|
|
176
191
|
if query not in valid_queries:
|
|
177
192
|
return f"Error: Invalid query '{query}'. Must be one of: {', '.join(valid_queries)}"
|
|
178
193
|
|
|
179
194
|
# Validate required parameters
|
|
180
|
-
if
|
|
195
|
+
if (
|
|
196
|
+
query in ["neighbors", "subgraph", "connected", "ancestors", "descendants"]
|
|
197
|
+
and not node_id
|
|
198
|
+
):
|
|
181
199
|
return f"Error: node_id is required for '{query}' query"
|
|
182
200
|
|
|
183
201
|
if query == "path" and (not node_id or not target_id):
|
|
@@ -189,8 +207,9 @@ Examples:
|
|
|
189
207
|
project_db = self.db_manager.get_project_db(project_path)
|
|
190
208
|
else:
|
|
191
209
|
import os
|
|
210
|
+
|
|
192
211
|
project_db = self.db_manager.get_project_for_path(os.getcwd())
|
|
193
|
-
|
|
212
|
+
|
|
194
213
|
if not project_db:
|
|
195
214
|
return "Error: Could not find project database"
|
|
196
215
|
|
|
@@ -206,28 +225,43 @@ Examples:
|
|
|
206
225
|
|
|
207
226
|
try:
|
|
208
227
|
if query == "neighbors":
|
|
209
|
-
return self._query_neighbors(
|
|
228
|
+
return self._query_neighbors(
|
|
229
|
+
graph_conn, node_id, relationship, node_type, direction
|
|
230
|
+
)
|
|
210
231
|
elif query == "path":
|
|
211
232
|
return self._query_path(graph_conn, node_id, target_id, relationship)
|
|
212
233
|
elif query == "subgraph":
|
|
213
|
-
return self._query_subgraph(
|
|
234
|
+
return self._query_subgraph(
|
|
235
|
+
graph_conn, node_id, depth, relationship, node_type, direction
|
|
236
|
+
)
|
|
214
237
|
elif query == "connected":
|
|
215
|
-
return self._query_connected(
|
|
238
|
+
return self._query_connected(
|
|
239
|
+
graph_conn, node_id, relationship, node_type, direction
|
|
240
|
+
)
|
|
216
241
|
elif query == "ancestors":
|
|
217
|
-
return self._query_ancestors(
|
|
242
|
+
return self._query_ancestors(
|
|
243
|
+
graph_conn, node_id, depth, relationship, node_type
|
|
244
|
+
)
|
|
218
245
|
elif query == "descendants":
|
|
219
|
-
return self._query_descendants(
|
|
246
|
+
return self._query_descendants(
|
|
247
|
+
graph_conn, node_id, depth, relationship, node_type
|
|
248
|
+
)
|
|
220
249
|
|
|
221
250
|
except Exception as e:
|
|
222
251
|
await tool_ctx.error(f"Failed to execute query: {str(e)}")
|
|
223
252
|
return f"Error executing query: {str(e)}"
|
|
224
253
|
|
|
225
|
-
def _query_neighbors(
|
|
226
|
-
|
|
227
|
-
|
|
254
|
+
def _query_neighbors(
|
|
255
|
+
self,
|
|
256
|
+
conn: sqlite3.Connection,
|
|
257
|
+
node_id: str,
|
|
258
|
+
relationship: Optional[str],
|
|
259
|
+
node_type: Optional[str],
|
|
260
|
+
direction: str,
|
|
261
|
+
) -> str:
|
|
228
262
|
"""Get direct neighbors of a node."""
|
|
229
263
|
cursor = conn.cursor()
|
|
230
|
-
|
|
264
|
+
|
|
231
265
|
# Check if node exists
|
|
232
266
|
cursor.execute("SELECT type, properties FROM nodes WHERE id = ?", (node_id,))
|
|
233
267
|
node_info = cursor.fetchone()
|
|
@@ -235,76 +269,89 @@ Examples:
|
|
|
235
269
|
return f"Error: Node '{node_id}' not found"
|
|
236
270
|
|
|
237
271
|
neighbors = []
|
|
238
|
-
|
|
272
|
+
|
|
239
273
|
# Get outgoing edges
|
|
240
274
|
if direction in ["both", "outgoing"]:
|
|
241
275
|
query = """SELECT e.target, e.relationship, e.weight, n.type, n.properties
|
|
242
276
|
FROM edges e JOIN nodes n ON e.target = n.id
|
|
243
277
|
WHERE e.source = ?"""
|
|
244
278
|
params = [node_id]
|
|
245
|
-
|
|
279
|
+
|
|
246
280
|
if relationship:
|
|
247
281
|
query += " AND e.relationship = ?"
|
|
248
282
|
params.append(relationship)
|
|
249
283
|
if node_type:
|
|
250
284
|
query += " AND n.type = ?"
|
|
251
285
|
params.append(node_type)
|
|
252
|
-
|
|
286
|
+
|
|
253
287
|
cursor.execute(query, params)
|
|
254
288
|
for row in cursor.fetchall():
|
|
255
|
-
neighbors.append(
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
289
|
+
neighbors.append(
|
|
290
|
+
{
|
|
291
|
+
"direction": "outgoing",
|
|
292
|
+
"node_id": row[0],
|
|
293
|
+
"relationship": row[1],
|
|
294
|
+
"weight": row[2],
|
|
295
|
+
"node_type": row[3],
|
|
296
|
+
"properties": json.loads(row[4]) if row[4] else {},
|
|
297
|
+
}
|
|
298
|
+
)
|
|
299
|
+
|
|
264
300
|
# Get incoming edges
|
|
265
301
|
if direction in ["both", "incoming"]:
|
|
266
302
|
query = """SELECT e.source, e.relationship, e.weight, n.type, n.properties
|
|
267
303
|
FROM edges e JOIN nodes n ON e.source = n.id
|
|
268
304
|
WHERE e.target = ?"""
|
|
269
305
|
params = [node_id]
|
|
270
|
-
|
|
306
|
+
|
|
271
307
|
if relationship:
|
|
272
308
|
query += " AND e.relationship = ?"
|
|
273
309
|
params.append(relationship)
|
|
274
310
|
if node_type:
|
|
275
311
|
query += " AND n.type = ?"
|
|
276
312
|
params.append(node_type)
|
|
277
|
-
|
|
313
|
+
|
|
278
314
|
cursor.execute(query, params)
|
|
279
315
|
for row in cursor.fetchall():
|
|
280
|
-
neighbors.append(
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
316
|
+
neighbors.append(
|
|
317
|
+
{
|
|
318
|
+
"direction": "incoming",
|
|
319
|
+
"node_id": row[0],
|
|
320
|
+
"relationship": row[1],
|
|
321
|
+
"weight": row[2],
|
|
322
|
+
"node_type": row[3],
|
|
323
|
+
"properties": json.loads(row[4]) if row[4] else {},
|
|
324
|
+
}
|
|
325
|
+
)
|
|
326
|
+
|
|
289
327
|
if not neighbors:
|
|
290
328
|
return f"No neighbors found for node '{node_id}'"
|
|
291
|
-
|
|
329
|
+
|
|
292
330
|
# Format output
|
|
293
331
|
output = [f"Neighbors of '{node_id}' ({node_info[0]}):\n"]
|
|
294
332
|
for n in neighbors:
|
|
295
333
|
arrow = "<--" if n["direction"] == "incoming" else "-->"
|
|
296
|
-
output.append(
|
|
334
|
+
output.append(
|
|
335
|
+
f" {node_id} {arrow}[{n['relationship']}]--> {n['node_id']} ({n['node_type']})"
|
|
336
|
+
)
|
|
297
337
|
if n["properties"]:
|
|
298
|
-
output.append(
|
|
299
|
-
|
|
338
|
+
output.append(
|
|
339
|
+
f" Properties: {json.dumps(n['properties'], indent=6)[:100]}"
|
|
340
|
+
)
|
|
341
|
+
|
|
300
342
|
output.append(f"\nTotal neighbors: {len(neighbors)}")
|
|
301
343
|
return "\n".join(output)
|
|
302
344
|
|
|
303
|
-
def _query_path(
|
|
304
|
-
|
|
345
|
+
def _query_path(
|
|
346
|
+
self,
|
|
347
|
+
conn: sqlite3.Connection,
|
|
348
|
+
start: str,
|
|
349
|
+
end: str,
|
|
350
|
+
relationship: Optional[str],
|
|
351
|
+
) -> str:
|
|
305
352
|
"""Find shortest path between two nodes using BFS."""
|
|
306
353
|
cursor = conn.cursor()
|
|
307
|
-
|
|
354
|
+
|
|
308
355
|
# Check if nodes exist
|
|
309
356
|
cursor.execute("SELECT id FROM nodes WHERE id IN (?, ?)", (start, end))
|
|
310
357
|
existing = [row[0] for row in cursor.fetchall()]
|
|
@@ -312,224 +359,253 @@ Examples:
|
|
|
312
359
|
return f"Error: Start node '{start}' not found"
|
|
313
360
|
if end not in existing:
|
|
314
361
|
return f"Error: End node '{end}' not found"
|
|
315
|
-
|
|
362
|
+
|
|
316
363
|
# BFS to find shortest path
|
|
317
364
|
queue = deque([(start, [start])])
|
|
318
365
|
visited = {start}
|
|
319
|
-
|
|
366
|
+
|
|
320
367
|
while queue:
|
|
321
368
|
current, path = queue.popleft()
|
|
322
|
-
|
|
369
|
+
|
|
323
370
|
if current == end:
|
|
324
371
|
# Found path, get edge details
|
|
325
372
|
output = [f"Shortest path from '{start}' to '{end}':\n"]
|
|
326
|
-
|
|
373
|
+
|
|
327
374
|
for i in range(len(path) - 1):
|
|
328
375
|
src, tgt = path[i], path[i + 1]
|
|
329
|
-
|
|
376
|
+
|
|
330
377
|
# Get edge details
|
|
331
378
|
query = "SELECT relationship, weight FROM edges WHERE source = ? AND target = ?"
|
|
332
379
|
cursor.execute(query, (src, tgt))
|
|
333
380
|
edge = cursor.fetchone()
|
|
334
|
-
|
|
381
|
+
|
|
335
382
|
if edge:
|
|
336
383
|
output.append(f" {src} --[{edge[0]}]--> {tgt}")
|
|
337
384
|
else:
|
|
338
385
|
output.append(f" {src} --> {tgt}")
|
|
339
|
-
|
|
386
|
+
|
|
340
387
|
output.append(f"\nPath length: {len(path) - 1} edge(s)")
|
|
341
388
|
return "\n".join(output)
|
|
342
|
-
|
|
389
|
+
|
|
343
390
|
# Get neighbors
|
|
344
391
|
query = "SELECT target FROM edges WHERE source = ?"
|
|
345
392
|
params = [current]
|
|
346
393
|
if relationship:
|
|
347
394
|
query += " AND relationship = ?"
|
|
348
395
|
params.append(relationship)
|
|
349
|
-
|
|
396
|
+
|
|
350
397
|
cursor.execute(query, params)
|
|
351
|
-
|
|
398
|
+
|
|
352
399
|
for (neighbor,) in cursor.fetchall():
|
|
353
400
|
if neighbor not in visited:
|
|
354
401
|
visited.add(neighbor)
|
|
355
402
|
queue.append((neighbor, path + [neighbor]))
|
|
356
|
-
|
|
357
|
-
return f"No path found from '{start}' to '{end}'" + (f" with relationship '{relationship}'" if relationship else "")
|
|
358
403
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
404
|
+
return f"No path found from '{start}' to '{end}'" + (
|
|
405
|
+
f" with relationship '{relationship}'" if relationship else ""
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
def _query_subgraph(
|
|
409
|
+
self,
|
|
410
|
+
conn: sqlite3.Connection,
|
|
411
|
+
node_id: str,
|
|
412
|
+
depth: int,
|
|
413
|
+
relationship: Optional[str],
|
|
414
|
+
node_type: Optional[str],
|
|
415
|
+
direction: str,
|
|
416
|
+
) -> str:
|
|
362
417
|
"""Get subgraph around a node up to specified depth."""
|
|
363
418
|
cursor = conn.cursor()
|
|
364
|
-
|
|
419
|
+
|
|
365
420
|
# Check if node exists
|
|
366
421
|
cursor.execute("SELECT type FROM nodes WHERE id = ?", (node_id,))
|
|
367
422
|
if not cursor.fetchone():
|
|
368
423
|
return f"Error: Node '{node_id}' not found"
|
|
369
|
-
|
|
424
|
+
|
|
370
425
|
# BFS to collect nodes and edges
|
|
371
426
|
nodes = {node_id: 0} # node_id -> depth
|
|
372
427
|
edges = set() # (source, target, relationship)
|
|
373
428
|
queue = deque([(node_id, 0)])
|
|
374
|
-
|
|
429
|
+
|
|
375
430
|
while queue:
|
|
376
431
|
current, current_depth = queue.popleft()
|
|
377
|
-
|
|
432
|
+
|
|
378
433
|
if current_depth >= depth:
|
|
379
434
|
continue
|
|
380
|
-
|
|
435
|
+
|
|
381
436
|
# Get edges based on direction
|
|
382
437
|
if direction in ["both", "outgoing"]:
|
|
383
438
|
query = """SELECT e.target, e.relationship, n.type
|
|
384
439
|
FROM edges e JOIN nodes n ON e.target = n.id
|
|
385
440
|
WHERE e.source = ?"""
|
|
386
441
|
params = [current]
|
|
387
|
-
|
|
442
|
+
|
|
388
443
|
if relationship:
|
|
389
444
|
query += " AND e.relationship = ?"
|
|
390
445
|
params.append(relationship)
|
|
391
446
|
if node_type:
|
|
392
447
|
query += " AND n.type = ?"
|
|
393
448
|
params.append(node_type)
|
|
394
|
-
|
|
449
|
+
|
|
395
450
|
cursor.execute(query, params)
|
|
396
|
-
|
|
451
|
+
|
|
397
452
|
for target, rel, _ in cursor.fetchall():
|
|
398
453
|
edges.add((current, target, rel))
|
|
399
454
|
if target not in nodes or nodes[target] > current_depth + 1:
|
|
400
455
|
nodes[target] = current_depth + 1
|
|
401
456
|
queue.append((target, current_depth + 1))
|
|
402
|
-
|
|
457
|
+
|
|
403
458
|
if direction in ["both", "incoming"]:
|
|
404
459
|
query = """SELECT e.source, e.relationship, n.type
|
|
405
460
|
FROM edges e JOIN nodes n ON e.source = n.id
|
|
406
461
|
WHERE e.target = ?"""
|
|
407
462
|
params = [current]
|
|
408
|
-
|
|
463
|
+
|
|
409
464
|
if relationship:
|
|
410
465
|
query += " AND e.relationship = ?"
|
|
411
466
|
params.append(relationship)
|
|
412
467
|
if node_type:
|
|
413
468
|
query += " AND n.type = ?"
|
|
414
469
|
params.append(node_type)
|
|
415
|
-
|
|
470
|
+
|
|
416
471
|
cursor.execute(query, params)
|
|
417
|
-
|
|
472
|
+
|
|
418
473
|
for source, rel, _ in cursor.fetchall():
|
|
419
474
|
edges.add((source, current, rel))
|
|
420
475
|
if source not in nodes or nodes[source] > current_depth + 1:
|
|
421
476
|
nodes[source] = current_depth + 1
|
|
422
477
|
queue.append((source, current_depth + 1))
|
|
423
|
-
|
|
478
|
+
|
|
424
479
|
# Format output
|
|
425
480
|
output = [f"Subgraph around '{node_id}' (depth={depth}):\n"]
|
|
426
481
|
output.append(f"Nodes ({len(nodes)}):")
|
|
427
|
-
|
|
482
|
+
|
|
428
483
|
# Get node details
|
|
429
484
|
for node, d in sorted(nodes.items(), key=lambda x: (x[1], x[0])):
|
|
430
485
|
cursor.execute("SELECT type FROM nodes WHERE id = ?", (node,))
|
|
431
486
|
node_type = cursor.fetchone()[0]
|
|
432
487
|
output.append(f" [{d}] {node} ({node_type})")
|
|
433
|
-
|
|
488
|
+
|
|
434
489
|
output.append(f"\nEdges ({len(edges)}):")
|
|
435
490
|
for src, tgt, rel in sorted(edges):
|
|
436
491
|
output.append(f" {src} --[{rel}]--> {tgt}")
|
|
437
|
-
|
|
492
|
+
|
|
438
493
|
return "\n".join(output)
|
|
439
494
|
|
|
440
|
-
def _query_connected(
|
|
441
|
-
|
|
442
|
-
|
|
495
|
+
def _query_connected(
|
|
496
|
+
self,
|
|
497
|
+
conn: sqlite3.Connection,
|
|
498
|
+
node_id: str,
|
|
499
|
+
relationship: Optional[str],
|
|
500
|
+
node_type: Optional[str],
|
|
501
|
+
direction: str,
|
|
502
|
+
) -> str:
|
|
443
503
|
"""Find all nodes connected to a node (transitive closure)."""
|
|
444
504
|
cursor = conn.cursor()
|
|
445
|
-
|
|
505
|
+
|
|
446
506
|
# Check if node exists
|
|
447
507
|
cursor.execute("SELECT type FROM nodes WHERE id = ?", (node_id,))
|
|
448
508
|
if not cursor.fetchone():
|
|
449
509
|
return f"Error: Node '{node_id}' not found"
|
|
450
|
-
|
|
510
|
+
|
|
451
511
|
# BFS to find all connected nodes
|
|
452
512
|
visited = {node_id}
|
|
453
513
|
queue = deque([node_id])
|
|
454
514
|
connections = [] # (node_id, node_type, distance)
|
|
455
515
|
distance = {node_id: 0}
|
|
456
|
-
|
|
516
|
+
|
|
457
517
|
while queue:
|
|
458
518
|
current = queue.popleft()
|
|
459
519
|
current_dist = distance[current]
|
|
460
|
-
|
|
520
|
+
|
|
461
521
|
# Get edges based on direction
|
|
462
522
|
neighbors = []
|
|
463
|
-
|
|
523
|
+
|
|
464
524
|
if direction in ["both", "outgoing"]:
|
|
465
525
|
query = """SELECT e.target, n.type FROM edges e
|
|
466
526
|
JOIN nodes n ON e.target = n.id
|
|
467
527
|
WHERE e.source = ?"""
|
|
468
528
|
params = [current]
|
|
469
|
-
|
|
529
|
+
|
|
470
530
|
if relationship:
|
|
471
531
|
query += " AND e.relationship = ?"
|
|
472
532
|
params.append(relationship)
|
|
473
533
|
if node_type:
|
|
474
534
|
query += " AND n.type = ?"
|
|
475
535
|
params.append(node_type)
|
|
476
|
-
|
|
536
|
+
|
|
477
537
|
cursor.execute(query, params)
|
|
478
538
|
neighbors.extend(cursor.fetchall())
|
|
479
|
-
|
|
539
|
+
|
|
480
540
|
if direction in ["both", "incoming"]:
|
|
481
541
|
query = """SELECT e.source, n.type FROM edges e
|
|
482
542
|
JOIN nodes n ON e.source = n.id
|
|
483
543
|
WHERE e.target = ?"""
|
|
484
544
|
params = [current]
|
|
485
|
-
|
|
545
|
+
|
|
486
546
|
if relationship:
|
|
487
547
|
query += " AND e.relationship = ?"
|
|
488
548
|
params.append(relationship)
|
|
489
549
|
if node_type:
|
|
490
550
|
query += " AND n.type = ?"
|
|
491
551
|
params.append(node_type)
|
|
492
|
-
|
|
552
|
+
|
|
493
553
|
cursor.execute(query, params)
|
|
494
554
|
neighbors.extend(cursor.fetchall())
|
|
495
|
-
|
|
555
|
+
|
|
496
556
|
for neighbor, n_type in neighbors:
|
|
497
557
|
if neighbor not in visited:
|
|
498
558
|
visited.add(neighbor)
|
|
499
559
|
queue.append(neighbor)
|
|
500
560
|
distance[neighbor] = current_dist + 1
|
|
501
561
|
connections.append((neighbor, n_type, current_dist + 1))
|
|
502
|
-
|
|
562
|
+
|
|
503
563
|
if not connections:
|
|
504
564
|
return f"No connected nodes found for '{node_id}'"
|
|
505
|
-
|
|
565
|
+
|
|
506
566
|
# Format output
|
|
507
567
|
output = [f"Nodes connected to '{node_id}' ({direction}):"]
|
|
508
568
|
output.append(f"\nTotal connected: {len(connections)}\n")
|
|
509
|
-
|
|
569
|
+
|
|
510
570
|
# Group by distance
|
|
511
571
|
by_distance = {}
|
|
512
572
|
for node, n_type, dist in connections:
|
|
513
573
|
if dist not in by_distance:
|
|
514
574
|
by_distance[dist] = []
|
|
515
575
|
by_distance[dist].append((node, n_type))
|
|
516
|
-
|
|
576
|
+
|
|
517
577
|
for dist in sorted(by_distance.keys()):
|
|
518
578
|
output.append(f"Distance {dist}:")
|
|
519
579
|
for node, n_type in sorted(by_distance[dist]):
|
|
520
580
|
output.append(f" {node} ({n_type})")
|
|
521
|
-
|
|
581
|
+
|
|
522
582
|
return "\n".join(output)
|
|
523
583
|
|
|
524
|
-
def _query_ancestors(
|
|
525
|
-
|
|
584
|
+
def _query_ancestors(
|
|
585
|
+
self,
|
|
586
|
+
conn: sqlite3.Connection,
|
|
587
|
+
node_id: str,
|
|
588
|
+
depth: int,
|
|
589
|
+
relationship: Optional[str],
|
|
590
|
+
node_type: Optional[str],
|
|
591
|
+
) -> str:
|
|
526
592
|
"""Find nodes that point TO this node (incoming edges only)."""
|
|
527
|
-
return self._query_subgraph(
|
|
593
|
+
return self._query_subgraph(
|
|
594
|
+
conn, node_id, depth, relationship, node_type, "incoming"
|
|
595
|
+
)
|
|
528
596
|
|
|
529
|
-
def _query_descendants(
|
|
530
|
-
|
|
597
|
+
def _query_descendants(
|
|
598
|
+
self,
|
|
599
|
+
conn: sqlite3.Connection,
|
|
600
|
+
node_id: str,
|
|
601
|
+
depth: int,
|
|
602
|
+
relationship: Optional[str],
|
|
603
|
+
node_type: Optional[str],
|
|
604
|
+
) -> str:
|
|
531
605
|
"""Find nodes that this node points TO (outgoing edges only)."""
|
|
532
|
-
return self._query_subgraph(
|
|
606
|
+
return self._query_subgraph(
|
|
607
|
+
conn, node_id, depth, relationship, node_type, "outgoing"
|
|
608
|
+
)
|
|
533
609
|
|
|
534
610
|
def register(self, mcp_server) -> None:
|
|
535
611
|
"""Register this tool with the MCP server."""
|