feat(mcp+cli): project-root auto-discovery + ListRoots integration#173
Merged
Conversation
Until now every CLI subcommand and MCP-client config needed an
explicit path to the project root:
codeiq mcp /home/dev/projects/codeiq # always required
codeiq stats /home/dev/projects/codeiq # always required
That made MCP-client configs noisy (one entry per project, each with
a hardcoded path) and made `codeiq <cmd>` inside an indexed project
require typing the path twice (once to cd, once to pass).
### New resolution chain (highest wins)
1. Explicit positional argument (legacy behavior; unchanged)
2. CODEIQ_PROJECT_ROOT environment variable
3. Walk up from $CWD looking for `.codeiq/graph/codeiq.kuzu`
(already-indexed; strongest signal)
4. Walk up from $CWD looking for `.git/` (repo root)
5. Error with an actionable message listing all three options
Every CLI subcommand picks this up automatically because it lives
in a shared resolver — `internal/projectroot/`.
### MCP ListRoots integration
In addition to the boot-time resolution above, the MCP server installs
an `InitializedHandler` that calls `session.ListRoots(ctx, nil)` once
the client completes `initialize`. If the client exposes workspace
roots, the server compares them to its boot-resolved root and emits
a clear stderr warning when they don't match.
We do NOT swap the open Kuzu handle mid-flight — that's a larger
refactor (per-session store cache + RootsListChanged invalidation).
The warning surfaces a misconfiguration; the operator restarts with
the right arg or env value. Tracked as a follow-up for a later PR.
### MCP-client config simplification
Before (per-project, hardcoded):
{ "mcpServers": { "codeiq": { "command": "codeiq",
"args": ["mcp", "/home/dev/projects/codeiq"] } } }
After (one config, works everywhere — clients spawn with cwd =
project root):
{ "mcpServers": { "codeiq": { "command": "codeiq",
"args": ["mcp"] } } }
### Implementation
Added:
internal/projectroot/resolver.go — layered resolver (~140 LoC)
internal/projectroot/resolver_test.go — 9 tests covering arg /
env / walk-up / fallback /
negative paths
internal/mcp/server.go (compareRootsWithClient,
uriToPath) — ListRoots comparison +
file:// URI handling
internal/mcp/server_test.go (TestServer-
WithResolvedRootInitializesCleanly) — verifies init handler
doesn't break the handshake
Modified:
internal/cli/util.go — resolvePath delegates to
projectroot.FromArgs; new
helpful error message
internal/cli/mcp.go — passes the resolved root
into ServerOptions
### Verification
CGO_ENABLED=1 go test ./... -count=1
→ 890 passed across 44 packages (was 880 + 10 new resolver tests)
Smoke tested 5 scenarios end-to-end:
1. Explicit arg → 2 nodes / 1 edge from temp project
2. CODEIQ_PROJECT_ROOT env (cwd != project) → same result
3. Walk-up from nested/deep/ (no arg/env) → resolves to project root
4. No signals (cwd is /tmp) → actionable error
5. `codeiq mcp` (no arg) + tools/list → handshake completes;
server issues `roots/list`
back to the client (visible
in JSON-RPC trace)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Until now every CLI subcommand and MCP-client config needed an explicit path:
```json
{ "mcpServers": { "codeiq": {
"command": "codeiq",
"args": ["mcp", "/home/dev/projects/codeiq"]
}}}
```
That's noisy (one config per project, each hardcoded) and breaks the natural `codeiq ` flow when you're already inside an indexed project.
After this PR
```json
{ "mcpServers": { "codeiq": {
"command": "codeiq",
"args": ["mcp"]
}}}
```
The server (and every other CLI subcommand) auto-discovers the project root via a layered chain — and the MCP server additionally asks the connected client for its workspace roots once `initialize` completes.
Resolution chain
Highest wins:
Every CLI subcommand picks this up automatically because it lives in a shared resolver.
MCP `ListRoots` integration
In addition to the boot-time resolution above, the MCP server installs an `InitializedHandler` that calls `session.ListRoots(ctx, nil)` once the client completes `initialize`. If the client exposes workspace roots, the server compares them to its boot-resolved root and emits a clear stderr warning when they don't match.
The server does not swap the open Kuzu handle mid-flight — that's a larger refactor (per-session store cache + RootsListChanged invalidation). The warning surfaces a misconfiguration; the operator restarts with the right arg or env value. Tracked as a follow-up.
Implementation
Added:
Modified:
Test plan
Out of scope
🤖 Generated with Claude Code