mcp-win-stdio-arch 0.3.0__tar.gz

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.
@@ -0,0 +1,63 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.so
5
+ .Python
6
+ build/
7
+ develop-eggs/
8
+ dist/
9
+ downloads/
10
+ eggs/
11
+ .eggs/
12
+ lib/
13
+ lib64/
14
+ parts/
15
+ sdist/
16
+ var/
17
+ wheels/
18
+ share/python-wheels/
19
+ *.egg-info/
20
+ .installed.cfg
21
+ *.egg
22
+ MANIFEST
23
+
24
+ *.manifest
25
+ *.spec
26
+ api.txt
27
+ pypi-api.txt
28
+
29
+
30
+ pip-log.txt
31
+ pip-delete-this-directory.txt
32
+ pypi api.txt
33
+ htmlcov/
34
+ .tox/
35
+ .nox/
36
+ .coverage
37
+ .coverage.*
38
+ .cache
39
+ nosetests.xml
40
+ coverage.xml
41
+ *.cover
42
+ *.py,cover
43
+ .hypothesis/
44
+ .pytest_cache/
45
+ cover/
46
+
47
+ *.mo
48
+ *.pot
49
+
50
+ .env
51
+ .venv
52
+ env/
53
+ venv/
54
+ ENV/
55
+ env.bak/
56
+ venv.bak/
57
+
58
+ .idea/
59
+ .vscode/
60
+ *.swp
61
+ *.swo
62
+
63
+ *.log
@@ -0,0 +1,38 @@
1
+ Metadata-Version: 2.5
2
+ Name: mcp-win-stdio-arch
3
+ Version: 0.3.0
4
+ Summary: Universal Architecture Knowledge Graph & Blueprint Model Context Protocol (MCP) server for full-stack codebases (Next.js, NestJS, Drizzle, PostgreSQL/MySQL).
5
+ Author: Mohan Kumar Indala
6
+ License-Expression: MIT
7
+ Keywords: ai,architecture,claude,drizzle,erd,knowledge-graph,llm,mcp,nestjs,nextjs,postgres,uml
8
+ Classifier: Development Status :: 4 - Beta
9
+ Classifier: Environment :: Win32 (MS Windows)
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: Microsoft :: Windows
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Software Development :: Documentation
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: mcp-win-stdio>=0.3.0
22
+ Requires-Dist: mcp>=1.2.0
23
+ Requires-Dist: networkx>=3.2.1
24
+ Description-Content-Type: text/markdown
25
+
26
+ # mcp-win-stdio-arch
27
+
28
+ Universal Architecture Knowledge Graph & Blueprint Model Context Protocol (MCP) server.
29
+
30
+ Traces full-stack applications from Frontend UI (`Next.js`), Client Transport (`TanStack Query`, `Axios`), Ingress Routes (`NestJS`), Domain Services, and Data Storage (`Drizzle ORM`, `PostgreSQL`/`MySQL`).
31
+
32
+ ## Tools Provided
33
+
34
+ 1. `blueprint_feature(domain, workspace_root)`: Generates full-stack vertical blueprint with Mermaid flowchart.
35
+ 2. `blueprint_trace(target, workspace_root)`: Ordered vertical execution path with source evidence and line numbers.
36
+ 3. `blueprint_erd(domain, workspace_root)`: Entity-Relationship Diagram (ERD) with tables, columns, and foreign keys.
37
+ 4. `blueprint_impact(node_id, direction, workspace_root)`: Upstream & downstream blast-radius analysis.
38
+ 5. `blueprint_audit(workspace_root)`: Architectural linting (missing FK indexes, broken routes, layer leaks, orphan entities).
@@ -0,0 +1,13 @@
1
+ # mcp-win-stdio-arch
2
+
3
+ Universal Architecture Knowledge Graph & Blueprint Model Context Protocol (MCP) server.
4
+
5
+ Traces full-stack applications from Frontend UI (`Next.js`), Client Transport (`TanStack Query`, `Axios`), Ingress Routes (`NestJS`), Domain Services, and Data Storage (`Drizzle ORM`, `PostgreSQL`/`MySQL`).
6
+
7
+ ## Tools Provided
8
+
9
+ 1. `blueprint_feature(domain, workspace_root)`: Generates full-stack vertical blueprint with Mermaid flowchart.
10
+ 2. `blueprint_trace(target, workspace_root)`: Ordered vertical execution path with source evidence and line numbers.
11
+ 3. `blueprint_erd(domain, workspace_root)`: Entity-Relationship Diagram (ERD) with tables, columns, and foreign keys.
12
+ 4. `blueprint_impact(node_id, direction, workspace_root)`: Upstream & downstream blast-radius analysis.
13
+ 5. `blueprint_audit(workspace_root)`: Architectural linting (missing FK indexes, broken routes, layer leaks, orphan entities).
@@ -0,0 +1,42 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "mcp-win-stdio-arch"
7
+ version = "0.3.0"
8
+ description = "Universal Architecture Knowledge Graph & Blueprint Model Context Protocol (MCP) server for full-stack codebases (Next.js, NestJS, Drizzle, PostgreSQL/MySQL)."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [
13
+ { name = "Mohan Kumar Indala" }
14
+ ]
15
+ keywords = ["mcp", "claude", "architecture", "knowledge-graph", "erd", "uml", "nextjs", "nestjs", "drizzle", "postgres", "ai", "llm"]
16
+ classifiers = [
17
+ "Development Status :: 4 - Beta",
18
+ "Environment :: Win32 (MS Windows)",
19
+ "Intended Audience :: Developers",
20
+ "License :: OSI Approved :: MIT License",
21
+ "Operating System :: Microsoft :: Windows",
22
+ "Programming Language :: Python :: 3",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Programming Language :: Python :: 3.14",
28
+ "Topic :: Software Development :: Documentation",
29
+ ]
30
+
31
+ dependencies = [
32
+ "mcp-win-stdio>=0.3.0",
33
+ "mcp>=1.2.0",
34
+ "networkx>=3.2.1",
35
+ ]
36
+
37
+ [project.scripts]
38
+ mcp-win-stdio-arch = "mcp_win_stdio.arch.cli:main"
39
+ mws-arch = "mcp_win_stdio.arch.cli:main"
40
+
41
+ [tool.hatch.build.targets.wheel]
42
+ packages = ["src/mcp_win_stdio"]
@@ -0,0 +1,6 @@
1
+ """
2
+ Universal Architecture Blueprint & Knowledge Graph module.
3
+ Part of mcp-win-stdio.
4
+ """
5
+
6
+ __version__ = "0.3.0"
@@ -0,0 +1,71 @@
1
+ """
2
+ CLI entry point for mcp-win-stdio-arch.
3
+ """
4
+
5
+ import argparse
6
+ import sys
7
+
8
+ from mcp_win_stdio.arch import __version__
9
+ from mcp_win_stdio.arch.server import mcp
10
+
11
+
12
+ def cmd_run(args: argparse.Namespace) -> None:
13
+ """Run Architecture Blueprint MCP server over stdio."""
14
+ mcp.run()
15
+
16
+
17
+ def cmd_doctor(args: argparse.Namespace) -> None:
18
+ """Check Architecture Blueprint MCP health."""
19
+ print(f"\n=== mcp-win-stdio-arch Doctor Diagnostic (v{__version__}) ===\n")
20
+ print(f"[OK] Python: {sys.version.split()[0]} ({sys.executable})")
21
+
22
+ for dep in ("mcp", "networkx"):
23
+ try:
24
+ __import__(dep)
25
+ print(f"[OK] Library: {dep:<14}")
26
+ except ImportError:
27
+ print(f"[FAIL] Library: {dep:<14} (MISSING - run `pip install {dep}`)")
28
+
29
+ print("\nDiagnostic complete.\n")
30
+
31
+
32
+ def cmd_guide(args: argparse.Namespace) -> None:
33
+ """Print guide and prompt recipes."""
34
+ print("""
35
+ === mcp-win-stdio-arch Guide & Recipes ===
36
+
37
+ Tools Provided:
38
+ - blueprint_feature(domain, workspace_root)
39
+ - blueprint_erd(domain, workspace_root)
40
+ - blueprint_trace(target, workspace_root)
41
+ - blueprint_impact(node_id, direction, workspace_root)
42
+ - blueprint_audit(workspace_root)
43
+ """)
44
+
45
+
46
+ def main() -> None:
47
+ parser = argparse.ArgumentParser(
48
+ prog="mcp-win-stdio-arch",
49
+ description="Universal Architecture Blueprint MCP Server",
50
+ )
51
+ subparsers = parser.add_subparsers(dest="subcommand", help="Available subcommands")
52
+
53
+ run_parser = subparsers.add_parser("run", help="Run MCP server over stdio")
54
+ run_parser.set_defaults(func=cmd_run)
55
+
56
+ doc_parser = subparsers.add_parser("doctor", help="Check dependencies and health")
57
+ doc_parser.set_defaults(func=cmd_doctor)
58
+
59
+ guide_parser = subparsers.add_parser("guide", help="Print tool guide")
60
+ guide_parser.set_defaults(func=cmd_guide)
61
+
62
+ args = parser.parse_args()
63
+ if hasattr(args, "func"):
64
+ args.func(args)
65
+ else:
66
+ # Default action: run stdio server
67
+ cmd_run(args)
68
+
69
+
70
+ if __name__ == "__main__":
71
+ main()
@@ -0,0 +1,214 @@
1
+ """
2
+ Core Directed Multigraph Engine using NetworkX
3
+ """
4
+
5
+ from typing import Any, Dict, List, Optional, Set
6
+ import networkx as nx
7
+
8
+ from .models import (
9
+ ArchitectureEdge,
10
+ ArchitectureNode,
11
+ EdgeType,
12
+ NodeKind,
13
+ ScanDiagnostic,
14
+ )
15
+
16
+
17
+ class ArchitectureGraph:
18
+ def __init__(self) -> None:
19
+ self.g = nx.MultiDiGraph()
20
+ self.nodes_map: Dict[str, ArchitectureNode] = {}
21
+ self.edges_map: Dict[str, ArchitectureEdge] = {}
22
+ self.diagnostics: List[ScanDiagnostic] = []
23
+
24
+ def clear(self) -> None:
25
+ self.g.clear()
26
+ self.nodes_map.clear()
27
+ self.edges_map.clear()
28
+ self.diagnostics.clear()
29
+
30
+ # --- Node Operations ---
31
+
32
+ def add_node(self, node: ArchitectureNode) -> None:
33
+ self.nodes_map[node.id] = node
34
+ self.g.add_node(
35
+ node.id,
36
+ kind=node.kind.value if isinstance(node.kind, NodeKind) else str(node.kind),
37
+ name=node.name,
38
+ domain=node.domain,
39
+ file_path=node.file_path,
40
+ )
41
+
42
+ def get_node(self, node_id: str) -> Optional[ArchitectureNode]:
43
+ return self.nodes_map.get(node_id)
44
+
45
+ def has_node(self, node_id: str) -> bool:
46
+ return node_id in self.nodes_map
47
+
48
+ def get_all_nodes(self) -> List[ArchitectureNode]:
49
+ return list(self.nodes_map.values())
50
+
51
+ def get_nodes_by_kind(self, kind: NodeKind) -> List[ArchitectureNode]:
52
+ return [n for n in self.nodes_map.values() if n.kind == kind]
53
+
54
+ def get_nodes_by_domain(self, domain: str) -> List[ArchitectureNode]:
55
+ return [n for n in self.nodes_map.values() if n.domain == domain]
56
+
57
+ # --- Edge Operations ---
58
+
59
+ def add_edge(self, edge: ArchitectureEdge) -> None:
60
+ if edge.source not in self.nodes_map or edge.target not in self.nodes_map:
61
+ raise ValueError(
62
+ f"Cannot add edge {edge.id}: source ({edge.source}) or target ({edge.target}) does not exist"
63
+ )
64
+
65
+ self.edges_map[edge.id] = edge
66
+ self.g.add_edge(
67
+ edge.source,
68
+ edge.target,
69
+ key=edge.id,
70
+ type=edge.type.value if isinstance(edge.type, EdgeType) else str(edge.type),
71
+ confidence=edge.confidence.value,
72
+ resolution=edge.resolution.value,
73
+ )
74
+
75
+ def get_edge(self, edge_id: str) -> Optional[ArchitectureEdge]:
76
+ return self.edges_map.get(edge_id)
77
+
78
+ def get_all_edges(self) -> List[ArchitectureEdge]:
79
+ return list(self.edges_map.values())
80
+
81
+ def get_outgoing_edges(self, node_id: str) -> List[ArchitectureEdge]:
82
+ if not self.g.has_node(node_id):
83
+ return []
84
+ edges = []
85
+ for _, _, key in self.g.out_edges(node_id, keys=True):
86
+ if key in self.edges_map:
87
+ edges.append(self.edges_map[key])
88
+ return edges
89
+
90
+ def get_incoming_edges(self, node_id: str) -> List[ArchitectureEdge]:
91
+ if not self.g.has_node(node_id):
92
+ return []
93
+ edges = []
94
+ for _, _, key in self.g.in_edges(node_id, keys=True):
95
+ if key in self.edges_map:
96
+ edges.append(self.edges_map[key])
97
+ return edges
98
+
99
+ # --- Diagnostics ---
100
+
101
+ def add_diagnostic(self, diagnostic: ScanDiagnostic) -> None:
102
+ self.diagnostics.append(diagnostic)
103
+
104
+ def get_diagnostics(self) -> List[ScanDiagnostic]:
105
+ return list(self.diagnostics)
106
+
107
+ # --- Traversals & Blast Radius ---
108
+
109
+ def trace_paths(
110
+ self,
111
+ from_node_id: str,
112
+ target_kind: Optional[NodeKind] = None,
113
+ max_depth: int = 10,
114
+ max_paths: int = 20,
115
+ ) -> List[List[str]]:
116
+ """
117
+ Traces paths from from_node_id to target_kind with cycle protection.
118
+ Returns list of path node ID lists.
119
+ """
120
+ if not self.has_node(from_node_id):
121
+ return []
122
+
123
+ results: List[List[str]] = []
124
+
125
+ def dfs(current_id: str, path: List[str], depth: int, visited: Set[str]):
126
+ if len(results) >= max_paths or depth >= max_depth:
127
+ return
128
+
129
+ curr_node = self.nodes_map.get(current_id)
130
+ if target_kind and curr_node and curr_node.kind == target_kind and len(path) > 1:
131
+ results.append(list(path))
132
+ return
133
+
134
+ extended = False
135
+ for edge in self.get_outgoing_edges(current_id):
136
+ nxt = edge.target
137
+ if nxt in visited:
138
+ continue
139
+
140
+ extended = True
141
+ visited.add(nxt)
142
+ path.append(nxt)
143
+
144
+ dfs(nxt, path, depth + 1, visited)
145
+
146
+ path.pop()
147
+ visited.remove(nxt)
148
+
149
+ if not extended and not target_kind and len(path) > 1:
150
+ results.append(list(path))
151
+
152
+ dfs(from_node_id, [from_node_id], 0, {from_node_id})
153
+ return results
154
+
155
+ def compute_blast_radius(
156
+ self,
157
+ node_id: str,
158
+ direction: str = "both",
159
+ max_depth: int = 8,
160
+ ) -> Dict[str, Any]:
161
+ """
162
+ Calculates upstream/downstream blast radius for refactoring.
163
+ """
164
+ if not self.has_node(node_id):
165
+ return {"root_node_id": node_id, "total_affected": 0, "impacted": []}
166
+
167
+ impacted: Dict[str, Dict[str, Any]] = {}
168
+
169
+ queue: List[tuple[str, int, str]] = []
170
+ if direction in ("downstream", "both"):
171
+ queue.append((node_id, 0, "downstream"))
172
+ if direction in ("upstream", "both"):
173
+ queue.append((node_id, 0, "upstream"))
174
+
175
+ visited: Set[str] = set()
176
+
177
+ while queue:
178
+ curr_id, depth, d = queue.pop(0)
179
+ vkey = f"{curr_id}:{d}"
180
+ if vkey in visited:
181
+ continue
182
+ visited.add(vkey)
183
+
184
+ if depth > 0 and curr_id not in impacted:
185
+ impacted[curr_id] = {
186
+ "node_id": curr_id,
187
+ "depth": depth,
188
+ "direction": d,
189
+ "kind": self.nodes_map[curr_id].kind.value,
190
+ "name": self.nodes_map[curr_id].name,
191
+ }
192
+
193
+ if depth >= max_depth:
194
+ continue
195
+
196
+ if d == "downstream":
197
+ for edge in self.get_outgoing_edges(curr_id):
198
+ queue.append((edge.target, depth + 1, "downstream"))
199
+ else:
200
+ for edge in self.get_incoming_edges(curr_id):
201
+ queue.append((edge.source, depth + 1, "upstream"))
202
+
203
+ return {
204
+ "root_node_id": node_id,
205
+ "total_affected": len(impacted),
206
+ "impacted": list(impacted.values()),
207
+ }
208
+
209
+ def stats(self) -> Dict[str, int]:
210
+ return {
211
+ "node_count": len(self.nodes_map),
212
+ "edge_count": len(self.edges_map),
213
+ "diagnostic_count": len(self.diagnostics),
214
+ }
@@ -0,0 +1,61 @@
1
+ """
2
+ Deterministic URN & Node ID Generators
3
+ """
4
+
5
+ import re
6
+
7
+
8
+ def normalize_path(path_str: str) -> str:
9
+ """Normalize file paths to clean POSIX format."""
10
+ return path_str.replace("\\", "/").lstrip("/")
11
+
12
+
13
+ def normalize_route_path(raw_path: str) -> str:
14
+ """
15
+ Normalize route paths across frameworks:
16
+ Converts :id, ${id}, [id] to {param}
17
+ """
18
+ p = raw_path.strip().split("?")[0].split("#")[0]
19
+ p = re.sub(r"\$\{[^}]+\}", "{param}", p)
20
+ p = re.sub(r":([a-zA-Z0-9_]+)", "{param}", p)
21
+ p = re.sub(r"\[([a-zA-Z0-9_]+)\]", "{param}", p)
22
+ if not p.startswith("/"):
23
+ p = "/" + p
24
+ if len(p) > 1 and p.endswith("/"):
25
+ p = p[:-1]
26
+ return p.lower()
27
+
28
+
29
+ class NodeIdentifier:
30
+ @staticmethod
31
+ def for_surface(workspace: str, file_path: str, component_name: str) -> str:
32
+ return f"surface:{workspace}:{normalize_path(file_path)}#{component_name}"
33
+
34
+ @staticmethod
35
+ def for_client_transport(workspace: str, file_path: str, symbol: str) -> str:
36
+ return f"client:{workspace}:{normalize_path(file_path)}#{symbol}"
37
+
38
+ @staticmethod
39
+ def for_ingress_route(method: str, normalized_path: str) -> str:
40
+ return f"route:{method.upper()}:{normalize_route_path(normalized_path)}"
41
+
42
+ @staticmethod
43
+ def for_domain_logic(workspace: str, file_path: str, class_name: str, method: str = "") -> str:
44
+ symbol = f"{class_name}.{method}" if method else class_name
45
+ return f"logic:{workspace}:{normalize_path(file_path)}#{symbol}"
46
+
47
+ @staticmethod
48
+ def for_storage_entity(schema: str, table_name: str) -> str:
49
+ return f"storage:{schema.lower()}:{table_name.lower()}"
50
+
51
+ @staticmethod
52
+ def for_field(owner_id: str, field_name: str) -> str:
53
+ return f"field:{owner_id}#{field_name}"
54
+
55
+ @staticmethod
56
+ def for_contract(workspace: str, file_path: str, type_name: str) -> str:
57
+ return f"contract:{workspace}:{normalize_path(file_path)}#{type_name}"
58
+
59
+ @staticmethod
60
+ def for_edge(source: str, edge_type: str, target: str) -> str:
61
+ return f"edge:{source}--{edge_type}-->{target}"
@@ -0,0 +1,96 @@
1
+ """
2
+ Core Canonical Schema: Universal Architectural Primitives (UIR)
3
+ """
4
+
5
+ from dataclasses import dataclass, field
6
+ from enum import Enum
7
+ from typing import Any, Dict, List, Optional
8
+
9
+
10
+ class NodeKind(str, Enum):
11
+ SURFACE = "Surface" # UI Page, Component, Screen, Template
12
+ CLIENT_TRANSPORT = "ClientTransport" # Hook, API Client method, RPC client
13
+ INGRESS_ROUTE = "IngressRoute" # HTTP Route, Controller method, RPC handler
14
+ DOMAIN_LOGIC = "DomainLogic" # Service, Use Case, Business domain function
15
+ STORAGE_ENTITY = "StorageEntity" # ORM Model, SQL Table, View, Collection
16
+ FIELD = "Field" # Database Column or DTO Property
17
+ CONTRACT = "Contract" # DTO, Zod Schema, OpenAPI Request/Response
18
+ EXTERNAL_SYSTEM = "ExternalSystem" # Third-party API, Message Broker
19
+ CONFIGURATION = "Configuration" # Env var, Route prefix, Feature flag
20
+ JOB = "Job" # Cron job, Queue consumer, Worker
21
+
22
+
23
+ class EdgeType(str, Enum):
24
+ DECLARES = "DECLARES" # e.g. Controller declares method, Schema declares Table
25
+ IMPORTS = "IMPORTS" # Module imports another module
26
+ CALLS = "CALLS" # Method or function invocation
27
+ USES_HOOK = "USES_HOOK" # Surface uses query/mutation hook
28
+ ROUTES_TO = "ROUTES_TO" # Client request matches an ingress route
29
+ INJECTS = "INJECTS" # Dependency injection
30
+ READS = "READS" # Logic queries a table or field
31
+ WRITES = "WRITES" # Logic mutates a table or field
32
+ SERIALIZES_AS = "SERIALIZES_AS" # Entity shapes into DTO
33
+ REFERENCES_FK = "REFERENCES_FK" # Foreign-key relationship
34
+ PUBLISHES = "PUBLISHES" # Event-driven message publishing
35
+ CONSUMES = "CONSUMES" # Event-driven message consumption
36
+ CONFIGURED_BY = "CONFIGURED_BY" # Component depends on configuration
37
+
38
+
39
+ class Confidence(str, Enum):
40
+ HIGH = "high" # Statically resolved symbol call, explicit foreign key, exact route match
41
+ MEDIUM = "medium" # Inferred through convention or partial types
42
+ LOW = "low" # Heuristic or ambiguous string matching
43
+
44
+
45
+ class Resolution(str, Enum):
46
+ STATIC = "static" # AST symbol checker or static code analysis
47
+ INFERRED = "inferred" # Normalized route or naming pattern match
48
+ RUNTIME = "runtime" # Database introspection or live trace
49
+ MANUAL = "manual" # User/developer annotated
50
+
51
+
52
+ @dataclass
53
+ class EvidenceLocation:
54
+ file_path: str
55
+ start_line: Optional[int] = None
56
+ end_line: Optional[int] = None
57
+ symbol: Optional[str] = None
58
+
59
+
60
+ @dataclass
61
+ class EdgeEvidence:
62
+ rule_id: str
63
+ locations: List[EvidenceLocation] = field(default_factory=list)
64
+ explanation: Optional[str] = None
65
+ snippet: Optional[str] = None
66
+
67
+
68
+ @dataclass
69
+ class ArchitectureNode:
70
+ id: str
71
+ kind: NodeKind
72
+ name: str
73
+ file_path: Optional[str] = None
74
+ domain: Optional[str] = None
75
+ metadata: Dict[str, Any] = field(default_factory=dict)
76
+
77
+
78
+ @dataclass
79
+ class ArchitectureEdge:
80
+ id: str
81
+ source: str
82
+ target: str
83
+ type: EdgeType
84
+ evidence: EdgeEvidence
85
+ confidence: Confidence = Confidence.HIGH
86
+ resolution: Resolution = Resolution.STATIC
87
+ metadata: Dict[str, Any] = field(default_factory=dict)
88
+
89
+
90
+ @dataclass
91
+ class ScanDiagnostic:
92
+ code: str
93
+ severity: str # "info" | "warning" | "error"
94
+ message: str
95
+ location: Optional[EvidenceLocation] = None
96
+ attempted_resolution: Optional[Dict[str, Any]] = None
@@ -0,0 +1,96 @@
1
+ """
2
+ Mermaid Flowchart and ERD Renderers
3
+ """
4
+
5
+ import re
6
+ from typing import List, Optional
7
+
8
+ from .graph import ArchitectureGraph
9
+ from .models import ArchitectureEdge, ArchitectureNode, NodeKind
10
+
11
+
12
+ def sanitize(name: str) -> str:
13
+ return re.sub(r"[^a-zA-Z0-9_]", "_", name)
14
+
15
+
16
+ class MermaidRenderer:
17
+ @staticmethod
18
+ def render_flowchart(
19
+ graph: ArchitectureGraph,
20
+ nodes: Optional[List[ArchitectureNode]] = None,
21
+ edges: Optional[List[ArchitectureEdge]] = None,
22
+ ) -> str:
23
+ active_nodes = nodes or graph.get_all_nodes()
24
+ active_edges = edges or graph.get_all_edges()
25
+
26
+ lines = ["flowchart TD"]
27
+
28
+ groups: dict[str, list[ArchitectureNode]] = {}
29
+ for node in active_nodes:
30
+ kind_str = node.kind.value if isinstance(node.kind, NodeKind) else str(node.kind)
31
+ groups.setdefault(kind_str, []).append(node)
32
+
33
+ order = [
34
+ "Surface",
35
+ "ClientTransport",
36
+ "IngressRoute",
37
+ "DomainLogic",
38
+ "StorageEntity",
39
+ "Contract",
40
+ ]
41
+
42
+ for kind in order:
43
+ items = groups.get(kind, [])
44
+ if not items:
45
+ continue
46
+ lines.append(f' subgraph {kind}_Layer ["{kind} Layer"]')
47
+ for n in items:
48
+ sid = sanitize(n.id)
49
+ lines.append(f' {sid}["{n.name}\\n({kind})"]')
50
+ lines.append(" end")
51
+
52
+ for e in active_edges:
53
+ sid = sanitize(e.source)
54
+ tid = sanitize(e.target)
55
+ etype = e.type.value if hasattr(e.type, "value") else str(e.type)
56
+ conf = e.confidence.value if hasattr(e.confidence, "value") else str(e.confidence)
57
+ lines.append(f' {sid} -->|"{etype} ({conf})"| {tid}')
58
+
59
+ return "\n".join(lines)
60
+
61
+ @staticmethod
62
+ def render_erd(graph: ArchitectureGraph, domain: Optional[str] = None) -> str:
63
+ tables = (
64
+ graph.get_nodes_by_domain(domain)
65
+ if domain
66
+ else graph.get_nodes_by_kind(NodeKind.STORAGE_ENTITY)
67
+ )
68
+ tables = [t for t in tables if t.kind == NodeKind.STORAGE_ENTITY]
69
+
70
+ lines = ["erDiagram"]
71
+ for t in tables:
72
+ lines.append(f" {sanitize(t.name)} {{")
73
+ # find fields
74
+ fields = [
75
+ f for f in graph.get_nodes_by_kind(NodeKind.FIELD)
76
+ if f.metadata.get("owner_id") == t.id
77
+ ]
78
+ if not fields:
79
+ lines.append(" string id PK")
80
+ else:
81
+ for f in fields:
82
+ pk = "PK" if f.metadata.get("is_pk") else ""
83
+ fk = "FK" if f.metadata.get("is_fk") else ""
84
+ tag = ",".join(filter(None, [pk, fk]))
85
+ dtype = f.metadata.get("data_type", "string")
86
+ lines.append(f" {dtype} {f.name} {tag}")
87
+ lines.append(" }")
88
+
89
+ for e in graph.get_all_edges():
90
+ if e.type == "REFERENCES_FK" or getattr(e.type, "value", None) == "REFERENCES_FK":
91
+ s = graph.get_node(e.source)
92
+ t = graph.get_node(e.target)
93
+ if s and t:
94
+ lines.append(f' {sanitize(s.name)} }}o--|| {sanitize(t.name)} : "references"')
95
+
96
+ return "\n".join(lines)
@@ -0,0 +1,250 @@
1
+ """
2
+ Real-world AST Scanner for Showreel (Drizzle ORM + NestJS Backend + Next.js Frontend)
3
+ """
4
+
5
+ import os
6
+ import re
7
+ from pathlib import Path
8
+ from typing import Dict, List, Optional
9
+
10
+ from .graph import ArchitectureGraph
11
+ from .identity import NodeIdentifier, normalize_route_path
12
+ from .models import (
13
+ ArchitectureEdge,
14
+ ArchitectureNode,
15
+ Confidence,
16
+ EdgeEvidence,
17
+ EdgeType,
18
+ EvidenceLocation,
19
+ NodeKind,
20
+ Resolution,
21
+ )
22
+
23
+
24
+ class ShowreelScanner:
25
+ """
26
+ Scans full-stack Showreel architecture:
27
+ Frontend (Next.js/TanStack) -> Backend (NestJS) -> Schema (Drizzle).
28
+ """
29
+
30
+ def __init__(self, workspace_root: str = r"Z:\projects\showreel") -> None:
31
+ self.workspace_root = Path(workspace_root)
32
+ self.frontend_root = self.workspace_root / "rfc_location_booking_frontend"
33
+ self.backend_root = self.workspace_root / "rfc_location_booking_backend"
34
+
35
+ def scan_all(self, graph: ArchitectureGraph, domain: Optional[str] = "prop-management") -> None:
36
+ # 1. Scan Drizzle DB Schemas
37
+ self.scan_drizzle_schemas(graph, domain)
38
+ # 2. Scan NestJS Controllers & Services
39
+ self.scan_nestjs_backend(graph, domain)
40
+ # 3. Scan Next.js Features & React Query Hooks
41
+ self.scan_nextjs_frontend(graph, domain)
42
+
43
+ def scan_drizzle_schemas(self, graph: ArchitectureGraph, domain: Optional[str] = None) -> None:
44
+ schema_dir = self.backend_root / "src" / "database" / "schema"
45
+ if not schema_dir.exists():
46
+ return
47
+
48
+ schema_files = list(schema_dir.glob("*.ts"))
49
+ table_var_map: Dict[str, str] = {} # varName -> tableNodeId
50
+
51
+ for sfile in schema_files:
52
+ try:
53
+ content = sfile.read_text(encoding="utf-8")
54
+ except Exception:
55
+ continue
56
+
57
+ rel_path = str(sfile.relative_to(self.workspace_root))
58
+
59
+ # Match: export const <varName> = pgTable('tableName', { ... })
60
+ # or export const <varName> = publicSchema.table('tableName', { ... })
61
+ table_matches = re.finditer(
62
+ r"export\s+const\s+([a-zA-Z0-9_]+)\s*=\s*(?:pgTable|[a-zA-Z0-9_]+\.table)\s*\(\s*['\"]([^'\"]+)['\"]",
63
+ content,
64
+ )
65
+
66
+ for m in table_matches:
67
+ var_name = m.group(1)
68
+ table_name = m.group(2)
69
+ node_id = NodeIdentifier.for_storage_entity("public", table_name)
70
+
71
+ table_node = ArchitectureNode(
72
+ id=node_id,
73
+ kind=NodeKind.STORAGE_ENTITY,
74
+ name=table_name,
75
+ file_path=rel_path,
76
+ domain=domain or "prop-management",
77
+ metadata={"var_name": var_name, "orm": "drizzle"},
78
+ )
79
+ graph.add_node(table_node)
80
+ table_var_map[var_name] = node_id
81
+
82
+ # Parse foreign keys: .references(() => otherTable.id)
83
+ fk_matches = re.finditer(
84
+ r"export\s+const\s+([a-zA-Z0-9_]+)\s*=\s*(?:pgTable|[a-zA-Z0-9_]+\.table).*?\.references\(\s*\(\)\s*=>\s*([a-zA-Z0-9_]+)\.",
85
+ content,
86
+ re.DOTALL,
87
+ )
88
+ for fk in fk_matches:
89
+ src_var = fk.group(1)
90
+ tgt_var = fk.group(2)
91
+ if src_var in table_var_map and tgt_var in table_var_map and src_var != tgt_var:
92
+ src_id = table_var_map[src_var]
93
+ tgt_id = table_var_map[tgt_var]
94
+ edge_id = NodeIdentifier.for_edge(src_id, "REFERENCES_FK", tgt_id)
95
+ if not graph.get_edge(edge_id):
96
+ graph.add_edge(
97
+ ArchitectureEdge(
98
+ id=edge_id,
99
+ source=src_id,
100
+ target=tgt_id,
101
+ type=EdgeType.REFERENCES_FK,
102
+ confidence=Confidence.HIGH,
103
+ resolution=Resolution.STATIC,
104
+ evidence=EdgeEvidence(
105
+ rule_id="DRIZZLE_FK_REFERENCES",
106
+ locations=[EvidenceLocation(file_path=rel_path)],
107
+ explanation=f"FK: {src_var} -> {tgt_var}",
108
+ ),
109
+ )
110
+ )
111
+
112
+ def scan_nestjs_backend(self, graph: ArchitectureGraph, domain: Optional[str] = None) -> None:
113
+ modules_dir = self.backend_root / "src" / "modules"
114
+ if not modules_dir.exists():
115
+ return
116
+
117
+ for ctrl_file in modules_dir.rglob("*controller.ts"):
118
+ try:
119
+ content = ctrl_file.read_text(encoding="utf-8")
120
+ except Exception:
121
+ continue
122
+
123
+ rel_path = str(ctrl_file.relative_to(self.workspace_root))
124
+
125
+ # Match @Controller('prefix')
126
+ prefix_match = re.search(r"@Controller\(\s*['\"]([^'\"]*)['\"]\s*\)", content)
127
+ base_prefix = prefix_match.group(1) if prefix_match else ""
128
+ if not base_prefix.startswith("/"):
129
+ base_prefix = "/" + base_prefix
130
+
131
+ # Match routes: @Get(':id'), @Post(), etc.
132
+ method_matches = re.finditer(
133
+ r"@(Get|Post|Put|Patch|Delete)\(\s*(?:['\"]([^'\"]*)['\"])?\s*\)\s*(?:async\s+)?([a-zA-Z0-9_]+)\s*\(",
134
+ content,
135
+ )
136
+
137
+ for mm in method_matches:
138
+ http_method = mm.group(1).upper()
139
+ sub_path = mm.group(2) or ""
140
+ fn_name = mm.group(3)
141
+
142
+ full_path = f"{base_prefix}/{sub_path}".replace("//", "/")
143
+ # Showreel uses standard api prefix (e.g. /api/v1/prop/...)
144
+ norm_route_id = NodeIdentifier.for_ingress_route(http_method, full_path)
145
+
146
+ route_node = ArchitectureNode(
147
+ id=norm_route_id,
148
+ kind=NodeKind.INGRESS_ROUTE,
149
+ name=f"{http_method} {full_path}",
150
+ file_path=rel_path,
151
+ domain=domain or "prop-management",
152
+ metadata={"handler": fn_name, "method": http_method, "path": full_path},
153
+ )
154
+ graph.add_node(route_node)
155
+
156
+ # Link to Service in same module if called
157
+ svc_call_match = re.search(r"this\.([a-zA-Z0-9_]+Service)\.([a-zA-Z0-9_]+)\(", content)
158
+ if svc_call_match:
159
+ svc_class = svc_call_match.group(1)
160
+ svc_method = svc_call_match.group(2)
161
+ svc_id = NodeIdentifier.for_domain_logic("showreel", rel_path, svc_class, svc_method)
162
+ if not graph.has_node(svc_id):
163
+ graph.add_node(
164
+ ArchitectureNode(
165
+ id=svc_id,
166
+ kind=NodeKind.DOMAIN_LOGIC,
167
+ name=f"{svc_class}.{svc_method}",
168
+ file_path=rel_path,
169
+ domain=domain or "prop-management",
170
+ )
171
+ )
172
+ edge_id = NodeIdentifier.for_edge(norm_route_id, "CALLS", svc_id)
173
+ if not graph.get_edge(edge_id):
174
+ graph.add_edge(
175
+ ArchitectureEdge(
176
+ id=edge_id,
177
+ source=norm_route_id,
178
+ target=svc_id,
179
+ type=EdgeType.CALLS,
180
+ confidence=Confidence.HIGH,
181
+ resolution=Resolution.STATIC,
182
+ evidence=EdgeEvidence(
183
+ rule_id="NESTJS_CONTROLLER_SERVICE_CALL",
184
+ locations=[EvidenceLocation(file_path=rel_path, symbol=fn_name)],
185
+ ),
186
+ )
187
+ )
188
+
189
+ def scan_nextjs_frontend(self, graph: ArchitectureGraph, domain: Optional[str] = None) -> None:
190
+ features_dir = self.frontend_root / "features"
191
+ if not features_dir.exists():
192
+ return
193
+
194
+ for hook_file in features_dir.rglob("*.ts*"):
195
+ try:
196
+ content = hook_file.read_text(encoding="utf-8")
197
+ except Exception:
198
+ continue
199
+
200
+ rel_path = str(hook_file.relative_to(self.workspace_root))
201
+
202
+ # Match TanStack Query hooks: export function useEstimationQuery or const useX =
203
+ hook_match = re.search(r"export\s+(?:function|const)\s+(use[a-zA-Z0-9_]+)", content)
204
+ if not hook_match:
205
+ continue
206
+
207
+ hook_name = hook_match.group(1)
208
+ hook_id = NodeIdentifier.for_client_transport("showreel", rel_path, hook_name)
209
+
210
+ # Look for endpoint pattern: e.g. /api/v1/prop/estimations or axios.get('/api/...')
211
+ endpoint_match = re.search(r"['\"](\/api\/[a-zA-Z0-9_\-\/]+(?:\$\{[^}]+\})?)['\"]", content)
212
+ endpoint_url = endpoint_match.group(1) if endpoint_match else None
213
+
214
+ client_node = ArchitectureNode(
215
+ id=hook_id,
216
+ kind=NodeKind.CLIENT_TRANSPORT,
217
+ name=hook_name,
218
+ file_path=rel_path,
219
+ domain=domain or "prop-management",
220
+ metadata={"endpoint": endpoint_url},
221
+ )
222
+ graph.add_node(client_node)
223
+
224
+ # Match with backend route if endpoint matches
225
+ if endpoint_url:
226
+ norm_route_id = NodeIdentifier.for_ingress_route("GET", endpoint_url)
227
+ # Check if route exists or starts with pattern
228
+ for rnode in graph.get_nodes_by_kind(NodeKind.INGRESS_ROUTE):
229
+ r_path = rnode.metadata.get("path", "")
230
+ if normalize_route_path(r_path) == normalize_route_path(endpoint_url) or (
231
+ r_path and r_path in endpoint_url
232
+ ):
233
+ edge_id = NodeIdentifier.for_edge(hook_id, "ROUTES_TO", rnode.id)
234
+ if not graph.get_edge(edge_id):
235
+ graph.add_edge(
236
+ ArchitectureEdge(
237
+ id=edge_id,
238
+ source=hook_id,
239
+ target=rnode.id,
240
+ type=EdgeType.ROUTES_TO,
241
+ confidence=Confidence.HIGH,
242
+ resolution=Resolution.INFERRED,
243
+ evidence=EdgeEvidence(
244
+ rule_id="HTTP_CLIENT_ROUTE_MATCH",
245
+ locations=[EvidenceLocation(file_path=rel_path, symbol=hook_name)],
246
+ explanation=f"Matched {endpoint_url} to backend {r_path}",
247
+ ),
248
+ )
249
+ )
250
+ break
@@ -0,0 +1,133 @@
1
+ """
2
+ Server definition for mcp-win-stdio-arch (FastMCP)
3
+ Part of mcp-win-stdio.
4
+ """
5
+
6
+ import json
7
+ from typing import Any, Dict, Optional
8
+
9
+ try:
10
+ from mcp.server.mcpserver import MCPServer as FastMCP
11
+ except (ImportError, ModuleNotFoundError):
12
+ from mcp.server.fastmcp import FastMCP # type: ignore[import-not-found]
13
+
14
+ from .graph import ArchitectureGraph
15
+ from .models import NodeKind
16
+ from .renderers import MermaidRenderer
17
+ from .scanner import ShowreelScanner
18
+
19
+ mcp = FastMCP("arch-mcp")
20
+
21
+ # In-memory graph cache
22
+ _GRAPH = ArchitectureGraph()
23
+ _SCANNED_ROOT: Optional[str] = None
24
+
25
+
26
+ def _ensure_graph(workspace_root: str = r"Z:\projects\showreel") -> ArchitectureGraph:
27
+ global _GRAPH, _SCANNED_ROOT
28
+ if _SCANNED_ROOT != workspace_root or _GRAPH.stats()["node_count"] == 0:
29
+ _GRAPH.clear()
30
+ scanner = ShowreelScanner(workspace_root)
31
+ scanner.scan_all(_GRAPH)
32
+ _SCANNED_ROOT = workspace_root
33
+ return _GRAPH
34
+
35
+
36
+ @mcp.tool()
37
+ def blueprint_feature(
38
+ domain: str = "prop-management",
39
+ workspace_root: str = r"Z:\projects\showreel",
40
+ ) -> Dict[str, Any]:
41
+ """
42
+ Generates a full-stack vertical architecture blueprint for a domain feature.
43
+ Connects: UI Views -> TanStack Hooks -> NestJS Controllers -> Services -> Drizzle DB Tables.
44
+ Returns: Mermaid flowchart and structured node/edge statistics.
45
+ """
46
+ graph = _ensure_graph(workspace_root)
47
+ nodes = graph.get_nodes_by_domain(domain)
48
+ if not nodes:
49
+ nodes = graph.get_all_nodes()
50
+
51
+ mermaid = MermaidRenderer.render_flowchart(graph, nodes=nodes)
52
+ return {
53
+ "domain": domain,
54
+ "mermaid": mermaid,
55
+ "stats": graph.stats(),
56
+ "node_sample": [n.name for n in nodes[:15]],
57
+ }
58
+
59
+
60
+ @mcp.tool()
61
+ def blueprint_erd(
62
+ domain: Optional[str] = "prop-management",
63
+ workspace_root: str = r"Z:\projects\showreel",
64
+ ) -> Dict[str, Any]:
65
+ """
66
+ Generates an Entity-Relationship Diagram (ERD) with tables, columns, and foreign keys.
67
+ """
68
+ graph = _ensure_graph(workspace_root)
69
+ mermaid_erd = MermaidRenderer.render_erd(graph, domain=domain)
70
+ tables = [
71
+ n.name for n in graph.get_nodes_by_kind(NodeKind.STORAGE_ENTITY)
72
+ if not domain or n.domain == domain
73
+ ]
74
+ return {
75
+ "domain": domain,
76
+ "mermaid_erd": mermaid_erd,
77
+ "tables_count": len(tables),
78
+ "tables": tables,
79
+ }
80
+
81
+
82
+ @mcp.tool()
83
+ def blueprint_trace(
84
+ target: str,
85
+ workspace_root: str = r"Z:\projects\showreel",
86
+ ) -> Dict[str, Any]:
87
+ """
88
+ Traces the ordered execution path starting from a component, hook, or endpoint down to DB tables.
89
+ """
90
+ graph = _ensure_graph(workspace_root)
91
+ paths = graph.trace_paths(target, target_kind=NodeKind.STORAGE_ENTITY)
92
+ return {
93
+ "target": target,
94
+ "paths_found": len(paths),
95
+ "paths": paths,
96
+ }
97
+
98
+
99
+ @mcp.tool()
100
+ def blueprint_impact(
101
+ node_id: str,
102
+ direction: str = "upstream",
103
+ workspace_root: str = r"Z:\projects\showreel",
104
+ ) -> Dict[str, Any]:
105
+ """
106
+ Calculates the architectural blast radius before refactoring a table, column, or service.
107
+ """
108
+ graph = _ensure_graph(workspace_root)
109
+ return graph.compute_blast_radius(node_id, direction=direction)
110
+
111
+
112
+ @mcp.tool()
113
+ def blueprint_audit(
114
+ workspace_root: str = r"Z:\projects\showreel",
115
+ ) -> Dict[str, Any]:
116
+ """
117
+ Audits the architecture for broken routes, unindexed foreign keys, or layer leaks.
118
+ """
119
+ graph = _ensure_graph(workspace_root)
120
+ diagnostics = [
121
+ {
122
+ "code": d.code,
123
+ "severity": d.severity,
124
+ "message": d.message,
125
+ "location": d.location.file_path if d.location else None,
126
+ }
127
+ for d in graph.get_diagnostics()
128
+ ]
129
+ return {
130
+ "diagnostics_count": len(diagnostics),
131
+ "diagnostics": diagnostics,
132
+ "stats": graph.stats(),
133
+ }