Building a new parsimony plugin¶
A parsimony plugin is a standalone Python package that exposes connectors to
the parsimony kernel via an entry point. This guide walks through building
one from scratch.
For the authoritative contract: contract.md.
For internal / private plugins: building-a-private-connector.md.
For the full implementation walkthrough (provider research, schema design,
error mapping, testing): connector-implementation-guide.md.
Canonical template¶
The reference implementation is
ockham-sh/parsimony-connectors/packages/fred.
Copy its structure and adjust:
parsimony-<yourname>/
├── parsimony_<yourname>/
│ ├── __init__.py CONNECTORS (+ optional CATALOGS / RESOLVE_CATALOG)
│ ├── connectors.py @connector / @enumerator / @loader functions
│ └── py.typed
├── tests/
│ ├── test_conformance.py assert_plugin_valid — release-blocking
│ └── test_<yourname>_connectors.py happy path + error mapping (respx mocks)
├── .github/workflows/
│ ├── ci.yml lint + type + test + conformance
│ └── release.yml OIDC PyPI publish on tag
├── pyproject.toml entry-point registration, kernel pin, metadata, [project.urls] homepage
├── README.md
├── CHANGELOG.md
├── LICENSE Apache-2.0 for official plugins
└── .gitignore
Minimum pyproject.toml¶
[project]
name = "parsimony-<your-name>"
version = "0.1.0"
license = "Apache-2.0"
requires-python = ">=3.11"
dependencies = [
"parsimony-core>=0.4,<0.5",
"pydantic>=2.11,<3",
"pandas>=2.3,<3",
"httpx>=0.27,<1",
]
[project.urls]
Homepage = "https://your-provider.example"
[project.optional-dependencies]
dev = [
"pytest>=9.0",
"pytest-asyncio>=1.3",
"respx>=0.22",
"ruff>=0.15",
"mypy>=1.10",
]
[project.entry-points."parsimony.providers"]
<your-name> = "parsimony_<your_name>"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["parsimony_<your_name>"]
The kernel pin (parsimony-core>=0.4,<0.5) is the stability boundary.
There is no separate contract-version classifier — plugins depend on
parsimony-core via a standard range pin and rely on the stability
markings in the API reference.
[project.urls] Homepage is what the kernel surfaces via
Provider.homepage (e.g. in parsimony list output and in the
parsimony-mcp init env-template generator). Provider version is read
from the distribution metadata; do not export a module-level __version__.
Minimum plugin module¶
# parsimony_<your_name>/__init__.py
from parsimony import Connectors, connector, Result
@connector(env={"api_key": "<YOUR>_API_KEY"}, tags=["<your_name>", "tool"])
async def <your_name>_search(params: SearchParams, *, api_key: str) -> Result:
"""At least 40 chars — MCP tool descriptions need enough context for LLMs."""
...
CONNECTORS = Connectors([<your_name>_search])
Per-connector env vars live on the @connector(env={...}) decorator —
the consumer resolves them via Connectors.bind_env(). There is no
module-level ENV_VARS, PROVIDER_METADATA, or __version__.
If your plugin publishes catalog bundles, add a CATALOGS export:
# Static (namespaces known at import time):
CATALOGS = [("<your_name>", <your_name>_enumerate)]
# Or dynamic (async generator):
async def CATALOGS():
async for region in _fetch_regions():
yield f"<your_name>_{region.code.lower()}", partial(_enumerate, region=region)
See contract.md §6 for the full CATALOGS /
RESOLVE_CATALOG spec.
Conformance gate¶
Every release must pass the conformance suite:
# tests/test_conformance.py
import parsimony_<your_name>
from parsimony.testing import assert_plugin_valid
def test_plugin_conforms() -> None:
assert_plugin_valid(parsimony_<your_name>)
Run locally:
In CI, make the conformance job release-blocking.
Verify end-to-end discovery¶
After installing alongside parsimony-core:
Your plugin should appear. Add --strict to run the conformance suite
and exit non-zero on any failure:
Publishing¶
- Configure PyPI trusted publishing for your GitHub repo.
- Copy the workflow files from
parsimony-connectors/packages/fred/.github/workflows/into your repo. - Tag a release (
git tag v0.1.0 && git push --tags); GitHub Actions publishes to PyPI via OIDC (no tokens in secrets).
Checklist before cutting v0.1.0¶
- [ ]
parsimony_<your_name>module exportsCONNECTORS. - [ ] Optional:
CATALOGS,RESOLVE_CATALOG. - [ ] Per-connector
@connector(env={...})declarations cover every required keyword-only dep. - [ ]
[project.urls] Homepageset inpyproject.toml. - [ ] Entry point registered in
pyproject.tomlunderparsimony.providers. - [ ]
parsimony.testing.assert_plugin_valid(module)passes. - [ ] Tool-tagged connectors have ≥40-character descriptions.
- [ ] Unit tests cover happy path + at least one error path (401, 429, empty).
- [ ]
README.mddocuments install, setup, example usage. - [ ] Apache-2.0
LICENSEfile present. - [ ] CI workflows green on main.
When to create a per-provider vs protocol-grouped plugin¶
- Per-provider (
parsimony-<provider>) when the API is bespoke. Default. - Protocol-grouped (
parsimony-<protocol>) only when multiple providers share a wire protocol, >60% of implementation, dependency tree, and maintenance cadence. Examples:parsimony-sdmx,parsimony-pxweb.