Skip to main content
Glama
nouhouari
by nouhouari
README.md
# MPDS-MCP

Multi-Project Design System MCP Server — an HTTP server that exposes design tokens,
component specs, and validation utilities via a JSON REST API and an MCP JSON-RPC
endpoint. Multiple design-system projects can coexist, each with parent-child
inheritance for token resolution.

## Architecture

```
modules/
  mcp-server/   # Express HTTP server (this binary, port MCP_PORT)
  registry/     # Project CRUD (SQLite-backed)
  tokens/       # Token storage, overrides, semantic resolution
  components/   # Component spec storage and overrides
  patterns/     # Pattern library: patterns, variants, composition rules, layout guidelines
  validate/     # Color-pair + snippet validation
  preview/      # HTML showcase generator (generate_showcase MCP tool)
  db/           # Shared SQLite connection + migrations
```

## Inheritance model

Every project carries a nullable `parentId` (`registry`):

- A project with `parentId = null` is a **base** design system.
- A project with a `parentId` is a **child** that inherits from its base and may
  **override** individual tokens, component-spec fields, and patterns.
- Inheritance is **two levels deep only** — creating a child of a child is
  rejected with `MAX_INHERITANCE_DEPTH` (no grandchildren).
- A base cannot be deleted while it still has children (`BASE_HAS_CHILDREN`), and
  a project cannot be deleted while it still owns tokens (`PROJECT_HAS_TOKENS`).

```mermaid
graph TD
    Base["<b>Base design system</b><br/>parentId = null"]
    ChildA["<b>Child: Brand A</b><br/>parentId = Base"]
    ChildB["<b>Child: Brand B</b><br/>parentId = Base"]
    GC(["Grandchild<br/>❌ MAX_INHERITANCE_DEPTH"])

    Base --> ChildA
    Base --> ChildB
    ChildA -. rejected .-> GC

    classDef base fill:#1f4e5f,stroke:#0d2b36,color:#fff;
    classDef child fill:#2d6a4f,stroke:#1b4332,color:#fff;
    classDef bad fill:#7f1d1d,stroke:#450a0a,color:#fff,stroke-dasharray: 4 3;
    class Base base;
    class ChildA,ChildB child;
    class GC bad;
```

### How values resolve

`resolveTokens` (and the equivalent component/pattern resolvers) merge the base
layer with the child's own layer. For a **base** project every value is tagged
`source: "base"`; for a **child** the base values are inherited and any keys the
child defines win, tagged `source: "override"`:

```mermaid
flowchart LR
    subgraph BASE["Base project (parent)"]
        BA["color.primary = #1f4e5f"]
        BB["color.surface = #ffffff"]
        BC["radius.md = 8px"]
    end

    subgraph CHILD["Child project (own layer)"]
        OA["color.primary = #2d6a4f"]
    end

    BASE -->|"inherited (source: base)"| R
    CHILD -->|"overrides (source: override)"| R

    subgraph R["resolveTokens(child) → resolved set"]
        RA["color.primary = #2d6a4f  (override)"]
        RB["color.surface = #ffffff  (base)"]
        RC["radius.md = 8px  (base)"]
    end
```

Resolution order is: start from the parent's values, then apply the child's own
values by key — so a child key of the same name replaces the inherited one while
every un-overridden base key flows through unchanged.

## Running locally

### Prerequisites

- Node.js 20+ and npm
- A writable directory for the SQLite DB (or use `:memory:` for a transient session)

### Start (development)

```bash
npm ci
MCP_SECRET=dev DB_PATH=/tmp/mpds-dev.db MCP_PORT=3100 \
  npm --prefix modules/mcp-server run dev
```

Health check:

```bash
curl http://localhost:3100/health
# → {"status":"ok"}
```

### Start (production build)

```bash
npm ci
npm --prefix modules/mcp-server run build
MCP_SECRET=<secret> DB_PATH=/home/mpds/data/mpds.db MPDS_ENV=production \
  npm --prefix modules/mcp-server start
```

## Environment variables

| Variable     | Required             | Default       | Description                                      |
|--------------|----------------------|---------------|--------------------------------------------------|
| `MCP_SECRET` | Yes (production)     | `""` (no auth)| Bearer token — every `/api/*` request must carry it |
| `DB_PATH`    | Yes                  | —             | SQLite file path or `:memory:` (transient)        |
| `MCP_PORT`   | No                   | random port   | TCP port the server listens on                    |
| `MPDS_ENV`   | No                   | —             | Set to `production` to enforce `MCP_SECRET` check |
| `HOME`       | No (set by OS/Docker)| —             | Used for allowed DB path prefix validation        |

## API endpoints

All endpoints (except `/health`) require `Authorization: Bearer <MCP_SECRET>`.

Error envelope: `{ "error": { "code": "...", "message": "..." } }`

### Health

```
GET /health
```

### Projects

```
GET    /api/projects
POST   /api/projects           body: { id, name, parentId? }
GET    /api/projects/:id
DELETE /api/projects/:id
```

### Tokens

```
GET    /api/projects/:projectId/tokens
PUT    /api/projects/:projectId/tokens/:key     body: { value, description? }
DELETE /api/projects/:projectId/tokens/:key/override
```

### Components

```
GET    /api/projects/:projectId/components
GET    /api/projects/:projectId/components/:componentId
PUT    /api/projects/:projectId/components/:componentId/override
DELETE /api/projects/:projectId/components/:componentId/override
```

### Validation

```
POST   /api/validate/color-pair     body: { foreground, background }
```

### Patterns

```
GET    /api/projects/:projectId/patterns
POST   /api/projects/:projectId/patterns                       body: { id, name, category, description?, tags?, guidanceUrl? }
GET    /api/projects/:projectId/patterns/:patternId
PATCH  /api/projects/:projectId/patterns/:patternId            body: { name?, description?, tags?, guidanceUrl? }
DELETE /api/projects/:projectId/patterns/:patternId
```

### Pattern variants

```
GET    /api/projects/:projectId/patterns/:patternId/variants
POST   /api/projects/:projectId/patterns/:patternId/variants   body: { name, appliesAt, description? }
GET    /api/projects/:projectId/patterns/:patternId/variants/:variantId
PATCH  /api/projects/:projectId/patterns/:patternId/variants/:variantId
DELETE /api/projects/:projectId/patterns/:patternId/variants/:variantId
```

### Composition rules

```
GET    /api/projects/:projectId/composition-rules
POST   /api/projects/:projectId/composition-rules              body: { patternAId, patternBId, relation, guidance? }
DELETE /api/projects/:projectId/composition-rules/:ruleId
```

`relation` enum: `NESTING_ALLOWED` | `NESTING_FORBIDDEN` | `OVERRIDE_CAUTION` | `SIBLING_ONLY` | `EXCLUSIVE`

### Layout guidelines

```
GET    /api/projects/:projectId/layout-guidelines
POST   /api/projects/:projectId/layout-guidelines              body: { type, name, description?, data }
GET    /api/projects/:projectId/layout-guidelines/:guidelineId
PATCH  /api/projects/:projectId/layout-guidelines/:guidelineId
DELETE /api/projects/:projectId/layout-guidelines/:guidelineId
```

`type` enum: `breakpoints` | `spacing` | `grid` | `alignment` | `typography` | `animation`

See [`contracts/P1/`](contracts/P1/) and [`contracts/P2/`](contracts/P2/) for the full frozen OpenAPI specs.

## MCP configuration

The server exposes a JSON-RPC 2.0 endpoint at `POST /mcp` (requires the same `Authorization: Bearer <MCP_SECRET>` header as the REST API). Configure it in your MCP client as an HTTP server.

### Claude Desktop (`~/Library/Application Support/Claude/claude_desktop_config.json`)

```json
{
  "mcpServers": {
    "mpds": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "http://localhost:3100/mcp"],
      "env": {
        "MCP_REMOTE_HEADER_AUTHORIZATION": "Bearer <MCP_SECRET>"
      }
    }
  }
}
```

### Claude Code (`.claude/settings.json` in your project)

```json
{
  "mcpServers": {
    "mpds": {
      "type": "http",
      "url": "http://localhost:3100/mcp",
      "headers": {
        "Authorization": "Bearer <MCP_SECRET>"
      }
    }
  }
}
```

### GitHub Copilot (`.vscode/mcp.json` in your workspace)

```json
{
  "inputs": [
    {
      "type": "promptString",
      "id": "mpds_secret",
      "description": "MPDS MCP bearer token",
      "password": true
    }
  ],
  "servers": {
    "mpds": {
      "type": "http",
      "url": "http://localhost:3100/mcp",
      "headers": {
        "Authorization": "Bearer ${input:mpds_secret}"
      }
    }
  }
}
```

The `input` block causes VS Code to prompt for the secret once per session and store it in the system keychain. To skip the prompt, replace `${input:mpds_secret}` with the token literal (not recommended for shared workspaces).

### OpenCode (`opencode.json` in your project root, or `~/.config/opencode/opencode.json` globally)

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "mpds": {
      "type": "remote",
      "url": "http://localhost:3100/mcp",
      "enabled": true,
      "headers": {
        "Authorization": "Bearer <MCP_SECRET>"
      }
    }
  }
}
```

To avoid hard-coding the secret, reference an environment variable using OpenCode's `{env:VAR}` interpolation:

```json
{
  "$schema": "https://opencode.ai/config.json",
  "mcp": {
    "mpds": {
      "type": "remote",
      "url": "http://localhost:3100/mcp",
      "enabled": true,
      "headers": {
        "Authorization": "Bearer {env:MCP_SECRET}"
      }
    }
  }
}
```

### Available MCP methods

#### Read & validate

| Method | Description | Required params |
|---|---|---|
| `list_projects` | List all design-system projects | — |
| `get_tokens` | Resolve tokens for a project (with inheritance) | `projectId`, `category?` |
| `get_design_system` | Full token map + component specs | `projectId` |
| `get_component_spec` | Single component spec | `projectId`, `componentId` |
| `validate_color_pair` | WCAG contrast ratio | `fg`, `bg`, `context` (`normal`\|`large`\|`ui`) |
| `validate_token_pair` | Contrast check using token keys | `projectId`, `fgKey`, `bgKey`, `context` |
| `validate_snippet` | Lint HTML/JSX for a11y issues | `content`, `contentType?` (`html`\|`jsx`) |
| `create_guideline` | Create a design guideline | `id`, `title`, `body?`, `tags?` |
| `search_guidelines` | Full-text search guidelines | `query` |
| `propose_token_override` | Propose a token value change for review | `projectId`, `tokenKey`, `proposedValue`, `rationale?`, `agentId?` |
| `list_proposals` | List pending token proposals | `projectId` |

#### Write — projects & tokens

| Method | Description | Required params |
|---|---|---|
| `create_project` | Create a project | `id`, `name`, `parentId?` |
| `update_project` | Rename a project | `projectId`, `name` |
| `delete_project` | Delete a project | `projectId` |
| `list_tokens` | List all tokens for a project | `projectId`, `category?` |
| `get_token` | Get a single token | `projectId`, `key` |
| `create_token` | Create a token | `projectId`, `key`, `category`, `value`, `isSemantic?`, `semanticRef?` |
| `update_token` | Update a token value (OCC) | `projectId`, `key`, `version`, `value?`, `semanticRef?` |
| `set_token` | Set/override a token value | `projectId`, `key`, `value`, `version` |
| `delete_token` | Delete a token (OCC) | `projectId`, `key`, `version` |
| `delete_token_override` | Remove a child project override | `projectId`, `key` |

#### Write — components

| Method | Description | Required params |
|---|---|---|
| `create_component` | Create a component spec | `projectId`, `id`, `name`, `description?`, `props?`, `variants?`, `states?`, … |
| `update_component` | Update a component spec (OCC) | `projectId`, `componentId`, `version`, `name?`, `description?`, … |
| `delete_component` | Delete a component spec | `projectId`, `componentId` |

#### Write — pattern library

| Method | Description | Required params |
|---|---|---|
| `create_pattern` | Create a pattern | `projectId`, `id`, `name`, `category`, `description?`, `tags?` |
| `update_pattern` | Update a pattern | `projectId`, `patternId`, `name?`, `description?`, `tags?` |
| `delete_pattern` | Delete a pattern | `projectId`, `patternId` |
| `create_variant` | Add a variant to a pattern | `projectId`, `patternId`, `name`, `appliesAt`, `description?` |
| `update_variant` | Update a variant | `projectId`, `patternId`, `variantId`, `name?`, `appliesAt?` |
| `delete_variant` | Delete a variant | `projectId`, `patternId`, `variantId` |
| `create_composition_rule` | Define a pattern relationship | `projectId`, `patternAId`, `patternBId`, `relation`, `guidance?` |
| `delete_composition_rule` | Remove a composition rule | `projectId`, `ruleId` |
| `create_layout_guideline` | Create a layout guideline | `projectId`, `type`, `name`, `description?`, `data` |
| `update_layout_guideline` | Update a layout guideline | `projectId`, `guidelineId`, `name?`, `description?`, `data?` |
| `delete_layout_guideline` | Delete a layout guideline | `projectId`, `guidelineId` |

#### Showcase

| Method | Description | Required params |
|---|---|---|
| `generate_showcase` | Generate a self-contained HTML design system preview (color palette, component gallery, pattern library) | `projectId`, `title?` |

```bash
# Save and open the showcase locally
curl -s http://localhost:3100/mcp \
  -H "Authorization: Bearer <MCP_SECRET>" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"generate_showcase","params":{"projectId":"my-ds"}}' \
  | jq -r '.result.html' > /tmp/showcase.html && open /tmp/showcase.html
```

All requests follow JSON-RPC 2.0:

```bash
curl -s http://localhost:3100/mcp \
  -H "Authorization: Bearer <MCP_SECRET>" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"list_projects","params":{}}' | jq
```

## Running tests

Tests use an in-memory SQLite database and require no external services:

```bash
DB_PATH=:memory: MCP_SECRET=test npm test
```

## Docker

```bash
docker build -t mpds-mcp .
docker run -p 3100:3100 \
  -e MCP_SECRET=<secret> \
  -e DB_PATH=/home/mpds/data/mpds.db \
  -v $(pwd)/data:/home/mpds/data \
  mpds-mcp
```

See [`INSTALL.md`](INSTALL.md) for full setup and docker-compose instructions.