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? InstallYour first MCP callConcepts.

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, and watch against 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? Run mcpal tool list cursor:linear. No copy-paste of mcp.json.
  • Auditing what's installed. mcpal server discover reports 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.

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.
  • Troubleshootmcpal debug doctor, common failures.

Reference — look up specifics.

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

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:

  1. Alias from mcpal server add.
  2. An http(s):// URL (anonymous HTTP server).
  3. Path to a JSON file with one ServerSpec.
  4. <source>:<name> — a discovered server (cursor:linear).
  5. 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:

ClientmacOSLinuxWindows
Claude Code~/.claude.jsonsamesame
Claude Desktop~/Library/Application Support/Claude/~/.config/Claude/%APPDATA%/Claude/
Cursor~/.cursor/mcp.jsonsamesame
Zed~/.config/zed/settings.jsonsamesame
opencode~/.config/opencode/opencode.jsonsamesame

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

ModeStorageCommand
Inline bearerOS keyringmcpal auth login <ref> --bearer <TOKEN>
BearerEnvenvironment variableTOML: auth = { type = "bearer_env", env = "MY_TOKEN" }
OAuth 2.1OS keyring (stored credentials)mcpal auth login <ref> --oauth
One-shotenvironmentMCPAL_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

SourceFiles
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 call spawns the server fresh over stdio. Initialization (the initialize handshake, the tools/list exchange a server may do on connect) runs every call. A long-running mcpal serve daemon that holds warm sessions is on the roadmap.
  • The tool catalogue is not cached on disk. tool list round-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 haveOne-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) URLthe URL plus --auth MODEmcpal --auth env:GH_TOKEN tool list https://api.githubcopilot.com/mcp/
A ServerSpec JSON file on diskthe pathmcpal 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 sourcesthe namemcpal tool list linear
A registry serverinstall first, then callmcpal 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:

--authBehaviour
oauth (default)Send the stored OAuth access token
none / anonSend no Authorization header
env:VARRead the bearer token from $VAR at call time
bearer:TOKENSend 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 with server add.
  • Registry servers that declare required env vars and you didn't pass --envmcpal server install exits with E0017.

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):

  1. --profile NAME on the command line
  2. MCPAL_PROFILE env var
  3. default-profile: key in mcpal.yml
  4. 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:

ModeWhere the secret livesBest for
Inline bearerOS keyring under bearer:<ref>personal access tokens, CI service tokens
BearerEnvenvironment variable named in config.tomlsecrets injected by another tool (sops, vault, GitHub Actions)
OAuth 2.1 + PKCE + DCROS keyring under oauth:<ref>user-facing services that authenticate humans (Notion, Linear, Atlassian)
MCPAL_BEARERenvironment variableone-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:

  1. AuthSpec::Bearer { token } (rare; avoid writing tokens to TOML).
  2. AuthSpec::BearerEnv { env }.
  3. OAuth blob in the keyring under oauth:<ref>.
  4. Bearer keyring entry under bearer:<ref>.
  5. MCPAL_BEARER env 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 a bearer:<ref> keyring entry?
  • oauth_stored — is there an oauth:<ref> keyring entry?
  • oauth_access_token_present — does the blob contain an access_token field? (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.

KeyContents
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 $VARBearerEnv.
  • Server authenticates end users (Notion, Linear, etc.) → OAuth.
  • One-shot in a script that shouldn't touch the keyringMCPAL_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

KeyAction
Tab / Shift+Tabcycle pane focus
j/k, /move selection
gg / Gjump to top / bottom
Enterdrill in (sidebar → detail tabs → schema / form)
Escdrill back, or close a modal
/filter the sidebar; Esc clears, Enter accepts
ccall the selected tool (Detail focus, Tools tab)
l / Rightnext tab in Detail (Tools → Resources → Prompts)
bopen a bearer-token input for the selected server
?toggle help overlay
q, Ctrl-Cquit

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:

  • progress becomes → progress N/M.
  • log becomes → 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> --oauth in 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:

kindMeaning
textPlain text/text content block.
image / audioBase64 content with a media MIME.
mcp_uiEmbedded resource whose URI starts with ui://.
openai_appEmbedded resource with vnd.openai.app+json MIME.
resourceEmbedded resource that isn't UI (data attachment).
resource_linkPointer 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 --open proves 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 --save lets 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 --save writes the payload to a regular file you can feed to whatever scanner or linter you'd run on third-party code: grep for script 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 inspect reports ui_resources: 0, the server isn't sending UI — talk to the server team. If it reports ui_resources: 1 and your client still shows nothing, the bug lives in the client.

  • Capturing fixtures. Once --save produces 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 postMessage round-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 --strict validator is on the list.

See also

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 json and --query <jmespath> remove most uses of jq.

Exit codes

CodeMeaningError code(s)Common fix
0success
1generic errorE0000check stderr
2usage / invalid argumentsE0002, E0009, E0010mcpal <subcommand> --help
3server reference not foundE0001mcpal server discover
4auth requiredE0003mcpal auth login <ref>
5auth expiredE0004mcpal auth refresh <ref>
6transport / not yet supportedE0005, E0008network unreachable or mcpal raw
7server returned a JSON-RPC errorE0006check args against tool describe
8request timed outE0007retry; with --timeout, raise the value
130interrupted (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 value flags — 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

VarEffect
MCPAL_CONFIGpath to config.toml
MCPAL_PROFILEaccepted but unused (will gate profile selection later)
MCPAL_BEARERone-shot bearer for any HTTP server
MCPAL_SAMPLING_HANDLERshell command for sampling/createMessage
MCPAL_CHILD_STDERR=inheritun-silence the spawned stdio server's stderr
RUST_LOGtracing 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 value throughout.

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 discover lists 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 E0001 for 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 -y on 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 with mcpal tool describe <ref> <name> and rebuild with mcpal 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:

ValueEffect
(unset) / captureDefault. Stderr piped into a 64-line tail; flushed into the error chain on failure.
inheritStream child stderr live to the parent's stderr. Best for diagnosis.
nullDiscard. 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

Methodmcpal verbStatus
initializeevery command (handshake)implicit
pingserver ping <ref>via initialize handshake
tools/listtool list <ref>
tools/calltool call <ref> <name> --key value
resources/listresource list <ref>
resources/readresource read <ref> <uri>
resources/templates/listresource template list <ref>
resources/subscriberesource subscribe <ref> <uri>
resources/unsubscriberesource unsubscribe <ref> <uri>
prompts/listprompt list <ref>
prompts/getprompt get <ref> <name> --key value
completion/completeprompt complete <ref> <name> --arg F=P / resource complete <ref> --template <uri> --arg F=P
logging/setLevellogging set-level <ref> <level>
any other / future methodraw <ref> <method> --params …passthrough

Server-initiated requests

MethodDefault handlerOverride
roots/listreturns --root <path> paths--root flag
elicitation/create (form)TTY prompt → Accept; non-TTY → Decline--no-interactive to always decline
elicitation/create (url)prints URL → Acceptn/a
sampling/createMessageMethodNotFound--sampling-handler <CMD>
logging/messagerouted via tracing; emitted from mcpal watchRUST_LOG=…
notifications/progressemitted by mcpal watchn/a
notifications/resources/updatedemitted by mcpal watchn/a
notifications/{tools,prompts,resources}/list_changedemitted by mcpal watchn/a
notifications/cancelledemitted by mcpal watchn/a

Transports

TransportStatus
stdio (child process)
Streamable HTTP (rustls)
SSE (legacy 2024-11-05)not enabled
WebSocketnot enabled

Auth

ModeStatus
Bearer (inline / env / keyring)
OAuth 2.1 + PKCE + DCR (RFC 7591)
OAuth 2.1 + CIMD (client metadata URL, SEP-991)pending
Custom HTTP headersvia ServerSpec::Http { headers }
mTLSpending

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 _metaclient.rs; rmcp upgrade required
W3C Trace Context in _meta (traceparent, tracestate, baggage, SEP-414)follow-up after rmcp upgrade
server/discover methodnew 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 JSONoutput 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:

  1. Alias registered via mcpal server add.
  2. An http:// or https:// URL.
  3. Path to a JSON file with one ServerSpec.
  4. <source>:<name> from discovery (e.g. cursor:linear).
  5. 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 value pairs: 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> --help for 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> --oauth for 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 $PATH and 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 -v for 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 --version and 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[0 instead of tools[0]).
  • Trying to flatten a non-array (foo[] where foo is an object).
  • A function call against a missing field.

Fixes:

  • Run the same command without --query to 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-validation to 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, no server add. mcpal tool list "cmd:npx -y @mcp/server-everything".
  • --auth MODE global flag pairs with an inline https:// <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.md carries 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_handler returns anyhow::Result instead of a hand-rolled Result<_, String>.
  • Config::load drops the Path::exists pre-check and matches ErrorKind::NotFound directly; one less stat() per startup.
  • --query no longer JSON-string-roundtrips its jmespath result.
  • Internal registry DTOs (Envelope, Server, Package, EnvVar, …) move to pub(crate). ServerWrapper renamed to ServerEntry. EnvVarHint collapsed 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_refreshing made one redundant keyring read per call; collapsed to a single load on the hot path.

[0.4.1]

Added

  • mcpal server install prompts 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 new E0017 error.
  • TUI: connecting to a server whose stored spec carries empty env values pops a Configure '<server>' modal — fill in, save (writes to config.toml), connect.
  • book/src/test-corpus.md — curated list of tricky MCP servers exercised on every release.

Changed

  • Registry-declared environmentVariables default to required unless they carry a default value or explicitly set isRequired: false. Matches the official registry's actual schema.
  • registry::to_spec now returns (ServerSpec, RequiredEnvHint) so callers can prompt instead of bailing.

Fixed

  • mcpal server install io.github.codeurali/dataverse silently produced a spec without DATAVERSE_ENV_URL, then the server crashed on initialize. 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 + user settings.json chat.mcp.servers + Continue extension storage) and Codex CLI (~/.codex/config.toml).
  • --discover-from PATH global flag for ad-hoc mcp.json files (repeatable).
  • Book Discovery chapter listing every supported client; troubleshooting section for stdio servers that die on initialize.

Changed

  • mcpal server install picks 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|capture controls the mode; the TUI pins null to 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 with connection closed: initialize response now includes the child's stderr instead of empty context.

[0.3.1]

Changed

  • mcpal server list now shows owned + discovered entries by default. --owned narrows to mcpal-registered; --discovered narrows to discovery-imported. --all is kept (hidden) for back-compat with scripts.

Fixed

  • TUI no longer silently swallows tools/list, resources/list, or prompts/list failures. Errors surface to the output pane (<ref>: tools/list failed: …) so an empty tab has a visible explanation.

[0.3.0]

Added

  • mcpal.yml collection file. Define profiles: + calls:, then mcpal run NAME --profile prod to invoke a saved tool call. Source-first: commit the file, share with teammates.
  • mcpal run verb with --dry-run (resolve + print without opening a connection) and --params-override K=V (overlay raw values after templating).
  • {{profile.X}} + {{env.X}} substitution inside params; {{{{ 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 add one-liner: --bearer / --bearer-env / --oauth / --header / --force / --no-login accepted alongside the transport flags. Writes spec + materialises the credential (keyring for literal bearers, bearer_env in the spec for env refs, inline browser flow for OAuth) in one command.
  • E0013 server already exists error code; --force overrides.
  • Interactive TUI (mcpal tui) — split-pane browser for servers, tools, resources, prompts; live notification stream; bearer + OAuth + tool-call composer.
  • .deb packages 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.
  • --help Examples blocks for server 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; --query is documented as a JMESPath filter.
  • Server import promotes Authorization: Bearer … headers to keyring or bearer_env automatically.

Fixed

  • TUI rendering corruption against servers that bleed installer progress to the controlling terminal (uv / fastmcp). stdio children launch via setsid and 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-climcpal so cargo-dist publishes Formula/mcpal.rb and brew install pawelb0/tap/mcpal works.

[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-RPC raw escape hatch; watch for notifications; JMESPath --query; OS-keyring credentials.