Skip to main content
Glama
README.md
# πŸ—οΈ Passepartout

**The master key for piloting self-hosted WordPress from any AI client.**

Passepartout is a [Model Context Protocol](https://modelcontextprotocol.io) (MCP)
server that lets an AI assistant β€” Claude Desktop today, any MCP client tomorrow β€”
read and write **content** (posts, pages, media, taxonomies) across **all of your
self-hosted WordPress sites**, using a simple shared key instead of WordPress
Application Passwords.

```
AI client  ──MCP──►  Passepartout  ──REST + X-Passepartout-Key──►  your WordPress sites
```

---

## Why this exists

If you run self-hosted WordPress and tried to connect it to an AI agent, you have
probably hit this wall:

- **Application Passwords silently disappear.** Behind a reverse proxy (most managed
  hosts), WordPress' internal `is_ssl()` check fails, the "Application Passwords"
  section vanishes from the profile screen, and nothing tells you why.
- **No OAuth for self-hosted.** The official tooling assumes WordPress.com-style
  OAuth, which self-hosted installs don't have out of the box.
- **The web connector is left out.** Most self-hosted MCP paths only work with a
  local desktop proxy.

The lock jammed. So Passepartout is a **skeleton key**: a tiny companion plugin
validates a secret you control (`X-Passepartout-Key`), and the server speaks the
plain WordPress REST API. No Application Passwords, no SSL-detection roulette, and
**one server drives every site you own**.

> Named after Jules Verne's tireless globe-trotting valet β€” and after the French
> *passe-partout*, the key that opens every door.

---

## Features

- πŸ”‘ **Custom-key auth** β€” bypasses Application Passwords entirely.
- 🌍 **Multi-site** β€” manage many WordPress installs from a single server; target one
  with a `site` argument.
- πŸ“ **Content tools** β€” list/get/create/update/delete **posts** and **pages**,
  manage **categories/tags**, **search**, and **upload media from a URL**.
- πŸ›Ÿ **Safe by default** β€” new posts/pages default to `draft`; destructive deletes are
  explicitly annotated and trash-first.
- 🧱 **Clean TypeScript** β€” MCP SDK, Zod-validated inputs, strict mode, MIT-licensed.

---

## Quick start

### 1. Install the companion plugin (per site)

Copy [`wp-plugin/passepartout-auth.php`](wp-plugin/passepartout-auth.php) to
`wp-content/mu-plugins/passepartout-auth.php` on each WordPress site, then add the
secret to `wp-config.php` (above the *"That's all, stop editing!"* line):

```php
define( 'PASSEPARTOUT_KEY', 'a-long-random-secret' );
// Optional β€” act as a specific user instead of the first administrator:
// define( 'PASSEPARTOUT_USER_ID', 1 );
```

> Use a long, random secret and serve the site over HTTPS.

### 2. Configure the server

Create a `sites.json` (see [`sites.example.json`](sites.example.json)):

```json
[
  { "id": "baair", "label": "BAAIR Solutions", "url": "https://baair.solutions", "key": "a-long-random-secret" }
]
```

The `key` here must match `PASSEPARTOUT_KEY` on that site.

### 3. Add it to your MCP client

Claude Desktop (`claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "passepartout": {
      "command": "npx",
      "args": ["-y", "passepartout-mcp@latest"],
      "env": {
        "PASSEPARTOUT_SITES_FILE": "/absolute/path/to/sites.json"
      }
    }
  }
}
```

Restart the client fully, then try: *"List the draft posts on baair."*

---

## Configuration

Pick **one** of three styles:

| Variable | Meaning |
| --- | --- |
| `PASSEPARTOUT_SITES_FILE` | Absolute path to a JSON array of sites (recommended). |
| `PASSEPARTOUT_SITES` | Inline JSON array of sites. |
| `WP_URL` + `WP_KEY` (+ `WP_SITE_ID`, `WP_LABEL`) | Single-site shorthand. |

Each site object: `{ "id", "url", "key", "label?" }`.

---

## Tools

| Tool | What it does | Destructive |
| --- | --- | --- |
| `wp_list_sites` | List configured sites (never exposes keys) | no |
| `wp_list_posts` / `wp_get_post` | Browse and read posts | no |
| `wp_create_post` / `wp_update_post` | Author and edit posts (draft by default) | no |
| `wp_delete_post` | Trash (or force-delete) a post | **yes** |
| `wp_list_pages` / `wp_get_page` | Browse and read pages | no |
| `wp_create_page` / `wp_update_page` | Author and edit pages | no |
| `wp_list_terms` / `wp_create_term` | Manage categories and tags | no |
| `wp_list_media` / `wp_upload_media_from_url` | Browse media; import an image by URL | no |
| `wp_search` | Search posts and pages | no |

---

## Security model

- The companion plugin authenticates a request **only** when `X-Passepartout-Key`
  matches `PASSEPARTOUT_KEY`, using a constant-time comparison.
- The server **never** logs or returns your keys; `wp_list_sites` omits them.
- Capabilities follow the WordPress role of the resolved user β€” use a dedicated
  machine user with the least privilege you need.
- Always serve sites over HTTPS. Rotate a key by changing it on both sides.

---

## Build from source

```bash
npm install
npm run build
node dist/index.js   # expects the env config above
```

Inspect tools interactively with the MCP Inspector:

```bash
npx @modelcontextprotocol/inspector node dist/index.js
```

---

## Roadmap

- [ ] Streamable HTTP transport + OAuth, for the **claude.ai web connector**
- [ ] Custom post types & ACF fields
- [ ] WP-CLI-over-SSH fallback adapter
- [ ] Structured (`outputSchema`) responses

Contributions welcome β€” see [CONTRIBUTING.md](CONTRIBUTING.md).

## License

MIT Β© Alexandre Bruneau (BAAIR)