discourse-mcp
# discourse-mcp (Cloudflare-friendly fork)
Fork of the official [discourse/discourse-mcp](https://github.com/discourse/discourse-mcp) that routes Discourse API traffic through **[impers](https://github.com/lexiforest/impers)** (curl-impersonate / curl_cffi-style TLS fingerprints).
Use this when the target forum sits behind **Cloudflare** (or similar) and the stock MCP gets `403 Just a moment...` from Node's native `fetch`.
| | Upstream `@discourse/mcp` | This fork |
|--|---------------------------|-----------|
| HTTP | `fetch` | `impers` + browser impersonate |
| Default fingerprint | — | `chrome120` (override with `--impersonate` / profile) |
| CF 403 | fails immediately | **delayed retry** (same profile, backoff) |
| Images | markdown `upload://` only | **`discourse_read_image`** returns MCP `image` content (base64) for vision models |
| HTTP rate limit | write tools ~1/s only | Optional **sliding window** on all HTTP (`rate_limit_*` profile fields) |
| libcurl-impersonate | — | impers resolve order; if missing, **`gh` download fallback** |
Requires **Node.js >= 24**.
---
## Install in Grok Build
### 1. Profile (auth)
Create `~/.grok/discourse-profile.json` (example for a User API key):
```json
{
"site": "https://www.example.com/",
"impersonate": "chrome120",
"auth_pairs": [
{
"site": "https://www.example.com/",
"user_api_key": "YOUR_USER_API_KEY",
"user_api_client_id": "discourse-mcp"
}
],
"read_only": false,
"allow_writes": true,
"log_level": "info",
"rate_limit_max": 15,
"rate_limit_window_ms": 60000,
"rate_limit_min_interval_ms": 200
}
```
### HTTP sliding-window rate limit
Measured on uscardforum (Cloudflare / edge):
| Observation | Value |
|-------------|--------|
| Burst until first **429** | **~20** sequential requests |
| Recovery after full burst | **~40s** of continued 429 on probes, then OK |
| Sustained 1 req / 0.8–1.2s | Still hits 429 after **~20** → **not pure RPS** |
| Sustained 1 rps in earlier run | Can stay green for short runs, but window still caps at ~20 |
So the dominant pattern is **~20 requests per rolling ~60s window**, not a fixed RPS ceiling. Client-side limit should be a **sliding window**.
Profile / CLI:
| Field | Meaning |
|-------|---------|
| `rate_limit_max` | Max requests in the window (`0` = off, default) |
| `rate_limit_window_ms` | Window length (default `60000`) |
| `rate_limit_min_interval_ms` | Optional min gap between requests |
uscardforum-safe defaults used locally: **15 / 60s** + **200ms** min interval (margin under the ~20 hard edge).
**Cross-process / all clients:** when rate limiting is enabled, timestamps are stored under a **shared cache directory** (not `~/.grok`):
| Platform | Default state dir |
|----------|-------------------|
| macOS / Linux | `~/.cache/discourse-mcp/rate-limit` (or `$XDG_CACHE_HOME/discourse-mcp/rate-limit`) |
| Windows | `%LOCALAPPDATA%/discourse-mcp/rate-limit` |
Override with profile `rate_limit_state_dir` or env `DISCOURSE_MCP_RATE_LIMIT_DIR`.
One file per site origin (`sha1(origin).json`) + `.lock`. Multiple Grok/Claude/Cursor MCP processes share the same window. If the file lock fails, the process falls back to in-memory limiting and logs an error.
Generate a User API key (no admin required):
```bash
npx -y github:Sakura-Byte/discourse-mcp generate-user-api-key \
--site https://www.example.com/ \
--save-to ~/.grok/discourse-profile.json
```
### 2. `~/.grok/config.toml`
```toml
[mcp_servers.discourse]
command = "npx"
args = [
"-y",
"github:Sakura-Byte/discourse-mcp",
"--profile",
"/Users/YOU/.grok/discourse-profile.json",
]
enabled = true
startup_timeout_sec = 120
```
> First launch may download npm deps and `libcurl-impersonate` (or use `gh` if GitHub API is rate-limited). Prefer a longer `startup_timeout_sec`.
### 3. Refresh MCP
- New Grok session, or `/mcps` → `r` refresh
- Check: `grok mcp doctor discourse`
---
## Install in other MCP clients
### Claude Desktop / Cursor-style JSON
```json
{
"mcpServers": {
"discourse": {
"command": "npx",
"args": [
"-y",
"github:Sakura-Byte/discourse-mcp",
"--profile",
"/absolute/path/to/profile.json"
]
}
}
}
```
### Local clone
```bash
git clone https://github.com/Sakura-Byte/discourse-mcp.git
cd discourse-mcp
npm install # runs prepare → tsc build
node dist/index.js --profile /path/to/profile.json
```
Grok / client:
```toml
[mcp_servers.discourse]
command = "node"
args = ["/ABS/PATH/discourse-mcp/dist/index.js", "--profile", "/path/to/profile.json"]
enabled = true
startup_timeout_sec = 60
```
---
## libcurl-impersonate
Resolution order (impers native):
1. `LIBCURL_IMPERSONATE_PATH`
2. `LIBCURL_PATH`
3. Common install paths
4. GitHub download into `~/.cache/impers/libcurl-impersonate/{platform}-{arch}/`
5. System libcurl (no impersonate)
If step 4 fails (e.g. API rate limit), this fork tries **`gh`**:
| Situation | Behaviour |
|-----------|-----------|
| `gh` installed + logged in | `gh release download` into the same cache dir |
| `gh` installed, not logged in | Interactive `gh auth login` if TTY; else print instructions |
| no `gh` | Install/login hints + manual extract path |
```bash
brew install gh && gh auth login # recommended once
```
Override: `DISCOURSE_MCP_REQUIRE_IMPERSONATE=0` allows starting without impersonate (CF sites will usually fail).
---
## CLI / profile flags (same as upstream + extras)
| Flag / field | Meaning |
|--------------|---------|
| `--profile path.json` | Load config from file |
| `--site URL` | Tether to one site |
| `--auth_pairs '[...]'` | Per-site API / User API keys |
| `--allow_writes --read_only=false` | Enable write tools |
| `--impersonate chrome120` | Browser fingerprint (also in profile) |
| `--log_level debug` | Verbose HTTP |
### Reading images (for vision models)
MCP tools should return images as **Image Content** blocks (not only a base64 string inside JSON text):
```json
{
"content": [
{ "type": "text", "text": "{...metadata...}" },
{ "type": "image", "data": "<base64>", "mimeType": "image/jpeg" }
]
}
```
Use **`discourse_read_image`**:
```json
{ "post_id": 8564338, "max_images": 2 }
```
```json
{ "url": "upload://q9cFN6AzFPTkcn4CVcNmkuXNDTi.jpeg" }
```
```json
{ "url": "https://asset-cdn.example.com/.../photo.jpeg" }
```
Typical flow: `discourse_read_post` → see `` in `raw` → `discourse_read_image` with `post_id` or that `upload://` URL.
See upstream docs for the rest of the tool list (search, read topic/post, create post/topic, drafts, …).
---
## Upstream
Based on [discourse/discourse-mcp](https://github.com/discourse/discourse-mcp) (MIT).
Cloudflare / impers / `gh` bootstrap changes live in this fork only.
TDQS
Scored across 13 tools
Each tool targets a distinct resource or operation: users, chat messages, drafts, queries, topics, posts, images, and site selection. Even search and filter_topics are clearly differentiated by keyword vs. structured query, and read_topic/read_post operate at different granularities.
Tools follow a discourse_verb_noun pattern, but use multiple verbs for retrieval (list, get, read) without a strict rule. For instance, get_chat_messages returns a collection while list_user_posts uses 'list', and read_topic/read_post use 'read' for content-heavy resources.
13 tools is well-scoped, covering users, content, search, queries, and site management without unnecessary overlap or bloat. The count aligns well with the server's apparent purpose as a comprehensive Discourse read/query interface.
The set covers read/search/query needs comprehensively, including topics, posts, images, users, chat, and Data Explorer. Minor gaps exist such as no category/tag listing tool and no write operations, but filter_topics can work around category/tag needs, making the limitations workable.