Skip to content

Commit b63749d

Browse files
committed
Add graph tool to MCP server
1 parent 0ab47a5 commit b63749d

5 files changed

Lines changed: 122 additions & 1 deletion

File tree

docs/mcp-integration.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,28 @@ Generate a project structure using specified definition and options.
6969
- `mappings` (optional): Variable mappings for template substitution
7070
- `structures_path` (optional): Custom path to structure definitions
7171

72-
### 4. validate_structure
72+
### 4. graph_structure
73+
Visualize dependency graphs from `folders[].struct` references.
74+
75+
```json
76+
{
77+
"name": "graph_structure",
78+
"arguments": {
79+
"structure_definition": "project/python",
80+
"structures_path": "/path/to/custom/structures",
81+
"include_all": false,
82+
"output_format": "mermaid"
83+
}
84+
}
85+
```
86+
87+
**Parameters:**
88+
- `structure_definition` (optional): Name or local YAML path to graph. Required unless `include_all` is `true`
89+
- `structures_path` (optional): Custom path to structure definitions
90+
- `include_all` (optional): Include all available structures instead of a single root (default: `false`)
91+
- `output_format` (optional): Output format - `text`, `json`, or `mermaid` (default: `text`)
92+
93+
### 5. validate_structure
7394
Validate a structure configuration YAML file.
7495

7596
```json

structkit/commands/mcp.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ def execute(self, args):
4949
print(" - list_structures: List all available structure definitions")
5050
print(" - get_structure_info: Get detailed information about a structure")
5151
print(" - generate_structure: Generate structures with various options")
52+
print(" - graph_structure: Visualize structure dependency graphs")
5253
print(" - validate_structure: Validate structure configuration files")
5354
print("\nExamples:")
5455
print(" structkit mcp --server --transport stdio --debug")

structkit/mcp_server.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
2. Getting detailed information about structures
77
3. Generating structures with various options
88
4. Validating structure configurations
9+
5. Visualizing structure dependency graphs
910
"""
1011
import asyncio
1112
import logging
@@ -17,6 +18,7 @@
1718
from fastmcp import FastMCP
1819

1920
from structkit.commands.generate import GenerateCommand
21+
from structkit.commands.graph import GraphCommand
2022
from structkit.commands.validate import ValidateCommand
2123
from structkit import __version__
2224

@@ -167,6 +169,30 @@ class Args:
167169
return f"Dry run completed for structure '{structure_definition}' at '{base_path}'"
168170
return f"Structure '{structure_definition}' generated successfully at '{base_path}'"
169171

172+
def _graph_structure_logic(
173+
self,
174+
structure_definition: Optional[str] = None,
175+
structures_path: Optional[str] = None,
176+
include_all: bool = False,
177+
output_format: str = "text",
178+
) -> str:
179+
if not include_all and not structure_definition:
180+
return "Error: structure_definition is required unless include_all is true"
181+
182+
if output_format not in {"text", "json", "mermaid"}:
183+
return "Error: output_format must be one of: text, json, mermaid"
184+
185+
import argparse
186+
dummy_parser = argparse.ArgumentParser()
187+
command = GraphCommand(dummy_parser)
188+
graph = command.build_graph(structure_definition, structures_path, include_all)
189+
190+
if output_format == "json":
191+
return command.format_json(graph)
192+
if output_format == "mermaid":
193+
return command.format_mermaid(graph)
194+
return command.format_text(graph)
195+
170196
def _validate_structure_logic(self, yaml_file: Optional[str]) -> str:
171197
if not yaml_file:
172198
return "Error: yaml_file is required"
@@ -247,6 +273,32 @@ async def generate_structure(
247273
self.logger.debug(f"MCP response: generate_structure len={len(result)} preview=\n{preview}")
248274
return result
249275

276+
@self.app.tool(name="graph_structure", description="Visualize structure dependency graphs from folders[].struct references")
277+
async def graph_structure(
278+
structure_definition: Optional[str] = None,
279+
structures_path: Optional[str] = None,
280+
include_all: bool = False,
281+
output_format: str = "text",
282+
) -> str:
283+
self.logger.debug(
284+
"MCP request: graph_structure args=%s",
285+
{
286+
"structure_definition": structure_definition,
287+
"structures_path": structures_path,
288+
"include_all": include_all,
289+
"output_format": output_format,
290+
},
291+
)
292+
result = self._graph_structure_logic(
293+
structure_definition,
294+
structures_path,
295+
include_all,
296+
output_format,
297+
)
298+
preview = result if len(result) <= 1000 else result[:1000] + f"... [truncated {len(result)-1000} chars]"
299+
self.logger.debug(f"MCP response: graph_structure len={len(result)} preview=\n{preview}")
300+
return result
301+
250302
@self.app.tool(name="validate_structure", description="Validate a structure configuration YAML file")
251303
async def validate_structure(yaml_file: str) -> str:
252304
self.logger.debug(f"MCP request: validate_structure args={{'yaml_file': {yaml_file!r}}}")

tests/test_commands_more.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,17 @@ async def fake_start(args):
167167
mock_start.assert_called_once()
168168

169169

170+
def test_mcp_command_lists_graph_tool(parser):
171+
command = MCPCommand(parser)
172+
args = parser.parse_args([])
173+
174+
with patch('builtins.print') as mock_print:
175+
command.execute(args)
176+
177+
printed = "\n".join(call.args[0] for call in mock_print.call_args_list)
178+
assert 'graph_structure' in printed
179+
180+
170181
# ValidateCommand error-path tests on helpers
171182

172183
def test_validate_structure_config_errors(parser):

tests/test_mcp_integration.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""
22
Tests for MCP (Model Context Protocol) integration with FastMCP stdio transport.
33
"""
4+
import json
45
import os
56
import tempfile
67
import unittest
@@ -43,6 +44,41 @@ def test_generate_structure_logic(self):
4344
)
4445
self.assertIsInstance(text, str)
4546

47+
def test_graph_structure_logic(self):
48+
with tempfile.TemporaryDirectory() as temp_dir:
49+
app_path = os.path.join(temp_dir, "app.yaml")
50+
library_path = os.path.join(temp_dir, "library.yaml")
51+
with open(app_path, "w") as f:
52+
yaml.dump({
53+
"files": [],
54+
"folders": [
55+
{"src": {"struct": "library"}}
56+
],
57+
}, f)
58+
with open(library_path, "w") as f:
59+
yaml.dump({"files": []}, f)
60+
61+
text = self.server._graph_structure_logic("app", temp_dir)
62+
self.assertIn("app", text)
63+
self.assertIn("library", text)
64+
65+
payload = json.loads(
66+
self.server._graph_structure_logic("app", temp_dir, output_format="json")
67+
)
68+
self.assertEqual(payload["edges"], [{"from": "app", "to": "library"}])
69+
70+
mermaid = self.server._graph_structure_logic(
71+
"app", temp_dir, output_format="mermaid"
72+
)
73+
self.assertIn('n_app["app"] --> n_library["library"]', mermaid)
74+
75+
def test_graph_structure_logic_validates_arguments(self):
76+
text = self.server._graph_structure_logic()
77+
self.assertIn("structure_definition is required", text)
78+
79+
text = self.server._graph_structure_logic("app", output_format="dot")
80+
self.assertIn("output_format must be one of", text)
81+
4682
def test_validate_structure_logic(self):
4783
# Missing yaml_file
4884
text = self.server._validate_structure_logic(None)

0 commit comments

Comments
 (0)