Skip to content

Domain-type the ElicitationRequester seam - #5456

Merged
tgrunnagle merged 2 commits into
mainfrom
vmcp-core-p1-3_issue_5436
Jun 5, 2026
Merged

Domain-type the ElicitationRequester seam#5456
tgrunnagle merged 2 commits into
mainfrom
vmcp-core-p1-3_issue_5436

Conversation

@tgrunnagle

Copy link
Copy Markdown
Contributor

Domain-type the ElicitationRequester seam

Closes #5436

Summary

The composer's elicitation seam (SDKElicitationRequester) leaked mcp-go's mcp.ElicitationRequest/mcp.ElicitationResult types across what is about to become the VMCP domain 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-typed vmcp.ElicitationRequester plus ElicitationRequest/ElicitationResult value types that carry no mcp-go types, and moves all mcp-go translation into the sole adapter in pkg/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)

  • Added pkg/vmcp/elicitation.go defining the domain ElicitationRequester interface and the ElicitationRequest/ElicitationResult value types, with no mcp-go imports. URL-mode fields (ElicitationID/URL/Mode) are deliberately omitted because the composer never constructs them.
  • Added the go:generate directive and regenerated mock at pkg/vmcp/mocks/mock_elicitation_requester.go.

pkg/vmcp/composer

  • Retyped DefaultElicitationHandler and NewDefaultElicitationHandler from SDKElicitationRequester to the domain vmcp.ElicitationRequester; deleted the old SDK-typed interface.
  • RequestElicitation now builds a domain ElicitationRequest and reads a domain ElicitationResult. All security validation (timeout cap, schema size/depth, content size) and the Content any -> map[string]any assertion are unchanged.
  • Removed the github.com/mark3labs/mcp-go/mcp import from the elicitation path; package composer no longer imports mcp-go for elicitation (verified — no remaining mcp-go references in the package).
  • Removed the obsolete composer mock (mocks/mock_sdk_elicitation_requester.go).

pkg/vmcp/server

  • sdkElicitationAdapter now satisfies vmcp.ElicitationRequester and is the sole point translating domain types to/from mcp-go: domain request -> mcp.ElicitationParams, and mcp.ElicitationResult -> domain result (string(Action), Content pass-through).
  • NewSDKElicitationAdapter return type changed from composer.SDKElicitationRequester to vmcp.ElicitationRequester.

Implementation Details

  • The adapter now depends on a minimal internal mcpElicitationRequester seam (just RequestElicitation) instead of *server.MCPServer directly, so the domain ⇄ mcp-go translation can be unit-tested against a fake SDK without a live session. *server.MCPServer satisfies it in production.
  • Meta is only attached when the caller actually sets it. Because mcp.NewMetaFromMap mutates its argument (it deletes progressToken), the adapter clones the map first (maps.Clone) to avoid mutating caller-provided data, per the copy-before-mutate convention.
  • ElicitationResult.Content stays typed as any so the composer's existing map[string]any assertion and validation behave byte-for-byte as before.

Testing

  • Adapter translation tests: domain request maps to the equivalent mcp.ElicitationParams; mcp.ElicitationResult maps back with Action string-converted and Content preserved. Tests exercise the new fake-SDK seam.
  • Composer handler and integration tests updated to drive elicitation through the domain requester (via the regenerated mock) and assert the unchanged ElicitationResponse for accept/decline/cancel/timeout.
  • Security validation parity confirmed through the domain seam: schema size, schema depth, response content size limits, and the timeout cap.
  • task test passes; task lint-fix clean; mocks regenerated via task gen.

Additional Notes

  • No public API change: server.New's signature and the SDK-backed elicitation flow embedders use via (*Server).MCPServer() are unchanged.
  • Unblocks P1.4 New(cfg) -> VMCP core constructor #5437 (New(cfg) -> VMCP core constructor), which depends on the domain ElicitationRequester.
  • Out of scope (unchanged): URL-mode elicitation, security validation logic/defaults/error types, and the composer.ElicitationProtocolHandler/ElicitationResponse/ElicitationConfig public shapes.

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
@github-actions github-actions Bot added the size/L Large PR: 600-999 lines changed label Jun 4, 2026
@codecov

codecov Bot commented Jun 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 68.86%. Comparing base (255686a) to head (d988921).
⚠️ Report is 3 commits behind head on main.

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.
📢 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.

@tgrunnagle tgrunnagle left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 of pkg/vmcp/composer.
  • Security validation (timeout cap/default, schema size/depth, content size/depth) and the any → map[string]any assertion are genuinely untouched and remain on-path in unchanged order.
  • The internal mcpElicitationRequester test seam is private, single-method, and adapter-local — does not trip anti-pattern #8.
  • Corrects a stale doc comment (max 1hourmax 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).

Comment thread pkg/vmcp/composer/elicitation_handler_test.go
Comment thread pkg/vmcp/composer/elicitation_handler_test.go
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
@tgrunnagle
tgrunnagle marked this pull request as ready for review June 4, 2026 16:06
@github-actions github-actions Bot added size/L Large PR: 600-999 lines changed and removed size/L Large PR: 600-999 lines changed labels Jun 4, 2026
@tgrunnagle
tgrunnagle merged commit 9968da8 into main Jun 5, 2026
45 checks passed
@tgrunnagle
tgrunnagle deleted the vmcp-core-p1-3_issue_5436 branch June 5, 2026 15:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/L Large PR: 600-999 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

P1.3 Domain-typed ElicitationRequester (bounded rewrite)

2 participants