.cursor/skills/mcp-tool-development/SKILL.md
Guide for adding or modifying MCP tools and understanding command discovery in kubectl-mtv. Use when working on the MCP server, adding new tools, or changing how commands are exposed to AI assistants.
npx skillsauth add yaacov/kubectl-mtv mcp-tool-developmentInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
The MCP server exposes kubectl-mtv commands as structured tools for AI assistants. CLI commands are auto-discovered; MCP tools rarely need changes.
cmd/mcpserver/mcpserver.go -> Server startup, tool registration
pkg/mcp/discovery/registry.go -> Command discovery from help --machine
pkg/mcp/tools/mtv_read.go -> Read-only tool (get, describe, health)
pkg/mcp/tools/mtv_write.go -> Write tool (create, delete, patch, start)
pkg/mcp/tools/mtv_help.go -> Help/documentation tool
pkg/mcp/util/util.go -> Command execution, response parsing
New CLI commands are automatically exposed via MCP. No MCP code changes needed if:
get, describe, create, delete, patch, start, cancel, archive, unarchive, cutover, health).pkg/cmd/help/generator.go getCategory().RunE).The discovery flow: help --machine -> JSON schema -> Registry -> tool descriptions.
Each tool has an input struct with jsonschema tags:
type MTVReadInput struct {
Command string `json:"command" jsonschema:"Command path (e.g. get plan, get inventory vm)"`
Flags map[string]any `json:"flags,omitempty" jsonschema:"All parameters as key-value pairs"`
DryRun bool `json:"dry_run,omitempty" jsonschema:"If true, returns CLI command instead of executing"`
Fields []string `json:"fields,omitempty" jsonschema:"Limit JSON to these top-level keys only"`
}
func GetMTVReadTool(registry *discovery.Registry) *mcp.Tool {
return &mcp.Tool{
Name: "mtv_read",
Description: registry.GenerateReadOnlyDescription(),
OutputSchema: mtvOutputSchema,
}
}
func HandleMTVRead(registry *discovery.Registry) func(context.Context, *mcp.CallToolRequest, MTVReadInput) (*mcp.CallToolResult, any, error) {
return func(ctx context.Context, req *mcp.CallToolRequest, input MTVReadInput) (*mcp.CallToolResult, any, error) {
ctx = extractKubeCredsFromRequest(ctx, req)
if err := validateCommandInput(input.Command); err != nil {
return nil, nil, err
}
cmdPath := normalizeCommandPath(input.Command)
if !registry.IsReadOnly(cmdPath) {
return nil, nil, fmt.Errorf("unknown or write command '%s'", input.Command)
}
if input.DryRun {
ctx = util.WithDryRun(ctx, true)
}
args := buildArgs(cmdPath, input.Flags)
result, err := util.RunKubectlMTVCommand(ctx, args)
if err != nil {
return nil, nil, fmt.Errorf("command failed: %w", err)
}
data, err := util.UnmarshalJSONResponse(result)
if err != nil {
return nil, nil, err
}
if errResult := buildCLIErrorResult(data); errResult != nil {
return errResult, nil, nil
}
return nil, data, nil
}
}
In cmd/mcpserver/mcpserver.go createMCPServerWithHeaderCapture():
// Read tools (always registered)
tools.AddToolWithCoercion(server, tools.GetMTVReadTool(registry),
wrapWithHeaders(tools.HandleMTVRead(registry), capturedHeaders))
mcp.AddTool(server, tools.GetMTVHelpTool(),
wrapWithHeaders(tools.HandleMTVHelp, capturedHeaders))
// Write tool (skipped in read-only mode)
if !readOnlyMode {
tools.AddToolWithCoercion(server, tools.GetMTVWriteTool(registry),
wrapWithHeaders(tools.HandleMTVWrite(registry), capturedHeaders))
}
AddToolWithCoercion handles boolean coercion (string "true"/"false" -> bool). Use for tools with boolean flags.mcp.AddTool is standard registration. Use when no coercion needed.wrapWithHeaders injects HTTP auth headers for SSE mode.pkg/mcp/tools/mtv_newtool.go with input struct, GetTool(), and HandleTool().cmd/mcpserver/mcpserver.go createMCPServerWithHeaderCapture().pkg/mcp/tools/mtv_newtool_test.go.| Function | Location | Purpose |
|----------|----------|---------|
| util.RunKubectlMTVCommand() | pkg/mcp/util/util.go | Execute kubectl-mtv subprocess |
| util.UnmarshalJSONResponse() | pkg/mcp/util/util.go | Parse JSON, strip command field, truncate |
| buildCLIErrorResult() | pkg/mcp/tools/mtv_read.go | Convert non-zero exit to IsError response |
| validateCommandInput() | pkg/mcp/tools/mtv_read.go | Reject garbled LLM input early |
| normalizeCommandPath() | pkg/mcp/tools/mtv_read.go | "get plan" -> "get/plan" |
| buildArgs() | pkg/mcp/tools/mtv_read.go | Flags map -> CLI args |
| help.MarkMCPHidden() | pkg/cmd/help/generator.go | Hide flags from MCP schema |
All tools return the same envelope:
{
"return_value": 0,
"data": {},
"output": "text output",
"stderr": "error output"
}
The Registry (pkg/mcp/discovery/) splits commands into ReadOnly and ReadWrite maps keyed by path ("get/plan", "create/provider"). It also holds GlobalFlags, Parents (non-runnable nodes like get/inventory), and generates tool descriptions.
development
Step-by-step guide for updating the kubev2v/forklift Go dependency in kubectl-mtv. Use when bumping the forklift version, syncing settings or CRD types, or checking for upstream changes.
tools
Reference for building, releasing, publishing images, and deploying kubectl-mtv. Use when creating releases, building binaries or container images, deploying to OpenShift, or updating the Krew plugin index.
tools
Guide for writing and running tests in kubectl-mtv, including Go unit tests, MCP e2e tests (Python/pytest), and linting. Use when adding tests, running the test suite, or debugging test failures.
documentation
Guide for adding and editing chapters in the kubectl-mtv technical guide (Jekyll + GitHub Pages). Use when writing documentation, adding chapters, or updating the guide table of contents.