# Eddie Brain

The agentic intelligence layer for the Eddie Design System. Exposes an MCP server so AI coding tools (Claude Code, Cursor, etc.) can query component metadata, validate token usage, check design system health, and compose canonical UI patterns.

## What It Does

- **`eddie_get_component`** — Full component docs: props, slots, events, guidelines
- **`eddie_search`** — Search across components, tokens, and recipes
- **`eddie_get_token`** — Token value, tier, intent, and which components use it
- **`eddie_compose_recipe`** — Canonical composition patterns for a given UI intent (e.g. "login form", "destructive confirmation dialog")
- **`eddie_check_health`** — Health scores across tokens, naming, docs, coverage, accessibility, and consistency
- **`eddie_validate_file`** — Validate a file against Eddie conventions. Style/template sources (`.scss`, `.ts`, `.html`, …) are checked for token usage, naming, slot contracts, and accessibility. Style Dictionary token `.json` is checked against the theme-inheritance rule — a *variant* theme (`bfw-dark`) may declare colors and nothing else, because it inherits typography, shadow geometry, borders and motion from its parent
- **`eddie_suggest_fix`** — Suggest fixes for detected violations
- **`eddie_get_relationships`** — Component composition relationships

## Setup

### Prerequisites

- Node.js (same version as the monorepo)
- The `eddie-design-system` repo cloned locally

### 1. Install dependencies

From the repo root:

```bash
npm install
```

### 2. Build the package

```bash
cd packages/eddie-brain
npm run build
```

This runs `tsc` and copies the static knowledge base files (`components.json`, `tokens.json`, etc.) into `dist/`.

Verify the output:

```bash
ls dist/mcp/server.js
```

### 3. MCP registration is automatic

The repo's `.mcp.json` at the root handles registration for Claude Code:

```json
{
  "mcpServers": {
    "eddie-brain": {
      "command": "node",
      "args": ["packages/eddie-brain/dist/mcp/server.js"],
      "env": {
        "EDDIE_ROOT": "."
      }
    }
  }
}
```

Claude Code reads `.mcp.json` on startup when your working directory is inside `eddie-design-system`. The `EDDIE_ROOT: "."` env var tells the server where to find the design system files. No additional configuration needed.

### Rebuilding After Changes

Whenever `src/` or `data/` files change, rebuild and restart your editor session:

```bash
cd packages/eddie-brain
npm run build
```

## Remote (hosted) MCP over HTTP

The same server can run as a **remote, Streamable HTTP MCP** so projects can connect over a URL — no local install or repo checkout. The tool implementations are shared via `createEddieBrainServer()`; the transports differ:

- `src/mcp/server.ts` — stdio entrypoint (local; used by `.mcp.json`) + the shared `createEddieBrainServer()` factory and `loadBrainContext()`.
- `src/mcp/http.ts` — Streamable HTTP entrypoint. `handleNodeMcpRequest` (Node `http.Server`, for local testing) and `handleWebMcpRequest` (Fetch API, for Netlify/Workers/Deno/Bun). Both run **stateless**.
- `src/mcp/serverless.ts` + `scripts/bundle-serverless.js` — the Fetch handler, pre-bundled by esbuild into one self-contained file at `dist/serverless/mcp.mjs` and copied to `netlify/functions/mcp.mjs`. Netlify deploys it with `node_bundler = "none"` (ships it verbatim); its own bundler otherwise scans the monorepo's `.ts`/`.d.ts` and fails.

The knowledge graph is bundled into `dist/graph/` at build (`scripts/copy-graph.js`) so a disk-less deployment loads it via the `EDDIE_ROOT`-unset fallback in `loadBrainContext()`.

Test the HTTP server locally before deploying:

```bash
npm run build          # compiles + bundles the graph into dist/graph/
npm run mcp:http       # serves http://localhost:3939/mcp (uses the bundled dist/graph)

# List tools over HTTP:
curl -sS http://localhost:3939/mcp \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

See [`docs/ECOSYSTEM.md` → "Connect to Eddie's Brain (MCP)"](../../docs/ECOSYSTEM.md#connect-to-eddies-brain-mcp) for how consumer projects wire up the remote URL.

## CLI

The package also ships a standalone CLI:

```bash
# Health report for the whole system
node packages/eddie-brain/dist/cli/brain.js health

# Full scan (monitor + analyze)
node packages/eddie-brain/dist/cli/brain.js scan
node packages/eddie-brain/dist/cli/brain.js scan --component ed-button

# Validate tokens and naming in source files
node packages/eddie-brain/dist/cli/brain.js validate src/
node packages/eddie-brain/dist/cli/brain.js validate --fix

# Compose a pattern from components
node packages/eddie-brain/dist/cli/brain.js compose "destructive confirmation dialog"

# View audit log
node packages/eddie-brain/dist/cli/brain.js audit
node packages/eddie-brain/dist/cli/brain.js audit --since 2026-03-01
```

## The catalog (`/catalog.json`, `eddie_get_catalog`)

`init` also writes the **catalog** (#1887): the A2UI v1.0-shaped document a
renderer or composer reads, as opposed to the graph a human or a hand-writing
agent reads. Every node an agent may name, its props as JSON Schema, its slots
as child lists, and the Eddie tag it renders as under
`metadata.extensions.eddie_tag`.

```
.eddie-brain/catalog.json           default profile — every graph entry
.eddie-brain/catalog.realtime.json  realtime profile — page-composition vocabulary
```

Node names are UAX #31 identifiers, as A2UI v1.0 requires: `ed-r-stat-card`
becomes `StatCard`, page templates take a `Template` suffix
(`AppDashboardTemplate`), and a recipe that collides with a core component
takes a `Recipe` suffix (`SearchFormRecipe`). Any other collision fails the
build. Profiles are data in `src/catalog/profiles.ts`; the `realtime` profile
carries five composition roots (`DashboardPage`, `FormPage`, `ArticlePage`,
`ListingPage`, `HomePage`) with `allowedParents: ["Surface"]` and
`allowedChildren`, and realtime-ui's composition rules as `instructions`.

Served from the site root (`https://ds.bradfrost.com/catalog.json?profile=realtime`,
CORS-open) and over MCP by `eddie_get_catalog({ profile, format, component })`.
Two more profiles lift consumer vocabularies verbatim (#1896): `hub` (the
bf-brain homepage's 27 nodes) and `chameleon` (the resources site's 9). The
wire format a composer emits against these catalogs — message names, the
v0.8 → v1.0 rename table, and the reserved `metadata.extensions.eddie_*`
keys — is declared in [`docs/A2UI-EDDIE.md`](../../docs/A2UI-EDDIE.md).
Every profile also carries the built-in `Text` node (`eddie_kind: "text"`), the
only way words reach a surface, and an `antiPatterns` array — the graph's
`@antipattern` rules with their selectors — so a renderer can evaluate them on
the expanded tree (#1898). `contractComplete` is `false` until core components declare `allowedChildren`
and templates declare their regions (#1685, #1686). No timestamp in the
document on purpose: it is regenerated by `init` and gated by the CI
graph-freshness check, so it must be deterministic.

Three root fields (`profile`, `contractComplete`, `eddieVersion`) sit outside
A2UI's `catalog_definition.json`, which is `additionalProperties: false` at the
root; A2UI's own `assemble_catalog.py` accepts the document (it checks for a
valid Draft 2020-12 schema), a strict root validator will flag those three keys.

## Selection criteria and anti-patterns (`@useWhen`, `@notWhen`, `@antipattern`)

Two kinds of JSDoc tag land in the graph as *data* rather than prose (#1888):

- **`@useWhen` / `@notWhen`** on every component, recipe and page (#2051):
  the asks that should resolve there ("a dashboard, admin view, report, P&L")
  and the similar-looking asks that belong elsewhere, naming the alternative.
  Parsed into `guidelines.useWhen` / `notWhen`; `eddie_search` and
  `eddie_compose_recipe` rank on them, and the catalog carries them as
  `eddie_useWhen` / `eddie_notWhen`. Both rankers read an ask the same way
  (`src/text/term-match.ts`, #2062): whole words with plurals folded, function
  words and request verbs ("I need…", "show…") dropped, tag names matched by
  whole word, or at half weight from the front of one. A `@notWhen` counts against an entry
  only for the situation before its dash — never for the words describing the
  alternative, and never for a word the entry also claims for itself. Each
  term's contribution is then weighted by how rare the word is across the
  catalog (`src/text/term-weights.ts`, #2080): "byline", which one entry
  claims, counts over two and a half times as much as "page", which half of them do.
  The weights are derived from the loaded graph, never authored. `test/selection-criteria.test.ts` is the
  gate: it fails when any component lacks either, unless the component sits in
  that test's `EXEMPT` map with a reason, and it requires every prop with two
  or more enumerated values to name each value (`ENUM_EXEMPT` for the
  exceptions). `npm run docs:check` still runs the older, narrower form of the
  tags check, on page templates and composition recipes only.
- **`@antipattern <selector> — <reason> — fix: <fix>`** on the component that
  owns a structural rule (prefix `(warning)` to lower the severity). Parsed into
  `.eddie-brain/anti-patterns.json`; the selector grammar is deliberately small
  — `A > B` (child), `A B` (descendant), `*`, `[slot=name]`, `:not(tag)`,
  `:not([slot=name])` — and a selector outside it fails `init` with the
  component named. `AntiPatternValidator` (inside `eddie_validate_file`)
  reports each hit with the reason and the fix; `eddie_get_component` lists
  the rules from both sides of the selector; every catalog profile's
  `instructions` end with a generated Anti-patterns section.

## Development

```bash
npm run dev      # TypeScript watch mode
npm test         # Run tests
npm run health   # Quick health check
```

## Architecture

See [SPEC.md](./SPEC.md) for the full technical specification, including the knowledge graph schema, trust level model, health scoring categories, and implementation phases.
