Backstop MCP - Local Development
3 min read
Overview
Run the server on your machine against a real Backstop tenant and a local PostgreSQL. The service is a Python project driven by uv, not by pnpm or turbo.
Writes are real. There is no Backstop sandbox in this setup. Write tools create, change, and delete records in whichever tenant
BACKSTOP_BASE_URLpoints at, as the user you log in with.
Prerequisites
Tool | Why |
|---|---|
Installs Python 3.14 (pinned in | |
Docker | Local PostgreSQL, and the test suite starts a PostgreSQL container |
A Backstop login with a personal API token | To log in through the form. The login must be a Backstop system user |
1. Start PostgreSQL
The repository's docker-compose.yaml runs PostgreSQL 17 on port 9001 with user and password postgres. It does not create this service's database, so create it once:
# from the repository root
docker compose up -d postgres
docker compose exec postgres createdb -U postgres backstop_mcp2. Configure
cd services/backstop-mcp
cp .env.example .env
uv run python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"uv run creates the environment first if it does not exist yet.
In .env, set at least:
APP_ENV=development
PUBLIC_BASE_URL=http://localhost:9010
BACKSTOP_BASE_URL=https://<firm>.backstopsolutions.com
BACKSTOP_MCP_ENCRYPTION_KEY=<the key printed above>.env.example already points DB_* at the compose database and documents most other variables. APP_ENV=development allows the http://localhost issuer, which production refuses. The full list is in Configuration.
3. Install, migrate, run
uv sync --locked
uv run alembic upgrade head
uv run backstop-mcpbackstop-mcp loads .env and serves on PORT (default 9010):
URL | What |
|---|---|
| MCP endpoint (streamable HTTP) |
| OAuth discovery |
| Login form (reached through the OAuth flow, not directly) |
| Process is up |
| PostgreSQL is reachable |
| Prometheus metrics |
4. Connect a client
Use any MCP client that supports OAuth over streamable HTTP, for example the MCP Inspector:
npx @modelcontextprotocol/inspectorPoint it at http://localhost:9010/mcp. The client registers itself, your browser opens the login form, and after you submit your Backstop username and API token the client is connected. Start with a read tool such as list_system_users or search_organizations.
Migrations
uv run alembic upgrade head # apply
uv run alembic revision --autogenerate -m "message" # create
uv run alembic downgrade -1 # roll back oneAlembic reads the same DB_URL / DATABASE_URL / DB_* variables as the server. Migrations cover this service's backstop_credentials table and the shared mcp_auth_* tables.
Tests, Lint, Types
The same checks the [CI] Python workflow runs:
pnpm install --frozen-lockfile # once, from the repository root, for biome
cd services/backstop-mcp
uv sync --locked
uv run ruff format --check . && uv run ruff check .
pnpm exec biome check . --no-errors-on-unmatched
uv run basedpyright # do not pass `.`, that type-checks .venv
uv run pytest # needs Docker runninguv run ruff format . applies the formatting, and uv run pytest tests/features/auth -k refresh runs a subset.
Feature tests mock the Backstop HTTP API and do not need PostgreSQL. App, auth, and database tests start a PostgreSQL container.
In a fresh git worktree, run uv sync first. Without a .venv, basedpyright resolves no imports and reports thousands of errors that are not real. See the repository's DEVELOP.md, "Python Services".
Changing the Code
Read AGENT_README.md before adding or changing a feature. It sets the layout every feature follows and the rules tests enforce:
a tool is registered by adding it to
server/tools/registry.py,nothing under
features/importsserver/,every cached provider is listed in
teardown.PROVIDERS.
How the Backstop REST API itself behaves (records, filters, paging, write payloads, errors) is in the Backstop Data Model.
agent-explore/ holds developer scripts for reading the live API and Backstop's help centre. It is not part of the shipped server.
Troubleshooting
Startup error | Cause |
|---|---|
|
|
| The key in |
| Neither |
PostgreSQL's | The |