climcp

package module
v0.0.0-...-71a4713 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 13, 2026 License: MIT Imports: 14 Imported by: 0

README

cli-mcp

Go Reference Go Report Card Tests status

cli-mcp turns a urfave/cli v3 command tree into a CLI by default, with a batteries-included Model Context Protocol server attached. One binary, three usage forms, same code path.

app := &cli.Command{Name: "demo", Commands: []*cli.Command{ /* your tree */ }}
climcp.AttachMCP(app, climcp.Options{Name: "demo", Version: "v0.0.0"})
app.Run(ctx, os.Args)

That's it. app is now a regular CLI you can run directly, AND an MCP server when invoked as app mcp serve (stdio) or app mcp serve-http (Streamable HTTP).

The three forms

Given a hello subcommand, the same code is reachable as:

1. Plain CLI — humans run subcommands directly:

./demo hello kai --loud
# HELLO, KAI!

2. MCP server, called via raw JSON-RPC — agents call tools over the Streamable HTTP transport:

./demo mcp serve-http --addr 127.0.0.1:8080 &
curl -sS -X POST http://127.0.0.1:8080/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"hello","arguments":{"args":["kai"],"loud":true}}}'
# ... "text":"HELLO, KAI!\n"

3. MCP server, called via mcporter — the client wraps MCP tools as subcommands, so the caller sees a CLI shape again:

# mcporter.json
# {"mcpServers":{"demo":{"command":"./demo","args":["mcp","serve"]}}}
mcporter call demo hello kai --loud
# HELLO, KAI!

Form 1 runs the Action directly. Forms 2 and 3 round-trip through MCP and end up running the same Action with the same flags and args. The mcp subcommand group is auto-excluded from the MCP tool surface so calling MCP doesn't expose "how to start MCP".

See examples/dual-mode for the runnable version.

Features

  • Every leaf command becomes an MCP tool; tool name is the dot-joined path from the root's children.
  • Input schema derived from urfave flags (typed) and positional args.
  • stdio transport for editor/agent integrations, Streamable HTTP (current spec) for remote.
  • Built on the official modelcontextprotocol/go-sdk.
  • Relays webops.* metadata onto MCP tool.Meta for cli-web-ops consumption.
  • Composes with cli-guard: Guard-wrapped actions audit and validate every tool call.

Documentation

See docs/FEATURES.md for a feature inventory, examples/ for runnable demos, and deploy/Caddyfile.example for the production-shaped Caddy-in-front posture. Local dev verbs live in .coily/coily.yaml; coily lint validates that against the Makefile.

Support

If you found a bug or have a feature request, create a new issue. Participation in this community is governed by the Code of Conduct. Security disclosures go through SECURITY.md.

Sibling repos in the cli-* family: cli-guard, cli-web-docs, cli-web-ops.

See also

Prior art in the CLI-as-MCP-server space:

  • njayp/ophis - Closest analog. Bridges Cobra command trees to MCP. Executes tools by spawning the CLI as a subprocess; cli-mcp runs them in-process, which is what lets it compose cleanly with cli-guard and avoids PATH-capture workarounds.
  • f/mcptools - Generalized CLI for interacting with MCP servers, with a proxy mode that exposes shell scripts and inline commands as MCP tools. Lower ceiling on schema (no derivation from a real command tree), but useful when you don't have a structured CLI to project.
  • FastMCP - Python-side equivalent of the auto-derivation instinct: generates tool definitions from type hints and docstrings instead of from a CLI tree. Different source artifact, same goal of "write the code once, get an MCP server."
License

See LICENSE.

Documentation

Overview

Package climcp projects a urfave/cli v3 command tree as a Model Context Protocol (MCP) server. Every leaf command becomes an MCP tool; the tool name is the underscore-joined command path, the input schema is derived from the command's flags and positional arguments. Set Options.NameJoiner to "." for dot-joined output.

Three entry points:

  • ServeStdio runs an MCP server over stdio (the default transport for editor/agent integrations). One goroutine per tool call.
  • StreamableHTTPHandler returns an http.Handler that speaks the current MCP remote transport (Streamable HTTP, 2025-03-26 spec). Mount on any net/http server; put Caddy or another reverse proxy in front for TLS, auth, and rate limiting.
  • NewServer returns the underlying *mcp.Server for callers who want to wire their own transport.

The deprecated HTTP+SSE transport is available via the SDK but not surfaced here; new deployments should use Streamable HTTP.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func AddrFromEnv

func AddrFromEnv(fallback string) string

AddrFromEnv returns os.Getenv("ADDR") if non-empty, otherwise fallback. The convention "container reads $ADDR, local default is safe-by-default" recurs across cli-mcp consumers; this avoids re-implementing it in every main.go. Callers that want a different env var can just read it themselves.

func AttachMCP

func AttachMCP(root *cli.Command, opts Options)

AttachMCP appends an "mcp" subcommand group to root with two leaves:

  • "mcp serve" - run as MCP server over stdio.
  • "mcp serve-http" - run as MCP server over Streamable HTTP.

The added subtree is automatically added to opts.SkipPaths so it is not exposed as MCP tools. The pitch: same binary is a normal CLI by default and an MCP server when invoked as `<root> mcp serve` or `<root> mcp serve-http`. Three usage shapes fall out:

./demo hello world                          # plain CLI
./demo mcp serve-http --addr :8080          # serve MCP over HTTP
mcporter call demo hello world              # MCP client, back to CLI shape

Flags on `mcp serve-http`:

  • --addr listen address. Default 127.0.0.1:8080. $ADDR env overrides.
  • --landing text body for GET /. Empty disables the route.
  • --no-health disable the /healthz endpoint.

Callers wanting custom flags or a different subcommand name should skip this and wire ServeStdio / RunStreamableHTTP themselves; AttachMCP is the batteries-included default, not a constraint.

func NewServer

func NewServer(root *cli.Command, opts Options) (*mcp.Server, error)

NewServer wires up an *mcp.Server with one tool per leaf command in root. Caller is responsible for running the server on a transport.

Example

Project a urfave/cli command tree as an MCP server. Every leaf command becomes an MCP tool; the tool name is the underscore-joined path. Caller is responsible for running the returned server on a transport.

app := &cli.Command{
	Name: "demo",
	Commands: []*cli.Command{
		{Name: "hello", Usage: "greet someone"},
	},
}

srv, err := climcp.NewServer(app, climcp.Options{Name: "demo", Version: "v0.0.0"})
fmt.Println("ok:", srv != nil && err == nil)
Output:
ok: true

func RunStreamableHTTP

func RunStreamableHTTP(ctx context.Context, root *cli.Command, opts Options, srv HTTPServerOptions) error

RunStreamableHTTP builds the projected MCP server from root and serves it over Streamable HTTP. Mounts the MCP handler at srv.MCPPath, a healthcheck at srv.HealthPath, and (if srv.Landing is non-empty) a text landing page at "/". Blocks until ctx is cancelled or the listener errors. On ctx cancellation the server is shutdown with a 5-second grace period.

Typical container shape:

ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer cancel()
err := climcp.RunStreamableHTTP(ctx, root, climcp.Options{Name: "myapp", Version: "v1.0.0"},
    climcp.HTTPServerOptions{Addr: climcp.AddrFromEnv("0.0.0.0:8080"), Landing: "myapp\n"})

func ServeStdio

func ServeStdio(ctx context.Context, root *cli.Command, opts Options) error

ServeStdio is the convenience entry point: build a server from root and run it over stdio until the context is cancelled or the peer disconnects.

func StreamableHTTPHandler

func StreamableHTTPHandler(root *cli.Command, opts Options) (http.Handler, error)

StreamableHTTPHandler returns an http.Handler that exposes the projected command tree over MCP's Streamable HTTP transport (the current remote transport per the 2025-03-26 spec).

Mount on a net/http server and front with a reverse proxy that handles TLS, auth, and rate limiting:

handler, _ := climcp.StreamableHTTPHandler(root, climcp.Options{Name: "myapp", Version: "v1.0.0"})
http.ListenAndServe("127.0.0.1:9090", handler)

See examples/serve-http for a runnable example and deploy/Caddyfile.example for the recommended Caddy-in-front posture.

Example

StreamableHTTPHandler returns an http.Handler for the current MCP remote transport. Mount on any net/http server; front with a reverse proxy in production.

app := &cli.Command{
	Name:     "demo",
	Commands: []*cli.Command{{Name: "hello"}},
}

handler, err := climcp.StreamableHTTPHandler(app, climcp.Options{Name: "demo", Version: "v0.0.0"})
fmt.Println("ok:", handler != nil && err == nil)
Output:
ok: true

func WriteJSON

func WriteJSON(w io.Writer, v any) error

WriteJSON encodes v as indented JSON onto w. Convenience for tool Action functions that return structured data: MCP renders the text content verbatim, so indented JSON is what an agent sees in its tool result.

Errors from json.Encoder.Encode (e.g. unencodable types) propagate unchanged.

Types

type HTTPServerOptions

type HTTPServerOptions struct {
	// Addr is the listen address. Default "127.0.0.1:8080".
	Addr string

	// MCPPath is the route the Streamable HTTP handler is mounted on.
	// The handler is mounted at both "<path>" and "<path>/" so clients
	// can append a trailing slash either way. Default "/mcp".
	MCPPath string

	// HealthPath is a GET endpoint that returns 200 + "ok\n". Default
	// "/healthz". The zero value gets the default; to opt out, set
	// DisableHealth.
	HealthPath string

	// DisableHealth, if true, suppresses the healthcheck endpoint.
	// Otherwise HealthPath (or its default) is mounted.
	DisableHealth bool

	// Landing, if non-empty, is served as text/plain on "GET /". If
	// empty, "/" falls through to net/http's default (404). Useful for
	// telling a curious operator what the service is and where the MCP
	// endpoint lives.
	Landing string

	// ReadHeaderTimeout is forwarded to http.Server.ReadHeaderTimeout.
	// Default 10s. Important for Slowloris hardening and required by
	// the gosec linter in this repo's CI.
	ReadHeaderTimeout time.Duration
}

HTTPServerOptions configure RunStreamableHTTP. The zero value is a usable local-dev shape: 127.0.0.1:8080, /mcp, /healthz, no landing page. Production deployments typically override Addr to "0.0.0.0:PORT" (or use AddrFromEnv) and set Landing to a human-readable string so `GET /` returns something other than 404.

type Options

type Options struct {
	// Name is the MCP server name (advertised to the client). Required.
	Name string

	// Version is the MCP server version. Required.
	Version string

	// SkipPaths, if non-nil, lists command paths that should NOT be
	// exposed as MCP tools. Each entry is the dot-joined path from the
	// root (excluding the root's own name). Example: []string{"internal.debug"}
	// hides the `internal debug` subcommand.
	SkipPaths []string

	// IncludeGroups, if true, also exposes non-leaf command groups as
	// MCP tools that print --help. Default false (leaves only).
	IncludeGroups bool

	// NameJoiner is the separator used to join command-path segments
	// into the wire-visible MCP tool name. Default "_" (e.g. "module_latest").
	// Underscore is the safe default for MCP clients whose selector
	// grammar parses `server.tool` and would otherwise split inside the
	// tool name (mcporter). Set to "." for dot-joined output if you know
	// every consumer handles dots correctly.
	// Does not affect SkipPaths, which always use dots internally.
	NameJoiner string
}

Options configure the MCP server projection.

type ToolInput

type ToolInput map[string]any

ToolInput is the dynamic shape passed to AddTool's handler. Keys are flag names; "args" is the positional array.

Directories

Path Synopsis
examples
annotated-favorites command
Command annotated-favorites projects a CLI whose commands carry the webops.* metadata namespace, so a cli-web-ops client can render them as labeled, grouped, optionally-confirmed mobile buttons on the home screen.
Command annotated-favorites projects a CLI whose commands carry the webops.* metadata namespace, so a cli-web-ops client can render them as labeled, grouped, optionally-confirmed mobile buttons on the home screen.
composition-with-guard command
Command composition-with-guard shows how cli-mcp composes with cli-guard.
Command composition-with-guard shows how cli-mcp composes with cli-guard.
dual-mode command
Command dual-mode shows cli-mcp's headline shape: a single binary that is a normal CLI by default and an MCP server when invoked as `dual-mode mcp serve` or `dual-mode mcp serve-http`.
Command dual-mode shows cli-mcp's headline shape: a single binary that is a normal CLI by default and an MCP server when invoked as `dual-mode mcp serve` or `dual-mode mcp serve-http`.
large-tree command
Command large-tree projects a realistic multi-level CLI as an MCP server over stdio.
Command large-tree projects a realistic multi-level CLI as an MCP server over stdio.
serve command
Command serve runs a tiny CLI as an MCP server over stdio.
Command serve runs a tiny CLI as an MCP server over stdio.
serve-http command
Command serve-http runs a tiny CLI as an MCP server over the Streamable HTTP transport (2025-03-26 spec) using climcp's batteries-included runner.
Command serve-http runs a tiny CLI as an MCP server over the Streamable HTTP transport (2025-03-26 spec) using climcp's batteries-included runner.
skip-paths command
Command skip-paths demonstrates Options.SkipPaths (suppress specific commands from MCP projection) and Options.IncludeGroups (also expose non-leaf groups as MCP tools that print --help).
Command skip-paths demonstrates Options.SkipPaths (suppress specific commands from MCP projection) and Options.IncludeGroups (also expose non-leaf groups as MCP tools that print --help).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL