mcpal
Inspect, call, and script any MCP server — over stdio or HTTP, with OAuth, from one CLI. The curl-equivalent for the Model Context Protocol.
Your editors (Claude Desktop, Cursor, Zed, opencode) configure dozens of MCP servers. Once configured, the only way to drive them is from inside that chat app. mcpal is the shell tool that was missing: point it at any server and call tools, read resources, get prompts, run raw JSON-RPC, or stream notifications.
New here? Install → Your first MCP call → Concepts.
mcpal server list --all
mcpal tool call cursor:linear get-issue --id ENG-123
mcpal auth login notion --oauth
mcpal --query 'content[0].text' tool call ev echo --message hi
MCP is a JSON-RPC contract between LLM-aware clients and servers that expose tools, resources, and prompts. mcpal plays the client role from outside any specific LLM app.
Why mcpal exists
LLM-facing MCP clients (Claude Desktop, Claude Code, Cursor, Zed, opencode) configure dozens of MCP servers — GitHub, Linear, Notion, a filesystem sandbox, a Postgres bridge, an internal HTTP service. Once they're configured, the only way to drive them is from inside that chat client. There is no curl-like tool for testing, scripting, or inspecting them.
mcpal fills that gap:
- Debugging MCP servers you're building. Run
tool call,tool describe,raw, andwatchagainst the server you just started. Skip the round-trip through a chat UI. - Scripting integrations in CI. A nightly job that pulls Linear tickets, opens a Notion page, files a GitHub issue, or syncs filesystem state — all through the same servers your team's editors already use.
- Calling already-configured servers from outside an LLM app.
Cursor configured
linear? Runmcpal tool list cursor:linear. No copy-paste ofmcp.json. - Auditing what's installed.
mcpal server discoverreports every server the supported clients know about, with paths, transports, and scopes.
The protocol itself is the same MCP that Anthropic published; mcpal just exposes its surface to a shell prompt.
Who it's for
- MCP server authors who want a curl for their server.
- Platform engineers wiring MCP servers into pipelines.
- Anyone running multiple MCP-aware editors who wants one tool to call any of their servers.
- Operators who'd rather paste a stack trace into an issue than a screenshot of a chat error.
What a real session looks like
Read Linear issues from the editor's already-configured Linear MCP:
mcpal tool list cursor:linear --names-only
mcpal tool call cursor:linear get-issue --id ENG-123
mcpal --query 'content[0].text' \
tool call cursor:linear list-my-issues --state in_progress
Pull GitHub releases over HTTP:
mcpal server add gh --http https://api.githubcopilot.com/mcp/
mcpal auth login gh --bearer $GH_TOKEN
mcpal --output json tool call gh list_releases --owner anthropics --repo claude-code
Mount a local filesystem sandbox:
mcpal server add fs -- npx -y @modelcontextprotocol/server-filesystem $HOME/projects
mcpal tool call fs read_file --path README.md
mcpal tool call fs search_files --pattern '*.toml'
Talk to an HTTP doc-search service (anonymous):
mcpal server add ctx7 --http https://mcp.context7.com/mcp
mcpal tool call ctx7 search --query 'Rust async runtimes'
Read OAuth-protected Notion:
mcpal server add notion --http https://mcp.notion.com/v1
mcpal auth login notion --oauth
mcpal --query '[].name' tool list notion
How this book is organised
The chapters follow the Diátaxis framework: a single tutorial, problem-driven how-tos, factual reference, and explanation.
Tutorial — start here if you have never used mcpal.
- Your first MCP call — install, register the reference server, list and call tools.
How-to guides — find the recipe for the problem you have.
- One-line MCP — drive any server in a single
shell command:
cmd:, URL, JSON spec, discovered ref. - Recipes — short task-driven snippets, including a cookbook against real servers.
- Authenticate to an HTTP server — bearer or OAuth 2.1, with a step-by-step walk-through of the OAuth handshake.
- Script around mcpal — exit codes,
--query, JSON output, CI patterns. - Troubleshoot —
mcpal debug doctor, common failures.
Reference — look up specifics.
- Protocol compliance matrix — every MCP method and the verb that calls it.
- Error codes — every
E####in long form.
Explanation — read once for the model.
- Concepts — references, transports, auth modes, output.
- Why a CLI for MCP — where a shell client earns its place next to an MCP-aware chat app, and where it doesn't.
License
MIT
Install
Pick whichever package manager is already on your machine. Every method drops the same binary; the difference is who's curating the metadata.
macOS / Linux — Homebrew
brew tap pawelb0/tap
brew install pawelb0/tap/mcpal
Debian / Ubuntu — .deb
curl -fsSLO https://github.com/pawelb0/mcpal/releases/latest/download/mcpal_amd64.deb
sudo dpkg -i mcpal_amd64.deb
Any platform — cargo
cargo install --git https://github.com/pawelb0/mcpal --path crates/mcpal
Needs a Rust toolchain (rustup recommended). Builds against the current main.
Prebuilt binary — curl | sh
curl -fsSL https://raw.githubusercontent.com/pawelb0/mcpal/main/dist/install.sh | sh
Drops the binary into $HOME/.local/bin. Read the script first if you're cautious — it's short.
Windows
cargo install --git https://github.com/pawelb0/mcpal --path crates/mcpal
Tested on Windows 11 + Windows Terminal. Credentials persist via DPAPI (the OS-native secret store). A prebuilt MSI and winget / Scoop manifests are on the roadmap.
Shell completions
mcpal completion bash > ~/.local/share/bash-completion/completions/mcpal
mcpal completion zsh > ~/.zsh/completions/_mcpal # ensure dir is on $fpath
mcpal completion fish > ~/.config/fish/completions/mcpal.fish
mcpal completion powershell >> $PROFILE
Re-source your shell after writing the completion file.
Verify
mcpal --version
mcpal debug doctor
debug doctor runs a quick local sanity check — config path, keyring backend, presence of npx for stdio servers.
Curious what shipped in the version you got? See the Changelog.
Your first MCP call
By the end you will have called a real MCP tool from your shell — mcpal installed, a reference server registered, a live response back. A lesson, not a reference; follow it top to bottom.
Time: about five minutes once npx is cached.
You will need: a shell, cargo, and npx (Node.js 18+).
1. Install
cargo install --path crates/mcpal
mcpal --version
Output:
mcpal 0.1.1
Prebuilt binaries: GitHub Releases (planned).
2. Register a stdio server
mcpal server add ev -- npx -y @modelcontextprotocol/server-everything
Output:
added server 'ev'
Tokens after -- form the spawned command. ev is the local alias.
3. Verify it speaks MCP
mcpal server ping ev
Output:
ok: true
ref: ev
4. List the server's tools
mcpal tool list ev
The reference server exposes about a dozen tools — echo,
get-sum, trigger-long-running-operation, and so on.
5. Call one
mcpal tool call ev echo --message hi
Output:
content:
- type: text
text: 'Echo: hi'
That round-trip is a real MCP tools/call request and response.
6. Filter the response
mcpal --query 'content[0].text' tool call ev echo --message hi
Output:
'Echo: hi'
--query runs JMESPath on the response before printing.
Next
- Recipes — copy-paste snippets per task.
- Authenticate to an HTTP server — bearer or OAuth 2.1.
- Concepts — how
<ref>resolves, transports, output formats.
Concepts
The model behind mcpal. Read once. The four moving parts are references, transports, auth modes, and output. Each is explained below; the how-to pages link back here whenever they refer to one of them.
Server reference (<ref>)
Every command that talks to an MCP server takes a <ref> positional. It
resolves in this order:
- Alias from
mcpal server add. - An
http(s)://URL (anonymous HTTP server). - Path to a JSON file with one
ServerSpec. <source>:<name>— a discovered server (cursor:linear).- Bare
<name>if unambiguous across discovered sources.
mcpal tool list ev
mcpal tool list https://mcp.example/mcp
mcpal tool list ./spec.json
mcpal tool list cursor:linear
mcpal tool list tavily
Transports
- stdio: mcpal spawns a child process and exchanges JSON-RPC over its stdin/stdout.
- Streamable HTTP: single endpoint, optional SSE stream. TLS via rustls.
The legacy 2024-11-05 SSE transport is not enabled.
Discovery
mcpal reads other clients' MCP config files:
| Client | macOS | Linux | Windows |
|---|---|---|---|
| Claude Code | ~/.claude.json | same | same |
| Claude Desktop | ~/Library/Application Support/Claude/ | ~/.config/Claude/ | %APPDATA%/Claude/ |
| Cursor | ~/.cursor/mcp.json | same | same |
| Zed | ~/.config/zed/settings.json | same | same |
| opencode | ~/.config/opencode/opencode.json | same | same |
Also: LM Studio (~/.lmstudio/mcp.json), Windsurf
(~/.codeium/windsurf/mcp_config.json), Cline (VS Code
globalStorage).
mcpal server discover lists everything found. mcpal server list --all shows owned and discovered together. Discovered servers are
addressable as <source>:<name> without copying their config.
Auth
| Mode | Storage | Command |
|---|---|---|
| Inline bearer | OS keyring | mcpal auth login <ref> --bearer <TOKEN> |
BearerEnv | environment variable | TOML: auth = { type = "bearer_env", env = "MY_TOKEN" } |
| OAuth 2.1 | OS keyring (stored credentials) | mcpal auth login <ref> --oauth |
| One-shot | environment | MCPAL_BEARER=… mcpal tool list <ref> |
Tokens live in the OS keyring (Keychain on macOS, Secret Service on
Linux, Credential Manager on Windows), never in config.toml. OAuth
flow: open browser → loopback callback → token exchange → store in
keyring.
The full lifecycle is in Auth deep dive.
Output
mcpal tool list <ref> # yaml (default)
mcpal --output json tool list <ref> # pretty JSON
mcpal --query 'content[0].text' … # JMESPath filter applied first
YAML is the default. Use --output json for jq. --query applies
a JMESPath expression before rendering.
Exit codes: see Scripting & exit codes.
Discovery
mcpal can pull MCP server definitions from other clients you already
have installed. Run mcpal server discover to scan, or mcpal server list (default) to see your registered + discovered entries side by side.
Supported clients
| Source | Files |
|---|---|
claude-code | ~/.claude.json |
claude-desktop | ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) / %APPDATA%\Claude\claude_desktop_config.json (Windows) |
cursor | ~/.cursor/mcp.json, project .cursor/mcp.json |
opencode | ~/.config/opencode/opencode.json |
vscode | <Code config>/User/mcp.json, project .vscode/mcp.json |
vscode-user | <Code config>/User/settings.json (chat.mcp.servers key) |
continue | <Code config>/User/globalStorage/continue.continue/config.json |
cline | <Code config>/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json |
codex | ~/.codex/config.toml |
lm-studio | ~/.lmstudio/mcp.json |
windsurf | ~/.codeium/windsurf/mcp_config.json |
zed | ~/.config/zed/settings.json (context_servers key) |
<Code config> resolves to %APPDATA%\Code on Windows,
~/Library/Application Support/Code on macOS, ~/.config/Code on Linux.
Refer to a discovered server with <source>:<name> — e.g.
mcpal tool list cursor:linear. Bare names resolve when unambiguous.
Custom paths
mcpal --discover-from ~/.config/private/team.json server list --discovered
--discover-from is repeatable and combines with the built-in sources.
Files must use the { "mcpServers": { "<name>": { ... } } } shape.
Missing files are skipped silently; parse errors log under -v.
Why a CLI for MCP
MCP servers were designed to be driven from inside an LLM client. The chat app holds the connection, lists every tool the server exposes, and lets the model call them. That model works well inside a conversation. It is awkward everywhere else.
This page explains where a shell-level client like mcpal earns its place, and where it does not.
Tool definitions cost tokens you may not need
When a chat client connects to an MCP server, it loads every tool's name, description, and JSON schema into the model's context. A server with forty tools costs context whether the model uses two of them or all of them. Connect four such servers and a non-trivial slice of the context window is gone before the first user message.
mcpal calls into the same servers without paying that price. The shell types one line:
mcpal tool call cursor:linear get-issue --id ENG-123
When an agent script needs to discover what's available, it asks for exactly what it needs:
mcpal tool list cursor:linear --names-only # names only
mcpal tool describe cursor:linear get-issue # one schema
mcpal tool template cursor:linear get-issue # a known-good skeleton
No upfront catalogue. Cost scales with what the script invokes, not with what the server advertises.
The shell already knows how to compose things
MCP responses are JSON-RPC frames inside a chat-only protocol. Once the chat tab closes, the call is gone. mcpal returns the same data to standard output, with stable exit codes:
mcpal --output json tool list cursor:linear |
jq -r '.[] | select(.name | startswith("get_")) | .name'
mcpal --query 'content[0].text' tool call ev echo --message hi
That's pipes, redirection, jq, xargs, cron, CI runners, exit-code
branching. None of it needs the model in the loop.
Reproducing a failure is mcpal server ping <ref> followed by the
exact command that broke, pasted into an issue with a stack trace
instead of a screenshot.
One authentication, shared across invocations
mcpal auth login runs the OAuth 2.1 + PKCE flow (or stores a
bearer token) once and writes credentials to the OS keyring. Every
subsequent mcpal tool call, every CI job that exports the same
profile, and every script in mcpal.yml reads from the same place.
There is no per-call browser dance, no token pasted into shell
history.
Servers your editor already configured
Most teams already have a working mcp.json somewhere. Claude
Desktop, Cursor, Zed, opencode, VS Code, Codex CLI — each writes
config to disk. mcpal server discover reads those files and makes
the servers addressable:
mcpal server discover
mcpal tool list cursor:linear
mcpal server list --all # owned + discovered
No duplication. The Linear server your editor configured is the same Linear server your shell can now drive.
When mcpal is the wrong answer
A CLI in front of MCP is not always the right move:
- Interactive use from inside the chat app. If the workflow is "ask the model, watch it call the tool," the chat client already owns that path. mcpal adds nothing.
- Real-time bidirectional flows. Sampling, elicitation, and
long-lived subscriptions live more naturally inside a connected
client. mcpal exposes them (
mcpal watch,--sampling-handler) but they are not its strongest surface. - End users without a terminal. A non-developer running a SaaS integration is not the audience.
Limitations
mcpal does not remove every MCP cost the published critiques point at:
- Each
mcpal tool callspawns the server fresh over stdio. Initialization (theinitializehandshake, thetools/listexchange a server may do on connect) runs every call. A long-runningmcpal servedaemon that holds warm sessions is on the roadmap. - The tool catalogue is not cached on disk.
tool listround-trips to the server. A local cache with a TTL is plausible future work.
Known gaps. The shell surface is what exists today; the project sits alongside MCP-aware chat clients, not in their place.
One-line MCP
You can drive an MCP server in a single shell command. No server add,
no config file edits, no auth flow up front. Pick a <ref> shape that
matches what you have:
| You have | One-line <ref> | Example |
|---|---|---|
| A local stdio command (npx, uvx, docker, anything) | cmd:<command> [args] | mcpal tool call "cmd:npx -y @modelcontextprotocol/server-everything" echo --message hi |
| An HTTP(S) URL | the URL plus --auth MODE | mcpal --auth env:GH_TOKEN tool list https://api.githubcopilot.com/mcp/ |
A ServerSpec JSON file on disk | the path | mcpal tool call ./spec.json read_file --path README.md |
| A server one of your editors already configured | <source>:<name> | mcpal tool call cursor:linear get-issue --id ENG-123 |
| A bare name that's unambiguous across discovered sources | the name | mcpal tool list linear |
| A registry server | install first, then call | mcpal server install io.github.foo/bar && mcpal tool list bar |
The order above is the resolution order. mcpal debug explain E0001
prints the same precedence in long form.
cmd: — ephemeral stdio
cmd:<command> [args] spawns the named program over stdio for the
duration of the call. The spec is never written to disk. Tokens after
cmd: are split on whitespace.
# everything server
mcpal tool list "cmd:npx -y @modelcontextprotocol/server-everything"
mcpal tool call "cmd:npx -y @modelcontextprotocol/server-everything" \
echo --message hi
# filesystem sandbox at $HOME/projects
mcpal tool call "cmd:npx -y @modelcontextprotocol/server-filesystem $HOME/projects" \
read_file --path README.md
# uv-managed Python server
mcpal --query 'content[0].text' \
tool call "cmd:uvx awslabs.aws-api-mcp-server@latest" describe_regions
# docker
mcpal tool list "cmd:docker run --rm -i ghcr.io/example/mcp"
Quote the whole cmd:… string so your shell groups it as one argument.
Values that need their own spaces, glob characters, or shell escapes
won't survive whitespace-splitting — for anything that fancy, use
mcpal server add and persist the spec.
cmd: carries no environment variables. Pass them via your shell
(API_KEY=… mcpal …) or persist with mcpal server add … --env K=V.
https://… — ephemeral HTTP
A literal URL resolves to an HTTP ServerSpec. By default the spec
carries auth = oauth. The first call without a stored token will
print a warning telling you to run mcpal auth login --oauth <url>.
mcpal tool list https://mcp.context7.com/mcp
mcpal --output json tool call https://mcp.context7.com/mcp \
search --query 'Rust async runtimes'
--auth MODE — pick the auth flavour inline
Override the default with the global --auth flag. Modes:
--auth | Behaviour |
|---|---|
oauth (default) | Send the stored OAuth access token |
none / anon | Send no Authorization header |
env:VAR | Read the bearer token from $VAR at call time |
bearer:TOKEN | Send the literal token (leaks to shell history) |
# anonymous HTTP MCP — no warning about a missing OAuth token
mcpal --auth none tool list https://mcp.context7.com/mcp
# token comes from an env var, never touches the spec file or history
GH_TOKEN=ghp_… mcpal --auth env:GH_TOKEN \
tool list https://api.githubcopilot.com/mcp/
# literal token (avoid — lives in shell history)
mcpal --auth bearer:ghp_… tool list https://api.githubcopilot.com/mcp/
For repeated use, persist with mcpal server add … --bearer-env GH_TOKEN
so future calls don't need the --auth flag at all.
./spec.json — ephemeral file
A path to a JSON ServerSpec resolves inline. Useful when a teammate
hands you a saved spec or when you generate one in CI:
cat > /tmp/ev.json <<'EOF'
{ "transport": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-everything"] }
EOF
mcpal tool list /tmp/ev.json
<source>:<name> and bare names — already-configured
mcpal server discover (or mcpal server list --all) prints the
<source>:<name> form of every server your editors already know about.
Either form is a valid <ref>:
mcpal server discover
mcpal tool list cursor:linear
mcpal tool list linear # if only one source has 'linear'
Registry — one extra line
The MCP Registry returns servers that often need env vars, so a true one-liner isn't always safe. The minimum is two:
mcpal server install io.github.modelcontextprotocol/server-everything --no-prompt
mcpal tool list server-everything
If you pre-supply env vars with --env K=V, you can install
non-interactively:
mcpal server install io.github.modelcontextprotocol/server-filesystem \
--env FS_ROOT=$HOME/projects --no-prompt
For full registry behaviour see Recipes.
What doesn't work in one line
- stdio with arguments that contain whitespace or shell metacharacters
—
cmd:is a whitespace split. Persist the spec withserver add. - Registry servers that declare required env vars and you didn't pass
--env—mcpal server installexits withE0017.
Static bearer tokens via --auth bearer:TOKEN do work in one line
but are noisy: the token lands in shell history and process listings.
--auth env:VAR is the safer one-line form.
Quick reference
# stdio ephemeral
mcpal tool list "cmd:npx -y @modelcontextprotocol/server-everything"
# HTTP — pick auth inline
mcpal --auth none tool list https://mcp.context7.com/mcp
mcpal --auth env:TOKEN tool list https://api.githubcopilot.com/mcp/
mcpal --auth oauth tool list https://mcp.notion.com/v1 # default
# spec file
mcpal tool list /tmp/ev.json
# already configured
mcpal tool list cursor:linear
mcpal tool list linear # if unambiguous
Recipes
Short, problem-driven snippets. Each section answers one question.
Replace <ref> with any server reference — see
Concepts → Server reference.
Real-server cookbook
Concrete examples against publicly known MCP servers. Tokens go to
the OS keyring (mcpal auth login); none of the commands write
secrets to disk.
Filesystem (local sandbox, stdio)
A scoped read/write surface over a directory tree. Useful for any command that wants file access without giving the LLM the whole machine.
mcpal server add fs -- \
npx -y @modelcontextprotocol/server-filesystem $HOME/projects
mcpal tool list fs --names-only
mcpal tool call fs read_file --path README.md
mcpal tool call fs list_directory --path .
mcpal tool call fs search_files --pattern '*.toml' --path .
mcpal --query 'content[0].text' tool call fs read_file --path Cargo.toml
The sandbox is the path passed at spawn time; you can pass multiple paths after the package name.
Fetch (HTTP client, stdio)
A bounded HTTP fetcher for one-off requests:
mcpal server add fetch -- npx -y @modelcontextprotocol/server-fetch
mcpal tool call fetch fetch --url https://httpbin.org/json
Time
mcpal server add time -- npx -y @modelcontextprotocol/server-time
mcpal tool call time get_current_time --timezone Europe/Warsaw
GitHub (HTTP, bearer)
The hosted GitHub MCP at api.githubcopilot.com/mcp/ accepts a
personal access token (classic or fine-grained) as a bearer:
mcpal server add gh --http https://api.githubcopilot.com/mcp/
mcpal auth login gh --bearer ghp_xxx # or use $GITHUB_TOKEN
mcpal tool list gh --names-only | head
mcpal --query '[].name' \
tool call gh list_repositories_for_user --username anthropics
mcpal --output json \
tool call gh search_issues --q 'repo:anthropics/claude-code is:open label:bug' \
| jq '.[].title'
For CI, put the token in an env var and reference it from config.toml:
[server.gh]
transport = "http"
url = "https://api.githubcopilot.com/mcp/"
auth = { type = "bearer_env", env = "GITHUB_TOKEN" }
Linear (HTTP, OAuth)
Linear's MCP authenticates users via OAuth 2.1. mcpal runs the full PKCE + DCR flow for you; no developer-console step:
mcpal server add linear --http https://mcp.linear.app/mcp
mcpal auth login linear --oauth # browser → consent → done
mcpal --query '[].name' tool list linear
# Find issues assigned to me, in progress:
mcpal --query 'content[0].text' \
tool call linear list_my_issues --state in_progress
# Comment on one:
mcpal tool call linear create_comment \
--issueId ENG-123 --body 'updated branch is up'
mcpal auth refresh linear rotates the access token when it expires;
mcpal also refreshes eagerly within 30 s of expiry. See the
OAuth walk-through for what each
step does on the wire.
Notion (HTTP, OAuth)
mcpal server add notion --http https://mcp.notion.com/v1
mcpal auth login notion --oauth
mcpal tool list notion --names-only
mcpal --query 'content[0].text' \
tool call notion search --query 'meeting notes 2026 Q2'
mcpal tool call notion append_block \
--pageId 8a7…b21 \
--blocks '[{"type":"paragraph","text":"shipped v0.1"}]'
Context7 (HTTP, anonymous)
A free hosted docs-search MCP — no auth, useful as a sanity check:
mcpal server add ctx7 --http https://mcp.context7.com/mcp
mcpal tool call ctx7 search --query 'rmcp ServiceExt'
Postgres (stdio, env-injected creds)
mcpal server add db -- \
npx -y @modelcontextprotocol/server-postgres \
postgres://user:pass@localhost:5432/app
mcpal tool list db
mcpal tool call db query --sql 'select count(*) from users'
Put the connection string in an env var and --env it through:
mcpal server add db \
--env DATABASE_URL="$DATABASE_URL" \
-- npx -y @modelcontextprotocol/server-postgres '$DATABASE_URL'
opencode's already-configured servers
If you run opencode, every server in ~/.config/opencode/opencode.json
is callable directly:
mcpal server discover --source opencode
mcpal tool list opencode:tavily
mcpal tool call opencode:tavily search --query 'Rust async runtimes'
Same pattern for cursor:, claude-code:, zed:, etc. See
Concepts → Discovery for the source list.
Install a server from the MCP Registry
mcpal server search filesystem --limit 5
mcpal server install io.github.Oncorporation/filesystem-server --as fs
mcpal server install io.github.foo/bar --env API_KEY=$KEY
mcpal tool list fs
server install resolves the registry entry's package
(npm → npx, pypi → uvx, oci → docker run) or its
streamable-http remote into a ServerSpec and writes it to your config.
Required env vars without defaults must be supplied via --env K=V.
List servers already on the machine
mcpal server discover
Use an mcp.json without registering it
mcpal --mcp-json ./mcp.json tool list <name>
Global flag; overlays for the session and writes nothing to disk.
Call a tool with a JSON arg body
mcpal tool call ev some-tool --params '{"message":"hi","count":3}'
From stdin or file:
echo '{"message":"hi"}' | mcpal tool call ev some-tool --params -
mcpal tool call ev some-tool --params @args.json
--cli-input-json accepts a base body from a path or - (stdin). Mix --params with
--key value overrides; flag values win:
mcpal tool call ev some-tool --params @base.json --message override
Or generate a skeleton:
mcpal --output json tool template ev some-tool \
| jq '.field = "value"' \
| mcpal tool call ev some-tool --cli-input-json -
Extract one field from a response
mcpal --query 'content[0].text' tool call ev echo --message hi
--query is JMESPath. See the tutorial.
Watch a long-running tool
# terminal 1
mcpal watch ev
# terminal 2
mcpal tool call ev trigger-long-running-operation --duration 10 --steps 5
One YAML doc per notification (progress, log, resource-updated, list-changed). Ctrl-C to exit.
Send a raw JSON-RPC method
mcpal raw <ref> some/new-method --params '{"foo":"bar"}'
mcpal raw <ref> some/method --params @payload.json
mcpal raw <ref> some/method --params -
Loop a tool over many inputs
for q in rust go python; do
mcpal tool call github search --q "$q stars:>1000" --per_page 3
done
seq 1 50 | xargs -P 8 -I {} \
mcpal tool call worker process --batch-id {}
Branch on exit code
mcpal tool call ev echo --message hi
case $? in
0) echo "ok" ;;
3) echo "ref not found"; exit 1 ;;
4) mcpal auth login ev --oauth ;;
5) mcpal auth refresh ev ;;
*) echo "see above"; exit $? ;;
esac
Disable interactive prompts (CI)
mcpal --no-interactive tool call <ref> …
Elicitation requests auto-decline. Bearer tokens come from
MCPAL_BEARER or --bearer, never a TTY prompt.
External sampling handler
mcpal --sampling-handler "claude --output json" \
tool call <ref> trigger-sampling-request --prompt "summarize"
mcpal pipes CreateMessageRequestParams JSON to the handler's stdin and
reads CreateMessageResult JSON from its stdout.
Expose workspace roots
mcpal --root ~/src/my-project --root /tmp \
tool call <ref> get-roots-list
Resources
mcpal resource list <ref>
mcpal resource read <ref> demo://resource/static/document/architecture.md
mcpal resource template list <ref>
mcpal resource subscribe <ref> some://uri
Prompts
mcpal prompt list <ref>
mcpal prompt get <ref> some-prompt --city Dallas --state Texas
Diff two servers' capabilities
mcpal diff <ref-a> <ref-b>
mcpal diff <ref-a> <ref-b> --only tools
Reports added, removed, and changed entries per category
(tools, resources, prompts). A tool counts as changed when its
inputSchema differs between the two servers.
Shell completions
mcpal completion zsh > ~/.zfunc/_mcpal
bash and fish work the same way.
Completing tool / resource / prompt names
tool list, resource list, and prompt list accept --names-only,
which prints one name (or URI) per line on stdout. Wire it into your
shell's completion. For zsh, with the cursor after mcpal tool call ev :
_mcpal_tools() {
# $words: (mcpal tool call <ref> …); the ref is words[-2] from CURRENT.
local ref=${words[-2]}
compadd -- $(mcpal tool list "$ref" --names-only 2>/dev/null)
}
compdef _mcpal_tools 'mcpal tool call'
Bash equivalent (inside your complete -F function, with $prev already
set to the ref token):
COMPREPLY=( $(compgen -W "$(mcpal tool list "$prev" --names-only 2>/dev/null)" -- "$cur") )
stdio servers may leak their own stderr (Starting default (STDIO) server... and similar) during completion. The 2>/dev/null above
suppresses it. Setting MCPAL_CHILD_STDERR=inherit un-silences it
again.
Collections
Drop a mcpal.yml at your project root, define saved tool calls and
profile-scoped variables, then run them by name:
mcpal run get-issue --profile prod
The collection file is plain YAML — check it into git, share it with
teammates, switch environments with --profile. Secrets stay out of
the file ({{env.X}} reads them at runtime from your shell or
.envrc).
Minimal example
default-profile: dev
profiles:
dev:
issue_id: ENG-1
workspace: my-team
prod:
issue_id: ENG-999
workspace: my-team
calls:
get-issue:
server: cursor:linear
tool: get-issue
params:
id: "{{profile.issue_id}}"
workspace: "{{profile.workspace}}"
echo-token:
server: gh
tool: list_repos
params:
owner: "{{env.GH_USER}}"
server accepts any <ref> mcpal already understands — an alias from
mcpal server add, a <source>:<name> pair from mcpal server discover, or an https:// URL.
Lookup
mcpal run walks from the current directory up to the filesystem
root looking for mcpal.yml. First hit wins. Override with
--collection PATH:
mcpal --collection ./mcpal.staging.yml run get-issue
If no file is found, E0015.
Profiles
Pick which one is active with (in precedence order):
--profile NAMEon the command lineMCPAL_PROFILEenv vardefault-profile:key inmcpal.yml- literal
default
If the active name isn't a profile in the file, E0016.
Naming caveat: don't name a profile default. The literal string
default is the fallback marker — if both your --profile resolves
to default and the file declares default-profile: dev, the
file's dev wins. Pick any other name for the dev/staging baseline.
Templating
Two namespaces, nothing else:
{{profile.X}}— reads from the active profile's key/value map.{{env.X}}— reads from your OS environment.
Substitution happens before the call is sent. Unresolved variables
fail loudly with E0014 (all misses listed in one message); the
request never reaches the server.
Escape literal {{ with {{{{.
Dry-run
mcpal run echo --dry-run
Prints the resolved (server, tool, params) JSON and exits without
opening a connection. Useful for CI assertions on what a call would
do.
One-off overrides
mcpal run echo --params-override message="custom value"
--params-override overlays raw K=V pairs onto the rendered params
after templating. Repeatable.
Authenticate to an HTTP server
One-liner
mcpal server add gh --http https://api.githubcopilot.com/mcp/ --bearer $GH_TOKEN
mcpal server add notion --http https://mcp.notion.com/v1 --oauth
mcpal auth login is for rotating a token later or recovering from a
mid-OAuth failure — see below.
How to attach credentials to a remote MCP server, by mode. Stdio servers inherit the parent shell's env and don't take credentials; this page is HTTP only.
mcpal supports three auth modes, plus a one-shot env override:
| Mode | Where the secret lives | Best for |
|---|---|---|
| Inline bearer | OS keyring under bearer:<ref> | personal access tokens, CI service tokens |
BearerEnv | environment variable named in config.toml | secrets injected by another tool (sops, vault, GitHub Actions) |
| OAuth 2.1 + PKCE + DCR | OS keyring under oauth:<ref> | user-facing services that authenticate humans (Notion, Linear, Atlassian) |
MCPAL_BEARER | environment variable | one-shot scripts that don't want to touch the keyring |
The OS keyring is Keychain on macOS, Secret Service on Linux,
Credential Manager on Windows. mcpal never writes secrets to
config.toml.
Bearer tokens
Store the token in the keyring:
mcpal auth login github --bearer ghp_xxx
mcpal tool list github
Read from stdin (good when the token comes from another tool):
secret-tool get service mcp-github | mcpal auth login github --bearer -
Use a different env var per call:
MCPAL_BEARER=ghp_xxx mcpal tool list github
Already configured the same server in another MCP client (Cursor,
Claude Code, opencode, ...)? Import — mcpal lifts the bearer out
of the source mcp.json, stores it in the keyring, and leaves
your config.toml clean:
mcpal server discover # confirm it's there
mcpal server import --from cursor github # alias = github
mcpal server import --from opencode gh --as gh # rename on import
mcpal tool list github # just works
${VAR} placeholders in the source's Authorization header are
preserved — they become BearerEnv entries pointing at the same
env var, so the secret keeps living in your shell, not on disk.
Reference an env var from config.toml (lets one config travel
between machines without baking the secret in):
[server.github]
transport = "http"
url = "https://api.githubcopilot.com/mcp/"
auth = { type = "bearer_env", env = "GITHUB_MCP_TOKEN" }
Credentials are resolved per call in this order:
AuthSpec::Bearer { token }(rare; avoid writing tokens to TOML).AuthSpec::BearerEnv { env }.- OAuth blob in the keyring under
oauth:<ref>. - Bearer keyring entry under
bearer:<ref>. MCPAL_BEARERenv var.
Inspect what's stored:
mcpal auth status github
# {ref: github, bearer: true, oauth: false}
Remove credentials:
mcpal auth logout github
OAuth 2.1 + PKCE + DCR
For services that authenticate human users — Notion, Linear, Atlassian, and so on. mcpal runs the full OAuth 2.1 authorization-code flow with PKCE and Dynamic Client Registration; you do not need to pre-register mcpal in a developer console.
The shortest path
mcpal server add notion --http https://mcp.notion.com/v1
mcpal auth login notion --oauth
# → browser opens, you click "allow", terminal prints "authorized"
mcpal tool list notion
That's it for happy path. The rest of this section explains what actually happened so you can debug it when it doesn't.
What mcpal auth login --oauth does
Five RFCs interlock here: OAuth 2.1 (the framework), RFC 7636 PKCE (protects the auth code in transit), RFC 7591 Dynamic Client Registration (lets mcpal register itself without a developer console trip), RFC 8414 AS metadata (tells mcpal where the endpoints are), and RFC 9728 Protected Resource Metadata (the MCP server points at its authorization server).
The flow, step by step:
1. Resource metadata probe. mcpal sends a single GET to the MCP
server's URL. If the server responds with a 401 and a
WWW-Authenticate: Bearer resource_metadata="<url>" header, mcpal
follows the link. Otherwise it falls back to the well-known path:
GET /.well-known/oauth-protected-resource
→ {
"resource": "https://mcp.notion.com/v1",
"authorization_servers": ["https://mcp.notion.com"]
}
The response tells mcpal which authorization server (AS) gates this resource.
2. AS metadata discovery. mcpal asks that AS for its endpoints:
GET https://mcp.notion.com/.well-known/oauth-authorization-server
→ {
"issuer": "https://mcp.notion.com",
"authorization_endpoint": ".../authorize",
"token_endpoint": ".../token",
"registration_endpoint": ".../register",
"response_types_supported": ["code"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none"]
}
If the server doesn't ship oauth-authorization-server, mcpal also
tries /.well-known/openid-configuration (OpenID Connect-style).
3. Dynamic Client Registration. Most public MCP servers don't want you to pre-register a client; they accept RFC 7591 DCR. mcpal POSTs:
POST .../register
{
"client_name": "mcpal",
"redirect_uris": ["http://127.0.0.1:<random>/callback"],
"grant_types": ["authorization_code", "refresh_token"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}
The AS replies with a client_id (and optionally client_secret).
mcpal stores both in the keyring under client:<ref> so the next
login on the same machine reuses the same client.
4. PKCE setup. mcpal generates a random code_verifier and
derives code_challenge = base64url(sha256(code_verifier)). The
challenge goes into the authorize URL; the verifier stays on disk
until the token exchange.
5. Loopback callback. mcpal binds a TCP listener on
127.0.0.1:0, captures the assigned port, and prints the authorize
URL:
.../authorize?
response_type=code&
client_id=<from step 3>&
redirect_uri=http://127.0.0.1:54321/callback&
state=<csrf token>&
code_challenge=<from step 4>&
code_challenge_method=S256&
resource=https%3A%2F%2Fmcp.notion.com%2Fv1
mcpal opens that URL in the default browser unless you pass
--no-browser. The user clicks "allow" (or rejects). The AS
redirects the browser to http://127.0.0.1:54321/callback?code=…&state=….
mcpal's listener catches the request, validates state against the
CSRF token it generated, and reads the code.
6. Token exchange. mcpal POSTs the code plus the PKCE verifier:
POST .../token
grant_type=authorization_code&
code=<from step 5>&
redirect_uri=http://127.0.0.1:54321/callback&
client_id=<from step 3>&
code_verifier=<from step 4>
The AS validates code_verifier matches code_challenge, replies
with an access token and (usually) a refresh token:
{
"access_token": "...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "..."
}
7. Store. mcpal writes the response plus token_received_at into
the keyring under oauth:<ref>. Every subsequent mcpal <verb> <ref> reads that blob and sends Authorization: Bearer <access_token>.
Refresh
Access tokens are short-lived (an hour is typical). Before each call,
mcpal checks now() + 30s >= token_received_at + expires_in. If yes
it sends the refresh token to the AS:
POST .../token
grant_type=refresh_token&
refresh_token=<stored>
…and replaces the stored blob. You can also refresh by hand:
mcpal auth refresh notion
If the refresh token has also expired or been revoked, mcpal surfaces
E0004; re-run mcpal auth login notion --oauth.
Variations
No browser. Useful over SSH:
mcpal auth login notion --oauth --no-browser
# mcpal prints the URL; you open it on another machine
The loopback callback still has to reach mcpal. If the auth happens
on a different machine, that's harder; either tunnel the port back
(ssh -L 54321:127.0.0.1:54321 ... then visit
http://127.0.0.1:54321/callback?... locally) or run the whole flow
on the workstation and copy ~/Library/Application Support/mcpal/config.toml plus the keyring entry.
Pre-registered client. Some services don't support DCR. Add the
client_id and secret directly to the keyring (advanced; see
mcpal/oauth source for the JSON shape).
Self-signed AS. Not currently supported; rmcp uses rustls with the system trust store.
What mcpal debug doctor checks for auth
mcpal debug doctor reports per-server:
bearer_stored— is there abearer:<ref>keyring entry?oauth_stored— is there anoauth:<ref>keyring entry?oauth_access_token_present— does the blob contain anaccess_tokenfield? (False after a failed refresh.)
It also round-trips a canary key to confirm the keyring is alive.
Where each token lives
All entries are under keyring service mcpal.
| Key | Contents |
|---|---|
bearer:<ref> | raw bearer string |
oauth:<ref> | JSON StoredCredentials (rmcp): { client_id, token_response, granted_scopes, token_received_at } |
client:<ref> | DCR result { client_id, client_secret? } |
On Linux the keyring lives in Secret Service
(org.freedesktop.secrets); the linux-native-sync-persistent
feature talks to it directly. Headless Linux without a Secret Service
daemon needs MCPAL_BEARER or gnome-keyring-daemon --start.
Choosing a mode
- Personal access token for one service → bearer in the keyring.
- Token rotated by another tool / available as
$VAR→BearerEnv. - Server authenticates end users (Notion, Linear, etc.) → OAuth.
- One-shot in a script that shouldn't touch the keyring →
MCPAL_BEARER=… mcpal ….
Interactive TUI
mcpal tui opens a three-pane curses-style browser over every MCP
server that mcpal can see (owned + discovered). Useful when you do
not remember a tool's exact name or schema and want to call it
without composing a shell command first.
┌─ Servers ──────────────┬─ Detail ───────────────────────────┐
│ > opencode:linear ⚡ │ Tools (12) Resources (3) Prompts │
│ cursor:notion 🔒 │ list_issues │
│ ev ● │ create_comment │
│ mcp.context7.com ⚡ │ add_assignee │
│ fs (stdio) ● │ … │
├─ Output ───────────────┴─────────────────────────────────────┤
│ $ connect opencode:linear │
│ ✓ connected to opencode:linear (12 tools) │
│ $ tool call opencode:linear list_issues │
│ ✓ opencode:linear list_issues │
└ Tab cycle · Enter open · / filter · ? help · q quit ─────────┘
Layout
The three panes are Sidebar (servers), Detail (tools / resources / prompts of the selected server, or a tool schema / result), and Output (a 200-line ring of command echoes plus live notifications from connected servers).
Icons in the sidebar tag the transport:
●stdio⚡HTTP, no auth required🔒HTTP, OAuth required
Key map
| Key | Action |
|---|---|
Tab / Shift+Tab | cycle pane focus |
j/k, ↓/↑ | move selection |
gg / G | jump to top / bottom |
Enter | drill in (sidebar → detail tabs → schema / form) |
Esc | drill back, or close a modal |
/ | filter the sidebar; Esc clears, Enter accepts |
c | call the selected tool (Detail focus, Tools tab) |
l / Right | next tab in Detail (Tools → Resources → Prompts) |
b | open a bearer-token input for the selected server |
? | toggle help overlay |
q, Ctrl-C | quit |
Calling a tool
c on a tool opens a form modal whose fields come from the tool's
inputSchema. Each field is labelled with its type (str, int,
num, bool, json) and an * when required. Tab cycles
fields. Enter submits. The terminal renders the response inline
in the Detail pane; the Output pane gets a one-line echo with a
✓ or ✗.
If the schema declares object or array, the field stores a raw
JSON literal — paste it directly.
Notifications
mcpal forwards every notification it sees from a connected server into the Output pane:
progressbecomes→ progress N/M.logbecomes→ log <level>: <message>.- list-changed / resource-updated become a generic
→ <kind>line.
The buffer is bounded at 200 lines.
Building without the TUI
The TUI is gated behind the tui feature, which is on by default.
To get a smaller mcpal binary without ratatui, crossterm,
tui-input, and tui-textarea:
cargo install mcpal --no-default-features
mcpal tui then prints "unrecognized command" and exits 2.
What's not in v1
- In-TUI OAuth flow. If a server returns 401, drop out and run
mcpal auth login <ref> --oauthin another shell. :command bar.- Persistent layout / theme overrides.
File issues at https://github.com/pawelb0/mcpal/issues.
UI-rich MCP servers
Some MCP servers don't just return text — they return UI. A
weather server may hand back an interactive chart. A booking
agent may serve a confirm-this-trip form. A dashboard tool may
embed a React component you can poke at right inside the host
application. This chapter is about the two ways MCP servers
ship that UI today, what those payloads look like on the wire,
and how mcpal ui inspect lets you debug them from a terminal
without spinning up a full MCP client.
Why UI started showing up in MCP
The base MCP protocol was built for text. A tool returns
content: [{ type: "text", text: "…" }], the client renders
it, the LLM reads it, end of story. That works for code
assistants and chatty agents. It falls apart the moment your
tool wants to do something a paragraph of Markdown can't do:
- Render a chart with the actual numbers, not their textual description.
- Let the user pick from a long, dynamic list (calendars, catalogs, dashboards) without forcing the LLM to enumerate options.
- Capture a structured action — a "buy this", "approve that", "set these dates" — that the LLM can then continue acting on.
To paper over that gap, two parallel UI standards appeared on top of MCP:
-
mcp-ui is an open standard. A tool result includes one or more embedded resources whose URI starts with
ui://. The resource body is HTML (or an iframe pointer). The client renders the HTML in a webview and routes any user actions back to the server as fresh tool calls. -
OpenAI Apps SDK is OpenAI's flavour, used by apps that live inside ChatGPT. A tool result includes an embedded resource with MIME type
application/vnd.openai.app+json. That JSON describes a component tree (their own runtime), which ChatGPT renders natively.
Both ride the standard MCP wire format — they just stuff their payload inside a resource block. Neither is part of the MCP spec proper. mcpal handles them the same way a curious user would: detect the payload, classify it, and let you peel it open.
What a UI response looks like
Strip a tool call response of its envelope and you get a
CallToolResult:
{
"content": [
{
"type": "text",
"text": "Here is your weather:"
},
{
"type": "resource",
"resource": {
"uri": "ui://weather/london",
"mimeType": "text/html",
"text": "<html>…interactive forecast…</html>"
}
}
],
"isError": false
}
The text block is for the LLM ("describe this"). The
resource block is for the user's eyeballs. A naive MCP client
that doesn't speak mcp-ui prints the text, drops the resource
on the floor, and the rich UI is invisible to the user.
For OpenAI Apps the resource looks like:
{
"type": "resource",
"resource": {
"uri": "openai://app/booking-confirm/3a91…",
"mimeType": "application/vnd.openai.app+json",
"text": "{\"component\":\"BookingConfirm\",\"props\":{…}}"
}
}
Same shape, different MIME, different runtime needed to render.
mcpal ui inspect: triage from a terminal
mcpal ui inspect calls a tool and tells you exactly what
came back, block by block:
mcpal ui inspect demo-server show_weather --params '{"city":"London"}'
Output (YAML by default; --output json for JSON):
reference: demo-server
tool: show_weather
ui_resources: 1
is_error: false
hits:
- index: 0
kind: text
size_bytes: 32
- index: 1
kind: mcp_ui
uri: ui://weather/london
mime_type: text/html
size_bytes: 2814
kind is the headline classification:
kind | Meaning |
|---|---|
text | Plain text/text content block. |
image / audio | Base64 content with a media MIME. |
mcp_ui | Embedded resource whose URI starts with ui://. |
openai_app | Embedded resource with vnd.openai.app+json MIME. |
resource | Embedded resource that isn't UI (data attachment). |
resource_link | Pointer to a resource the server doesn't inline. |
ui_resources is the count of mcp_ui + openai_app blocks.
That's the number you care about when asking did this server
actually return any UI?.
Save the payload to disk
By default inspect only prints the summary. Pass --save
to dump UI/app payloads to /tmp/mcpal-ui-<pid>-<index>.{html,json,js}:
mcpal ui inspect demo-server show_weather --params '{"city":"London"}' --save
The summary lines now end with paths you can cat, grep,
diff, or pipe into a linter.
Open in a browser
--open implies --save and additionally hands each file to
your OS opener (open on macOS, xdg-open on Linux,
explorer on Windows):
mcpal ui inspect demo-server show_weather --params '{"city":"London"}' --open
A mcp-ui HTML payload will load straight into a browser as a standalone file — most demos work this way out of the box. An OpenAI Apps JSON payload won't render directly: it needs OpenAI's runtime. You get the descriptor on disk so you can read it, validate it, or hand it to whatever harness you're building.
The TUI badge
mcpal tui paints a magenta UI ✦ next to the tool name in
the Detail pane whenever a call result carries an mcp-ui or
OpenAI Apps block. No keystroke needed — the classifier runs
on every result. To save the payload from a TUI session, drop
back to the CLI:
mcpal ui inspect <ref> <tool> --params '<the args>' --save
A future release may bind a key to that directly. For now the badge is the cue; the saving is one shell away.
When this is useful
-
Building an mcp-ui server. You wrote a tool that returns HTML in a
ui://resource and your client doesn't render it.mcpal ui inspect --save --openproves whether the payload is well-formed and whether the HTML stands up on its own, before you start blaming the client. -
Validating an OpenAI App. Apps SDK components are opaque JSON descriptors.
mcpal ui inspect --savelets you diff what your tool returned today against what it returned yesterday — easy regression check without booting ChatGPT. -
Security review. UI resources are arbitrary HTML or JS served by an MCP server you may not fully trust.
mcpal ui inspect --savewrites the payload to a regular file you can feed to whatever scanner or linter you'd run on third-party code: grep forscript src=, run an HTML validator, check for inline event handlers, whatever your threat model demands. -
Debugging a client that should render UI but doesn't. If
mcpal ui inspectreportsui_resources: 0, the server isn't sending UI — talk to the server team. If it reportsui_resources: 1and your client still shows nothing, the bug lives in the client. -
Capturing fixtures. Once
--saveproduces a file, you have a golden HTML/JSON artifact to check into a test suite. Replay against a stub, snapshot-test the renderer, done.
What mcpal ui inspect does not do
-
It does not render the UI itself. mcpal is a terminal tool; it tells you what's there and hands the file to your real browser or a downstream harness.
-
It does not relay user interactions back to the server. A full mcp-ui experience includes
postMessageround-trips from the rendered iframe back to the MCP server, which then may issue new tool calls. Implementing that bridge would turn mcpal into a webview host. Out of scope. -
It does not validate against either standard's spec. The classifier is pattern-match on URI prefix + MIME, nothing deeper. Adding a
--strictvalidator is on the list.
See also
- mcp-ui spec and reference servers: https://github.com/idosal/mcp-ui
- OpenAI Apps SDK: https://platform.openai.com/docs/guides/apps-sdk
- Recipes → Real-server cookbook for non-UI tool calls
- Authenticate to an HTTP server for the bearer / OAuth flow you'll need before calling most production UI servers
Script around mcpal
How to drive mcpal from scripts, shells, and CI. Covers exit codes,
machine output, JMESPath filtering, the raw escape hatch, env
vars, and a sample GitHub Actions step.
Contract:
- stdout is data. Informational output goes to stderr.
- Exit codes are stable per failure class; the wording around them is not.
--output jsonand--query <jmespath>remove most uses ofjq.
Exit codes
| Code | Meaning | Error code(s) | Common fix |
|---|---|---|---|
| 0 | success | — | — |
| 1 | generic error | E0000 | check stderr |
| 2 | usage / invalid arguments | E0002, E0009, E0010 | mcpal <subcommand> --help |
| 3 | server reference not found | E0001 | mcpal server discover |
| 4 | auth required | E0003 | mcpal auth login <ref> |
| 5 | auth expired | E0004 | mcpal auth refresh <ref> |
| 6 | transport / not yet supported | E0005, E0008 | network unreachable or mcpal raw |
| 7 | server returned a JSON-RPC error | E0006 | check args against tool describe |
| 8 | request timed out | E0007 | retry; with --timeout, raise the value |
| 130 | interrupted (Ctrl-C) | E0011 | — |
Each error prints error[E####]: plus hints. mcpal debug explain E####
shows the long form.
--output json
mcpal --output json tool list <ref> | jq -r '.[].name'
mcpal --output json server ping <ref> | jq -r '.peerInfo.serverInfo.version'
YAML is the default for human reading. Set --output json in pipelines.
--query (JMESPath)
For one-liners:
mcpal --query 'content[0].text' tool call ev echo --message hi
mcpal --query '[].name' tool list ev
mcpal --query 'peerInfo.serverInfo.{name:name,version:version}' server ping ev
Standard JMESPath. Tutorial.
Reading args from stdin or files
tool call accepts:
--key valueflags — typed JSON values (numbers, booleans, JSON literals).--cli-input-json @path/to.json— read a base object from a file.--cli-input-json -— read from stdin.- Mix: base from file or stdin, override with
--key value.
echo '{"a":1,"b":2}' | mcpal tool call ev some --cli-input-json - --b 99
raw for unmapped methods
mcpal raw <ref> some/method --params '{"k":"v"}'
mcpal raw <ref> some/method --params @payload.json
mcpal raw <ref> some/method --params -
With --query and --output:
mcpal --query 'tools[].name' --output json raw <ref> tools/list
watch
mcpal watch <ref>
One YAML doc per notification (progress, log, resource-updated, list-changed). Run alongside another terminal that drives requests.
Env vars
| Var | Effect |
|---|---|
MCPAL_CONFIG | path to config.toml |
MCPAL_PROFILE | accepted but unused (will gate profile selection later) |
MCPAL_BEARER | one-shot bearer for any HTTP server |
MCPAL_SAMPLING_HANDLER | shell command for sampling/createMessage |
MCPAL_CHILD_STDERR=inherit | un-silence the spawned stdio server's stderr |
RUST_LOG | tracing filter (e.g. info,mcpal=debug) |
GitHub Actions
- run: cargo install --path crates/mcpal
- run: |
mcpal server add api --http $MCP_URL
mcpal debug doctor --output json
mcpal --output json tool list api > tools.json
env:
MCPAL_BEARER: ${{ secrets.MCP_TOKEN }}
MCPAL_CONFIG: ${{ runner.temp }}/mcpal.toml
Don't
- Parse human stderr. mcpal's wording changes; the exit code and the
error[E####]prefix don't. - Rely on TTY colors in scripts. mcpal already disables ANSI on non-TTY stdout.
- Rely on argument order beyond positionals. Use
--key valuethroughout.
Troubleshoot
How to diagnose a failed call. Start with mcpal debug doctor, then
look up the failing E#### code below. Each section maps to one
exit-code class. The full prose for each code is in
Error codes.
mcpal debug doctor
mcpal debug doctor
Checks: config readable, keyring round-trip, auth state per server,
discovery counts. YAML default; --output json for bug reports.
E0001 — "server 'X' not found"
error[E0001]: server 'foo' not found (owned, URL, path, or discovered)
help: run `mcpal server discover` to scan installed MCP clients for servers
help: or `mcpal server list --all` to see what's already configured
help: or add one: `mcpal server add <alias> --stdio <command>`
mcpal server discoverlists every client config mcpal scans.- If you copied a config from Cursor or Claude Desktop, try
mcpal --mcp-json ./mcp.json tool list <name>and skip registration. mcpal debug explain E0001for the resolver order.
E0003 / E0004 — auth
- E0003: no credentials.
mcpal auth login <ref> --bearer …or--oauth. - E0004: server rejected the token.
mcpal auth refresh <ref>; if refresh fails, re-login.
mcpal auth status <ref> shows what's stored.
E0005 — transport error
No response from the server.
- HTTP: verify with
curl -I <url>that the host is reachable. - stdio: confirm the command runs standalone.
npx -yon a cold cache installs the package (10–60s); subsequent runs complete in <5s. - Re-run with
-v(or-vv) for the request trace. - Reproduce with
mcpal server ping <ref>.
E0006 — server-returned error
A well-formed JSON-RPC error from the server.
- The tool, resource, or prompt name is wrong. Check
mcpal tool list <ref>. - The arguments don't match
inputSchema. Verify withmcpal tool describe <ref> <name>and rebuild withmcpal tool template <ref> <name>.
E0007 — request timed out
Triggered when no response arrives within the deadline. First npx -y
runs commonly hit this on a cold cache. Retry; subsequent runs hit the
cache and complete in <5s. Also check the server isn't waiting on
stdin.
E0008 — not yet supported
The MCP feature isn't wired in mcpal yet. Use
mcpal raw <ref> <method> --params <…> to send the JSON-RPC directly.
E0009 — JMESPath errors
error[E0009]: query: search: …
help: JMESPath syntax — see https://jmespath.org/tutorial.html
help: common: `.field` projects, `[]` flattens, `[?cond]` filters
help: preview without the filter to inspect the shape first
Print the unfiltered response first to see the shape:
mcpal --output json tool list <ref>
mcpal --query '[].name' tool list <ref>
E0010 — JSON payload didn't parse
Shell quoting is the common cause:
# wrong: shell strips the inner quotes
mcpal raw ev tools/call --params {"name":"echo"}
# right
mcpal raw ev tools/call --params '{"name":"echo","arguments":{"message":"hi"}}'
Use mcpal tool template <ref> <name> for a known-good skeleton, or
--cli-input-json @file.json.
Spawned server stderr is hidden
Spawned stdio servers' stderr is redirected to /dev/null. To see it:
MCPAL_CHILD_STDERR=inherit mcpal tool call <ref> …
Keyring failures on Linux
If mcpal debug doctor reports keyring round-trip failed, the session has
no running Secret Service daemon. Install gnome-keyring or kwallet.
In CI or containers, skip the keyring entirely with MCPAL_BEARER=….
Filing a bug
mcpal --version
mcpal --output json doctor
Include the failing command and its -vv trace. The error[E####]
prefix is stable; quote it verbatim.
Server dies on initialize — read its stderr
mcpal tool list <ref> failing with
E0006: connection closed: initialize response means the stdio child
exited before completing the MCP handshake. The error chain now
includes the last lines of the child's stderr — read it.
If the chain is still empty, the child died silently or printed to stdout (a protocol violation). Run it in inherit mode to stream stderr live:
MCPAL_CHILD_STDERR=inherit mcpal tool list <ref>
The TUI always nulls child stderr to keep its alt-screen clean. Use the CLI for diagnosis.
The relevant env var values are:
| Value | Effect |
|---|---|
(unset) / capture | Default. Stderr piped into a 64-line tail; flushed into the error chain on failure. |
inherit | Stream child stderr live to the parent's stderr. Best for diagnosis. |
null | Discard. Used by mcpal tui automatically. |
Registry install completes but the server crashes on first call
If mcpal server install <ref> succeeded silently and then
mcpal tool list <ref> reports E0006: connection closed: initialize response, the registry entry likely declares required environment
variables that weren't set.
mcpal v0.4.1+ prompts for these on a TTY. Re-install:
mcpal server remove <ref>
mcpal server install <ref>
# mcpal lists each declared env var and asks for a value
In CI or other non-TTY environments, pre-supply each variable:
mcpal server install <ref> --env VAR_A=… --env VAR_B=…
mcpal server search <ref> shows the entry's declared variables and
their descriptions. See also E0017.
Protocol compliance matrix
Every MCP method, server-initiated request, transport, and auth mode,
and the mcpal verb (or flag) that drives it. Unmarked rows are wired
in. pending rows are not.
Methods
| Method | mcpal verb | Status |
|---|---|---|
initialize | every command (handshake) | implicit |
ping | server ping <ref> | via initialize handshake |
tools/list | tool list <ref> | |
tools/call | tool call <ref> <name> --key value | |
resources/list | resource list <ref> | |
resources/read | resource read <ref> <uri> | |
resources/templates/list | resource template list <ref> | |
resources/subscribe | resource subscribe <ref> <uri> | |
resources/unsubscribe | resource unsubscribe <ref> <uri> | |
prompts/list | prompt list <ref> | |
prompts/get | prompt get <ref> <name> --key value | |
completion/complete | prompt complete <ref> <name> --arg F=P / resource complete <ref> --template <uri> --arg F=P | |
logging/setLevel | logging set-level <ref> <level> | |
| any other / future method | raw <ref> <method> --params … | passthrough |
Server-initiated requests
| Method | Default handler | Override |
|---|---|---|
roots/list | returns --root <path> paths | --root flag |
elicitation/create (form) | TTY prompt → Accept; non-TTY → Decline | --no-interactive to always decline |
elicitation/create (url) | prints URL → Accept | n/a |
sampling/createMessage | MethodNotFound | --sampling-handler <CMD> |
logging/message | routed via tracing; emitted from mcpal watch | RUST_LOG=… |
notifications/progress | emitted by mcpal watch | n/a |
notifications/resources/updated | emitted by mcpal watch | n/a |
notifications/{tools,prompts,resources}/list_changed | emitted by mcpal watch | n/a |
notifications/cancelled | emitted by mcpal watch | n/a |
Transports
| Transport | Status |
|---|---|
| stdio (child process) | |
| Streamable HTTP (rustls) | |
| SSE (legacy 2024-11-05) | not enabled |
| WebSocket | not enabled |
Auth
| Mode | Status |
|---|---|
| Bearer (inline / env / keyring) | |
| OAuth 2.1 + PKCE + DCR (RFC 7591) | |
| OAuth 2.1 + CIMD (client metadata URL, SEP-991) | pending |
| Custom HTTP headers | via ServerSpec::Http { headers } |
| mTLS | pending |
Spec 2026-07-28 release candidate
The next protocol revision changes enough on the wire that adopting
it is a planned upgrade, not a free ride. mcpal tracks rmcp's
support for it; the column below records where each delta lands on
the mcpal side.
| Change (SEP) | Where it lands in mcpal |
|---|---|
Handshake removed: no more initialize / initialized (SEP-2575) | client.rs connect path; rmcp upgrade required |
Session header Mcp-Session-Id deprecated (SEP-2567) | HTTP transport; rmcp upgrade required |
Required routing headers Mcp-Method, Mcp-Name, MCP-Protocol-Version (SEP-2243) | HTTP transport; rmcp upgrade required |
Client info, version, capabilities move to per-request _meta | client.rs; rmcp upgrade required |
W3C Trace Context in _meta (traceparent, tracestate, baggage, SEP-414) | follow-up after rmcp upgrade |
server/discover method | new verb under mcpal server …, post-upgrade |
ttlMs / cacheScope on list and resource read results (SEP-2549) | enables local catalogue cache (see Why a CLI for MCP) |
Full JSON Schema 2020-12 in inputSchema (SEP-2106) | jsonschema crate already on 2020-12; verify --skip-validation path |
outputSchema unrestricted; structuredContent any JSON | output rendering; small change in output.rs |
Resource-missing error: -32002 → -32602 (SEP-2164) | exit.rs classifier table |
OAuth iss validation per RFC 9207 (SEP-2468) | rmcp AuthorizationManager; verify on upgrade |
OIDC application_type in DCR (SEP-837) | rmcp DCR call; verify on upgrade |
| Credentials bound to issuer (SEP-2352) | keyring key naming under review |
| Roots / Sampling / Logging deprecated (SEP-2577) | matrix above will mark deprecated when 2026-07-28 lands |
Tasks extension (tasks/get, tasks/update, tasks/cancel) | new verb group, scoped after rmcp upgrade |
Extensions framework (reverse-DNS IDs, extensions map) | raw already covers ad-hoc calls |
Error codes
Every error has a stable E#### code. At the CLI you see a rustc-style
block; this page is the long form. mcpal debug explain E#### prints the
same text.
E0000 — generic
mcpal couldn't classify the failure. The displayed text is whatever the
underlying library reported. If you can reproduce it, open an issue
with the command, the full message, and the -v trace output.
Exit code 1.
E0001 — server reference not found
mcpal didn't recognise the <ref>. References resolve in this order:
- Alias registered via
mcpal server add. - An
http://orhttps://URL. - Path to a JSON file with one
ServerSpec. <source>:<name>from discovery (e.g.cursor:linear).- A bare
<name>if unambiguous across discovered sources.
Fixes:
mcpal server discover— list everything installed clients already configured.mcpal server list --all— owned + discovered.mcpal server add <alias> --stdio <command>— register a stdio server.mcpal server add <alias> --http <url>— register an HTTP server.
Exit code 3.
E0002 — usage / invalid arguments
mcpal couldn't parse the arguments. Most commonly a malformed
--key value pair or an unknown flag.
Fixes:
- Pass
--key valuepairs:mcpal tool call ev echo --message hi. - For nested JSON, use
--cli-input-json @args.json(or-for stdin). mcpal tool template <ref> <name>prints a valid skeleton.mcpal <subcommand> --helpfor the full grammar.
Exit code 2.
E0003 — auth required
The server (or the tool, resource, or prompt) needs credentials and none are configured.
Fixes:
- Bearer:
mcpal auth login <ref> --bearer <TOKEN>. - OAuth:
mcpal auth login <ref> --oauth. - One-shot env:
MCPAL_BEARER=… mcpal tool list <ref>.
Tokens persist in the OS keyring, not the TOML config.
Exit code 4.
E0004 — auth expired
The server rejected the credentials mcpal sent. The access token has likely expired.
Fixes:
mcpal auth refresh <ref>to mint a new access token.mcpal auth login <ref> --oauthfor a full re-authorize when refresh fails.mcpal auth status <ref>to see what's stored.
Exit code 5.
E0005 — transport error
mcpal couldn't talk to the server. For stdio, the spawned process may have failed to start; for HTTP, the URL is wrong or unreachable.
Fixes:
- Verify the URL with
curl -I <url>(HEAD should return 200/4xx, not a network error). - For stdio: confirm the command is on
$PATHand runs standalone. - Re-run with
-v(or-vv) to see the underlying request. - Reproduce with
mcpal server ping <ref>.
Exit code 6.
E0006 — server returned a JSON-RPC error
A well-formed JSON-RPC error from the server. Common causes:
- The tool, resource, or prompt name is wrong.
- The arguments don't match
inputSchema. - A server-side runtime failure.
Fixes:
mcpal tool describe <ref> <name>— confirm the input schema.mcpal tool template <ref> <name>— get a valid skeleton.- Re-run with
-vfor the raw JSON-RPC frame.
Exit code 7.
E0007 — request timed out
The server didn't respond within the deadline. For stdio servers, the
common cause is npx -y @some-pkg doing a fresh install (~30s on a
cold cache).
Fixes:
- Retry; subsequent runs hit the npx cache.
- Check the server isn't waiting on stdin (some stdio servers prompt for config on first launch).
Exit code 8.
E0008 — not yet supported
mcpal recognised the request but it isn't wired up yet.
Fixes:
- Check
mcpal --versionand update if a newer release is out. - For advanced flows,
mcpal raw <ref> <method> --params …sends arbitrary JSON-RPC directly.
Exit code 6.
E0009 — bad JMESPath query
--query couldn't compile or returned an error. Common causes:
- Unbalanced brackets or quotes (
tools[0instead oftools[0]). - Trying to flatten a non-array (
foo[]wherefoois an object). - A function call against a missing field.
Fixes:
- Run the same command without
--queryto inspect the actual shape. - Cheat sheet:
field,field.subfield,arr[],arr[0],arr[].field,arr[?field == 'x'].name. - Tutorial: https://jmespath.org/tutorial.html.
Exit code 2.
E0010 — JSON payload didn't parse
mcpal expected a JSON document and got something else. This happens
with mcpal raw --params <inline|@file|-> and
mcpal tool call --cli-input-json <file|->.
Fixes:
- Quote inline JSON for your shell:
mcpal raw ev tools/call --params '{"name":"echo","arguments":{"message":"hi"}}' - For files:
@/absolute/or/relative/path.json(note the@). mcpal tool template <ref> <name>for a known-good skeleton.
Exit code 2.
E0012 — schema validation failed
mcpal validates the arguments you pass to tool call against the
tool's inputSchema before sending the request. A schema check turned
up one or more violations (missing required field, wrong type, value
outside the allowed enum, unknown property when the schema is strict).
Fixes:
mcpal tool describe <ref> <name>shows the full schema.mcpal tool template <ref> <name>prints a known-good skeleton.- Pass
--skip-validationto dispatch the call without checking (useful if the server's schema is buggy or stricter than reality).
Exit code 2.
E0011 — interrupted by Ctrl-C
You pressed Ctrl-C while mcpal was waiting on a response from the server. mcpal drops the in-flight request and exits with code 130 (the conventional code for SIGINT-terminated programs).
The server may still complete the operation on its end — mcpal just
stops waiting. There is no MCP method today to tell the server "never
mind" once the request is in flight. For a hard deadline instead, pass
--timeout <SECS>.
Exit code 130.
E0013 — server already exists
mcpal server add <name> failed because <name> is already in the
config. Pick a different name, run mcpal server remove <name> first,
or re-run with --force to overwrite. mcpal server list shows the
current entries.
E0014 — template variable not set
mcpal run couldn't resolve a {{profile.X}} or {{env.X}} placeholder.
The error lists the unset variables. Fix by adding the key to the active profile
(profiles.<name>.<key>:) in mcpal.yml, exporting the env var, or passing
--params-override KEY=VAL to bypass.
E0015 — collection not found
mcpal run walked from the current directory up to the filesystem root looking
for mcpal.yml and didn't find one. Drop a mcpal.yml at your project root or
pass --collection PATH to point at an explicit file.
E0016 — profile not in collection
The active profile (--profile NAME, MCPAL_PROFILE, or default-profile:)
isn't declared in the collection's profiles: block. Add it, pick a different
profile, or remove the default-profile: key.
E0017 — registry server requires env vars
mcpal server install found the registry entry declares environment
variables but none were supplied. Either re-run on a TTY (mcpal will
prompt with the registry's description per variable), or pre-supply
with --env VAR=value (repeatable). mcpal server search <ref> shows
the entry's declared variables.
Test corpus
A curated list of MCP servers to sanity-check mcpal against on every release. Each row stresses a different edge of the protocol or the mcpal surface.
stdio + required env
io.github.codeurali/dataverse — Microsoft Dataverse
mcpal server install io.github.codeurali/dataverse
# Prompts for DATAVERSE_ENV_URL on TTY; bails with E0017 otherwise.
mcpal tool list dataverse
Stresses: env-var prompt path (v0.4.1); registry semver-max (v0.4.0).
awslabs.aws-api-mcp-server — AWS API via uvx
mcpal server add aws-api \
--env AWS_PROFILE=default --env AWS_REGION=us-east-1 \
-- uvx awslabs.aws-api-mcp-server@latest
mcpal tool list aws-api
Stresses: long cold start (~30s); uvx; --env propagation.
@modelcontextprotocol/server-postgres
mcpal server add pg \
--env DATABASE_URL=postgres://localhost/test \
-- npx -y @modelcontextprotocol/server-postgres
Stresses: DATABASE_URL; SQL injection in tool args; resource subscriptions.
Broken on init
mcp-dataverse@0.1.0
mcpal server add bd --force -- npx -y mcp-dataverse@0.1.0
mcpal tool list bd
# error[E0006]: ... (child stderr: ENOENT package.json)
Stresses: child stderr surfacing (v0.4.0). Without the v0.4.0 fix the failure is opaque.
HTTP + OAuth (PKCE + DCR)
Notion
mcpal server add notion --http https://mcp.notion.com/v1 --oauth
mcpal tool list notion
mcpal auth refresh notion
Stresses: browser handshake; refresh-token storage; loopback listener.
HTTP + static bearer
GitHub Copilot MCP
mcpal server add gh \
--http https://api.githubcopilot.com/mcp/ \
--bearer "$GH_TOKEN"
mcpal tool list gh
Stresses: --bearer keyring write; promote-from-import.
Pagination + notifications + resources
@modelcontextprotocol/server-everything
mcpal server add ev -- npx -y @modelcontextprotocol/server-everything
mcpal tool list ev | wc -l # 100+ tools
mcpal watch ev # streams progress + log + list-changed
mcpal resource subscribe ev demo://resource/dynamic/0
mcpal tool call ev sample --message hi # exercises sampling
mcpal tool call ev eliciting --message x # exercises elicitation
Stresses: pagination; notification stream; resource subscribe; sampling / elicitation handlers.
mcp-ui / OpenAI Apps payloads
(Pending: a stable demo server. For now use the unit tests in
crates/mcpal/src/commands/ui.rs and add fixture servers when they
become available.)
Multi-source same-name
chrome-devtools is typically registered in both opencode and
claude-code configs. Verify:
mcpal server discover --source opencode | grep chrome-devtools
mcpal server discover --source claude-code | grep chrome-devtools
mcpal tool list opencode:chrome-devtools
mcpal tool list claude-code:chrome-devtools
mcpal tool list chrome-devtools # ambiguous — fails with hint
Stresses: bare-name disambiguation.
fastmcp banner
(Pending: a local FastMCP demo. Stresses: controlling-terminal detach via setsid; TUI alt-screen integrity.)
Known gaps (currently UNTESTED)
- HTTP servers behind a private CA / self-signed cert.
- Windows Store install of Claude Desktop (
%LOCALAPPDATA%\Packages\...). - Servers that emit JSON on stdout outside the MCP framing (protocol violation; mcpal's behaviour is undefined).
Every release ritual runs at least the stdio + HTTP + everything-server smoke before tagging.
Changelog
All notable changes documented here. Format: Keep a Changelog. Versioning: SemVer.
[Unreleased]
[0.4.2]
Added
cmd:<command> [args]ephemeral stdio<ref>— call any local MCP server in one shell line, noserver add.mcpal tool list "cmd:npx -y @mcp/server-everything".--auth MODEglobal flag pairs with an inlinehttps://<ref>to pick auth on the fly:oauth(default),none/anon,env:VAR,bearer:TOKEN.book/src/one-liners.md— every one-line<ref>shape in one table, with auth modes and the limits of each.book/src/why-cli.md— Explanation chapter on when a shell client earns its place next to MCP-aware chat apps.book/src/protocol-matrix.mdcarries a roadmap table for the 2026-07-28 RC.
Changed
- Resolver order documented and stable: owned alias →
cmd:→ URL → JSON path →<source>:<name>→ bare name. E0001 message lists the new precedence. mcpal-core::handler::run_sampling_handlerreturnsanyhow::Resultinstead of a hand-rolledResult<_, String>.Config::loaddrops thePath::existspre-check and matchesErrorKind::NotFounddirectly; one less stat() per startup.--queryno longer JSON-string-roundtrips its jmespath result.- Internal registry DTOs (
Envelope,Server,Package,EnvVar, …) move topub(crate).ServerWrapperrenamed toServerEntry.EnvVarHintcollapsed into(String, Option<String>). - Unit coverage doubled: 60 → 130 tests (oauth math, JMESPath, resolver order, exit classifier, runtime deadline, TUI focus, sidebar filter, diff edges, discover descriptors).
Fixed
oauth::access_token_refreshingmade one redundant keyring read per call; collapsed to a single load on the hot path.
[0.4.1]
Added
mcpal server installprompts for declared environment variables on a TTY using each variable's registry-provided description as the hint. Non-TTY (or--no-prompt) bails with the newE0017error.- TUI: connecting to a server whose stored spec carries empty env values pops a
Configure '<server>'modal — fill in, save (writes toconfig.toml), connect. book/src/test-corpus.md— curated list of tricky MCP servers exercised on every release.
Changed
- Registry-declared
environmentVariablesdefault to required unless they carry adefaultvalue or explicitly setisRequired: false. Matches the official registry's actual schema. registry::to_specnow returns(ServerSpec, RequiredEnvHint)so callers can prompt instead of bailing.
Fixed
mcpal server install io.github.codeurali/dataversesilently produced a spec withoutDATAVERSE_ENV_URL, then the server crashed oninitialize. The prompt path or--env DATAVERSE_ENV_URL=…resolves this end-to-end.
[0.4.0]
Added
- Discovery sources for VS Code (workspace
.vscode/mcp.json+ usersettings.jsonchat.mcp.servers+ Continue extension storage) and Codex CLI (~/.codex/config.toml). --discover-from PATHglobal flag for ad-hocmcp.jsonfiles (repeatable).- Book
Discoverychapter listing every supported client; troubleshooting section for stdio servers that die oninitialize.
Changed
mcpal server installpicks the highest semver-compatible version from the registry instead of the first match.- Stdio child stderr is captured by default into a 64-line ring buffer; on connect failure the tail is attached to the error chain.
MCPAL_CHILD_STDERR=null|inherit|capturecontrols the mode; the TUI pinsnullto keep its alt-screen clean.
Fixed
mcpal server install io.github.<owner>/<name>silently picked the lowest version when multiple existed.mcpal tool list <stdio-ref>failing withconnection closed: initialize responsenow includes the child's stderr instead of empty context.
[0.3.1]
Changed
mcpal server listnow shows owned + discovered entries by default.--ownednarrows to mcpal-registered;--discoverednarrows to discovery-imported.--allis kept (hidden) for back-compat with scripts.
Fixed
- TUI no longer silently swallows
tools/list,resources/list, orprompts/listfailures. Errors surface to the output pane (<ref>: tools/list failed: …) so an empty tab has a visible explanation.
[0.3.0]
Added
mcpal.ymlcollection file. Defineprofiles:+calls:, thenmcpal run NAME --profile prodto invoke a saved tool call. Source-first: commit the file, share with teammates.mcpal runverb with--dry-run(resolve + print without opening a connection) and--params-override K=V(overlay raw values after templating).{{profile.X}}+{{env.X}}substitution insideparams;{{{{escapes a literal{{. Unresolved variables collected and reported in one error.E0014(template variable not set),E0015(collection not found),E0016(profile not in collection) error codes.- Book chapter
Collections; README Quickstart subsection for saved calls. - Windows install note in the book — DPAPI keyring; MSI / winget roadmap.
[0.2.0]
Added
mcpal server addone-liner:--bearer / --bearer-env / --oauth / --header / --force / --no-loginaccepted alongside the transport flags. Writes spec + materialises the credential (keyring for literal bearers,bearer_envin the spec for env refs, inline browser flow for OAuth) in one command.E0013 server already existserror code;--forceoverrides.- Interactive TUI (
mcpal tui) — split-pane browser for servers, tools, resources, prompts; live notification stream; bearer + OAuth + tool-call composer. .debpackages for Debian / Ubuntu attached to every release.mcpal ui inspect— classifies mcp-ui (ui://) and OpenAI Apps (application/vnd.openai.app+json) payloads in tool results.- Trace events for elicitation + sampling in the notification stream.
--helpExamples blocks forserver add,tool call,auth login,raw.- Book chapters: Install, TUI, UI-rich MCP servers, Changelog.
Changed
- README + book quickstarts collapsed:
server add+auth login→ single command. - README hero reworked semble-style; tagline + badges + nav pills.
- Book sidebar reordered — Concepts moved ahead of How-to guides.
- Dropped "AWS-CLI" framing from doc strings + book prose;
--queryis documented as a JMESPath filter. - Server import promotes
Authorization: Bearer …headers to keyring orbearer_envautomatically.
Fixed
- TUI rendering corruption against servers that bleed installer progress to the controlling terminal (uv / fastmcp). stdio children launch via
setsidand have stderr nulled. - Control bytes in server-supplied strings sanitised before render.
- Esc inside the TUI preserves detail context;
h/ Left navigates to the previous tab.
[0.1.1]
Fixed
- Homebrew tap formula naming. Renamed crate
mcpal-cli→mcpalso cargo-dist publishesFormula/mcpal.rbandbrew install pawelb0/tap/mcpalworks.
[0.1.0]
Added
- Initial release. CLI client for the Model Context Protocol: stdio + Streamable HTTP transports; OAuth 2.1 (PKCE + DCR); discovery from Claude Desktop / Cursor / opencode
mcp.json; tool, resource, prompt commands; JSON-RPCrawescape hatch;watchfor notifications; JMESPath--query; OS-keyring credentials.