AE Test Bridge MCP
# 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
Scored across 7 tools
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.
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.
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.
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.