Skip to content

Expose hook for embedder-driven MCP elicitation#4934

Open
JAORMX wants to merge 1 commit intomainfrom
worktree-issue-4930-elicitation-hook
Open

Expose hook for embedder-driven MCP elicitation#4934
JAORMX wants to merge 1 commit intomainfrom
worktree-issue-4930-elicitation-hook

Conversation

@JAORMX
Copy link
Copy Markdown
Collaborator

@JAORMX JAORMX commented Apr 20, 2026

Summary

  • Why: Embedders that wrap the vMCP composer in their own pipeline (gateway-level audit, custom elicitation surface, ahead-of-time schema validation) cannot today drive MCP elicitation through the mark3labs *server.MCPServer that actually handles /mcp traffic. The field is unexported, there is no accessor, and the SDK adapter constructor is also unexported. A parallel MCPServer built by the embedder does not work because ClientSession correlation is keyed to the server that received initialize.
  • What: Two additive, read-only seams on pkg/vmcp/server:
    • (*Server).MCPServer() *server.MCPServer returns the authoritative serving instance.
    • NewSDKElicitationAdapter exports the existing adapter constructor so embedders can obtain a composer.SDKElicitationRequester bound to the serving MCPServer.
  • Doc comments spell out the in-process trust boundary, lifecycle ownership (Start/Stop only), the "prefer per-session over global tool registration" footgun, and the MCP 2025-06-18 elicitation caller contract: ctx propagation with a bounded deadline, client capability advertisement, distinct accept/decline/cancel handling, flat-schema shape, and no secrets or internal addressing in prompt payloads.

Closes #4930

Type of change

  • Bug fix
  • New feature
  • Refactoring (no behavior change)
  • Dependency update
  • Documentation
  • Other (describe):

Test plan

  • Unit tests (task test)
  • Linting (task lint-fix)
  • Manual testing (describe below)

Manual verification in the worktree:

  • task lint-fix — 0 issues.
  • go test -count=1 ./pkg/vmcp/server/... — all green, including the new TestServer_MCPServer_ReturnsSameInstance whitebox test that asserts the accessor returns the exact pointer stored at construction.
  • go build ./... — clean.
  • Grep for stale newSDKElicitationAdapter references — none remain.

Does this introduce a user-facing change?

No end-user behavior change. Public API of pkg/vmcp/server gains two exported symbols ((*Server).MCPServer() and NewSDKElicitationAdapter); the addition is purely additive and no existing call site behavior changes.

Implementation plan

Approved implementation plan

Approach

Implement options (a) + (b) from the issue — the two minimal, orthogonal, additive seams.

Rejected alternatives:

  • Option (c) (WithElicitationHandler override): the stated use case is embedders running their own composer pipeline, not replacing toolhive's internal one. (c) would add surface without serving the described need.
  • A NewElicitationRequester(*Server) composer.SDKElicitationRequester that hides the mark3labs type was considered (mitigates anti-pattern Implement secret injection #5 in .claude/rules/vmcp-anti-patterns.md). Rejected because the issue explicitly asks for *MCPServer disclosure and the embedder use case extends beyond elicitation (hooks, notifications, sampling). The doc comment on MCPServer() acknowledges the SDK-type exposure as a deliberate, narrow tradeoff.

Changes

  1. Export NewSDKElicitationAdapter in pkg/vmcp/server/sdk_elicitation_adapter.go (was newSDKElicitationAdapter). Struct stays unexported; only the constructor is public. Doc expanded to direct embedders to (*Server).MCPServer(). One internal caller updated (server.go:344) and one test reference updated.
  2. Add (*Server).MCPServer() *server.MCPServer in pkg/vmcp/server/server.go, placed next to SessionManager(), with a doc comment covering:
    • Intent: same-server correlation for embedder elicitation.
    • Trust boundary: in-process only.
    • Lifecycle: Start/Stop own it; callers MUST NOT close or reconfigure.
    • Tool-registration footgun: prefer per-session over global.
    • MCP 2025-06-18 elicitation prerequisites: bounded-ctx, client capability, accept/decline/cancel, flat schema, no secrets in prompts.

Things NOT changing

  • composer.SDKElicitationRequester / composer.NewDefaultElicitationHandler — already public.
  • Config struct — no new field.
  • Construction path — no behavior change.

Anti-pattern check (.claude/rules/vmcp-anti-patterns.md)

Pre-code reviews

Reviewed by toolhive-expert, security-advisor, mcp-protocol-expert, and Plan agents in parallel before any code was written. Findings (em-dash removal in docs, protocol-caller notes, test name and redundancy) folded into the final doc comments and test.

Post-code reviews

Reviewed by code-reviewer, toolhive-expert, mcp-protocol-expert, security-advisor, unit-test-writer, and the vmcp-review skill in parallel on the diff. All findings addressed; no blocking issues remain.

🤖 Generated with Claude Code

@github-actions github-actions Bot added the size/XS Extra small PR: < 100 lines changed label Apr 20, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 20, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 69.74%. Comparing base (68f4c2f) to head (51850ff).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4934      +/-   ##
==========================================
- Coverage   69.76%   69.74%   -0.02%     
==========================================
  Files         560      560              
  Lines       56475    56477       +2     
==========================================
- Hits        39397    39391       -6     
- Misses      14055    14063       +8     
  Partials     3023     3023              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Embedders that wrap the vMCP composer in their own pipeline cannot
today target elicitation at the mark3labs MCPServer instance that
actually serves /mcp: the field is unexported, there is no accessor,
and the SDK adapter constructor is also unexported. A parallel
MCPServer built by the embedder does not work because ClientSession
correlation is keyed to the server that received initialize.

Add two additive, read-only seams on pkg/vmcp/server:

  - (*Server).MCPServer() returns the authoritative serving instance.
  - NewSDKElicitationAdapter exports the existing adapter constructor
    so embedders can obtain a composer.SDKElicitationRequester bound
    to the serving MCPServer.

Doc comments spell out the in-process trust boundary, lifecycle
ownership, the "prefer per-session over global tool registration"
footgun, and MCP 2025-06-18 elicitation caller obligations (ctx
propagation with a bounded deadline, elicitation capability on the
client, distinct accept/decline/cancel handling, flat-schema shape,
no secrets or internal addressing in prompts).

Closes #4930

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@JAORMX JAORMX force-pushed the worktree-issue-4930-elicitation-hook branch from 23b3bb5 to 51850ff Compare April 24, 2026 12:37
@JAORMX
Copy link
Copy Markdown
Collaborator Author

JAORMX commented Apr 24, 2026

Had to rebase

@github-actions github-actions Bot added size/XS Extra small PR: < 100 lines changed and removed size/XS Extra small PR: < 100 lines changed labels Apr 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/XS Extra small PR: < 100 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

vmcp/server: expose a hook for embedders to drive MCP elicitation

2 participants