fix(mcp): match two tool vocabularies to what the tools accept - #958
Merged
Conversation
delegation_history filtered on `profile` and admitted `coder` and `researcher` in three copies: the agent-facing description, the JSON-Schema enum, and the runtime validator. The one tool in this package that submits a delegation record is delegate_ui_audit, and it submits `ui-auditor`, so an agent asking for its UI-audit history got a TypeError and the two profiles it could ask for were written by nothing but tests. `delegationProfiles` is now the one list the description, the schema and the validator read, and DelegationProfile derives from it. QuestionDecision's escalate arm declared `to: 'parent' | 'user' | string`, which collapses to string, while answer_question accepted exactly two targets. Anything else fell through to an error naming the wrong cause. The type narrows to the two the tool accepts, from the same list the schema reads, and an unaccepted escalateTo is refused with a message naming the value. The api-surface record's header gains the third limit this change measured: the digest reads the declaration a build emitted, so restating a type without changing what it means still moves it.
tangletools
approved these changes
Aug 21, 2026
tangletools
left a comment
Contributor
There was a problem hiding this comment.
✅ Auto-approved drewstone PR — d34d5560
This PR was opened by the trusted drewstone account.
This approval is provisional and was applied by the local stand-in because the pr-reviewer webhook host is unreachable (2026-08-21). CI on this head is fully green. The full PR reviewer audit re-runs via the resweep when the service returns and will publish findings if it detects issues.
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.
Problem
Two MCP tools declare a vocabulary that disagrees with what they accept.
1.
delegation_historycannot select the only profile anything writesDelegationProfilehas three members (src/mcp/types.ts:21):coder,researcher,ui-auditor.delegation_historynames two, in three separate copies:tools/delegation-history.ts:36profile ("coder" | "researcher")enumtools/delegation-history.ts:46['coder', 'researcher']tools/delegation-history.ts:68TypeErroron anything elseThere is exactly one
queue.submitcall in the product —tools/delegate-ui-audit.ts:313— and it submitsprofile: 'ui-auditor'.coderandresearcherare written only by tests (tests/mcp/task-queue.test.ts:35,101).So an agent asking "did my last UI audit land?" gets a
TypeError, and the two profiles it is allowed to ask for always return nothing. The root cause is recorded in the repo already:tools/delegate.ts:6says the per-profile delegation tools were replaced by one genericdelegateverb, and the per-profile vocabularies stayed behind.2.
answer_questionadvertises an open escalation target and accepts twoQuestionDecision's escalate arm readsto: 'parent' | 'user' | string(tools/coordination.ts:100). In TypeScript that union collapses tostring, so the type promises any escalation target. The tool's schema (:2267) and its handler guard (:2325) accept exactlyparentanduser. Anything else falls through to:which names the wrong cause: the caller did provide
escalateTo.Change
delegationProfilesis the one list (src/mcp/types.ts), andDelegationProfilederives from it. The description, the schemaenumand the validator all read it, so a profile added there cannot be one a tool refuses. The validator's message names the accepted set instead of spelling two of them.QuestionDecision.escalate.tonarrows to'parent' | 'user', from the same single list the schema enum reads, and a present-but-unacceptedescalateTois refused with a message naming the value and the accepted set.delegationProfiles,questionEscalationTargetsandQuestionEscalationTargetare exported from./mcp. That is not decoration:docs:apirefuses a type referenced by an exported type that is not itself exported, so deriving a public type from a list makes the list public. It is also the useful half — a consumer building a profile filter or an escalation control reads the list instead of restating it.A false positive I hit, and the note it earned
Rewriting
DelegationProfilefrom a spelled-out union to(typeof delegationProfiles)[number]is the same type. #953's shape digest reads the declaration a build emitted, so it moved anyway and asked for a minor bump this change did not strictly owe:The second is a real narrowing. The first is not, and the gate cannot tell — deciding that two differently-written declarations denote one type is the subtyping question that record deliberately does not answer. It errs toward demanding a bump, which is the safe direction, and this is the third limit its header now states. That is a one-sentence addition to
scripts/lib/api-surface.mjs, in this pull request because this is where the case was measured; nothing else in that file changes.Proof
Simplification
Simplification: three copies of the delegation-profile list become one owner that the description, the schema and the validator all read; the escalation target's two copies become one.
Net: +59 / -16 lines across 5 source and test files (plus the version bump's regenerated record, fixtures and generated docs); 3 vocabulary copies removed, 2 lists now have one owner each.
Not done here: whether
coderandresearchershould survive at all. Nothing writes them since the genericdelegateverb replaced the per-profile tools, and the same leftover explainsDelegateResearchArgsandResearchSource— a public-surface deletion that needs a decision, filed as item 8 of #954.Tests: +1 (
delegation_historyaccepts every profile a queued record can carry — today it throws for the only one the product writes), -0 deleted. The escalation change moves only which error text a refused target produces; both paths already threw, so it gets no test — the narrowed type is what makes the disagreement unrepresentable.Refs #954