Skip to content

Add thv vmcp serve and thv vmcp validate subcommands #4883

@yrobla

Description

@yrobla

Description

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.

Dependencies: #4879
Blocks: #4885, #4886

Acceptance Criteria

  • 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:

  1. Define a newVMCPCommand() constructor that creates the parent vmcp command and attaches serve and validate subcommands.
  2. Define newVMCPServeCommand() and newVMCPValidateCommand() as separate constructor functions (or inline within newVMCPCommand() if the file stays compact).
  3. 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.
  4. In commands.go, add rootCmd.AddCommand(newVMCPCommand()) after the existing rootCmd.AddCommand(newMCPCommand()) line, and add "vmcp": true to the informationalCommands map.
  5. 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
  • pkg/vmcp/cli/serve.go (created by Extract shared vMCP logic into pkg/vmcp/cli/ (serve + validate) #4879) — The Serve(ctx context.Context, cfg ServeConfig) error function and ServeConfig struct this item must call
  • pkg/vmcp/cli/validate.go (created by Extract shared vMCP logic into pkg/vmcp/cli/ (serve + validate) #4879) — The Validate(ctx context.Context, cfg ValidateConfig) error function and ValidateConfig struct 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 conventions

Component Interfaces

// cmd/thv/app/vmcp.go

// newVMCPCommand returns the top-level "vmcp" Cobra command with subcommands attached.
func newVMCPCommand() *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 MCP
Server (vMCP) locally without Kubernetes. A vMCP aggregates multiple MCP
servers from a ToolHive group into a single unified endpoint.`,
    }
    cmd.AddCommand(newVMCPServeCommand())
    cmd.AddCommand(newVMCPValidateCommand())
    return cmd
}

// newVMCPServeCommand returns the "vmcp serve" subcommand.
func newVMCPServeCommand() *cobra.Command {
    var (
        configPath  string
        host        string
        port        int
        enableAudit bool
    )
    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 starts
listening for MCP client connections, aggregating tools, resources, and
prompts from all configured backend MCP servers.`,
        RunE: func(cmd *cobra.Command, _ []string) error {
            return vmcpcli.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")
    return cmd
}

// newVMCPValidateCommand returns the "vmcp validate" subcommand.
func newVMCPValidateCommand() *cobra.Command {
    var configPath string
    cmd := &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, middleware
configuration correctness, and backend configuration validity. Exits 0
for valid configurations, non-zero with a descriptive error otherwise.`,
        RunE: func(cmd *cobra.Command, _ []string) error {
            return vmcpcli.Validate(cmd.Context(), vmcpcli.ValidateConfig{
                ConfigPath: configPath,
            })
        },
    }
    cmd.Flags().StringVarP(&configPath, "config", "c", "", "Path to vMCP configuration file (required)")
    _ = cmd.MarkFlagRequired("config")
    return cmd
}
// 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)

Integration Tests

  • go build ./cmd/thv/... passes — confirms the new command compiles and the import from pkg/vmcp/cli/ resolves correctly
  • thv --help output includes vmcp in 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

  • 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)

Out of Scope

References

  • RFC THV-0059 — Authoritative design; Phase 2 specifies thv vmcp serve and thv vmcp validate subcommands
  • GitHub Issue #4808 — Parent tracking issue
  • cmd/thv/app/mcp.go and cmd/thv/app/mcp_serve.go — Direct structural models for vmcp.go
  • cmd/thv/app/commands.go — Registration point for NewRootCmd() and IsInformationalCommand()
  • cmd/vmcp/app/commands.go — Source of flag names, defaults, and descriptions to replicate
  • docs/arch/10-virtual-mcp-architecture.md — Existing vMCP architecture documentation
  • .claude/rules/cli-commands.md — Thin wrapper principle and new command checklist

Metadata

Metadata

Assignees

No one assigned

    Labels

    cliChanges that impact CLI functionalityenhancementNew feature or requestvmcpVirtual MCP Server related issues

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions