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
Create cmd/thv/app/vmcp.go with the vmcp top-level Cobra command and serve and validate subcommands, wiring all associated flags to the pkg/vmcp/cli/ functions established in #4879. Register the new command in NewRootCmd() in commands.go and add "vmcp": true to the IsInformationalCommand() map so the command bypasses container runtime preflight checks. Regenerate CLI documentation via task docs. This is Phase 2 of RFC THV-0059 and makes thv vmcp serve and thv vmcp validate available to users for the first time.
Context
#4879 extracts the vMCP serve and validate business logic from cmd/vmcp/app/commands.go into the new pkg/vmcp/cli/ package, exposing cli.Serve(ctx, cfg) and cli.Validate(ctx, cfg) as importable Go functions. This item is the CLI wiring layer: it creates the thin Cobra command tree in cmd/thv/app/vmcp.go that parses flags and delegates immediately to those library functions — no business logic belongs here.
The existing cmd/thv/app/mcp.go (and its subcommand in mcp_serve.go) is the direct structural model: a constructor function returning a *cobra.Command with subcommands attached, plus a helper function for shared flags. The informationalCommands map in commands.go (line 104) currently contains version, search, completion, registry, mcp, and skill; vmcp must be added here because it does not require a running container runtime.
The standalone cmd/vmcp/app/commands.go binary is unaffected by this item and continues to function identically for Kubernetes deployments.
cmd/thv/app/vmcp.go exists with SPDX copyright header (// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc. / // SPDX-License-Identifier: Apache-2.0)
A newVMCPCommand() function returns a *cobra.Command with Use: "vmcp" containing serve and validate subcommands
The serve subcommand accepts --config/-c (string, path to vMCP config file), --host (string, default "127.0.0.1"), --port (int, default 4483), and --enable-audit (bool, default false) flags, matching the flag names already present in cmd/vmcp/app/commands.go
The validate subcommand accepts --config/-c (string, required) and exits 0 for valid configs and non-zero with a descriptive error message for invalid configs
Both subcommands delegate entirely to pkg/vmcp/cli/ functions — zero business logic in cmd/thv/app/vmcp.go
rootCmd.AddCommand(newVMCPCommand()) is added to NewRootCmd() in cmd/thv/app/commands.go
"vmcp": true is added to the informationalCommands map in IsInformationalCommand() in cmd/thv/app/commands.go
thv --help lists vmcp as an available subcommand
thv vmcp --help shows both serve and validate subcommands with correct descriptions
thv vmcp serve --config <valid-path> starts the vMCP server and logs its address
thv vmcp validate --config <valid-path> exits 0 and prints config summary; exits non-zero with a descriptive error for an invalid config
task docs is run and the generated CLI documentation in docs/ reflects the new vmcp command
go build ./cmd/thv/... succeeds with no errors
All existing tests pass (no regressions)
Code reviewed and approved
Technical Approach
Recommended Implementation
Create cmd/thv/app/vmcp.go following the same pattern as cmd/thv/app/mcp.go and cmd/thv/app/mcp_serve.go:
Define a newVMCPCommand() constructor that creates the parent vmcp command and attaches serve and validate subcommands.
Define newVMCPServeCommand() and newVMCPValidateCommand() as separate constructor functions (or inline within newVMCPCommand() if the file stays compact).
In the RunE of each subcommand, read flag values and construct the appropriate config struct (cli.ServeConfig, cli.ValidateConfig) from Extract shared vMCP logic into pkg/vmcp/cli/ (serve + validate) #4879, then call cli.Serve(ctx, cfg) or cli.Validate(ctx, cfg) and return the error.
In commands.go, add rootCmd.AddCommand(newVMCPCommand()) after the existing rootCmd.AddCommand(newMCPCommand()) line, and add "vmcp": true to the informationalCommands map.
Run task docs to regenerate the CLI reference documentation.
The thin wrapper principle is a hard rule: cmd/thv/app/ files only parse flags, call pkg/ functions, and format output. If any logic beyond flag extraction and function delegation is tempting, it belongs in pkg/vmcp/cli/ instead.
Patterns & Frameworks
Thin wrapper principle: cmd/thv/app/vmcp.go contains only flag definitions, config struct population, and delegation — no business logic
SPDX headers: Every new .go file must open with the two SPDX comment lines exactly as they appear in all other files in cmd/thv/app/
Constructor pattern: Use a newVMCPCommand() function returning *cobra.Command (not a package-level var), consistent with newMCPCommand(), newVersionCmd(), etc.
Flag style: Use cmd.Flags().StringVarP / cmd.Flags().IntVar / cmd.Flags().BoolVar for local flags; use the same flag names as cmd/vmcp/app/commands.go (--host, --port, --enable-audit, --config) for consistency
Error wrapping: Return errors from pkg/vmcp/cli/ directly; wrap with fmt.Errorf("...: %w", err) only if adding context
Signal handling: The serve subcommand's RunE can rely on the context cancellation provided by Cobra's root command — no additional signal.Notify needed in this file (the existing root PersistentPreRunE handles it, or the pkg/vmcp/cli/Serve function handles it internally)
task docs requirement: CLI documentation must be regenerated after any command change; this is enforced by CI
Code Pointers
cmd/thv/app/mcp.go — Direct structural model: newMCPCommand() constructor, subcommand registration, flag helper pattern to follow
cmd/thv/app/mcp_serve.go — Model for the serve subcommand: how to define RunE, handle context, and set flags
cmd/thv/app/commands.go — Two locations to modify: NewRootCmd() (add rootCmd.AddCommand(newVMCPCommand())) and IsInformationalCommand() (add "vmcp": true to the map at line 104)
cmd/vmcp/app/commands.go — Source of truth for flag names, default values, and command descriptions to replicate in the thin wrapper; also shows the full runServe and newValidateCmd logic that was extracted into pkg/vmcp/cli/ by Extract shared vMCP logic into pkg/vmcp/cli/ (serve + validate) #4879
// cmd/thv/app/vmcp.go// newVMCPCommand returns the top-level "vmcp" Cobra command with subcommands attached.funcnewVMCPCommand() *cobra.Command {
cmd:=&cobra.Command{
Use: "vmcp",
Short: "Run and manage a Virtual MCP Server locally",
Long: `The vmcp command provides subcommands to run and validate a Virtual MCPServer (vMCP) locally without Kubernetes. A vMCP aggregates multiple MCPservers from a ToolHive group into a single unified endpoint.`,
}
cmd.AddCommand(newVMCPServeCommand())
cmd.AddCommand(newVMCPValidateCommand())
returncmd
}
// newVMCPServeCommand returns the "vmcp serve" subcommand.funcnewVMCPServeCommand() *cobra.Command {
var (
configPathstringhoststringportintenableAuditbool
)
cmd:=&cobra.Command{
Use: "serve",
Short: "Start the Virtual MCP Server",
Long: `Start the Virtual MCP Server to aggregate and proxy multiple MCP servers.The server reads the configuration file specified by --config and startslistening for MCP client connections, aggregating tools, resources, andprompts from all configured backend MCP servers.`,
RunE: func(cmd*cobra.Command, _ []string) error {
returnvmcpcli.Serve(cmd.Context(), vmcpcli.ServeConfig{
ConfigPath: configPath,
Host: host,
Port: port,
EnableAudit: enableAudit,
})
},
}
cmd.Flags().StringVarP(&configPath, "config", "c", "", "Path to vMCP configuration file (required)")
cmd.Flags().StringVar(&host, "host", "127.0.0.1", "Host address to bind to")
cmd.Flags().IntVar(&port, "port", 4483, "Port to listen on")
cmd.Flags().BoolVar(&enableAudit, "enable-audit", false, "Enable audit logging with default configuration")
_=cmd.MarkFlagRequired("config")
returncmd
}
// newVMCPValidateCommand returns the "vmcp validate" subcommand.funcnewVMCPValidateCommand() *cobra.Command {
varconfigPathstringcmd:=&cobra.Command{
Use: "validate",
Short: "Validate a vMCP configuration file",
Long: `Validate the vMCP configuration file for syntax and semantic errors.This command checks YAML syntax, required field presence, middlewareconfiguration correctness, and backend configuration validity. Exits 0for valid configurations, non-zero with a descriptive error otherwise.`,
RunE: func(cmd*cobra.Command, _ []string) error {
returnvmcpcli.Validate(cmd.Context(), vmcpcli.ValidateConfig{
ConfigPath: configPath,
})
},
}
cmd.Flags().StringVarP(&configPath, "config", "c", "", "Path to vMCP configuration file (required)")
_=cmd.MarkFlagRequired("config")
returncmd
}
// cmd/thv/app/commands.go — changes to IsInformationalCommand()informationalCommands:=map[string]bool{
"version": true,
"search": true,
"completion": true,
"registry": true,
"mcp": true,
"skill": true,
"vmcp": true, // add this line
}
// cmd/thv/app/commands.go — change to NewRootCmd()rootCmd.AddCommand(newMCPCommand())
rootCmd.AddCommand(newVMCPCommand()) // add this line after newMCPCommand()
Note: The exact shape of ServeConfig and ValidateConfig is determined by #4879. The interface shown above reflects what the intake documents and #4879's issue body specify; adjust field names and required/optional flags to match the actual structs produced by #4879 if they differ.
Testing Strategy
Per organizational standards (.claude/rules/testing.md), CLI commands in cmd/thv/app/ are covered by E2E tests only — no unit tests in this package unless testing output formatting or flag validation helpers. The E2E tests for thv vmcp serve and thv vmcp validate are scoped to #4888; this item only needs to verify build correctness and structural integration.
Unit Tests (not applicable for this item — thin wrapper with no business logic)
Running thv vmcp with no subcommand prints help and exits 0 (Cobra default behavior; ensure RunE is not set on the parent command, only Run with cmd.Help())
Passing --config to the parent vmcp command rather than the subcommand is rejected by Cobra with a clear error (flag scoped to subcommands only)
Description
Create
cmd/thv/app/vmcp.gowith thevmcptop-level Cobra command andserveandvalidatesubcommands, wiring all associated flags to thepkg/vmcp/cli/functions established in #4879. Register the new command inNewRootCmd()incommands.goand add"vmcp": trueto theIsInformationalCommand()map so the command bypasses container runtime preflight checks. Regenerate CLI documentation viatask docs. This is Phase 2 of RFC THV-0059 and makesthv vmcp serveandthv vmcp validateavailable to users for the first time.Context
#4879 extracts the vMCP serve and validate business logic from
cmd/vmcp/app/commands.gointo the newpkg/vmcp/cli/package, exposingcli.Serve(ctx, cfg)andcli.Validate(ctx, cfg)as importable Go functions. This item is the CLI wiring layer: it creates the thin Cobra command tree incmd/thv/app/vmcp.gothat parses flags and delegates immediately to those library functions — no business logic belongs here.The existing
cmd/thv/app/mcp.go(and its subcommand inmcp_serve.go) is the direct structural model: a constructor function returning a*cobra.Commandwith subcommands attached, plus a helper function for shared flags. TheinformationalCommandsmap incommands.go(line 104) currently containsversion,search,completion,registry,mcp, andskill;vmcpmust be added here because it does not require a running container runtime.The standalone
cmd/vmcp/app/commands.gobinary is unaffected by this item and continues to function identically for Kubernetes deployments.Dependencies: #4879
Blocks: #4885, #4886
Acceptance Criteria
cmd/thv/app/vmcp.goexists with SPDX copyright header (// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc./// SPDX-License-Identifier: Apache-2.0)newVMCPCommand()function returns a*cobra.CommandwithUse: "vmcp"containingserveandvalidatesubcommandsservesubcommand accepts--config/-c(string, path to vMCP config file),--host(string, default"127.0.0.1"),--port(int, default4483), and--enable-audit(bool, defaultfalse) flags, matching the flag names already present incmd/vmcp/app/commands.govalidatesubcommand accepts--config/-c(string, required) and exits 0 for valid configs and non-zero with a descriptive error message for invalid configspkg/vmcp/cli/functions — zero business logic incmd/thv/app/vmcp.gorootCmd.AddCommand(newVMCPCommand())is added toNewRootCmd()incmd/thv/app/commands.go"vmcp": trueis added to theinformationalCommandsmap inIsInformationalCommand()incmd/thv/app/commands.gothv --helplistsvmcpas an available subcommandthv vmcp --helpshows bothserveandvalidatesubcommands with correct descriptionsthv vmcp serve --config <valid-path>starts the vMCP server and logs its addressthv vmcp validate --config <valid-path>exits 0 and prints config summary; exits non-zero with a descriptive error for an invalid configtask docsis run and the generated CLI documentation indocs/reflects the newvmcpcommandgo build ./cmd/thv/...succeeds with no errorsTechnical Approach
Recommended Implementation
Create
cmd/thv/app/vmcp.gofollowing the same pattern ascmd/thv/app/mcp.goandcmd/thv/app/mcp_serve.go:newVMCPCommand()constructor that creates the parentvmcpcommand and attachesserveandvalidatesubcommands.newVMCPServeCommand()andnewVMCPValidateCommand()as separate constructor functions (or inline withinnewVMCPCommand()if the file stays compact).RunEof each subcommand, read flag values and construct the appropriate config struct (cli.ServeConfig,cli.ValidateConfig) from Extract shared vMCP logic intopkg/vmcp/cli/(serve + validate) #4879, then callcli.Serve(ctx, cfg)orcli.Validate(ctx, cfg)and return the error.commands.go, addrootCmd.AddCommand(newVMCPCommand())after the existingrootCmd.AddCommand(newMCPCommand())line, and add"vmcp": trueto theinformationalCommandsmap.task docsto regenerate the CLI reference documentation.The thin wrapper principle is a hard rule:
cmd/thv/app/files only parse flags, callpkg/functions, and format output. If any logic beyond flag extraction and function delegation is tempting, it belongs inpkg/vmcp/cli/instead.Patterns & Frameworks
cmd/thv/app/vmcp.gocontains only flag definitions, config struct population, and delegation — no business logic.gofile must open with the two SPDX comment lines exactly as they appear in all other files incmd/thv/app/newVMCPCommand()function returning*cobra.Command(not a package-levelvar), consistent withnewMCPCommand(),newVersionCmd(), etc.cmd.Flags().StringVarP/cmd.Flags().IntVar/cmd.Flags().BoolVarfor local flags; use the same flag names ascmd/vmcp/app/commands.go(--host,--port,--enable-audit,--config) for consistencypkg/vmcp/cli/directly; wrap withfmt.Errorf("...: %w", err)only if adding contextservesubcommand'sRunEcan rely on the context cancellation provided by Cobra's root command — no additionalsignal.Notifyneeded in this file (the existing rootPersistentPreRunEhandles it, or thepkg/vmcp/cli/Servefunction handles it internally)task docsrequirement: CLI documentation must be regenerated after any command change; this is enforced by CICode Pointers
cmd/thv/app/mcp.go— Direct structural model:newMCPCommand()constructor, subcommand registration, flag helper pattern to followcmd/thv/app/mcp_serve.go— Model for theservesubcommand: how to defineRunE, handle context, and set flagscmd/thv/app/commands.go— Two locations to modify:NewRootCmd()(addrootCmd.AddCommand(newVMCPCommand())) andIsInformationalCommand()(add"vmcp": trueto the map at line 104)cmd/vmcp/app/commands.go— Source of truth for flag names, default values, and command descriptions to replicate in the thin wrapper; also shows the fullrunServeandnewValidateCmdlogic that was extracted intopkg/vmcp/cli/by Extract shared vMCP logic intopkg/vmcp/cli/(serve + validate) #4879pkg/vmcp/cli/serve.go(created by Extract shared vMCP logic intopkg/vmcp/cli/(serve + validate) #4879) — TheServe(ctx context.Context, cfg ServeConfig) errorfunction andServeConfigstruct this item must callpkg/vmcp/cli/validate.go(created by Extract shared vMCP logic intopkg/vmcp/cli/(serve + validate) #4879) — TheValidate(ctx context.Context, cfg ValidateConfig) errorfunction andValidateConfigstruct this item must call.claude/rules/cli-commands.md— Thin wrapper principle; adding new commands checklist; E2E-first testing policy for CLI commands.claude/rules/go-style.md— SPDX header requirement, error handling conventionsComponent Interfaces
Note: The exact shape of
ServeConfigandValidateConfigis determined by #4879. The interface shown above reflects what the intake documents and #4879's issue body specify; adjust field names and required/optional flags to match the actual structs produced by #4879 if they differ.Testing Strategy
Per organizational standards (
.claude/rules/testing.md), CLI commands incmd/thv/app/are covered by E2E tests only — no unit tests in this package unless testing output formatting or flag validation helpers. The E2E tests forthv vmcp serveandthv vmcp validateare scoped to #4888; this item only needs to verify build correctness and structural integration.Unit Tests (not applicable for this item — thin wrapper with no business logic)
pkg/vmcp/cli/(Extract shared vMCP logic intopkg/vmcp/cli/(serve + validate) #4879)Integration Tests
go build ./cmd/thv/...passes — confirms the new command compiles and the import frompkg/vmcp/cli/resolves correctlythv --helpoutput includesvmcpin the subcommand list (verifiable via a simple shell assertion in CI or manually)thv vmcp validate --config <missing-file>exits non-zero with a descriptive error (smoke test; full coverage in E2E tests: quick mode and config-file mode #4888)Edge Cases
thv vmcpwith no subcommand prints help and exits 0 (Cobra default behavior; ensureRunEis not set on the parent command, onlyRunwithcmd.Help())--configto the parentvmcpcommand rather than the subcommand is rejected by Cobra with a clear error (flag scoped to subcommands only)Out of Scope
thv vmcp initsubcommand — that is Addthv vmcp initsubcommand #4885 (depends on Addinit.gotopkg/vmcp/cli/for config scaffolding #4882 as well)--optimizer,--optimizer-embedding,--embedding-model, and--embedding-imageflags forserve— those are Wire optimizer flags intothv vmcp serve#4887thv vmcp serve --group <name>without--config) — that is Implement zero-config quick mode forthv vmcp serve#4886thv vmcplifecycle — those are E2E tests: quick mode and config-file mode #4888EmbeddingServiceManagerimplementation — that is Implement EmbeddingServiceManager in pkg/vmcp/cli/ #4884vmcpbinary (cmd/vmcp/) — unchanged by this itempkg/vmcp/packages beyond what is importedReferences
thv vmcp serveandthv vmcp validatesubcommandscmd/thv/app/mcp.goandcmd/thv/app/mcp_serve.go— Direct structural models forvmcp.gocmd/thv/app/commands.go— Registration point forNewRootCmd()andIsInformationalCommand()cmd/vmcp/app/commands.go— Source of flag names, defaults, and descriptions to replicatedocs/arch/10-virtual-mcp-architecture.md— Existing vMCP architecture documentation.claude/rules/cli-commands.md— Thin wrapper principle and new command checklist