Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
ef66d5a
Migrate to GO SDK
dimetron Jun 24, 2026
303449c
fix(security): resolve govulncheck CVEs and relax MCP SDK input schema
dimetron Jun 30, 2026
ca15b0f
feat(utils): add mcp_inspect tool and bump MCP SDK to v1.7.0
dimetron Sep 18, 2026
55ec83c
chore(lint): bump golangci-lint to v2.13.2 and add config for go 1.27
dimetron Sep 20, 2026
fd86ae0
refactor(errors): type ToolError.Context as map[string]string
dimetron Sep 20, 2026
0fb37de
refactor(kubescape): replace untyped response maps with structs
dimetron Sep 20, 2026
7fa76df
style: gofmt errors struct and type the last test-only maps
dimetron Sep 20, 2026
a7c6740
refactor: return typed outputs from all MCP handlers
dimetron Sep 21, 2026
3797289
ci: pin Go toolchain from go.mod instead of a stale version spec
dimetron Sep 21, 2026
da4f1b8
test(e2e): type e2e assertions and fix setup races
dimetron Sep 21, 2026
5ae1ea4
test(e2e): sweep every read-only tool for typed-output conformance
dimetron Sep 21, 2026
cd0258b
chore(deps): bump Go dependencies and bundled CLI versions
dimetron Sep 21, 2026
31a0e0a
docs: add migration spec set and CLAUDE.md agent guide
dimetron Sep 21, 2026
a6ecb79
docs: correct CLAUDE.md to point at AGENTS.md and drop stale API
dimetron Sep 21, 2026
a1cb019
fix(security): redact credentials in mcp_inspect and close two review…
dimetron Sep 22, 2026
3add4ac
Merge origin/main: resolve dependency, Makefile and e2e conflicts
dimetron Sep 22, 2026
6dc138b
chore: drop migration spec set and the tool-name golden test
dimetron Sep 22, 2026
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: '^1.26.1'
go-version-file: 'go.mod'
cache: false

- name: Run cmd/main.go tests
Expand All @@ -64,7 +64,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v6
with:
go-version: '^1.26.1'
go-version-file: 'go.mod'
cache: false

- name: Create k8s Kind Cluster
Expand Down
11 changes: 11 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
version: "2"

linters:
default: standard
exclusions:
rules:
# Pre-existing findings, identical on `main` and after the SDK migration
# (verified: 20 issues on both trees). Deferred — paying down 14
# unchecked-error sites is out of scope for the SDK migration.
- linters: [errcheck, staticcheck]
path: (pkg|internal|cmd|test)/
68 changes: 57 additions & 11 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ tools/
│ └── tag.yaml # Release tagging
├── Makefile # Build orchestration
├── Dockerfile # Multi-stage build (multi-arch)
├── go.mod # Go 1.25.6
├── go.mod # Go 1.27.0
├── DEVELOPMENT.md # Development setup and standards
└── CONTRIBUTION.md # Contribution process
```
Expand Down Expand Up @@ -125,18 +125,55 @@ Before submitting changes, run `make fmt && make lint && make test`.

### Tool Registration Pattern

Each provider implements a `RegisterTools` function that adds MCP tool handlers to the server:
Each provider implements a `RegisterTools` function that adds MCP tool handlers to the server. Registration goes through the wrapper in `internal/mcp`, which records the tool's provider for metrics and relaxes the inferred input schema so optional fields stay optional:

```go
func RegisterTools(server *server.MCPServer, readOnly bool) {
server.AddTool(mcp.NewTool("tool_name", ...), handleToolName)
func RegisterTools(s *mcp.Server, readOnly bool) {
mcp.AddTool(s, "k8s", &mcp.Tool{
Name: "k8s_get_resources",
Description: "Get Kubernetes resources",
}, handleGetResources)

if !readOnly {
server.AddTool(mcp.NewTool("write_tool", ...), handleWriteTool)
mcp.AddTool(s, "k8s", &mcp.Tool{
Name: "k8s_delete_resource",
Description: "Delete a Kubernetes resource",
}, handleDeleteResource)
}
}
```

Handler functions are prefixed with `handle` (e.g., `handleKubectlGetEnhanced`, `handleHelmList`).
Handlers are registered with a typed input and a typed output: `func handleX(ctx context.Context, req *mcp.CallToolRequest, in xInput) (*mcp.CallToolResult, xOutput, error)`. Handler functions are prefixed with `handle` (e.g., `handleKubectlGetEnhanced`, `handleHelmList`).

### Typed MCP Inputs and Outputs

All MCP tool inputs and outputs must be strongly typed. The Go MCP SDK derives an input and output JSON schema from the handler's `In` and `Out` type parameters, populates `CallToolResult.StructuredContent` from the typed `Out` value, and validates that value against the inferred output schema on every call — so an untyped or wrongly-shaped `Out` is not merely untidy, it breaks the tool.

- Define a concrete input struct for every tool with `json` and `jsonschema` tags.
- Define a concrete output DTO for every structured response.
- Never register handlers with `Out=any`; typed outputs enable output schema inference and validation.
- Do not use `any`, `interface{}`, `map[string]any`, `map[string]interface{}`, `[]any`, or `[]interface{}` for handler inputs, handler outputs, public response DTOs, or tests.
- Handler signature: `func handleX(ctx, req, in xInput) (*mcp.CallToolResult, xOutput, error)`.

**Raw CLI text.** Most providers wrap CLI output in text. Use the shared `mcp.TextOutput` wrapper (`{"output": "..."}`) instead of inventing a per-tool shape, and return it through the helpers so the zero value on an error path still validates:

```go
// success — text is preserved in Content and mirrored in StructuredContent
return mcp.TextResult(output)

// tool-level failure — IsError=true, and the empty TextOutput keeps the
// inferred output schema satisfied
return mcp.TextError("resource_name is required")
```

When a helper builds the `*mcp.CallToolResult` itself (e.g. `runKubectlCommand` returning `(*mcp.CallToolResult, error)`), convert it with `mcp.TextOf(res)` and return `res, mcp.TextOf(res), err` so the typed value matches the returned result.

**Output-schema pitfalls.** These are enforced by the SDK at call time and are easy to trip:

- **Zero values are validated on every path, including errors.** A field whose zero value marshals to `null` but whose schema type is non-nullable (notably `map[K]V`) makes *all* error returns fail with `validating tool output`. Give such fields `omitempty`. Slices and pointers infer as nullable (`["null", ...]`) and are safe.
- **`json.RawMessage` does not mean "arbitrary JSON".** The schema inference treats it as a byte slice and validation then rejects real objects and arrays. For genuinely dynamic JSON, return the raw text through `mcp.TextOutput` rather than a `json.RawMessage` field, or re-indent it in place with `json.Indent` without decoding into `interface{}` (see `prettyJSONBody` in `pkg/prometheus`).
- **Not every type can be an `Out`.** Third-party structs with custom JSON marshallers can fail schema inference, which makes `mcp.AddTool` *panic* at registration (the server will not start). `v1beta1.WorkloadConfigurationScan` is one such type; such handlers return `mcp.TextOutput`. `cmd/tools_output_schema_test.go` registers every provider and fails if any `Out` type cannot produce a valid schema.
- **A third-party type may be used** as an `Out` field where inference succeeds (`[]v1beta1.Match`, `v1beta1.ExecCalls`, `metav1.LabelSelector` all work today); prefer a local summary DTO where it does not.

### CommandBuilder Pattern

Expand Down Expand Up @@ -211,7 +248,11 @@ The `internal/cache` package provides a thread-safe generic `Cache[T]` with TTL:
- **Ginkgo v2 + Gomega** for behavioral tests
- **testify** for assertions and mocking
- Table-driven tests for comprehensive coverage
- **Minimum 80% test coverage** enforced by CI
- **Minimum 80% test coverage** is the repository standard. CI runs
`go test -v -cover` and reports coverage but has no threshold gate, so the
standard is on you to check (`go test -cover ./pkg/...`). Every `pkg/` package
currently exceeds it; `internal/commands` and `internal/cmd` are below it and
predate the standard.

### Mock Infrastructure

Expand All @@ -228,6 +269,7 @@ ctx := cmd.WithShellExecutor(context.Background(), mockExecutor)
- Unit tests: co-located `*_test.go` files in each package
- E2E tests: `test/e2e/` (requires Kind cluster)
- All public functions require unit tests
- Decode structured tool results into the same output DTOs used by production code. Avoid `map[string]interface{}` / `[]interface{}` assertions in tests.

---

Expand Down Expand Up @@ -281,6 +323,9 @@ Types: `feat`, `fix`, `docs`, `refactor`, `test`, `chore`, `perf`, `ci`
- Do not return Go errors from MCP handlers — use `ToolError.ToMCPResult()` instead.
- Do not duplicate logic across providers — extract to `internal/` packages.
- Do not bypass the cache for read operations.
- Do not use untyped maps or `any` for MCP tool input/output schemas or public response bodies.
- Do not register a handler with `Out=any` — the SDK cannot infer or validate an output schema, and the typed-output contract is what keeps the tool callable.
- Do not add a map-typed field to an output DTO without `omitempty`, and do not use `json.RawMessage` as a dynamic-JSON output field — both make the SDK reject valid results at call time (see the output-schema pitfalls above).
- Do not add new tool providers without a corresponding `RegisterTools` function.
- Do not commit without running `make fmt && make lint && make test`.

Expand All @@ -294,10 +339,11 @@ Types: `feat`, `fix`, `docs`, `refactor`, `test`, `chore`, `perf`, `ci`
4. Register the provider in `cmd/main.go` inside `registerMCP()`.
5. Add input validation using `internal/security/`.
6. Use `CommandBuilder` for CLI execution.
7. Return errors via `ToolError.ToMCPResult()`.
8. Write unit tests with mock shell executor (80% coverage minimum).
9. Add E2E tests if the tool interacts with a cluster.
10. Run `make fmt && make lint && make test` before submitting.
7. Define concrete typed input and output DTOs; avoid `any`, `interface{}`, and untyped maps. Return `mcp.TextResult(...)` / `mcp.TextError(...)` for raw CLI text, and a concrete DTO for a structured response. Watch the output-schema pitfalls above (`omitempty` on map fields, no `json.RawMessage` fields, no `Out` types that fail schema inference).
8. Return errors via `ToolError.ToMCPResult()`; remember the `Out` value must still validate on the error path, so return the zero value of the DTO (or `mcp.TextOutput{}`).
9. Write unit tests with mock shell executor (80% coverage minimum).
10. Add E2E tests if the tool interacts with a cluster.
11. Run `make fmt && make lint && make test` before submitting.

---

Expand Down
72 changes: 72 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working in this repository.

**The repository guide lives in [`AGENTS.md`](./AGENTS.md).** It covers the architecture,
tool-provider layout, the typed MCP input/output contract, error handling, caching, testing,
CI/CD, commit conventions and the "what not to do" list. Read it before making changes; it is
the single source of truth and is kept current.

This file only adds the few things AGENTS.md does not spell out.

## Architecture Overview

A Go MCP (Model Context Protocol) server that wraps Kubernetes and cloud-native CLIs
(`kubectl`, `helm`, `istioctl`, `cilium`, `kubectl-argo-rollouts`, `kubescape`,
the Prometheus HTTP API) behind a single typed MCP interface. It does not reimplement the
tools' behaviour; it validates input, invokes the CLI, and returns a typed result.

Two design points that are easy to get wrong:

- **Registration goes through `internal/mcp`, not the SDK directly.** `mcp.AddTool` records the
provider for metrics and relaxes the inferred input schema so optional fields stay optional.
Never call `sdk.AddTool` from a provider.
- **Every handler returns a concrete `Out` type.** The SDK infers an output schema from it,
populates `structuredContent`, and validates the value on every call — including error paths.
See "Typed MCP Inputs and Outputs" in AGENTS.md for the three pitfalls that break tools.

## Run Locally

```bash
go run ./cmd # defaults to stdio
./bin/kagent-tools --stdio # stdio transport
./bin/kagent-tools --http --port 8084 # HTTP transport
```

Useful flags: `--tools k8s,helm` (limit providers), `--kubeconfig <path>`,
`--read-only` (do not register write tools), `--metrics-port`.

## Development Practices

- Run the narrowest useful test first, then broaden: `go test -tags=test -v -cover ./pkg/<provider>`
before `make test`.
- `make test` = build + lint + all tests. `make test-only` skips build/lint.
- Use the mock shell executor for unit tests; never shell out to real CLIs in unit tests.
- Keep functions focused and testable, and use `context` for cancellation in long-running work.

### Test Coverage

- The project targets 80% coverage; every `pkg/` package currently exceeds it (lowest is
`pkg/kubescape` at ~85%, highest ~99%).
- **CI does not enforce a coverage gate.** The `go-unit-tests` job runs `go test -v -cover`,
which reports coverage but does not fail the build on a threshold. Treat 80% as the
repository standard to maintain, not as an automated gate — check it yourself with
`go test -cover ./pkg/...`.
- `internal/commands` and `internal/cmd` are below 80% and predate that standard.

## Logging

Structured logging lives in `internal/logger` (not `pkg/logger`). Prefer the package-level
logger used by the surrounding code.

## Commit Messages

Conventional Commits, with a `Signed-off-by` trailer (DCO is enforced on pull requests):
`feat`, `fix`, `docs`, `refactor`, `test`, `chore`, `perf`, `ci`.

## Additional Resources

- [AGENTS.md](AGENTS.md) — the repository guide (authoritative)
- [DEVELOPMENT.md](DEVELOPMENT.md) — setup and code standards
- [CONTRIBUTION.md](CONTRIBUTION.md) — contribution process and PR guidelines
- [docs/quickstart.md](docs/quickstart.md) — quick start guide
16 changes: 8 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,11 @@ tidy: ## Run go mod tidy to ensure dependencies are up to date.

.PHONY: test
test: build lint ## Run all tests with build, lint, and coverage
go test -tags=test -v -cover ./pkg/... ./internal/...
go test -tags=test -v -cover ./pkg/... ./internal/... ./cmd/...

.PHONY: test-only
test-only: ## Run tests only (without build/lint for faster iteration)
go test -tags=test -v -cover ./pkg/... ./internal/...
go test -tags=test -v -cover ./pkg/... ./internal/... ./cmd/...

.PHONY: e2e
e2e: test retag
Expand Down Expand Up @@ -136,11 +136,11 @@ DOCKER_BUILDER ?= docker buildx
DOCKER_BUILD_ARGS ?= --pull --load --platform linux/$(LOCALARCH) --builder $(BUILDX_BUILDER_NAME)

# tools image build args
TOOLS_ISTIO_VERSION ?= 1.30.1
TOOLS_ISTIO_VERSION ?= 1.31.0
TOOLS_ARGO_ROLLOUTS_VERSION ?= 1.10.0
TOOLS_KUBECTL_VERSION ?= 1.36.2
TOOLS_HELM_VERSION ?= 4.2.2
TOOLS_CILIUM_VERSION ?= 0.19.4
TOOLS_KUBECTL_VERSION ?= 1.37.0
TOOLS_HELM_VERSION ?= 4.3.0
TOOLS_CILIUM_VERSION ?= 0.20.0

# build args
TOOLS_IMAGE_BUILD_ARGS = --build-arg VERSION=$(VERSION)
Expand Down Expand Up @@ -278,12 +278,12 @@ $(LOCALBIN):
mkdir -p $(LOCALBIN)

GOLANGCI_LINT = $(LOCALBIN)/golangci-lint
GOLANGCI_LINT_VERSION ?= v1.63.4
GOLANGCI_LINT_VERSION ?= v2.13.2

.PHONY: golangci-lint
golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary.
$(GOLANGCI_LINT): $(LOCALBIN)
$(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION))
$(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION))

# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist
# $1 - target path with name of binary
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ Provides documentation query functionality:
Provides general utility functions:

- **shell**: Execute shell commands
- **mcp_inspect**: Echo input and return request headers for MCP client debugging

## Building and Running

Expand Down
Loading
Loading