seam-code 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- seam/__init__.py +3 -0
- seam/_data/schema.sql +225 -0
- seam/_web/assets/index-BL_tqprR.js +216 -0
- seam/_web/assets/index-GTKUhVyD.css +1 -0
- seam/_web/index.html +13 -0
- seam/analysis/__init__.py +14 -0
- seam/analysis/affected.py +254 -0
- seam/analysis/builtins.py +966 -0
- seam/analysis/byte_budget.py +217 -0
- seam/analysis/changes.py +709 -0
- seam/analysis/cluster_naming.py +260 -0
- seam/analysis/clustering.py +216 -0
- seam/analysis/confidence.py +699 -0
- seam/analysis/embeddings.py +195 -0
- seam/analysis/flows.py +708 -0
- seam/analysis/impact.py +444 -0
- seam/analysis/imports.py +994 -0
- seam/analysis/imports_ext.py +780 -0
- seam/analysis/imports_resolve.py +176 -0
- seam/analysis/processes.py +453 -0
- seam/analysis/relevance.py +155 -0
- seam/analysis/rwr.py +129 -0
- seam/analysis/staleness.py +328 -0
- seam/analysis/steer.py +282 -0
- seam/analysis/synthesis.py +253 -0
- seam/analysis/synthesis_channels.py +433 -0
- seam/analysis/testpaths.py +103 -0
- seam/analysis/traversal.py +470 -0
- seam/cli/__init__.py +0 -0
- seam/cli/install.py +232 -0
- seam/cli/main.py +2602 -0
- seam/cli/output.py +137 -0
- seam/cli/read.py +244 -0
- seam/cli/serve.py +145 -0
- seam/config.py +551 -0
- seam/indexer/__init__.py +0 -0
- seam/indexer/cluster_index.py +425 -0
- seam/indexer/db.py +496 -0
- seam/indexer/embedding_index.py +183 -0
- seam/indexer/field_access.py +536 -0
- seam/indexer/field_access_c_cpp.py +643 -0
- seam/indexer/field_access_ext.py +708 -0
- seam/indexer/field_access_ext2.py +408 -0
- seam/indexer/field_access_go_rust.py +737 -0
- seam/indexer/field_access_php_swift.py +888 -0
- seam/indexer/field_access_ts.py +626 -0
- seam/indexer/graph.py +321 -0
- seam/indexer/graph_c.py +562 -0
- seam/indexer/graph_c_cpp.py +39 -0
- seam/indexer/graph_common.py +644 -0
- seam/indexer/graph_cpp.py +615 -0
- seam/indexer/graph_csharp.py +651 -0
- seam/indexer/graph_go.py +723 -0
- seam/indexer/graph_go_rust.py +39 -0
- seam/indexer/graph_java.py +689 -0
- seam/indexer/graph_java_csharp.py +38 -0
- seam/indexer/graph_php.py +914 -0
- seam/indexer/graph_python.py +628 -0
- seam/indexer/graph_ruby.py +748 -0
- seam/indexer/graph_rust.py +653 -0
- seam/indexer/graph_scope_infer.py +902 -0
- seam/indexer/graph_scope_infer_ext.py +723 -0
- seam/indexer/graph_scope_infer_ext2.py +992 -0
- seam/indexer/graph_swift.py +1014 -0
- seam/indexer/graph_swift_infer.py +515 -0
- seam/indexer/graph_typescript.py +663 -0
- seam/indexer/migrations.py +816 -0
- seam/indexer/parser.py +204 -0
- seam/indexer/pipeline.py +197 -0
- seam/indexer/signatures.py +634 -0
- seam/indexer/signatures_ext.py +780 -0
- seam/indexer/sync.py +287 -0
- seam/indexer/synthesis_index.py +291 -0
- seam/indexer/tokenize.py +79 -0
- seam/installer/__init__.py +67 -0
- seam/installer/claude.py +97 -0
- seam/installer/codex.py +94 -0
- seam/installer/core.py +127 -0
- seam/installer/cursor.py +61 -0
- seam/installer/guide.py +110 -0
- seam/installer/jsonfile.py +85 -0
- seam/installer/markdownfile.py +146 -0
- seam/installer/tomlfile.py +72 -0
- seam/query/__init__.py +0 -0
- seam/query/clusters.py +206 -0
- seam/query/comments.py +217 -0
- seam/query/context.py +293 -0
- seam/query/engine.py +940 -0
- seam/query/fts.py +328 -0
- seam/query/names.py +470 -0
- seam/query/pack.py +433 -0
- seam/query/semantic.py +339 -0
- seam/query/structure.py +727 -0
- seam/server/__init__.py +0 -0
- seam/server/graph_api.py +437 -0
- seam/server/handler_common.py +323 -0
- seam/server/impact_handler.py +615 -0
- seam/server/mcp.py +556 -0
- seam/server/tools.py +697 -0
- seam/server/trace_handler.py +184 -0
- seam/server/web.py +922 -0
- seam/watcher/__init__.py +0 -0
- seam/watcher/__main__.py +56 -0
- seam/watcher/daemon.py +237 -0
- seam_code-0.3.0.dist-info/METADATA +318 -0
- seam_code-0.3.0.dist-info/RECORD +109 -0
- seam_code-0.3.0.dist-info/WHEEL +4 -0
- seam_code-0.3.0.dist-info/entry_points.txt +2 -0
- seam_code-0.3.0.dist-info/licenses/LICENSE +21 -0
seam/server/mcp.py
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
"""MCP server setup — FastMCP stdio transport, twelve tools registered.
|
|
2
|
+
|
|
3
|
+
Creates and configures the MCP server instance.
|
|
4
|
+
Tool handlers in tools.py are thin adapters; this module wires them to FastMCP.
|
|
5
|
+
|
|
6
|
+
Usage (from cli/main.py):
|
|
7
|
+
server = create_server(conn, root)
|
|
8
|
+
server.run(transport="stdio")
|
|
9
|
+
|
|
10
|
+
Tools registered (Phase 0 + Phase 1 + Phase 1b + Phase 2 + Phase 3 + Phase 6 + Tier D11):
|
|
11
|
+
seam_query — FTS5 + 1-hop graph expansion search
|
|
12
|
+
seam_context — 360-degree symbol view (callers, callees, location, cluster)
|
|
13
|
+
seam_search — full-text search (FTS5 BM25)
|
|
14
|
+
seam_impact — blast-radius analysis by risk tier (Phase 1)
|
|
15
|
+
seam_trace — shortest call/dependency path between two symbols (Phase 1)
|
|
16
|
+
seam_changes — git diff → changed symbols → risk level (Phase 1)
|
|
17
|
+
seam_why — semantic comments (WHY/HACK/NOTE/TODO/FIXME) near a location (Phase 1b)
|
|
18
|
+
seam_clusters — list clusters or members of a cluster (Phase 2)
|
|
19
|
+
seam_affected — changed files → impacted test files via reverse-dependency BFS (Phase 3)
|
|
20
|
+
seam_context_pack — enriched context bundle: target + neighbors + WHY + peers (Phase 6)
|
|
21
|
+
seam_flows — execution flows: entry points + forward call-chain expansion
|
|
22
|
+
seam_structure — whole-repo directory/file/container structure tree (Tier D11)
|
|
23
|
+
|
|
24
|
+
Design:
|
|
25
|
+
- One FastMCP instance per process; connection is injected at creation time.
|
|
26
|
+
- Tools are closures capturing conn + root so FastMCP's decorator pattern
|
|
27
|
+
(which does not pass state through the call signature) stays clean.
|
|
28
|
+
- Return types are Any to avoid FastMCP structured-output mode, which wraps
|
|
29
|
+
results in a Pydantic model we don't need.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import sqlite3
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
from typing import Any
|
|
35
|
+
|
|
36
|
+
from mcp.server.fastmcp import FastMCP
|
|
37
|
+
from mcp.server.fastmcp.exceptions import ToolError
|
|
38
|
+
|
|
39
|
+
import seam.config as config
|
|
40
|
+
from seam.analysis.changes import DEFAULT_BASE_REF
|
|
41
|
+
from seam.server.tools import (
|
|
42
|
+
handle_seam_affected,
|
|
43
|
+
handle_seam_changes,
|
|
44
|
+
handle_seam_clusters,
|
|
45
|
+
handle_seam_context,
|
|
46
|
+
handle_seam_context_pack,
|
|
47
|
+
handle_seam_flows,
|
|
48
|
+
handle_seam_impact,
|
|
49
|
+
handle_seam_query,
|
|
50
|
+
handle_seam_search,
|
|
51
|
+
handle_seam_structure,
|
|
52
|
+
handle_seam_trace,
|
|
53
|
+
handle_seam_why,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Limit defaults/bounds (mirrors tools.py constants — kept local to avoid circular import)
|
|
57
|
+
_QUERY_LIMIT_DEFAULT = 10
|
|
58
|
+
_SEARCH_LIMIT_DEFAULT = 20
|
|
59
|
+
_IMPACT_DEPTH_DEFAULT = 3
|
|
60
|
+
_IMPACT_DIRECTION_DEFAULT = "upstream"
|
|
61
|
+
_TRACE_DEPTH_DEFAULT = 10
|
|
62
|
+
_CHANGES_SCOPE_DEFAULT = "working"
|
|
63
|
+
# Import DEFAULT_BASE_REF from analysis.changes instead of redefining it
|
|
64
|
+
# to avoid drift when the canonical default changes.
|
|
65
|
+
_CHANGES_BASE_REF_DEFAULT = DEFAULT_BASE_REF
|
|
66
|
+
|
|
67
|
+
_AFFECTED_DEPTH_DEFAULT = config.SEAM_AFFECTED_DEPTH
|
|
68
|
+
_IMPACT_LIMIT_DEFAULT = config.SEAM_IMPACT_MAX_RESULTS
|
|
69
|
+
_IMPACT_MAX_BYTES_DEFAULT = config.SEAM_IMPACT_MAX_BYTES
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _finalize(result: Any) -> Any:
|
|
73
|
+
"""Normalize a handler result to the MCP transport's native failure contract.
|
|
74
|
+
|
|
75
|
+
WHY: FastMCP only sets isError=True when a tool *raises* — returning an error
|
|
76
|
+
dict leaves isError=False, so a protocol-compliant agent (which checks isError)
|
|
77
|
+
reads a rejection as success. The handlers return {"error": CODE, "message": ...}
|
|
78
|
+
sentinels (kept for the CLI's {ok:false,error:{code,message}} envelope), so here —
|
|
79
|
+
at the MCP boundary only — we raise on that sentinel and let FastMCP flip isError.
|
|
80
|
+
A None ("handler found nothing") becomes a structured {"found": false} so agents
|
|
81
|
+
never receive empty content for a valid no-result answer. Every other value
|
|
82
|
+
(success dict, list) passes through byte-identical.
|
|
83
|
+
|
|
84
|
+
The CLI and MCP thus expose the SAME code+message via each transport's native
|
|
85
|
+
error signal — not byte-identical JSON (clean JSON cannot survive FastMCP's
|
|
86
|
+
"Error executing tool <name>: " content prefix on a raise).
|
|
87
|
+
"""
|
|
88
|
+
if result is None:
|
|
89
|
+
return {"found": False}
|
|
90
|
+
if isinstance(result, dict) and "error" in result and "message" in result:
|
|
91
|
+
raise ToolError(f"{result['error']}: {result['message']}")
|
|
92
|
+
return result
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def create_server(conn: sqlite3.Connection, root: Path) -> FastMCP:
|
|
96
|
+
"""Configure and return a FastMCP server with all twelve Seam tools registered.
|
|
97
|
+
|
|
98
|
+
Phase 0: seam_query, seam_context, seam_search
|
|
99
|
+
Phase 1: seam_impact, seam_trace, seam_changes
|
|
100
|
+
Phase 1b: seam_why
|
|
101
|
+
Phase 2: seam_clusters
|
|
102
|
+
Phase 3: seam_affected
|
|
103
|
+
Phase 6: seam_context_pack
|
|
104
|
+
Flows: seam_flows
|
|
105
|
+
Tier D11: seam_structure
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
conn: Open SQLite connection to the Seam index DB.
|
|
109
|
+
root: Project root Path — used to relativize file paths in results
|
|
110
|
+
and as the git repo root for seam_changes.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
A FastMCP instance ready for server.run(transport="stdio").
|
|
114
|
+
"""
|
|
115
|
+
mcp: FastMCP = FastMCP(name="seam")
|
|
116
|
+
|
|
117
|
+
@mcp.tool()
|
|
118
|
+
def seam_query(concept: str, limit: int = _QUERY_LIMIT_DEFAULT) -> Any:
|
|
119
|
+
"""Find all code related to a concept using hybrid search (FTS5 + 1-hop graph expansion).
|
|
120
|
+
|
|
121
|
+
Use this when you need to find where a concept lives across the codebase.
|
|
122
|
+
|
|
123
|
+
No `verbose` flag: query results carry no Phase 4/5 enrichment fields, so lean
|
|
124
|
+
mode would be a no-op — query is enrichment-free, like seam_search.
|
|
125
|
+
"""
|
|
126
|
+
return _finalize(handle_seam_query(conn, concept, root, limit=limit))
|
|
127
|
+
|
|
128
|
+
@mcp.tool()
|
|
129
|
+
def seam_context(symbol: str = "", verbose: bool = True, uid: str | None = None) -> Any:
|
|
130
|
+
"""Get a 360-degree view of a symbol: its callers, callees, file location, and docstring.
|
|
131
|
+
|
|
132
|
+
Use before touching any existing function or class.
|
|
133
|
+
|
|
134
|
+
Pass uid (a stable handle from a seam_search/seam_query result) instead of
|
|
135
|
+
symbol to pin the EXACT (file, line) symbol — bypassing homonym ambiguity and
|
|
136
|
+
saving a disambiguation round-trip. When uid is given, symbol is ignored.
|
|
137
|
+
|
|
138
|
+
Set verbose=false to omit heavy enrichment fields (decorators, is_exported,
|
|
139
|
+
visibility, qualified_name) and receive a compact response. signature and all
|
|
140
|
+
core identity fields are always kept.
|
|
141
|
+
verbose=true (default) is byte-identical to the pre-Phase-8 output.
|
|
142
|
+
|
|
143
|
+
Returns {found: false} when the symbol/uid is not in the index.
|
|
144
|
+
"""
|
|
145
|
+
return _finalize(handle_seam_context(conn, symbol, root, verbose=verbose, uid=uid))
|
|
146
|
+
|
|
147
|
+
@mcp.tool()
|
|
148
|
+
def seam_search(text: str, limit: int = _SEARCH_LIMIT_DEFAULT) -> Any:
|
|
149
|
+
"""Full-text search across all indexed symbol names and docstrings (FTS5 BM25).
|
|
150
|
+
|
|
151
|
+
Use when you know a keyword but not the exact symbol name.
|
|
152
|
+
Supports FTS5 operators: AND, OR, NOT, phrase search in quotes.
|
|
153
|
+
"""
|
|
154
|
+
return _finalize(handle_seam_search(conn, text, root, limit=limit))
|
|
155
|
+
|
|
156
|
+
@mcp.tool()
|
|
157
|
+
def seam_impact(
|
|
158
|
+
target: str = "",
|
|
159
|
+
direction: str = _IMPACT_DIRECTION_DEFAULT,
|
|
160
|
+
max_depth: int = _IMPACT_DEPTH_DEFAULT,
|
|
161
|
+
include_tests: bool = False,
|
|
162
|
+
verbose: bool = True,
|
|
163
|
+
limit: int = _IMPACT_LIMIT_DEFAULT,
|
|
164
|
+
max_bytes: int = _IMPACT_MAX_BYTES_DEFAULT,
|
|
165
|
+
uid: str | None = None,
|
|
166
|
+
) -> Any:
|
|
167
|
+
"""Blast-radius analysis — what breaks if I change this symbol?
|
|
168
|
+
|
|
169
|
+
Returns all symbols that depend on the target (upstream), that the target
|
|
170
|
+
depends on (downstream), or both — grouped into risk tiers by distance:
|
|
171
|
+
WILL_BREAK (distance 1) — direct dependents, definitely affected.
|
|
172
|
+
LIKELY_AFFECTED (distance 2) — indirect dependents, probably affected.
|
|
173
|
+
MAY_NEED_TESTING (distance 3+) — transitive dependents, test to be sure.
|
|
174
|
+
|
|
175
|
+
Each entry carries the aggregated path confidence (EXTRACTED | INFERRED | AMBIGUOUS)
|
|
176
|
+
so you know which conclusions to lean on and which to verify by reading.
|
|
177
|
+
Each entry also carries is_test (bool) so you can distinguish production dependents
|
|
178
|
+
from test-only callers.
|
|
179
|
+
|
|
180
|
+
E4 — Edge provenance (SEAM_EDGE_PROVENANCE=on, default):
|
|
181
|
+
kind — the edge kind via which this dependent was reached. Full
|
|
182
|
+
vocabulary: call | import | extends | implements | instantiates
|
|
183
|
+
| holds | reads | writes | uses. Lets you distinguish a hard
|
|
184
|
+
call-edge dependent from a data-coupling (reads/holds) or
|
|
185
|
+
signature-coupling (uses) dependent. Always present in both
|
|
186
|
+
verbose and lean modes (core field, never stripped).
|
|
187
|
+
synthesized_by — synthesis channel name when the edge is heuristic (e.g.
|
|
188
|
+
"interface-override", "closure-collection", "event-emitter"),
|
|
189
|
+
null when statically extracted. Lets you weight entries that
|
|
190
|
+
rest on over-approximated synthesized edges differently.
|
|
191
|
+
Null is RETAINED (null = "static edge", the informative common
|
|
192
|
+
case — unlike best_candidate which is E1-omitted when null).
|
|
193
|
+
Stripped in lean mode (verbose=false), like resolved_by.
|
|
194
|
+
AMBIGUITY (important): null does NOT, on its own, prove this
|
|
195
|
+
edge is static — an index that was never synthesis-rebuilt
|
|
196
|
+
(pre-v12, or SEAM_EDGE_SYNTHESIS=off at index time) carries
|
|
197
|
+
null for EVERY edge. So all-null across a result can mean
|
|
198
|
+
"no synthesized edges traversed" OR "synthesis never ran" — it
|
|
199
|
+
does not distinguish them; run `seam init` to populate. A
|
|
200
|
+
MISSING key (not null) means SEAM_EDGE_PROVENANCE=off or lean
|
|
201
|
+
mode stripped it — three distinct "no value" states.
|
|
202
|
+
Set SEAM_EDGE_PROVENANCE=off for byte-identical pre-E4 output.
|
|
203
|
+
|
|
204
|
+
E4 — Truncation steer (SEAM_IMPACT_STEER=on, default):
|
|
205
|
+
next_actions — top-level list[str] of ready-to-act prose hints, PRESENT only
|
|
206
|
+
when ≥1 entry was trimmed (by count cap or byte ceiling).
|
|
207
|
+
ABSENT when nothing was trimmed (so its presence is an
|
|
208
|
+
unambiguous "there is more" signal). Hints name the specific
|
|
209
|
+
direction+tier+count trimmed and the exact remedy (raise limit,
|
|
210
|
+
zero max_bytes, etc.) — so you know what to do, not just that
|
|
211
|
+
something was dropped. An all-trimmed response (empty entries
|
|
212
|
+
but non-empty risk_summary) includes a warning that the blast
|
|
213
|
+
radius was trimmed to nothing, NOT that no dependents exist.
|
|
214
|
+
Set SEAM_IMPACT_STEER=off to suppress next_actions entirely.
|
|
215
|
+
|
|
216
|
+
By default (include_tests=false) the result is the PRODUCTION blast radius:
|
|
217
|
+
test-file dependents are filtered out and their count is reported as hidden_tests.
|
|
218
|
+
This keeps "what breaks?" focused on production code (test callers otherwise
|
|
219
|
+
dominate the tiers and trip the per-tier cap). Set include_tests=true to include
|
|
220
|
+
test dependents too; or use seam_affected to get the impacted test files directly.
|
|
221
|
+
|
|
222
|
+
Set verbose=false to omit heavy enrichment fields (resolved_by, best_candidate,
|
|
223
|
+
synthesized_by) from every tier entry and get a compact response. kind is always
|
|
224
|
+
kept even in lean mode. verbose=true (default) returns all enrichment fields.
|
|
225
|
+
NOTE: risk_summary, truncated, and the per-tier cap apply regardless of verbose.
|
|
226
|
+
Use limit=0 for the full, uncapped transitive set.
|
|
227
|
+
|
|
228
|
+
limit controls the per-tier entry cap (default: SEAM_IMPACT_MAX_RESULTS=25).
|
|
229
|
+
Set limit=0 to disable the cap and receive all transitive entries.
|
|
230
|
+
The response always includes risk_summary: {direction: {tier: count}} computed
|
|
231
|
+
from the full pre-cap result, so the blast radius size is always visible.
|
|
232
|
+
When entries were truncated, truncated: {direction: {tier: omitted}} is included.
|
|
233
|
+
|
|
234
|
+
max_bytes controls the per-call character budget for the serialized output
|
|
235
|
+
(characters of compact JSON). Default: SEAM_IMPACT_MAX_BYTES (0 = unlimited).
|
|
236
|
+
When > 0, runs after the per-tier count cap and E2/E3 relevance ordering, trimming
|
|
237
|
+
entries from the least-valuable end (downstream before upstream, MAY_NEED_TESTING
|
|
238
|
+
before WILL_BREAK, tail before front) until the output fits. Byte-dropped counts
|
|
239
|
+
are merged into truncated additively. When the ceiling fires, byte_capped is added:
|
|
240
|
+
{"limit": <budget>, "omitted": <total entries dropped by the byte pass>}.
|
|
241
|
+
byte_capped is absent when max_bytes=0 or when everything fit (no trimming).
|
|
242
|
+
risk_summary remains the honest full pre-cap total regardless of byte trimming.
|
|
243
|
+
|
|
244
|
+
Pass uid (a stable handle from a seam_search/seam_query result) instead of
|
|
245
|
+
target to pin the exact symbol and skip homonym re-disambiguation.
|
|
246
|
+
|
|
247
|
+
Use before editing any symbol to understand the blast radius.
|
|
248
|
+
"""
|
|
249
|
+
return _finalize(
|
|
250
|
+
handle_seam_impact(
|
|
251
|
+
conn,
|
|
252
|
+
target,
|
|
253
|
+
root,
|
|
254
|
+
direction=direction,
|
|
255
|
+
max_depth=max_depth,
|
|
256
|
+
include_tests=include_tests,
|
|
257
|
+
verbose=verbose,
|
|
258
|
+
limit=limit,
|
|
259
|
+
max_bytes=max_bytes,
|
|
260
|
+
uid=uid,
|
|
261
|
+
)
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
@mcp.tool()
|
|
265
|
+
def seam_trace(
|
|
266
|
+
source: str = "",
|
|
267
|
+
target: str = "",
|
|
268
|
+
max_depth: int = _TRACE_DEPTH_DEFAULT,
|
|
269
|
+
verbose: bool = True,
|
|
270
|
+
uid: str | None = None,
|
|
271
|
+
target_uid: str | None = None,
|
|
272
|
+
) -> Any:
|
|
273
|
+
"""Trace the call/dependency path between two symbols.
|
|
274
|
+
|
|
275
|
+
Returns the shortest path from source to target as an ordered list of hops,
|
|
276
|
+
where each hop carries the edge kind and per-edge confidence
|
|
277
|
+
(EXTRACTED | INFERRED | AMBIGUOUS).
|
|
278
|
+
|
|
279
|
+
Full edge kind vocabulary (E4 corrected from stale 'call | import'):
|
|
280
|
+
call | import | extends | implements | instantiates | holds | reads | writes | uses
|
|
281
|
+
The hop kind reflects the actual relationship traversed — e.g. a 'holds' hop
|
|
282
|
+
means one class stores the other as a typed field, while a 'reads' hop means
|
|
283
|
+
a field-access read edge was traversed.
|
|
284
|
+
|
|
285
|
+
E4 — synthesized_by on each hop (SEAM_EDGE_PROVENANCE=on, default):
|
|
286
|
+
synthesized_by — synthesis channel name when the hop is a heuristic synthesized
|
|
287
|
+
edge (e.g. "interface-override"), null when statically extracted.
|
|
288
|
+
Lets you see which hops in a path rest on over-approximations.
|
|
289
|
+
In lean mode (verbose=false), synthesized_by is stripped
|
|
290
|
+
(like resolved_by) — kind is always kept.
|
|
291
|
+
AMBIGUITY (important): null on a hop does NOT prove the hop is
|
|
292
|
+
static — an index never synthesis-rebuilt (pre-v12, or
|
|
293
|
+
SEAM_EDGE_SYNTHESIS=off at index time) carries null for EVERY
|
|
294
|
+
hop. all-null can mean "no synthesized hops" OR "synthesis never
|
|
295
|
+
ran"; run `seam init` to populate. A MISSING key means lean mode
|
|
296
|
+
or SEAM_EDGE_PROVENANCE=off.
|
|
297
|
+
|
|
298
|
+
Also returns one-hop callers and callees for both symbols so you can see
|
|
299
|
+
the immediate neighborhood alongside the path.
|
|
300
|
+
|
|
301
|
+
Use this when you need to understand how control flows from one symbol to
|
|
302
|
+
another, or to answer "how does X reach Y?" without manual grep.
|
|
303
|
+
|
|
304
|
+
Returns found=false (paths=[]) when no path exists — this is a real,
|
|
305
|
+
distinguishable "not connected" answer, not an error.
|
|
306
|
+
|
|
307
|
+
Per-hop confidence lets you flag any hop that rests on an AMBIGUOUS edge
|
|
308
|
+
(name collision at extraction time) so you know which conclusions are certain
|
|
309
|
+
and which need manual verification.
|
|
310
|
+
|
|
311
|
+
Set verbose=false to omit heavy fields (resolved_by, best_candidate,
|
|
312
|
+
synthesized_by) from every hop and edge hop. kind is always kept.
|
|
313
|
+
verbose=true (default) is byte-identical to pre-Phase-8 (plus E4 fields).
|
|
314
|
+
|
|
315
|
+
Pass uid / target_uid (stable handles from seam_search/seam_query results)
|
|
316
|
+
instead of source / target to pin exact symbols and skip re-disambiguation.
|
|
317
|
+
"""
|
|
318
|
+
return _finalize(
|
|
319
|
+
handle_seam_trace(
|
|
320
|
+
conn,
|
|
321
|
+
source,
|
|
322
|
+
target,
|
|
323
|
+
root,
|
|
324
|
+
max_depth=max_depth,
|
|
325
|
+
verbose=verbose,
|
|
326
|
+
uid=uid,
|
|
327
|
+
target_uid=target_uid,
|
|
328
|
+
)
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
@mcp.tool()
|
|
332
|
+
def seam_changes(
|
|
333
|
+
scope: str = _CHANGES_SCOPE_DEFAULT,
|
|
334
|
+
base_ref: str = _CHANGES_BASE_REF_DEFAULT,
|
|
335
|
+
) -> Any:
|
|
336
|
+
"""Pre-commit risk check — map git diff to affected symbols and risk level.
|
|
337
|
+
|
|
338
|
+
Diffs the working tree / staged set / branch against a git ref, maps each
|
|
339
|
+
changed line range to the symbols it touched, runs impact analysis, and
|
|
340
|
+
returns an overall risk level:
|
|
341
|
+
low — no downstream dependents found
|
|
342
|
+
medium — transitive dependents (MAY_NEED_TESTING)
|
|
343
|
+
high — indirect dependents (LIKELY_AFFECTED)
|
|
344
|
+
critical — direct dependents (WILL_BREAK)
|
|
345
|
+
|
|
346
|
+
scope values:
|
|
347
|
+
working — git diff (unstaged working tree vs index)
|
|
348
|
+
staged — git diff --cached (staged changes only)
|
|
349
|
+
branch — git diff <base_ref>...HEAD (entire branch vs base ref)
|
|
350
|
+
|
|
351
|
+
Use before committing to understand what your changes break.
|
|
352
|
+
Fails (isError) with NOT_A_GIT_REPO when run outside a git repository.
|
|
353
|
+
"""
|
|
354
|
+
return _finalize(handle_seam_changes(conn, root, base_ref=base_ref, scope=scope))
|
|
355
|
+
|
|
356
|
+
@mcp.tool()
|
|
357
|
+
def seam_why(
|
|
358
|
+
file: str | None = None,
|
|
359
|
+
line: int | None = None,
|
|
360
|
+
symbol: str | None = None,
|
|
361
|
+
) -> Any:
|
|
362
|
+
"""Return semantic comments (WHY/HACK/NOTE/TODO/FIXME) near a location or symbol.
|
|
363
|
+
|
|
364
|
+
Lookup modes (at least one of file or symbol is required):
|
|
365
|
+
file only — all semantic comments in that file.
|
|
366
|
+
file + line — comments within ±15 lines of that line number.
|
|
367
|
+
symbol — comments inside the symbol's body and just above its definition.
|
|
368
|
+
|
|
369
|
+
Result fields per comment:
|
|
370
|
+
file — path relative to project root.
|
|
371
|
+
line — 1-based line number.
|
|
372
|
+
marker — WHY | HACK | NOTE | TODO | FIXME.
|
|
373
|
+
text — comment body after the marker, stripped.
|
|
374
|
+
|
|
375
|
+
Returns an empty list (not an error) when a file/symbol has no semantic comments.
|
|
376
|
+
Use before editing a function to read the documented intent and known caveats.
|
|
377
|
+
"""
|
|
378
|
+
return _finalize(handle_seam_why(conn, root, file=file, line=line, symbol=symbol))
|
|
379
|
+
|
|
380
|
+
@mcp.tool()
|
|
381
|
+
def seam_clusters(cluster_id: int | None = None) -> Any:
|
|
382
|
+
"""List all code clusters (functional areas) or the members of one cluster.
|
|
383
|
+
|
|
384
|
+
With no argument: returns [{id, label, size}] — an overview of all detected
|
|
385
|
+
functional areas in the codebase, sorted by id.
|
|
386
|
+
|
|
387
|
+
With cluster_id: returns [{name, file, line, kind}] — all symbols that belong
|
|
388
|
+
to that cluster, with file paths relative to the project root.
|
|
389
|
+
|
|
390
|
+
Clusters are computed during `seam init` using Louvain community detection over
|
|
391
|
+
the call/import graph. Each cluster represents a cohesive group of symbols that
|
|
392
|
+
frequently call or import each other.
|
|
393
|
+
|
|
394
|
+
Returns an empty list (not an error) when the index has no cluster data —
|
|
395
|
+
either the repo has no symbols, or `seam init` hasn't been run yet.
|
|
396
|
+
|
|
397
|
+
Use this to understand the codebase's functional areas before diving into a
|
|
398
|
+
specific subsystem. Then use seam_context to see a symbol's cluster peers.
|
|
399
|
+
"""
|
|
400
|
+
return _finalize(handle_seam_clusters(conn, root, cluster_id=cluster_id))
|
|
401
|
+
|
|
402
|
+
@mcp.tool()
|
|
403
|
+
def seam_affected(
|
|
404
|
+
changed_files: list[str],
|
|
405
|
+
depth: int = _AFFECTED_DEPTH_DEFAULT,
|
|
406
|
+
) -> Any:
|
|
407
|
+
"""Find which test files are impacted by a set of changed source files.
|
|
408
|
+
|
|
409
|
+
Given a list of changed file paths, traverses the reverse-dependency graph
|
|
410
|
+
(upstream impact) to find all test files that depend on symbols in those files.
|
|
411
|
+
|
|
412
|
+
Result shape:
|
|
413
|
+
changed_files — the input files (relativized to project root)
|
|
414
|
+
affected_tests — sorted unique test files that must be re-run
|
|
415
|
+
total_dependents_traversed — count of all dependency entries examined
|
|
416
|
+
|
|
417
|
+
Usage pattern (agent workflow):
|
|
418
|
+
1. Get changed files from git: `git diff --name-only`
|
|
419
|
+
2. Pass them to seam_affected to get the impacted test files
|
|
420
|
+
3. Run only those tests: `pytest <affected_tests>`
|
|
421
|
+
|
|
422
|
+
A changed file that is itself a test file is always included in affected_tests.
|
|
423
|
+
Files not in the index are silently skipped (they have no graph dependents).
|
|
424
|
+
Fails (isError) with INVALID_INPUT when changed_files is empty.
|
|
425
|
+
|
|
426
|
+
Use this to determine the minimal set of tests to run before committing.
|
|
427
|
+
"""
|
|
428
|
+
return _finalize(handle_seam_affected(conn, changed_files, root, depth=depth))
|
|
429
|
+
|
|
430
|
+
@mcp.tool()
|
|
431
|
+
def seam_context_pack(symbol: str, verbose: bool = True) -> Any:
|
|
432
|
+
"""Get a ready-to-paste context bundle for a symbol.
|
|
433
|
+
|
|
434
|
+
Returns a single payload containing:
|
|
435
|
+
target — the symbol's full 360-degree view (file, kind, docstring,
|
|
436
|
+
signature, decorators, is_exported, visibility, ambiguous flag,
|
|
437
|
+
cluster info, raw callers/callees name lists)
|
|
438
|
+
callers — 1-hop callers, each enriched with {name, file, line, kind,
|
|
439
|
+
signature} — not just names. Capped at SEAM_PACK_NEIGHBOR_LIMIT.
|
|
440
|
+
callees — 1-hop callees, similarly enriched and capped.
|
|
441
|
+
why — WHY/HACK/NOTE/TODO/FIXME comments attached to the symbol.
|
|
442
|
+
Capped at SEAM_PACK_MAX_COMMENTS.
|
|
443
|
+
cluster_peers — the symbol's functional-area peers (from seam_clusters).
|
|
444
|
+
truncated — {callers, callees, comments} counts of entries dropped by caps.
|
|
445
|
+
|
|
446
|
+
When a neighbor name has no indexed declaration (external/unindexed symbol),
|
|
447
|
+
it is silently skipped in callers/callees — not an error.
|
|
448
|
+
|
|
449
|
+
Use this before modifying a symbol to get everything you need in one call:
|
|
450
|
+
location, enriched neighbors, rationale comments, and functional area — without
|
|
451
|
+
the five separate seam_context / seam_why / seam_context-on-each-neighbor calls.
|
|
452
|
+
|
|
453
|
+
Set verbose=false to omit heavy enrichment fields (decorators, is_exported,
|
|
454
|
+
visibility, qualified_name) from target and every neighbor. signature and core
|
|
455
|
+
fields are always kept. verbose=true (default) is byte-identical to pre-Phase-8.
|
|
456
|
+
|
|
457
|
+
Returns {found: false} when the symbol is not in the index (same contract as
|
|
458
|
+
seam_context). Fails (isError) with INVALID_INPUT when symbol is blank/whitespace.
|
|
459
|
+
"""
|
|
460
|
+
return _finalize(handle_seam_context_pack(conn, symbol, root, verbose=verbose))
|
|
461
|
+
|
|
462
|
+
@mcp.tool()
|
|
463
|
+
def seam_flows(entry: str | None = None) -> Any:
|
|
464
|
+
"""Discover execution flows — how the program actually runs, end to end.
|
|
465
|
+
|
|
466
|
+
With no argument: returns {"entry_points": [{name, kind, file, reach}]} —
|
|
467
|
+
the codebase's top execution starting points (call-graph roots ranked by
|
|
468
|
+
how many symbols they reach downstream). On a real repo these are the CLI
|
|
469
|
+
commands, web routes, MCP handlers, and main() — derived structurally, no AI.
|
|
470
|
+
|
|
471
|
+
With an entry name: returns that entry point's flow tree — a depth/breadth-
|
|
472
|
+
capped, cycle-safe expansion of what it calls, transitively:
|
|
473
|
+
entry, kind, file
|
|
474
|
+
steps — nested [{name, kind, file, line, confidence, children,
|
|
475
|
+
truncated}] following the call chain forward
|
|
476
|
+
total_steps — number of symbols in the flow
|
|
477
|
+
truncated — True if depth/breadth caps cut part of the tree
|
|
478
|
+
|
|
479
|
+
Use this to answer "how does feature X work?" in ONE call instead of reading
|
|
480
|
+
the entry file and chasing every callee by hand. Start with no argument to
|
|
481
|
+
see the entry points, then drill into the one you care about.
|
|
482
|
+
|
|
483
|
+
Each symbol appears once (first reach wins — cycle-safe). Step confidence
|
|
484
|
+
uses the fast name-count resolver; use seam_impact/seam_trace for import-
|
|
485
|
+
promoted confidence. Returns {found: false} when the entry name is unknown.
|
|
486
|
+
"""
|
|
487
|
+
return _finalize(handle_seam_flows(conn, root, entry=entry))
|
|
488
|
+
|
|
489
|
+
@mcp.tool()
|
|
490
|
+
def seam_structure(
|
|
491
|
+
path: str | None = None,
|
|
492
|
+
depth: int | None = None,
|
|
493
|
+
nodes: int | None = None,
|
|
494
|
+
symbols: bool = False,
|
|
495
|
+
) -> Any:
|
|
496
|
+
"""Get the whole-repository directory/file/container structure tree.
|
|
497
|
+
|
|
498
|
+
By default this is a MODULE/AREA OVERVIEW — a nested tree of:
|
|
499
|
+
dir nodes — directories in the repository hierarchy
|
|
500
|
+
file nodes — indexed source files under each directory
|
|
501
|
+
container nodes — class/interface/type symbols within each file
|
|
502
|
+
|
|
503
|
+
Standalone module-level functions and method/member symbols are rolled up into
|
|
504
|
+
counts rather than listed as nodes — this keeps the tree a compact "what are the
|
|
505
|
+
main modules?" skeleton instead of an exhaustive symbol dump. Pass symbols=True to
|
|
506
|
+
also list standalone functions under each file (for a detailed structural view).
|
|
507
|
+
|
|
508
|
+
Each node carries:
|
|
509
|
+
kind: 'dir' | 'file' | 'container' | 'function'
|
|
510
|
+
name: display name (dir basename, file name, symbol name)
|
|
511
|
+
path: repo-root-relative path; null for container and function nodes
|
|
512
|
+
symbol_count: total symbol rows in this subtree
|
|
513
|
+
area: functional-area label from cluster data (null if no clustering)
|
|
514
|
+
children: child nodes
|
|
515
|
+
members: count of method/member rows rolled into this container (0 for non-containers)
|
|
516
|
+
truncated: count of nodes omitted by depth/node caps (0 = nothing trimmed)
|
|
517
|
+
|
|
518
|
+
Optional scoping and bounds (Slice 3):
|
|
519
|
+
path: Scope the tree to a subdirectory. A relative path resolves against the
|
|
520
|
+
repo root (NOT the server cwd); an absolute path is honoured as-is.
|
|
521
|
+
An unknown or out-of-tree path degrades to {found: false} — never an error.
|
|
522
|
+
depth: Maximum nesting depth (root=0). Nodes beyond this depth are dropped
|
|
523
|
+
and counted in `truncated`. Defaults to SEAM_STRUCTURE_MAX_DEPTH (8).
|
|
524
|
+
nodes: Maximum total non-root nodes. Excess nodes are dropped BFS-order (closest
|
|
525
|
+
to root survive) and counted in `truncated`. 0 = unlimited.
|
|
526
|
+
Defaults to SEAM_STRUCTURE_MAX_NODES (2000).
|
|
527
|
+
symbols: When true, also list standalone module-level functions as nodes under
|
|
528
|
+
each file. Default false = compact module/area overview (dirs, files,
|
|
529
|
+
classes only). Turn on for a detailed per-symbol structural view.
|
|
530
|
+
|
|
531
|
+
Use this to get a structural overview before diving into a specific file or
|
|
532
|
+
symbol, or to understand how files and containers are organized across the repo.
|
|
533
|
+
|
|
534
|
+
Returns {found: false} when the (scoped) tree has no symbols — an empty/not-yet
|
|
535
|
+
indexed repo, or a scope path that matches no indexed files.
|
|
536
|
+
"""
|
|
537
|
+
# Pass the scope path string straight through: handle_seam_structure /
|
|
538
|
+
# build_structure resolve a relative path against `root`, not the server cwd.
|
|
539
|
+
scope_path = Path(path) if path else None
|
|
540
|
+
result = handle_seam_structure(
|
|
541
|
+
conn,
|
|
542
|
+
root,
|
|
543
|
+
path=scope_path,
|
|
544
|
+
max_depth=depth,
|
|
545
|
+
max_nodes=nodes,
|
|
546
|
+
include_functions=symbols,
|
|
547
|
+
)
|
|
548
|
+
# Normalize a genuinely-empty (scoped) tree to the not-found sentinel so
|
|
549
|
+
# _finalize emits {found: false} — matching every sibling read tool and the
|
|
550
|
+
# docstring contract. The CLI keeps rendering the (possibly empty) tree itself.
|
|
551
|
+
tree = result["tree"]
|
|
552
|
+
if not tree["children"] and tree["symbol_count"] == 0:
|
|
553
|
+
return _finalize(None)
|
|
554
|
+
return _finalize(result)
|
|
555
|
+
|
|
556
|
+
return mcp
|