Skip to content

P1.1 Define VMCP interface + core Config #5434

Description

@tgrunnagle

Description

Add the public, identity-parameterized VMCP domain interface and the core Config
struct to the root pkg/vmcp package (new file pkg/vmcp/vmcp.go). This is the
contract-only first step of the vMCP New/Serve split: it establishes the domain
boundary that New(cfg) -> VMCP (#5437) implements and that Serve and decorators
consume — with no implementation yet. Placing the interface in the root package,
alongside the existing Tool/Resource/Prompt/result types in types.go, lets it
reference those domain types directly without introducing an import cycle.

Context

This is the very first PR of Phase 1 of RFC THV-0076 (the vMCP core-interface refactor),
which extracts a small, identity-parameterized domain object from today's god-object
server.New. The interface is the primary contract (architecture.md "API Contracts
→ VMCP interface", lines 84-138): every method takes an explicit *auth.Identity and
never reads identity from context (anti-pattern #1), and no mcp-go types cross the
boundary (anti-pattern #5). The method set is the non-session-scoped generalization of
the existing Caller interface (pkg/vmcp/session/types/session.go:28), which is the
prior art for the shape and the nil-identity/anonymous semantics. Per decision R2, meta
is retained on CallTool/GetPrompt (diverging from the RFC's illustrative
signature) to match Caller and preserve _meta forwarding through
vmcp.BackendClient.CallTool (types.go:646; the BackendClient interface is declared
at types.go:642). Per R6, ResourceReadResult.Meta may be
nil due to an SDK handler limitation. See research.md (organizational standards: stdlib
testing, SPDX headers, go-style) and architecture.md "Core Principles" for the full
rationale.

Parent Story: #5430
Dependencies: None (root; can start immediately)
Blocks: #5437

Acceptance Criteria

  • VMCP interface declared in pkg/vmcp/vmcp.go with ListTools, CallTool,
    ListResources, ReadResource, ListPrompts, GetPrompt, LookupTool,
    LookupResource, LookupPrompt, and Close, with identity as an explicit
    *auth.Identity parameter on every data method.
  • meta (map[string]any) is retained on CallTool and GetPrompt (R2), with a
    doc note on the _meta-forwarding rationale.
  • No mcp-go types appear anywhere in the interface signature (anti-pattern Implement secret injection #5);
    the interface references only root-package domain types + *auth.Identity.
  • The interface doc comment encodes the full behavioral contract: identity explicit
    and never read from context; nil identity = anonymous; decorators may only subtract
    reachability; args/meta maps are read-only (copy-before-mutate); and
    ResourceReadResult.Meta may be nil per the SDK handler limitation (R6).
  • Core Config struct declared with its typed fields (collaborators,
    workflowDefs, the domain ElicitationRequester, Authz, cross-cutting
    TelemetryProvider/AuditConfig, and the injected health.StatusProvider) — field
    declarations only, no wiring/construction logic (P1.4 New(cfg) -> VMCP core constructor #5437 fills the bodies). Typed (not
    comment-only) so the first PR doesn't draw an empty-struct lint nit (C4).
  • Package compiles with no import cycle (task build green); SPDX header on the new
    .go file.
  • PR is ≤ 400 LOC and ≤ 10 files changed (excluding tests/docs/generated).
  • 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

Create pkg/vmcp/vmcp.go in the root pkg/vmcp package and declare the VMCP
interface exactly as specified in architecture.md (lines 105-127), generalizing the
existing Caller method set to be non-session-scoped. Add the core Config struct as a
field-only declaration documenting the collaborators it will hold (aggregator, router,
backend registry, backend client, composer, workflow defs, the Authz config for the
later admission seam, plus cross-cutting TelemetryProvider/AuditConfig) — but no
constructor body
: New(cfg *Config) (VMCP, error) is defined and implemented in
#5437, not here.

Declare the core Config with its typed fields (aggregator, router, backend registry,
backend client, the domain ElicitationRequester, workflow defs, the Authz config for
the later admission seam, the cross-cutting TelemetryProvider/AuditConfig, and the
injected health.StatusProvider) — field declarations only, no constructor body: New
is implemented in #5437. Typed fields (not a comment-only stub) mean #5437 fills in
bodies rather than the struct shape, and the first PR doesn't draw an empty-struct lint
nit (C4).

This PR is the contract only. Per the task scope, declare the func New(cfg *Config) (VMCP, error) (and Serve) signatures as forward declarations only if needed to make
the package compile
; otherwise omit them entirely and leave their bodies to #5437 /
#5439. Prefer not to add stubs that the linter will flag as unused — keep the surface
to the interface, its doc contract, and the Config type. The full doc comment on the
interface is a first-class deliverable here: it is the durable encoding of the contract
that downstream PRs must honor.

Patterns & Frameworks

  • Domain-only boundary: no mcp-go imports in vmcp.go. The interface speaks only in
    root-package domain types (Tool, Resource, Prompt, ToolCallResult,
    ResourceReadResult, PromptGetResult) and *auth.Identity (anti-pattern Implement secret injection #5).
  • Identity explicit, never from context on the public path (anti-pattern fix(typo): corrects readme #1) —
    ctx is for cancellation/trace only.
  • Decorator extension model: the doc contract states decorators hold only an
    inner VMCP and may only subtract reachability; they cannot widen access. Mirrors the
    list-filter/call-deny pairing in pkg/authz/tool_filter.go.
  • Conventions: .claude/rules/go-style.md (SPDX header, doc comments, copy-before-
    mutating-caller-input), .claude/rules/vmcp-anti-patterns.md. Per R8, tests in this
    area use stdlib testing.T + testify — though this contract-only PR may need no new
    tests beyond a compile check.

Code Pointers

  • pkg/vmcp/vmcp.go (new) — the VMCP interface + core Config. Authoritative shape:
    architecture.md "API Contracts → VMCP interface" (lines 89-134).
  • pkg/vmcp/types.go:367,406,424Tool, Resource, Prompt structs the interface
    returns; Tool.BackendID (386), Resource.BackendID (420), Prompt.BackendID (435)
    — logical ids safe to expose; populated by the advertising filter (pinned in P1.2 Pin BackendID through the advertising filter (tests) #5435).
  • pkg/vmcp/types.go:512,552,576ToolCallResult, ResourceReadResult,
    PromptGetResult returned by the call/read/get methods.
  • pkg/vmcp/types.go:556-561ResourceReadResult.Meta nil note (R6): the SDK
    resources/read handler cannot forward _meta; the interface doc must document this.
  • pkg/vmcp/types.go:646vmcp.BackendClient.CallTool (the _meta-forwarding path
    that motivates keeping meta on CallTool, R2); the BackendClient interface is
    declared at types.go:642.
  • pkg/vmcp/session/types/session.go:28Caller interface (prior art). CallTool
    with meta (41-48), ReadResource (58), GetPrompt (71), Close (80); the VMCP
    method set generalizes this to be non-session-scoped.
  • pkg/vmcp/session/types/session.go:182,224,228ShouldAllowAnonymous,
    ErrUnauthorizedCaller, ErrNilCaller: the nil-identity/anonymous semantics the doc
    contract references (the concrete behavior is reproduced by New in P1.4 New(cfg) -> VMCP core constructor #5437).
  • pkg/vmcp/server/server.go:301server.New (7-param signature); stays stable and
    is not touched by this PR.
  • github.com/stacklok/toolhive/pkg/authauth.Identity (the explicit identity param;
    redacts Token/UpstreamTokens, so it is never logged — Core Principle Implement secret injection #5).

Component Interfaces

The exact contract to declare (root pkg/vmcp package). meta is retained on
CallTool/GetPrompt per R2. The doc comment shown is the load-bearing deliverable.

// VMCP is the core Virtual MCP domain object.
//
// Contract:
//   - Identity is an explicit parameter on every method and is NEVER read from
//     context (anti-pattern #1). A nil identity is anonymous; bound-identity
//     mismatch is the caller's concern only at the session layer (in Serve), not
//     here — the core takes an already-authenticated *auth.Identity.
//   - Implementations MUST be safe for concurrent use.
//   - Decorators may only SUBTRACT reachability: filter list output or refuse a
//     call before delegating to inner. They have no path to backends except
//     through inner, so they cannot widen access.
//   - args/meta maps are treated as read-only; the core copies before mutating.
//   - ReadResource results: Meta may be nil — the mcp-go resources/read handler
//     cannot forward _meta (see types.go:556-561). Do not rely on it (R6).
type VMCP interface {
	ListTools(ctx context.Context, identity *auth.Identity) ([]Tool, error)
	CallTool(ctx context.Context, identity *auth.Identity, name string,
		args map[string]any, meta map[string]any) (*ToolCallResult, error)

	ListResources(ctx context.Context, identity *auth.Identity) ([]Resource, error)
	ReadResource(ctx context.Context, identity *auth.Identity, uri string) (*ResourceReadResult, error)

	ListPrompts(ctx context.Context, identity *auth.Identity) ([]Prompt, error)
	GetPrompt(ctx context.Context, identity *auth.Identity, name string,
		args map[string]any) (*PromptGetResult, error)

	// Lookup* resolve an advertised name/URI to the capability (incl. BackendID)
	// WITHOUT invoking it. Returns an error for an unknown/unadvertised name —
	// the validation seam for the call path. Lookups apply the same admission
	// filter as List*, so they never resolve a denied capability.
	LookupTool(ctx context.Context, identity *auth.Identity, name string) (*Tool, error)
	LookupResource(ctx context.Context, identity *auth.Identity, uri string) (*Resource, error)
	LookupPrompt(ctx context.Context, identity *auth.Identity, name string) (*Prompt, error)

	// Close releases core-held resources (backend connections, etc.). Idempotent.
	Close() error
}

// Config holds the collaborators New assembles into the core (typed fields declared
// here; New's body lands in #5437). Cross-cutting TelemetryProvider/AuditConfig are
// consumed by both New and Serve (not a clean partition, R3); HealthStatusProvider is
// the read-only health view built at the composition root (A2; nil => no filtering).
type Config struct {
	Aggregator           aggregator.Aggregator
	Router               router.Router
	BackendRegistry      vmcp.BackendRegistry
	BackendClient        vmcp.BackendClient
	WorkflowDefs         map[string]*composer.WorkflowDefinition
	Elicitation          ElicitationRequester  // domain-typed (#5436)
	Authz                *authz.Config          // feeds the admission seam (#5438)
	TelemetryProvider    *telemetry.Provider    // cross-cutting (also on ServerConfig, R3)
	AuditConfig          *audit.Config          // cross-cutting (also on ServerConfig, R3)
	HealthStatusProvider health.StatusProvider  // injected; built at the composition root (A2)
	// Exact field set is finalized by New (#5437) and deriveCoreConfig (#5444);
	// this PR declares the shape (typed fields), not the construction logic.
}

// Implemented in #5437; declared here only if required to compile.
// func New(cfg *Config) (VMCP, error)

Testing Strategy

Unit Tests

  • Compile-time conformance is the primary guarantee for a contract-only PR
    (task build / task test green). No behavior to exercise yet — New is unimplemented.
  • (Optional) A trivial compile-only assertion that a nil/placeholder value can be
    typed as VMCP, if it adds clarity without introducing a dead stub.

Integration / Behavioral Parity Tests

  • None required for this PR — server.New is untouched and the core is not yet wired
    into the live path; the parity suite remains green unchanged.

Edge Cases

  • Verify no import cycle is introduced between pkg/vmcp (root) and types.go/
    pkg/auth (the reason the interface lives in the root package).
  • Confirm zero mcp-go imports in vmcp.go (grep guard) — enforces anti-pattern Implement secret injection #5 at
    the boundary from day one.

Out of Scope

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-triageIssue needs initial triage by a maintainerrefactorvmcpVirtual MCP Server related issues

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions