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.
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# 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@latestPre-built binaries for Linux, macOS, and Windows are attached to each release.
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:
./climcp.json~/.config/climcp/config.json~/.climcp.json
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'])"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 listOr 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 replacementsImport accepts both the mcpServers and servers config shapes. Name clashes
are skipped by default (reported), unless you pass --overwrite.
| 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>".
| 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.
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'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 README — fs 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"
doneEach 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.
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.
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).
make test # go test ./...
make vet # go vet ./...
make fmt # gofmt -w .
make build # -> ./bin/climcptestdata/mockserver is a tiny stdio MCP server used by the end-to-end tests.
MIT © Asynkron