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
22 changes: 22 additions & 0 deletions server/tools/_services.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from __future__ import annotations

from server.state import get_service_registry
from server.store.service_registry import load_effective_services


async def unknown_service_message(service: str) -> str:
"""Explain an unknown-service lookup in terms of what the server can actually see.

The effective service list merges config.yaml with dynamically-registered
services, so a miss has two very different causes: a genuine typo, or a server
that loaded no config at all (e.g. config.yaml not mounted into the container).
Listing the known names tells those apart instead of guessing at config.yaml.
"""
services = await load_effective_services(get_service_registry())
if not services:
return (
f"Service `{service}` not found — this server has no services at all "
f"(config.yaml is empty or was never loaded, and nothing is registered)."
)
known = ", ".join(f"`{s.name}`" for s in sorted(services, key=lambda s: s.name))
return f"Service `{service}` not found. Known services: {known}."
3 changes: 2 additions & 1 deletion server/tools/history.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from server.embeddings import get_embedding_provider
from server.indexer.git_history import GitHistoryPipeline
from server.state import get_commit_store
from server.tools._services import unknown_service_message


def register_history_tools(mcp: MCPServer) -> None:
Expand Down Expand Up @@ -126,7 +127,7 @@ async def index_history(
if service:
result = await pipeline.index_service(service, force=force)
if "error" in result:
return f"Service `{service}` not found in config.yaml."
return await unknown_service_message(service)
lines = [
f"Git history indexed for `{service}`:",
f"- New commits: {result['new']}",
Expand Down
3 changes: 2 additions & 1 deletion server/tools/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from server.indexer.pipeline import IndexPipeline
from server.state import get_store
from server.tools._services import unknown_service_message


def register_index_tools(mcp: MCPServer) -> None:
Expand All @@ -25,7 +26,7 @@ async def reindex(
if service:
result = await pipeline.index_service(service, force=force)
if "error" in result:
return f"Service `{service}` not found in config.yaml."
return await unknown_service_message(service)
return (
f"Reindex complete for `{service}`:\n"
f"- Files indexed: {result['files']}\n"
Expand Down
10 changes: 8 additions & 2 deletions tests/tools/test_history.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

from types import SimpleNamespace
from unittest.mock import AsyncMock, patch

from server.tools.history import register_history_tools
Expand Down Expand Up @@ -111,18 +112,23 @@ async def test_get_commit_formats_changed_files_and_truncation() -> None:
assert "Diff truncated" in result


async def test_index_history_reports_unknown_service() -> None:
async def test_index_history_unknown_service_lists_known_services() -> None:
index_history = _tool("index_history")
pipeline = AsyncMock()
pipeline.index_service.return_value = {"error": 1}

with (
patch("server.tools.history.get_commit_store", return_value=AsyncMock()),
patch("server.tools.history.GitHistoryPipeline", return_value=pipeline),
patch("server.tools._services.get_service_registry"),
patch(
"server.tools._services.load_effective_services",
AsyncMock(return_value=[SimpleNamespace(name="orders")]),
),
):
result = await index_history(service="unknown")

assert result == "Service `unknown` not found in config.yaml."
assert result == "Service `unknown` not found. Known services: `orders`."


async def test_index_history_single_service_reports_diff_updates() -> None:
Expand Down
35 changes: 33 additions & 2 deletions tests/tools/test_index.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

from types import SimpleNamespace
from unittest.mock import AsyncMock, patch

from server.tools.index import register_index_tools
Expand All @@ -10,18 +11,48 @@ def _tool(name: str):
return get_tool(register_index_tools, name)


async def test_reindex_reports_unknown_service() -> None:
async def test_reindex_unknown_service_lists_known_services() -> None:
reindex = _tool("reindex")
pipeline = AsyncMock()
pipeline.index_service.return_value = {"error": 1, "files": 0, "chunks": 0}

with (
patch("server.tools.index.get_store", return_value=AsyncMock()),
patch("server.tools.index.IndexPipeline", return_value=pipeline),
patch("server.tools._services.get_service_registry"),
patch(
"server.tools._services.load_effective_services",
AsyncMock(
return_value=[
SimpleNamespace(name="orders"),
SimpleNamespace(name="catalog"),
]
),
),
):
result = await reindex(service="unknown")

assert result == "Service `unknown` not found in config.yaml."
assert result == "Service `unknown` not found. Known services: `catalog`, `orders`."


async def test_reindex_unknown_service_flags_empty_service_list() -> None:
reindex = _tool("reindex")
pipeline = AsyncMock()
pipeline.index_service.return_value = {"error": 1, "files": 0, "chunks": 0}

with (
patch("server.tools.index.get_store", return_value=AsyncMock()),
patch("server.tools.index.IndexPipeline", return_value=pipeline),
patch("server.tools._services.get_service_registry"),
patch(
"server.tools._services.load_effective_services",
AsyncMock(return_value=[]),
),
):
result = await reindex(service="auth-server")

assert "has no services at all" in result
assert "never loaded" in result


async def test_reindex_single_service_reports_counts() -> None:
Expand Down