feat(mcp): the server carries the working method, and describe fans out

**A subagent inherits its session's MCP tools; it does not inherit its skills.**
So the method A3 put in the skill reaches the main thread and no arm running
below it, and the one place every caller sees is the server's own
`instructions` and its tool descriptions. Both are truncated by Claude Code at
2 KB, and truncation is worse than rejection here -- a reader gets the first
half of a method and no sign the rest existed -- so what travels is the SHORT
form and the long one stays in the skill, which has no such cap. A test holds
it under the limit WITH a control, so the assertion is a measurement and not a
tautology.

`okf_describe` without `bundle_id` now describes every served bundle, where it
refused and `okf_ask` in the same position fanned out. The tool a caller is
told to read FIRST was the one requiring a name it did not have yet, and a
tool that refuses the call its sibling accepts is a shape a client must be
told out of band -- the configuration this server exists to remove. The named
call's shape is byte-unchanged, and so is every one-to-one server's: the
fan-out replaces an ERROR, so no caller's bytes move.

`okf project`'s closing lines and the README's first screen carry the one line
the USER runs to register the server on user scope, verified against Claude
Code's own MCP documentation (`claude mcp add [options] <name> -- <command>`).
Nothing here starts Claude Code, and the line says whose it is. Measured:
a project bundle at `<root>/<project>/.okf/<id>` is depth 3, inside
`MAX_DISCOVERY_DEPTH`, so a `--root` server finds what `okf project` wrote --
a test builds one and discovers it rather than reasoning about the walk.

A5, and it was free: `okf skill` without `--out` now refuses in the same
`refused (<code>)` form as every other refusal in this chain. The exit code
does not move -- 2 was already right, "the run did not happen" -- what was
wrong was that a caller parsing our form got argparse's line on the one flag
everybody forgets.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-21 00:03:59 +02:00
commit e3408435d0
5 changed files with 289 additions and 13 deletions

173
tests/test_mcp_entry.py Normal file
View file

@ -0,0 +1,173 @@
"""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_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