feat: unified messaging TUI MVP — Synapse + 7 mautrix bridges + Textual TUI + MCP draft staging

One-command local stack: cp .env.example .env && make up. Idempotent
bootstrap renders Synapse + bridge configs from pristine upstream examples,
provisions databases, registrations, double puppeting, and the admin user.
Textual TUI with bridge manager, in-terminal QR login, and drafts panel.
MCP server exposes read tools and a human-gated draft send workflow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Prad Nukala
2026-07-08 12:13:13 -04:00
co-authored by Claude Fable 5
commit d57086cd00
29 changed files with 5602 additions and 0 deletions
+101
View File
@@ -0,0 +1,101 @@
#!/bin/bash
# Smoke tests: stack health, appservice round-trip, MCP tools + draft gating.
set -euo pipefail
cd "$(dirname "$0")/.."
set -a; source .env; set +a
HS=http://localhost:8008
pass() { echo "PASS: $*"; }
fail() { echo "FAIL: $*"; exit 1; }
echo "== 1/4 containers"
docker compose ps --format '{{.Service}} {{.State}} {{.Health}}' | tee /tmp/vortex-ps.txt
for svc in postgres synapse mcp; do
grep -q "^$svc running" /tmp/vortex-ps.txt || fail "$svc not running"
done
IFS=',' read -ra BRIDGES <<< "${BRIDGES_ENABLED:-}"
for b in "${BRIDGES[@]}"; do
grep -q "^$b running" /tmp/vortex-ps.txt || fail "bridge $b not running"
done
for oneshot in bootstrap provision; do
st=$(docker compose ps -a --format '{{.Service}} {{.ExitCode}}' $oneshot | awk '{print $2}')
[ "$st" = 0 ] || fail "$oneshot exit code $st"
done
pass "all services up, one-shots exited 0"
echo "== 2/4 synapse health + login"
curl -sf $HS/health >/dev/null || fail "synapse /health"
TOKEN=$(curl -sf -X POST $HS/_matrix/client/v3/login -d "{
\"type\":\"m.login.password\",
\"identifier\":{\"type\":\"m.id.user\",\"user\":\"$MATRIX_USER\"},
\"password\":\"$MATRIX_PASSWORD\",\"initial_device_display_name\":\"verify\"}" |
python3 -c 'import json,sys;print(json.load(sys.stdin)["access_token"])')
[ -n "$TOKEN" ] || fail "login"
pass "synapse healthy, admin login works"
echo "== 3/4 bridge bot round-trip"
FIRST_BRIDGE=$(echo "${BRIDGES_ENABLED}" | cut -d, -f1)
BOT="@${FIRST_BRIDGE}bot:${MATRIX_SERVER_NAME}"
python3 - "$HS" "$TOKEN" "$BOT" <<'EOF' || fail "bridge bot did not respond"
import json, sys, time, urllib.request
hs, token, bot = sys.argv[1:4]
def api(method, path, body=None):
req = urllib.request.Request(hs + path, method=method,
data=json.dumps(body).encode() if body is not None else None,
headers={"Authorization": "Bearer " + token})
return json.load(urllib.request.urlopen(req))
room = api("POST", "/_matrix/client/v3/createRoom",
{"invite": [bot], "is_direct": True, "preset": "trusted_private_chat"})["room_id"]
api("PUT", f"/_matrix/client/v3/rooms/{room}/send/m.room.message/{time.time_ns()}",
{"msgtype": "m.text", "body": "help"})
deadline = time.time() + 30
since = None
while time.time() < deadline:
q = f"/_matrix/client/v3/sync?timeout=5000" + (f"&since={since}" if since else "")
resp = api("GET", q)
since = resp["next_batch"]
events = resp.get("rooms", {}).get("join", {}).get(room, {}).get("timeline", {}).get("events", [])
for e in events:
if e["sender"] == bot and e["type"] == "m.room.message":
print("bot replied:", e["content"]["body"][:80].replace("\n", " "))
sys.exit(0)
sys.exit(1)
EOF
pass "appservice round-trip via $BOT"
echo "== 4/4 MCP tools + draft gating"
docker compose exec -T mcp python - <<'EOF' || fail "mcp checks"
import asyncio, json, os
from fastmcp import Client
async def main():
port = os.environ.get("MCP_HTTP_PORT", "8765")
async with Client(f"http://localhost:{port}/mcp") as c:
tools = {t.name for t in await c.list_tools()}
assert {"list_rooms", "read_messages", "create_draft", "send_draft"} <= tools, tools
rooms = (await c.call_tool("list_rooms")).data
assert isinstance(rooms, list), rooms
print(f"list_rooms: {len(rooms)} rooms")
room_id = rooms[0]["room_id"] if rooms else "!dummy:localhost"
d = (await c.call_tool("create_draft",
{"room_id": room_id, "body": "verify draft"})).data
did = d["draft_id"]
drafts = (await c.call_tool("list_drafts")).data
assert any(x["draft_id"] == did for x in drafts)
try:
await c.call_tool("send_draft", {"draft_id": did})
raise SystemExit("send_draft should have been blocked")
except Exception as e:
assert "disabled" in str(e).lower() or "human" in str(e).lower(), e
print("send_draft correctly blocked:", str(e)[:80])
await c.call_tool("discard_draft", {"draft_id": did})
print("draft lifecycle OK")
asyncio.run(main())
EOF
pass "mcp list_rooms + draft lifecycle + send gating"
echo
echo "ALL CHECKS PASSED"