Skip to main content
Glama
NeuralIO444

AE Test Bridge MCP

by NeuralIO444
README.md
# AE Test Bridge MCP

A fake Adobe After Effects bridge listener — same wire protocol as the
real thing — for testing bridge *client* code without needing real After
Effects running. Also exposed as an MCP server, so an AI agent can spin
one up, script its responses, and inspect what a client under test sent
it, entirely conversationally.

Part of a small set of related repos:

- **[AE_Bridge_MCP](https://github.com/NeuralIO444/AE_Bridge_MCP)** — the
  real MCP server for live After Effects introspection/eval.
- **[AE_Eval](https://github.com/NeuralIO444/AE_Eval)** — the same
  capability as a plain terminal CLI.
- **[IPC_Client](https://github.com/NeuralIO444/IPC_Client)** — the
  general-purpose transport client this repo's `MockAEBridge` speaks the
  same protocol as (this repo re-implements the send/recv framing
  inline rather than depending on that package, to stay a standalone,
  zero-dependency install).

This repo exists because none of the three above had anything to test
*against* other than mocking the transport itself — a real fake server
closes that gap and lets a client under test exercise its actual socket
code, not a stand-in for it.

## What it does

- **`ae_test_bridge_mcp/mock_server.py`** — `MockAEBridge`: a real TCP
  server. Register canned responses with `.when(match, response)`
  (matched by a substring in the request's `script` field, or an
  arbitrary predicate), a `.default(response)` fallback for anything
  unmatched, then point a real client at `bridge.host`/`bridge.port`.
  Every request it receives is recorded in `bridge.received_requests`
  for assertions.
- **`ae_test_bridge_mcp/server.py`** — an MCP server wrapping one
  `MockAEBridge` instance behind 7 tools (below), for an AI agent to
  drive directly instead of writing Python.

It cannot actually evaluate the ExtendScript in a request's `script`
field — there's no JS engine here, it's a test double. You tell it what
to say back; it doesn't compute anything.

## Tools exposed over MCP

| Tool | What it does |
|---|---|
| `start_mock_bridge` | Start listening (optional `port`, default: pick a free one). Returns the bound host/port. |
| `stop_mock_bridge` | Stop the running mock bridge. |
| `queue_response` | Register a canned response for requests whose `script` contains a given substring. |
| `set_default_response` | Set the fallback response for anything no `queue_response` rule matched. |
| `get_received_requests` | Return every request received so far, in order. |
| `clear_received_requests` | Clear the recorded history without stopping the bridge or its rules. |
| `clear_rules` | Remove all `queue_response` rules without stopping the bridge or clearing request history. The default response (if set) is unaffected. |

## Install

```bash
pip install -e .
# or, for running tests too:
pip install -e ".[dev]"
```

Zero runtime dependencies.

## Usage as a Python library (pytest, or anywhere else)

```python
from ae_test_bridge_mcp import MockAEBridge

with MockAEBridge() as bridge:
    bridge.when("app.project.activeItem.name", {"status": "OK", "result": "Hero Comp"})
    bridge.default({"status": "OK", "result": None})

    # Point whatever you're testing at 127.0.0.1:bridge.port instead of
    # the real After Effects listener's fixed port (45445) --
    # e.g. ipc_client.execute_job(job, port=bridge.port).

    assert bridge.received_requests[-1]["script"] == "..."
```

Responses can also be a one-argument callable `(request_dict) -> dict`
for dynamic or stateful behavior (an incrementing counter, echoing part
of the request back, simulating a value that changes across calls):

```python
bridge.when("echo", lambda req: {"status": "OK", "result": req["script"]})
```

Rules are checked in registration order — the first match wins.

## Usage as an MCP server

```json
{
  "mcpServers": {
    "ae-test-bridge": {
      "command": "python3",
      "args": ["-m", "ae_test_bridge_mcp"]
    }
  }
}
```

A typical agent-driven session: call `start_mock_bridge`, note the
returned port, tell the client under test to connect to that port
instead of the real one, call `queue_response` for the scenarios you
want to exercise, run the client, then call `get_received_requests` to
confirm it sent what you expected.

## Design notes

- **Hardened tool dispatch.** Every MCP tool call is wrapped so a bad
  argument (wrong type, missing field) returns a normal `isError: true`
  result — it can never raise out of `handle_call_tool` and kill the
  whole persistent stdio server process. This was a real bug found in a
  sibling repo during a review pass; fixed here from the start.
- **One bridge at a time, by design.** The MCP tool surface manages a
  single module-level `MockAEBridge` instance — `start_mock_bridge`
  fails loudly if one is already running rather than silently leaking
  the old one. For multiple concurrent fake bridges in the same process,
  use the `MockAEBridge` class directly instead of the MCP tools.
- **Each accepted connection runs on its own thread**, so a slow or
  stuck client doesn't block other connections. Rule/state mutation is
  lock-protected so registering rules from the main thread while a
  request is mid-dispatch is safe.
- **A raising response callable never kills the connection silently.**
  If a `.when()`/`.default()` callable raises, the client gets back a
  clean `{"status": "ERROR", "error": "... raised ValueError: ..."}`
  instead of the socket just closing with zero bytes.
- **A registered response that isn't JSON-serializable** (a `set`, an
  arbitrary object) gets the same treatment — a clean error naming the
  problem, not a silent close.
- **Declared message length is capped** (`max_size`, default 10 MiB,
  same class of protection as `IPC_Client`'s own `max_size` — built
  independently here since this repo deliberately has zero
  dependencies) so a peer claiming a huge body can't make the server
  block trying to read it.
- **`stop()` force-closes in-flight connections** rather than waiting
  out their own read timeout, so tearing down a bridge mid-test doesn't
  leave a stray thread appending to `received_requests` after the
  caller believes it's gone.

## Testing

```bash
pip install -e ".[dev]"
pytest tests/
```

Tests use real sockets throughout — no mocking of the `socket` module —
covering rule matching/ordering, dynamic responses, request recording,
malformed input on both the raw-socket layer and the MCP tool-argument
layer, and the full MCP JSON-RPC stdio loop.

## Changelog

- **v1.1.0** — Hardening pass from a first-round review: a raising
  response/default callable now returns a clean `ERROR` result instead
  of silently dropping the connection; a non-JSON-serializable response
  gets the same treatment instead of closing with zero bytes; incoming
  messages are capped at `max_size` (default 10 MiB) so a peer can't
  make the server block trying to read an oversized declared length;
  `stop()` now force-closes any in-flight connections and joins their
  threads instead of waiting out their own read timeout. Also added the
  `clear_rules` tool/method for symmetry with `clear_received_requests`.
- **v1.0.0** — Initial extraction, as a mock-bridge test double inspired
  by (but not a direct port of) Dimension's own `test_ae_bridge_mcp.py`
  patterns.

## License

MIT — see [LICENSE](LICENSE).

TDQS

A4/5.0

Scored across 7 tools

Disambiguation5/5

Each tool has a clearly distinct role: lifecycle management (start/stop), response configuration (queue vs default), and request inspection/reset. The relationship between queue_response and set_default_response is well-defined, so there is no real ambiguity.

Naming Consistency5/5

All tools follow a consistent snake_case verb_noun pattern: start_, stop_, queue_, set_, get_, clear_. The naming style is uniform and predictable across the entire set.

Tool Count5/5

Seven tools is well-scoped for a mock bridge server. Each tool covers a necessary capability—lifecycle, response control, request inspection, and state reset—without redundancy or bloat.

Completeness4/5

The surface covers the full testing workflow: start/stop the bridge, configure responses, inspect received requests, and reset state. Minor extras like checking bridge status or removing individual rules are absent but not critical to the core purpose.

Maintenance

ActivityMaintained
ResponsivenessUnresponsive