Architecture
Elliot is three small Python/TypeScript services that share one contract: a connector file.
┌─────────────┐ MCP / HTTP ┌──────────────────────────┐ HTTP / SQL ┌──────────────┐
│ AGENT │ ─────────────────▶ │ elliot-mcp-plugin :3000 │ ◀───────────────▶ │ SOURCES │
│ Claude Code │ │ FastMCP · tool registry │ │ REST · PG │
│ Cursor │ └────────────┬─────────────┘ │ MySQL · CSV │
│ Codex │ │ │ local files │
└─────────────┘ ▼ └──────────────┘
┌──────────────────────────┐
│ connector-runtime :3001 │
│ safe SQL · session log │
└────────────┬─────────────┘
▼
┌──────────────────────────┐
│ elliot-studio :5173 │
│ observe · run · edit │
└──────────────────────────┘
│
▼
NDJSON audit log of every callelliot-mcp-plugin (:3000)
A FastMCP server (Streamable HTTP transport) that exposes:
tools/list,tools/call— the user-defined tools from every loaded connectorprompts/list,prompts/get— skills + the canonicalgetting_startedpromptresources/list,resources/read— templates, error-code dictionary, install docs- A set of platform tools:
elliot_upload_file,elliot_discover_source,build-connector,lint-connector,run-eval,deploy-connector
This is the only endpoint any agent talks to.
elliot-connector-runtime (:3001)
The execution engine. When the plugin forwards tools/call, the runtime:
- Materialises the requested sources into an ephemeral in-memory SQLite database
- Binds parameters and executes the connector's SQL (never string-interpolated)
- Streams rows back, paginated and projected per the connector spec
- Writes one NDJSON line to the audit log:
{ts, tool, args, rows, bytes, duration_ms, error, session_id}
A 30-second TTL + mtime cache hot-reloads connector files without restart.
elliot-studio (:5173)
A React 19 + Vite SPA. Reads from the same audit log and runtime API. Pages:
| Page | Purpose |
|---|---|
| Dashboard | Sources list, auth status, row preview |
| Tools | Per-tool detail, parameter form, SQL viewer, run |
| Skills | Prompt template preview, tool dependencies |
| Playground | Run any tool/skill, raw JSON / table toggle |
| Metrics | Audit log with filter + CSV export |
How a single call flows
- Agent calls
tools/callon:3000with{ name: "list_animals", arguments: { species: "cat" } } - Plugin validates the tool exists and the arguments match the parameter schema
- Plugin forwards to runtime on
:3001 - Runtime fetches
animalssource (cache or live) - Runtime runs
SELECT * FROM animals WHERE :species IS NULL OR species = :specieswith boundspecies="cat" - Runtime returns rows + writes audit line
- Plugin streams MCP
tools/callresponse back to agent - Studio shows the call within ~1 second
Total moving parts: three services, one log, one contract.
Why ephemeral in-memory SQLite?
- No persistent state — every call is fresh
- One SQL engine across heterogeneous sources (REST + Postgres + CSV in one query)
- Cheap to spin up — fewer than 5 ms for typical sources
- Safe — destroying state between calls is the default, not an opt-in
Why NDJSON audit?
- Greppable on disk (
grep tool=list_animals audit.log | tail -n100) - Streamable into any log tool (Loki, Datadog, ELK)
- One line per call, no parser ambiguity