@oriro/orirocli 0.1.9 → 0.1.12
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.
- package/README.md +16 -18
- package/dist/cli.js +4776 -2964
- package/package.json +2 -2
- package/skills/craft/ai-engineering/SKILL.md +2 -2
- package/skills/graphify/SKILL.md +0 -619
- package/skills/graphify/__init__.py +0 -28
- package/skills/graphify/__main__.py +0 -4582
- package/skills/graphify/affected.py +0 -154
- package/skills/graphify/always_on/agents-md.md +0 -12
- package/skills/graphify/always_on/antigravity-rules.md +0 -14
- package/skills/graphify/always_on/claude-md.md +0 -9
- package/skills/graphify/always_on/gemini-md.md +0 -9
- package/skills/graphify/always_on/kiro-steering.md +0 -5
- package/skills/graphify/always_on/vscode-instructions.md +0 -17
- package/skills/graphify/analyze.py +0 -724
- package/skills/graphify/benchmark.py +0 -155
- package/skills/graphify/build.py +0 -487
- package/skills/graphify/cache.py +0 -417
- package/skills/graphify/callflow_html.py +0 -2020
- package/skills/graphify/cluster.py +0 -272
- package/skills/graphify/command-kilo.md +0 -15
- package/skills/graphify/dedup.py +0 -429
- package/skills/graphify/detect.py +0 -1379
- package/skills/graphify/diagnostics.py +0 -390
- package/skills/graphify/export.py +0 -1408
- package/skills/graphify/extract.py +0 -11570
- package/skills/graphify/global_graph.py +0 -159
- package/skills/graphify/google_workspace.py +0 -223
- package/skills/graphify/hooks.py +0 -457
- package/skills/graphify/ingest.py +0 -331
- package/skills/graphify/llm.py +0 -1896
- package/skills/graphify/manifest.py +0 -4
- package/skills/graphify/mcp_ingest.py +0 -392
- package/skills/graphify/multigraph_compat.py +0 -212
- package/skills/graphify/pg_introspect.py +0 -142
- package/skills/graphify/prs.py +0 -748
- package/skills/graphify/querylog.py +0 -70
- package/skills/graphify/report.py +0 -218
- package/skills/graphify/scip_ingest.py +0 -363
- package/skills/graphify/security.py +0 -336
- package/skills/graphify/semantic_cleanup.py +0 -319
- package/skills/graphify/serve.py +0 -1309
- package/skills/graphify/skill-aider.md +0 -1246
- package/skills/graphify/skill-amp.md +0 -613
- package/skills/graphify/skill-claw.md +0 -616
- package/skills/graphify/skill-codex.md +0 -613
- package/skills/graphify/skill-copilot.md +0 -616
- package/skills/graphify/skill-devin.md +0 -1372
- package/skills/graphify/skill-droid.md +0 -613
- package/skills/graphify/skill-kilo.md +0 -625
- package/skills/graphify/skill-kiro.md +0 -615
- package/skills/graphify/skill-opencode.md +0 -608
- package/skills/graphify/skill-pi.md +0 -615
- package/skills/graphify/skill-trae.md +0 -614
- package/skills/graphify/skill-vscode.md +0 -612
- package/skills/graphify/skill-windows.md +0 -651
- package/skills/graphify/skills/amp/references/add-watch.md +0 -56
- package/skills/graphify/skills/amp/references/exports.md +0 -71
- package/skills/graphify/skills/amp/references/extraction-spec.md +0 -68
- package/skills/graphify/skills/amp/references/github-and-merge.md +0 -46
- package/skills/graphify/skills/amp/references/hooks.md +0 -33
- package/skills/graphify/skills/amp/references/query.md +0 -249
- package/skills/graphify/skills/amp/references/transcribe.md +0 -48
- package/skills/graphify/skills/amp/references/update.md +0 -179
- package/skills/graphify/skills/claude/references/add-watch.md +0 -56
- package/skills/graphify/skills/claude/references/exports.md +0 -71
- package/skills/graphify/skills/claude/references/extraction-spec.md +0 -68
- package/skills/graphify/skills/claude/references/github-and-merge.md +0 -46
- package/skills/graphify/skills/claude/references/hooks.md +0 -33
- package/skills/graphify/skills/claude/references/query.md +0 -103
- package/skills/graphify/skills/claude/references/transcribe.md +0 -48
- package/skills/graphify/skills/claude/references/update.md +0 -179
- package/skills/graphify/skills/claw/references/add-watch.md +0 -56
- package/skills/graphify/skills/claw/references/exports.md +0 -71
- package/skills/graphify/skills/claw/references/extraction-spec.md +0 -29
- package/skills/graphify/skills/claw/references/github-and-merge.md +0 -46
- package/skills/graphify/skills/claw/references/hooks.md +0 -33
- package/skills/graphify/skills/claw/references/query.md +0 -249
- package/skills/graphify/skills/claw/references/transcribe.md +0 -48
- package/skills/graphify/skills/claw/references/update.md +0 -179
- package/skills/graphify/skills/codex/references/add-watch.md +0 -56
- package/skills/graphify/skills/codex/references/exports.md +0 -71
- package/skills/graphify/skills/codex/references/extraction-spec.md +0 -29
- package/skills/graphify/skills/codex/references/github-and-merge.md +0 -46
- package/skills/graphify/skills/codex/references/hooks.md +0 -33
- package/skills/graphify/skills/codex/references/query.md +0 -249
- package/skills/graphify/skills/codex/references/transcribe.md +0 -48
- package/skills/graphify/skills/codex/references/update.md +0 -179
- package/skills/graphify/skills/copilot/references/add-watch.md +0 -56
- package/skills/graphify/skills/copilot/references/exports.md +0 -71
- package/skills/graphify/skills/copilot/references/extraction-spec.md +0 -68
- package/skills/graphify/skills/copilot/references/github-and-merge.md +0 -46
- package/skills/graphify/skills/copilot/references/hooks.md +0 -33
- package/skills/graphify/skills/copilot/references/query.md +0 -249
- package/skills/graphify/skills/copilot/references/transcribe.md +0 -48
- package/skills/graphify/skills/copilot/references/update.md +0 -179
- package/skills/graphify/skills/droid/references/add-watch.md +0 -56
- package/skills/graphify/skills/droid/references/exports.md +0 -71
- package/skills/graphify/skills/droid/references/extraction-spec.md +0 -68
- package/skills/graphify/skills/droid/references/github-and-merge.md +0 -46
- package/skills/graphify/skills/droid/references/hooks.md +0 -33
- package/skills/graphify/skills/droid/references/query.md +0 -249
- package/skills/graphify/skills/droid/references/transcribe.md +0 -48
- package/skills/graphify/skills/droid/references/update.md +0 -179
- package/skills/graphify/skills/kilo/references/add-watch.md +0 -56
- package/skills/graphify/skills/kilo/references/exports.md +0 -71
- package/skills/graphify/skills/kilo/references/extraction-spec.md +0 -68
- package/skills/graphify/skills/kilo/references/github-and-merge.md +0 -46
- package/skills/graphify/skills/kilo/references/hooks.md +0 -33
- package/skills/graphify/skills/kilo/references/query.md +0 -249
- package/skills/graphify/skills/kilo/references/transcribe.md +0 -48
- package/skills/graphify/skills/kilo/references/update.md +0 -179
- package/skills/graphify/skills/kiro/references/add-watch.md +0 -56
- package/skills/graphify/skills/kiro/references/exports.md +0 -71
- package/skills/graphify/skills/kiro/references/extraction-spec.md +0 -29
- package/skills/graphify/skills/kiro/references/github-and-merge.md +0 -46
- package/skills/graphify/skills/kiro/references/hooks.md +0 -33
- package/skills/graphify/skills/kiro/references/query.md +0 -249
- package/skills/graphify/skills/kiro/references/transcribe.md +0 -48
- package/skills/graphify/skills/kiro/references/update.md +0 -179
- package/skills/graphify/skills/opencode/references/add-watch.md +0 -56
- package/skills/graphify/skills/opencode/references/exports.md +0 -71
- package/skills/graphify/skills/opencode/references/extraction-spec.md +0 -68
- package/skills/graphify/skills/opencode/references/github-and-merge.md +0 -46
- package/skills/graphify/skills/opencode/references/hooks.md +0 -33
- package/skills/graphify/skills/opencode/references/query.md +0 -249
- package/skills/graphify/skills/opencode/references/transcribe.md +0 -48
- package/skills/graphify/skills/opencode/references/update.md +0 -179
- package/skills/graphify/skills/pi/references/add-watch.md +0 -56
- package/skills/graphify/skills/pi/references/exports.md +0 -71
- package/skills/graphify/skills/pi/references/extraction-spec.md +0 -29
- package/skills/graphify/skills/pi/references/github-and-merge.md +0 -46
- package/skills/graphify/skills/pi/references/hooks.md +0 -33
- package/skills/graphify/skills/pi/references/query.md +0 -249
- package/skills/graphify/skills/pi/references/transcribe.md +0 -48
- package/skills/graphify/skills/pi/references/update.md +0 -179
- package/skills/graphify/skills/trae/references/add-watch.md +0 -56
- package/skills/graphify/skills/trae/references/exports.md +0 -71
- package/skills/graphify/skills/trae/references/extraction-spec.md +0 -68
- package/skills/graphify/skills/trae/references/github-and-merge.md +0 -46
- package/skills/graphify/skills/trae/references/hooks.md +0 -35
- package/skills/graphify/skills/trae/references/query.md +0 -249
- package/skills/graphify/skills/trae/references/transcribe.md +0 -48
- package/skills/graphify/skills/trae/references/update.md +0 -179
- package/skills/graphify/skills/vscode/references/add-watch.md +0 -56
- package/skills/graphify/skills/vscode/references/exports.md +0 -71
- package/skills/graphify/skills/vscode/references/extraction-spec.md +0 -68
- package/skills/graphify/skills/vscode/references/github-and-merge.md +0 -46
- package/skills/graphify/skills/vscode/references/hooks.md +0 -33
- package/skills/graphify/skills/vscode/references/query.md +0 -249
- package/skills/graphify/skills/vscode/references/transcribe.md +0 -48
- package/skills/graphify/skills/vscode/references/update.md +0 -179
- package/skills/graphify/skills/windows/references/add-watch.md +0 -56
- package/skills/graphify/skills/windows/references/exports.md +0 -71
- package/skills/graphify/skills/windows/references/extraction-spec.md +0 -68
- package/skills/graphify/skills/windows/references/github-and-merge.md +0 -46
- package/skills/graphify/skills/windows/references/hooks.md +0 -33
- package/skills/graphify/skills/windows/references/query.md +0 -249
- package/skills/graphify/skills/windows/references/transcribe.md +0 -48
- package/skills/graphify/skills/windows/references/update.md +0 -179
- package/skills/graphify/symbol_resolution.py +0 -538
- package/skills/graphify/transcribe.py +0 -184
- package/skills/graphify/tree_html.py +0 -582
- package/skills/graphify/validate.py +0 -72
- package/skills/graphify/watch.py +0 -898
- package/skills/graphify/wiki.py +0 -282
|
@@ -1,392 +0,0 @@
|
|
|
1
|
-
"""mcp_ingest.py — Extract MCP (Model Context Protocol) server configuration files.
|
|
2
|
-
|
|
3
|
-
Reads `.mcp.json` / `claude_desktop_config.json` / `mcp.json` / `mcp_servers.json`
|
|
4
|
-
and turns the `mcpServers` map into Graphify nodes and edges.
|
|
5
|
-
|
|
6
|
-
Symmetry with `serve.py`: Graphify exposes itself AS an MCP server. This module
|
|
7
|
-
indexes MCP servers AS a corpus type, completing the loop — an agent that runs
|
|
8
|
-
graphify with `--mcp` can now query its own configured MCP layer.
|
|
9
|
-
|
|
10
|
-
Entry point:
|
|
11
|
-
extract_mcp_config(path: Path) -> dict[str, list[dict]]
|
|
12
|
-
|
|
13
|
-
Returns `{"nodes": [...], "edges": [...]}` compatible with Graphify's
|
|
14
|
-
extraction-result format. Returns `{"nodes": [...], "edges": [...], "error": "..."}`
|
|
15
|
-
when the file is malformed, too large, or has no `mcpServers` map — the empty
|
|
16
|
-
result keeps it indistinguishable from "no MCP config here" for downstream
|
|
17
|
-
callers.
|
|
18
|
-
|
|
19
|
-
Detected filenames (case-sensitive, matched on basename):
|
|
20
|
-
- .mcp.json (Claude Code project config)
|
|
21
|
-
- claude_desktop_config.json (Claude Desktop)
|
|
22
|
-
- mcp.json (generic / per-tool)
|
|
23
|
-
- mcp_servers.json (alternate naming)
|
|
24
|
-
|
|
25
|
-
Schema emitted:
|
|
26
|
-
Node kinds:
|
|
27
|
-
- file the config file itself (label = filename)
|
|
28
|
-
- mcp_server one per entry under mcpServers
|
|
29
|
-
- mcp_command executable (npx, uvx, node, python, ...) — global ID
|
|
30
|
-
- mcp_package npm / pypi package id parsed from args — global ID
|
|
31
|
-
- env_var env variable NAME only — global ID. VALUES ARE NEVER READ.
|
|
32
|
-
|
|
33
|
-
Edge relations:
|
|
34
|
-
- contains file -> mcp_server
|
|
35
|
-
- references mcp_server -> mcp_command
|
|
36
|
-
- references mcp_server -> mcp_package
|
|
37
|
-
- requires_env mcp_server -> env_var (new relation; distinguishes
|
|
38
|
-
env dependencies from generic refs)
|
|
39
|
-
|
|
40
|
-
Security:
|
|
41
|
-
- Env var VALUES are never read, persisted, labelled, or surfaced. Only env
|
|
42
|
-
var NAMES become nodes. (`env: {"API_KEY": "sk-..."}` -> node "API_KEY" only.)
|
|
43
|
-
- File size capped at 1 MiB (matches extract_json).
|
|
44
|
-
- All labels go through `sanitize_label` (control characters stripped, length
|
|
45
|
-
capped) before emission.
|
|
46
|
-
- Args are NOT persisted as nodes/edges to avoid leaking paths or secrets that
|
|
47
|
-
some servers embed as positional args.
|
|
48
|
-
|
|
49
|
-
Cross-config emergent edges:
|
|
50
|
-
Because `mcp_command`, `mcp_package`, and `env_var` nodes use global IDs (no
|
|
51
|
-
per-file stem prefix), the same package or env var across two MCP configs
|
|
52
|
-
produces shared nodes — naturally surfacing "what configs depend on this
|
|
53
|
-
thing?" via graph traversal. Server nodes ARE stem-scoped so two configs
|
|
54
|
-
declaring different servers under the same key (e.g., both have "filesystem")
|
|
55
|
-
do not collide.
|
|
56
|
-
"""
|
|
57
|
-
|
|
58
|
-
from __future__ import annotations
|
|
59
|
-
|
|
60
|
-
import json
|
|
61
|
-
import re
|
|
62
|
-
import unicodedata
|
|
63
|
-
from pathlib import Path
|
|
64
|
-
from typing import Any
|
|
65
|
-
|
|
66
|
-
from graphify.security import sanitize_label
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
MCP_CONFIG_FILENAMES: frozenset[str] = frozenset({
|
|
70
|
-
".mcp.json",
|
|
71
|
-
"claude_desktop_config.json",
|
|
72
|
-
"mcp.json",
|
|
73
|
-
"mcp_servers.json",
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
_MAX_BYTES = 1_048_576 # 1 MiB — same cap as extract_json
|
|
77
|
-
_MAX_SERVERS_PER_FILE = 200 # generous; flags pathological configs
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def is_mcp_config_path(path: Path) -> bool:
|
|
81
|
-
"""Return True when ``path`` is a recognised MCP config filename."""
|
|
82
|
-
return path.name in MCP_CONFIG_FILENAMES
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def extract_mcp_config(path: Path) -> dict[str, Any]:
|
|
86
|
-
"""Parse an MCP config file into Graphify nodes and edges.
|
|
87
|
-
|
|
88
|
-
Behaviour matches other extractors in `extract.py`:
|
|
89
|
-
- returns ``{"nodes": [...], "edges": [...]}`` on success
|
|
90
|
-
- returns ``{"nodes": [], "edges": [], "error": "<reason>"}`` on parse
|
|
91
|
-
failure, oversize file, or missing ``mcpServers`` map
|
|
92
|
-
"""
|
|
93
|
-
try:
|
|
94
|
-
with path.open("rb") as fh:
|
|
95
|
-
raw = fh.read(_MAX_BYTES + 1)
|
|
96
|
-
except OSError as exc:
|
|
97
|
-
return {"nodes": [], "edges": [], "error": f"mcp_ingest read error: {exc}"}
|
|
98
|
-
|
|
99
|
-
if len(raw) > _MAX_BYTES:
|
|
100
|
-
return {"nodes": [], "edges": [], "error": "mcp config too large to index"}
|
|
101
|
-
|
|
102
|
-
try:
|
|
103
|
-
text = raw.decode("utf-8")
|
|
104
|
-
except UnicodeDecodeError as exc:
|
|
105
|
-
return {"nodes": [], "edges": [], "error": f"mcp_ingest decode error: {exc}"}
|
|
106
|
-
|
|
107
|
-
try:
|
|
108
|
-
doc = json.loads(text)
|
|
109
|
-
except json.JSONDecodeError as exc:
|
|
110
|
-
return {"nodes": [], "edges": [], "error": f"mcp_ingest json error: {exc}"}
|
|
111
|
-
|
|
112
|
-
if not isinstance(doc, dict):
|
|
113
|
-
return {"nodes": [], "edges": [], "error": "mcp_ingest: root is not an object"}
|
|
114
|
-
|
|
115
|
-
servers = doc.get("mcpServers")
|
|
116
|
-
if not isinstance(servers, dict):
|
|
117
|
-
# Some tools nest the map (e.g., {"mcp": {"servers": {...}}}). Try one
|
|
118
|
-
# well-known alternate shape but do not search exhaustively.
|
|
119
|
-
nested = doc.get("mcp")
|
|
120
|
-
if isinstance(nested, dict):
|
|
121
|
-
servers = nested.get("servers")
|
|
122
|
-
if not isinstance(servers, dict):
|
|
123
|
-
return {"nodes": [], "edges": [], "error": "mcp_ingest: no mcpServers map"}
|
|
124
|
-
|
|
125
|
-
str_path = str(path)
|
|
126
|
-
file_nid = _make_id(str_path)
|
|
127
|
-
nodes: list[dict[str, Any]] = []
|
|
128
|
-
edges: list[dict[str, Any]] = []
|
|
129
|
-
seen_node_ids: set[str] = set()
|
|
130
|
-
seen_edge_keys: set[tuple[str, str, str]] = set()
|
|
131
|
-
|
|
132
|
-
_add_node(
|
|
133
|
-
nodes, seen_node_ids,
|
|
134
|
-
nid=file_nid,
|
|
135
|
-
label=path.name,
|
|
136
|
-
kind="mcp_config_file",
|
|
137
|
-
source_file=str_path,
|
|
138
|
-
line=1,
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
file_stem = _file_stem(path)
|
|
142
|
-
server_count = 0
|
|
143
|
-
for server_name, spec in servers.items():
|
|
144
|
-
if not isinstance(server_name, str) or not server_name:
|
|
145
|
-
continue
|
|
146
|
-
if not isinstance(spec, dict):
|
|
147
|
-
# Skip non-object server entries silently — the broken entry is
|
|
148
|
-
# the user's, not ours.
|
|
149
|
-
continue
|
|
150
|
-
if server_count >= _MAX_SERVERS_PER_FILE:
|
|
151
|
-
break
|
|
152
|
-
server_count += 1
|
|
153
|
-
_emit_server(
|
|
154
|
-
server_name=server_name,
|
|
155
|
-
spec=spec,
|
|
156
|
-
file_nid=file_nid,
|
|
157
|
-
file_stem=file_stem,
|
|
158
|
-
source_file=str_path,
|
|
159
|
-
nodes=nodes,
|
|
160
|
-
edges=edges,
|
|
161
|
-
seen_node_ids=seen_node_ids,
|
|
162
|
-
seen_edge_keys=seen_edge_keys,
|
|
163
|
-
)
|
|
164
|
-
|
|
165
|
-
return {"nodes": nodes, "edges": edges}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
def _emit_server(
|
|
169
|
-
*,
|
|
170
|
-
server_name: str,
|
|
171
|
-
spec: dict[str, Any],
|
|
172
|
-
file_nid: str,
|
|
173
|
-
file_stem: str,
|
|
174
|
-
source_file: str,
|
|
175
|
-
nodes: list[dict[str, Any]],
|
|
176
|
-
edges: list[dict[str, Any]],
|
|
177
|
-
seen_node_ids: set[str],
|
|
178
|
-
seen_edge_keys: set[tuple[str, str, str]],
|
|
179
|
-
) -> None:
|
|
180
|
-
"""Emit nodes/edges for one entry under ``mcpServers``."""
|
|
181
|
-
server_nid = _make_id(file_stem, "mcp_server", server_name)
|
|
182
|
-
_add_node(
|
|
183
|
-
nodes, seen_node_ids,
|
|
184
|
-
nid=server_nid,
|
|
185
|
-
label=server_name,
|
|
186
|
-
kind="mcp_server",
|
|
187
|
-
source_file=source_file,
|
|
188
|
-
line=1, # JSON doesn't expose line numbers without a parser pass
|
|
189
|
-
)
|
|
190
|
-
_add_edge(
|
|
191
|
-
edges, seen_edge_keys,
|
|
192
|
-
source=file_nid,
|
|
193
|
-
target=server_nid,
|
|
194
|
-
relation="contains",
|
|
195
|
-
source_file=source_file,
|
|
196
|
-
line=1,
|
|
197
|
-
)
|
|
198
|
-
|
|
199
|
-
command = spec.get("command")
|
|
200
|
-
if isinstance(command, str) and command.strip():
|
|
201
|
-
cmd_label = command.strip()
|
|
202
|
-
cmd_nid = _make_id("mcp_command", cmd_label)
|
|
203
|
-
_add_node(
|
|
204
|
-
nodes, seen_node_ids,
|
|
205
|
-
nid=cmd_nid,
|
|
206
|
-
label=cmd_label,
|
|
207
|
-
kind="mcp_command",
|
|
208
|
-
source_file=source_file,
|
|
209
|
-
line=1,
|
|
210
|
-
)
|
|
211
|
-
_add_edge(
|
|
212
|
-
edges, seen_edge_keys,
|
|
213
|
-
source=server_nid,
|
|
214
|
-
target=cmd_nid,
|
|
215
|
-
relation="references",
|
|
216
|
-
source_file=source_file,
|
|
217
|
-
line=1,
|
|
218
|
-
context="command",
|
|
219
|
-
)
|
|
220
|
-
|
|
221
|
-
args = spec.get("args")
|
|
222
|
-
if isinstance(args, list):
|
|
223
|
-
package = _detect_package_from_args(args)
|
|
224
|
-
if package:
|
|
225
|
-
pkg_nid = _make_id("mcp_package", package)
|
|
226
|
-
_add_node(
|
|
227
|
-
nodes, seen_node_ids,
|
|
228
|
-
nid=pkg_nid,
|
|
229
|
-
label=package,
|
|
230
|
-
kind="mcp_package",
|
|
231
|
-
source_file=source_file,
|
|
232
|
-
line=1,
|
|
233
|
-
)
|
|
234
|
-
_add_edge(
|
|
235
|
-
edges, seen_edge_keys,
|
|
236
|
-
source=server_nid,
|
|
237
|
-
target=pkg_nid,
|
|
238
|
-
relation="references",
|
|
239
|
-
source_file=source_file,
|
|
240
|
-
line=1,
|
|
241
|
-
context="package",
|
|
242
|
-
)
|
|
243
|
-
|
|
244
|
-
env = spec.get("env")
|
|
245
|
-
if isinstance(env, dict):
|
|
246
|
-
# ONLY KEYS. Values may contain secrets and are never read here.
|
|
247
|
-
for env_name in env.keys():
|
|
248
|
-
if not isinstance(env_name, str) or not env_name:
|
|
249
|
-
continue
|
|
250
|
-
env_nid = _make_id("env_var", env_name)
|
|
251
|
-
_add_node(
|
|
252
|
-
nodes, seen_node_ids,
|
|
253
|
-
nid=env_nid,
|
|
254
|
-
label=env_name,
|
|
255
|
-
kind="env_var",
|
|
256
|
-
source_file=source_file,
|
|
257
|
-
line=1,
|
|
258
|
-
)
|
|
259
|
-
_add_edge(
|
|
260
|
-
edges, seen_edge_keys,
|
|
261
|
-
source=server_nid,
|
|
262
|
-
target=env_nid,
|
|
263
|
-
relation="requires_env",
|
|
264
|
-
source_file=source_file,
|
|
265
|
-
line=1,
|
|
266
|
-
)
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
# ── Package detection from args ───────────────────────────────────────────────
|
|
270
|
-
|
|
271
|
-
# Patterns observed in real MCP server configs:
|
|
272
|
-
# ["-y", "@modelcontextprotocol/server-filesystem", "/data"] (npx)
|
|
273
|
-
# ["-y", "@org/pkg@1.2.3"]
|
|
274
|
-
# ["mcp-server-fetch"] (uvx / python)
|
|
275
|
-
# ["mcp-server-time", "--local-timezone=UTC"]
|
|
276
|
-
# ["@scoped/some-mcp"] (pnpx)
|
|
277
|
-
# ["mcp-server-fetch"] (uvx direct)
|
|
278
|
-
_NPM_PKG_RE = re.compile(r"^@[a-z0-9][a-z0-9._-]*/[a-z0-9][a-z0-9._-]*(?:@[\w.\-+]+)?$")
|
|
279
|
-
_PY_MCP_PKG_RE = re.compile(r"^[a-z0-9][a-z0-9._-]*-mcp(?:-[a-z0-9._-]+)?$|^mcp-[a-z0-9][a-z0-9._-]*$")
|
|
280
|
-
_ARG_FLAG_RE = re.compile(r"^-{1,2}\w")
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
def _detect_package_from_args(args: list[Any]) -> str | None:
|
|
284
|
-
"""Return the first arg that looks like an npm or pypi package id, else None.
|
|
285
|
-
|
|
286
|
-
Skips short flags (-y, --yes) and option arguments (--local-timezone=UTC).
|
|
287
|
-
"""
|
|
288
|
-
for raw in args:
|
|
289
|
-
if not isinstance(raw, str):
|
|
290
|
-
continue
|
|
291
|
-
arg = raw.strip()
|
|
292
|
-
if not arg or _ARG_FLAG_RE.match(arg):
|
|
293
|
-
continue
|
|
294
|
-
if _NPM_PKG_RE.match(arg):
|
|
295
|
-
return _strip_version(arg)
|
|
296
|
-
if _PY_MCP_PKG_RE.match(arg):
|
|
297
|
-
return arg
|
|
298
|
-
return None
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
def _strip_version(pkg: str) -> str:
|
|
302
|
-
"""Drop the ``@version`` suffix from an npm package id, preserving the scope.
|
|
303
|
-
|
|
304
|
-
Scoped: ``@scope/name`` or ``@scope/name@1.2.3`` — there are at most two
|
|
305
|
-
``@`` chars; the second is the version separator.
|
|
306
|
-
Unscoped: ``name`` or ``name@1.2.3``.
|
|
307
|
-
"""
|
|
308
|
-
if pkg.startswith("@"):
|
|
309
|
-
version_at = pkg.find("@", 1)
|
|
310
|
-
return pkg if version_at == -1 else pkg[:version_at]
|
|
311
|
-
version_at = pkg.find("@")
|
|
312
|
-
return pkg if version_at == -1 else pkg[:version_at]
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
# ── Node / edge construction (Graphify schema) ────────────────────────────────
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
def _add_node(
|
|
319
|
-
nodes: list[dict[str, Any]],
|
|
320
|
-
seen: set[str],
|
|
321
|
-
*,
|
|
322
|
-
nid: str,
|
|
323
|
-
label: str,
|
|
324
|
-
kind: str,
|
|
325
|
-
source_file: str,
|
|
326
|
-
line: int,
|
|
327
|
-
) -> None:
|
|
328
|
-
"""Append a node if not already present. ``kind`` is metadata, not file_type."""
|
|
329
|
-
if not nid or nid in seen:
|
|
330
|
-
return
|
|
331
|
-
seen.add(nid)
|
|
332
|
-
nodes.append({
|
|
333
|
-
"id": nid,
|
|
334
|
-
"label": sanitize_label(label),
|
|
335
|
-
"file_type": "code",
|
|
336
|
-
"source_file": source_file,
|
|
337
|
-
"source_location": f"L{line}",
|
|
338
|
-
"metadata": {"mcp_kind": kind},
|
|
339
|
-
})
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
def _add_edge(
|
|
343
|
-
edges: list[dict[str, Any]],
|
|
344
|
-
seen: set[tuple[str, str, str]],
|
|
345
|
-
*,
|
|
346
|
-
source: str,
|
|
347
|
-
target: str,
|
|
348
|
-
relation: str,
|
|
349
|
-
source_file: str,
|
|
350
|
-
line: int,
|
|
351
|
-
context: str | None = None,
|
|
352
|
-
) -> None:
|
|
353
|
-
"""Append an edge if (source, target, relation) is not already present."""
|
|
354
|
-
if not source or not target or source == target:
|
|
355
|
-
return
|
|
356
|
-
key = (source, target, relation)
|
|
357
|
-
if key in seen:
|
|
358
|
-
return
|
|
359
|
-
seen.add(key)
|
|
360
|
-
edge: dict[str, Any] = {
|
|
361
|
-
"source": source,
|
|
362
|
-
"target": target,
|
|
363
|
-
"relation": relation,
|
|
364
|
-
"confidence": "EXTRACTED",
|
|
365
|
-
"confidence_score": 1.0,
|
|
366
|
-
"source_file": source_file,
|
|
367
|
-
"source_location": f"L{line}",
|
|
368
|
-
"weight": 1.0,
|
|
369
|
-
}
|
|
370
|
-
if context:
|
|
371
|
-
edge["context"] = context
|
|
372
|
-
edges.append(edge)
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
# ── ID helpers (kept local; mirror extract.py shape) ──────────────────────────
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
def _make_id(*parts: str) -> str:
|
|
379
|
-
"""Build a stable node ID. Must match extract._make_id's normalisation rules."""
|
|
380
|
-
combined = "_".join(p.strip("_.") for p in parts if p)
|
|
381
|
-
combined = unicodedata.normalize("NFKC", combined)
|
|
382
|
-
cleaned = re.sub(r"[^\w]+", "_", combined, flags=re.UNICODE)
|
|
383
|
-
cleaned = re.sub(r"_+", "_", cleaned)
|
|
384
|
-
return cleaned.strip("_").casefold()
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
def _file_stem(path: Path) -> str:
|
|
388
|
-
"""Mirror extract._file_stem: include parent dir name to disambiguate."""
|
|
389
|
-
parent = path.parent.name
|
|
390
|
-
if parent and parent not in (".", ""):
|
|
391
|
-
return f"{parent}.{path.stem}"
|
|
392
|
-
return path.stem
|
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
"""Runtime compatibility probe for Graphify MultiDiGraph mode.
|
|
2
|
-
|
|
3
|
-
Verifies that the current NetworkX runtime supports the behaviors a future
|
|
4
|
-
opt-in --multigraph build will rely on. The probe is BEHAVIOR-based, not
|
|
5
|
-
version-based — both NX 3.4.2 (Py 3.10 lane) and NX 3.6.1+ (Py 3.11+ lane)
|
|
6
|
-
pass. The probe result is cached for the process lifetime via lru_cache.
|
|
7
|
-
|
|
8
|
-
No call sites added yet; downstream multigraph PRs will gate on
|
|
9
|
-
require_multigraph_capabilities() before enabling MDG mode.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
from __future__ import annotations
|
|
13
|
-
|
|
14
|
-
from collections.abc import Callable
|
|
15
|
-
from dataclasses import dataclass
|
|
16
|
-
from functools import lru_cache
|
|
17
|
-
import sys
|
|
18
|
-
from typing import Any
|
|
19
|
-
|
|
20
|
-
import networkx as nx
|
|
21
|
-
from networkx.readwrite import json_graph
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
@dataclass(frozen=True)
|
|
25
|
-
class CapabilityCheck:
|
|
26
|
-
name: str
|
|
27
|
-
ok: bool
|
|
28
|
-
detail: str
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@dataclass(frozen=True)
|
|
32
|
-
class MultigraphCapabilityResult:
|
|
33
|
-
python_version: str
|
|
34
|
-
networkx_version: str
|
|
35
|
-
checks: tuple[CapabilityCheck, ...]
|
|
36
|
-
|
|
37
|
-
@property
|
|
38
|
-
def ok(self) -> bool:
|
|
39
|
-
return all(check.ok for check in self.checks)
|
|
40
|
-
|
|
41
|
-
@property
|
|
42
|
-
def failed(self) -> tuple[CapabilityCheck, ...]:
|
|
43
|
-
return tuple(check for check in self.checks if not check.ok)
|
|
44
|
-
|
|
45
|
-
def error_message(self) -> str:
|
|
46
|
-
if self.ok:
|
|
47
|
-
return (
|
|
48
|
-
"Graphify MultiDiGraph capability probe passed "
|
|
49
|
-
f"(Python {self.python_version}, NetworkX {self.networkx_version})."
|
|
50
|
-
)
|
|
51
|
-
failed = "; ".join(f"{check.name}: {check.detail}" for check in self.failed)
|
|
52
|
-
return (
|
|
53
|
-
"error: --multigraph requires NetworkX keyed MultiDiGraph node-link "
|
|
54
|
-
"round-trip support. "
|
|
55
|
-
f"Detected Python {self.python_version}, NetworkX {self.networkx_version}. "
|
|
56
|
-
f"Failed capability check(s): {failed}. "
|
|
57
|
-
"Default simple graph mode remains available."
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def _check(name: str, func: Callable[[], bool | str]) -> CapabilityCheck:
|
|
62
|
-
try:
|
|
63
|
-
detail = func()
|
|
64
|
-
except Exception as exc:
|
|
65
|
-
return CapabilityCheck(name, False, f"{type(exc).__name__}: {exc}")
|
|
66
|
-
if detail is True:
|
|
67
|
-
return CapabilityCheck(name, True, "ok")
|
|
68
|
-
if isinstance(detail, str):
|
|
69
|
-
return CapabilityCheck(name, False, detail)
|
|
70
|
-
return CapabilityCheck(name, False, f"unexpected result {detail!r}")
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def _build_probe_graph() -> nx.MultiDiGraph:
|
|
74
|
-
graph = nx.MultiDiGraph()
|
|
75
|
-
graph.add_node("a", label="A")
|
|
76
|
-
graph.add_node("b", label="B")
|
|
77
|
-
graph.add_edge("a", "b", key="calls:a.py:L1", relation="calls", source_file="a.py")
|
|
78
|
-
graph.add_edge("a", "b", key="imports:a.py:L2", relation="imports", source_file="a.py")
|
|
79
|
-
return graph
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def _probe_keyed_parallel_edges() -> bool | str:
|
|
83
|
-
graph = _build_probe_graph()
|
|
84
|
-
if not graph.is_multigraph() or not graph.is_directed():
|
|
85
|
-
return f"probe graph type was {type(graph).__name__}"
|
|
86
|
-
if graph.number_of_edges("a", "b") != 2:
|
|
87
|
-
return f"expected 2 keyed parallel edges, got {graph.number_of_edges('a', 'b')}"
|
|
88
|
-
keys = set(graph["a"]["b"].keys())
|
|
89
|
-
expected = {"calls:a.py:L1", "imports:a.py:L2"}
|
|
90
|
-
if keys != expected:
|
|
91
|
-
return f"expected keys {sorted(expected)}, got {sorted(keys)}"
|
|
92
|
-
return True
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
def _probe_node_link_round_trip() -> bool | str:
|
|
96
|
-
graph = _build_probe_graph()
|
|
97
|
-
data = json_graph.node_link_data(graph, edges="links")
|
|
98
|
-
if data.get("multigraph") is not True:
|
|
99
|
-
return f"serialized multigraph flag was {data.get('multigraph')!r}"
|
|
100
|
-
if data.get("directed") is not True:
|
|
101
|
-
return f"serialized directed flag was {data.get('directed')!r}"
|
|
102
|
-
links = data.get("links")
|
|
103
|
-
if not isinstance(links, list) or len(links) != 2:
|
|
104
|
-
length = 0 if not isinstance(links, list) else len(links)
|
|
105
|
-
return f"serialized links length was {length}"
|
|
106
|
-
serialized_keys: set[str] = set()
|
|
107
|
-
for edge in links:
|
|
108
|
-
if isinstance(edge, dict):
|
|
109
|
-
edge_key = edge.get("key")
|
|
110
|
-
if isinstance(edge_key, str):
|
|
111
|
-
serialized_keys.add(edge_key)
|
|
112
|
-
expected = {"calls:a.py:L1", "imports:a.py:L2"}
|
|
113
|
-
if serialized_keys != expected:
|
|
114
|
-
return f"serialized keys {sorted(serialized_keys)} did not match {sorted(expected)}"
|
|
115
|
-
loaded = json_graph.node_link_graph(data, edges="links")
|
|
116
|
-
if not isinstance(loaded, nx.MultiDiGraph):
|
|
117
|
-
return f"round-trip graph type was {type(loaded).__name__}"
|
|
118
|
-
if loaded.number_of_edges("a", "b") != 2:
|
|
119
|
-
return f"round-trip edge count was {loaded.number_of_edges('a', 'b')}"
|
|
120
|
-
loaded_keys = set(loaded["a"]["b"].keys())
|
|
121
|
-
if loaded_keys != expected:
|
|
122
|
-
return f"round-trip keys {sorted(loaded_keys)} did not match {sorted(expected)}"
|
|
123
|
-
return True
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
def _probe_duplicate_key_overwrite_semantics() -> bool | str:
|
|
127
|
-
graph = nx.MultiDiGraph()
|
|
128
|
-
graph.add_edge("x", "y", key="same", marker="first")
|
|
129
|
-
graph.add_edge("x", "y", key="same", marker="second")
|
|
130
|
-
edges = list(graph.edges(keys=True, data=True))
|
|
131
|
-
if len(edges) != 1:
|
|
132
|
-
return f"expected one edge after duplicate-key add, got {len(edges)}"
|
|
133
|
-
if edges[0][3].get("marker") != "second":
|
|
134
|
-
return f"expected second attr overwrite, got {edges[0][3].get('marker')!r}"
|
|
135
|
-
return True
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
def _probe_reserved_key_attr_rejected() -> bool | str:
|
|
139
|
-
"""Verify the Python language guarantee that NetworkX add_edge inherits.
|
|
140
|
-
|
|
141
|
-
Python forbids passing the same keyword argument twice — once explicitly
|
|
142
|
-
and once via **kwargs. This probe confirms that protection still applies
|
|
143
|
-
to nx.MultiDiGraph.add_edge: a future loader that builds attrs from JSON
|
|
144
|
-
will be reliably protected from accidentally setting `key` via attrs while
|
|
145
|
-
also passing `key=` explicitly.
|
|
146
|
-
|
|
147
|
-
The probe always passes on any Python 3.x version. Its purpose is to
|
|
148
|
-
document the invariant explicitly in the probe suite so that if a future
|
|
149
|
-
Python version relaxes this rule (extremely unlikely), the probe surfaces
|
|
150
|
-
the regression.
|
|
151
|
-
"""
|
|
152
|
-
graph = nx.MultiDiGraph()
|
|
153
|
-
attrs: dict[str, Any] = {"key": "attr-key", "relation": "calls"}
|
|
154
|
-
try:
|
|
155
|
-
graph.add_edge("a", "b", key="schema-key", **attrs)
|
|
156
|
-
except TypeError:
|
|
157
|
-
return True
|
|
158
|
-
return "add_edge accepted duplicate key keyword and attr; loader must not rely on this"
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
def _probe_remove_edges_from_two_tuple_semantics() -> bool | str:
|
|
162
|
-
graph = nx.MultiDiGraph()
|
|
163
|
-
graph.add_edge("a", "b", key="one")
|
|
164
|
-
graph.add_edge("a", "b", key="two")
|
|
165
|
-
graph.remove_edges_from([("a", "b")])
|
|
166
|
-
remaining = graph.number_of_edges("a", "b")
|
|
167
|
-
if remaining != 1:
|
|
168
|
-
return f"expected one remaining edge after two-tuple removal, got {remaining}"
|
|
169
|
-
return True
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
def _probe_to_undirected_preserves_multigraph_type() -> bool | str:
|
|
173
|
-
graph = _build_probe_graph()
|
|
174
|
-
undirected = graph.to_undirected()
|
|
175
|
-
undirected_view = graph.to_undirected(as_view=True)
|
|
176
|
-
if not isinstance(undirected, nx.MultiGraph):
|
|
177
|
-
return f"to_undirected() returned {type(undirected).__name__}"
|
|
178
|
-
if not isinstance(undirected_view, nx.MultiGraph):
|
|
179
|
-
return f"to_undirected(as_view=True) returned {type(undirected_view).__name__}"
|
|
180
|
-
return True
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
@lru_cache(maxsize=1)
|
|
184
|
-
def probe_multigraph_capabilities() -> MultigraphCapabilityResult:
|
|
185
|
-
checks = (
|
|
186
|
-
_check("keyed_parallel_edges", _probe_keyed_parallel_edges),
|
|
187
|
-
_check("node_link_edges_links_round_trip", _probe_node_link_round_trip),
|
|
188
|
-
_check("duplicate_key_overwrite_semantics", _probe_duplicate_key_overwrite_semantics),
|
|
189
|
-
_check("reserved_key_attr_rejected", _probe_reserved_key_attr_rejected),
|
|
190
|
-
_check(
|
|
191
|
-
"remove_edges_from_two_tuple_semantics",
|
|
192
|
-
_probe_remove_edges_from_two_tuple_semantics,
|
|
193
|
-
),
|
|
194
|
-
_check(
|
|
195
|
-
"to_undirected_preserves_multigraph_type",
|
|
196
|
-
_probe_to_undirected_preserves_multigraph_type,
|
|
197
|
-
),
|
|
198
|
-
)
|
|
199
|
-
return MultigraphCapabilityResult(
|
|
200
|
-
python_version=(
|
|
201
|
-
f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
202
|
-
),
|
|
203
|
-
networkx_version=nx.__version__,
|
|
204
|
-
checks=checks,
|
|
205
|
-
)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
def require_multigraph_capabilities() -> MultigraphCapabilityResult:
|
|
209
|
-
result = probe_multigraph_capabilities()
|
|
210
|
-
if not result.ok:
|
|
211
|
-
raise RuntimeError(result.error_message())
|
|
212
|
-
return result
|