Skip to main content
Glama
mwhobrey

wmcp

by mwhobrey
README.md
# wmcp

**MCP exposure infrastructure** — how to run Model Context Protocol servers so Cursor, Claude Desktop, and Claude Code can reach them from WSL, Windows, or macOS.

This repo is the transport/wiring layer, plus an optional **personas** server (`compose` / `list_prompts`). It also ships a tiny `example` server (`echo` / `ping`) so you can verify the plumbing. Bring your own tools and prompt packs.

**Requires Node.js ≥ 18.**

## What an MCP server is (short version)

An MCP server is a process that speaks [Model Context Protocol](https://modelcontextprotocol.io) — JSON-RPC messages describing **tools**, **prompts**, and/or **resources**. Clients (Cursor, Claude) call those tools during a chat.

The interesting part is not the tools themselves. It is **how the client connects**:

| Transport | Who speaks it | How it runs |
|-----------|---------------|-------------|
| **stdio** | Cursor (local), Claude Desktop | Client spawns `node dist/….js` and talks over stdin/stdout |
| **Streamable HTTP** | Cursor, Claude Desktop (URL mode) | One long-lived Node HTTP server; client POSTs to `http://host:port/<name>` |
| **Legacy SSE** | Claude Code CLI | Same HTTP server; `GET /<name>/sse` + `POST /<name>/message` |

`wmcp` implements all three from one codebase.

```
┌─────────────────┐     stdio      ┌──────────────────┐
│ Cursor / Claude │ ◄────────────► │ example-server   │  (per-process)
│   (local spawn) │                │ or stdio-bridge  │
└─────────────────┘                └──────────────────┘

┌─────────────────┐   HTTP/SSE     ┌──────────────────┐
│ Cursor / Claude │ ◄────────────► │   http-server    │──► example
│   (URL mode)    │                │   :8820          │──► personas (opt)
└─────────────────┘                └──────────────────┘
```

Typical setup when the real Node runtime lives in **WSL**:

1. Run `http-server` inside WSL (systemd user unit or `npm start`).
2. Point Windows/mac Cursor at `http://localhost:8820/example` (WSL localhost forwarding).
3. Or, for stdio-only clients on Windows: `wsl.exe … node dist/example-server.js`.

On **macOS / native Linux**, skip the `wsl.exe` path — run Node locally and point Cursor at `http://localhost:8820/<server>` or a direct `node dist/….js` stdio command.

## Quick start

```bash
git clone https://github.com/mwhobrey/wmcp.git
cd wmcp
npm install
npm run build
npm start                    # HTTP front door on :8820
curl http://localhost:8820/health
```

stdio smoke test (separate terminal):

```bash
node dist/example-server.js
# or: node dist/stdio-bridge.js example
```

### Wire Cursor

If `transport` is `"sse"` (the default in the example config), **start the HTTP server first** (`npm start`) and leave it running. Cursor connects to URLs; nothing is listening otherwise.

**Option A — paste a minimal entry** (safest; does not touch other MCP servers):

```json
{
  "mcpServers": {
    "example": {
      "url": "http://localhost:8820/example"
    }
  }
}
```

Add that under your existing `mcpServers` in Cursor's MCP config, then reload MCP and call `ping` on `example`.

For stdio instead of HTTP:

```json
{
  "mcpServers": {
    "example": {
      "command": "node",
      "args": ["/absolute/path/to/wmcp/dist/example-server.js"]
    }
  }
}
```

**Option B — generate configs from this repo** (WSL + Windows):

> Warning: `npm run sync:mcp` **overwrites** `~/.cursor/mcp.json` and, when `/mnt/c` is present, `C:\Users\<windowsUser>\.cursor\mcp.json`. Back up first if you already have other servers configured.

1. Copy and edit config:

   ```bash
   cp config/mcp-servers.example.json config/mcp-servers.json
   # set projectRoot, nodePath, wslUser, windowsUser
   ```

2. With the HTTP server running (for `"transport": "sse"`):

   ```bash
   npm run sync:mcp
   ```

3. Reload MCP in Cursor. Call the `ping` tool on the `example` server.

`transport` in `mcp-servers.json`:

- `"sse"` (default) → Cursor entries are `{ "url": "http://localhost:8820/example" }` — **http-server must be up**
- `"stdio"` → Cursor spawns Node (or `wsl.exe` on Windows) — no shared HTTP process needed

## Optional: personas server

Markdown prompt packs with hot reload. Tools:

| Tool | Purpose |
|------|---------|
| `compose` | Merge layers into one system prompt (see order below) |
| `list_prompts` | List loaded prompts by category |

Also exposes MCP **prompts** (`list` / `get`) for clients that use that capability.

**Compose merge order** (each layer optional except when `base` is left on):

1. `base` (`personas/base`) — omitted if `base: false`
2. `team` — repo/org policy card
3. `persona` — role
4. `skill` — mode
5. `script` — playbook

Sections are joined with `---`. Template args apply to every included layer.

**Enable on the HTTP front door:**

```bash
cp env.example .env   # then set WMCP_ENABLE_PERSONAS=1
# PERSONAS_DIR=/absolute/path/to/prompts   # optional; default ./prompts

npm start
# → http://localhost:8820/personas
```

Cursor URL entry:

```json
{
  "mcpServers": {
    "personas": {
      "url": "http://localhost:8820/personas"
    }
  }
}
```

**Or run stdio directly** (no `WMCP_ENABLE_PERSONAS` required):

```bash
npm run start:personas
# or: node dist/stdio-bridge.js personas
```

Sample prompts ship under `prompts/` (`personas/`, `skills/`, `scripts/`, `teams/`). Replace them with your own — edits hot-reload; no restart needed.

Frontmatter shape:

```markdown
---
name: engineer
description: Short description for list_prompts
category: persona
args:
  - name: stack
    description: Primary tech stack
    required: false
---

# Role: Engineer

{{stack}}Primary stack: {{stack}}.{{/stack}}
```

Template syntax: `{{var}}`, `{{var|default}}`, and `{{var}}…{{/var}}` (block omitted when unset).

## Adding your own MCP server

1. Create `src/my-server.ts` — construct an MCP `Server`, register tools, expose `getServer()`.
2. Register a factory in `src/registry.ts`.
3. Add a `servers.my` entry in `config/mcp-servers.json` with `"script": "my-server.js"`.
4. `npm run build && npm run sync:mcp` (and restart `http-server` if using URL mode).

The HTTP and stdio layers stay unchanged. They only need a name → `() => Server` factory.

## Layout

```
src/
  example-server.ts   # reference MCP tools (echo, ping)
  personas-server.ts  # optional compose / list_prompts
  registry.ts         # name → factory map (+ env toggles)
  http-server.ts      # Streamable HTTP + legacy SSE multiplexer
  stdio-bridge.ts     # named stdio launcher
  env-flags.ts
  is-main-module.ts
prompts/              # sample prompt pack (swap for yours)
config/
  mcp-servers.example.json
scripts/
  sync-mcp-config.mjs
  fix-permissions.mjs
systemd/
  wmcp.service
```

## Environment

| Variable | Default | Purpose |
|----------|---------|---------|
| `MCP_PORT` | `8820` | HTTP listen port |
| `MCP_SSE_PING_MS` | `30000` | SSE keep-alive comment interval (`0` disables) |
| `WMCP_ENABLE_PERSONAS` | off | Register `personas` on the HTTP front door (`1`/`true`/`yes`/`on`) |
| `PERSONAS_DIR` | `./prompts` | Markdown prompt root (`personas/`, `skills/`, `scripts/`, `teams/`) |

See `env.example`. Load via `.env` in the project root (`dotenv`) or export in the shell / systemd unit.

## systemd (optional)

```bash
# edit User / paths in systemd/wmcp.service first
mkdir -p ~/.config/systemd/user
cp systemd/wmcp.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now wmcp
systemctl --user status wmcp
```

To enable personas under systemd, set `WMCP_ENABLE_PERSONAS=1` in `.env` (or `Environment=` in the unit).

## Design notes

- **One HTTP process, many logical servers** — routes by path prefix; each request gets a fresh MCP `Server` instance for Streamable HTTP (stateless).
- **Fail per server at boot** — a broken factory is skipped; others still serve.
- **Personas are optional** — off by default so the public repo stays a transport template; flip the env when you want compose/list_prompts on HTTP. Direct `npm run start:personas` always works.
- **Private prompt packs stay private** — point `PERSONAS_DIR` at an external directory, or replace `prompts/` locally and don't commit company content.

## License

MIT