Implement stateless core VMCP constructor#5457
Conversation
There was a problem hiding this comment.
Large PR Detected
This PR exceeds 1000 lines of changes and requires justification before it can be reviewed.
How to unblock this PR:
Add a section to your PR description with the following format:
## Large PR Justification
[Explain why this PR must be large, such as:]
- Generated code that cannot be split
- Large refactoring that must be atomic
- Multiple related changes that would break if separated
- Migration or data transformationAlternative:
Consider splitting this PR into smaller, focused changes (< 1000 lines each) for easier review and reduced risk.
See our Contributing Guidelines for more details.
This review will be automatically dismissed once you add the justification section.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #5457 +/- ##
==========================================
+ Coverage 69.17% 69.22% +0.05%
==========================================
Files 634 636 +2
Lines 64473 64724 +251
==========================================
+ Hits 44596 44803 +207
- Misses 16577 16614 +37
- Partials 3300 3307 +7 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
tgrunnagle
left a comment
There was a problem hiding this comment.
🔍 Multi-agent review — Phase 1 core VMCP constructor
Verdict: COMMENT. Faithful, well-executed "relocate, don't rewrite" refactor; safe to merge as additive code (core not yet wired into the request path). One HIGH-severity latent parity defect and two MEDIUM/LOW test-coverage gaps warrant attention before the core is wired in (Phase 3). Reviewed by 5 specialist agents (general quality, architecture/parity, concurrency, security, tests). Codex cross-review skipped (CLI not installed).
Consensus findings
| # | Finding | Location | Severity | Score |
|---|---|---|---|---|
| F1 | CallTool composite branch lacks the conflict gate ListTools applies → executes a withheld composite on name conflict (legacy routes to backend) |
core_calls.go:47 |
HIGH (latent) | 9 |
| F2 | C2 filterHealthyBackends copy drops the legacy "source" debug-log field |
core_vmcp.go:399 |
LOW | 8 |
| F3 | Workflow-level telemetry instruments not carried into composerFactory — confirm Serve-layer deferral |
core_vmcp.go:142 |
LOW | 7 |
| F4 | stopStateStore silent interface{ Stop() } assertion; a 2nd store impl lacks Stop() (latent leak) |
core_vmcp.go:337 |
LOW | 7 |
| F5 | TestIdentityNotLogged covers 2 of ~8 paths; auth.Identity has no slog.LogValuer |
core_vmcp_test.go:393 |
LOW | 7 |
| F6 | Missing issue-mandated capability-set parity test vs ProcessPreQueriedCapabilities |
core_vmcp_test.go:158 |
MEDIUM | 7 |
| F7 | executeComposite json.Marshal-failure branch (6th) untested |
core_calls_test.go:260 |
LOW | 6 |
| F8 | GetPrompt copy-before-mutate unverified (asymmetric with CallTool) |
core_calls_test.go:178 |
LOW | 6 |
| F9 | No goroutine-leak guard for the constructor error-path | core_vmcp_test.go:116 |
LOW | 6 |
Holistic notes
- Strengths: state-store constants, telemetry-before-engine ordering,
executeCompositeerror semantics, andfilterHealthyBackendsinclude/exclude rules all match legacy; comment line-citations verified accurate; clean dependency direction (no cycle); no mcp-go type crosses the boundary; identity never logged.Close()actually fixes a pre-existing goroutine leak in the relocatedserver.Newcode. - F1 is the one to weigh: it contradicts the "no behavior change" label and the issue's parity criteria. Not reachable in production from this PR (additive), so non-blocking — but fix it or add an explicit tracking note before Phase 3 wires the core in.
- Docs: no
task docsneeded. Minor: the PR "Changes" table listselicitation.go/composer//sdk_elicitation_adapter*.go, which are not in this diff (they're in the stacked base#5436).
Inline comments below map 1:1 to the table. REQUEST_CHANGES was intentionally not auto-set — set it yourself if you'd prefer to block on F1.
CallTool's composite branch only applied the accessibility filter, so on a composite/backend tool-name collision it executed the withheld composite while ListTools advertised the backend tool (advertise-vs-execute divergence). Extract a shared accessibleComposites helper used by both advertisedTools and CallTool: on a conflict all composites are dropped and the name falls through to backend routing, so advertised == executed, matching the legacy decorator. Addresses #5457 review comments: - HIGH core_calls.go:47 (3358054909): apply the conflict gate to composite execution so a name collision routes to the backend, not the composite
Addresses #5457 review comments: - LOW core_vmcp.go:399 (3358054913): restore the "source" (health_monitor| registry) field on the backend-exclusion debug log to match the legacy filter - LOW core_vmcp.go:142 (3358054917): note that workflow-level telemetry wrapping is intentionally deferred to the session/Serve layer (#5439), not a regression - LOW core_vmcp.go:337 (3358054921): capture the state-store stopper at construction and warn loudly if absent, replacing the silent Close-time capability assertion (go-style: no silent no-ops)
Addresses #5457 review comments: - LOW core_vmcp_test.go:393 (3358054924): drive all identity-taking methods in TestIdentityNotLogged and assert the log buffer is non-empty so the check is not vacuous - LOW core_calls_test.go:260 (3358054931): cover executeComposite's json.Marshal-failure branch (unserializable Output) - LOW core_calls_test.go:178 (3358054936): add TestGetPrompt_CopyBeforeMutate asserting GetPrompt does not mutate the caller's args
Relocate the domain wiring buried in server.New (telemetry decoration, workflow auditor, in-memory state store + workflow engine, per-session composer factory, fail-fast workflow validation) into a working New(cfg) (VMCP, error) in pkg/vmcp/core, turning the #5434 contract into an identity-parameterized core that aggregates capabilities, routes calls, and drives composite workflows. Implements #5437: - All ten VMCP methods on *coreVMCP. The core is stateless w.r.t. sessions: List/Lookup/Call health-filter the registry and aggregate on demand, then route through a per-call SessionRouter bound to the fresh routing table (removing context-injected capability coupling). "The core filters, Serve caches." - Injected health.StatusProvider drives filterHealthyBackends before aggregation, preserving today's exact include/exclude rules (copied from discovery/middleware.go; temporary C2 duplication until Phase 3). - Composite tools are advertised in ListTools (decorators may only subtract) and executed in CallTool via the relocated per-session composer pattern. - Close() stops the state store cleanup goroutine and is idempotent. - Relocate the backend-client telemetry decorator to pkg/vmcp/internal/backendtelemetry so server and core share it without an import cycle; server.New behavior is unchanged. Identity is an explicit *auth.Identity on every method, never read from context and never logged. The Cedar admission seam (#5438) is not wired here. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixed issues from code review: - HIGH: ReadResource/GetPrompt now pass the advertised name straight to the backend client (which owns the single name translation), matching CallTool and removing the fragile reliance on idempotent re-translation - MEDIUM: add TestCallTool_ResolvesRenamedTool (dot-convention/prefixed name resolution) and table-driven TestExecuteComposite covering the error/timeout/nil-result/result.Error branches - MEDIUM: clarify that core aggregation is intentionally uncached (Serve caches) and that validateWorkflowDefs returns nil-when-empty by design Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CallTool's composite branch only applied the accessibility filter, so on a composite/backend tool-name collision it executed the withheld composite while ListTools advertised the backend tool (advertise-vs-execute divergence). Extract a shared accessibleComposites helper used by both advertisedTools and CallTool: on a conflict all composites are dropped and the name falls through to backend routing, so advertised == executed, matching the legacy decorator. Addresses #5457 review comments: - HIGH core_calls.go:47 (3358054909): apply the conflict gate to composite execution so a name collision routes to the backend, not the composite
Addresses #5457 review comments: - LOW core_vmcp.go:399 (3358054913): restore the "source" (health_monitor| registry) field on the backend-exclusion debug log to match the legacy filter - LOW core_vmcp.go:142 (3358054917): note that workflow-level telemetry wrapping is intentionally deferred to the session/Serve layer (#5439), not a regression - LOW core_vmcp.go:337 (3358054921): capture the state-store stopper at construction and warn loudly if absent, replacing the silent Close-time capability assertion (go-style: no silent no-ops)
Addresses #5457 review comments: - LOW core_vmcp_test.go:393 (3358054924): drive all identity-taking methods in TestIdentityNotLogged and assert the log buffer is non-empty so the check is not vacuous - LOW core_calls_test.go:260 (3358054931): cover executeComposite's json.Marshal-failure branch (unserializable Output) - LOW core_calls_test.go:178 (3358054936): add TestGetPrompt_CopyBeforeMutate asserting GetPrompt does not mutate the caller's args
3d6bcd2 to
7dc752c
Compare
Large PR justification has been provided. Thank you!
|
✅ Large PR justification has been provided. The size review has been dismissed and this PR can now proceed with normal review. |
A core built with a nil Elicitation produced a non-nil DefaultElicitationHandler that would nil-deref the moment a workflow reached an elicitation step — a new failure mode versus the legacy server.New path, which always wired a non-nil adapter. Add defense in depth: - composer.DefaultElicitationHandler.RequestElicitation returns the new ErrElicitationNotConfigured instead of dereferencing a nil requester, so every caller fails cleanly. - core.New rejects construction when Elicitation is nil and any workflow contains an elicitation step (directly or as a forEach inner step), surfacing the misconfiguration at startup per the documented contract. Addresses #5457 review comments: - blocker core_vmcp.go:134 (3365728160): nil cfg.Elicitation would nil-deref on the first elicitation step
Summary
RFC Phase 1 of the vMCP refactor (epic #5419, story #5430) extracts a stateless, identity-explicit domain core out of the
server.Newgod-object. This turns theVMCPcontract from #5434 into a working core that aggregates backend capabilities, routes calls, and drives composite-tool workflows — without mcp-go types crossing the boundary and without changingserver.New's signature or observable behavior.server.New's body, coupling the domain logic to the transport god-object and to context-injected capabilities. Phase 1 relocates that wiring behind a clean, identity-parameterized contract so later phases can re-home the transport (Serve, P2.1 Serve skeleton + ServerConfig #5439) and wire the Cedar admission seam (P1.5 Core admission seam (bounded rewrite) #5438) without untangling it again.pkg/vmcp/coreimplements*coreVMCPandNew(cfg *Config) (VMCP, error)with all 10 interface methods, relocating (not rewriting) the domain wiring fromserver.New: telemetry backend-client decoration, the optional workflow auditor, the in-memory state store + workflow engine, the per-call composer factory, and fail-fast workflow validation.List/Lookup/Callhealth-filter the registry and aggregate on demand, then route through a per-callrouter.NewSessionRouterbound to the freshly aggregated routing table. This removes composite tools' dependency on context-injected capabilities (vmcp anti-pattern fix(typo): corrects readme #1). The core filters;Servecaches (a later decorator).health.StatusProviderdrivesfilterHealthyBackendsbefore aggregation, preserving today's exact include/exclude rules (healthy/degraded/empty included; unhealthy/unknown/unauthenticated excluded). A nil provider means no filtering (all backends included), matching today's no-monitor behavior.ListTools(the core is the only layer that can add them, since decorators may only subtract reachability) and executed inCallToolthrough the relocated per-session composer pattern.Close()stops the state-store cleanup goroutine and is idempotent (sync.Once).pkg/vmcp/server/telemetry.goto a new shared leaf packagepkg/vmcp/internal/backendtelemetry(exported asMonitorBackends) so bothserverandcorecan use it without an import cycle.*auth.Identityon every method, never read from context and never logged. The Cedar admission seam (P1.5 Core admission seam (bounded rewrite) #5438) is intentionally not wired here.Closes #5437
Type of change
Test plan
task test)task lint-fix)Unit tests for
pkg/vmcp/corecover: constructor validation (loud failure on nil required collaborators), on-demand aggregation, health-filtering parity withdiscovery/middleware.go, lookups (including renamed/prefixed capability names and unknown-name errors), copy-before-mutate ofargs/meta, composite-tool execution across allexecuteCompositebranches, idempotentClose, and verification that identity is never logged. Existingpkg/vmcp/serverand the relocatedpkg/vmcp/internal/backendtelemetrytests pass unchanged.golangci-lintreports 0 issues for the changed packages.Changes
pkg/vmcp/core/core_vmcp.go*coreVMCP,Newconstructor, list/lookup paths, health filtering, workflow validation, idempotentClose.pkg/vmcp/core/core_calls.goCallTool/ReadResource/GetPromptcall paths and composite-tool execution.pkg/vmcp/core/core.goNewto the previously contract-onlyConfig.pkg/vmcp/elicitation.goElicitationRequesterseam (no mcp-go types).pkg/vmcp/internal/backendtelemetry/server/telemetry.go; exported asMonitorBackends.pkg/vmcp/server/server.gobackendtelemetry.MonitorBackends; signature and behavior unchanged.pkg/vmcp/composer/,pkg/vmcp/server/sdk_elicitation_adapter*.goElicitationRequester; SDK translation stays confined to the adapter.Does this introduce a user-facing change?
No. The core is additive and not yet wired into the request path;
server.New's signature and observable behavior are unchanged.Special notes for reviewers
filterHealthyBackendsis a deliberate copy ofdiscovery/middleware.go's implementation. The discovery middleware keeps its own copy on the legacyserver.Newpath until that path is removed in Phase 3 (P2.4 Replace discovery-into-context with direct VMCP calls #5442/P3.2 Reduce server.New body to the wrapper #5445). The include/exclude rules are kept identical across both copies — this is not accidental duplication.task lint-fixfinding is a pre-existing gosec G115 incmd/thv/app/upgrade.go(the.golangci.ymlgosec exclude list omits G115).task testfailures arepkg/mcp/server/TestBuildServerConfig, which require a container runtime (Docker/Podman) not present locally.Large PR Justification
This PR is large for three reasons, none of which are net-new production logic:
Atomic full-scope implementation (multiple related changes that would break if separated).
Issue P1.4 New(cfg) -> VMCP core constructor #5437 pre-planned a split into TASK-104a (constructor + wiring + list/lookup +
Close) and TASK-104b (the call/read/get paths + composite execution). Per an explicitup-front decision, both land here as one change. Splitting them would leave
*coreVMCPnot satisfying the
VMCPinterface between PRs (the interface requires all ten methods),so the halves are not independently mergeable as working code. Actual net-new production
logic is ~580 LOC across 4 files (
core_vmcp.go,core_calls.go,core.go,server.go).A file move inflates the diff (refactor that must be atomic).
The backend-client telemetry decorator was relocated from
pkg/vmcp/server/telemetry.goto a shared
pkg/vmcp/internal/backendtelemetry/package so bothserverandcorecanuse it without an import cycle. Git counts this as ~500 added + deleted lines, but it is a
near-verbatim move (≈94% similarity) with net-zero behavior change —
server.New'ssignature and observable behavior are unchanged.
Comprehensive unit tests (test-only, low review risk).
The remaining bulk is table-driven tests for the new core package (constructor validation,
on-demand aggregation, health-filtering parity, lookups, copy-before-mutate, composite
execution, advertise-vs-execute conflict parity, idempotent
Close, identity-not-logged)plus the relocated telemetry tests.
Splitting further would either ship non-compiling intermediate states or separate tests from
the code they cover, both of which reduce reviewability rather than improve it.