fix(mcp): Align tool ergonomics with real agent call patterns - #25
Merged
Conversation
Forensics (CTX-0048, note PX-0079) ranked recurring agent call failures
on the MCP surface; this implements all six commander decisions:
- pinned_path: absolute paths are now legal when they normalize+resolve
inside the pinned workspace root (existing targets symlink-resolved as
before); only escapes are rejected. Relative behavior unchanged.
(83/83 historical calls used absolute paths.)
- ctxctl_read: lines is optional — omitted or empty yields the whole
file, mirroring CLI open-ended-range semantics; inputSchema updated.
- validate_arguments: benign numeric coercion — a JSON number for a
string-typed argument becomes its decimal string form (lines:100 ->
"100", single-line meaning preserved). bool/object/array/null still
rejected naming key, expected type, and got type.
- unknown tool errors enumerate the registry: 'unknown tool `X`;
available: ...' from a single alphabetical TOOL_TABLE source.
- missing-argument errors append a usage example built from that tool's
argument table so it can never drift from the schema.
- unknown-argument errors list the valid keys.
Also: ctxctl_symbol not-found errors gain up to 3 case-insensitive
prefix-then-substring suggestions ('; similar: A, B') from the file's
actual symbols; silent when none.
JSON-RPC framing and success-path payloads stay byte-compatible; only
error paths, validation, and schema descriptions change. The CLI keeps
unrestricted local paths by design.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements all six commander decisions from task CTX-0047, closing the ranked friction inventory established by forensics (CTX-0048, note PX-0079). Only error paths, validation, and schema descriptions change; JSON-RPC framing and success-path payloads remain byte-compatible. The CLI keeps unrestricted local paths by design (local-trusted vs MCP remote-distrusted).
Forensics evidence
pinned_pathrejected every one of them outright (absolute paths are not allowed), making it the single biggest recurring breaker.lineson whole-file reads, numericlines: 100arguments rejected by validation, unhelpfulunknown tool: X/ bare missing-argument errors forcing schema round-trips, and dead-endsymbol not found: Xtypos.Decisions implemented
pinned_path): absolute input is lexically normalized (.dropped,..applied without touching the filesystem); existing targets are additionally symlink-resolved against the canonical root exactly like before. Accepted paths are returned normalized-but-not-canonicalized so handler not-found diagnostics are preserved. Escapes (lexical or symlinked) are still rejected with the samepath escapes workspace roottext. Relative behavior unchanged.linesoptional on read: omitted or empty string now yields the whole file (open-ended range semantics).inputSchema.requireddrops to["file"]; the description documents"N","N-M","N-", comma lists, and omitted/empty = whole file. Note:-M(from start of file) is not documented because the shared range parser does not support it and the CLI must stay untouched.validate_arguments: a JSON number for a string-typed argument coerces to its decimal string form (lines: 100→"100", keeping single-line meaning per P03). Bool/object/array/null are still rejected, now naming key, expected type, and got type (argument \lines` must be a string, got boolean`). Explicit null stays absent-as-before.unknown tool `X`; available: ctxctl_deps, ctxctl_exec, ctxctl_outline, ctxctl_read, ctxctl_symbol— derived from a single alphabeticalTOOL_TABLEthat also feeds validation, hints, and suggestions, so it cannot drift.expected: {"file": "<workspace-relative or absolute path under workspace>", "no_doc": <boolean>, "no_lines": <boolean>}.unknown argument `encoding`; valid arguments: file, lines.Plus:
ctxctl_symbolnot-found errors append up to 3 case-insensitive prefix-then-substring suggestions from the file's actual symbols (; similar: NodeHandle, NodeState), silently skipped when there are none.Test coverage
Unit (
mcp.rstests module) + integration (tests/mcp_test.rs), mirroring the adapted P00-P10 matrix:lines→ success, whole filelines: 100-style numeric argument reads that line (was a rejection test; expectations updated to new design)..escapes with no content leakrequired == ["file"], documented formatsAll existing tests kept green; only assertions pinning the removed "absolute paths are not allowed" message were minimally rewritten to the new message content.
Gates:
cargo fmt --check✓ ·cargo clippy --all-features -- -D warnings✓ ·cargo test --workspace✓ (278 tests)