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.

Files changed (178) hide show
  1. hanzo_mcp/__init__.py +6 -0
  2. hanzo_mcp/__main__.py +1 -1
  3. hanzo_mcp/analytics/__init__.py +2 -2
  4. hanzo_mcp/analytics/posthog_analytics.py +76 -82
  5. hanzo_mcp/cli.py +31 -36
  6. hanzo_mcp/cli_enhanced.py +94 -72
  7. hanzo_mcp/cli_plugin.py +27 -17
  8. hanzo_mcp/config/__init__.py +2 -2
  9. hanzo_mcp/config/settings.py +112 -88
  10. hanzo_mcp/config/tool_config.py +32 -34
  11. hanzo_mcp/dev_server.py +66 -67
  12. hanzo_mcp/prompts/__init__.py +94 -12
  13. hanzo_mcp/prompts/enhanced_prompts.py +809 -0
  14. hanzo_mcp/prompts/example_custom_prompt.py +6 -5
  15. hanzo_mcp/prompts/project_todo_reminder.py +0 -1
  16. hanzo_mcp/prompts/tool_explorer.py +10 -7
  17. hanzo_mcp/server.py +17 -21
  18. hanzo_mcp/server_enhanced.py +15 -22
  19. hanzo_mcp/tools/__init__.py +56 -28
  20. hanzo_mcp/tools/agent/__init__.py +16 -19
  21. hanzo_mcp/tools/agent/agent.py +82 -65
  22. hanzo_mcp/tools/agent/agent_tool.py +152 -122
  23. hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +66 -62
  24. hanzo_mcp/tools/agent/clarification_protocol.py +55 -50
  25. hanzo_mcp/tools/agent/clarification_tool.py +11 -10
  26. hanzo_mcp/tools/agent/claude_cli_tool.py +21 -20
  27. hanzo_mcp/tools/agent/claude_desktop_auth.py +130 -144
  28. hanzo_mcp/tools/agent/cli_agent_base.py +59 -53
  29. hanzo_mcp/tools/agent/code_auth.py +102 -107
  30. hanzo_mcp/tools/agent/code_auth_tool.py +28 -27
  31. hanzo_mcp/tools/agent/codex_cli_tool.py +20 -19
  32. hanzo_mcp/tools/agent/critic_tool.py +86 -73
  33. hanzo_mcp/tools/agent/gemini_cli_tool.py +21 -20
  34. hanzo_mcp/tools/agent/grok_cli_tool.py +21 -20
  35. hanzo_mcp/tools/agent/iching_tool.py +404 -139
  36. hanzo_mcp/tools/agent/network_tool.py +89 -73
  37. hanzo_mcp/tools/agent/prompt.py +2 -1
  38. hanzo_mcp/tools/agent/review_tool.py +101 -98
  39. hanzo_mcp/tools/agent/swarm_alias.py +87 -0
  40. hanzo_mcp/tools/agent/swarm_tool.py +246 -161
  41. hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +134 -92
  42. hanzo_mcp/tools/agent/tool_adapter.py +21 -11
  43. hanzo_mcp/tools/common/__init__.py +1 -1
  44. hanzo_mcp/tools/common/base.py +3 -5
  45. hanzo_mcp/tools/common/batch_tool.py +46 -39
  46. hanzo_mcp/tools/common/config_tool.py +120 -84
  47. hanzo_mcp/tools/common/context.py +1 -5
  48. hanzo_mcp/tools/common/context_fix.py +5 -3
  49. hanzo_mcp/tools/common/critic_tool.py +4 -8
  50. hanzo_mcp/tools/common/decorators.py +58 -56
  51. hanzo_mcp/tools/common/enhanced_base.py +29 -32
  52. hanzo_mcp/tools/common/fastmcp_pagination.py +91 -94
  53. hanzo_mcp/tools/common/forgiving_edit.py +91 -87
  54. hanzo_mcp/tools/common/mode.py +15 -17
  55. hanzo_mcp/tools/common/mode_loader.py +27 -24
  56. hanzo_mcp/tools/common/paginated_base.py +61 -53
  57. hanzo_mcp/tools/common/paginated_response.py +72 -79
  58. hanzo_mcp/tools/common/pagination.py +50 -53
  59. hanzo_mcp/tools/common/permissions.py +4 -4
  60. hanzo_mcp/tools/common/personality.py +186 -138
  61. hanzo_mcp/tools/common/plugin_loader.py +54 -54
  62. hanzo_mcp/tools/common/stats.py +65 -47
  63. hanzo_mcp/tools/common/test_helpers.py +31 -0
  64. hanzo_mcp/tools/common/thinking_tool.py +4 -8
  65. hanzo_mcp/tools/common/tool_disable.py +17 -12
  66. hanzo_mcp/tools/common/tool_enable.py +13 -14
  67. hanzo_mcp/tools/common/tool_list.py +36 -28
  68. hanzo_mcp/tools/common/truncate.py +23 -23
  69. hanzo_mcp/tools/config/__init__.py +4 -4
  70. hanzo_mcp/tools/config/config_tool.py +42 -29
  71. hanzo_mcp/tools/config/index_config.py +37 -34
  72. hanzo_mcp/tools/config/mode_tool.py +175 -55
  73. hanzo_mcp/tools/database/__init__.py +15 -12
  74. hanzo_mcp/tools/database/database_manager.py +77 -75
  75. hanzo_mcp/tools/database/graph.py +137 -91
  76. hanzo_mcp/tools/database/graph_add.py +30 -18
  77. hanzo_mcp/tools/database/graph_query.py +178 -102
  78. hanzo_mcp/tools/database/graph_remove.py +33 -28
  79. hanzo_mcp/tools/database/graph_search.py +97 -75
  80. hanzo_mcp/tools/database/graph_stats.py +91 -59
  81. hanzo_mcp/tools/database/sql.py +107 -79
  82. hanzo_mcp/tools/database/sql_query.py +30 -24
  83. hanzo_mcp/tools/database/sql_search.py +29 -25
  84. hanzo_mcp/tools/database/sql_stats.py +47 -35
  85. hanzo_mcp/tools/editor/neovim_command.py +25 -28
  86. hanzo_mcp/tools/editor/neovim_edit.py +21 -23
  87. hanzo_mcp/tools/editor/neovim_session.py +60 -54
  88. hanzo_mcp/tools/filesystem/__init__.py +31 -30
  89. hanzo_mcp/tools/filesystem/ast_multi_edit.py +329 -249
  90. hanzo_mcp/tools/filesystem/ast_tool.py +4 -4
  91. hanzo_mcp/tools/filesystem/base.py +1 -1
  92. hanzo_mcp/tools/filesystem/batch_search.py +316 -224
  93. hanzo_mcp/tools/filesystem/content_replace.py +4 -4
  94. hanzo_mcp/tools/filesystem/diff.py +71 -59
  95. hanzo_mcp/tools/filesystem/directory_tree.py +7 -7
  96. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +49 -37
  97. hanzo_mcp/tools/filesystem/edit.py +4 -4
  98. hanzo_mcp/tools/filesystem/find.py +173 -80
  99. hanzo_mcp/tools/filesystem/find_files.py +73 -52
  100. hanzo_mcp/tools/filesystem/git_search.py +157 -104
  101. hanzo_mcp/tools/filesystem/grep.py +8 -8
  102. hanzo_mcp/tools/filesystem/multi_edit.py +4 -8
  103. hanzo_mcp/tools/filesystem/read.py +12 -10
  104. hanzo_mcp/tools/filesystem/rules_tool.py +59 -43
  105. hanzo_mcp/tools/filesystem/search_tool.py +263 -207
  106. hanzo_mcp/tools/filesystem/symbols_tool.py +94 -54
  107. hanzo_mcp/tools/filesystem/tree.py +35 -33
  108. hanzo_mcp/tools/filesystem/unix_aliases.py +13 -18
  109. hanzo_mcp/tools/filesystem/watch.py +37 -36
  110. hanzo_mcp/tools/filesystem/write.py +4 -8
  111. hanzo_mcp/tools/jupyter/__init__.py +4 -4
  112. hanzo_mcp/tools/jupyter/base.py +4 -5
  113. hanzo_mcp/tools/jupyter/jupyter.py +67 -47
  114. hanzo_mcp/tools/jupyter/notebook_edit.py +4 -4
  115. hanzo_mcp/tools/jupyter/notebook_read.py +4 -7
  116. hanzo_mcp/tools/llm/__init__.py +5 -7
  117. hanzo_mcp/tools/llm/consensus_tool.py +72 -52
  118. hanzo_mcp/tools/llm/llm_manage.py +101 -60
  119. hanzo_mcp/tools/llm/llm_tool.py +226 -166
  120. hanzo_mcp/tools/llm/provider_tools.py +25 -26
  121. hanzo_mcp/tools/lsp/__init__.py +1 -1
  122. hanzo_mcp/tools/lsp/lsp_tool.py +228 -143
  123. hanzo_mcp/tools/mcp/__init__.py +2 -3
  124. hanzo_mcp/tools/mcp/mcp_add.py +27 -25
  125. hanzo_mcp/tools/mcp/mcp_remove.py +7 -8
  126. hanzo_mcp/tools/mcp/mcp_stats.py +23 -22
  127. hanzo_mcp/tools/mcp/mcp_tool.py +129 -98
  128. hanzo_mcp/tools/memory/__init__.py +39 -21
  129. hanzo_mcp/tools/memory/knowledge_tools.py +124 -99
  130. hanzo_mcp/tools/memory/memory_tools.py +90 -108
  131. hanzo_mcp/tools/search/__init__.py +7 -2
  132. hanzo_mcp/tools/search/find_tool.py +297 -212
  133. hanzo_mcp/tools/search/unified_search.py +366 -314
  134. hanzo_mcp/tools/shell/__init__.py +8 -7
  135. hanzo_mcp/tools/shell/auto_background.py +56 -49
  136. hanzo_mcp/tools/shell/base.py +1 -1
  137. hanzo_mcp/tools/shell/base_process.py +75 -75
  138. hanzo_mcp/tools/shell/bash_session.py +2 -2
  139. hanzo_mcp/tools/shell/bash_session_executor.py +4 -4
  140. hanzo_mcp/tools/shell/bash_tool.py +24 -31
  141. hanzo_mcp/tools/shell/command_executor.py +12 -12
  142. hanzo_mcp/tools/shell/logs.py +43 -33
  143. hanzo_mcp/tools/shell/npx.py +13 -13
  144. hanzo_mcp/tools/shell/npx_background.py +24 -21
  145. hanzo_mcp/tools/shell/npx_tool.py +18 -22
  146. hanzo_mcp/tools/shell/open.py +19 -21
  147. hanzo_mcp/tools/shell/pkill.py +31 -26
  148. hanzo_mcp/tools/shell/process_tool.py +32 -32
  149. hanzo_mcp/tools/shell/processes.py +57 -58
  150. hanzo_mcp/tools/shell/run_background.py +24 -25
  151. hanzo_mcp/tools/shell/run_command.py +5 -5
  152. hanzo_mcp/tools/shell/run_command_windows.py +5 -5
  153. hanzo_mcp/tools/shell/session_storage.py +3 -3
  154. hanzo_mcp/tools/shell/streaming_command.py +141 -126
  155. hanzo_mcp/tools/shell/uvx.py +24 -25
  156. hanzo_mcp/tools/shell/uvx_background.py +35 -33
  157. hanzo_mcp/tools/shell/uvx_tool.py +18 -22
  158. hanzo_mcp/tools/todo/__init__.py +6 -2
  159. hanzo_mcp/tools/todo/todo.py +50 -37
  160. hanzo_mcp/tools/todo/todo_read.py +5 -8
  161. hanzo_mcp/tools/todo/todo_write.py +5 -7
  162. hanzo_mcp/tools/vector/__init__.py +40 -28
  163. hanzo_mcp/tools/vector/ast_analyzer.py +176 -143
  164. hanzo_mcp/tools/vector/git_ingester.py +170 -179
  165. hanzo_mcp/tools/vector/index_tool.py +96 -44
  166. hanzo_mcp/tools/vector/infinity_store.py +283 -228
  167. hanzo_mcp/tools/vector/mock_infinity.py +39 -40
  168. hanzo_mcp/tools/vector/project_manager.py +88 -78
  169. hanzo_mcp/tools/vector/vector.py +59 -42
  170. hanzo_mcp/tools/vector/vector_index.py +30 -27
  171. hanzo_mcp/tools/vector/vector_search.py +64 -45
  172. hanzo_mcp/types.py +6 -4
  173. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.0.dist-info}/METADATA +1 -1
  174. hanzo_mcp-0.8.0.dist-info/RECORD +185 -0
  175. hanzo_mcp-0.7.7.dist-info/RECORD +0 -182
  176. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.0.dist-info}/WHEEL +0 -0
  177. {hanzo_mcp-0.7.7.dist-info → hanzo_mcp-0.8.0.dist-info}/entry_points.txt +0 -0
  178. {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 Annotated, Optional, TypedDict, Unpack, final, override, List, Dict, Any
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__(self, permission_manager: PermissionManager, db_manager: DatabaseManager):
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 = ["neighbors", "path", "subgraph", "connected", "ancestors", "descendants"]
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 query in ["neighbors", "subgraph", "connected", "ancestors", "descendants"] and not node_id:
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(graph_conn, node_id, relationship, node_type, direction)
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(graph_conn, node_id, depth, relationship, node_type, direction)
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(graph_conn, node_id, relationship, node_type, direction)
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(graph_conn, node_id, depth, relationship, node_type)
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(graph_conn, node_id, depth, relationship, node_type)
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(self, conn: sqlite3.Connection, node_id: str,
226
- relationship: Optional[str], node_type: Optional[str],
227
- direction: str) -> str:
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
- "direction": "outgoing",
257
- "node_id": row[0],
258
- "relationship": row[1],
259
- "weight": row[2],
260
- "node_type": row[3],
261
- "properties": json.loads(row[4]) if row[4] else {}
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
- "direction": "incoming",
282
- "node_id": row[0],
283
- "relationship": row[1],
284
- "weight": row[2],
285
- "node_type": row[3],
286
- "properties": json.loads(row[4]) if row[4] else {}
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(f" {node_id} {arrow}[{n['relationship']}]--> {n['node_id']} ({n['node_type']})")
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(f" Properties: {json.dumps(n['properties'], indent=6)[:100]}")
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(self, conn: sqlite3.Connection, start: str, end: str,
304
- relationship: Optional[str]) -> str:
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
- def _query_subgraph(self, conn: sqlite3.Connection, node_id: str, depth: int,
360
- relationship: Optional[str], node_type: Optional[str],
361
- direction: str) -> str:
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(self, conn: sqlite3.Connection, node_id: str,
441
- relationship: Optional[str], node_type: Optional[str],
442
- direction: str) -> str:
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(self, conn: sqlite3.Connection, node_id: str, depth: int,
525
- relationship: Optional[str], node_type: Optional[str]) -> str:
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(conn, node_id, depth, relationship, node_type, "incoming")
593
+ return self._query_subgraph(
594
+ conn, node_id, depth, relationship, node_type, "incoming"
595
+ )
528
596
 
529
- def _query_descendants(self, conn: sqlite3.Connection, node_id: str, depth: int,
530
- relationship: Optional[str], node_type: Optional[str]) -> str:
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(conn, node_id, depth, relationship, node_type, "outgoing")
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."""