Skip to main content
Glama
mcp-app-harness
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.

Related MCP server: Nibble

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.

Available Tools

8 tools
assign_ticketC

Assign a ticket to someone; moves it to in_progress.

Inputs: assignee (required), ticket_id (required). Returns: object

close_ticketD

Close a ticket.

Inputs: ticket_id (required). Returns: object

create_ticketB

Open a new support ticket. priority ∈ low|normal|high|urgent.

Inputs: title (required), priority. Returns: object

get_ticketB

Fetch a single ticket by id.

Inputs: ticket_id (required). Returns: object

list_ticketsA

List tickets, optionally filtered by status (open|in_progress|closed).

Inputs: status. Returns: result

populateC

Reset the app and load the deterministic baseline fixture.

Inputs: fixture_path. Returns: object

restoreC

Overwrite application state with a previously captured snapshot.

Inputs: state (required). Returns: object

snapshotB

Capture the full application state as a JSON document.

Inputs: no named parameters. Returns: object

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    C2.8/5.0

    Scored across 8 tools

    Disambiguation5/5

    Each tool has a clearly distinct purpose: ticket tools (create, read, update status, list) and state management tools (populate, restore, snapshot) operate on different aspects with no overlap.

    Naming Consistency4/5

    Ticket tools follow consistent verb_noun pattern (e.g., create_ticket), but state management tools use single-word verbs (populate, restore, snapshot), creating a minor inconsistency in naming conventions.

    Tool Count5/5

    8 tools is well-scoped for a ticket system with testing utilities; each tool earns its place without being excessive or insufficient.

    Completeness3/5

    Core ticket lifecycle is covered (create, get, list, assign, close), but missing update for other fields (e.g., priority, description) and no reopen functionality, leaving notable gaps.

    Related MCP Connectors

    Related MCP Servers