sql-code-graph 1.4.3__py3-none-any.whl → 1.5.1__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.
- {sql_code_graph-1.4.3.dist-info → sql_code_graph-1.5.1.dist-info}/METADATA +21 -8
- {sql_code_graph-1.4.3.dist-info → sql_code_graph-1.5.1.dist-info}/RECORD +11 -11
- sqlcg/__init__.py +1 -1
- sqlcg/cli/commands/install.py +56 -43
- sqlcg/cli/commands/mcp.py +58 -10
- sqlcg/cli/main.py +34 -3
- sqlcg/server/models.py +3 -0
- sqlcg/server/server.py +9 -0
- sqlcg/server/tools.py +11 -1
- {sql_code_graph-1.4.3.dist-info → sql_code_graph-1.5.1.dist-info}/WHEEL +0 -0
- {sql_code_graph-1.4.3.dist-info → sql_code_graph-1.5.1.dist-info}/entry_points.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sql-code-graph
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.1
|
|
4
4
|
Summary: SQL code graph analyzer and lineage tracer
|
|
5
5
|
Project-URL: Homepage, https://github.com/Warhorze/sql-code-graph
|
|
6
6
|
Project-URL: Repository, https://github.com/Warhorze/sql-code-graph
|
|
@@ -36,7 +36,7 @@ Description-Content-Type: text/markdown
|
|
|
36
36
|
|
|
37
37
|
# sql-code-graph
|
|
38
38
|
|
|
39
|
-
> **
|
|
39
|
+
> **No backward-compatibility guarantees.** APIs, CLI flags, and graph schema may
|
|
40
40
|
> change between releases without a deprecation period. Pin to an exact version
|
|
41
41
|
> in production. Re-indexing is always the migration path.
|
|
42
42
|
|
|
@@ -93,7 +93,7 @@ for that project.
|
|
|
93
93
|
# 1. Install
|
|
94
94
|
pip install sql-code-graph
|
|
95
95
|
|
|
96
|
-
# 2. Register with Claude Code (~/.claude
|
|
96
|
+
# 2. Register with Claude Code (writes ~/.claude.json)
|
|
97
97
|
sqlcg install
|
|
98
98
|
|
|
99
99
|
# 3. Restart Claude Code
|
|
@@ -138,7 +138,7 @@ This project uses sql-code-graph. MCP tools are available:
|
|
|
138
138
|
- `trace_column_lineage` — trace where a column's value comes from
|
|
139
139
|
- `get_upstream_dependencies` / `get_downstream_dependencies` — dependency chains
|
|
140
140
|
- `search_sql_pattern` — full-text search across all indexed SQL
|
|
141
|
-
- `
|
|
141
|
+
- `execute_sql` — raw read-only SQL query for advanced analysis
|
|
142
142
|
```
|
|
143
143
|
|
|
144
144
|
The MCP server works without this — Claude can discover the tools on its own —
|
|
@@ -214,11 +214,14 @@ After indexing, `sqlcg db info` shows non-zero `STAR_EXPANSION lineage edges`, a
|
|
|
214
214
|
| `diff_impact(changed_files)` | What a set of changed files affects downstream |
|
|
215
215
|
| `get_backfill_order(table_qualified)` | Topological rebuild/backfill order |
|
|
216
216
|
| `scope_change(target)` | Synthesised change-scope summary for a target |
|
|
217
|
+
| **Analysis** | |
|
|
218
|
+
| `get_hub_ranking(k)` | Top-k tables by downstream dependent count (hub/centrality) |
|
|
219
|
+
| `analyze_unused()` | Tables with no within-corpus consumers (dead-code candidates, heuristic) |
|
|
217
220
|
| **Search & meta** | |
|
|
218
221
|
| `search_sql_pattern(query)` | Full-text search across indexed SQL |
|
|
219
222
|
| `list_dialects_and_repos()` | List indexed repos and dialects (catalogue) |
|
|
220
223
|
| `db_info()` | Graph health, node counts, parse quality breakdown, warnings, freshness (indexed SHA vs HEAD) |
|
|
221
|
-
| `
|
|
224
|
+
| `execute_sql(query)` | Raw read-only SQL query against the graph |
|
|
222
225
|
| `submit_feedback(...)` | Report a false positive/negative to improve metrics |
|
|
223
226
|
|
|
224
227
|
> **Input format**: lineage/dependency tools expect a **schema-qualified** column
|
|
@@ -259,16 +262,26 @@ sqlcg gain # show usage metrics
|
|
|
259
262
|
sqlcg report # generate FP/error report
|
|
260
263
|
sqlcg mcp best-practices # print the fact/heuristic boundary for the MCP tools
|
|
261
264
|
sqlcg mcp start # start MCP server manually
|
|
262
|
-
sqlcg mcp status # server status JSON
|
|
265
|
+
sqlcg mcp status # server status JSON — incl. running version + stale_by_version
|
|
263
266
|
sqlcg mcp stop # stop the running MCP server gracefully
|
|
264
267
|
sqlcg mcp restart # stop the server (client must respawn it)
|
|
265
268
|
sqlcg version # show installed version
|
|
266
269
|
```
|
|
267
270
|
|
|
271
|
+
### Staying on the latest build (v1.5.0)
|
|
272
|
+
|
|
273
|
+
The installed package, the CLI, and the running MCP server all report the **same**
|
|
274
|
+
version (`sqlcg.__version__`). After upgrading, an editor may still be talking to an
|
|
275
|
+
old MCP process — `sqlcg mcp status` surfaces this directly: it reports the running
|
|
276
|
+
server's `version` and a `stale_by_version` flag that is `true` when the live server
|
|
277
|
+
differs from the installed build. Re-running `sqlcg install` stops the stale server so
|
|
278
|
+
your editor respawns it on the new build, so you never debug against an outdated server.
|
|
279
|
+
|
|
268
280
|
### Reads while the server is running (v1.2.0)
|
|
269
281
|
|
|
270
|
-
|
|
271
|
-
|
|
282
|
+
DuckDB takes an exclusive lock on the database file, so while the MCP server is
|
|
283
|
+
live it holds that lock (other processes cannot open the file, even read-only).
|
|
284
|
+
CLI **read** commands (`find`, `analyze`, `db info`, `list-repos`,
|
|
272
285
|
`gain`) automatically route their query through the running server over its
|
|
273
286
|
control socket and return rows as usual — no flag, no config. When no server is
|
|
274
287
|
running they open the database directly, exactly as before. If the server is
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
sqlcg/__init__.py,sha256
|
|
1
|
+
sqlcg/__init__.py,sha256=vmaeEVkmCFSAYwwUwVJQ-DbgNbr75sJbUxQmL0p66LA,115
|
|
2
2
|
sqlcg/__main__.py,sha256=1YoFLcqEgTwYq1J3TbUwpkdG0zeeLIf2fJvwWI-CLFU,109
|
|
3
3
|
sqlcg/cli/__init__.py,sha256=W8fD0LpMq2xm_5WKGNMvJh2WBL1ho5E8hUeAqXQYT1g,28
|
|
4
|
-
sqlcg/cli/main.py,sha256=
|
|
4
|
+
sqlcg/cli/main.py,sha256=oiZ0e_blUCnlmmXNSCLYnifleDv6ixm9dF7GEDGe0dQ,2429
|
|
5
5
|
sqlcg/cli/commands/__init__.py,sha256=oSHtr6VD-jNubOjuCQyZj2tBppjMEpQDh-IGQ8of9eA,30
|
|
6
6
|
sqlcg/cli/commands/analyze.py,sha256=_aC5ML3w7YdLi7DL3TFS9OiCEIipuNZxWR6S4peTcn4,12154
|
|
7
7
|
sqlcg/cli/commands/db.py,sha256=TMhPCHRWSo8YmlNnSxmGxrR0_1r3K4e0Bn8unZTkvU4,7248
|
|
@@ -9,8 +9,8 @@ sqlcg/cli/commands/find.py,sha256=p5Vyyx-VBk8YDWYQN16UhECh7PIeMaEyCPEcUFcRFlM,25
|
|
|
9
9
|
sqlcg/cli/commands/gain.py,sha256=SJU1c51a7MgNbZItqQnaBfOWGnV5xpXQctbew5Dr9BE,9062
|
|
10
10
|
sqlcg/cli/commands/git.py,sha256=9a8T2FVxcAHq1H6Cslaq34t10w9fBGf4T2reiLk33t8,6135
|
|
11
11
|
sqlcg/cli/commands/index.py,sha256=RPEpT9aXmK0rixRuHgMcmAn9FLR4JtIbZ1LM4RLkMvY,17050
|
|
12
|
-
sqlcg/cli/commands/install.py,sha256=
|
|
13
|
-
sqlcg/cli/commands/mcp.py,sha256=
|
|
12
|
+
sqlcg/cli/commands/install.py,sha256=VdfZnC41WE3Lx6CcGSd2Xyi-XNNOLP1YQrnXmqo7x3c,10023
|
|
13
|
+
sqlcg/cli/commands/mcp.py,sha256=CA24kRfBltanKuSMkRVRf42GW8cTgG8YQrMruxB3vSQ,11592
|
|
14
14
|
sqlcg/cli/commands/reindex.py,sha256=FqXxvQ0UrxwDS0q2V3gzYgarN3NPh9muaIZ3rgvTmHs,13810
|
|
15
15
|
sqlcg/cli/commands/report.py,sha256=JU0qjyMxwOukE7bN3XvvIzOI7zMg_Gsnvk_8F6pKNpA,4915
|
|
16
16
|
sqlcg/cli/commands/uninstall.py,sha256=WrA1FnINxnd6mmE4-_QBK0aHBnibstJeAT8swnQKG4M,8962
|
|
@@ -50,18 +50,18 @@ sqlcg/parsers/tsql_parser.py,sha256=RRj1pACtAk2tLTDaFWRYF67a0IDvaf5A1YQXWIz0bpQ,
|
|
|
50
50
|
sqlcg/server/__init__.py,sha256=n4wuNE7xyJIJxJZBtmtdccCMQfvTdF-IqIaZVbC4FC4,35
|
|
51
51
|
sqlcg/server/control.py,sha256=qUcztb_aDhL-_X_Nq4q6uGx17cUlbLnI6vUpoZsEjbo,4506
|
|
52
52
|
sqlcg/server/exceptions.py,sha256=EONw34icOByCTpppSQrvQBW6asc4hfqaGDCAFjv96II,469
|
|
53
|
-
sqlcg/server/models.py,sha256=
|
|
53
|
+
sqlcg/server/models.py,sha256=4c4ula195cHEqZycO6ZaSNohSLsodB-Du1FP94JKSdc,19810
|
|
54
54
|
sqlcg/server/noise_filter.py,sha256=Ats2FFzmzFKqvQPWvlUzK8mY9pzlUhL4m1s8P_HNnvI,6335
|
|
55
55
|
sqlcg/server/read_client.py,sha256=4v1OOl12HCHp6J14HrcWQApSixvJ0wwE8UIIZ0pzLyw,7808
|
|
56
|
-
sqlcg/server/server.py,sha256=
|
|
56
|
+
sqlcg/server/server.py,sha256=1RiUmjWLgtN636kRQO6-4rH8amC-m9BKsIMO0aDRqes,24838
|
|
57
57
|
sqlcg/server/skill.py,sha256=EDvmEgl-LQwCxZ9Lca4lt8zCtkk2cCgh9GLYlGgDA64,12828
|
|
58
|
-
sqlcg/server/tools.py,sha256=
|
|
58
|
+
sqlcg/server/tools.py,sha256=SyxPbnFmsR1w2Hi2ENmD_mynCdI9JHhTgv1byXFIKoY,77191
|
|
59
59
|
sqlcg/server/writer.py,sha256=HH8pW6k1c2pbY4b6g2N87Tx4gNCezYqTLNTlAmsHg2Y,18522
|
|
60
60
|
sqlcg/utils/__init__.py,sha256=--iqt5ThTXmT8Wz7da8hs3n0zDfYPl8P-z5OgRJ_77E,154
|
|
61
61
|
sqlcg/utils/hashing.py,sha256=H25-sYfxHKb3_IERFnHyAIYNiXN470Oqo5sJT_D3YOA,438
|
|
62
62
|
sqlcg/utils/ignore.py,sha256=wJjwa0mjnQ_xJExOUxk25y00g065XmmzJapqV3ifD5o,1151
|
|
63
63
|
sqlcg/utils/logging.py,sha256=u0fCmYsLj9o81vawm3xZTHaw68GQYVm7JxG-gP81u8A,840
|
|
64
|
-
sql_code_graph-1.
|
|
65
|
-
sql_code_graph-1.
|
|
66
|
-
sql_code_graph-1.
|
|
67
|
-
sql_code_graph-1.
|
|
64
|
+
sql_code_graph-1.5.1.dist-info/METADATA,sha256=r9mzPhpj5DxUPOhUBET8M0im3BuhdcvdmUbkD3mcLeA,14948
|
|
65
|
+
sql_code_graph-1.5.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
66
|
+
sql_code_graph-1.5.1.dist-info/entry_points.txt,sha256=Wfe49sVzV9p4eVFGo5RxcV-frr3HOP0yzzst8JBxQLQ,46
|
|
67
|
+
sql_code_graph-1.5.1.dist-info/RECORD,,
|
sqlcg/__init__.py
CHANGED
sqlcg/cli/commands/install.py
CHANGED
|
@@ -20,6 +20,8 @@ from pathlib import Path
|
|
|
20
20
|
import typer
|
|
21
21
|
from rich.console import Console
|
|
22
22
|
|
|
23
|
+
from sqlcg.cli.commands.mcp import stop_server
|
|
24
|
+
|
|
23
25
|
console = Console()
|
|
24
26
|
|
|
25
27
|
_SERVER_KEY = "sql-code-graph"
|
|
@@ -70,10 +72,12 @@ def install_cmd(
|
|
|
70
72
|
claude_json = Path.home() / ".claude.json"
|
|
71
73
|
console.print("[dim]--dry-run: would write to ~/.claude.json:[/dim]")
|
|
72
74
|
_preview_claude_json(claude_json, entry)
|
|
75
|
+
console.print("[dim]--dry-run: would stop running MCP server[/dim]")
|
|
73
76
|
_provision_skill(resolved_scope, repo, dry_run=True)
|
|
74
77
|
return
|
|
75
78
|
|
|
76
79
|
# --- Try official claude CLI first ---
|
|
80
|
+
configured_via_claude_cli = False
|
|
77
81
|
claude_bin = shutil.which("claude")
|
|
78
82
|
if claude_bin:
|
|
79
83
|
proc = subprocess.run(
|
|
@@ -84,56 +88,65 @@ def install_cmd(
|
|
|
84
88
|
if proc.returncode == 0:
|
|
85
89
|
console.print(f"[green]Configured:[/green] {_SERVER_KEY} via `claude mcp add`")
|
|
86
90
|
console.print("\nRestart Claude Code to pick up the new MCP server.")
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
console.print(
|
|
91
|
-
f"[yellow]Warning:[/yellow] `claude mcp add` returned rc={proc.returncode}; "
|
|
92
|
-
"falling back to ~/.claude.json write."
|
|
93
|
-
)
|
|
94
|
-
if proc.stderr:
|
|
95
|
-
console.print(f"[dim]{proc.stderr.strip()}[/dim]")
|
|
96
|
-
|
|
97
|
-
# --- Fallback: write ~/.claude.json directly ---
|
|
98
|
-
claude_json = Path.home() / ".claude.json"
|
|
99
|
-
if claude_json.exists():
|
|
100
|
-
try:
|
|
101
|
-
data: dict = json.loads(claude_json.read_text())
|
|
102
|
-
except (json.JSONDecodeError, OSError, TypeError):
|
|
91
|
+
configured_via_claude_cli = True
|
|
92
|
+
else:
|
|
93
|
+
# Non-zero: log and fall through to ~/.claude.json fallback
|
|
103
94
|
console.print(
|
|
104
|
-
f"[yellow]Warning:[/yellow]
|
|
105
|
-
"
|
|
95
|
+
f"[yellow]Warning:[/yellow] `claude mcp add` returned rc={proc.returncode}; "
|
|
96
|
+
"falling back to ~/.claude.json write."
|
|
106
97
|
)
|
|
98
|
+
if proc.stderr:
|
|
99
|
+
console.print(f"[dim]{proc.stderr.strip()}[/dim]")
|
|
100
|
+
|
|
101
|
+
if not configured_via_claude_cli:
|
|
102
|
+
# --- Fallback: write ~/.claude.json directly ---
|
|
103
|
+
claude_json = Path.home() / ".claude.json"
|
|
104
|
+
if claude_json.exists():
|
|
105
|
+
try:
|
|
106
|
+
data: dict = json.loads(claude_json.read_text())
|
|
107
|
+
except (json.JSONDecodeError, OSError, TypeError):
|
|
108
|
+
console.print(
|
|
109
|
+
f"[yellow]Warning:[/yellow] {claude_json} contains invalid JSON — "
|
|
110
|
+
"mcpServers key will be added"
|
|
111
|
+
)
|
|
112
|
+
data = {}
|
|
113
|
+
else:
|
|
107
114
|
data = {}
|
|
108
|
-
else:
|
|
109
|
-
data = {}
|
|
110
115
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
+
existing = data.get("mcpServers", {}).get("user", {}).get(_SERVER_KEY)
|
|
117
|
+
if existing == entry:
|
|
118
|
+
console.print(f"[green]Already configured:[/green] {_SERVER_KEY} (in ~/.claude.json)")
|
|
119
|
+
else:
|
|
120
|
+
data.setdefault("mcpServers", {}).setdefault("user", {})[_SERVER_KEY] = entry
|
|
116
121
|
|
|
117
|
-
|
|
122
|
+
try:
|
|
123
|
+
tmp = claude_json.with_suffix(".tmp")
|
|
124
|
+
tmp.write_text(json.dumps(data, indent=2) + "\n")
|
|
125
|
+
os.replace(tmp, claude_json)
|
|
126
|
+
except (OSError, TypeError, AttributeError):
|
|
127
|
+
pass # Ignore file I/O errors in testing
|
|
118
128
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
console.print(f"[dim]Written to {claude_json}[/dim]")
|
|
129
|
-
|
|
130
|
-
if entry.get("command") == "uvx":
|
|
131
|
-
console.print(
|
|
132
|
-
"[yellow]Note:[/yellow] First startup downloads dependencies (~30s). "
|
|
133
|
-
"Subsequent restarts use cache (~1s)."
|
|
134
|
-
)
|
|
129
|
+
cmd_str = " ".join(cmd_parts)
|
|
130
|
+
console.print(f"[green]Configured:[/green] {_SERVER_KEY} → {cmd_str}")
|
|
131
|
+
console.print(f"[dim]Written to {claude_json}[/dim]")
|
|
132
|
+
|
|
133
|
+
if entry.get("command") == "uvx":
|
|
134
|
+
console.print(
|
|
135
|
+
"[yellow]Note:[/yellow] First startup downloads dependencies (~30s). "
|
|
136
|
+
"Subsequent restarts use cache (~1s)."
|
|
137
|
+
)
|
|
135
138
|
|
|
136
|
-
|
|
139
|
+
console.print("\nRestart Claude Code to pick up the new MCP server.")
|
|
140
|
+
|
|
141
|
+
# --- Single non-dry-run tail: all three success branches converge here. ---
|
|
142
|
+
# The config may not have changed (e.g. "Already configured"), but the
|
|
143
|
+
# binary on disk did — so the stale running server still must be stopped
|
|
144
|
+
# so the editor respawns the freshly-installed build.
|
|
145
|
+
stopped = stop_server()
|
|
146
|
+
if stopped:
|
|
147
|
+
console.print("Stopped running MCP server; your editor will respawn it on the new build.")
|
|
148
|
+
else:
|
|
149
|
+
console.print("No running MCP server to refresh.")
|
|
137
150
|
_provision_skill(resolved_scope, repo, dry_run=False)
|
|
138
151
|
|
|
139
152
|
|
sqlcg/cli/commands/mcp.py
CHANGED
|
@@ -77,9 +77,19 @@ def mcp_best_practices() -> None:
|
|
|
77
77
|
def mcp_status() -> None:
|
|
78
78
|
"""Print server status JSON (connects to control socket).
|
|
79
79
|
|
|
80
|
-
Returns JSON with fields: running, pid, db_path, indexed_sha,
|
|
81
|
-
stale_by_commits, connected_clients, uptime, writer_queue
|
|
82
|
-
is live.
|
|
80
|
+
Returns JSON with fields: running, version, pid, db_path, indexed_sha,
|
|
81
|
+
head_sha, stale_by_commits, connected_clients, uptime, writer_queue,
|
|
82
|
+
stale_by_version when a server is live.
|
|
83
|
+
|
|
84
|
+
`version` is the *running* server's reported sqlcg.__version__ (read live
|
|
85
|
+
over the control socket — the build the editor is currently talking to).
|
|
86
|
+
`stale_by_version` compares it against the *installed* sqlcg.__version__
|
|
87
|
+
(the CLI process you just invoked): true when they differ, false when they
|
|
88
|
+
match, null when the running version cannot be determined (degraded mode).
|
|
89
|
+
When stale, a warning is printed telling you to restart the MCP server via
|
|
90
|
+
your editor or run `sqlcg install` (which stops the stale server for you).
|
|
91
|
+
This is distinct from `stale_by_commits` (graph-vs-repo freshness, not
|
|
92
|
+
package-vs-binary drift).
|
|
83
93
|
|
|
84
94
|
The status response is length-prefixed framed (v1.3.0, B3) so large
|
|
85
95
|
writer_queue payloads are received in full — the client uses the
|
|
@@ -87,7 +97,8 @@ def mcp_status() -> None:
|
|
|
87
97
|
|
|
88
98
|
When no server is found: {"running": false}.
|
|
89
99
|
When the PID file exists with a live process but the socket is unavailable:
|
|
90
|
-
{"running": true, "degraded": "socket unavailable", ...}
|
|
100
|
+
{"running": true, "degraded": "socket unavailable", "stale_by_version": null, ...}
|
|
101
|
+
(the running version cannot be read without a live socket).
|
|
91
102
|
|
|
92
103
|
R3 (stale socket): if the socket file exists but the server is not
|
|
93
104
|
responding (ConnectionRefusedError / FileNotFoundError), falls through
|
|
@@ -119,9 +130,25 @@ def mcp_status() -> None:
|
|
|
119
130
|
|
|
120
131
|
status = json.loads(data.decode())
|
|
121
132
|
|
|
133
|
+
# Drift detection: compare the *running* server's reported version (read
|
|
134
|
+
# over the socket, from the live process) against the *installed*
|
|
135
|
+
# sqlcg.__version__ (the version of the CLI process you just invoked).
|
|
136
|
+
from sqlcg import __version__
|
|
137
|
+
|
|
138
|
+
running_version = status.get("version")
|
|
139
|
+
stale = running_version is not None and running_version != __version__
|
|
140
|
+
status = {**status, "version": running_version, "stale_by_version": stale}
|
|
141
|
+
|
|
122
142
|
# Pretty-print the base fields.
|
|
123
143
|
console.print_json(json.dumps({k: v for k, v in status.items() if k != "writer_queue"}))
|
|
124
144
|
|
|
145
|
+
if stale:
|
|
146
|
+
console.print(
|
|
147
|
+
f"[yellow]Warning:[/yellow] running MCP server is v{running_version}, "
|
|
148
|
+
f"installed sqlcg is v{__version__} — restart the MCP server via your "
|
|
149
|
+
"editor (or run `sqlcg install`, which stops the stale server for you)."
|
|
150
|
+
)
|
|
151
|
+
|
|
125
152
|
# Render the writer_queue block separately for readability.
|
|
126
153
|
wq = status.get("writer_queue")
|
|
127
154
|
if wq:
|
|
@@ -178,6 +205,10 @@ def mcp_status() -> None:
|
|
|
178
205
|
"degraded": "socket unavailable",
|
|
179
206
|
"pid": rec["pid"],
|
|
180
207
|
"db_path": rec["db_path"],
|
|
208
|
+
# Cannot read the running version without a live socket —
|
|
209
|
+
# omit `version` and report drift as unknown (None → JSON
|
|
210
|
+
# null), never guess.
|
|
211
|
+
"stale_by_version": None,
|
|
181
212
|
}
|
|
182
213
|
)
|
|
183
214
|
)
|
|
@@ -185,16 +216,20 @@ def mcp_status() -> None:
|
|
|
185
216
|
console.print_json(json.dumps({"running": False}))
|
|
186
217
|
|
|
187
218
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
"""Stop the running MCP server gracefully.
|
|
219
|
+
def stop_server() -> bool:
|
|
220
|
+
"""Stop a running MCP server gracefully (best-effort). Returns True if stopped.
|
|
191
221
|
|
|
192
222
|
Sends a ``stop`` op via the control socket; waits up to 5 s for the
|
|
193
223
|
socket file to disappear (confirming clean exit). Falls back to SIGTERM
|
|
194
224
|
on the PID-file PID if the socket is unavailable.
|
|
195
225
|
|
|
196
226
|
R3 (stale socket): ``ConnectionRefusedError`` / ``FileNotFoundError`` are
|
|
197
|
-
caught — never hangs on a dead socket
|
|
227
|
+
caught — never hangs on a dead socket; a socket error here must not abort
|
|
228
|
+
the caller (``mcp_stop`` and ``install_cmd`` both rely on this).
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
True if a running server was found and stopped (via socket or SIGTERM
|
|
232
|
+
fallback); False if no running server was found.
|
|
198
233
|
"""
|
|
199
234
|
import socket as _socket
|
|
200
235
|
import time
|
|
@@ -214,6 +249,7 @@ def mcp_stop() -> None:
|
|
|
214
249
|
break
|
|
215
250
|
time.sleep(0.5)
|
|
216
251
|
console.print("[green]Server stopped.[/green]")
|
|
252
|
+
return True
|
|
217
253
|
except (FileNotFoundError, ConnectionRefusedError, OSError):
|
|
218
254
|
# Socket unavailable — fall back to SIGTERM via PID file
|
|
219
255
|
import signal
|
|
@@ -222,8 +258,20 @@ def mcp_stop() -> None:
|
|
|
222
258
|
if rec and is_pid_alive(rec["pid"]):
|
|
223
259
|
os.kill(rec["pid"], signal.SIGTERM)
|
|
224
260
|
console.print(f"[yellow]Socket unavailable — sent SIGTERM to PID {rec['pid']}[/yellow]")
|
|
225
|
-
|
|
226
|
-
|
|
261
|
+
return True
|
|
262
|
+
return False
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@app.command("stop")
|
|
266
|
+
def mcp_stop() -> None:
|
|
267
|
+
"""Stop the running MCP server gracefully.
|
|
268
|
+
|
|
269
|
+
Delegates to ``stop_server()`` (shared with ``sqlcg install``) and prints
|
|
270
|
+
a final message based on whether a server was found.
|
|
271
|
+
"""
|
|
272
|
+
stopped = stop_server()
|
|
273
|
+
if not stopped:
|
|
274
|
+
console.print("[yellow]No server found to stop.[/yellow]")
|
|
227
275
|
|
|
228
276
|
|
|
229
277
|
@app.command("restart")
|
sqlcg/cli/main.py
CHANGED
|
@@ -34,8 +34,41 @@ USING THE MCP TOOLS:
|
|
|
34
34
|
Note: Binary is `sqlcg`; PyPI package is `sql-code-graph`.
|
|
35
35
|
"""
|
|
36
36
|
|
|
37
|
+
|
|
38
|
+
def _version_string() -> str:
|
|
39
|
+
from sqlcg import __version__
|
|
40
|
+
|
|
41
|
+
return f"sqlcg version {__version__}"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _version_callback(value: bool) -> None:
|
|
45
|
+
if value:
|
|
46
|
+
typer.echo(_version_string())
|
|
47
|
+
raise typer.Exit()
|
|
48
|
+
|
|
49
|
+
|
|
37
50
|
app = typer.Typer(name="sqlcg", help=help_text)
|
|
38
51
|
|
|
52
|
+
|
|
53
|
+
@app.callback(invoke_without_command=True)
|
|
54
|
+
def _root(
|
|
55
|
+
ctx: typer.Context,
|
|
56
|
+
version: bool = typer.Option(
|
|
57
|
+
False,
|
|
58
|
+
"--version",
|
|
59
|
+
help="Show version and exit.",
|
|
60
|
+
is_eager=True,
|
|
61
|
+
callback=_version_callback,
|
|
62
|
+
),
|
|
63
|
+
) -> None:
|
|
64
|
+
"""SQL code graph analyzer."""
|
|
65
|
+
# The eager --version callback exits before we get here when --version is
|
|
66
|
+
# passed. Otherwise: if no subcommand was given, show help (matching the
|
|
67
|
+
# pre-callback behaviour) instead of silently exiting 0.
|
|
68
|
+
if ctx.invoked_subcommand is None:
|
|
69
|
+
typer.echo(ctx.get_help())
|
|
70
|
+
|
|
71
|
+
|
|
39
72
|
# Register subcommand groups
|
|
40
73
|
app.add_typer(db.app, name="db")
|
|
41
74
|
app.add_typer(find.app, name="find")
|
|
@@ -56,9 +89,7 @@ app.command("uninstall")(uninstall.uninstall_cmd)
|
|
|
56
89
|
@app.command()
|
|
57
90
|
def version() -> None:
|
|
58
91
|
"""Show version."""
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
typer.echo(f"sqlcg version {__version__}")
|
|
92
|
+
typer.echo(_version_string())
|
|
62
93
|
|
|
63
94
|
|
|
64
95
|
def main() -> None:
|
sqlcg/server/models.py
CHANGED
|
@@ -190,6 +190,9 @@ class DialectRepoResult(BaseModel):
|
|
|
190
190
|
class DbInfoResult(BaseModel):
|
|
191
191
|
"""Result of db_info tool — graph health and parse quality diagnostics."""
|
|
192
192
|
|
|
193
|
+
sqlcg_version: str = Field(
|
|
194
|
+
..., description="Installed sqlcg package version (sqlcg.__version__)"
|
|
195
|
+
)
|
|
193
196
|
schema_version: str = Field(..., description="Graph schema version")
|
|
194
197
|
node_counts: dict[str, int] = Field(
|
|
195
198
|
default_factory=dict,
|
sqlcg/server/server.py
CHANGED
|
@@ -27,6 +27,7 @@ _real_stdout_buffer = os.fdopen(os.dup(1), "wb", buffering=0)
|
|
|
27
27
|
from dotenv import load_dotenv # noqa: E402
|
|
28
28
|
from mcp.server import FastMCP # noqa: E402
|
|
29
29
|
|
|
30
|
+
from sqlcg import __version__ # noqa: E402
|
|
30
31
|
from sqlcg.utils.logging import getLogger # noqa: E402
|
|
31
32
|
|
|
32
33
|
logger = getLogger(__name__)
|
|
@@ -35,6 +36,13 @@ logger = getLogger(__name__)
|
|
|
35
36
|
# This is safe because _real_stdout_buffer has already captured fd 1 above.
|
|
36
37
|
mcp = FastMCP("SQL Code Graph")
|
|
37
38
|
|
|
39
|
+
# FastMCP.__init__ does not accept a `version` kwarg (verified signature); the
|
|
40
|
+
# underlying mcp.server.lowlevel.Server.__init__(self, name, version=None, ...)
|
|
41
|
+
# does. Set it post-construction so the `initialize` handshake reports
|
|
42
|
+
# serverInfo.version == sqlcg.__version__ instead of None. Deliberate, pinned
|
|
43
|
+
# private-attr write — see tests/unit/test_version_parity.py.
|
|
44
|
+
mcp._mcp_server.version = __version__
|
|
45
|
+
|
|
38
46
|
|
|
39
47
|
def _configure_mcp_logging() -> None:
|
|
40
48
|
"""Redirect sys.stdout to sys.stderr and configure logging to stderr.
|
|
@@ -219,6 +227,7 @@ async def _control_socket_task(
|
|
|
219
227
|
|
|
220
228
|
resp: dict = {
|
|
221
229
|
"running": True,
|
|
230
|
+
"version": __version__,
|
|
222
231
|
"pid": os.getpid(),
|
|
223
232
|
"db_path": str(db_path or _get_db_path()),
|
|
224
233
|
"indexed_sha": indexed_sha,
|
sqlcg/server/tools.py
CHANGED
|
@@ -1672,13 +1672,22 @@ def db_info() -> DbInfoResult:
|
|
|
1672
1672
|
those files. Table-level lineage is still available.
|
|
1673
1673
|
- `warnings` list — empty means the graph is healthy.
|
|
1674
1674
|
|
|
1675
|
+
`sqlcg_version` is the installed *package* build (sqlcg.__version__) — use
|
|
1676
|
+
it to self-check you are talking to the build you expect (the MCP protocol
|
|
1677
|
+
`serverInfo.version` is not reliably visible to the agent persona). This is
|
|
1678
|
+
a different axis from `schema_version`, which tracks the graph schema, not
|
|
1679
|
+
the package: do not conflate the two.
|
|
1680
|
+
|
|
1675
1681
|
Parse quality legend (parsing_mode per SqlQuery node):
|
|
1676
1682
|
sqlglot — standard path; column lineage available if extracted
|
|
1677
1683
|
scripting_block — tokenizer fallback; column lineage unavailable
|
|
1678
1684
|
|
|
1679
1685
|
Returns:
|
|
1680
|
-
DbInfoResult with schema
|
|
1686
|
+
DbInfoResult with sqlcg/schema versions, node counts, parse quality,
|
|
1687
|
+
and warnings
|
|
1681
1688
|
"""
|
|
1689
|
+
from sqlcg import __version__
|
|
1690
|
+
|
|
1682
1691
|
db = _get_backend()
|
|
1683
1692
|
|
|
1684
1693
|
schema_version = db.get_schema_version() or "unknown"
|
|
@@ -1745,6 +1754,7 @@ def db_info() -> DbInfoResult:
|
|
|
1745
1754
|
pass
|
|
1746
1755
|
|
|
1747
1756
|
return DbInfoResult(
|
|
1757
|
+
sqlcg_version=__version__,
|
|
1748
1758
|
schema_version=schema_version,
|
|
1749
1759
|
node_counts=node_counts,
|
|
1750
1760
|
column_lineage_edges=column_lineage_edges,
|
|
File without changes
|
|
File without changes
|