Domain-type the ElicitationRequester seam - #5456
Conversation
Replace composer.SDKElicitationRequester, which leaked mcp-go's ElicitationRequest/ElicitationResult across the soon-to-be VMCP boundary, with a domain-typed vmcp.ElicitationRequester interface and ElicitationRequest/ElicitationResult value types that carry no mcp-go types. This confines all mcp-go translation to the sanctioned adapter (server/sdk_elicitation_adapter.go), closing anti-pattern #5 on the elicitation path ahead of New(cfg) -> VMCP (#5437). Implements changes for issue #5436: - Add pkg/vmcp/elicitation.go with the domain interface and value types (form-mode only; no URL-mode fields) - Retype DefaultElicitationHandler to the domain requester; security validation and the Content any->map assertion are unchanged - Make sdkElicitationAdapter the sole domain <-> mcp-go translator, with an internal SDK seam so translation is unit-testable; copy Meta before NewMetaFromMap mutates it and omit _meta when nil - Regenerate mocks under pkg/vmcp/mocks; drop composer/mocks
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #5456 +/- ##
=======================================
Coverage 68.85% 68.86%
=======================================
Files 634 634
Lines 64439 64471 +32
=======================================
+ Hits 44370 44398 +28
- Misses 16789 16794 +5
+ Partials 3280 3279 -1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
tgrunnagle
left a comment
There was a problem hiding this comment.
Automated multi-agent review
Recommendation: COMMENT — clean, well-scoped R4 bounded rewrite. The production change is faithful and low-risk; the only actionable items are two issue-mandated test-strength gaps (non-blocking).
Summary
Replaces the mcp-go–typed composer.SDKElicitationRequester with a domain-typed vmcp.ElicitationRequester + value types carrying no SDK types, and confines all mcp-go translation to the sole adapter in pkg/vmcp/server. Verified against the field-mapping table in #5436 (mcp-go v0.54.0): mapping is one-to-one, URL-mode fields correctly omitted, error returned unwrapped so errors.Is(context.DeadlineExceeded) still fires, and maps.Clone(req.Meta) correctly guards the caller's map against NewMetaFromMap's in-place progressToken delete.
What's strong
- Dependency direction improves: adapter now depends on
pkg/vmcp(domain) instead ofpkg/vmcp/composer. - Security validation (timeout cap/default, schema size/depth, content size/depth) and the
any → map[string]anyassertion are genuinely untouched and remain on-path in unchanged order. - The internal
mcpElicitationRequestertest seam is private, single-method, and adapter-local — does not trip anti-pattern #8. - Corrects a stale doc comment (
max 1hour→max 10min).
Findings (2 MEDIUM, both test-strength; non-blocking)
See inline comments. Both are edge cases the #5436 Testing Strategy explicitly lists as required.
Consensus summary
| # | Category | Severity | Finding | Score |
|---|---|---|---|---|
| 1 | Test Coverage | MEDIUM | Issue-required "non-map Content → nil" edge case is untested |
8/10 |
| 2 | Test Coverage | MEDIUM | timeout_capped_to_max asserts only no-error; would pass even if cap logic were deleted |
8/10 |
Dropped (score < 7, FYI): mockSDK test vars now hold a domain mock (rename → mockRequester); MetaIsCopied doesn't assert the non-progressToken key survives; no handler-level RequestedSchema mapping assertion; Meta doc comment doesn't note progressToken is reserved.
5 specialist agents (architecture, MCP protocol, security, Go quality, tests). Codex cross-review skipped (CLI not installed).
Addresses #5456 review comments: - MEDIUM elicitation_handler_test.go (3357216826): add accept case with non-map Content asserting it is treated as nil, covering the warn-log else branch the #5436 Testing Strategy requires - MEDIUM elicitation_handler_test.go (3357216877): replace the weak timeout_capped_to_max table case with a dedicated test that captures the context deadline and asserts it is capped to maxElicitationTimeout, so the timeout-bomb protection is actually guarded
Domain-type the ElicitationRequester seam
Closes #5436
Summary
The composer's elicitation seam (
SDKElicitationRequester) leaked mcp-go'smcp.ElicitationRequest/mcp.ElicitationResulttypes across what is about to become theVMCPdomain boundary. This is bounded rewrite R4: before the core constructor (New(cfg) -> VMCP, #5437) is introduced, that SDK coupling must be confined to the adapter so it does not cross the new boundary (vMCP anti-pattern #5). This PR replaces the SDK-typed interface with a domain-typedvmcp.ElicitationRequesterplusElicitationRequest/ElicitationResultvalue types that carry no mcp-go types, and moves all mcp-go translation into the sole adapter inpkg/vmcp/server.This is a one-to-one field mirror of the mcp-go shapes the composer already uses — not a redesign. Request/response semantics, security validation, defaults, and error transforms are unchanged.
Changes Made
pkg/vmcp(new domain seam)pkg/vmcp/elicitation.godefining the domainElicitationRequesterinterface and theElicitationRequest/ElicitationResultvalue types, with no mcp-go imports. URL-mode fields (ElicitationID/URL/Mode) are deliberately omitted because the composer never constructs them.go:generatedirective and regenerated mock atpkg/vmcp/mocks/mock_elicitation_requester.go.pkg/vmcp/composerDefaultElicitationHandlerandNewDefaultElicitationHandlerfromSDKElicitationRequesterto the domainvmcp.ElicitationRequester; deleted the old SDK-typed interface.RequestElicitationnow builds a domainElicitationRequestand reads a domainElicitationResult. All security validation (timeout cap, schema size/depth, content size) and theContentany -> map[string]anyassertion are unchanged.github.com/mark3labs/mcp-go/mcpimport from the elicitation path;package composerno longer imports mcp-go for elicitation (verified — no remaining mcp-go references in the package).mocks/mock_sdk_elicitation_requester.go).pkg/vmcp/serversdkElicitationAdapternow satisfiesvmcp.ElicitationRequesterand is the sole point translating domain types to/from mcp-go: domain request ->mcp.ElicitationParams, andmcp.ElicitationResult-> domain result (string(Action),Contentpass-through).NewSDKElicitationAdapterreturn type changed fromcomposer.SDKElicitationRequestertovmcp.ElicitationRequester.Implementation Details
mcpElicitationRequesterseam (justRequestElicitation) instead of*server.MCPServerdirectly, so the domain ⇄ mcp-go translation can be unit-tested against a fake SDK without a live session.*server.MCPServersatisfies it in production.Metais only attached when the caller actually sets it. Becausemcp.NewMetaFromMapmutates its argument (it deletesprogressToken), the adapter clones the map first (maps.Clone) to avoid mutating caller-provided data, per the copy-before-mutate convention.ElicitationResult.Contentstays typed asanyso the composer's existingmap[string]anyassertion and validation behave byte-for-byte as before.Testing
mcp.ElicitationParams;mcp.ElicitationResultmaps back withActionstring-converted andContentpreserved. Tests exercise the new fake-SDK seam.ElicitationResponsefor accept/decline/cancel/timeout.task testpasses;task lint-fixclean; mocks regenerated viatask gen.Additional Notes
server.New's signature and the SDK-backed elicitation flow embedders use via(*Server).MCPServer()are unchanged.New(cfg) -> VMCPcore constructor), which depends on the domainElicitationRequester.composer.ElicitationProtocolHandler/ElicitationResponse/ElicitationConfigpublic shapes.