Skip to content

Commit ca005a9

Browse files
HumanBean17claude
andauthored
add walk-up config discovery and configurable source root (#270)
* add walk-up config discovery and source_root YAML field Implements propose/active/DIRS-HIERARCHY-PROPOSE.md as a single PR. - discover_project_root(start) walks from start upward looking for .java-codebase-rag.yml/.yaml, first match wins, stops at $HOME (inclusive). Mirrors git's .git discovery. - resolve_operator_config() uses two-phase resolution: phase 1 finds the config file directory via walk-up, phase 2 resolves effective source root from CLI > env > YAML source_root > discovery > cwd. - server.py _project_root() and _resolve_lancedb_uri() use walk-up when JAVA_CODEBASE_RAG_SOURCE_ROOT is unset. - cli.py init emits a soft warning when a parent config is detected. - YAML config gains an optional source_root field resolved relative to the config file directory (not cwd). - mcp.json.example shows minimal zero-env-var config. - README and CONFIGURATION docs updated for walk-up and source_root. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix server YAML source_root resolution and add review tests - server.py main() passes source_root=None to resolve_operator_config so YAML source_root is resolved in Phase 2, not skipped. The previous code passed _project_root() as source_root, which caused the CLI path to skip YAML resolution (C1 from code review). - cli.py _cmd_diagnose_ignore uses cfg.source_root instead of server._project_root() to avoid divergence when YAML source_root is set (I3 from code review). - add test for YAML source_root resolution via server path. - add tests for init parent-config warning detection. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix server startup tests for source_root=None change Update test_mcp_server_loads_yaml_config_at_startup and test_mcp_server_yaml_config_precedence_env_over_yaml to expect resolve_operator_config(source_root=None) instead of the old resolve_operator_config(source_root=_project_root()). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent f25f63a commit ca005a9

9 files changed

Lines changed: 426 additions & 39 deletions

File tree

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ If vector hits come back and graph expansion adds neighbor symbols, the install
8484

8585
## Wire into an MCP host
8686

87+
The server discovers your project automatically: it walks up from cwd looking for `.java-codebase-rag.yml` (or `.yaml`), like git finds `.git`. No env vars required if you have a YAML config in your project tree. For full precedence details, see [`docs/CONFIGURATION.md`](./docs/CONFIGURATION.md).
88+
8789
### Claude Code
8890

8991
With the package installed, the console script `java-codebase-rag-mcp` is on your `PATH`. Register it project-scoped:
@@ -92,7 +94,7 @@ With the package installed, the console script `java-codebase-rag-mcp` is on you
9294
claude mcp add --transport stdio java-codebase-rag -- java-codebase-rag-mcp
9395
```
9496

95-
Then set env vars (`JAVA_CODEBASE_RAG_INDEX_DIR`, `JAVA_CODEBASE_RAG_SOURCE_ROOT`, `SBERT_MODEL`, …) in `.mcp.json` or your shell profile. For a project-scoped `.mcp.json` template, see [`mcp.json.example`](./mcp.json.example). Official docs: [Claude Code settings](https://docs.anthropic.com/en/docs/claude-code/settings).
97+
No env vars needed — the server walks up from cwd to find `.java-codebase-rag.yml`. For a minimal `.mcp.json` template, see [`mcp.json.example`](./mcp.json.example). Official docs: [Claude Code settings](https://docs.anthropic.com/en/docs/claude-code/settings).
9698

9799
### Claude Desktop
98100

@@ -102,16 +104,14 @@ Edit `claude_desktop_config.json` (macOS: `~/Library/Application Support/Claude/
102104
{
103105
"mcpServers": {
104106
"java-codebase-rag": {
105-
"command": "java-codebase-rag-mcp",
106-
"env": {
107-
"JAVA_CODEBASE_RAG_INDEX_DIR": "/ABSOLUTE/PATH/TO/.java-codebase-rag",
108-
"JAVA_CODEBASE_RAG_SOURCE_ROOT": "/ABSOLUTE/PATH/TO/your-java-project"
109-
}
107+
"command": "java-codebase-rag-mcp"
110108
}
111109
}
112110
}
113111
```
114112

113+
The server discovers the project via walk-up from the cwd of the MCP host process. If your Java project is not the cwd, either set `JAVA_CODEBASE_RAG_SOURCE_ROOT` in the `env` block or add a `source_root` field to `.java-codebase-rag.yml` (see [`docs/CONFIGURATION.md`](./docs/CONFIGURATION.md)).
114+
115115
See [`mcp.json.example`](./mcp.json.example) for the same shape in `.mcp.json` (Claude Code project-scoped) form.
116116

117117
### Driving the MCP from an agent

docs/CONFIGURATION.md

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,22 @@ For the architecture rationale (the GPS metaphor, three-layer design, future wor
2222

2323
The operator-facing surface is **six** variables (plus MCP-only `JAVA_CODEBASE_RAG_SOURCE_ROOT` below). Precedence for knobs that also exist as CLI flags or YAML entries is **CLI flag > env var > YAML > built-in default** (see [`JAVA-CODEBASE-RAG-CLI.md`](./JAVA-CODEBASE-RAG-CLI.md)).
2424

25+
### Source root discovery and precedence
26+
27+
The server and CLI resolve the effective Java source root through a precedence chain:
28+
29+
| Priority | Source | How it resolves |
30+
|---|---|---|
31+
| 1 (highest) | CLI `--source-root` | Absolute, or relative to cwd |
32+
| 2 | `JAVA_CODEBASE_RAG_SOURCE_ROOT` env var | Absolute, or relative to cwd |
33+
| 3 | YAML `source_root` field | **Relative to the config file directory** (not cwd) |
34+
| 4 | Walk-up discovery | Walk from cwd upward to find `.java-codebase-rag.yml`; uses the config file's directory as source root |
35+
| 5 (lowest) | cwd | No config found, no YAML override |
36+
37+
Walk-up checks each directory from cwd upward for `.java-codebase-rag.yml` or `.java-codebase-rag.yaml`. The **first match wins** (closest to cwd). The walk stops at `$HOME` (inclusive — `$HOME` itself is checked) or the filesystem root. This mirrors how git finds `.git`.
38+
39+
### Variables
40+
2541
| Variable | Purpose |
2642
|---|---|
2743
| `JAVA_CODEBASE_RAG_INDEX_DIR` | Local filesystem **directory** for Lance tables, the Kuzu file `code_graph.kuzu`, and cocoindex state (`cocoindex.db`). Not a `lancedb://` or cloud URI — use a path. Default: `./.java-codebase-rag/` under the resolved Java tree root. |
@@ -31,14 +47,14 @@ The operator-facing surface is **six** variables (plus MCP-only `JAVA_CODEBASE_R
3147
| `JAVA_CODEBASE_RAG_RUN_HEAVY` | Test gate: set to `1` / `true` / `yes` to run the slow cocoindex + Lance end-to-end test (`pytest`); not used in normal operator workflows. |
3248
| `JAVA_CODEBASE_RAG_HINTS_ENABLED` | When `0` / `false` / `no`, suppress `hints_structured` and `advisories` from all MCP tool responses. Overridable via `.java-codebase-rag.yml` `hints.enabled`. Default: enabled. |
3349

34-
**MCP host launchers** also set `JAVA_CODEBASE_RAG_SOURCE_ROOT` to the Java repository root when it differs from the server process cwd (see `mcp.json.example` in the repo root).
50+
**MCP host launchers** also set `JAVA_CODEBASE_RAG_SOURCE_ROOT` to the Java repository root when it differs from the server process cwd (see `mcp.json.example` in the repo root). When the env var is unset, the server walks up from cwd to discover the config automatically.
3551

3652
Only the names in the table above (plus `JAVA_CODEBASE_RAG_SOURCE_ROOT` for MCP hosts) are read as configuration. Project config belongs in **`.java-codebase-rag.yml`** (or `.yaml`).
3753

3854
**Paths and conventions** (for scripts and operators):
3955

4056
- **`JAVA_CODEBASE_RAG_INDEX_DIR`** — filesystem path to the index directory (not a URI). Lance opens this directory; Kuzu is always `<index-dir>/code_graph.kuzu`; cocoindex keeps **`cocoindex.db`** next to them.
41-
- **Java tree root** — CLI: `--source-root` (else cwd). MCP stdio: set `JAVA_CODEBASE_RAG_SOURCE_ROOT` when the Java repo root differs from the server process cwd.
57+
- **Java tree root** — CLI: `--source-root` (else walk-up discovery, else cwd). MCP stdio: `JAVA_CODEBASE_RAG_SOURCE_ROOT` env var (else walk-up from cwd). YAML: `source_root` field resolved relative to the config file directory.
4258
- **`microservice_roots`** — configure only under **`microservice_roots:`** in `.java-codebase-rag.yml` (or `.yaml`).
4359
- **Chunk context diagnostics / heavy tests**`JAVA_CODEBASE_RAG_DEBUG_CONTEXT`, `JAVA_CODEBASE_RAG_RUN_HEAVY` (see the table above).
4460

@@ -48,16 +64,24 @@ Python package: **`java_codebase_rag`** (`python -m java_codebase_rag.cli`).
4864

4965
## 2. Project YAML reference (`.java-codebase-rag.yml`)
5066

51-
A single file at the project root (the directory you pass as `--source-root`, or cwd) holds everything that isn't an environment variable. The two accepted filenames are `.java-codebase-rag.yml` and `.java-codebase-rag.yaml`; if both exist, `.yml` wins.
67+
A single file at the project root (the directory you pass as `--source-root`, or discovered via walk-up, or cwd) holds everything that isn't an environment variable. The two accepted filenames are `.java-codebase-rag.yml` and `.java-codebase-rag.yaml`; if both exist, `.yml` wins.
5268

5369
**All keys are optional.** A project with no YAML at all uses built-in defaults plus env vars. Add only the keys you need.
5470

5571
```yaml
5672
# .java-codebase-rag.yml — full reference, every key annotated.
57-
# Place at the project root (same directory you pass as --source-root).
73+
# Place at the project root (same directory you pass as --source-root),
74+
# or anywhere above it — the server walks up from cwd to find it.
5875

5976
# -------- Core knobs (mirror env vars; precedence: CLI > env > YAML > default) --------
6077

78+
# Source root: where your Java source tree lives. When set, resolves relative to
79+
# this config file's directory (not cwd). Useful when the config file lives outside
80+
# the Java tree (e.g. in a monorepo root above multiple Java projects).
81+
# When omitted, defaults to the directory containing this config file (found via walk-up).
82+
# CLI: --source-root. Env: JAVA_CODEBASE_RAG_SOURCE_ROOT.
83+
source_root: ./my-java-project
84+
6185
# Index directory: where Lance tables, code_graph.kuzu, and cocoindex.db live.
6286
# - Tilde (`~`) is expanded; `$VAR` is NOT (use absolute paths or `~`).
6387
# - Relative paths resolve against source_root, not cwd.
@@ -171,6 +195,7 @@ async_producer_overrides:
171195

172196
| Field | Expanded? | Notes |
173197
|---|---|---|
198+
| `source_root` | partial | `~` expanded; `$VAR` is NOT expanded. Relative paths resolve against the **config file directory** (not cwd). |
174199
| `index_dir` | partial | `~` expanded; `$VAR` is NOT expanded. Relative paths resolve against `source_root`. |
175200
| `embedding.model` (when path-shaped) | yes | Path-shape = starts with `/`, `./`, `../`, `~`, or contains `$`. Plain `org/name` is treated as a hub id and passed through. Applies to the value after CLI > env > YAML > default precedence. Long-lived MCP hosts also apply the same expansion when reading `SBERT_MODEL` from the process environment (so table metadata and search agree with `index_common` defaults). |
176201
| `embedding.device` | n/a | Device strings (`cpu`, `cuda`, `mps`) aren't paths. |
@@ -179,7 +204,7 @@ async_producer_overrides:
179204

180205
**Tips & gotchas:**
181206

182-
- **The file must be at `source_root`**, not in `$HOME`. The MCP server reads `JAVA_CODEBASE_RAG_SOURCE_ROOT` to find it; the CLI uses `--source-root` (else cwd).
207+
- **The file is discovered by walking up from cwd** — like git finds `.git`. Place it at or above your project root. The walk stops at `$HOME` (inclusive). You can also set `JAVA_CODEBASE_RAG_SOURCE_ROOT` or use `--source-root` to bypass discovery entirely.
183208
- **Don't commit secrets** into this YAML — it sits next to your source tree and is read by every operator who clones it.
184209
- **Rebuild after editing brownfield overrides.** Run a full `java-codebase-rag reprocess` (no flags) so Lance and Kuzu stay coherent, or use `--graph-only` / `--vectors-only` when you know only one store needs invalidation. Editing `embedding.model` requires a vector rebuild (`reprocess` or `--vectors-only`).
185210
- **Diagnose what's loaded.** `java-codebase-rag meta` prints the resolved config and each value's `*_source` (`cli` / `env` / `yaml` / `default`) — see `embedding_model_source`, `embedding_device_source`, `index_dir_source`.

java_codebase_rag/cli.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from java_codebase_rag.config import (
1717
ResolvedOperatorConfig,
1818
describe_path_sizes,
19+
discover_project_root,
1920
emit_legacy_env_hints_if_present,
2021
emit_legacy_yaml_hint_if_needed,
2122
index_dir_has_existing_artifacts,
@@ -231,6 +232,18 @@ def _cmd_init(args: argparse.Namespace) -> int:
231232
cfg = _resolved_from_ns(args)
232233
_startup_hints(cfg)
233234
cfg.apply_to_os_environ()
235+
parent_cfg_dir = discover_project_root(cfg.source_root.parent)
236+
if parent_cfg_dir is not None:
237+
from java_codebase_rag.config import YAML_CONFIG_FILENAMES
238+
239+
for name in YAML_CONFIG_FILENAMES:
240+
if (parent_cfg_dir / name).is_file():
241+
print(
242+
f"Warning: found existing config at {parent_cfg_dir / name}. "
243+
"Creating a new project here will create a separate index.",
244+
file=sys.stderr,
245+
)
246+
break
234247
occupied, paths = index_dir_has_existing_artifacts(cfg.index_dir)
235248
if occupied:
236249
_emit(
@@ -521,13 +534,12 @@ def _cmd_tables(args: argparse.Namespace) -> int:
521534

522535

523536
def _cmd_diagnose_ignore(args: argparse.Namespace) -> int:
524-
import server # lazy
525537
from path_filtering import LayeredIgnore # lazy
526538

527539
cfg = _resolved_from_ns(args)
528540
_startup_hints(cfg)
529541
cfg.apply_to_os_environ()
530-
root = server._project_root()
542+
root = cfg.source_root
531543
raw = Path(args.path)
532544
try:
533545
abs_path = raw.resolve() if raw.is_absolute() else (root / raw).resolve()

java_codebase_rag/config.py

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,28 @@ def emit_legacy_yaml_hint_if_needed(source_root: Path) -> None:
115115
return
116116

117117

118+
def discover_project_root(start: Path) -> Path | None:
119+
"""Walk from *start* upward looking for a YAML config file.
120+
121+
Returns the directory containing the first matching config file
122+
(closest to *start*), or ``None`` if no config is found before
123+
reaching ``$HOME`` (inclusive — ``$HOME`` itself is checked) or
124+
the filesystem root.
125+
"""
126+
home = Path.home().resolve()
127+
cur = start.resolve()
128+
while True:
129+
for name in YAML_CONFIG_FILENAMES:
130+
if (cur / name).is_file():
131+
return cur
132+
if cur == home:
133+
return None
134+
parent = cur.parent
135+
if parent == cur:
136+
return None
137+
cur = parent
138+
139+
118140
def find_yaml_config_file(source_root: Path) -> Path | None:
119141
for name in YAML_CONFIG_FILENAMES:
120142
p = source_root / name
@@ -277,10 +299,33 @@ def resolve_operator_config(
277299
cli_embedding_model: str | None = None,
278300
cli_embedding_device: str | None = None,
279301
) -> ResolvedOperatorConfig:
280-
root = (source_root or Path.cwd()).expanduser().resolve()
281-
yaml_dict = load_yaml_mapping(root)
302+
# Phase 1 — find the config file directory.
303+
if source_root is not None:
304+
config_dir = source_root.expanduser().resolve()
305+
else:
306+
discovered = discover_project_root(Path.cwd())
307+
if discovered is not None:
308+
config_dir = discovered
309+
else:
310+
config_dir = Path.cwd().resolve()
311+
312+
yaml_dict = load_yaml_mapping(config_dir)
313+
314+
# Phase 2 — resolve effective source root.
315+
env_root = os.environ.get(ENV_SOURCE_ROOT, "").strip()
316+
if source_root is not None:
317+
effective_root = source_root.expanduser().resolve()
318+
elif env_root:
319+
effective_root = Path(env_root).expanduser().resolve()
320+
else:
321+
yaml_sr = yaml_dict.get("source_root")
322+
if isinstance(yaml_sr, str) and yaml_sr.strip():
323+
effective_root = (config_dir / Path(yaml_sr.strip()).expanduser()).resolve()
324+
else:
325+
effective_root = config_dir
326+
282327
index_dir, index_src = _resolve_index_dir_path(
283-
source_root=root, cli_index_dir=cli_index_dir, yaml_dict=yaml_dict
328+
source_root=effective_root, cli_index_dir=cli_index_dir, yaml_dict=yaml_dict
284329
)
285330
model, model_src = _pick_str(
286331
cli_val=cli_embedding_model,
@@ -304,7 +349,7 @@ def resolve_operator_config(
304349
ku = index_dir / "code_graph.kuzu"
305350
coco = index_dir / "cocoindex.db"
306351
return ResolvedOperatorConfig(
307-
source_root=root,
352+
source_root=effective_root,
308353
index_dir=index_dir,
309354
kuzu_path=ku,
310355
cocoindex_db=coco,

mcp.json.example

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,37 @@
22
"mcpServers": {
33
"java-codebase-rag": {
44
"type": "stdio",
5-
"command": "java-codebase-rag-mcp",
6-
"env": {
7-
"JAVA_CODEBASE_RAG_INDEX_DIR": "/ABSOLUTE/PATH/TO/.java-codebase-rag",
8-
"JAVA_CODEBASE_RAG_SOURCE_ROOT": "/ABSOLUTE/PATH/TO/your-java-project",
9-
"SBERT_MODEL": "sentence-transformers/all-MiniLM-L6-v2"
10-
}
5+
"command": "java-codebase-rag-mcp"
116
}
127
}
138
}
9+
10+
// ──────────────────────────────────────────────────────────────────────────────
11+
// 1. MINIMAL CONFIG — no env vars required
12+
//
13+
// Requires a `.java-codebase-rag.yml` in (or above) your Java project root.
14+
// The server walks up from cwd to find the config file (like git finds .git).
15+
// Run `java-codebase-rag init` from the project root first to create the index.
16+
//
17+
// Claude Code: drop this as `.mcp.json` in your project root.
18+
// Claude Desktop: paste into ~/Library/Application Support/Claude/claude_desktop_config.json
19+
// and add `"cwd": "/path/to/your-java-project"` inside the server block.
20+
// ──────────────────────────────────────────────────────────────────────────────
21+
22+
// ──────────────────────────────────────────────────────────────────────────────
23+
// 2. FULL CONFIG — explicit env vars (works without .java-codebase-rag.yml)
24+
//
25+
// {
26+
// "mcpServers": {
27+
// "java-codebase-rag": {
28+
// "type": "stdio",
29+
// "command": "java-codebase-rag-mcp",
30+
// "env": {
31+
// "JAVA_CODEBASE_RAG_INDEX_DIR": "/ABSOLUTE/PATH/TO/.java-codebase-rag",
32+
// "JAVA_CODEBASE_RAG_SOURCE_ROOT": "/ABSOLUTE/PATH/TO/your-java-project",
33+
// "SBERT_MODEL": "sentence-transformers/all-MiniLM-L6-v2"
34+
// }
35+
// }
36+
// }
37+
// }
38+
// ──────────────────────────────────────────────────────────────────────────────

server.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
emit_vectors_finish,
1717
emit_vectors_start,
1818
)
19-
from java_codebase_rag.config import emit_legacy_env_hints_if_present, resolved_sbert_model_for_process_env, resolve_operator_config
19+
from java_codebase_rag.config import discover_project_root, emit_legacy_env_hints_if_present, resolved_sbert_model_for_process_env, resolve_operator_config
2020
from kuzu_queries import KuzuGraph, resolve_kuzu_path
2121
from mcp.server.fastmcp import FastMCP
2222
from pydantic import BaseModel, Field
@@ -94,7 +94,7 @@ class IndexInfoOutput(BaseModel):
9494
def _resolve_lancedb_uri() -> str:
9595
raw = os.environ.get("JAVA_CODEBASE_RAG_INDEX_DIR", "").strip()
9696
if not raw:
97-
raw = str((Path.cwd() / ".java-codebase-rag").resolve())
97+
raw = str((_project_root() / ".java-codebase-rag").resolve())
9898
p = Path(raw).expanduser()
9999
if not str(raw).startswith(("s3://", "gs://", "az://")):
100100
try:
@@ -108,6 +108,9 @@ def _project_root() -> Path:
108108
env = os.environ.get("JAVA_CODEBASE_RAG_SOURCE_ROOT", "").strip()
109109
if env:
110110
return Path(env).expanduser().resolve()
111+
discovered = discover_project_root(Path.cwd())
112+
if discovered is not None:
113+
return discovered
111114
return Path.cwd().resolve()
112115

113116

@@ -575,8 +578,10 @@ def main() -> None:
575578

576579
# Load YAML config and apply embedding settings to environment
577580
# This ensures SBERT_MODEL and SBERT_DEVICE from .java-codebase-rag.yml are available
578-
# before any tool handler runs (same behavior as CLI path)
579-
cfg = resolve_operator_config(source_root=_project_root())
581+
# before any tool handler runs (same behavior as CLI path).
582+
# Pass source_root=None so walk-up + YAML source_root resolution happens
583+
# inside resolve_operator_config (CLI > env > YAML > discovery > cwd).
584+
cfg = resolve_operator_config(source_root=None)
580585
cfg.apply_to_os_environ()
581586
mcp_v2.set_hints_enabled(cfg.hints_enabled)
582587

0 commit comments

Comments
 (0)