Metadata-Version: 2.5
Name: mcp-servers-cli
Version: 0.1.0
Summary: Inspect, call and drive any MCP server from the command line
Project-URL: Homepage, https://github.com/zerotropism/mcp-servers-cli
Project-URL: Issues, https://github.com/zerotropism/mcp-servers-cli/issues
License-Expression: MIT
License-File: LICENSE
Keywords: agent,cli,llm,mcp,model-context-protocol
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.12
Requires-Dist: cyclopts>=4
Requires-Dist: fastmcp<5,>=4
Requires-Dist: ollama>=0.6.2
Requires-Dist: rich>=14
Provides-Extra: anthropic
Requires-Dist: anthropic>=1.8.0; extra == 'anthropic'
Description-Content-Type: text/markdown

# mcp-servers-cli

Inspect, call and drive any MCP server from the command line. Point it at a local process, a
remote HTTP endpoint, or an entry in a `claude_desktop_config.json`-style file, and it lists the
tools, resources and prompts the server exposes — then lets you exercise them.

## Installation

Run it without installing anything, with [uv](https://docs.astral.sh/uv/):

```bash
uvx mcp-servers-cli inspect --stdio "uvx mcp-server-fetch"
```

Or install it once, with the Anthropic backend if you need it:

```bash
uv tool install mcp-servers-cli
uv tool install 'mcp-servers-cli[anthropic]'
```

From a clone, for development:

```bash
uv sync --all-groups
uv run mcp-servers-cli --help
```

## Commands

Every command takes exactly one target: `--stdio`, `--http` or `--config` with `--server`.

```bash
# What does this server expose?
mcp-servers-cli inspect --stdio "uv run server.py"
mcp-servers-cli inspect --http https://example.com/mcp
mcp-servers-cli inspect --config config.json --server fetch

# Call one tool
mcp-servers-cli call fetch '{"url": "https://example.com"}' --stdio "uvx mcp-server-fetch"

# Read one resource
mcp-servers-cli read "tasks://stats" --stdio "uv run server.py"

# Inspect, then stay interactive
mcp-servers-cli repl --stdio "uv run server.py"

# Let a model use the tools to answer
mcp-servers-cli agent "What is 2 + 3?" --model qwen3.5:4b --stdio "uv run server.py"
```

Inside the REPL: `call <tool> <json>`, `read <uri>`, `list`, `quit`.

## Agent

`agent` hands the server's tools to a model and lets it call them until it answers in text. Each
call is printed as it happens, then the answer:

```
-> add {"a": 2, "b": 3}
<- add ok (0.1s)
The sum is 5.
```

- `--model` is required: small local models can mishandle nested arguments, and a silent default
  would hide that behind a plausible failure.
- `--backend ollama` (default) talks to the server named by `OLLAMA_HOST`, localhost otherwise.
- `--backend anthropic` needs the extra, `uvx --from 'mcp-servers-cli[anthropic]' mcp-servers-cli`,
  and reads `ANTHROPIC_API_KEY` from the environment.
- A failing tool, an unknown tool name or an invented argument does not stop the run: the model
  reads the error or gets the cleaned call, and can correct itself.
- `--max-steps` (default 10) bounds the model turns; running out is an error, not a silent stop.
- `--dry-run` asks the model once and prints the calls it would make, running none: a safe first
  look at a server whose tools write or delete.
- `--trace run.jsonl` writes one JSON object per executed call (time, tool, arguments, duration,
  error flag, a 500-character excerpt of the result) and a closing `end` line with the model
  turns, the number of calls and of failed calls, and the total duration.
- When a tool call failed, a warning follows the answer on stderr: a model can answer as if its
  calls had worked (see below).

A trace reads with any JSON tool, for instance the slowest calls first:

```bash
jq -r 'select(.event == "tool") | "\(.duration_ms) ms  \(.tool)"' run.jsonl | sort -rn
```

Arguments and results are written as they are: keep traces out of version control when a server
handles secrets.

## Model requirements

Measured on 24 September 2026 against `mcpserver-template` (in-memory backend), three runs per
model, with one prompt: add three tasks, complete one, list the pending ones. It takes a string
argument, an integer read from an earlier result, and a nested object (`filter_tasks`).

| Model            | Correct answers | Tool calls | Failed calls | Model turns |
|------------------|-----------------|------------|--------------|-------------|
| `qwen3.5:4b-mlx` | 3 / 3           | 5          | 0            | 4           |
| `llama3.2:3b`    | 0 / 3           | 1 to 3     | 1 to 2       | 2           |

`llama3.2:3b` failed every run. Its first planned call was `filter_tasks` with invented fields,
before any task existed. In the run examined in detail, that filter was rejected and
`complete_task` targeted a task that did not exist; yet each of the three answers described the
work as done. That is the failure to watch for with small models: not a crash, but a fluent and
false answer. Read the `<-` lines, or the warning printed after the answer, before trusting it.

`qwen3.5:4b-mlx` sent the three `add_task` calls in one turn. Twice it filtered on the server
(`{"status": "pending"}`); once it fetched every task and filtered the result itself. Both gave
the right answer.

Unknown top-level arguments are dropped before a call; an invented field inside a nested object
is left to fail. Dropping `title` from a mistaken filter would widen it to every task and return
a wrong answer that looks right.

## Client capabilities

MCP lets a server ask its client for three things during a call: a completion from the client's
model (sampling), an answer from the user (elicitation), and the directories it may work in
(roots). `mcp-servers-cli` declares none of them. A server that asks gets a one-line refusal,
`Sampling not supported`, `Elicitation not supported` or `List roots not supported`: `call`
prints it as an error, `agent` hands it to the model as a failed call.

FastMCP 4 negotiates the 2026-07-28 protocol revision by default. On such a connection a server
no longer sends these requests itself: its tool returns an input-required result (SEP-2322)
listing what it needs, and the client answers before the tool runs again. The refusals above
are what such a server receives.

Handlers will come with the first server of this portfolio that needs one: sampling bridged to
the `agent` backend, roots from the command line. Elicitation needs someone at the keyboard,
which `agent` does not assume.

## Configuring the server you launch

A stdio server runs as a subprocess, and the MCP SDK forwards only a whitelist of environment
variables to it — `HOME`, `LOGNAME`, `PATH`, `SHELL`, `TERM`, `USER`. Anything else the server
reads from its environment is silently absent, and it falls back to its defaults.

`--env` is how you pass the rest:

```bash
mcp-servers-cli call add_task '{"title": "buy milk"}' \
  --env TASK_BACKEND=sqlite --env DB_PATH=tasks.db \
  --stdio "uv run --directory ../mcpserver-template mcpserver-template"
```

`~` and `$VARS` are expanded in the command and in every argument, including entries read from
a config file.

For a remote server, `MCP_TOKEN` is sent as a bearer token when set.

## Noisy servers

A stdio server writes its own logs to stderr, and they land in your terminal. Servers built on
older MCP SDKs answer FastMCP's capability probe with a wall of validation errors before falling
back to the legacy protocol — the inspection still succeeds, but the output is buried.

`--quiet` discards that stream. It is not the default on purpose: when a server fails to start,
the reason is in its first stderr line, and hiding it turns a clear error into a bare
"Connection closed".

## Errors

A failure prints one line naming its cause and exits with status 1:

```
error: Client failed to connect: [Errno 2] No such file or directory: 'uvx'
```

When a stdio server dies while starting, its own stderr line comes first and the error line points
to it. Set `MCP_SERVERS_CLI_DEBUG=1` to get the full traceback instead.

## Configuration file

The `--config` mode reads the `mcpServers` format used by Claude Desktop:

```json
{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "${HOME}/Developer"]
    },
    "fetch": { "command": "uvx", "args": ["mcp-server-fetch"] },
    "remote": { "url": "https://example.com/mcp" }
  }
}
```

## Project structure

```
src/mcp_servers_cli/
├── transports.py   one builder per transport, plus path expansion
├── inspection.py   reads a server into dataclasses
├── rendering.py    turns inspection data, results and agent progress into output
├── repl.py         interactive loop over a connected client
├── errors.py       turns a failure into one line
├── llm.py          provider-neutral conversation model and the LLMBackend protocol
├── backends/       one module per provider, translating to and from that model
├── agent.py        the tool loop: model turns and tool calls, printing nothing
├── trace.py        JSON Lines record of an agent run
└── cli.py          cyclopts commands
```

Inspection returns data and never prints; rendering never talks to a server. That is what lets
the tests run an in-memory FastMCP server and assert on structures rather than on captured
stdout.

Adding a transport means adding a builder in `transports.py` and a target option in `cli.py`,
without touching the existing ones.

Only `backends/` imports an LLM SDK; everything else works on the neutral types of `llm.py`.
Adding a provider means adding one module there and one line in `backends/__init__.py`.

## Tests

```bash
uv run pytest
```

No network and no LLM. The inspection and agent-loop tests run against an in-memory FastMCP
server with a scripted model; the transport tests check expansion rules; the rendering tests
capture a rich console; the backend tests translate real SDK objects through a stand-in client;
the trace tests write to a temporary directory.
Two kinds of test start a process: the error tests launch a command that does not exist, and the
agent command is tested end to end against `tests/fixtures/add_server.py` over stdio.

## Dependencies

| Package    | Role                                  |
|------------|---------------------------------------|
| `fastmcp`  | MCP client and transports             |
| `cyclopts` | Commands and help, from type hints    |
| `rich`     | Tables and JSON highlighting          |
| `ollama`   | Local models, the default backend     |
| `anthropic` | Anthropic backend, optional: `mcp-servers-cli[anthropic]` |

`cyclopts` and `rich` already ship in FastMCP's dependency tree; they are declared explicitly
rather than relied on transitively.
