From 49f6965d3c3e93b7404c9d3c7d2558c0ce452d9a Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Fri, 12 Sep 2025 21:19:06 +0700 Subject: [PATCH 1/2] docs(research): breakdown mathom --- mathom.md | 212 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 mathom.md diff --git a/mathom.md b/mathom.md new file mode 100644 index 00000000..c62d3785 --- /dev/null +++ b/mathom.md @@ -0,0 +1,212 @@ +--- +title: Breakdown Mathom - run and monitor MCP servers locally +short_title: Breakdown Mathom +description: 'Technical analysis of the Mathom Server architecture, implementation, and design patterns.' +date: 2025-09-07 +authors: + - vdhieu +tags: + - breakdown + - deepdive + - mcp + - llm + - architecture +toc: true +--- + +> **TL;DR** +**mathom** helps you run and moniror MCP servers on your own machine. It has a web dashboard, a small Go *runner that starts the servers, and a TypeScript CLI (`mcx`). If a server only talks stdio, mathom can add a small proxy so web clients can talk to it. + +## What the app does + +* Start servers with `mcx` (from a name, an npm package, or a Docker image). You can pass extra args and env. +* See logs live in a simple dashboard (dark/light). +* Sign in with OAuth (easy in dev, tighten later for prod). +* Use with editors like Claude Desktop or Cursor by setting `command: "mcx"` and `env.MATHOM_URL`. + +## How the parts fit together + +```mermaid +flowchart LR + subgraph "Dashboard (web)" + D_API["API for run & logs"] + D_UI["Pages: logs • status"] + end + + subgraph "Runner (podrift, Go)" + R_SPAWN["Start process (stdio)"] + R_LOGS["Read stdout/stderr"] + end + + subgraph "CLI (mcx)" + C_LOCAL["mcx "] + C_DOCKER["mcx --docker "] + end + + subgraph "Data (PostgreSQL)" + T_INST["instances"] + T_LOGS["logs"] + end + + subgraph "Proxy (stdio to web)" + P["stdio ⇄ SSE/HTTP"] + end + + C_LOCAL -->|run| D_API + C_DOCKER -->|run| D_API + D_API <-->|HTTP :9090| R_SPAWN + R_LOGS -->|live stream| D_API + D_UI -->|query| T_INST + D_UI -->|tail| T_LOGS + + C_DOCKER --> P + P --> R_SPAWN +``` + +*In short:* `mcx` asks the API to start a server. The Go runner starts it and streams logs back. If the server is inside Docker and only speaks stdio, the proxy exposes a small web endpoint. + +## How it works + +* Dashboard – Next.js app with API routes for run and logs. Stores data in Postgres. +* Runner – Go program that starts the server process, watches it, and sends logs. +* TypeScript CLI: + + * `mcx ` – run a local stdio server (by name or npm package). + * `mcx --docker -e KEY=VAL` – run the server in Docker and add the proxy if needed. +* `quickstart.sh` builds/starts everything. +* `docker-compose.yaml` links the parts for dev. +### Higligted design patterns being used: + +* Supervisor (runner): the Go runner starts the server process, watches it, and reports status. Goal: keep the child process healthy and its logs flowing. + +* Adapter / Proxy: turns a stdio‑only server into something web clients can talk to (SSE/HTTP). Useful when the tool was never built for the network. + +* Command (CLI): mcx has clear subcommands/handlers (e.g., run, --docker). Each command maps to one action. Keeps CLI code small and easy to test. + +* Observer (logs via SSE): the UI subscribes to log events from the server and updates the view when new lines arrive. Push, not polling. + + + +### Key flows + +#### 1) Run a local stdio server + +```mermaid +sequenceDiagram + autonumber + participant Dev as Developer + participant MCX as mcx (CLI) + participant API as Dashboard API + participant RT as Runner (Go) + participant DB as Postgres + participant UI as Dashboard + + Dev->>MCX: mcx @modelcontextprotocol/server-filesystem + MCX->>API: POST /instances/run {cmd, env} + API->>RT: start() + RT->>RT: spawn child (stdio) + RT-->>API: status: running + RT->>API: send log lines (live) + API->>DB: save instance + logs + UI-->>API: GET /instances/:id/logs/stream + API-->>UI: log lines +``` + +* **What’s hard**: keeping the child process up, handling fast logs, knowing when the server is “ready”. + +#### 2) Run a Docker server that needs the web + +```mermaid +sequenceDiagram + autonumber + participant Dev as Developer + participant MCX as mcx (CLI) + participant API as Dashboard API + participant RT as Runner (Go) + participant PR as Proxy + participant UI as Dashboard + + Dev->>MCX: mcx --docker mcp/github-mcp-server -e TOKEN=... + MCX->>API: POST /instances/run {image, env} + API->>RT: start container + RT->>PR: wire stdio pipes + PR-->>RT: expose SSE/HTTP endpoint + RT-->>API: status: running + UI-->>API: subscribe to logs +``` + +* **Note**: the proxy adds a little overhead but lets web clients work. + +## Data and CRUD map + +| Entity | Fields (main) | Created by | Read by | Updated by | Deleted by | +| ---------- | -------------------------------------------------------------- | ------------- | ---------------- | --------------- | --------------- | +| `Instance` | `id`, `name`, `cmd/image`, `env`, `status`, `pid`, `createdAt` | API on run | UI, runner | Runner (status) | API/cleanup job | +| `LogLine` | `id`, `instanceId`, `ts`, `level`, `text` | Runner | UI/history | — | TTL job | +| `Session` | `userId`, `provider`, `createdAt`, `expiresAt` | Auth callback | API (auth check) | Auth middleware | Expiry/Logout | + +* **Store**: Postgres (ORM). +* **Indexes**: `logs(instanceId, ts)` for fast reads; `instances(status)` for filters. +* **Retention**: set TTL on `logs`. + +## How to run it (and why) + +```bash +# start everything +./quickstart.sh +``` + +```bash +# run servers +mcx auth login # need to login to the current terminal session +mcx @modelcontextprotocol/server-filesystem # run a local mcp server +mcx --docker mcp/github-mcp-server -e GITHUB_PERSONAL_ACCESS_TOKEN=... # run a dockerized mcp server +``` + +```jsonc +// Add the MCP to Claude/Cursor +{ + "mcpServers": { + "filesystem": { + "command": "mcx", + "args": ["@modelcontextprotocol/server-filesystem"], + "env": { "MATHOM_URL": "http://localhost:5050" } + } + } +} +``` + +* **Why this design**: stdio is simple for local tools; Docker keeps deps clean; the proxy lets web clients talk to stdio servers. +* **Throughput**: log stream scales with lines; keep queues bounded so the UI doesn’t lag. + +## Hard parts and how they’re solved + +* Stdio vs web: a small proxy turns stdio into SSE/HTTP. + Why hard: many MCP servers only speak stdio; editors/tools may expect HTTP/SSE. + Cost: tiny extra latency and memory. + +* Fast log streams +Why hard: a noisy server can flood the UI and DB.Fix: use bounded queues/backpressure; batch writes; drop policy with a clear UI note. + +* Login without pain (in dev): OAuth via the dashboard; easy defaults in dev; lock it down for prod. + Why hard: real HTTPS and solid provider setup later. + +* Live logs that don’t stall: use SSE for streaming and save logs in the DB. + Need: TTL and flow control for very chatty servers. + +## Small tips + +* Use `mcx` instead of `npx` for a cleaner flow. +* `./quickstart.sh` gets you running fast. +* Keep sample configs for Claude/Cursor handy. + +## Speed and resource notes + +* **SSE** is light for one‑way log streams. +* **Host‑like networking** in dev keeps localhost simple. +* **Docker stdio** is cheaper than writing a full web server. + +## Limitations and next steps + +* **Lots of logs**: add TTL/compaction and a per‑instance size limit. +* **Teams**: add permissions and audit later. From 85db46731718dc040f3f988b2d135b29eb98b4d0 Mon Sep 17 00:00:00 2001 From: Hieu Vu Date: Tue, 23 Sep 2025 11:23:53 +0700 Subject: [PATCH 2/2] chore: revise mathom.md for improved clarity and detail Updated the documentation to enhance clarity and detail about the features and architecture of the mathom application, including improvements in the description of the dashboard, runner, CLI, and data management. --- mathom.md | 129 +++++++++++++++++++++++++++--------------------------- 1 file changed, 64 insertions(+), 65 deletions(-) diff --git a/mathom.md b/mathom.md index c62d3785..2447c85a 100644 --- a/mathom.md +++ b/mathom.md @@ -19,73 +19,83 @@ toc: true ## What the app does -* Start servers with `mcx` (from a name, an npm package, or a Docker image). You can pass extra args and env. -* See logs live in a simple dashboard (dark/light). -* Sign in with OAuth (easy in dev, tighten later for prod). -* Use with editors like Claude Desktop or Cursor by setting `command: "mcx"` and `env.MATHOM_URL`. +* Start servers with `mcx` (from a name, an npm package, or a Docker image). +* Live Monitoring: View real-time logs and status in a modern web UI (supports dark/light themes). +* Authentication: Sign in via OAuth2 (with dev-friendly defaults) to secure the dashboard and server endpoints. +* Editor Integration: Easily use local servers with tools like Claude Desktop or Cursor by configuring them to call mcx and pointing MATHOM_URL to your local Mathom instance. ## How the parts fit together ```mermaid flowchart LR - subgraph "Dashboard (web)" - D_API["API for run & logs"] - D_UI["Pages: logs • status"] + subgraph "Dashboard (Next.js Web UI)" + D_API["API (HTTP routes for run & logs)"] + D_UI["UI Pages: Logs, Status, etc."] end - subgraph "Runner (podrift, Go)" - R_SPAWN["Start process (stdio)"] - R_LOGS["Read stdout/stderr"] + subgraph "Runner (Podrift, Go)" + R_SPAWN["Spawn server process"] + R_LOGS["Capture stdout/stderr"] end - subgraph "CLI (mcx)" - C_LOCAL["mcx "] + subgraph "CLI (mcx, TypeScript)" + C_LOCAL["mcx "] C_DOCKER["mcx --docker "] end subgraph "Data (PostgreSQL)" - T_INST["instances"] - T_LOGS["logs"] + T_INST["instances table"] + T_LOGS["logs table"] end - subgraph "Proxy (stdio to web)" - P["stdio ⇄ SSE/HTTP"] + subgraph "Proxy (stdio ⇄ web adapter)" + P["mathom-proxy (mcp-proxy)"] end - C_LOCAL -->|run| D_API - C_DOCKER -->|run| D_API - D_API <-->|HTTP :9090| R_SPAWN - R_LOGS -->|live stream| D_API + C_LOCAL -->|start server| D_API + C_DOCKER -->|start server| D_API + D_API -->|HTTP call| R_SPAWN + R_SPAWN -->|if Docker| P + P -->|launch| R_SPAWN + R_LOGS --> D_API D_UI -->|query| T_INST - D_UI -->|tail| T_LOGS - - C_DOCKER --> P - P --> R_SPAWN + D_UI -->|stream logs| T_LOGS ``` -*In short:* `mcx` asks the API to start a server. The Go runner starts it and streams logs back. If the server is inside Docker and only speaks stdio, the proxy exposes a small web endpoint. +The system is composed of several cooperating components: +- Dashboard (Next.js Web App): Provides a UI and HTTP API (on port 5050) for starting/stopping servers and streaming logs. -## How it works +- CLI (mcx - TypeScript): User-facing CLI to send commands to the dashboard API. For example, mcx my-server or mcx --docker some-image triggers the dashboard to launch that server. -* Dashboard – Next.js app with API routes for run and logs. Stores data in Postgres. -* Runner – Go program that starts the server process, watches it, and sends logs. -* TypeScript CLI: +- Runner (“Podrift” – Go): A background process that the dashboard API calls to actually spawn processes or containers. It acts as a supervisor: starting the MCP server process (either directly or in Docker), streaming back its stdout/stderr logs, and tracking its status. - * `mcx ` – run a local stdio server (by name or npm package). - * `mcx --docker -e KEY=VAL` – run the server in Docker and add the proxy if needed. -* `quickstart.sh` builds/starts everything. -* `docker-compose.yaml` links the parts for dev. -### Higligted design patterns being used: +- Proxy (Rust mcp-proxy): A lightweight adapter that connects stdio-only servers to the web. It runs the server as a subprocess and exposes an HTTP/SSE endpoint for clients -* Supervisor (runner): the Go runner starts the server process, watches it, and reports status. Goal: keep the child process healthy and its logs flowing. +- PostgreSQL Database: Stores persistent data – instances (running server metadata), log lines, and user sessions. -* Adapter / Proxy: turns a stdio‑only server into something web clients can talk to (SSE/HTTP). Useful when the tool was never built for the network. +*In short:* `mcx` asks the API to start a server. The Go runner starts it and streams logs back. If the server is inside Docker and only speaks stdio, the proxy exposes a small web endpoint. + +## How it works -* Command (CLI): mcx has clear subcommands/handlers (e.g., run, --docker). Each command maps to one action. Keeps CLI code small and easy to test. +- Dashboard & API: The Next.js app serves both the user interface and RESTful endpoints. For example, there’s an POST /instances/run endpoint that the CLI calls to start a new server instance. The dashboard also provides an SSE or similar streaming endpoint for logs (e.g. GET /instances/:id/logs/stream) so the UI can tail logs live. +- CLI (mcx): This thin client finds the local Mathom API (by default at localhost:5050) and sends the appropriate request. It supports different subcommands or flags. +- Go Runner (Podrift): The runner is a Go service responsible for actually executing the server processes. On startup, the dashboard API connects to the runner (likely via HTTP or a local RPC) and instructs it to start a process or container: + - For a local process, Podrift uses os/exec to spawn the command (e.g., running the npm package via npx or launching a binary). It pipes the stdout/stderr. + - For a Dockerized server, Podrift uses Docker Engine APIs to run the specified image. It doesn’t run the image’s default entrypoint directly; instead it wraps it with the mathom-proxy if needed (more on that below). + - Podrift also monitors the process/container status (running, exited, etc.) and reports changes back to the dashboard (which updates the UI and instance status accordingly). -* Observer (logs via SSE): the UI subscribes to log events from the server and updates the view when new lines arrive. Push, not polling. +- Data Storage: The Postgres database keeps track of: + - Instances: Each launched server instance has a record (with fields like id, name, command or image, env vars, status, PID or container ID, start time, etc.). + - Log Lines: Each stdout/stderr line (or batch of lines) from servers is saved with a timestamp and severity level. These are keyed by instance ID for retrieval + - Sessions: User login sessions (OAuth provider info, expiry, etc.) to manage authentication. +By storing logs and instances, Mathom can show historical logs, persist server configurations, and implement cleanup policies (e.g., auto-stopping or removing old instances, log retention via TTL). +### Design Patterns at Play +- Supervisor Pattern: The runner (Podrift) acts as a supervisor that spawns child processes (the MCP servers) and keeps them running. It handles restarts or shutdowns and captures their output. This ensures the MCP servers don’t need to manage their own daemonization. +- Adapter/Proxy Pattern: The mathom-proxy (built on mcp-proxy) is essentially an adapter that converts a standard input/output interface into a network service. If an MCP server wasn’t originally built with HTTP or SSE, this proxy adds that capability externally without modifying the server itself. +- Command Pattern (CLI): The mcx CLI is structured around clear subcommands and flags which map to distinct actions (run local, run docker, auth, etc.). This separation makes the CLI logic easy to extend and test. +- Observer Pattern (Logs via SSE): The dashboard UI “observes” log events through a streaming endpoint. New log entries are pushed to the UI in real-time as they are produced, rather than the UI polling repeatedly for updates. This push-based log feed is implemented with SSE for efficiency. ### Key flows @@ -112,7 +122,7 @@ sequenceDiagram API-->>UI: log lines ``` -* **What’s hard**: keeping the child process up, handling fast logs, knowing when the server is “ready”. +**Challenges**: For local processes, one tricky aspect is determining when the server is actually ready to accept requests. The runner may start a process, but the MCP server might take a few seconds to initialize. Mathom’s strategy is to treat the process as “running” as soon as it’s spawned and piping output. It’s up to the user or client to handle initial readiness (often the server prints a “listening” message, which you can see in logs). Another issue is handling very fast or noisy log output without dropping lines or overwhelming the UI/DB – Mathom addresses this with backpressure and batching in the log pipeline (more on this later). #### 2) Run a Docker server that needs the web @@ -135,7 +145,7 @@ sequenceDiagram UI-->>API: subscribe to logs ``` -* **Note**: the proxy adds a little overhead but lets web clients work. +**Note**: the proxy adds a little overhead but lets web clients work. ## Data and CRUD map @@ -145,11 +155,11 @@ sequenceDiagram | `LogLine` | `id`, `instanceId`, `ts`, `level`, `text` | Runner | UI/history | — | TTL job | | `Session` | `userId`, `provider`, `createdAt`, `expiresAt` | Auth callback | API (auth check) | Auth middleware | Expiry/Logout | -* **Store**: Postgres (ORM). -* **Indexes**: `logs(instanceId, ts)` for fast reads; `instances(status)` for filters. -* **Retention**: set TTL on `logs`. +**Store**: Postgres (ORM). +**Indexes**: `logs(instanceId, ts)` for fast reads; `instances(status)` for filters. +**Retention**: set TTL on `logs`. -## How to run it (and why) +## How to run ```bash # start everything @@ -176,29 +186,18 @@ mcx --docker mcp/github-mcp-server -e GITHUB_PERSONAL_ACCESS_TOKEN=... # run a d } ``` -* **Why this design**: stdio is simple for local tools; Docker keeps deps clean; the proxy lets web clients talk to stdio servers. -* **Throughput**: log stream scales with lines; keep queues bounded so the UI doesn’t lag. - ## Hard parts and how they’re solved -* Stdio vs web: a small proxy turns stdio into SSE/HTTP. - Why hard: many MCP servers only speak stdio; editors/tools may expect HTTP/SSE. - Cost: tiny extra latency and memory. - -* Fast log streams -Why hard: a noisy server can flood the UI and DB.Fix: use bounded queues/backpressure; batch writes; drop policy with a clear UI note. - -* Login without pain (in dev): OAuth via the dashboard; easy defaults in dev; lock it down for prod. - Why hard: real HTTPS and solid provider setup later. - -* Live logs that don’t stall: use SSE for streaming and save logs in the DB. - Need: TTL and flow control for very chatty servers. - -## Small tips - -* Use `mcx` instead of `npx` for a cleaner flow. -* `./quickstart.sh` gets you running fast. -* Keep sample configs for Claude/Cursor handy. +- Turning STDIO into a Web API: +Many MCP servers only communicate through stdin/stdout. Mathom solves this by using a lightweight Rust mcp-proxy that translates STDIO into HTTP/SSE. This allows any tool—regardless of whether it was built with networking support—to be accessed by web clients or editors with minimal latency overhead. +- Dynamic Docker Wrapping: +To run stdio-only servers inside Docker, Mathom dynamically creates a wrapped container. Podrift overrides the container’s entrypoint so that the mathom-proxy binary starts first and then launches the original MCP server as a child process. This gives the container an HTTP/SSE endpoint without modifying the original image. +- Log Volume and Speed: +Noisy servers can generate huge log streams. Mathom uses backpressure and batching when forwarding logs to the dashboard via SSE. The proxy or runner can drop or batch lines when the UI or database cannot keep up, ensuring the UI stays responsive without losing critical log history. +- OAuth2 in Local Development: +Local OAuth flows are tricky because they usually require HTTPS callbacks. Mathom provides developer-friendly defaults that allow http://localhost redirects, so mcx auth login can open a browser, complete the OAuth handshake, and issue a CLI token seamlessly. +- Container Lifecycle Management: +To avoid wasting resources, Podrift automatically stops idle containers after a configurable timeout (about 180 seconds by default). Stopped containers are not removed, so they can be restarted quickly when the next request arrives—similar to “scale-to-zero” serverless platforms but running locally. ## Speed and resource notes