Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,9 @@ jobs:

- name: Run unit tests with coverage
run: |
module_path="$(go list -m)"
go test -v -race -coverprofile=unit.out \
$(go list ./... | grep -v '^github.com/Agent-Hellboy/mcp-runtime/test')
$(go list ./... | grep -v -x "${module_path}/test/integration")

- name: Test Sentinel service modules (api, ui)
run: |
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ If instructions conflict, prefer **this repo** (`README`, CRDs, `v1alpha1` types

| Area | Path | Notes |
|------|------|--------|
| User-facing CLI | `cmd/mcp-runtime/`, `internal/cli/root/`, `internal/cli/` | Entrypoint, foldered Cobra command routing, and command behavior for `setup`, `status`, `registry`, `server`, `access`, … |
| User-facing CLI | `cmd/mcp-runtime/`, `internal/cli/root/`, `internal/cli/<command>/`, `internal/cli/core/` | Entrypoint, foldered Cobra command routing, command-owned behavior for `setup`, `status`, `registry`, `server`, `access`, …, and shared CLI kernel code |
| Operator (controller) | `cmd/operator/`, `internal/operator/` | `MCPServer` reconciliation, ingress, gateway wiring |
| API & CRD types | `api/v1alpha1/` | Source of truth for object shapes; CRD YAML in `config/crd/bases/` |
| Access control (shared) | `pkg/access/` | Grants, sessions, policy pieces used by API and gateway |
Expand All @@ -21,7 +21,7 @@ If instructions conflict, prefer **this repo** (`README`, CRDs, `v1alpha1` types
| E2E | `test/e2e/`, `test/integration/` | Kind script and envtest-based integration tests |
| Agent tool config | `.claude/`, `.codex/skills/` | `.claude/skills` should symlink to `../.codex/skills` so Claude Desktop and the Codex CLI use the same local skills |

**Patterns worth mirroring:** search for similar packages before adding new abstractions; keep CLI errors consistent with `internal/cli/errors.go` and `pkg/errx/`.
**Patterns worth mirroring:** search for similar packages before adding new abstractions; keep CLI errors consistent with `internal/cli/core/errors.go` and `pkg/errx/`.

## Build, test, and quality (before you push)

Expand Down
31 changes: 29 additions & 2 deletions cmd/mcp-runtime/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"

"mcp-runtime/internal/cli"
"mcp-runtime/internal/cli/core"
cliroot "mcp-runtime/internal/cli/root"
)

Expand Down Expand Up @@ -44,9 +44,15 @@ var rootCmd = &cobra.Command{
- MCP server deployments
- Platform configuration`,
Version: fmt.Sprintf("%s (commit: %s, built: %s)", version, commit, date),
// Keep usage enabled during Cobra validation; command wrappers disable it
// after validation passes so runtime errors do not dump command help.
SilenceUsage: false,
// main() prints the error itself, so silence Cobra's own error print to
// avoid duplicates.
SilenceErrors: true,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
// Set debug mode globally so logStructuredError can check it
cli.SetDebugMode(debug)
core.SetDebugMode(debug)
},
}

Expand All @@ -56,6 +62,27 @@ func init() {

func initCommands(logger *zap.Logger) {
cliroot.AddCommands(rootCmd, logger)
silenceUsageAfterValidation(rootCmd)
}

func silenceUsageAfterValidation(cmd *cobra.Command) {
if cmd.RunE != nil {
runE := cmd.RunE
cmd.RunE = func(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true
return runE(cmd, args)
}
}
if cmd.Run != nil {
run := cmd.Run
cmd.Run = func(cmd *cobra.Command, args []string) {
cmd.SilenceUsage = true
run(cmd, args)
}
}
for _, child := range cmd.Commands() {
silenceUsageAfterValidation(child)
}
}

// newConsoleLogger returns a human-friendly console logger with timestamps and caller info.
Expand Down
54 changes: 54 additions & 0 deletions cmd/mcp-runtime/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package main

import (
"bytes"
"errors"
"strings"
"testing"

"github.com/spf13/cobra"
)

func TestRootCommandHelp(t *testing.T) {
Expand All @@ -28,3 +31,54 @@ func TestRootCommandHelp(t *testing.T) {
t.Fatalf("help output missing expected text")
}
}

func TestSilenceUsageAfterValidation(t *testing.T) {
t.Run("keeps usage for validation errors", func(t *testing.T) {
root := &cobra.Command{Use: "root", SilenceErrors: true}
cmd := &cobra.Command{
Use: "needs-arg [name]",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return nil
},
}
root.AddCommand(cmd)
silenceUsageAfterValidation(root)

var out bytes.Buffer
root.SetOut(&out)
root.SetErr(&out)
root.SetArgs([]string{"needs-arg"})

if err := root.Execute(); err == nil {
t.Fatal("expected validation error")
}
if !strings.Contains(out.String(), "Usage:") {
t.Fatalf("expected usage for validation error, got: %q", out.String())
}
})

t.Run("hides usage for runtime errors", func(t *testing.T) {
root := &cobra.Command{Use: "root", SilenceErrors: true}
cmd := &cobra.Command{
Use: "runtime",
RunE: func(cmd *cobra.Command, args []string) error {
return errors.New("runtime failed")
},
}
root.AddCommand(cmd)
silenceUsageAfterValidation(root)

var out bytes.Buffer
root.SetOut(&out)
root.SetErr(&out)
root.SetArgs([]string{"runtime"})

if err := root.Execute(); err == nil {
t.Fatal("expected runtime error")
}
if strings.Contains(out.String(), "Usage:") {
t.Fatalf("did not expect usage for runtime error, got: %q", out.String())
}
})
}
2 changes: 1 addition & 1 deletion docs/cluster-readiness.md
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@ Missing pieces are warnings, not errors — the command surfaces them so you can
`./bin/mcp-runtime cluster doctor` runs post-install diagnostics:

- Detects your distribution (k3s / kind / minikube / docker-desktop / generic).
- Checks the installed MCP Runtime namespaces, CRDs, operator, Traefik ingress, registry, Sentinel, and MCPServer reconciliation path, including readiness of the temporary smoke deployment.
- Checks the installed MCP Runtime namespaces, CRDs, operator, Traefik ingress, registry, Sentinel, and MCPServer reconciliation path. The MCPServer smoke uses an existing ready app image when available; otherwise it falls back to `registry.k8s.io/pause:3.9` and validates deployment/service/ingress reconciliation plus pod scheduling without a TCP readiness wait.
- Prefers k3s' bundled Traefik in `kube-system/traefik` when the active cluster is k3s, then falls back to the repo-managed `traefik/traefik` install.
- Verifies registry reachability, registry image-pull smoke behavior, and common pod image-pull failures.
- Reports `http: server gave HTTP response to HTTPS client` when kubelet/containerd tried HTTPS against the HTTP dev registry, including the affected pod and image where possible.
Expand Down
15 changes: 8 additions & 7 deletions docs/internals/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,12 @@ Governance-related changes usually span `api/v1alpha1/access_types.go`, `pkg/acc
```mermaid
flowchart TB
Cmd[cmd/mcp-runtime] --> CLIRoot[internal/cli/root]
CLIRoot --> InternalCLI[internal/cli]
InternalCLI --> Metadata[pkg/metadata]
InternalCLI --> Manifest[pkg/manifest]
InternalCLI --> K8sClient[pkg/k8sclient]
InternalCLI --> Access[pkg/access]
CLIRoot --> CLICommands[internal/cli/<command>]
CLICommands --> CLICore[internal/cli/core]
CLICommands --> Metadata[pkg/metadata]
CLICommands --> Manifest[pkg/manifest]
CLICommands --> K8sClient[pkg/k8sclient]
CLICommands --> Access[pkg/access]
CmdOp[cmd/operator] --> Operator[internal/operator]
Operator --> API[api/v1alpha1]
Operator --> K8sClient
Expand All @@ -118,7 +119,7 @@ flowchart TB
Metadata --> API
```

Keep shared behavior in `pkg/` only when multiple binaries or services need it. CLI top-level command routing belongs in `internal/cli/root` and `internal/cli/<command>`; CLI-only behavior belongs in `internal/cli`; reconciliation behavior belongs in `internal/operator`; HTTP service glue belongs near the service that owns the endpoint.
Keep shared behavior in `pkg/` only when multiple binaries or services need it. CLI top-level command routing belongs in `internal/cli/root` and `internal/cli/<command>`; CLI-only shared infrastructure belongs in `internal/cli/core`; reconciliation behavior belongs in `internal/operator`; HTTP service glue belongs near the service that owns the endpoint.

## Learning path

Expand Down Expand Up @@ -148,7 +149,7 @@ workflows.

| Change | Read first | Verify with |
|---|---|---|
| Add or change a CLI flag | `internal/cli/root`, `internal/cli`, `cmd/mcp-runtime`, golden CLI tests | `go test ./internal/cli/... ./test/golden/... -count=1` |
| Add or change a CLI flag | `internal/cli/root`, `internal/cli/<command>`, `internal/cli/core`, `cmd/mcp-runtime`, golden CLI tests | `go test ./internal/cli/... ./test/golden/... -count=1` |
| Change a CRD field | `api/v1alpha1`, CRD YAML, operator reconciliation, docs/API reference | `go test ./api/v1alpha1/... ./internal/operator/... -count=1` |
| Change generated manifests | `pkg/metadata`, `pkg/manifest`, `config/`, examples | targeted package tests plus manifest diff review |
| Change reconciliation behavior | `internal/operator`, API types, k8s helpers | `go test ./internal/operator/... -race -count=1` |
Expand Down
33 changes: 17 additions & 16 deletions docs/internals/cmd-mcp-runtime.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,26 @@ go doc -cmd ./cmd/mcp-runtime

The entrypoint should not contain business logic for setup, registry, server,
access, or Sentinel behavior. Route top-level commands through
`internal/cli/root`, and keep command behavior in `internal/cli` until a focused
migration moves that domain into its own command package.
`internal/cli/root`. Command folders should own Cobra wiring and, where already
migrated, package-local managers; shared CLI-only infrastructure lives in
`internal/cli/core`.

## Command Tree

The root command wires these internal command groups:

| Command | Routing package | Behavior files |
|---|---|---|
| `bootstrap` | `internal/cli/bootstrap` | `internal/cli/bootstrap.go` |
| `cluster` | `internal/cli/cluster` | `internal/cli/cluster.go` and `cluster_doctor.go` |
| `setup` | `internal/cli/setup` | `internal/cli/setup.go`, `setup_plan.go`, `setup_steps.go` |
| `status` | `internal/cli/status` | `internal/cli/status.go` |
| `registry` | `internal/cli/registry` | `internal/cli/registry.go` |
| `server` | `internal/cli/server` | `internal/cli/server.go`, `build.go` |
| `pipeline` | `internal/cli/pipeline` | `internal/cli/pipeline.go` |
| `access` | `internal/cli/access` | `internal/cli/access.go` |
| `auth` | `internal/cli/auth` | `internal/cli/auth.go` |
| `sentinel` | `internal/cli/sentinel` | `internal/cli/sentinel.go` |
| `bootstrap` | `internal/cli/bootstrap` | `bootstrap.go` |
| `cluster` | `internal/cli/cluster` | `cluster.go`, `manager.go`, `doctor.go`, `doctor_impl.go`, `register.go`, … |
| `setup` | `internal/cli/setup` | `setup.go`, `platform.go`, `flow.go`, `steps.go`, `providers.go`, setup-owned helpers under `internal/cli/setup/` |
| `status` | `internal/cli/status` | `status.go`, shared workload/probe helpers in `internal/cli/platformstatus` |
| `registry` | `internal/cli/registry` | `registry.go`, `manager.go`, `defaults.go`, registry-owned helpers under `internal/cli/registry/` |
| `server` | `internal/cli/server` | `server.go`, `manager.go`, `validation.go`, `build.go`, `build_image.go`, server-owned helpers under `internal/cli/server/` |
| `pipeline` | `internal/cli/pipeline` | `command.go`, `generate.go`, `deploy.go` |
| `access` | `internal/cli/access` | `access.go`, `manager.go`, `validation.go` |
| `auth` | `internal/cli/auth` | `auth.go` |
| `sentinel` | `internal/cli/sentinel` | `sentinel.go`, `manager.go`, shared workload/probe helpers in `internal/cli/platformstatus` |

When adding a command, wire it here only after the implementation has focused
package tests and help text is ready for golden snapshots.
Expand All @@ -53,10 +54,10 @@ CLI UX changes should preserve these expectations:
- Logs are readable in terminals and CI.
- Global flags stay minimal; feature-specific flags belong on their command.
- Commands that shell out to external tools are testable through runner
abstractions in `internal/cli`.
- Top-level command folders under `internal/cli/<command>` should stay thin
while they delegate to `internal/cli`; move behavior there only as a focused
follow-up with package-local tests.
abstractions in `internal/cli/core`.
- Top-level command folders under `internal/cli/<command>` should keep Cobra
wiring thin and delegate to package-local managers or explicit shared
services.

Before changing this package, run:

Expand Down
Loading
Loading