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
|
|
@ -731,7 +731,10 @@ def parse_args(argv: list[str] | None) -> argparse.Namespace:
|
|||
help="the OKF bundle to instantiate a skill for. Only read with --for-bundle",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--out", type=Path, required=True, help="the skill directory to write (SKILL.md inside)"
|
||||
"--out",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="the skill directory to write (SKILL.md inside). Required",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--example-question",
|
||||
|
|
@ -764,6 +767,20 @@ def parse_args(argv: list[str] | None) -> argparse.Namespace:
|
|||
def main(argv: list[str] | None = None) -> int:
|
||||
args = parse_args(argv)
|
||||
try:
|
||||
# Checked here rather than by `required=True`, so the one flag
|
||||
# everybody forgets refuses in the same `refused (<code>)` form every
|
||||
# other refusal in this chain uses. The CODE is 2 either way -- "the
|
||||
# run did not happen" -- which is what argparse already gave; what was
|
||||
# wrong was that a caller parsing our form got one line that did not
|
||||
# match.
|
||||
if args.out is None:
|
||||
print(
|
||||
"refused (out_missing): name the skill directory with --out; "
|
||||
"there is no default, because writing a skill into the current "
|
||||
"directory is not a place anyone asked for",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 2
|
||||
if args.for_bundle and args.bundle is None:
|
||||
print("refused (bundle_missing): --for-bundle needs a bundle", file=sys.stderr)
|
||||
return 2
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue