Math Circuit Studio
by Lxvi101
README.md
# Math Circuit Studio
A small, self-hosted MCP App for ChatGPT/Codex that can:
- draw interactive 2D function plots, 3D surfaces, and parametric 3D curves;
- embed a pinned [CircuitJS1](https://github.com/pfalstad/circuitjs1) editor for explanatory circuits;
- render short Manim MP4s from a constrained slide/equation schema.
It has no database, queue, S3 bucket, CDN, or external simulation API. One Docker
container runs the MCP server, hosts every browser asset, and invokes Manim locally.
Generated videos and CircuitJS export files live in a named Docker volume and expire
automatically.
CircuitJS is used as an interactive teaching canvas. This project intentionally does
not expose SPICE, arbitrary Manim Python, or electrical design verification.
## Architecture
```mermaid
flowchart LR
C["ChatGPT or an MCP client"] -->|"Streamable HTTP /mcp"| A["Node MCP server"]
A --> W["Unified in-chat widget"]
W --> P["Plotly + mathjs lazy-loaded for graphs"]
W --> J["Bundled CircuitJS iframe"]
A --> M["Constrained Manim templates"]
M --> V["Local artifacts volume"]
W --> V
```
## Tools
| Tool | Intended input | Result |
|---|---|---|
| `plot_math` | mathjs expressions and finite ranges | interactive 2D/3D Plotly view |
| `show_circuit` | CircuitJS **Export as Text** content | embedded CircuitJS canvas |
| `render_manim_explanation` | 1–8 text/LaTeX slides | locally rendered MP4 player |
All three tools use the same MCP App resource at
`ui://math-circuit-studio/viewer-v2.html`. Every structured tool result has a matching
output schema. The resource declares only the deployed origin in its CSP; there are
no public CDN dependencies.
## Run locally with Docker
Requirements: Docker Compose v2 and roughly 4 GB RAM for building CircuitJS and
rendering Manim.
```bash
cp .env.example .env
# For local use, set PUBLIC_ORIGIN=http://localhost:3000 in .env
docker compose -f compose.yaml -f compose.local.yaml up --build
```
Check `http://localhost:3000/readyz` (dependency readiness) or `/healthz` (process
liveness). The MCP endpoint is
`http://localhost:3000/mcp` and the bundled CircuitJS page is
`http://localhost:3000/circuitjs/circuitjs.html`.
The first image build compiles the pinned CircuitJS GWT application, so it is much
slower than subsequent cached builds.
## Deploy with Dokploy
1. Create a **Docker Compose** application from this repository and select
`compose.yaml`.
2. In Dokploy's Environment tab, set at least:
```dotenv
PUBLIC_ORIGIN=https://visuals.your-domain.example
WIDGET_DOMAIN=https://visuals.your-domain.example
MEMORY_LIMIT=4G
CPU_LIMIT=2.0
```
3. In **Domains**, add the public hostname to service `app`, container port `3000`,
and enable HTTPS. Do not add Traefik labels to the Compose file; Dokploy manages
them from this domain configuration.
4. Deploy, then verify `https://visuals.your-domain.example/readyz` and
`https://visuals.your-domain.example/circuitjs/circuitjs.html`.
5. Add `https://visuals.your-domain.example/mcp` as the remote MCP server in ChatGPT
developer mode.
Dokploy writes dashboard environment values to `.env`; this Compose file references
them explicitly with `${VARIABLE}` interpolation. It uses only a named volume, never
an absolute host bind mount, so redeploys remain portable on a single Dokploy node.
Do not run multiple replicas: the render-concurrency guard is process-local and the
artifact volume is local. This is intentionally a simple single-instance service.
## Configuration
| Variable | Default | Purpose |
|---|---:|---|
| `PUBLIC_ORIGIN` | `http://localhost:3000` | Exact external origin used in iframe/video URLs and widget CSP |
| `WIDGET_DOMAIN` | `PUBLIC_ORIGIN` | Dedicated HTTPS origin declared to the ChatGPT widget host |
| `MANIM_ENABLED` | `true` | Enables the Manim tool |
| `MANIM_TIMEOUT_MS` | `75000` | Hard timeout for one render |
| `MANIM_MAX_CONCURRENT` | `1` | Simultaneous render limit |
| `ARTIFACT_TTL_HOURS` | `72` | Local MP4 and CircuitJS export retention |
| `MCP_BEARER_TOKEN` | empty | Optional static bearer protection for clients that can send it |
| `MEMORY_LIMIT` | `4G` | Runtime Compose memory limit |
| `CPU_LIMIT` | `2.0` | Runtime Compose CPU limit |
| `CIRCUITJS_REF` | pinned SHA | Reproducible CircuitJS source revision |
`PUBLIC_ORIGIN` must be the external HTTPS origin in ChatGPT. Set `WIDGET_DOMAIN` to
that same dedicated origin for plugin submission. A localhost value in a remote
deployment will make nested CircuitJS frames and video URLs point at the wrong machine.
## ChatGPT integration notes
The server uses Streamable HTTP at `/mcp`, serves an MCP App HTML resource with the
`text/html;profile=mcp-app` MIME type, and returns structured content for the widget.
The CircuitJS editor is a nested same-origin frame, so the resource CSP includes the
configured origin in `frameDomains`. Circuit export text is loaded from a short,
content-addressed `/circuit-files/...` URL instead of being placed in a potentially
oversized query string.
The default deployment has no authentication to keep first-time self-hosting simple.
That means anyone who can reach `/mcp` can request renders. For an internet-facing
production deployment, put the endpoint behind an OAuth-capable gateway compatible
with your MCP client, or use `MCP_BEARER_TOKEN` with a client that can attach a static
Authorization header. Keep the video and CircuitJS routes publicly readable so the
sandboxed widget can load them.
## Local development
Node 22+ is required.
```bash
npm ci
npm run check
npm test
MANIM_ENABLED=false PUBLIC_ORIGIN=http://localhost:3000 npm start
```
The host build does not install Manim or compile CircuitJS; those are supplied by the
Docker image. The normal test suite disables Manim and verifies MCP schemas, metadata,
input rejection, graph results, concurrent circuit storage, and widget resources. The
container test additionally exercises a real Manim/LaTeX render and cache hit.
The included `.mcp.json` points Codex at `http://localhost:3000/mcp`, so start the
Compose application before using the repo-local plugin configuration. For a remote
instance, change that URL to the deployed HTTPS endpoint.
## Operational and safety boundaries
- Manim source is generated only by server-owned templates. User/model content is
inserted as escaped string literals; arbitrary Python is not accepted. LaTeX is
restricted to a math-oriented command and environment allowlist before TeX runs.
- Graph expressions are parsed and restricted to numeric operators, selected math
functions, constants, and the variables expected by each graph mode. Assignment,
object access, imports, and function definitions are rejected.
- The container runs as `manimuser`, drops Linux capabilities, uses a read-only root
filesystem, and writes only to `/tmp` plus the artifact volume.
- Manim jobs are synchronous, cached, timeout-bound, and deliberately limited. On
timeout or shutdown, the complete Manim/ffmpeg/LaTeX process group is terminated.
This avoids Redis and a worker queue, but it is intended for short explanations
rather than production video jobs.
- CircuitJS browser behavior is useful for intuition and demos, not component sizing,
compliance, safety analysis, or sign-off.
## CircuitJS license
CircuitJS1 is GPL-2.0 licensed. The Docker image exposes the exact license at
`/licenses/circuitjs1-GPL-2.0.txt` and the complete pinned source used to build it at
`/licenses/circuitjs1-source.tar.gz`. Review `THIRD_PARTY_NOTICES.md` before
redistributing an image, especially if you modify CircuitJS.
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues