Skip to content

Bind a Cobra CLI

A tool built on Cobra becomes an MCP server by mounting one command:

import (
    mcpcli "gitlab.com/phpboyscout/go/mcp/cli"
    mcpcobra "gitlab.com/phpboyscout/go/mcp/cobra"
)

root.AddCommand(mcpcli.Command(mcpcli.WithBinding(mcpcobra.WithExposure(isExposed))))

my-tool mcp start now serves every exposed command over stdio, my-tool mcp tools exports the catalogue, and my-tool mcp vscode enable registers the server with the editor. The command tree reference lists the rest.

The remainder of this page is the long way, for a host that wants to own the binding and the server itself. It is what cli.Command does inside.

Import the optional binding:

import mcpcobra "gitlab.com/phpboyscout/go/mcp/cobra"

Bind once the whole tree is registered. Pass the exposure policy the application already has, and the logger:

binding, err := mcpcobra.Bind(root,
    mcpcobra.WithExposure(isExposed),
    mcpcobra.WithLogger(logger),
)
if err != nil {
    return err // an unsupported flag type or a name collision, named precisely
}

Every runnable, unhidden command the policy accepts is now an operation named <root>_<path>, with the command's flags as a flags object, positional arguments as args, and stdout, stderr and the exit code as the result.

Register and host:

registry, err := mcp.New(binding.Operations(), mcp.WithPolicy(mcp.AllowRegistered()))
if err != nil {
    return err
}

host, err := server.New(registry, server.WithIdentity("my-tool", version))
if err != nil {
    return err
}

defer func() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    _ = host.Shutdown(ctx)
    _ = binding.Shutdown(ctx)
}()

return host.RunStdio(ctx)

Each invocation runs the command as a subprocess of the same binary, with the host's environment and working directory, stdin closed, a five-minute timeout and one MiB of retained output. One command runs at a time; a second call gets a retryable busy failure while search and inspection stay available.

Cancelling a call stops the command and everything it started. If that cannot be confirmed within five seconds, binding.Status() says so until it can.

A command with a custom flag type needs a codec, or an exclusion:

mcpcobra.WithCodec("level", mcpcobra.TextCodec(json.RawMessage(`{"enum":["debug","info","warn"]}`)))

Read the Cobra binding reference for the filters, the codec table and the cleanup boundary.