You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Implement the concrete *coreVMCP and New(cfg *Config) (VMCP, error) constructor
in the root pkg/vmcp package by relocating (not rewriting) the domain wiring that
lives in server.New today. This is the structural heart of RFC Phase 1: it turns the VMCP contract from #5434 into a working, identity-parameterized core that aggregates
capabilities, routes calls, and drives composite workflows — without any mcp-go types
crossing the boundary and without touching server.New.
Context
The core constructor relocates the collaborator wiring currently buried in server.New's
god-object body (server.go:330-465): telemetry backend-client decoration, the workflow
auditor, the in-memory state store + NewWorkflowEngine, the per-session sessionComposerFactory/NewSessionRouter pattern, and validateWorkflows fail-fast.
Per architecture.md ("New (core) wiring relocated", lines ~278-284 and
"Statelessness, Session Consistency & Health Filtering (R5/R7)"), the core filters,
Serve caches: the core is stateless w.r.t. sessions, takes an injected health.StatusProvider, and runs filterHealthyBackends internally over the backend
registry before aggregating — preserving today's exact include/exclude rules. The StatusProvider is built at the composition root (the server.New wrapper / test
harness, per A2) and the same *health.Monitor is lifecycled by Serve (#5443); the
core only reads it, and a nil StatusProvider means no health filtering (all backends
included), matching today's no-monitor behavior. ListTools
drives aggregator.AggregateCapabilities on demand; CallTool/ReadResource/GetPrompt
route via router.Router + composer; Lookup* resolve advertised names (incl. BackendID) without invoking. The admission seam is NOT wired here — it lands in #5438. Identity is an explicit *auth.Identity on every method, never read from
context (anti-pattern #1).
Split note (pre-planned axis): This PR is likely to exceed the 400 LOC / 10 file
limit — it relocates the whole server.go:330-465 wiring and implements 10 interface
methods and unit tests — so plan to split it along this axis:
TASK-104a — the New constructor + collaborator wiring + the injected health.StatusProvider/filterHealthyBackends + the list/lookup paths
(ListTools/ListResources/ListPrompts/Lookup*) + Close. The shared
constructor wiring and health filtering land here.
TASK-104b — the call/read/get paths (CallTool/ReadResource/GetPrompt),
including composite-tool execution through the per-session composer; reuses the *coreVMCP constructed in 104a.
If split: TASK-104a depends on #5434 + #5436, TASK-104b depends on TASK-104a, and
both fan into #5438 / #5439 the same way #5437 does. Use the /split-pr skill.
Acceptance Criteria
New(cfg *Config) (VMCP, error) returns a working *coreVMCP with all interface
methods implemented: ListTools/CallTool/ListResources/ReadResource/ ListPrompts/GetPrompt/LookupTool/LookupResource/LookupPrompt/Close.
The core runs filterHealthyBackends from an injectedhealth.StatusProvider
(built at the composition root, not constructed by the core; a nil provider ⇒ no
filtering, all backends included) over backendRegistry.List(ctx) before aggregating,
preserving today's exact include/exclude rules (healthy/degraded/empty included;
unhealthy/unknown/unauthenticated excluded — discovery/middleware.go:185-188).
ListTools drives aggregator.AggregateCapabilities on demand (no per-session
state held in the core); Lookup* resolve advertised names incl. BackendID without
invoking the backend.
PR is ≤ 400 LOC and ≤ 10 files changed (excluding tests/docs/generated). If it
exceeds the limit, split via /split-pr into TASK-104a / TASK-104b.
server.New signature and observable behavior unchanged.
All tests pass (task test); lint clean (task lint-fix).
Code reviewed and approved.
Technical Approach
Recommended Implementation
Add pkg/vmcp/core.go (or fold into vmcp.go) holding *coreVMCP. New assembles the
collaborators by relocating the wiring from server.New (server.go:330-465) — do
not redesign it:
Telemetry backend-client decoration — when cfg.TelemetryProvider != nil, decorate
the backend client via monitorBackends (server.go:353-367) before building the
workflow engine, so workflow backend calls are instrumented.
Workflow auditor — when cfg.AuditConfig != nil, validate and build the audit.WorkflowAuditor (server.go:370-381).
State store + workflow engine — composer.NewInMemoryStateStore(5*time.Minute, 1*time.Hour) + composer.NewWorkflowEngine(rt, backendClient, elicitationHandler, stateStore, workflowAuditor, nil) (server.go:386-387). The elicitationHandler is
built from the domain-typedElicitationRequester delivered by P1.3 Domain-typed ElicitationRequester (bounded rewrite) #5436 — no mcp-go
types here.
Per-session composer factory — relocate the sessionComposerFactory closure that
builds a per-session composer.Composer bound to the session's routing table via router.NewSessionRouter(sessionRT) (server.go:393-398). This is the decoupled
composite-routing pattern the refactor generalizes (it already removes composite tools'
dependency on context-injected DiscoveredCapabilities).
Health filtering — store the injected health.StatusProvider; each ListTools/ Lookup* call lists backends from the registry and runs filterHealthyBackends
(copied from discovery/middleware.go:157) before calling aggregator.AggregateCapabilities. This is an intentional, temporary duplication
(C2): the discovery middleware keeps its own copy on the legacy server.New path
(guarded to legacy in P2.4 Replace discovery-into-context with direct VMCP calls #5442) until that path is removed in Phase 3 (P3.2 Reduce server.New body to the wrapper #5445) — call
it out for reviewers so it is not mistaken for accidental duplication. A nil StatusProvider ⇒ no filtering (all backends included).
CallTool/ReadResource/GetPrompt route through router.Router + the composer for
composite tools. Lookup* resolve an advertised name/URI to the capability (incl. BackendID) without invoking, returning an error for an unknown/unadvertised name — the
validation seam for the call path. Apply nil/anonymous semantics matching ShouldAllowAnonymous / ErrNilCaller / ErrUnauthorizedCaller. Copy args/meta maps
before mutating (maps.Clone).
pkg/vmcp/core.go (new, or vmcp.go) — *coreVMCP + New(cfg *Config) (VMCP, error)
(architecture.md "New (core) wiring relocated", lines ~278-284).
pkg/vmcp/server/server.go:330-465 - the domain wiring being relocated: monitorBackends (353-367), workflow auditor (370-381), state store + NewWorkflowEngine (386-387), sessionComposerFactory (393-398), validateWorkflows (402; impl 1211).
pkg/vmcp/server/server.go:393 - sessionComposerFactory / router.NewSessionRouter(sessionRT) — the decoupled per-session composite-routing
pattern to generalize into the core.
pkg/vmcp/server/server.go:301 - server.New (7-param signature; stays stable,
not modified in this task).
pkg/vmcp/discovery/middleware.go:157 - filterHealthyBackends relocated into the core;
include/exclude rules at 185-188 (R5/R7). Already takes a health.StatusProvider.
pkg/vmcp/session/types/session.go:182,224-232 - ShouldAllowAnonymous, ErrNilCaller / ErrUnauthorizedCaller — the nil-identity / anonymous semantics the
core reproduces.
pkg/vmcp/types.go:130 - BackendTarget.GetBackendCapabilityName (renamed/prefixed
name resolution used by LookupTool/CallTool).
Component Interfaces
The core implements the VMCP contract from #5434 (architecture.md "API Contracts"). New accepts the relocated collaborators on the core Config; the health.StatusProvider is injected for internal health filtering. Shape only — not the
full implementation:
// New constructs the core VMCP by relocating server.New's domain wiring.// cfg carries the relocated collaborators: aggregator, router.Router,// vmcp.BackendRegistry, vmcp.BackendClient, the domain ElicitationRequester,// workflowDefs, and the injected health.StatusProvider (core filters, Serve caches).// Admission is NOT wired here (#5438).funcNew(cfg*Config) (VMCP, error)
typecoreVMCPstruct {
aggregator aggregator.Aggregatorrouter router.RouterbackendRegistry vmcp.BackendRegistrybackendClient vmcp.BackendClientcomposerFactoryfunc(sessionRT*vmcp.RoutingTable, sessionTools []vmcp.Tool) composer.ComposerworkflowEngine composer.ComposerworkflowDefsmap[string]*composer.WorkflowDefinitionhealth health.StatusProvider// injected; core runs filterHealthyBackends// ... (no admission seam yet — #5438)
}
// ListTools health-filters the registry, then aggregates on demand. Stateless// w.r.t. sessions. identity is explicit and never read from context.func (c*coreVMCP) ListTools(ctx context.Context, identity*auth.Identity) ([]Tool, error)
// CallTool routes via router.Router + composer; nil/anonymous semantics match// ShouldAllowAnonymous / ErrNilCaller / ErrUnauthorizedCaller. args/meta copied// before mutating.func (c*coreVMCP) CallTool(ctx context.Context, identity*auth.Identity, namestring,
argsmap[string]any, metamap[string]any) (*ToolCallResult, error)
// LookupTool resolves an advertised name (incl. BackendID) without invoking;// errors on an unknown/unadvertised name. Same health/advertising view as ListTools.func (c*coreVMCP) LookupTool(ctx context.Context, identity*auth.Identity, namestring) (*Tool, error)
// Close releases core-held resources. Idempotent.func (c*coreVMCP) Close() error
Testing Strategy
Unit Tests (stdlib testing.T + testify; mocks via gomock + task gen; no Ginkgo, R8)
New returns a working VMCP and errors loudly on nil required collaborators.
ListTools/ListResources/ListPrompts aggregate on demand against a mock backend
client/registry and return advertised capabilities with BackendID populated.
Health filtering: backends reported unhealthy/unknown/unauthenticated by the injected health.StatusProvider are excluded; healthy/degraded/empty included (parity with discovery/middleware.go:185-188).
LookupTool/LookupResource/LookupPrompt resolve renamed/prefixed names back via GetBackendCapabilityName; return an error for an unknown/unadvertised name.
CallTool/ReadResource/GetPrompt route to the correct backend via router.Router;
composite tools execute through the per-session composer.
Close() is idempotent (second call is a no-op, no panic).
args/meta maps passed by the caller are not mutated (copy-before-mutate).
Integration / Behavioral Parity Tests
Capability set produced by the core's ListTools is consistent with the
session-factory path (aggregator.ProcessPreQueriedCapabilities) — same aggregator,
same filter, same registry — confirming no double-aggregation / no parity drift.
server.New wrapper behavior unchanged (the core is additive and not yet wired in).
Edge Cases
Nil identity → anonymous semantics per ShouldAllowAnonymous.
Bound-session caller mismatch / nil caller surfaces ErrUnauthorizedCaller / ErrNilCaller semantics consistent with the session layer.
No mcp-go type appears in any VMCP method signature or returned value.
Identity is never written to logs (verify Token/UpstreamTokens not logged).
Description
Implement the concrete
*coreVMCPandNew(cfg *Config) (VMCP, error)constructorin the root
pkg/vmcppackage by relocating (not rewriting) the domain wiring thatlives in
server.Newtoday. This is the structural heart of RFC Phase 1: it turns theVMCPcontract from #5434 into a working, identity-parameterized core that aggregatescapabilities, routes calls, and drives composite workflows — without any mcp-go types
crossing the boundary and without touching
server.New.Context
The core constructor relocates the collaborator wiring currently buried in
server.New'sgod-object body (
server.go:330-465): telemetry backend-client decoration, the workflowauditor, the in-memory state store +
NewWorkflowEngine, the per-sessionsessionComposerFactory/NewSessionRouterpattern, andvalidateWorkflowsfail-fast.Per
architecture.md("New (core) wiring relocated", lines ~278-284 and"Statelessness, Session Consistency & Health Filtering (R5/R7)"), the core filters,
Serve caches: the core is stateless w.r.t. sessions, takes an injected
health.StatusProvider, and runsfilterHealthyBackendsinternally over the backendregistry before aggregating — preserving today's exact include/exclude rules. The
StatusProvideris built at the composition root (theserver.Newwrapper / testharness, per A2) and the same
*health.Monitoris lifecycled byServe(#5443); thecore only reads it, and a nil
StatusProvidermeans no health filtering (all backendsincluded), matching today's no-monitor behavior.
ListToolsdrives
aggregator.AggregateCapabilitieson demand;CallTool/ReadResource/GetPromptroute via
router.Router+ composer;Lookup*resolve advertised names (incl.BackendID) without invoking. The admission seam is NOT wired here — it lands in#5438. Identity is an explicit
*auth.Identityon every method, never read fromcontext (anti-pattern #1).
Parent Story: #[#5430]
Dependencies: #[#5434] (VMCP interface + core Config), #[#5436] (domain-typed
ElicitationRequester)Blocks: #5438, #5439
Acceptance Criteria
New(cfg *Config) (VMCP, error)returns a working*coreVMCPwith all interfacemethods implemented:
ListTools/CallTool/ListResources/ReadResource/ListPrompts/GetPrompt/LookupTool/LookupResource/LookupPrompt/Close.filterHealthyBackendsfrom an injectedhealth.StatusProvider(built at the composition root, not constructed by the core; a nil provider ⇒ no
filtering, all backends included) over
backendRegistry.List(ctx)before aggregating,preserving today's exact include/exclude rules (healthy/degraded/empty included;
unhealthy/unknown/unauthenticated excluded —
discovery/middleware.go:185-188).ListToolsdrivesaggregator.AggregateCapabilitieson demand (no per-sessionstate held in the core);
Lookup*resolve advertised names incl.BackendIDwithoutinvoking the backend.
ShouldAllowAnonymous/ErrNilCaller/ErrUnauthorizedCaller(session/types/session.go:182,224-232).Close()is implemented and idempotent (releases core-held resources).VMCPboundary (anti-pattern Implement secret injection #5); identity is neverlogged (it redacts
Token/UpstreamTokens).exceeds the limit, split via
/split-printo TASK-104a / TASK-104b.server.Newsignature and observable behavior unchanged.task test); lint clean (task lint-fix).Technical Approach
Recommended Implementation
Add
pkg/vmcp/core.go(or fold intovmcp.go) holding*coreVMCP.Newassembles thecollaborators by relocating the wiring from
server.New(server.go:330-465) — donot redesign it:
cfg.TelemetryProvider != nil, decoratethe backend client via
monitorBackends(server.go:353-367) before building theworkflow engine, so workflow backend calls are instrumented.
cfg.AuditConfig != nil, validate and build theaudit.WorkflowAuditor(server.go:370-381).composer.NewInMemoryStateStore(5*time.Minute, 1*time.Hour)+composer.NewWorkflowEngine(rt, backendClient, elicitationHandler, stateStore, workflowAuditor, nil)(server.go:386-387). TheelicitationHandlerisbuilt from the domain-typed
ElicitationRequesterdelivered by P1.3 Domain-typed ElicitationRequester (bounded rewrite) #5436 — no mcp-gotypes here.
sessionComposerFactoryclosure thatbuilds a per-session
composer.Composerbound to the session's routing table viarouter.NewSessionRouter(sessionRT)(server.go:393-398). This is the decoupledcomposite-routing pattern the refactor generalizes (it already removes composite tools'
dependency on context-injected
DiscoveredCapabilities).validateWorkflows(workflowComposer, workflowDefs)fail-fast(
server.go:402; implserver.go:1211).health.StatusProvider; eachListTools/Lookup*call lists backends from the registry and runsfilterHealthyBackends(copied from
discovery/middleware.go:157) before callingaggregator.AggregateCapabilities. This is an intentional, temporary duplication(C2): the discovery middleware keeps its own copy on the legacy
server.Newpath(guarded to legacy in P2.4 Replace discovery-into-context with direct VMCP calls #5442) until that path is removed in Phase 3 (P3.2 Reduce server.New body to the wrapper #5445) — call
it out for reviewers so it is not mistaken for accidental duplication. A nil
StatusProvider⇒ no filtering (all backends included).CallTool/ReadResource/GetPromptroute throughrouter.Router+ the composer forcomposite tools.
Lookup*resolve an advertised name/URI to the capability (incl.BackendID) without invoking, returning an error for an unknown/unadvertised name — thevalidation seam for the call path. Apply nil/anonymous semantics matching
ShouldAllowAnonymous/ErrNilCaller/ErrUnauthorizedCaller. Copyargs/metamapsbefore mutating (
maps.Clone).Patterns & Frameworks
ElicitationRequester(P1.3 Domain-typed ElicitationRequester (bounded rewrite) #5436); the mcp-goSDKElicitationAdapterand hooks remain inthe transport layer (anti-pattern Implement secret injection #5).
*coreVMCPis the inner-mostVMCP; decorators may onlysubtract reachability (list-filter / call-refuse), never widen.
server.New; the onlydeliberate rewrites in this epic (admission seam R1, elicitation R4) are NOT in this task
(R4 is the upstream P1.3 Domain-typed ElicitationRequester (bounded rewrite) #5436; R1 is the downstream P1.5 Core admission seam (bounded rewrite) #5438).
.claude/rules/go-style.md(SPDX header on the new.gofile;copy-before-mutate; constructor validation fails loudly on nil required deps),
.claude/rules/vmcp-anti-patterns.md(fix(typo): corrects readme #1 identity explicit; Bump golangci/golangci-lint-action from 2f856675483cb8b9378ee77ee0beb67955aca9d7 to 4696ba8babb6127d732c3c6dde519db15edab9ea #3 thin core, not a new godobject; Implement secret injection #5 no SDK leak),
.claude/rules/security.md(never log identity/tokens).Code Pointers
pkg/vmcp/core.go(new, orvmcp.go) —*coreVMCP+New(cfg *Config) (VMCP, error)(architecture.md "New (core) wiring relocated", lines ~278-284).
pkg/vmcp/server/server.go:330-465- the domain wiring being relocated:monitorBackends(353-367), workflow auditor (370-381), state store +NewWorkflowEngine(386-387),sessionComposerFactory(393-398),validateWorkflows(402; impl 1211).pkg/vmcp/server/server.go:393-sessionComposerFactory/router.NewSessionRouter(sessionRT)— the decoupled per-session composite-routingpattern to generalize into the core.
pkg/vmcp/server/server.go:301-server.New(7-param signature; stays stable,not modified in this task).
pkg/vmcp/aggregator/aggregator.go:63,78-AggregateCapabilities/ProcessPreQueriedCapabilities— the on-demand aggregationListToolsdrives.pkg/vmcp/discovery/middleware.go:157-filterHealthyBackendsrelocated into the core;include/exclude rules at 185-188 (R5/R7). Already takes a
health.StatusProvider.pkg/vmcp/session/types/session.go:182,224-232-ShouldAllowAnonymous,ErrNilCaller/ErrUnauthorizedCaller— the nil-identity / anonymous semantics thecore reproduces.
pkg/vmcp/types.go:130-BackendTarget.GetBackendCapabilityName(renamed/prefixedname resolution used by
LookupTool/CallTool).Component Interfaces
The core implements the
VMCPcontract from #5434 (architecture.md "API Contracts").Newaccepts the relocated collaborators on the coreConfig; thehealth.StatusProvideris injected for internal health filtering. Shape only — not thefull implementation:
Testing Strategy
Unit Tests (stdlib
testing.T+ testify; mocks via gomock +task gen; no Ginkgo, R8)Newreturns a workingVMCPand errors loudly on nil required collaborators.ListTools/ListResources/ListPromptsaggregate on demand against a mock backendclient/registry and return advertised capabilities with
BackendIDpopulated.health.StatusProviderare excluded; healthy/degraded/empty included (parity withdiscovery/middleware.go:185-188).LookupTool/LookupResource/LookupPromptresolve renamed/prefixed names back viaGetBackendCapabilityName; return an error for an unknown/unadvertised name.CallTool/ReadResource/GetPromptroute to the correct backend viarouter.Router;composite tools execute through the per-session composer.
Close()is idempotent (second call is a no-op, no panic).args/metamaps passed by the caller are not mutated (copy-before-mutate).Integration / Behavioral Parity Tests
ListToolsis consistent with thesession-factory path (
aggregator.ProcessPreQueriedCapabilities) — same aggregator,same filter, same registry — confirming no double-aggregation / no parity drift.
server.Newwrapper behavior unchanged (the core is additive and not yet wired in).Edge Cases
ShouldAllowAnonymous.ErrUnauthorizedCaller/ErrNilCallersemantics consistent with the session layer.VMCPmethod signature or returned value.Token/UpstreamTokensnot logged).Out of Scope
Admissionseam and its wiring intoList*/Call/Read/Get(P1.5 Core admission seam (bounded rewrite) #5438, R1) — not wired in this task.
Serve(ctx, VMCP, *ServerConfig),ServerConfig, and re-homing the transport(mcp-go server, hooks, middleware chain, AS runner, status reporter, optimizer, health
monitor) — Phase 2: Serve transport helper, re-home transport, replace discovery #5431 / P2.1 Serve skeleton + ServerConfig #5439.
server.Newto the wrapper and thederiveCoreConfig/deriveServerConfigsplit — Phase 3: Reduce server.New to wrapper + config split #5432 / P3.1 deriveCoreConfig/deriveServerConfig config split #5444.
ElicitationRequesterrewrite itself — upstream P1.3 Domain-typed ElicitationRequester (bounded rewrite) #5436 (consumedhere, not authored here).
VMCPinterface + coreConfigdeclaration — upstream P1.1 Define VMCP interface + core Config #5434 (consumed here).References