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:
parent
30edd3f5d8
commit
e3408435d0
5 changed files with 289 additions and 13 deletions
173
tests/test_mcp_entry.py
Normal file
173
tests/test_mcp_entry.py
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue