diff --git a/server.py b/server.py
index 526c4aa..80fb8a8 100644
--- a/server.py
+++ b/server.py
@@ -112,7 +112,44 @@ def __init__(self, source_root: Path):
def _detect_scope(self) -> str | None:
from graph_enrich import detect_microservice_from_path
- return detect_microservice_from_path(Path.cwd(), self.source_root)
+
+ candidate = detect_microservice_from_path(Path.cwd(), self.source_root)
+ if candidate is None:
+ return None
+ # Only auto-scope to a microservice that actually has indexed code.
+ # detect_microservice_from_path can mislabel a non-microservice
+ # top-level child of source_root — most importantly the config/context
+ # directory the MCP server is launched from (no build marker, no
+ # source) — via its "first path segment under root" fallback. Scoping
+ # every query to such a name yields zero matches, so all tools return
+ # empty. A real microservice the operator is working in is, by
+ # definition, present in the index, so validating against the indexed
+ # set cannot suppress a legitimate scope. When the index is unreadable
+ # (empty known set) we keep the detected candidate rather than silently
+ # disabling auto-scope on a transient graph error.
+ known = self._indexed_microservices()
+ if known and candidate not in known:
+ return None
+ return candidate
+
+ def _indexed_microservices(self) -> set[str]:
+ """Microservice names that have indexed type symbols.
+
+ Graph-only source of truth: the graph is always built alongside Lance,
+ and a Lance-only index (no graph) is not a supported state. Any failure
+ (graph missing, open error, empty index) returns an empty set, which
+ ``_detect_scope`` treats as "cannot validate — keep detection".
+ """
+ try:
+ if not LadybugGraph.exists():
+ return set()
+ # LadybugGraph.get() opens the DB and runs meta(); it can raise
+ # (e.g. RuntimeError on ontology-version mismatch). Caught here ->
+ # empty set -> _detect_scope keeps the detected scope.
+ counts = LadybugGraph.get().microservice_counts()
+ return {name for name in counts if name}
+ except Exception:
+ return set()
def _log_detection(self) -> None:
if self.default_scope:
diff --git a/tests/test_microservice_scope.py b/tests/test_microservice_scope.py
index d8ccb8d..01b1418 100644
--- a/tests/test_microservice_scope.py
+++ b/tests/test_microservice_scope.py
@@ -150,3 +150,121 @@ def test_detect_scope_integration(self, tmp_path):
nf = NodeFilter(role="CONTROLLER")
result = mgr.apply_auto_scope(nf)
assert result is nf
+
+
+class TestScopeManagerAutoScopeValidation:
+ """Auto-scope must not fire for a microservice absent from the index.
+
+ Regression: launching the MCP server from the config/context directory (a
+ top-level child of source_root with no build marker and no source) made
+ ``detect_microservice_from_path`` return that directory's name via its
+ "first path segment under root" fallback. ScopeManager then auto-scoped
+ every query to a microservice with zero indexed rows, so all tools
+ returned empty results. The detected scope is now validated against the
+ indexed microservice set; a candidate with no indexed code is suppressed.
+ """
+
+ @staticmethod
+ def _stub_index(monkeypatch, microservices: set[str]) -> None:
+ """Make ScopeManager._indexed_microservices() see a fake graph."""
+ import server
+
+ class _FakeGraph:
+ def microservice_counts(self):
+ return {name: 1 for name in microservices}
+
+ monkeypatch.setattr(
+ server.LadybugGraph, "exists", lambda db_path=None: len(microservices) > 0
+ )
+ monkeypatch.setattr(
+ server.LadybugGraph, "get", lambda db_path=None: _FakeGraph()
+ )
+
+ def test_context_dir_not_detected_as_microservice(self, tmp_path, monkeypatch):
+ """Launching from a codeless context dir must NOT auto-scope (the bug)."""
+ from server import ScopeManager
+
+ # Reported layout: source_root holds both the context dir and a real
+ # microservice; the server is launched from the context dir.
+ context_dir = tmp_path / "bank-chat-context"
+ context_dir.mkdir()
+ ms_dir = tmp_path / "microservice-a"
+ (ms_dir / "src").mkdir(parents=True)
+ (ms_dir / "pom.xml").write_text("")
+
+ # The index only knows the real microservice, not the context dir.
+ self._stub_index(monkeypatch, {"microservice-a"})
+ monkeypatch.chdir(context_dir)
+
+ mgr = ScopeManager(tmp_path)
+ assert mgr.default_scope is None
+
+ def test_real_microservice_dir_still_scopes(self, tmp_path, monkeypatch):
+ """Launching from inside an indexed microservice keeps auto-scope."""
+ from server import ScopeManager
+
+ ms_dir = tmp_path / "microservice-a"
+ (ms_dir / "src").mkdir(parents=True)
+ (ms_dir / "pom.xml").write_text("")
+
+ self._stub_index(monkeypatch, {"microservice-a"})
+ monkeypatch.chdir(ms_dir)
+
+ mgr = ScopeManager(tmp_path)
+ assert mgr.default_scope == "microservice-a"
+
+ def test_empty_index_keeps_detection(self, tmp_path, monkeypatch):
+ """When the index is missing (exists()->False), keep detection."""
+ from server import ScopeManager
+
+ ms_dir = tmp_path / "microservice-a"
+ ms_dir.mkdir()
+ (ms_dir / "pom.xml").write_text("")
+
+ # Graph missing -> exists() False -> empty known set.
+ self._stub_index(monkeypatch, set())
+ monkeypatch.chdir(ms_dir)
+
+ mgr = ScopeManager(tmp_path)
+ assert mgr.default_scope == "microservice-a"
+
+ def test_empty_graph_present_keeps_detection(self, tmp_path, monkeypatch):
+ """Graph present but reporting no microservices also keeps detection.
+
+ Covers the exists()->True branch with empty microservice_counts() —
+ distinct from test_empty_index_keeps_detection (missing graph). Both
+ paths must converge to keeping the detected scope rather than silently
+ disabling auto-scope.
+ """
+ import server
+ from server import ScopeManager
+
+ ms_dir = tmp_path / "microservice-a"
+ ms_dir.mkdir()
+ (ms_dir / "pom.xml").write_text("")
+
+ class _EmptyGraph:
+ def microservice_counts(self):
+ return {}
+
+ monkeypatch.setattr(server.LadybugGraph, "exists", lambda db_path=None: True)
+ monkeypatch.setattr(server.LadybugGraph, "get", lambda db_path=None: _EmptyGraph())
+ monkeypatch.chdir(ms_dir)
+
+ mgr = ScopeManager(tmp_path)
+ assert mgr.default_scope == "microservice-a"
+
+ def test_indexed_microservices_extracts_nonempty_keys(self, tmp_path, monkeypatch):
+ """_indexed_microservices drops empty-string buckets, keeps the rest."""
+ import server
+ from server import ScopeManager
+
+ class _FakeGraph:
+ def microservice_counts(self):
+ return {"chat-core": 140, "chat-assign": 50, "": 3}
+
+ monkeypatch.setattr(server.LadybugGraph, "exists", lambda db_path=None: True)
+ monkeypatch.setattr(server.LadybugGraph, "get", lambda db_path=None: _FakeGraph())
+
+ mgr = ScopeManager(tmp_path)
+ assert mgr._indexed_microservices() == {"chat-core", "chat-assign"}