mithra-mcp
by RuiciroRS
README.md
<div align="center">
# ☉ Mithra
**Your workspace as state an agent can reason over.**
A local-first command center for a one-person, multi-project workspace. It reads live git
state, Kanban boards, tasks, standups and production health — and exposes them two ways: as a
**GUI** you keep open all day, and as [**MCP**](https://modelcontextprotocol.io) tools any
client can query in natural language.
[](LICENSE)
· read-only · localhost only · no build step · Node 18+
</div>
---
```
npm start → the GUI: every repo in one window, with a real Claude terminal
npm run mcp → the MCP server: the same workspace as tools, over stdio
"which repos are stale?" → list_projects
"what should I attack first?" → next_actions
"is prod up?" → deploy_health
```
I run several projects at once — a SaaS, a B2C app, a landing site, a game project, and the
tooling that holds it together — as a single engineer. Context evaporates between sessions.
**Mithra** is what I built on Claude Code primitives to keep all of it in view.
Two surfaces, one workspace, **one config file**. The GUI is where I work; the MCP server is
how an agent reasons about the same state. Point it at your own folders with one JSON file and
it's yours — nothing about my workspace is baked into the code.
## The idea: bounded memory + retrieval, not a bigger prompt
Mithra is built on two convictions, each borrowed from work I admire.
**Bounded working memory — after [Hermes](https://nousresearch.com/) (Nous Research).** An agent
shouldn't drag an ever-growing transcript behind it. Mithra keeps a small, *frozen*
working-memory snapshot — a few hundred words of current focus, live decisions, and open loops —
injected at session start, with an alarm when it fills past ~80%. The bet: a tight, curated
memory beats an unbounded one that quietly rots.
But a bounded memory can't hold everything — and shouldn't try. That's what this repo is for.
**Instead of stuffing workspace state into the prompt, Mithra leaves it where it lives and
exposes it as queryable tools.** The model carries a small memory and *pulls live truth on
demand*. Bounded context, on-demand retrieval — the two halves of the same idea.
**Local-first — after [Odysseus](https://en.wikipedia.org/wiki/Odysseus).** Your workspace is
yours. Mithra reads your real filesystem from your machine: stdio transport, no network port,
no cloud, no data leaving the building (the one exception — a production health-check — is an
outbound ping to your own URLs). The command-center has no backend because it doesn't need one.
## Tools
| Tool | What it returns |
|------|-----------------|
| `list_projects` | Per-repo: branch, recent commits, uncommitted files, staleness, ahead/behind upstream |
| `get_board` | A project's Kanban board (parsed from Obsidian markdown) |
| `get_tasks` | Open tasks for a project from `TASKS.md`, flagging manual-only items |
| `daily_standup` | Today's commits since midnight + open-task counts, per project |
| `deploy_health` | HTTP ping (status + latency) to each production URL |
| `next_actions` | **What to attack first**, ranked — crosses focus priority + open tasks + repo signals (unpushed commits, uncommitted changes, staleness) and returns a prioritized list with the reason and a suggested task per project |
`next_actions` is the brain: it doesn't just report state, it weighs it. Focus priority
dominates; pending work (open tasks, unpushed commits, dirty trees, staleness) breaks ties and
surfaces a low-priority project that's quietly piling up. The model gets *a decision*, not a
dashboard to interpret.
Every response carries `sources`, so an answer can be checked instead of trusted:
```jsonc
// get_tasks("landing") → read from a file shared by every project
"sources": [{ "repo": null, "file": "TASKS.md", "via": "tasks", "match": ["landing"] }]
// next_actions() → one action, two origins
"sources": [
{ "repo": "landing", "file": null, "via": "git" },
{ "repo": null, "file": "TASKS.md", "via": "tasks", "match": ["landing"] }
]
```
It's an array because some answers cross two origins. `repo` is null when the content came from
a file shared across projects rather than from a repo, and `match` then names the declared config
filter that assigned that section to this project — see [Cross-repo ownership](#cross-repo-ownership).
Paths are workspace-relative, so output can be pasted into an issue without leaking a home directory.
## See it work in 30 seconds
No config, no repos of your own involved: `npm run demo` builds a throwaway workspace in your
temp folder — three fake projects with real git history, a `TASKS.md`, Kanban boards — and walks
every tool against it. This is verbatim output:
```console
$ npm run demo
☉ Mithra MCP — demo
building a throwaway workspace (3 fake repos, real git history)…
→ /tmp/mithra-demo-workspace
── list_projects ─────────────────────────────────────────────
"which repos are stale?"
web-app main · 1 dirty · 1d since last commit
chore: bump SDK to 2.4.0
landing main · 0 dirty · 2 ahead · 14d since last commit
copy: tighten the value prop above the fold
api main · 0 dirty · 2d since last commit
test: cover the token-reuse path
── next_actions ──────────────────────────────────────────────
"what should I attack first?"
1. web-app — priority #1 · 4 open task(s)
→ Wire the checkout webhook to the retry queue
⚑ 1 uncommitted file(s) · 1 manual task(s) of yours
src: web-app/git + TASKS.md ← web-app
2. landing — priority #3 · 2 open task(s) · unpushed work
→ Add the analytics pixel to the lead form
⚑ 2 unpushed commit(s) · untouched for 14d · 1 manual task(s) of yours
src: landing/git + TASKS.md ← landing
3. api — priority #2 · 1 open task(s)
→ Rate-limit the token endpoint per client, not per IP
src: api/git + TASKS.md ← api
── get_tasks ─────────────────────────────────────────────────
"what is open on the landing page?"
[Active] landing
- Add the analytics pixel to the lead form
- (you) Pick the headline variant for the A/B test (yours, manual)
src: TASKS.md ← landing
── get_board ─────────────────────────────────────────────────
"where is the web app right now?"
Ideas Usage-based pricing tier, Self-serve refunds
Next Retry queue for webhooks, Idempotency backfill
In progress Checkout hardening
Done Payment-intent migration, PSP sandbox parity
src: DemoVault/01_WebApp/Board.md ← 01_WebApp
── deploy_health ─────────────────────────────────────────────
"is prod up?"
● web-app 200 · 481ms · https://example.com
● landing 200 · 456ms · https://example.org
6 tools, read-only, over stdio. Every answer cites the repo and file behind it.
```
Note `landing`: priority #3, but it surfaces above `api` because two commits are sitting
unpushed and nobody has touched it in two weeks. That's `next_actions` weighing state instead
of reporting it. The demo folder is a temp directory — delete it whenever.
## Design notes
These were deliberate, and they're the interesting part:
- **Read-only by design.** This server never writes, commits, or touches secrets or prod.
Destructive actions belong behind a confirmation step in a GUI, not on an always-available
tool surface. An agent with broad write access to your real repos is a footgun; this draws
the line on purpose.
- **stdio transport, no network port.** JSON-RPC over stdio to a local client. Nothing is
exposed to the network.
- **Errors are visible, never swallowed.** If one repo fails to read, the rest still return and
the failed one carries an explicit `error` field. Failures surface; they don't hide behind an
empty result.
- **Pure data layer.** All workspace logic lives in `lib.js` with zero MCP coupling, so it's
testable without standing up a server (`npm run smoke`).
- **Zero workspace assumptions.** Everything user-specific lives in one gitignored config file;
the code knows nothing about any particular set of projects.
- **Answers cite their origin.** Every payload carries `sources` (see above). An agent reporting
cross-repo state should be auditable, not persuasive.
### Cross-repo ownership
When the same interface, task heading or board column shows up across several services, Mithra
doesn't guess which repo owns it — the config does. Each project is an explicit entry with its
`dir`, and `tasks.include` / `tasks.exclude` declare which sections of the shared files are
theirs. Two projects can point at the same folder and split by section.
That's a deliberate trade. Inferring ownership from code or name similarity buys convenience and
pays for it with confident wrong answers, and a confident wrong answer about who owns a contract
is expensive to unwind. Declared ownership is boring, hand-maintained, and checkable — which is
why every response reports the filter that claimed it, in `sources[].match`.
## Quick start
```bash
git clone https://github.com/RuiciroRS/mithra-mcp.git
cd mithra-mcp
npm install
cp mithra.config.example.json mithra.config.json # then edit it
npm run smoke # verifies the data layer against your workspace
npm start # the GUI, on http://127.0.0.1:7777
npm run mcp # the MCP server, on stdio
```
> **Sharing your screen?** `smoke` and `test:protocol` run against *your* workspace and print
> real task text, so they will happily show your private projects to a room. Use
> `npm run demo` instead — same six tools, throwaway workspace, nothing of yours in the output.
**Zero-config:** drop this folder *next to your repos* and skip the config file entirely —
Mithra defaults to `root: ".."` and **auto-scans the parent folder** for git repos.
## The GUI
`npm start` opens one local window over every repo — the surface I actually keep open.
Every screenshot below is the demo workspace (`npm run demo:ui`), so the projects,
commits and tasks on screen are fabricated. Nothing here is anyone's real work.
**Ask it what to do next, and it answers from the actual state of your repos** — commits,
staged files, task list, and the design docs sitting in your vault:

That is not a chat box with a system prompt. It is the Claude Code CLI in a pty, reading the
same workspace the MCP tools expose — so it can run `git` itself and cross the answer with
your board:

**Tasks** are parsed straight out of `TASKS.md`, and the ones only you can do are flagged:

**Designs** renders the specs in your vault next to the repo they belong to:

**Boards** are your Kanban files, read in place — no second source of truth:

What is in the window:
- **Project rail** — every project with its branch, uncommitted count and last commit, live.
- **A real embedded terminal.** Not a chat box: the actual Claude Code CLI, running in a pty
(`node-pty` + `xterm.js` over a WebSocket), already `cd`'d into the project you clicked.
Skill chips above it type `/code-review`, `/deep-research`… into the prompt.
- **Boards, tasks, docs, sessions** — the same reads the MCP tools expose, rendered.
- **One-click commit** for the small stuff that doesn't deserve a terminal.
- **Paste or drop an image** anywhere in the window. A terminal only carries text, so the
image is written to a temp file and its path typed into the prompt — the CLI reads it
from disk. Screenshots stop being a reason to leave the window.
Want to see it without configuring anything? `npm run demo:ui` opens the same window over the
throwaway demo workspace, on its own port, so nothing of yours is on screen.
**It binds to `127.0.0.1` and nothing else, on purpose.** That terminal launches a real Claude
CLI with your credentials; exposing it to a network would hand anyone on it a shell. There is
no auth layer because there is no remote surface to authenticate.
The GUI lives in [`ui/`](ui/) and shares `config.js` with the MCP server — one config file, two
surfaces. On Windows, [`ui/start.ps1`](ui/start.ps1) launches it as a chromeless Edge
`--app` window, and `ui/install-autostart.ps1` makes it come up at login.
## Configuration
Everything lives in **`mithra.config.json`** (gitignored — yours alone). Full reference in
[`mithra.config.example.json`](mithra.config.example.json).
| Key | Default | What it does |
|-----|---------|--------------|
| `root` | `".."` | Where your projects live (relative to this folder, or absolute). `MITHRA_DOCS` overrides it. |
| `tasksFile` | `"TASKS.md"` | Cross-project checklist at the root. Parsed as `## Status` → `### Project` → `- [ ] item`. |
| `manualTaskMarkers` | `["(you)"]` | A task containing one of these is flagged as your own manual action, not delegable. |
| `vault` | `null` | Optional Obsidian vault: `{ dir, sessionsDir, boardFile }`. Powers `get_board`. |
| `projects` | `"auto"` | `"auto"` scans `root`, or list them explicitly. |
GUI-only keys — the MCP server ignores them: `appName`, `lang` (`en`/`es`), `theme`, `host`,
`port`, `claudeBin` (`"auto"` finds it on PATH), `skills`, `docFiles`, `workingMemoryFile`,
`workingMemoryCap`.
### Explicit projects
```jsonc
"projects": [
{
"name": "My App",
"dir": "my-app", // folder under root
"type": "git", // "git" (uses git log) | "fs" (falls back to file mtime)
"deploy": "https://my-app.com", // optional — enables the health ping
"vault": "01_MyApp", // optional — folder in your vault
"priority": 1, // your focus order; null = outside the core order
"tasks": { "include": ["My App"], "exclude": [] }
}
]
```
### Wire it into an MCP client
`claude_desktop_config.json` (or any MCP client config):
```json
{
"mcpServers": {
"mithra": {
"command": "node",
"args": ["C:\\path\\to\\mithra-mcp\\index.js"],
"env": { "MITHRA_DOCS": "C:\\path\\to\\workspace" }
}
}
}
```
## Tests
```bash
npm run demo # throwaway workspace + a walk through all six tools
npm run demo:ui # the GUI over that same workspace, on port 7788
npm run smoke # data layer against your real workspace, per-tool pass/fail
npm run test:protocol # boots the server and drives it through a real MCP client
```
All three are workspace-agnostic: they pick their targets from config, so a fork runs them
without editing a line. `MITHRA_CONFIG=/path/to/other.json` points any of them at an alternate
workspace — that's how the demo runs without touching yours.
## Where this is going
The destination, no hedging: **a local-first agent that holds an entire one-person operation in
a memory small enough to reason over, retrieving live truth on demand instead of hoarding stale
context.** Concretely, next:
- **Writes behind confirmation.** `commit_and_push`, `create_task`, `move_card` — mutations the
model can *propose* and you approve in one step, never an always-on write surface.
- **More state, same shape.** CI status, error budgets, calendar, inbox — anything that answers
"what needs me right now" becomes another read-only tool.
- **The memory loop closes itself.** The working-memory snapshot stops being hand-curated: the
agent proposes what to promote, demote, and forget, with you as the editor.
- **The GUI and the tools converge.** The GUI already embeds the real Claude CLI; the next step
is wiring these tools into that terminal's own context, so they become how it thinks, not
just what it shows.
## Stack
Node (ESM) · [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk) · `zod`. No build step.
---
Built by **Ruiciro Rivera** as one piece of Mithra, a local-first agentic command-center on
Claude Code — [GitHub](https://github.com/RuiciroRS) ·
[LinkedIn](https://linkedin.com/in/ruiciro-rivera-serrano-b357b2153). Credits in
[CREDITS.md](CREDITS.md). Licensed [MIT](LICENSE).
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues