Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 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
761a438
fix(mcp): reap abandoned streamable HTTP sessions after the SDK migra…
dimetron Sep 22, 2026
ea3a317
fix(k8s,kubescape): exec argv, log --previous, log scope docs, sessio…
dimetron Sep 23, 2026
483ce29
refactor(mcp): drop the SDK type aliases, use go-sdk types directly
dimetron Sep 23, 2026
35f0af6
docs: make AGENTS.md the single guide, drop CLAUDE.md
dimetron Sep 23, 2026
e2ab3a0
chore(deps): bump istioctl to 1.31.1
dimetron Sep 23, 2026
67442da
Merge origin/main into feature/mcp-sdk-migration
dimetron Sep 23, 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)/
82 changes: 71 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 @@ -119,24 +119,75 @@ Each provider lives in `pkg/` and registers MCP tools via a `RegisterTools(serve

Before submitting changes, run `make fmt && make lint && make test`.

### 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`.

`make run` builds the image and runs the server in Docker on
`http://localhost:8084/mcp`.

---

## Code Conventions

### 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 +262,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 +283,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 +337,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 +353,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
2 changes: 1 addition & 1 deletion DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,6 @@ git commit -m "docs(readme): update installation instructions"
### Getting Help

- Check existing issues in the repository
- Review the CLAUDE.md file for project-specific guidance
- Review the AGENTS.md file for project-specific guidance
- Consult Go documentation and best practices
- Ask questions in code reviews or team discussions
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 ./cmd/... ./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 ./cmd/... ./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.1
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
3 changes: 2 additions & 1 deletion 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 Expand Up @@ -197,7 +198,7 @@ The server runs using sse transport for MCP communication.
| `--stdio` | `false` | Use stdio for communication instead of HTTP |
| `--tools` | `[]` (all) | Comma-separated list of tool providers to register |
| `--read-only` | `false` | Disable tools that perform write operations |
| `--session-idle-ttl` | `10m` | Reclaim streamable HTTP session state after this idle duration (`0` disables the sweeper) |
| `--session-idle-ttl` | `30m` | Close streamable HTTP sessions idle for this long (`0` disables the reaper) |
| `--kubeconfig` | `""` | Path to kubeconfig file (defaults to in-cluster config) |
| `--version`, `-v` | `false` | Show version information and exit |

Expand Down
Loading
Loading