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 zero-config quick-mode logic to pkg/vmcp/cli/serve.go: when no --config flag is provided but --group is specified, auto-generate a minimal in-memory *config.Config struct that sets groupRef from --group and binds the listener to 127.0.0.1 only. This eliminates the need for a config file for the common case of aggregating a ToolHive group, making thv vmcp serve --group <name> fully self-contained. Add unit tests covering the required groupRef field and the localhost binding security constraint.
Context
#4879 created pkg/vmcp/cli/serve.go with Serve(ctx, ServeConfig) and a ServeConfig struct. #4883 created cmd/thv/app/vmcp.go with the thv vmcp serve Cobra command, wired with a --config flag that is currently marked required. This item relaxes that requirement: when --config is absent, the serve logic in pkg/vmcp/cli/serve.go generates a minimal in-memory config from --group rather than returning an error. The --group flag is already present on the serve subcommand (established in #4883) and must be populated for quick mode to proceed.
RFC THV-0059 specifies this as Phase 3 quick mode: the auto-generated config uses dynamic backend discovery (groups manager), sets outgoingAuth.source: "inline" to avoid K8s API access, binds to 127.0.0.1 for security, and leaves all optional fields at their zero values so the full config validation path still runs. The generated config is purely in-memory — no YAML file is written to disk.
ServeConfig in pkg/vmcp/cli/serve.go gains a GroupRef string field (the --group flag value) alongside the existing ConfigPath string field
Serve() returns a descriptive error when both ConfigPath and GroupRef are empty (neither --config nor --group was given)
Serve() returns a descriptive error when GroupRef is empty and ConfigPath is also empty, with a message guiding the user to pass either --config or --group
When ConfigPath is empty and GroupRef is non-empty, Serve() generates a minimal in-memory *config.Config with Group (i.e. groupRef) set to the GroupRef value and OutgoingAuth.Source set to "inline"
The auto-generated config does not set a Host or Port override (those are controlled by ServeConfig.Host and ServeConfig.Port as before); the listener address remains 127.0.0.1 as the default for ServeConfig.Host
The auto-generated config passes config.NewValidator().Validate() without error
The --config flag on thv vmcp serve is no longer marked as required in cmd/thv/app/vmcp.go; omitting it while providing --group is valid
thv vmcp serve --group <name> starts successfully (smoke-level: no flag-parsing or config-generation error before the server attempts to bind)
thv vmcp serve with neither --config nor --group exits non-zero with a descriptive error
Unit tests in pkg/vmcp/cli/serve_test.go (or a dedicated quick_mode_test.go) cover: (a) quick-mode config generation sets groupRef correctly, (b) quick-mode config generation sets outgoingAuth.source to "inline", (c) the generated config passes NewValidator().Validate(), (d) both flags absent returns an error
All existing tests pass (no regressions)
Code reviewed and approved
Technical Approach
Recommended Implementation
The change is confined to pkg/vmcp/cli/serve.go (business logic) and the small flag-marking change in cmd/thv/app/vmcp.go (remove _ = cmd.MarkFlagRequired("config")).
In pkg/vmcp/cli/serve.go:
Add GroupRef string to ServeConfig.
At the top of Serve(), after reading cfg.ConfigPath and cfg.GroupRef, implement a branching config-load path:
If ConfigPath != "": use the existing loadAndValidateConfig(cfg.ConfigPath) path (unchanged).
If GroupRef != "" (and ConfigPath == ""): call a new unexported generateQuickModeConfig(groupRef string) (*config.Config, error) helper.
If both are empty: return fmt.Errorf("either --config or --group must be specified").
Implement generateQuickModeConfig:
Construct a *config.Config in-memory with Group: groupRef and OutgoingAuth: &config.OutgoingAuthConfig{Source: "inline"}.
Run config.NewValidator().Validate(cfg) before returning — fail fast if the minimal config is structurally invalid.
Return the validated config.
In cmd/thv/app/vmcp.go:
Remove _ = cmd.MarkFlagRequired("config") from newVMCPServeCommand().
Add cmd.Flags().StringVar(&group, "group", "", "ToolHive group name (used for zero-config quick mode when --config is omitted)").
Pass the group flag value as GroupRef in the vmcpcli.ServeConfig struct literal.
Patterns & Frameworks
Thin wrapper principle: All branching logic (--config vs --group) lives in pkg/vmcp/cli/serve.go, not in cmd/thv/app/vmcp.go. The Cobra command only passes flag values through.
SPDX headers: New files must open with // SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc. and // SPDX-License-Identifier: Apache-2.0.
Immutable variable assignment: Use immediately-assigned variables rather than var cfg *config.Config reassigned across branches; use an immediately-invoked anonymous function or a helper function to keep cfg single-assignment.
Error wrapping: fmt.Errorf("...: %w", err) for all wrapped errors; descriptive messages that name the missing flag.
Strict YAML parsing: The generated config is never serialized to YAML; it goes directly into the existing serve pipeline, so no marshaling/unmarshaling is needed.
No new external Go module dependencies: the config package is already imported in serve.go; no new imports.
Unit test style: Table-driven tests using testify/require assertions alongside source in pkg/vmcp/cli/; use config.NewValidator() directly to assert the generated config passes validation.
pkg/vmcp/config/config.go — Config struct (the Group field has YAML tag groupRef; OutgoingAuthConfig.Source must be "inline" for static/local mode); NewValidator() / Validator.Validate() for pre-flight check of generated config
cmd/vmcp/app/commands.go — Reference for how the existing serve path calls loadAndValidateConfig; shows what fields Config must minimally contain for the serve pipeline to proceed
pkg/vmcp/cli/serve_test.go (or new quick_mode_test.go) — Unit test file to create; follow table-driven test patterns from test/integration/vmcp/helpers/vmcp_server.go and .claude/rules/testing.md
.claude/rules/testing.md — Unit test style, testify/require, table-driven tests for pkg/ packages
Component Interfaces
// pkg/vmcp/cli/serve.go// ServeConfig holds all parameters for starting the vMCP server.// Exactly one of ConfigPath or GroupRef must be non-empty.typeServeConfigstruct {
ConfigPathstring// path to YAML config file; takes precedence over GroupRefGroupRefstring// ToolHive group name; used when ConfigPath is empty (quick mode)HoststringPortintEnableAuditbool
}
// generateQuickModeConfig constructs a minimal in-memory config for quick mode.// It sets groupRef from groupRef and outgoingAuth.source to "inline", then// validates the result. Returns an error if groupRef is empty or if validation fails.funcgenerateQuickModeConfig(groupRefstring) (*config.Config, error) {
cfg:=&config.Config{
Group: groupRef,
OutgoingAuth: &config.OutgoingAuthConfig{
Source: "inline",
},
}
validator:=config.NewValidator()
iferr:=validator.Validate(cfg); err!=nil {
returnnil, fmt.Errorf("quick-mode config validation failed: %w", err)
}
returncfg, nil
}
generateQuickModeConfig("default") returns a config with Group == "default" and OutgoingAuth.Source == "inline"
generateQuickModeConfig("") returns a non-nil error (empty groupRef is not valid)
The generated config (non-empty groupRef) passes config.NewValidator().Validate() without error
Serve() with both ConfigPath == "" and GroupRef == "" returns an error containing guidance to pass --config or --group
Serve() with GroupRef == "mygroup" and ConfigPath == "" calls generateQuickModeConfig path (verify by checking no file-not-found error before the bind step; can use a context that cancels immediately to limit execution)
pkg/vmcp/config/config.go — Config.Group field (groupRef YAML key), OutgoingAuthConfig.Source, NewValidator()
cmd/vmcp/app/commands.go — Existing runServe and loadAndValidateConfig logic that Serve() delegates to; shows required config fields for the serve pipeline
Description
Add zero-config quick-mode logic to
pkg/vmcp/cli/serve.go: when no--configflag is provided but--groupis specified, auto-generate a minimal in-memory*config.Configstruct that setsgroupReffrom--groupand binds the listener to127.0.0.1only. This eliminates the need for a config file for the common case of aggregating a ToolHive group, makingthv vmcp serve --group <name>fully self-contained. Add unit tests covering the requiredgroupReffield and the localhost binding security constraint.Context
#4879 created
pkg/vmcp/cli/serve.gowithServe(ctx, ServeConfig)and aServeConfigstruct. #4883 createdcmd/thv/app/vmcp.gowith thethv vmcp serveCobra command, wired with a--configflag that is currently marked required. This item relaxes that requirement: when--configis absent, the serve logic inpkg/vmcp/cli/serve.gogenerates a minimal in-memory config from--grouprather than returning an error. The--groupflag is already present on the serve subcommand (established in #4883) and must be populated for quick mode to proceed.RFC THV-0059 specifies this as Phase 3 quick mode: the auto-generated config uses dynamic backend discovery (groups manager), sets
outgoingAuth.source: "inline"to avoid K8s API access, binds to127.0.0.1for security, and leaves all optional fields at their zero values so the full config validation path still runs. The generated config is purely in-memory — no YAML file is written to disk.Dependencies: #4883
Blocks: #4887, #4888
Acceptance Criteria
ServeConfiginpkg/vmcp/cli/serve.gogains aGroupRef stringfield (the--groupflag value) alongside the existingConfigPath stringfieldServe()returns a descriptive error when bothConfigPathandGroupRefare empty (neither--confignor--groupwas given)Serve()returns a descriptive error whenGroupRefis empty andConfigPathis also empty, with a message guiding the user to pass either--configor--groupConfigPathis empty andGroupRefis non-empty,Serve()generates a minimal in-memory*config.ConfigwithGroup(i.e.groupRef) set to theGroupRefvalue andOutgoingAuth.Sourceset to"inline"HostorPortoverride (those are controlled byServeConfig.HostandServeConfig.Portas before); the listener address remains127.0.0.1as the default forServeConfig.Hostconfig.NewValidator().Validate()without error--configflag onthv vmcp serveis no longer marked as required incmd/thv/app/vmcp.go; omitting it while providing--groupis validthv vmcp serve --group <name>starts successfully (smoke-level: no flag-parsing or config-generation error before the server attempts to bind)thv vmcp servewith neither--confignor--groupexits non-zero with a descriptive errorpkg/vmcp/cli/serve_test.go(or a dedicatedquick_mode_test.go) cover: (a) quick-mode config generation setsgroupRefcorrectly, (b) quick-mode config generation setsoutgoingAuth.sourceto"inline", (c) the generated config passesNewValidator().Validate(), (d) both flags absent returns an errorTechnical Approach
Recommended Implementation
The change is confined to
pkg/vmcp/cli/serve.go(business logic) and the small flag-marking change incmd/thv/app/vmcp.go(remove_ = cmd.MarkFlagRequired("config")).In
pkg/vmcp/cli/serve.go:GroupRef stringtoServeConfig.Serve(), after readingcfg.ConfigPathandcfg.GroupRef, implement a branching config-load path:ConfigPath != "": use the existingloadAndValidateConfig(cfg.ConfigPath)path (unchanged).GroupRef != ""(andConfigPath == ""): call a new unexportedgenerateQuickModeConfig(groupRef string) (*config.Config, error)helper.fmt.Errorf("either --config or --group must be specified").generateQuickModeConfig:*config.Configin-memory withGroup: groupRefandOutgoingAuth: &config.OutgoingAuthConfig{Source: "inline"}.config.NewValidator().Validate(cfg)before returning — fail fast if the minimal config is structurally invalid.In
cmd/thv/app/vmcp.go:_ = cmd.MarkFlagRequired("config")fromnewVMCPServeCommand().cmd.Flags().StringVar(&group, "group", "", "ToolHive group name (used for zero-config quick mode when --config is omitted)").groupflag value asGroupRefin thevmcpcli.ServeConfigstruct literal.Patterns & Frameworks
--configvs--group) lives inpkg/vmcp/cli/serve.go, not incmd/thv/app/vmcp.go. The Cobra command only passes flag values through.// SPDX-FileCopyrightText: Copyright 2025 Stacklok, Inc.and// SPDX-License-Identifier: Apache-2.0.var cfg *config.Configreassigned across branches; use an immediately-invoked anonymous function or a helper function to keepcfgsingle-assignment.fmt.Errorf("...: %w", err)for all wrapped errors; descriptive messages that name the missing flag.configpackage is already imported inserve.go; no new imports.testify/requireassertions alongside source inpkg/vmcp/cli/; useconfig.NewValidator()directly to assert the generated config passes validation.Code Pointers
pkg/vmcp/cli/serve.go(created by Extract shared vMCP logic intopkg/vmcp/cli/(serve + validate) #4879) — Primary file to modify; addGroupReftoServeConfig, addgenerateQuickModeConfig, branch at top ofServe()pkg/vmcp/config/config.go—Configstruct (theGroupfield has YAML taggroupRef;OutgoingAuthConfig.Sourcemust be"inline"for static/local mode);NewValidator()/Validator.Validate()for pre-flight check of generated configcmd/thv/app/vmcp.go(created by Addthv vmcp serveandthv vmcp validatesubcommands #4883) — RemoveMarkFlagRequired("config"), add--groupflag, passGroupRefinServeConfigcmd/vmcp/app/commands.go— Reference for how the existing serve path callsloadAndValidateConfig; shows what fieldsConfigmust minimally contain for the serve pipeline to proceedpkg/vmcp/cli/serve_test.go(or newquick_mode_test.go) — Unit test file to create; follow table-driven test patterns fromtest/integration/vmcp/helpers/vmcp_server.goand.claude/rules/testing.md.claude/rules/go-style.md— Immutable variable assignment guidance, error handling, SPDX headers.claude/rules/testing.md— Unit test style,testify/require, table-driven tests forpkg/packagesComponent Interfaces
Testing Strategy
Unit Tests
generateQuickModeConfig("default")returns a config withGroup == "default"andOutgoingAuth.Source == "inline"generateQuickModeConfig("")returns a non-nil error (empty groupRef is not valid)config.NewValidator().Validate()without errorServe()with bothConfigPath == ""andGroupRef == ""returns an error containing guidance to pass--configor--groupServe()withGroupRef == "mygroup"andConfigPath == ""callsgenerateQuickModeConfigpath (verify by checking no file-not-found error before the bind step; can use a context that cancels immediately to limit execution)Integration Tests
Edge Cases
ConfigPathandGroupRefare set,ConfigPathtakes precedence (file-based path is preferred and theGroupRefis ignored)GroupRefcontaining special characters (spaces, slashes) is propagated as-is tocfg.Group— validation is delegated toconfig.NewValidator()Out of Scope
--optimizer,--optimizer-embedding,--embedding-model, and--embedding-imageflags — those are Wire optimizer flags intothv vmcp serve#4887thv vmcp initsubcommand — that is Addthv vmcp initsubcommand #4885thv vmcp initcovers that)vmcpbinary (cmd/vmcp/) — preserved unchangedoutgoingAuth.source: "inline"onlyReferences
127.0.0.1binding,groupRefpopulation, andoutgoingAuth.source: "inline"for static discoverypkg/vmcp/config/config.go—Config.Groupfield (groupRefYAML key),OutgoingAuthConfig.Source,NewValidator()cmd/vmcp/app/commands.go— ExistingrunServeandloadAndValidateConfiglogic thatServe()delegates to; shows required config fields for the serve pipelinecmd/thv/app/vmcp.go(Addthv vmcp serveandthv vmcp validatesubcommands #4883) — Thin wrapper where--configflag marking must be relaxed and--groupflag added.claude/rules/cli-commands.md— Thin wrapper principle; new flag checklist.claude/rules/go-style.md— SPDX headers, immutable variable assignment, error wrapping.claude/rules/testing.md— Unit test strategy forpkg/packages; table-driven tests withtestify/require