Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,17 +240,21 @@ Every other text field -- `class_names`, `method_names`, `base_types`, `field_ty
| `imports` | -- | `using` / `import` / `include` directives | all except SQL |
| `params` | METHOD | Parameter list for METHOD | C#, Python, JS, Rust, C++ |
| `declarations` | NAME | The declaration(s) of NAME (narrow with `symbol_kind`) | all |
| `body` | NAME | Full source of NAME's declaration | C# only |
| `body` | NAME | Full source of NAME's declaration(s). Works in both `query_codebase` (returns bodies across every matching file) and `query_single_file` (one file only). Narrow with `symbol_kind`. | C# only |
| `at` | LINE:COL | Deepest AST node at position + enclosing scope chain | C# only |
| `calls` | METHOD | Call sites of METHOD. Qualify with `Type.Method` to restrict by receiver -- the qualifier matches both the literal receiver text (`Foo.Save()`) **and** any receiver whose declared/inferred type resolves to that name via the method-scoped var-type map (`store.Save()` where `store: IRepository` matches `IRepository.Save`). When the receiver's type is conflicted in its scope, the qualified match is skipped -- the bare name still finds the call. Pass a METHOD name only -- a variable/receiver name silently returns empty; use `all_refs` on the variable for that. | all |
| `caller_of` | METHOD | Like `calls`, but groups call sites by the **enclosing caller** -- one row per `(TypeName.MemberName)` caller with a count of how many sites it contains. Collapses noisy `calls METHOD` output into a unique-caller view. Useful for "who depends on this". | C# only |
| `callee_of` | METHOD | The inverse -- walk the body of the method named METHOD and emit one row per distinct callee with an invocation count. Constructor calls (`new T()`) are reported as `T (N invocations, ctor)`. Useful for "what does this method depend on" / "what could be slow here". | C# only |
| `implements` | TYPE | Types that inherit/implement TYPE | all except SQL |
| `uses` | TYPE | Type references; narrow with `uses_kind` (`field`/`param`/`return`/`cast`/`base`/`locals`) | C# only |
| `casts` | TYPE | `(TYPE)expr` / `as TYPE` sites | C# only |
| `attrs` | NAME? | `[Attribute]` / `@decorator` / `#[attribute]` usages (omit NAME to list all) | C#, Python, JS |
| `accesses_of` | MEMBER | Access sites of property/field by name (`"Order.Status"` restricts) | C# only |
| `accesses_on` | TYPE | `.Member` accesses on locals/params/fields typed as TYPE (plus `new T { ... }` and `with` mutations). Returns nothing when the variable is only assigned, returned, or forwarded as an argument -- no `.Member` exists. Fall back to `all_refs` on the variable name. | C# only |
| `all_refs` | NAME | Every identifier occurrence (broadest -- AST-only, skips strings/comments). For SQL this is a plain substring scan over lines. | all |
| `var_type` | NAME | For each occurrence of NAME, report the resolved type from the method-scoped var-type map, or `(unresolved)` / `(conflicting)` when the resolver can't pin it down. Saves an `at LINE:COL` round-trip when you just want the type. | C# only |
| `var_type` | NAME | For each occurrence of NAME, report the resolved type from the method-scoped var-type map, or `(unresolved)` / `(conflicting)` when the resolver can't pin it down. Works in both `query_codebase` (every file that mentions NAME) and `query_single_file`. Saves an `at LINE:COL` round-trip when you just want the type. | C# only |

**Enclosing-scope filter (pattern modes).** `calls`, `uses`, `casts`, `accesses_of`, `accesses_on`, `all_refs` accept `enclosing_method="WriteBack"` and/or `enclosing_class="OrderProcessor"`. The two compose as a logical AND. Useful for pinpointing call sites in a specific member, e.g. `calls("Save", enclosing_method="WriteBack")` returns only the `Save()` calls that happen inside `WriteBack` methods. (C# only.)

**Visibility filter (declaration modes).** `declarations`, `classes`, `methods`, `fields` accept `visibility="public,internal,protected,private"` (comma-separated, any subset). The filter is applied AST-side per declaration using the same defaults the indexer uses: top-level types default to `internal`, nested types to `private`, interface members to `public`, enum members to `public`, class/struct/record members to `private`. Compound modifiers collapse to their dominant role (`protected internal` -> `protected`, `private protected` -> `private`). Languages other than C# currently don't capture visibility -- passing the filter against them returns nothing rather than over-matching.

Expand Down
73 changes: 66 additions & 7 deletions mcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,9 @@ def query_codebase(
symbol_kind: str = "",
uses_kind: str = "",
visibility: str = "",
head_lines: int = 0,
enclosing_method: str = "",
enclosing_class: str = "",
exclude_path: str = "",
) -> str:
"""Index pre-filter + tree-sitter AST. Returns one of three response shapes
Expand Down Expand Up @@ -273,6 +276,19 @@ def query_codebase(
nothing when this filter is set. (C# captures explicit
modifiers plus interface-public / enum-public / nested
type defaults.)
head_lines: For `body` and `declarations include_body=True` -- truncate
each emitted body to the first N source lines (signature +
body together), with a `... +K more lines` tail marker.
Default 0 = no truncation. Useful when scanning many bodies
at once.
enclosing_method:
For pattern modes (calls / uses / casts / accesses_of /
accesses_on / all_refs) -- only keep hits inside a member
with this exact name. Combine with `enclosing_class=` to
pinpoint a specific call-site context. (C# only.)
enclosing_class:
Same as above, narrowed to a type by name. Composes with
`enclosing_method=` as a logical AND. (C# only.)
exclude_path: Comma-separated list of folder paths to exclude from results.
Each value is matched as an exact ancestor folder, not a glob --
wildcards are not supported. Behavior:
Expand All @@ -290,21 +306,25 @@ def query_codebase(
query_codebase("uses", "IDataStore", uses_kind="param", sub="services")
query_codebase("implements", "IRepository")
query_codebase("declarations", "SaveChanges", symbol_kind="method")
query_codebase("body", "SaveChanges", symbol_kind="method") # full source of every match
query_codebase("var_type", "store", sub="services") # resolved type of every ``store`` occurrence
query_codebase("calls", "SaveChanges", sub="services,vendor")
query_codebase("calls", "SaveChanges", exclude_path="tests,generated")
query_codebase("uses", "IRepo", sub="services", exclude_path="services/legacy")"""
# File-targeted modes don't make sense for a codebase-wide search.
# `body`/`at`/`params`/`var_type` need an explicit file; listing modes
# `at`/`params` need an explicit file; listing modes
# (methods/fields/classes/imports/capabilities) only describe a single
# file's structure. Catch them here so the agent sees an actionable
# redirect instead of the daemon's generic "unknown mode" error.
# ``body`` and ``var_type`` work codebase-wide now (index pre-filter
# + per-file AST), so they're no longer in this set.
_FILE_ONLY = {
"methods", "fields", "classes", "imports", "capabilities",
"body", "at", "params", "var_type",
"at", "params",
}
m = mode.lower().strip().replace("-", "_")
if m in _FILE_ONLY:
if m in ("body", "at", "params", "var_type"):
if m in ("at", "params"):
why = "needs a specific file"
example_arg = pattern or ('"SaveChanges"' if m != "at" else '"42:10"')
example = f' query_single_file("{m}", {example_arg}, file="path/to/File.cs")'
Expand All @@ -321,6 +341,9 @@ def query_codebase(
"include_body": include_body,
"symbol_kind": symbol_kind or "", "uses_kind": uses_kind or "",
"visibility": visibility or "",
"head_lines": int(head_lines) if head_lines else 0,
"enclosing_method": enclosing_method or "",
"enclosing_class": enclosing_class or "",
"exclude_path": exclude_path or "",
})
except Exception as e:
Expand Down Expand Up @@ -429,6 +452,12 @@ def _qsf_call(file_rel: str) -> str:
args.append(f'uses_kind="{uses_kind}"')
if visibility:
args.append(f'visibility="{visibility}"')
if head_lines:
args.append(f'head_lines={int(head_lines)}')
if enclosing_method:
args.append(f'enclosing_method="{enclosing_method}"')
if enclosing_class:
args.append(f'enclosing_class="{enclosing_class}"')
return "query_single_file(" + ", ".join(args) + ")"

# Tier 2 -- many files: filenames + counts only.
Expand Down Expand Up @@ -459,11 +488,19 @@ def _qsf_call(file_rel: str) -> str:

output = "\n".join(out_lines)
if truncated_files:
notes = [f"\n\n{len(truncated_files)} file(s) had more than "
f"{_PER_FILE_DETAIL_LINES} hits -- showing first "
f"{_PER_FILE_DETAIL_LINES} of each. To see all hits in a file:"]
# Compact form: one short line per capped file showing how many
# extra hits are available. The agent already knows the mode and
# pattern; spelling out the full ``query_single_file(...)`` call
# for every capped file would burn ~100 chars per file in pure
# boilerplate. ``[+K capped] path`` is enough -- the agent can
# reissue a query_single_file call when it wants the rest.
notes = [f"\n\n{len(truncated_files)} file(s) capped at "
f"{_PER_FILE_DETAIL_LINES} hits each. Issue "
f"query_single_file(\"{m}\", \"{pattern}\", file=...) "
f"to see all hits."]
for rel, total in truncated_files:
notes.append(f" {_qsf_call(rel)} # {total} total hits")
extra = total - _PER_FILE_DETAIL_LINES
notes.append(f" [+{extra} capped] {rel}")
output += "\n".join(notes)

output, truncated = _truncate(output)
Expand All @@ -485,6 +522,9 @@ def query_single_file(
symbol_kind: str = "",
uses_kind: str = "",
visibility: str = "",
head_lines: int = 0,
enclosing_method: str = "",
enclosing_class: str = "",
head_limit: int = 250,
offset: int = 0,
) -> str:
Expand Down Expand Up @@ -515,6 +555,14 @@ def query_single_file(
calls METHOD Call sites of METHOD. Pass a METHOD name, not a
receiver -- `obj.Foo()` is matched by calls("Foo")
not calls("obj"); for variable usage use all_refs.
caller_of METHOD Like ``calls``, but groups results by the enclosing
caller -- one row per ``(TypeName.MemberName)``
with a count of how many call sites it contains.
(C# only.)
callee_of METHOD The inverse: walk the body of the method named
METHOD and emit one row per distinct callee with
invocation counts. Constructor calls show as
``T (N invocations, ctor)``. (C# only.)
implements TYPE Types that inherit/implement TYPE.
uses TYPE Type references. Omit `uses_kind` (or "all") for
the union of every role; narrow with `uses_kind`
Expand Down Expand Up @@ -570,6 +618,14 @@ def query_single_file(
language's defaults (interface members => public, nested
types => private, top-level types => internal); other
languages currently return nothing when this filter is set.
head_lines: For `body` and `declarations include_body=True` -- truncate
each body to the first N source lines (signature + body
together) with a `... +K more lines` tail marker.
Default 0 = no truncation.
enclosing_method / enclosing_class:
For pattern modes -- restrict hits to those inside a
member / type with the given name. Composes with both
filters as a logical AND. (C# only.)
head_limit: Max results to return (default 250). Use with offset to page.
offset: Skip first N results before applying head_limit (default 0).

Expand Down Expand Up @@ -613,6 +669,9 @@ def query_single_file(
symbol_kind=symbol_kind or None,
uses_kind=uses_kind or None,
visibility=visibility or None,
head_lines=int(head_lines) if head_lines else None,
enclosing_method=enclosing_method or None,
enclosing_class=enclosing_class or None,
)
except ValueError as e:
# Unknown extension or unsupported mode -- propagate the helpful
Expand Down
Loading
Loading