Skip to main content
Glama
Aniket-Sharma27

component-mcp-server

README.md
# design-system-mcp

A standalone [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that exposes design system component documentation to LLM clients. It reads Markdown files with YAML frontmatter from [`contexts/`](./contexts) and serves them over three MCP tools — `get_guidelines`, `list_components`, and `get_component_context` — so an LLM can look up the cross-cutting API/styling rules and a real component's props and events before generating code against it.

This guide covers deploying it standalone (no Docker) to a fresh AWS EC2 instance, running under [PM2](https://pm2.keithcirkel.co.uk/).

## 1. What this service is

- An MCP server, reachable over HTTP, that answers three questions for an LLM client: "what rules and practices apply to every component, regardless of which one I use?" (`get_guidelines`), "what components are documented?" (`list_components`), and "give me everything you know about component X" (`get_component_context`).
- `get_guidelines` takes no arguments and returns [`contexts/_guidelines.md`](./contexts/_guidelines.md) verbatim — three things in one call: component API conventions (event handling via CustomEvent/.detail, boolean prop syntax, element-vs-service APIs, item/group component pairs, theming), styling practices (e.g. dimension/background defaults, typography tokens being composite font shorthand values rather than standalone `font-size`/`font-weight`, no margin tokens — use padding/gap instead), and the full contextual (`--ion-cont-*`) design token catalog for color, spacing, sizing, border-radius, border-width, shadow, typography, motion, opacity, and layout. It exists so components get built against real API conventions and styling work never falls back to raw values (hex, px), literal (`--ion-lit-*`) tokens, or a token applied the wrong way. Some token entries are marked `(auto)` in the source file — their description was generated from the token name because the underlying CSS had no comment for it (mostly in the spacing/sizing/layout sections). The token *names* are exact and verified; treat `(auto)` *descriptions* as unverified until a human reviews them against Figma/source.
- `get_guidelines` merges what used to be two separate tools (`get_conventions` and `get_styling_rules`/`get_tokens`) — both were "call once per session before writing code," and splitting them only helps if some sessions need one but not the other, which isn't the case here. It's meant to be called once per session, before any component-specific lookup or styling work — `list_components`' and `get_guidelines`' own tool descriptions say so, so a well-behaved client calls it first without being told.
- Stateless: every `/mcp` request spins up a fresh MCP server + transport internally, so it's safe to run behind a load balancer with multiple concurrent clients and no session affinity required.
- Read-only against `contexts/` — there's no database, no write path; updating documentation means editing/adding a Markdown file in that directory (see [§9](#9-adding-or-updating-a-component)). `contexts/_guidelines.md` is the exception to "every file in `contexts/` is a component" — it's a shared reference doc, not a component, and is excluded from `list_components`' output and from `validate_component.py`'s per-component schema checks (any file starting with `_` is skipped by both).

## 2. Prerequisites

- **Node.js 18 or later** (LTS recommended — 20.x or newer). The build uses `NodeNext` module resolution and ES2022 target, which need a reasonably current Node.
- **npm** (ships with Node).
- **PM2**, installed globally on the instance:
  ```bash
  npm install -g pm2
  ```

## 3. Environment variables

Copy [`.env.example`](./.env.example) to `.env` and fill in real values — or export the same variables directly in the shell/systemd unit that starts PM2 (PM2 does not read `.env` files itself unless you load them into the shell first; see [§5](#5-starting-the-server-under-pm2)).

| Variable | Required | Purpose | How to set it |
|---|---|---|---|
| `API_KEY` | Yes — the server exits immediately at startup if this is unset | Bearer token that every `/mcp` request must present in `Authorization: Bearer <value>`. This is the only thing standing between the internet and your component docs, so it must be a real secret, not a memorable string. | Generate a random 32-byte hex token: `openssl rand -hex 32`. Store the result somewhere your deploy process can read it (a secrets manager, an untracked `.env`, or an EC2 instance's environment) — never commit it to git. |
| `PORT` | No (defaults to `3000`) | TCP port the Node process listens on. | Pick anything free on the instance; `3000` is fine if nothing else uses it. Combined with the reverse proxy in [§7](#7-tlshttps-required-before-any-public-exposure), this port should only ever be reached from `localhost`, never exposed directly to the internet. |

## 4. Install and build

From the repo root, in order:

```bash
npm install
npm run build
```

`npm run build` runs `tsc` and compiles `src/` to `dist/`. Confirm it produced output:

```bash
ls dist/
# expect: http.js  index.js  server.js  tools/
```

The HTTP entrypoint is `dist/http.js`.

## 5. Starting the server under PM2

Export the environment variables from [§3](#3-environment-variables) into the shell before starting PM2 — PM2 captures and persists the environment of the process that starts it, reusing it on subsequent `restart`/`resurrect`:

```bash
export API_KEY=$(cat /path/to/your/secret)   # or however you're sourcing it
export PORT=3000

pm2 start ecosystem.config.cjs
```

> The config file is `ecosystem.config.cjs` (not `.js`) — this package is `"type": "module"` in `package.json`, and PM2's config loader needs CommonJS, so `.cjs` is required, not a style choice.

Confirm it's actually running:

```bash
pm2 list
# expect a row for "design-system-mcp" with status "online"

pm2 logs design-system-mcp --lines 20
# expect: "design-system-mcp (Streamable HTTP) listening on port 3000"
```

### Surviving an instance reboot

Starting it once isn't enough — by default PM2 doesn't survive a reboot. Set up both of these, once:

```bash
pm2 save                # snapshots the current process list
pm2 startup              # prints an OS-specific command
# copy/paste and run the command pm2 startup prints, as root (e.g. via sudo) —
# this registers a systemd (or equivalent) service that resurrects
# "pm2 save"'s snapshot on boot
```

After any future change to what's running under PM2 (new app, changed env, etc.), re-run `pm2 save` so the snapshot stays current.

### Other PM2 commands you'll use

| Action | Command |
|---|---|
| Stop | `pm2 stop design-system-mcp` |
| Restart (drops in-flight connections) | `pm2 restart design-system-mcp` |
| Reload (zero-downtime) | `pm2 reload design-system-mcp` |
| Remove from PM2 entirely | `pm2 delete design-system-mcp` |
| Tail logs | `pm2 logs design-system-mcp` |

Logs also land in `logs/out.log` and `logs/error.log` in the repo root (gitignored), independent of `pm2 logs`.

## 6. Verifying it's working

A process showing "online" in `pm2 list` only proves Node is alive — it doesn't prove the service actually works. Do both of these:

**Health check** (confirms `contexts/` is present and readable, not just that the process exists):

```bash
curl -s http://localhost:3000/health
# expect: {"status":"ok","components":20}
# (component count will match whatever's actually in contexts/)
```

A `503` here means `contexts/` is missing or unreadable — investigate before moving on.

**A real end-to-end MCP call** — this exercises the actual tool logic, not just connectivity:

```bash
curl -s -X POST http://localhost:3000/mcp \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
      "name": "get_component_context",
      "arguments": { "componentName": "button" }
    }
  }'
```

Expect a `text/event-stream` response containing the button component's frontmatter, body, and raw content.

**`get_guidelines`** — same idea, no arguments:

```bash
curl -s -X POST http://localhost:3000/mcp \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/call",
    "params": {
      "name": "get_guidelines",
      "arguments": {}
    }
  }'
```

Expect the full contents of `contexts/_guidelines.md` back verbatim — API conventions (events/`.detail`, boolean props, `apiTypes`, `relatedComponents`, theming), styling practices (dimension/background defaults, typography-shorthand and no-margin-tokens notes), and the token catalog (border-radius/width, color, shadow, motion, opacity, sizing, spacing, layout, typography). Also confirm `list_components` doesn't include `_guidelines` in its output — it's a shared reference doc, not a component.

As a functional check, ask the connected coding assistant to build a form or similar UI and confirm: (a) it calls `get_guidelines` exactly once, without being reminded; (b) it doesn't set an explicit width/max-width/height/background-color on the outer container (those should come from the design system's own defaults per the styling practices in `_guidelines.md`, not be hardcoded); and (c) it uses the typography shorthand token and a padding/gap token for text sizing and vertical spacing — not raw `font-size`/`margin` values, and not a token named `margin-*` (there isn't one).

If you'd rather click through any of these interactively, point the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) at `http://localhost:3000/mcp` with the same bearer token.

Also confirm auth is actually enforced — this should return `401`, not the component data:

```bash
curl -s -o /dev/null -w "%{http_code}\n" -X POST http://localhost:3000/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"ping"}'
# expect: 401
```

## 7. TLS/HTTPS — required before any public exposure

**This server speaks plain HTTP only. It does not terminate TLS itself.** Do not point a public DNS record or security-group rule directly at port `3000` — put a reverse proxy in front that handles HTTPS, and only expose *that*.

The simplest option is [Caddy](https://caddyserver.com/): it gets you automatic HTTPS via Let's Encrypt with a few lines of config and no manual certificate renewal.

```bash
# Install Caddy (Debian/Ubuntu example — see caddyserver.com for other distros)
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update && sudo apt install caddy
```

Minimal `Caddyfile` (typically `/etc/caddy/Caddyfile`):

```
component-docs.yourdomain.com {
    reverse_proxy localhost:3000
}
```

Reload Caddy after editing:

```bash
sudo systemctl reload caddy
```

That's it — Caddy provisions and renews the Let's Encrypt certificate automatically as long as the domain's DNS points at this instance and ports 80/443 are open in your security group. Clients then connect to `https://component-docs.yourdomain.com/mcp`, never to the bare Node port.

## 8. Security notes

- **Rate limiting** is enabled: `/mcp` allows 100 requests/minute per IP; `/health` allows 300/minute (it's expected to be polled more often by uptime checks).
- **Bearer token auth is required** on all `/mcp` requests (`POST`, `GET`, `DELETE`) — a missing or incorrect `Authorization` header gets a `401`, never a peek at component data. `/health` does not require auth (it reveals no component content, just a readiness signal).
- The `API_KEY` value is the shared secret every consuming team needs. Distribute it out-of-band (a secrets manager, a password manager entry, a DM) — **never** in a committed config file, a Slack message that gets indexed, or a public repo. Rotate it by updating the deployed environment variable and restarting PM2 with `pm2 restart design-system-mcp --update-env` (after re-exporting the new `API_KEY` — see [§5](#5-starting-the-server-under-pm2)), then redistributing the new value to consumers.

  **`--update-env` is not optional here.** PM2 caches the environment a process was originally started with and reuses it on a plain `restart`, even if you've re-exported a new value in your shell first — the old key keeps working and the new one is rejected until you pass `--update-env` to force PM2 to reread the environment. Confirmed by testing directly: after re-exporting `API_KEY` and running plain `pm2 restart`, the *old* key still returned `200` and the *new* key returned `401`; re-running with `pm2 restart design-system-mcp --update-env` reversed both — old key `401`, new key `200`.

## 9. Adding or updating a component

1. Gather the component's full source material (Angular source, TS interfaces, design tokens, docs, Storybook stories, usage samples).
2. Feed [`extraction-prompt.md`](./extraction-prompt.md) to an LLM along with that material. It produces a single Markdown file (YAML frontmatter + body) in the format the rest of `contexts/` uses.
3. Save it as `contexts/<componentName>.md`.
4. **Run the validator — this is mandatory, not optional:**
   ```bash
   python3 validate_component.py contexts/<componentName>.md
   ```
   A new or updated component file is not considered done until this exits `0`. Fix every blocking issue it reports and re-run. Review warnings too — they're not always errors, but each should be a deliberate, explainable choice, not something overlooked.
5. No deploy or restart needed — `contexts/` is read at request time, so the change is live as soon as the file is saved on the running instance.

## 10. How teams connect once deployed

The server speaks MCP Streamable HTTP at `POST https://<your-deployed-host>/mcp` (through the reverse proxy from [§7](#7-tlshttps-required-before-any-public-exposure)), authenticated with the shared bearer token from [§8](#8-security-notes).

### Claude Code

```bash
claude mcp add --transport http component-docs https://<your-deployed-host>/mcp \
  --header "Authorization: Bearer <API_KEY>"
```

Or directly in `.mcp.json`:

```json
{
  "mcpServers": {
    "component-docs": {
      "type": "http",
      "url": "https://<your-deployed-host>/mcp",
      "headers": {
        "Authorization": "Bearer <API_KEY>"
      }
    }
  }
}
```

### opencode

```json
{
  "mcp": {
    "component-docs": {
      "type": "remote",
      "url": "https://<your-deployed-host>/mcp",
      "headers": {
        "Authorization": "Bearer <API_KEY>"
      },
      "enabled": true
    }
  }
}
```

Replace `<your-deployed-host>` with the real domain from [§7](#7-tlshttps-required-before-any-public-exposure) and `<API_KEY>` with the real token from [§3](#3-environment-variables) — never commit either into a shared config file.

TDQS

A4.3/5.0

Scored across 2 tools

Disambiguation5/5

The two tools have clearly distinct purposes: one lists available components, the other retrieves detailed content for a specific component. There is no overlap or ambiguity.

Naming Consistency5/5

Both tools follow a consistent verb_noun pattern with snake_case (list_components, get_component_context), making the naming predictable and clear.

Tool Count3/5

With only two tools, the server feels minimal but still covers basic discovery and retrieval. However, for a server named 'component-mcp-server', a few more tools (e.g., search or create) might be expected, so the count is borderline.

Completeness2/5

The server only supports reading components (list and get). Missing create, update, and delete operations leave significant gaps for full component lifecycle management, limiting its usefulness for agents needing to modify components.

Maintenance

ActivityMaintained
ResponsivenessNo issues