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
Add a runnable decorator example embedder that demonstrates the one supported
extension mechanism of the new vMCP core: decoration over the VMCP interface. The
example implements a subtract-only decorator that filters its ListTools output
and refuses the corresponding CallTool (via LookupTool) before delegating to inner, proving the two invariants of architecture.md Core Principle #3 —
(a) list and call stay consistent, and (b) a decorator can only subtract
reachability, never widen it.
Context
This is the final task of the vMCP New/Serve split (RFC Phase 4). By the time it
lands, the identity-parameterized VMCP interface, New(cfg) -> VMCP, and Serve(ctx, VMCP, *ServerConfig) -> *Server are merged (#5430/002/003), and server.New is a stable thin wrapper. #5446 documented the decorator extension
model in docs/arch/vmcp-library.md / pkg/vmcp/doc.go and added a forward
reference to "the decorator example embedder" — this task supplies that example so
the documented mechanism is also executable and compiler-verified.
The example is the concrete realization of the RFC §API Changes "Decorator
example" (filteringVMCP) and architecture.md Core Principle #3 (decorators
subtract, never widen; lines ~355-357). It mirrors the filter/deny pattern in pkg/authz/tool_filter.go
(filterToolsByPolicy drops disallowed tools from the advertised list; authorizeToolCall denies the matching call), but operates over the domain VMCP
interface instead of mcp-go types — so no mcp-go crosses the example's boundary
(Core Principle #1).
This task changes no production behavior and adds no CLI flags.
Parent Story: #[#5433] Dependencies: #[#5446] (the docs that name and forward-reference this example) Blocks: None (leaf task)
Acceptance Criteria
A runnable example embedder compiles and demonstrates a subtract-only
decorator over VMCP: the decorator holds only inner VMCP (plus a
consumer-defined policy) and has no path to backends except through inner.
The example demonstrates list/call consistency: a tool the decorator
filters out of ListTools is also denied by CallTool (the deny is decided via LookupTool before delegating), so "what is advertised" cannot drift from "what
is callable".
The example demonstrates that a decorator cannot widen access: it can only
remove items the inner core already advertised — it cannot surface a tool the
core did not return, nor make a call the core would refuse.
ListTools / CallTool (and at least the resource/prompt list+lookup shape,
or a clear note that they follow the same pattern) are covered so the symmetry is
visible.
The example does not import mcp-go and does not reach below the VMCP
interface (no routing/aggregator/registry internals) — extension is purely by
composition over VMCP (Core Principle fix(typo): corrects readme #1, anti-pattern Implement secret injection #5).
The example is exercised by the test suite (task test) — e.g. a godoc
testable example with // Output: or a unit test asserting the filter+deny
behavior — so "runnable + compiles" is enforced in CI, not just by inspection.
PR is ≤ 400 LOC and ≤ 10 files changed (excluding tests/docs/generated)
server.New signature and observable behavior unchanged
No production behavior change; no CLI flags added (task docs is a no-op,
generated docs/cli/thv_vmcp_*.md unchanged)
All tests pass (task test); lint clean (task lint-fix)
Code reviewed and approved
Technical Approach
Recommended Implementation
Implement a small filteringVMCP decorator that embeds/holds only inner vmcp.VMCP
and a consumer-defined capability policy, then delegates every method to inner:
ListTools calls inner.ListTools, then drops any tool the policy disallows
(keyed off the logical, safe-to-expose Tool.BackendID — never a backend
address; see research.md line 74 and Core Principle Implement secret store #4).
CallTool resolves the name to a *vmcp.Tool via inner.LookupTool (which yields
the BackendID), applies the same policy decision, and either returns a
sentinel "not allowed" error or delegates to inner.CallTool. Because list and
call consult the same policy against the same inner aggregation, they cannot
drift — this is exactly the tool_filter.go filter-then-deny shape lifted to the VMCP interface.
ListResources/ReadResource and ListPrompts/GetPrompt follow the identical
shape (filter the list, lookup-then-deny on access); implement them or document
that they mirror the tool path.
All other methods (LookupResource, LookupPrompt, Close, etc.) pass straight
through to inner.
The "cannot widen" property is structural: the decorator never constructs tools
or contacts a backend — its only data source is inner's return values, so it can
only return a subset. The example should make this self-evident (a short comment +
a demonstration that requesting a filtered tool yields the deny error rather than a
backend call).
Example location — confirm against the live repo before implementing. Two
repo-consistent options (the existing examples/ directory at the repo root holds
only JSON/YAML config samples, not Go programs, and there are currently no func Example* godoc examples or example_test.go files in the tree — so prefer
not to invent a new top-level convention):
Preferred — godoc testable example in pkg/vmcp (e.g. pkg/vmcp/example_decorator_test.go with func Example_filteringDecorator() and
an // Output: block, plus the filteringVMCP type in the same _test file or
a small pkg/vmcp/internal/example/ package). It is compiled and run by task test, lives next to the VMCP interface it decorates, and is discoverable
from godoc — satisfying "runnable" with zero new tooling.
Alternative — a tiny embedder main under cmd/ (matching the cmd/vmcp/main.go embedder convention) that wires New → filteringVMCP → Serve. This is closer to a real downstream embedder (e.g. stacklok/brood-box,
which embeds under internal/infra/mcp/) but adds a buildable binary; only take
this route if "embedder" is meant literally and a main is desired.
Whichever location is chosen, ensure #5446's forward reference ("see the
decorator example embedder") resolves to it.
Tool.BackendID is a logical id, safe to expose to decorators
(Core Principle Implement secret store #4; security.md "Don't store internal addressing"). Filter on it,
never on a backend address/URL.
Copy before mutating caller input if the example rebuilds/edits any returned
slice or args/meta map (.claude/rules/go-style.md). Prefer allocating a new
output slice (as filterToolsByPolicy does with make([]T, 0, len)).
pkg/vmcp/ (root package) - home of the VMCP interface (added in P1.1 Define VMCP interface + core Config #5434) and
the domain result types; the example lives here (preferred option above) and
decorates vmcp.VMCP using vmcp.Tool / vmcp.ToolCallResult etc.
pkg/vmcp/types.go - Tool.BackendID (line 386), Resource.BackendID (line 420), Prompt.BackendID (line 435); the logical ids the decorator filters on.
pkg/authz/tool_filter.go - reference implementation to mirror: filterToolsByPolicy (filter the advertised list) and authorizeToolCall (deny
the matching call). The example lifts this filter/deny pattern onto the VMCP
interface (domain types instead of mcp-go).
pkg/vmcp/doc.go (158 lines) - package docs updated by P4.1 Update vMCP architecture docs + doc.go #5446 that describe the
decorator extension model and forward-reference this example; keep the example
consistent with that text.
docs/arch/vmcp-library.md - the embedding doc (P4.1 Update vMCP architecture docs + doc.go #5446) whose decorator section
points to "the decorator example embedder"; ensure the link target matches the
chosen location.
cmd/vmcp/main.go - the existing embedder main convention (alternative
location option 2).
pkg/vmcp/server/server.go:301 - server.New (7-param signature; stays stable,
untouched by this task).
RFC THV-0076 §API Changes "Decorator example" (filteringVMCP, lines ~298-336) -
the canonical snippet this example realizes.
Component Interfaces
The example decorates the VMCP interface defined in #5434 (signatures per RFC
THV-0076 §API Changes, lines 248-272). Illustrative shape (final names/policy are at
the implementer's discretion; ErrCapabilityNotAllowed is an example-local sentinel,
not an existing domain error):
// CapabilityPolicy is a consumer-defined allow predicate keyed on the logical,// safe-to-expose BackendID (never a backend address).typeCapabilityPolicyinterface {
Allow(id*auth.Identity, backendIDstring) bool
}
// filteringVMCP is a subtract-only decorator: it holds ONLY the inner VMCP (plus a// policy) and has no path to backends except through inner, so it can only remove// reachability the inner core already grants — it cannot widen access.typefilteringVMCPstruct {
inner vmcp.VMCPpolicyCapabilityPolicy
}
func (f*filteringVMCP) ListTools(ctx context.Context, id*auth.Identity) ([]vmcp.Tool, error) {
tools, err:=f.inner.ListTools(ctx, id)
iferr!=nil {
returnnil, err
}
out:=make([]vmcp.Tool, 0, len(tools))
for_, t:=rangetools {
iff.policy.Allow(id, t.BackendID) { // subtract onlyout=append(out, t)
}
}
returnout, nil
}
func (f*filteringVMCP) CallTool(
ctx context.Context, id*auth.Identity, namestring, argsmap[string]any,
) (*vmcp.ToolCallResult, error) {
tool, err:=f.inner.LookupTool(ctx, id, name) // same source of truth as ListToolsiferr!=nil {
returnnil, err
}
if!f.policy.Allow(id, tool.BackendID) { // identical predicate => list/call stay consistentreturnnil, fmt.Errorf("%w: %s", ErrCapabilityNotAllowed, name)
}
returnf.inner.CallTool(ctx, id, name, args)
}
// ListResources/ReadResource, ListPrompts/GetPrompt follow the same shape;// LookupResource/LookupPrompt/Close delegate straight to inner.
Testing Strategy
Unit Tests / Runnable Example (the example must be exercised by task test)
List filtering: with a fake/mock inner VMCP advertising tools across two BackendIDs and a policy allowing only one, filteringVMCP.ListTools returns only
the allowed subset.
List/call consistency: a tool absent from the filtered ListTools output is
denied by CallTool with the sentinel error, and inner.CallTool is not
invoked for it.
Allowed pass-through: an allowed tool is returned by ListTools and its CallTool delegates to inner and returns the inner result unchanged.
Cannot widen: a tool the inner core does not advertise never appears in
the decorator's ListTools, and calling it is denied (the decorator has no way to
fabricate reachability).
If implemented as a godoc Example_*, an // Output: block makes the
demonstrated behavior assertion-checked by go test.
Integration / Behavioral Parity Tests
(If the embedder-main option is chosen) the example builds via task build
and New → filteringVMCP → Serve wiring compiles against the real API; no
parity assertion needed since the decorator is example-only and not in the server.New path.
Edge Cases
nil-identity / anonymous: the policy is consulted with the identity as passed;
the decorator does not read identity from context (Core Principle Do we want the container monitor? #2). Document the
policy's behavior for nil identity.
Empty inner list / inner error: ListTools/CallTool propagate inner's error
and an empty advertised set yields an empty filtered set (no panic on tools[:0]
style reuse — prefer a freshly allocated slice).
The docs that describe and forward-reference the decorator model — that is P4.1 Update vMCP architecture docs + doc.go #5446
(P4.1). This task only ensures the referenced example exists and matches the docs.
Adding the decorator (or its policy) to the live thv vmcp serve / server.New
path — the example is illustrative only.
A new transport-layer extension mechanism beyond decoration over VMCP (RFC
non-goal).
Description
Add a runnable decorator example embedder that demonstrates the one supported
extension mechanism of the new vMCP core: decoration over the
VMCPinterface. Theexample implements a subtract-only decorator that filters its
ListToolsoutputand refuses the corresponding
CallTool(viaLookupTool) before delegating toinner, proving the two invariants ofarchitecture.mdCore Principle #3 —(a) list and call stay consistent, and (b) a decorator can only subtract
reachability, never widen it.
Context
This is the final task of the vMCP New/Serve split (RFC Phase 4). By the time it
lands, the identity-parameterized
VMCPinterface,New(cfg) -> VMCP, andServe(ctx, VMCP, *ServerConfig) -> *Serverare merged (#5430/002/003), andserver.Newis a stable thin wrapper. #5446 documented the decorator extensionmodel in
docs/arch/vmcp-library.md/pkg/vmcp/doc.goand added a forwardreference to "the decorator example embedder" — this task supplies that example so
the documented mechanism is also executable and compiler-verified.
The example is the concrete realization of the RFC §API Changes "Decorator
example" (
filteringVMCP) andarchitecture.mdCore Principle #3 (decoratorssubtract, never widen; lines ~355-357). It mirrors the filter/deny pattern in
pkg/authz/tool_filter.go(
filterToolsByPolicydrops disallowed tools from the advertised list;authorizeToolCalldenies the matching call), but operates over the domainVMCPinterface instead of mcp-go types — so no mcp-go crosses the example's boundary
(Core Principle #1).
This task changes no production behavior and adds no CLI flags.
Parent Story: #[#5433]
Dependencies: #[#5446] (the docs that name and forward-reference this example)
Blocks: None (leaf task)
Acceptance Criteria
decorator over
VMCP: the decorator holds onlyinner VMCP(plus aconsumer-defined policy) and has no path to backends except through
inner.filters out of
ListToolsis also denied byCallTool(the deny is decided viaLookupToolbefore delegating), so "what is advertised" cannot drift from "whatis callable".
remove items the
innercore already advertised — it cannot surface a tool thecore did not return, nor make a call the core would refuse.
ListTools/CallTool(and at least the resource/prompt list+lookup shape,or a clear note that they follow the same pattern) are covered so the symmetry is
visible.
VMCPinterface (no routing/aggregator/registry internals) — extension is purely by
composition over
VMCP(Core Principle fix(typo): corrects readme #1, anti-pattern Implement secret injection #5).task test) — e.g. a godoctestable example with
// Output:or a unit test asserting the filter+denybehavior — so "runnable + compiles" is enforced in CI, not just by inspection.
server.Newsignature and observable behavior unchangedtask docsis a no-op,generated
docs/cli/thv_vmcp_*.mdunchanged)task test); lint clean (task lint-fix)Technical Approach
Recommended Implementation
Implement a small
filteringVMCPdecorator that embeds/holds onlyinner vmcp.VMCPand a consumer-defined capability policy, then delegates every method to
inner:ListToolscallsinner.ListTools, then drops any tool the policy disallows(keyed off the logical, safe-to-expose
Tool.BackendID— never a backendaddress; see
research.mdline 74 and Core Principle Implement secret store #4).CallToolresolves the name to a*vmcp.Toolviainner.LookupTool(which yieldsthe
BackendID), applies the same policy decision, and either returns asentinel "not allowed" error or delegates to
inner.CallTool. Because list andcall consult the same policy against the same
inneraggregation, they cannotdrift — this is exactly the
tool_filter.gofilter-then-deny shape lifted to theVMCPinterface.ListResources/ReadResourceandListPrompts/GetPromptfollow the identicalshape (filter the list, lookup-then-deny on access); implement them or document
that they mirror the tool path.
LookupResource,LookupPrompt,Close, etc.) pass straightthrough to
inner.The "cannot widen" property is structural: the decorator never constructs tools
or contacts a backend — its only data source is
inner's return values, so it canonly return a subset. The example should make this self-evident (a short comment +
a demonstration that requesting a filtered tool yields the deny error rather than a
backend call).
Example location — confirm against the live repo before implementing. Two
repo-consistent options (the existing
examples/directory at the repo root holdsonly JSON/YAML config samples, not Go programs, and there are currently no
func Example*godoc examples orexample_test.gofiles in the tree — so prefernot to invent a new top-level convention):
pkg/vmcp(e.g.pkg/vmcp/example_decorator_test.gowithfunc Example_filteringDecorator()andan
// Output:block, plus thefilteringVMCPtype in the same_testfile ora small
pkg/vmcp/internal/example/package). It is compiled and run bytask test, lives next to theVMCPinterface it decorates, and is discoverablefrom godoc — satisfying "runnable" with zero new tooling.
mainundercmd/(matching thecmd/vmcp/main.goembedder convention) that wiresNew→filteringVMCP→Serve. This is closer to a real downstream embedder (e.g.stacklok/brood-box,which embeds under
internal/infra/mcp/) but adds a buildable binary; only takethis route if "embedder" is meant literally and a
mainis desired.Whichever location is chosen, ensure #5446's forward reference ("see the
decorator example embedder") resolves to it.
Patterns & Frameworks
VMCPinterface — composition only; holdinner vmcp.VMCPand delegate. This is the supported extension seam (RFC;
architecture.mdCorePrinciple Bump golangci/golangci-lint-action from 2f856675483cb8b9378ee77ee0beb67955aca9d7 to 4696ba8babb6127d732c3c6dde519db15edab9ea #3). Do not use the
session.Decorator/NewDecoratingFactoryseam — the RFC's Alternative 2 explicitly rejects it for the public API
(SDK-coupled, session-scoped;
research.mdline 252).pkg/authz/tool_filter.go:ListToolsfilters,CallToollooks up + denies with the same predicate.VMCPboundary (Core Principle fix(typo): corrects readme #1, anti-pattern Implement secret injection #5); keepthe example in domain types +
*auth.Identityonly.Tool.BackendIDis a logical id, safe to expose to decorators(Core Principle Implement secret store #4;
security.md"Don't store internal addressing"). Filter on it,never on a backend address/URL.
slice or
args/metamap (.claude/rules/go-style.md). Prefer allocating a newoutput slice (as
filterToolsByPolicydoes withmake([]T, 0, len))..claude/rules/go-style.md(SPDX headers, error handling),.claude/rules/vmcp-anti-patterns.md,.claude/rules/security.md.Code Pointers
pkg/vmcp/(root package) - home of theVMCPinterface (added in P1.1 Define VMCP interface + core Config #5434) andthe domain result types; the example lives here (preferred option above) and
decorates
vmcp.VMCPusingvmcp.Tool/vmcp.ToolCallResultetc.pkg/vmcp/types.go-Tool.BackendID(line 386),Resource.BackendID(line 420),Prompt.BackendID(line 435); the logical ids the decorator filters on.pkg/authz/tool_filter.go- reference implementation to mirror:filterToolsByPolicy(filter the advertised list) andauthorizeToolCall(denythe matching call). The example lifts this filter/deny pattern onto the
VMCPinterface (domain types instead of mcp-go).
pkg/vmcp/doc.go(158 lines) - package docs updated by P4.1 Update vMCP architecture docs + doc.go #5446 that describe thedecorator extension model and forward-reference this example; keep the example
consistent with that text.
docs/arch/vmcp-library.md- the embedding doc (P4.1 Update vMCP architecture docs + doc.go #5446) whose decorator sectionpoints to "the decorator example embedder"; ensure the link target matches the
chosen location.
cmd/vmcp/main.go- the existing embeddermainconvention (alternativelocation option 2).
pkg/vmcp/server/server.go:301-server.New(7-param signature; stays stable,untouched by this task).
filteringVMCP, lines ~298-336) -the canonical snippet this example realizes.
Component Interfaces
The example decorates the
VMCPinterface defined in #5434 (signatures per RFCTHV-0076 §API Changes, lines 248-272). Illustrative shape (final names/policy are at
the implementer's discretion;
ErrCapabilityNotAllowedis an example-local sentinel,not an existing domain error):
Testing Strategy
Unit Tests / Runnable Example (the example must be exercised by
task test)inner VMCPadvertising tools across twoBackendIDs and a policy allowing only one,filteringVMCP.ListToolsreturns onlythe allowed subset.
ListToolsoutput isdenied by
CallToolwith the sentinel error, andinner.CallToolis notinvoked for it.
ListToolsand itsCallTooldelegates toinnerand returns the inner result unchanged.innercore does not advertise never appears inthe decorator's
ListTools, and calling it is denied (the decorator has no way tofabricate reachability).
Example_*, an// Output:block makes thedemonstrated behavior assertion-checked by
go test.Integration / Behavioral Parity Tests
mainoption is chosen) the example builds viatask buildand
New→filteringVMCP→Servewiring compiles against the real API; noparity assertion needed since the decorator is example-only and not in the
server.Newpath.Edge Cases
the decorator does not read identity from context (Core Principle Do we want the container monitor? #2). Document the
policy's behavior for
nilidentity.ListTools/CallToolpropagateinner's errorand an empty advertised set yields an empty filtered set (no panic on
tools[:0]style reuse — prefer a freshly allocated slice).
Out of Scope
VMCP,New,Serve,ServerConfig, orserver.New— shipped inPhase 1: VMCP interface, core constructor, admission + elicitation seams #5430/002/003; this task only consumes them in an example.
(P4.1). This task only ensures the referenced example exists and matches the docs.
thv vmcp serve/server.Newpath — the example is illustrative only.
VMCP(RFCnon-goal).
References
filteringVMCP),"Decorators cannot widen access" (lines ~525-526), Alternative 2 (why not
session.Decorator).architecture.md— Core Principle Bump golangci/golangci-lint-action from 2f856675483cb8b9378ee77ee0beb67955aca9d7 to 4696ba8babb6127d732c3c6dde519db15edab9ea #3 "Decorators subtract, never widen"(lines ~355-357); Core Principles fix(typo): corrects readme #1 (no mcp-go across the boundary), Implement secret store #4
(
BackendIDis a logical id); "Phase 4 — Docs + example" P4.2 (lines 434-438).pkg/authz/tool_filter.go— filter/deny reference pattern.