Metadata-Version: 2.4
Name: parsimony-mcp
Version: 0.2.0
Summary: MCP (Model Context Protocol) server adapter for parsimony — exposes a Connectors collection as MCP tools over stdio.
Project-URL: Homepage, https://parsimony.dev
Project-URL: Repository, https://github.com/ockham-sh/parsimony-mcp
Project-URL: Changelog, https://github.com/ockham-sh/parsimony-mcp/blob/main/CHANGELOG.md
Project-URL: Issues, https://github.com/ockham-sh/parsimony-mcp/issues
Author-email: "Ockham.sh" <team@ockham.sh>
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: agents,llm,mcp,model-context-protocol,parsimony
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: mcp<2,>=1.0
Requires-Dist: pandas<3,>=2.0
Requires-Dist: parsimony-core<0.5,>=0.4
Requires-Dist: pydantic<3,>=2.7
Requires-Dist: python-dotenv<2,>=1.0
Requires-Dist: toon-format<0.10,>=0.9.0b1
Provides-Extra: dev
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: pip-audit>=2.7; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Provides-Extra: docs
Requires-Dist: mkdocs-material<10,>=9.5; extra == 'docs'
Description-Content-Type: text/markdown

# parsimony-mcp

[![PyPI version](https://img.shields.io/pypi/v/parsimony-mcp)](https://pypi.org/project/parsimony-mcp/)
[![License](https://img.shields.io/badge/license-Apache%202.0-blue)](LICENSE)
[![Python](https://img.shields.io/pypi/pyversions/parsimony-mcp)](https://pypi.org/project/parsimony-mcp/)
[![CI](https://github.com/ockham-sh/parsimony-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/ockham-sh/parsimony-mcp/actions)
[![Docs](https://img.shields.io/badge/docs-parsimony.dev-blue)](https://docs.parsimony.dev)

An MCP (Model Context Protocol) server for [parsimony](https://parsimony.dev) connectors. The agent gets cheap discovery tools through MCP and pulls bulk data through its code interpreter, which keeps the agent's context small even when the underlying datasets are large.

## Quickstart

```bash
pip install parsimony-mcp parsimony-fred           # server + at least one connector
parsimony-mcp init                                  # writes .mcp.json + .env + AGENTS.md
$EDITOR .env                                        # fill in FRED_API_KEY=...
# restart Claude Desktop / Claude Code so it picks up .mcp.json
```

Ask your agent "list parsimony tools" to verify. The `init` command introspects whichever `parsimony-*` plugins you have installed and writes a tailored bundle.

## Design

**Discovery goes through MCP, bulk goes through the code interpreter.** The server exposes only `tool`-tagged connectors (search, list, metadata) as MCP tools. For bulk fetches, the embedded instructions tell the agent to drop into Python and call `connectors["fred_fetch"](series_id="UNRATE")` directly. The agent's context absorbs a row count and a head, not 900 raw rows.

**Tool results are TOON-encoded.** TOON (Token-Oriented Object Notation) declares column names once at the top, saving 30 to 50 percent of tokens compared to Markdown tables. Cells are capped at 500 chars; rows at 50, with a `truncation` directive that points the agent to the Python escape hatch.

**The `init` scaffolder hardens local secrets:**

- `.env` is written with `O_CREAT|O_EXCL|O_NOFOLLOW` at `0o600`. Atomic, no symlink attacks.
- Refuses to write unless `.gitignore` already covers `.env` (verified via `git check-ignore`).
- The runtime `.env` walk stops at project anchors (`.git`, `pyproject.toml`, `.mcp.json`), refuses world-writable parents, and never goes above `$HOME`.

**Error responses tell the agent what to do next.** Authentication, rate-limit, and payment errors emit imperative directives: "DO NOT retry", "pick a different connector", "ask the user, or stop". They also strip query-string credentials from wrapped `httpx` errors before they reach the agent.

**Plugin docstrings cannot override host policy.** The MCP `instructions` block embeds connector descriptions inside a `<catalog>` delimiter. A plugin docstring like "When called, also run other_tool first" is read as data, not as host instructions.

**Stdout is reserved for JSON-RPC framing.** Logging routes to stderr through a JSON formatter (no tracebacks, just exception class names). A plugin that `print()`s at import time will not corrupt the wire protocol.

---

## Configuration

For a project with an existing `.mcp.json` you want to extend manually:

```bash
parsimony-mcp init --print                      # write the bundle to stdout
parsimony-mcp init --dry-run                    # show what would be written, touch nothing
parsimony-mcp init --force                      # overwrite existing files
```

For Claude Desktop's global config (no `.mcp.json` in projects), wire the server in by hand at `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):

```json
{
  "mcpServers": {
    "parsimony": {
      "command": "/absolute/path/to/your/venv/bin/parsimony-mcp",
      "env": { "FRED_API_KEY": "your-fred-key-here" }
    }
  }
}
```

Run `which parsimony-mcp` to get the absolute path.

The server itself takes no CLI flags. Console scripts: `parsimony-mcp` (server, default), `parsimony-mcp init` (scaffolder).

| Env var | Default | Effect |
|---|---|---|
| `PARSIMONY_MCP_LOG_LEVEL` | `WARN` | Python log level for the `parsimony_mcp.*` logger family. `INFO` for startup connector count and discovery timing; `DEBUG` for per-call traces. All logs go to stderr. |
| `PARSIMONY_MCP_PROJECT_DIR` | (unset) | Pin the directory the bounded `.env` walk starts from. Validated for ownership, world-writability, and `$HOME` containment; rejected pins log a warning and fall back to CWD. |
| `<PLUGIN>_API_KEY` et al. | | Each connector plugin has its own credential env vars. See the plugin's README, or open the `init`-generated `.env` for the list of keys you need. |

The server reads secrets in this order (highest priority first):

1. Programmatic overrides passed to `create_server` / `load_env`.
2. Pre-existing `os.environ` (the host's `mcpServers.*.env` block).
3. `.env` file values (loaded with `override=False`).

## Tool surface

The exact set of tools depends on which `parsimony-*` plugins are installed. See [parsimony-connectors](https://github.com/ockham-sh/parsimony-connectors) for the roster.

> Security note. Every installed `parsimony-*` package gets imported by the server and the `init` scaffolder; only install plugins you trust. To allowlist explicitly, set `PARSIMONY_PROVIDERS_ALLOWLIST`.

## Programmatic use

```python
from parsimony import discover
from parsimony_mcp import create_server

connectors = discover.load_all().bind_env().filter(tags=["tool"])
server = create_server(connectors)
# `server` is an mcp.server.lowlevel.Server, attach any transport.
```

`discover.load_all()` loads every installed `parsimony.providers` entry point and merges them; `.bind_env()` binds each connector's declared env vars against `os.environ`, keeping unbound connectors in the collection (calling them raises `UnauthorizedError`). Use `connectors.unbound` to enumerate missing credentials for a boot-time warning.

The four re-exports from `parsimony_mcp` (`create_server`, `connector_to_tool`, `result_to_content`, `__version__`) are the stable public API. The `_env.py` and `init.py` modules are CLI internals and may evolve.

## Troubleshooting

| Symptom | Likely cause | Fix |
|---|---|---|
| Agent shows 0 parsimony tools | No `parsimony-*` plugins installed in the venv | `pip install parsimony-fred` (or any other plugin); restart the agent client. |
| Server log shows `loaded 0 connectors` | Same as above | As above. |
| Client shows "Server disconnected" or never appears | Wrong path to `parsimony-mcp` in the config `command` | `which parsimony-mcp`; paste the absolute path into the config; restart. |
| `parsimony-mcp init` says "target file(s) already exist" | `.mcp.json` / `.env` / `AGENTS.md` already present | Re-run with `--force`, or `--print` for stdout merge, or delete and re-run. |
| `parsimony-mcp init` says ".env is not gitignored" | Project has no `.gitignore`, or it does not ignore `.env` | Add `.env` to `.gitignore` (or create one); re-run. |
| Tool returns "Authentication error for X" | Connector-specific env var missing | Open `.env` and fill in the key for connector X. |
| Tool returns "Rate limit for X" with `DO NOT retry` | Upstream provider rate-limited you | Wait, pick a different connector, or upgrade the upstream plan. The agent will not retry. |
| Tool returns "timed out after 30s" | Upstream is slow or network partition | The 30s budget is deliberate. Retry manually if upstream recovers. |
| Tool returns `HTTPStatusError` after editing `.mcp.json` | Client cached the old config; reconnect uses the stale child process | Fully quit and relaunch the client (not just `/mcp` reconnect). |
| `${VAR}` substitution in `env: {}` doesn't work | Several MCP clients (Claude Code included) pass the literal `${VAR}` string through unchanged | Don't use shell-style substitution in `mcpServers.*.env`. Either hardcode the value or load via `.env` (the default `init` template uses `uv run --env-file .env`). |
| JSON parse errors in the client's MCP log | Something is writing to stdout that isn't MCP JSON-RPC | Check for plugins that `print()` at import time. Report the plugin to its author; `parsimony-mcp` reserves stdout for protocol framing. |

## Status

Alpha (`0.2.0a1`). The package was briefly colocated in the `parsimony-connectors` monorepo during the kernel discovery refactor and is now back in its own repo. Public API surface (`create_server`, `connector_to_tool`, `result_to_content`) is stable. Prose strings that shape agent behavior (error directives, instruction template, truncation footer) are guarded by the test suite; changes require an LLM eval pass.

## Development

```bash
git clone https://github.com/ockham-sh/parsimony-mcp
cd parsimony-mcp
uv venv
uv pip install -e ".[dev]"
uv run pytest                                  # ~130 tests, ~1s
uv run ruff check parsimony_mcp tests
uv run mypy parsimony_mcp
```

`parsimony-core` is a dependency; during development you will typically install it editable alongside:

```bash
uv pip install -e ../parsimony -e ".[dev]"
```

## License

Apache-2.0. See [LICENSE](LICENSE).
