`okf project`'s closing text and the README's first screen now say it in that order: register `okf mcp --root` once, and it answers from every project and reaches subagents; the skill beside the bundle is for someone who would rather register nothing; neither is made again when a bundle is rebuilt. Two tests hold the order in both places. The README's stale `<id>-consume` skill path is corrected to `okf-consume-any`. v1.1 order F, part F3. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
200 lines
8.1 KiB
Python
200 lines
8.1 KiB
Python
"""MCP as the standard entry: the method travels, and describe fans out.
|
|
|
|
Subagents inherit a session's MCP tools; they do not inherit its skills. So a
|
|
working method that lives only in a skill reaches the main thread and no arm
|
|
below it, and the one place it can reach every caller is the server's own
|
|
`instructions` and tool descriptions -- both capped by Claude Code at 2 KB
|
|
each, which is why what travels is the SHORT version and the long one stays in
|
|
the skill.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(PROJECT_ROOT / "src"))
|
|
|
|
from llm_ingestion_okf import consume as okf_consume # noqa: E402
|
|
from llm_ingestion_okf import mcp_server, project # noqa: E402
|
|
|
|
GOLDEN = PROJECT_ROOT / "examples" / "ingest-golden-segmented-okf-v0-2" / "expected-bundle"
|
|
FIXTURE = PROJECT_ROOT / "tests" / "fixtures" / "consume-bundle"
|
|
|
|
#: Claude Code truncates server instructions and each tool description at 2 KB
|
|
#: (`docs-en-mcp.md`). A description over it is not rejected -- it is CUT, which
|
|
#: is worse: the reader gets the first half of a method and no sign that the
|
|
#: rest existed.
|
|
CLIENT_TRUNCATION_BYTES = 2048
|
|
|
|
|
|
@pytest.fixture
|
|
def served(tmp_path: Path) -> mcp_server.Surface:
|
|
root = tmp_path / "root"
|
|
(root / "a").mkdir(parents=True)
|
|
(root / "b").mkdir(parents=True)
|
|
for name, source in (("a", GOLDEN), ("b", FIXTURE)):
|
|
for path in source.rglob("*"):
|
|
if path.is_file():
|
|
target = root / name / path.relative_to(source)
|
|
target.parent.mkdir(parents=True, exist_ok=True)
|
|
target.write_bytes(path.read_bytes())
|
|
return mcp_server.Surface(roots=(root,), fixed=None, profile=okf_consume.DEFAULT_PROFILE)
|
|
|
|
|
|
def _instructions(surface: mcp_server.Surface) -> str:
|
|
result = mcp_server.handle(surface, "initialize", {})
|
|
assert isinstance(result["instructions"], str)
|
|
return result["instructions"]
|
|
|
|
|
|
def test_the_instructions_carry_the_short_working_method(served: mcp_server.Surface) -> None:
|
|
text = _instructions(served)
|
|
for mark in ("bundle's own words", "sub-question", "ask again", "outside the cut"):
|
|
assert mark in text, f"the instructions do not say {mark!r}"
|
|
|
|
|
|
def test_the_instructions_fit_inside_what_the_client_keeps(
|
|
served: mcp_server.Surface,
|
|
) -> None:
|
|
text = _instructions(served)
|
|
assert len(text.encode("utf-8")) <= CLIENT_TRUNCATION_BYTES
|
|
# The control: the limit is one this text could realistically cross, so
|
|
# the assertion above is a measurement and not a tautology.
|
|
assert len(text.encode("utf-8")) > CLIENT_TRUNCATION_BYTES // 4
|
|
|
|
|
|
def test_every_tool_description_fits_and_the_asking_one_carries_the_method(
|
|
served: mcp_server.Surface,
|
|
) -> None:
|
|
by_name = {tool.name: tool for tool in mcp_server.tools(served)}
|
|
for name, tool in by_name.items():
|
|
assert len(tool.description.encode("utf-8")) <= CLIENT_TRUNCATION_BYTES, name
|
|
assert "ask again" in by_name["okf_ask"].description
|
|
assert "withheld" in by_name["okf_ask"].description
|
|
|
|
|
|
def test_describe_without_a_bundle_id_answers_for_every_served_bundle(
|
|
served: mcp_server.Surface,
|
|
) -> None:
|
|
"""It refused instead, where `okf_ask` in the same position fans out.
|
|
|
|
A tool that refuses the call a sibling tool accepts is a shape a client
|
|
has to learn out of band, which is the configuration this server exists to
|
|
remove.
|
|
"""
|
|
result = mcp_server.call_describe(served, {})
|
|
assert sorted(result["asked"]) == ["b-golden-segmented-okf-v0-2", "consume-fixture"]
|
|
cards = result["cards"]
|
|
assert isinstance(cards, list) and len(cards) == 2
|
|
assert sorted(str(card["bundle_id"]) for card in cards) == sorted(result["asked"])
|
|
|
|
|
|
def test_describe_with_a_bundle_id_is_the_card_it_always_was(
|
|
served: mcp_server.Surface,
|
|
) -> None:
|
|
named = mcp_server.call_describe(served, {"bundle_id": "consume-fixture"})
|
|
assert named["bundle_id"] == "consume-fixture"
|
|
assert "cards" not in named
|
|
|
|
|
|
def test_a_one_to_one_server_still_answers_with_its_own_card(tmp_path: Path) -> None:
|
|
surface = mcp_server.Surface(
|
|
roots=(GOLDEN,), fixed="b-golden-segmented-okf-v0-2", profile=okf_consume.DEFAULT_PROFILE
|
|
)
|
|
assert mcp_server.call_describe(surface, {})["bundle_id"] == "b-golden-segmented-okf-v0-2"
|
|
|
|
|
|
def test_the_ask_answer_carries_the_compact_withheld_block(
|
|
served: mcp_server.Surface,
|
|
) -> None:
|
|
"""A2 reaches the MCP arm because the payload is the payload."""
|
|
result = mcp_server.call_ask(served, {"question": "Hva sier veiledningen om krav?"})
|
|
for answer in result["answers"]:
|
|
block = answer["payload"]["withheld"]
|
|
assert set(block) == {"total", "by_rule", "nearest", "complete"}
|
|
|
|
|
|
def test_the_readme_carries_the_one_line_that_registers_the_server() -> None:
|
|
"""The user runs it. This package never starts Claude Code."""
|
|
readme = (PROJECT_ROOT / "README.md").read_text(encoding="utf-8")
|
|
assert "claude mcp add --scope user okf -- okf mcp --root" in readme
|
|
|
|
|
|
def test_the_project_summary_says_what_to_do_next_with_the_server(tmp_path: Path) -> None:
|
|
folder = tmp_path / "Dokumenter"
|
|
folder.mkdir()
|
|
(folder / "krav.md").write_text(
|
|
"## 4 Grunnforhold\n\nGrunnen er morene over berg.\n", encoding="utf-8", newline=""
|
|
)
|
|
_, _, summary = project.create(folder, out=tmp_path / "project")
|
|
assert "claude mcp add --scope user okf -- okf mcp --root" in summary
|
|
|
|
|
|
def test_the_project_summary_puts_the_server_first_and_the_skill_second(tmp_path: Path) -> None:
|
|
"""v1.1 F3: the server is the standard way in, the skill the supplement."""
|
|
folder = tmp_path / "Dokumenter"
|
|
folder.mkdir()
|
|
(folder / "krav.md").write_text(
|
|
"## 4 Grunnforhold\n\nGrunnen er morene over berg.\n", encoding="utf-8", newline=""
|
|
)
|
|
out = tmp_path / "project"
|
|
_, _, summary = project.create(folder, out=out)
|
|
server = summary.index("claude mcp add --scope user okf -- okf mcp --root")
|
|
skill = summary.index(f"start claude in {out}")
|
|
assert server < skill
|
|
assert "standard" in summary[:server]
|
|
assert "supplement" in summary[server:]
|
|
assert "Neither has to be made again when a bundle is rebuilt" in summary
|
|
|
|
|
|
def test_the_readme_first_screen_puts_the_server_first_and_the_skill_second() -> None:
|
|
readme = (PROJECT_ROOT / "README.md").read_text(encoding="utf-8")
|
|
first_screen = readme.split("## Known limitations", 1)[0]
|
|
server = first_screen.index("claude mcp add --scope user okf -- okf mcp --root")
|
|
skill = first_screen.index("The skill is the supplement")
|
|
assert server < skill
|
|
assert "standard way in" in first_screen[:server]
|
|
assert "Neither has to be made again" in first_screen
|
|
|
|
|
|
def test_a_project_bundle_is_where_a_root_server_finds_it(tmp_path: Path) -> None:
|
|
"""`--root <the directory holding projects>` must reach `<project>/.okf/<id>`.
|
|
|
|
Measured rather than reasoned: the walk is bounded at
|
|
`MAX_DISCOVERY_DEPTH`, and `.okf` spends one level of it.
|
|
"""
|
|
projects = tmp_path / "okf"
|
|
folder = projects / "Mitt Prosjekt" / "kilder"
|
|
folder.mkdir(parents=True)
|
|
(folder / "krav.md").write_text(
|
|
"## 4 Grunnforhold\n\nGrunnen er morene over berg.\n", encoding="utf-8", newline=""
|
|
)
|
|
project.create(folder, out=projects / "Mitt Prosjekt", bundle_id="mitt-prosjekt")
|
|
found = mcp_server.discover((projects,))
|
|
assert [served.bundle_id for served in found.bundles] == ["mitt-prosjekt"]
|
|
|
|
|
|
def test_the_skill_command_refuses_a_missing_out_in_its_own_form(tmp_path: Path) -> None:
|
|
"""A5: exit 2 was already right; the TEXT was argparse's, not okf's.
|
|
|
|
Every other refusal in this chain reads `refused (<code>): <what>`, and a
|
|
caller parsing that form got one line that did not match on the one flag
|
|
everybody forgets. The code stays 2 -- "the run did not happen" -- because
|
|
that is what it is.
|
|
"""
|
|
import subprocess
|
|
|
|
result = subprocess.run(
|
|
[sys.executable, "-m", "llm_ingestion_okf.cli", "skill"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
cwd=PROJECT_ROOT,
|
|
)
|
|
assert result.returncode == 2
|
|
assert "refused (out_missing)" in result.stderr
|
|
assert "--out" in result.stderr
|