Skip to content

asynkron/climcp

Repository files navigation

climcp

CI Go Reference Go Report Card License: MIT

A small CLI that bridges your shell to MCP servers (Model Context Protocol). Point it at a config file — the same mcpServers shape agents already use — and you can list the configured servers, describe the operations each exposes, and call those operations directly from the command line.

Both stdio (a spawned child process) and HTTP (Streamable HTTP / SSE) servers are supported.

Why — stop letting MCP servers eat your context

When you wire MCP servers directly into an agent, every server dumps the full definition of every tool it exposes — names, descriptions, and JSON schemas — into the model's context window, on every single turn. Connect a handful of servers and you've burned thousands of tokens before the agent has done anything. That context is gone: it's not available for the actual task, and you pay for it on every request.

climcp moves all of that out of the model and into the shell. The agent doesn't preload anything. It:

  • discovers servers on demand (climcp mcp list),
  • looks up a server's operations only when it needs them (climcp describe X),
  • and calls an operation as a plain shell command (climcp call "X.op(...)").

The only thing that ever enters the context window is the specific call the agent chose to make and the result it got back. No always-on tool schemas, no per-turn overhead — all that context is freed up for the work that matters.

And because it's just a CLI writing to stdout, you can compose calls with ordinary shell tooling — pipe results through jq, feed one call's output into the next, loop over them — which the MCP protocol itself cannot do. See Chaining calls.

$ climcp mcp list
2 MCP servers configured in ./climcp.json

  NAME  TRANSPORT  ENDPOINT
  fs    stdio      npx -y @modelcontextprotocol/server-filesystem /tmp
  docs  http       https://example.com/mcp

$ climcp call "fs.read_file(path: '/etc/hostname')"
my-machine

Install

# from source, into ./bin
make build

# install to /usr/local/bin (override with PREFIX=...)
make install

# or with the Go toolchain
go install github.com/asynkron/climcp@latest

Pre-built binaries for Linux, macOS, and Windows are attached to each release.

Configure

Create a climcp.json. The format is compatible with the usual mcp.json:

{
  "mcpServers": {
    "fs": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],
      "env": { "LOG_LEVEL": "info" },
      "cwd": "/optional/working/dir"
    },
    "docs": {
      "type": "http",
      "url": "https://example.com/mcp",
      "headers": { "Authorization": "Bearer XXX" }
    }
  }
}

The transport is inferred: a url — or an explicit type of http / sse / streamable-http — selects HTTP; otherwise the server is stdio.

Transport Required Optional
stdio command args, env, cwd
http url headers

The HTTP transport uses Streamable HTTP: responses delivered as application/json or text/event-stream are both handled, and a session id issued at initialize is reused on later requests.

When --config is not given, the first existing file is used, in order:

  1. ./climcp.json
  2. ~/.config/climcp/config.json
  3. ~/.climcp.json

This repo's config

This repository ships a climcp.json with a small, safe set of servers, all scoped to the current directory (no hard-coded paths), so it works for anyone who clones it and runs climcp from the repo root:

Server Command What it's for
gopls gopls mcp Go code intelligence — search symbols, package APIs, diagnostics, rename.
fs npx … server-filesystem . Read/write files within the repo.
git uvx mcp-server-git --repository . Status, diffs, log, blame on this repo.
time uvx mcp-server-time Current time and timezone conversions.
fetch uvx mcp-server-fetch Fetch a URL (HTML→markdown, or raw). Reaches the network.

Prerequisites: gopls (go install golang.org/x/tools/gopls@latest), npx (Node), and uvx (uv). A quick taste, powered by gopls:

climcp call "gopls.go_search(query: 'tooLargeError')"
climcp call "gopls.go_package_api(packagePaths: ['github.com/asynkron/climcp/internal/mcp'])"

Reuse or import an existing config

Because the format matches the usual mcpServers shape, you can point climcp straight at an agent's config without changing anything:

climcp --config ~/.cursor/mcp.json mcp list

Or import its servers into your own climcp.json so you don't have to repeat --config:

climcp import ~/.cursor/mcp.json            # merge into ./climcp.json
climcp import ~/.cursor/mcp.json --to ~/.config/climcp/config.json
climcp import ~/.cursor/mcp.json --overwrite --dry-run   # preview replacements

Import accepts both the mcpServers and servers config shapes. Name clashes are skipped by default (reported), unless you pass --overwrite.

Commands

Command Description
climcp mcp list List configured servers (name, transport, endpoint).
climcp describe <server> Connect and list the server's operations and parameters.
climcp call "<server>.<op>(args)" Invoke an operation with arguments.
climcp import <file> Merge servers from an existing config into your climcp.json.
climcp --help Detailed help — also lists your configured servers and the next steps.
climcp --version Print the version.

Flag-style aliases also work: climcp --describe <server> and climcp --call "<expr>".

Global flags

Flag Description
--config <path> Use a specific config file.
--json Emit JSON instead of formatted text (works with all three commands).
--timeout <dur> Abort if the server is unresponsive (default 60s; e.g. 30s, 2m).
--max-bytes <n> Fail a call whose response exceeds n bytes (default 51200 = 50 KB); 0 disables.
--no-color Disable colored output (also honors the NO_COLOR env var).

Colors are used automatically only when writing to a terminal; piped or redirected output is always plain.

Call syntax

A call expression has three parts: <server>.<operation>(<arguments>). The arguments accept two equivalent styles:

# 1) JSON object
climcp call 'fs.read_file({"path": "/tmp/a.txt", "tail": 20})'

# 2) collapsed function-call form — bare keys, single or double quotes
climcp call "fs.read_file(path: '/tmp/a.txt', tail: 20)"

The collapsed form supports nested objects and arrays:

climcp call "search.query(filter: {kind: 'file', tags: ['go', 'cli']}, limit: 5)"

Values may be quoted strings, numbers, true / false / null, objects, or arrays. Bare unquoted strings are rejected on purpose — always quote string values. Use empty parentheses for no arguments: climcp call "time.now()".

Pipe the raw result into jq:

climcp --json call "fs.list_directory(path: '/tmp')" | jq '.content'

Chaining calls

This is the part the MCP protocol can't do for you. Because every call is just a command that writes JSON to stdout, you can fetch → transform → fetch → transform in a single shell pipeline, with the intermediate data living in the shell instead of being shuttled back through the model's context.

A tool's payload is usually JSON encoded inside a text content block, so the recurring move is jq -r '.content[0].text' to unwrap it, then a second jq to reshape it. All three examples below run as-is against this repo's climcp.json.

1. Is our go.mod behind the latest Go release? — joins three servers: fetch (the web) + fs (local file) + time (a stamp).

latest=$(climcp --json call "fetch.fetch(url: 'https://go.dev/VERSION?m=text', raw: true)" \
         | jq -r '.content[0].text' | sed -n 's/^go\([0-9.]*\)$/\1/p' | head -1)
ours=$(climcp --json call "fs.read_text_file(path: 'go.mod')" \
       | jq -r '.content[0].text' | awk '/^go /{print $2}')
now=$(climcp --json call "time.get_current_time(timezone: 'Europe/Stockholm')" \
      | jq -r '.content[0].text' | jq -r '.datetime')

jq -n --arg latest "$latest" --arg ours "$ours" --arg now "$now" \
  '{checked_at:$now, latest_stable_go:$latest, our_go_directive:$ours,
    minor_versions_behind: (($latest|split(".")[1]|tonumber) - ($ours|split(".")[1]|tonumber))}'
# → {"checked_at":"2026-…","latest_stable_go":"1.26.4","our_go_directive":"1.23","minor_versions_behind":3}

2. Is my local commit in sync with GitHub? — cross-references a local MCP (git) against a remote one (fetch). No single MCP can see both sides.

local=$(climcp call "git.git_log(repo_path: '.', max_count: 1)" \
        | sed -n "s/^Commit: '\([0-9a-f]\{40\}\)'.*/\1/p")
remote=$(climcp --json call "fetch.fetch(url: 'https://api.github.com/repos/asynkron/climcp/commits?per_page=1', raw: true, max_length: 9000)" \
         | jq -r '.content[0].text' | sed -n '/^\[/,$p' | jq -r '.[0].sha')

jq -n --arg l "$local" --arg r "$remote" \
  '{local_head:$l[0:12], github_head:$r[0:12], in_sync:($l==$r)}'
# → {"local_head":"e2a4f8698f15","github_head":"e2a4f8698f15","in_sync":true}

3. Link-check the READMEfs reads a local file, we extract its URLs, then fetch fans out over the network. Local → transform → remote fan-out → join.

climcp --json call "fs.read_text_file(path: 'README.md')" \
  | jq -r '.content[0].text' \
  | grep -oE 'https?://[a-zA-Z0-9./?=_%:-]+' | sort -u \
  | while IFS= read -r u; do
      txt=$(climcp --json call "fetch.fetch(url: '$u', max_length: 80)" | jq -r '.content[0].text // empty')
      [ -n "$txt" ] && echo "$u" || echo "$u"
    done

Each step is independent and composable: swap a jq filter, redirect to a file, xargs the results into N parallel calls, feed one server's output into another. None of this is expressible in the MCP protocol, where the agent would have to carry every intermediate result through its own context just to hand it to the next tool call.

How it works

For describe and call, climcp opens the configured transport (spawning the child process for stdio, or POSTing to the URL for HTTP), performs the MCP initialize handshake over JSON-RPC 2.0, then issues tools/list or tools/call. A stdio child is shut down when the command finishes, and the whole operation is bounded by --timeout and cancelled on Ctrl-C.

Response-size guard

Because the whole point is to keep MCP output out of your context, a single tool that returns megabytes would defeat it. So a call whose rendered response exceeds --max-bytes (default 50 KB) is treated as a failure: climcp prints only a short preview, writes an explanatory error to stderr, and exits non-zero. Raise the cap with --max-bytes <n>, disable it with --max-bytes 0, or narrow the call (add a limit/path/query argument, or pipe --json through jq).

Development

make test     # go test ./...
make vet      # go vet ./...
make fmt      # gofmt -w .
make build    # -> ./bin/climcp

testdata/mockserver is a tiny stdio MCP server used by the end-to-end tests.

License

MIT © Asynkron

About

A CLI bridge to MCP servers — list, describe, and call stdio and HTTP MCP servers from your shell.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors