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 — the real MCP server for live After Effects introspection/eval.
AE_Eval — the same capability as a plain terminal CLI.
IPC_Client — the general-purpose transport client this repo's
MockAEBridgespeaks 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'sscriptfield, or an arbitrary predicate), a.default(response)fallback for anything unmatched, then point a real client atbridge.host/bridge.port. Every request it receives is recorded inbridge.received_requestsfor assertions.ae_test_bridge_mcp/server.py— an MCP server wrapping oneMockAEBridgeinstance 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 listening (optional |
| Stop the running mock bridge. |
| Register a canned response for requests whose |
| Set the fallback response for anything no |
| Return every request received so far, in order. |
| Clear the recorded history without stopping the bridge or its rules. |
| Remove all |
Install
pip install -e .
# or, for running tests too:
pip install -e ".[dev]"Zero runtime dependencies.
Usage as a Python library (pytest, or anywhere else)
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):
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
{
"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: trueresult — it can never raise out ofhandle_call_tooland 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
MockAEBridgeinstance —start_mock_bridgefails loudly if one is already running rather than silently leaking the old one. For multiple concurrent fake bridges in the same process, use theMockAEBridgeclass 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 asIPC_Client's ownmax_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 toreceived_requestsafter the caller believes it's gone.
Testing
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
ERRORresult 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 atmax_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 theclear_rulestool/method for symmetry withclear_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.pypatterns.
License
MIT — see LICENSE.