An MCP (Model Context Protocol) server for the Event Provenance Registry (EPR). It gives an AI assistant tools to fetch, search, and create EPR events, event receivers, and event receiver groups. Built on FastMCP 4 and httpx2.
New here? Build the server step by step in the workshop, or watch it work with the demos.
- Python 3.12 or newer
- A running EPR (the workshop compose file starts one)
- uv is recommended; pip works too
git clone https://github.com/xbcsmith/epr-mcp-python.git
cd epr-mcp-python
uv run eprmcp start --url http://localhost:8042uv run creates the environment on first use. Without uv:
python3 -m venv .venv && source .venv/bin/activate
pip install -e .
eprmcp start --url http://localhost:8042The server speaks MCP over one of two transports:
| Transport | Use it for | Command |
|---|---|---|
http |
A long-running server; Docker; remote clients | eprmcp start (the default) |
stdio |
An editor that starts the server itself (VS Code, Claude Desktop) | eprmcp start --transport stdio |
With http the MCP endpoint is http://127.0.0.1:8000/mcp. With stdio the
server must not write anything but protocol messages to stdout; it logs to
stderr.
Every option has an environment variable; the command line wins.
| Option | Environment | Default | Meaning |
|---|---|---|---|
--url |
EPR_URL |
http://localhost:8042 |
EPR base URL |
--token |
EPR_API_TOKEN, EPR_TOKEN |
none | Sent as Authorization: Bearer on every EPR request |
--transport |
MCP_TRANSPORT |
http |
http or stdio |
--host |
MCP_HOST |
0.0.0.0 |
Bind address for http |
--port |
MCP_PORT |
8000 |
Bind port for http |
--debug |
EPR_DEBUG |
off | Debug logging and a debugger on errors (not for stdio) |
eprmcp version prints the version.
The server registers nine tools. Arguments are passed flat; there is no extra
data wrapper inside them.
| Tool | Arguments |
|---|---|
fetch_event |
id: 26 character ULID |
fetch_receiver |
id |
fetch_group |
id |
search_events |
data: any of name, version, release, platform_id, package, description, success, event_receiver_id |
search_receivers |
data: any of name, type, version, description |
search_groups |
data: any of name, type, version, description |
create_event |
event_data: name, version, release, platform_id, package, description, payload, success, event_receiver_id |
create_receiver |
receiver_data: name, type, version, description, schema |
create_group |
group_data: name, type, version, description, event_receiver_ids |
Unknown fields are rejected, so a typo cannot silently turn a filtered search into an unfiltered one. Create tools return the new record's ID. See schema_validation.md for the exact rules.
import asyncio
from fastmcp import Client
async def main():
async with Client("http://127.0.0.1:8000/mcp") as client:
result = await client.call_tool("search_events", {"data": {"name": "checkout-service"}})
print(result.content[0].text)
asyncio.run(main())With the http transport the server also serves:
| Endpoint | Purpose |
|---|---|
GET /health |
Health check (plain text OK) |
GET /openapi.yaml, /openapi.json |
The OpenAPI description (see openapi_implementation.md) |
GET /docs |
Swagger UI for that description |
/mcp |
The MCP endpoint |
Both editors start the server themselves over stdio. VS Code, in
.vscode/mcp.json:
{
"servers": {
"epr-mcp": {
"type": "stdio",
"command": "uv",
"args": [
"run",
"--directory",
"/path/to/epr-mcp-python",
"eprmcp",
"start",
"--transport",
"stdio"
],
"env": { "EPR_URL": "http://localhost:8042" }
}
}
}Claude Desktop needs the full path to uv (it does not inherit your shell
PATH). A working pair of configurations and a troubleshooting table are in
module 06 of the workshop. Running
the server inside a container that an editor launches is unreliable; use
uv run.
To explore the server in a browser, use the MCP Inspector (see module 02).
The image installs the built wheel, so build the wheel first:
make docker-image # builds the wheel, then the image
docker run -p 8000:8000 \
-e EPR_URL=http://host.docker.internal:8042 \
-e EPR_TOKEN=your-token \
epr-mcp-python:latestOr with Compose, which reads .env (copy .env.example):
make wheel
docker compose up -d --build
curl http://localhost:8000/healthInside a container EPR_URL must be an address the container can reach, such as
http://host.docker.internal:8042 for an EPR on the host. See
docker_compose.md.
uv sync --extra lint --extra test # or: pip install -e '.[lint,test,build]'
make lint # ruff format and check: src, tests, demos
uv run pytest --cov=src --cov-fail-under=80| Make target | Does |
|---|---|
install |
Installs into a virtualenv |
lint |
Runs ruff on src/epr_mcp, tests, and demos |
tests |
Runs the tests with tox |
wheel |
Cleans, then builds the sdist and wheel into dist/ |
docker-image |
Builds the wheel, then the Docker image |
workshop-check |
Preflight for the workshop (needs uv and a running EPR) |
release |
Runs the tox release environment |
clean |
Removes build output and caches |
Functional tests that start the real server are skipped unless
EPR_MCP_FUNCTIONAL=1 is set.
Version 2.0 moves to FastMCP 4 and httpx2 and changes some behavior. If you run the server, check these:
- Dependencies.
fastmcp>=4,<5andhttpx2replacehttpx;pydantic>=2.12andstarlette>=1.0are required. Anything that importedhttpxfrom this package's environment must switch tohttpx2. TLS verification now uses the operating system trust store; setSSL_CERT_FILEorSSL_CERT_DIRfor a private CA. - Tool arguments are flat.
search_eventstakes{"data": {"name": "x"}}, not{"data": {"data": {"name": "x"}}}, andcreate_eventtakes{"event_data": {...}}. The old wrapped shape is rejected, and so are unknown fields. - Receivers need a schema.
create_receiverrequiresschema, which EPR itself requires. - Creates work against a real EPR. The create tools used to report failure
because EPR answers 200 (not 201) and returns only the new ID. They now accept
both and return
{"message": ..., "id": ...}. - The token is sent.
EPR_TOKEN(andEPR_API_TOKEN) is now sent as a bearer token on every EPR request. Before, it was read but never sent. openapi_serveris removed. It was not reachable from the command line. The/openapi.yaml,/openapi.json, and/docsroutes stay.- New options.
--transport stdio|http,--host, and--port, withMCP_TRANSPORT,MCP_HOST, andMCP_PORT. - Logging. Tool diagnostics go to the server log (stderr). They are no longer sent to MCP clients as log notifications, which FastMCP 4 deprecates.
- Packaging. The wheel is
py3-none-anyand now includesopenapi.yaml.
Connection failed to EPR server: EPR is down orEPR_URLis wrong. Inside Docker usehttp://host.docker.internal:8042for an EPR on the host.- Check EPR:
curl http://localhost:8042/healthz/readinessprints200when ready. - Check this server:
curl http://localhost:8000/healthprintsOK. - Compose logs:
docker compose logs -f epr-mcp-server. - Setting up the workshop or demos: workshop troubleshooting.
- Workshop: build the server step by step
- Demos: runnable demonstrations with presenter scripts
- Docker Compose
- OpenAPI endpoints
- Schema validation
- Migration notes and implementation plan
Apache 2.0. See LICENSE.