Skip to main content
Glama
tachyurgy
by tachyurgy

mcp-app-harness

A small, working reference implementation of the pattern behind an RL-training sandbox: take a real application, run it in a container, and expose it to an agent through a FastMCP server — with populate / snapshot / restore hooks so every episode starts from an identical, known state and any trajectory can be checkpointed and rewound.

It's deliberately tiny (a support-desk service + its MCP integration) so the seams are visible. Swap the demo app for any web/desktop app and the harness structure is unchanged.

┌────────────────┐        MCP tools/resources        ┌────────────────────┐
│  Agent / RL    │  ───────────────────────────────▶ │  FastMCP harness   │
│  policy        │  create_ticket, populate,          │  (mcp_harness/)    │
└────────────────┘  snapshot, restore, tickets://all  └─────────┬──────────┘
                                                                 │ HTTP (httpx)
                                                                 ▼
                                                       ┌────────────────────┐
                                                       │  App under test    │
                                                       │  FastAPI + SQLite  │
                                                       │  (app/)            │
                                                       └────────────────────┘

Why this exists

Integrating an app into a sandbox has three hard parts, and this repo shows a clean answer to each:

  1. Expose the app to an agent. Every product operation is a typed MCP tool (mcp_harness/server.py) and live state is an MCP resource. Schemas come free from the type hints.

  2. Make state reproducible. populate() seeds a deterministic baseline; snapshot() captures full state; restore() re-applies it. restore(snapshot(x)) == x is asserted in the tests (tests/test_hooks.py).

  3. Keep it testable locally. The HTTP client is transport-injectable, so the whole stack runs in-process over httpx.ASGITransport in CI — no sockets, no flakiness — while production uses real HTTP unchanged.

Layout

Path

Role

app/

The application under test — a FastAPI + SQLite support desk. Stands in for the third-party app you'd integrate. Ships an /admin state surface (dump/load/reset).

mcp_harness/server.py

The FastMCP server — product tools + lifecycle tools + resources.

mcp_harness/client.py

Transport-injectable async HTTP client to the app.

mcp_harness/hooks.py

populate / snapshot / restore — the sandbox lifecycle contract.

mcp_harness/config.py

Env-driven service configuration (12-factor).

fixtures/seed_baseline.json

Deterministic starting state for populate().

tests/

pytest: app, hooks, and the MCP tools driven via FastMCP's in-memory client.

Dockerfile / docker-compose.yml

One image, two services (app + harness), healthcheck-ordered boot.

Run it

Tests (fastest way to see it work):

uv venv --python 3.12 && source .venv/bin/activate
uv pip install -e ".[dev]"
pytest -q

Locally, two processes:

# terminal 1 — the app under test
./scripts/run_app.sh                       # http://localhost:8080/health

# terminal 2 — an episode: populate → act → snapshot → reset → restore
./scripts/demo_episode.sh

The MCP server:

python -m mcp_harness.server               # stdio (embed in an agent runtime)
MCP_TRANSPORT=http python -m mcp_harness.server   # HTTP transport on :9000

In a sandbox (Docker):

docker compose up --build
# app  → :8080   |   MCP harness (HTTP) → :9000

The MCP surface

Tools: create_ticket, list_tickets, get_ticket, assign_ticket, close_ticket, populate, snapshot, restore. Resources: tickets://all (live ticket list), health://app (liveness).

An agent's loop looks like: populate() → read tickets://all → call ticket tools → snapshot() to checkpoint. An RL runner scores the resulting state and restore()s to branch a new rollout.

Configuration (all via env — no code changes between environments)

Var

Default

Meaning

APP_BASE_URL

http://localhost:8080

Where the harness reaches the app

APP_DB_PATH

:memory:

SQLite path for the app (/data/... to persist on a volume)

MCP_TRANSPORT

stdio

stdio (embedded) or http (sandboxed port)

MCP_HOST / MCP_PORT

0.0.0.0 / 9000

HTTP transport bind

HARNESS_FIXTURE

fixtures/seed_baseline.json

Baseline for populate()

APP_HTTP_TIMEOUT

10

Client timeout (short, so a wedged app fails fast)

Adapting to a real app

app/ is the throwaway part. To integrate a real app:

  • Point AppClient (or a subclass) at the app's actual API.

  • Re-implement the three hooks against whatever state seam the app exposes — a DB volume snapshot, a docker commit, an export/import endpoint, or a management command. The tool/resource layer and the tests don't change.

License

MIT.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/tachyurgy/mcp-app-harness'

If you have feedback or need assistance with the MCP directory API, please join our Discord server