Skip to content

refactor(mcp)!: make elicitation the only "prompt" consent channel - #73

Merged
V3RON merged 2 commits into
mainfrom
refactor/drop-requires-user-interaction
Sep 21, 2026
Merged

V3RON merged 2 commits into
mainfrom
refactor/drop-requires-user-interaction

Conversation

@V3RON

@V3RON V3RON commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Removes the Claude Code-specific consent fallback for "prompt"-policy tools. MCP elicitation is now the only way to satisfy "prompt". If the client doesn't declare the elicitation capability, the call is denied.

This is the first step toward MCP discovery through a list/describe/call set of built-in tools, with app tools no longer listed one-to-one as MCP tools. The per-tool _meta flag can't survive that change, but elicitation works through a generic call tool.

What changed

  • MCP server: removed the requiresUserInteraction channel: the claude-code ≥ 2.1.199 clientInfo check, the per-connection emittedRequiresUserInteraction bookkeeping, and the _meta emission in tools/list. resolveToolCallConsent now either asks via elicitation or returns no consent.
  • Tool mapper: McpToolMapper no longer takes the emitRequiresUserInteractionFlag argument, and McpToolSchema drops _meta.
  • Daemon and shared types:
    • consent is now typed "elicitation" only.
    • A legacy consent: "client" from an older MCP server is accepted and ignored. The call then lands on the audited policy_denied/no_consent_channel denial. Any other unknown value is still invalid_request.
    • The no_consent_channel hint no longer mentions Claude Code.
  • Rejection behaviour: the daemon's existing "prompt" gate denies the call, including across mixed CLI and daemon versions. The MCP server returns the denial as a recoverable isError: true result that names the tool and says how to change its policy.
  • Docs:
    • ARCHITECTURE.md §1, §5, §9, §12 and §14 and SECURITY.md now describe one consent channel.
    • Both now also cover clients that answer elicitations automatically: Claude Code's Elicitation hook accepts, claude -p cancels.
    • CHANGELOG.md has a breaking-change entry.

Breaking change

A client that relied on the flag without supporting elicitation now gets policy_denied for "prompt" tools. Current Claude Code (checked with 2.1.278) declares elicitation, so it gets the elicitation prompt instead and isn't affected. I haven't checked whether any released version between 2.1.199 and 2.1.278 lacked elicitation.

To fix it:

  • Use a client that supports elicitation.
  • Or set the tool's policy to "allow". That removes the gate for every caller, including the CLI. It only takes effect after a daemon restart (appduct daemon stop), because config.json is read once at daemon start.

@appduct/shared's ToolsCallParams.consent and the audit consent field narrow to "elicitation". Existing audit files may still contain consent: "client", and nothing reads those files back.

Review

An adversarial review (fresh context, given only this PR) found no consent bypass: every path fails closed. Its findings were:

# Finding Resolution
1 A newer daemon rejected an older MCP server's consent: "client" as invalid_request, which skipped the audit log Fixed: "client" is treated as no consent, giving an audited no_consent_channel denial. Tested.
2 Docs claimed elicitation always reaches a human Fixed: SECURITY.md and ARCHITECTURE.md now document auto-accept (Elicitation hook) and auto-cancel (claude -p)
3 The breaking change affects fewer clients than stated Fixed: CHANGELOG and this description now say so
4 The "allow" workaround needs a daemon restart and applies to every caller Fixed: documented in the CHANGELOG and here
5 Lost coverage for consent on a non-ok outcome Fixed: new test where a call accepted via elicitation then errors still audits consent: "elicitation"
6 The published @appduct/shared types changed without a CHANGELOG note Fixed: added to the CHANGELOG
7 Stale comments Fixed

Testing

  • Integration tests in policy-and-audit.integration.test.ts, 24 passing:
    • A claude-code 2.1.199 client without elicitation gets no _meta. Its "prompt" call is denied with no_consent_channel, no tool_call frame reaches the app, and the audit record has no consent.
    • A legacy consent: "client" is denied and audited as no_consent_channel.
    • An unknown consent value is invalid_request.
    • An elicitation-accepted call that then errors audits consent: "elicitation".
    • The elicitation, CLI-denial, deny and allow tests are kept.
  • Full suite: pnpm turbo run test --filter=appduct --filter=@appduct/shared passes after the review fixes: shared 272, appduct 777 plus 1 skipped.
  • Docs: pnpm check:links passes.

Drop the Claude Code-specific fallback that emitted
_meta["anthropic/requiresUserInteraction"] on tools/list and sent
consent: "client" on tools.call. A "prompt"-policy call from an MCP
client that doesn't declare the elicitation capability is now denied
with policy_denied (no_consent_channel), like the CLI.

The daemon rejects consent: "client" as invalid_request, and the
tool mapper no longer takes the emit-flag argument.

BREAKING CHANGE: Claude Code clients without elicitation support can no
longer call "prompt"-policy tools over MCP; set the tool's policy to
"allow" in config.json to keep it callable.
…lid_request

Address review of #73: an older MCP server talking to a newer daemon
now lands on the audited no_consent_channel denial. Restore coverage
for consent on the error outcome, and document elicitation auto-accept,
the daemon restart the "allow" workaround needs, and the narrowed
@appduct/shared types.
@V3RON
V3RON added this pull request to stack #76 September 21, 2026 12:04
@V3RON
V3RON merged commit 618b792 into main Sep 21, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant