Skip to main content
Glama
azickri

Repliz MCP Server

by azickri
README.md
# Repliz MCP Server

A hosted [Model Context Protocol](https://modelcontextprotocol.io) server for the
**Repliz Public API**. It gives an AI assistant 105 tools for managing a Repliz
workspace in plain language: listing and replying to comments, scheduling posts,
handling DMs, browsing content and stats, researching Threads, managing
automations and templates, and more.

Users connect by pointing their MCP client at one URL — then either click
"Connect" and sign in with their Repliz Access Key and Secret Key (OAuth), or
send those keys as headers. There is nothing for them to install, build, or
keep updated.

```
https://mcp.repliz.com/mcp
```

This repository is the server that serves that URL.

---

## How it works

One process serves everyone. It holds **no credentials of its own** — each
request carries the caller's keys, which are verified against the Repliz API and
bound to that caller's session. A session owns its own API client, so no two
users ever share one.

```
MCP client ──(URL + keys)──► this server ──(Basic auth)──► api.repliz.com
```

Users authenticate in one of three ways. Keys come from the Repliz dashboard
under **Settings > API**.

**OAuth** — for Claude.ai, ChatGPT, and any client with a "Connect" button.
The user adds `https://mcp.repliz.com/mcp` as a connector, a sign-in page opens,
and they paste their keys there once. Enabled when `REPLIZ_OAUTH_SECRET` is set;
see [OAuth](#oauth).

**Headers** — for API integrations (the Anthropic Messages API, the OpenAI
Responses API) and clients with custom-header support (Cursor and most desktop
clients):

```http
Authorization: Basic base64(accessKey:secretKey)
```

```http
X-Repliz-Access-Key: <accessKey>
X-Repliz-Secret-Key: <secretKey>
```

---

## Running it

Requires **Node.js 18+** (Node 20.12+ to auto-load `.env`).

```bash
npm ci
npm run build
cp .env.example .env
npm start
```

The endpoint is `POST/GET/DELETE /mcp`, plus `GET /health` for monitoring.
With OAuth on, the server also answers `/.well-known/oauth-protected-resource/mcp`,
`/.well-known/oauth-authorization-server`, `/register`, `/authorize`, `/token`,
and `/oauth/login` (the sign-in form). Serve it over HTTPS in production, behind
a reverse proxy or platform TLS.

### Configuration

Everything is optional; the defaults suit a 1 GB VPS.

| Variable                         | Default                  | Description                                                        |
| -------------------------------- | ------------------------ | ------------------------------------------------------------------ |
| `PORT`                           | `3000`                   | Port to listen on                                                  |
| `REPLIZ_BASE_URL`                | `https://api.repliz.com` | Repliz API base URL                                                |
| `REPLIZ_MAX_SESSIONS`            | `300`                    | Cap on concurrent sessions; beyond it new connects get `503`       |
| `REPLIZ_SESSION_IDLE_MINUTES`    | `30`                     | Close a session after this long without a request                  |
| `REPLIZ_RATE_LIMIT_PER_MINUTE`   | `120`                    | Requests allowed per IP per minute                                 |
| `REPLIZ_TRUST_PROXY`             | `loopback`               | Which proxies may set `X-Forwarded-For`                            |
| `REPLIZ_REQUEST_TIMEOUT_SECONDS` | `30`                     | Timeout for outbound calls to the Repliz API                       |
| `REPLIZ_AUTH_CACHE_MINUTES`      | `5`                      | How long a verified credential pair is cached                      |
| `REPLIZ_MAX_UPLOAD_MB`           | `100`                    | Largest file `repliz_upload_file` will stream through the server    |
| `REPLIZ_ALLOWED_ORIGINS`         | —                        | Browser origins allowed to call `/mcp`; unset means no CORS headers |
| `REPLIZ_UPLOAD_HOSTS`            | R2 + Repliz storage      | Hosts accepted as presigned-upload targets                         |
| `REPLIZ_OAUTH_SECRET`            | —                        | Turns OAuth on; seals every token. At least 32 characters          |
| `REPLIZ_PUBLIC_URL`              | —                        | Public origin, e.g. `https://mcp.repliz.com`; required with OAuth  |
| `REPLIZ_OAUTH_ACCESS_TOKEN_MINUTES` | `60`                  | Access token lifetime                                              |
| `REPLIZ_OAUTH_REFRESH_TOKEN_DAYS`   | `30`                  | How long an unused connection stays signed in                      |

There is deliberately **no variable for the server's own Repliz credentials**.
A fallback like that would quietly lend the operator's workspace to any caller
who omitted their keys.

### OAuth

The Repliz API has no OAuth of its own, so this server acts as the
authorization server and wraps the user's keys:

```
Claude.ai ──► /mcp: 401 ──► discovery ──► /register ──► /authorize (sign-in page)
          ◄── code ◄── keys checked against Repliz ◄── user pastes keys
          ──► /token (PKCE) ──► access + refresh token ──► /mcp with Bearer
```

To turn it on, add two lines to `.env` and restart:

```bash
REPLIZ_PUBLIC_URL=https://mcp.repliz.com
REPLIZ_OAUTH_SECRET=<output of: openssl rand -base64 32>
```

There is **no token database**. Every client id, authorization code, and token
is the user's keys (plus expiry and client binding) encrypted with AES-256-GCM
under `REPLIZ_OAUTH_SECRET`. Only this server can open them, they survive a
restart, and nothing new needs deploying or backing up. What follows from that:

- **Keep `REPLIZ_OAUTH_SECRET` stable and private.** Changing it signs every
  OAuth user out — which is also the emergency switch if it leaks.
- **One token cannot be revoked early.** Instead, access tokens live an hour;
  each tool call still presents the sealed keys to Repliz, so a key rotated in
  the Repliz dashboard stops working at once; and a refresh re-checks the keys,
  sending the client back to the sign-in page. No revocation endpoint is
  advertised, because it could only pretend.
- **Refreshing extends the connection.** Each refresh issues a new refresh
  token, so a connection in regular use stays signed in; one left unused for
  `REPLIZ_OAUTH_REFRESH_TOKEN_DAYS` has to sign in again.

Clients identify themselves in either of two ways, and need no setup on this
side for either:

- **Client ID Metadata Documents** (what Claude and ChatGPT prefer). The
  `client_id` is an HTTPS URL, such as `https://chatgpt.com/oauth/client.json`,
  serving a JSON document that names the client and its redirect URIs. The
  server fetches it on first use and caches it as its `Cache-Control` says
  (between one minute and a day; an hour without the header). Only public
  clients are accepted: the document must allow `token_endpoint_auth_method`
  `none`, as both Claude's and ChatGPT's do.
- **Dynamic Client Registration** (RFC 7591), for everything else. The client
  id it hands out is the registration itself, sealed.

Accepted redirect URIs are any `https://` URL, `http://` on localhost only, and
a native app's own scheme such as `cursor://`. Every authorization response
carries the issuer (`iss`, RFC 9207), which ChatGPT needs to use its stable
redirect URI. Header authentication keeps working alongside OAuth.

### PM2 + Nginx on a VPS

```bash
npm ci && npm run build
cp .env.example .env           # PORT and the limits live here
pm2 start dist/index.js --name repliz-mcp
pm2 save && pm2 startup        # survive a reboot
```

**Do not add `-i` / `--instances`.** PM2's default fork mode is what you want.
Sessions live in this process's memory, so cluster mode would spread a user's
requests across workers that know nothing about each other, and every follow-up
request would be told its session does not exist. Scale by raising
`REPLIZ_MAX_SESSIONS` on a bigger box, not by adding instances.

Nginx needs two non-default settings, because the transport holds a
Server-Sent Events stream open and Nginx would otherwise buffer it and cut it
after 60 seconds:

```nginx
location /mcp {
    proxy_pass http://127.0.0.1:5301;
    proxy_http_version 1.1;
    proxy_set_header Connection "";

    proxy_buffering off;
    proxy_cache off;
    proxy_read_timeout 3600s;   # keep above REPLIZ_SESSION_IDLE_MINUTES

    proxy_set_header Host              $host;
    proxy_set_header X-Real-IP         $remote_addr;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}

# OAuth discovery, sign-in, and token endpoints live at the root.
location / {
    proxy_pass http://127.0.0.1:5301;
    proxy_set_header Host              $host;
    proxy_set_header X-Real-IP         $remote_addr;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
}
```

`$proxy_add_x_forwarded_for` is correct here: the server trusts only loopback,
so it reads the rightmost untrusted entry and a client cannot forge its way into
a fresh rate-limit bucket. It matters even more with OAuth on: `/register` allows
20 registrations per IP per hour, and without `X-Forwarded-For` every user looks
like `127.0.0.1` and shares that one budget.

### What a restart does

`pm2 restart` drops every live session — they exist only in this process's
memory. Nothing is queued or half-written, since every tool call is a single
synchronous call to the Repliz API, so the cost is small:

| | |
| --- | --- |
| **Mid-request** | That one call fails; retrying works. |
| **Idle** | The next request gets `404 Session not found`, which the MCP spec defines as "start a new session" — compliant clients re-initialize silently. |
| **After reconnecting** | Normal. One extra verification call, because the auth cache is empty again. |
| **OAuth users** | Stay signed in. Their tokens are sealed, not stored, so they open again after a restart. |

Shutdown on `SIGTERM` closes sessions and stops listening in about 0.1s, well
inside PM2's default `kill_timeout`.

---

## Sizing

The server is a thin proxy — every tool call is one outbound HTTPS request with
no computation in between — so CPU is effectively idle and memory is the only
thing to plan for. Measured:

| Live sessions | Process RSS |
| ------------- | ----------- |
| 0 (baseline)  | ~93 MB      |
| 10            | ~108 MB     |
| 50            | ~173 MB     |
| 100           | ~270 MB     |

That is roughly **1.8 MB per live session** on top of a ~93 MB baseline, most of
it the 105 tool schemas, which are instantiated per session so that credentials
stay isolated. A 1 GB VPS comfortably holds the default cap of 300; use ~1000 on
2 GB. Sessions are only live while a client is connected and idle ones are
reclaimed after 30 minutes, so the steady-state number is far below the total
user count.

`GET /health` reports live sessions, the cap, RSS, and uptime.

---

## Security

Because `/mcp` is open to the internet:

- **Credentials are verified before a session exists.** On `initialize` the
  server calls `GET /public/account/count` with the supplied keys and returns
  `401` if Repliz rejects them, so an anonymous caller cannot make the server
  allocate memory. Successes are cached briefly so reconnects stay cheap.
- **Sessions are bound to the credentials that opened them.** Every later
  request is re-checked against a fingerprint of those keys, in constant time.
  The session id travels as a plain header and can end up in a proxy access log
  or a client's debug output; without this check, replaying it would hand over
  the owner's workspace with no credentials at all. A mismatch gets `403`, a
  request with no credentials gets `401`.
- **Sessions expire and are capped.** Idle sessions are swept, because MCP
  clients routinely disappear without sending `DELETE` — the SDK's own
  `client.close()` does not send one. At capacity the server sweeps first, then
  evicts the least recently used session if it has been quiet for over a minute,
  so dead sessions cannot lock out real users; if every session is genuinely
  active, the new connect gets `503` rather than cutting someone off.
- **Requests are rate limited** per IP, counted against the address chosen by
  `REPLIZ_TRUST_PROXY`.
- **The OAuth sign-in page guards the keys typed into it.** It cannot be framed
  (`frame-ancestors 'none'`, `X-Frame-Options: DENY`), so a clickjacking
  overlay cannot trick a user into submitting. Any client can register itself,
  so the page escapes the client's chosen name and states where the user will be
  sent next — a client calling itself "Claude" still cannot hide its own
  redirect host. The form carries the authorization request sealed, so its
  redirect target cannot be altered, and there is no session cookie for a
  cross-site request to ride. Sign-in attempts are limited to 10 per IP per
  minute, and PKCE (S256) is mandatory. For a client identified by a metadata
  document, the page also names the host that published it.
- **Client metadata documents are fetched like untrusted URLs** — because the
  `client_id` is whatever the caller sends. The fetch refuses private and
  loopback addresses and redirects, times out after 5 seconds, and reads at most
  5 KB. A document whose `client_id` differs from its URL, that carries a client
  secret, or that lists a disallowed redirect URI is refused, and nothing
  fetched is ever relayed back to the caller.
- **Outbound calls time out**, so a stalled Repliz request cannot pin a session.
- **Uploads never touch the host's disk.** See [File uploads](#file-uploads).
- **Clean shutdown** on `SIGTERM` / `SIGINT`.

Two things to be aware of:

- Credentials are verified when a session opens, not on every call. A revoked
  key keeps working until its session goes idle; shorten
  `REPLIZ_SESSION_IDLE_MINUTES` if that matters.
- Sessions live in memory, so this is a single-instance server. Horizontal
  scaling needs a shared session store — see [Roadmap](#roadmap).

### File uploads

Uploading is three steps: `repliz_init_file` returns a one-time presigned URL,
`repliz_upload_file` sends the bytes, `repliz_complete_file` finalizes.

The middle step takes a **`sourceUrl`**, never a local path. This server is
shared, so its filesystem belongs to the operator: a path parameter would let
any connected user read the server's own files and ship them anywhere. Two
guards apply:

- the presigned PUT target must be a Repliz storage host (`REPLIZ_UPLOAD_HOSTS`)
- `sourceUrl` must resolve to a public address — loopback, private LAN ranges,
  and cloud metadata endpoints such as `169.254.169.254` are refused, as are
  redirects

Clients that would rather not route media through the server can `PUT` straight
to the presigned URL themselves and then call `repliz_complete_file`.

---

## Tools

105 tools across the Repliz Public API:

| Group               | Count | Tools                                                                                       |
| ------------------- | ----- | ------------------------------------------------------------------------------------------- |
| **Accounts**        | 6     | list, count, get, statistics, update automation, delete                                     |
| **Account Connect** | 42    | OAuth authorize / exchange / list / connect / reconnect for Facebook, Instagram, Threads, YouTube, LinkedIn, TikTok, Shopee, Twitter/X, Pinterest; QR session / connect / reconnect for WhatsApp; QR session / two-step password / connect / reconnect for Telegram |
| **Comments**        | 5     | list, get, reply, update status, delete                                                     |
| **Schedule**        | 7     | list, get, create, update, retry, delete, bulk delete                                       |
| **Chat**            | 5     | list, get, list messages, send message, mark read                                           |
| **Content**         | 9     | list, get, list comments, comment, statistics, DM commenter, like comment, delete comment, delete content |
| **Automation**      | 5     | list, get, create, update, delete                                                           |
| **Templates**       | 5     | list, get, create, update, delete                                                           |
| **Storage**         | 8     | statistics, list, get, init upload, upload, complete, delete, bulk delete                   |
| **Reports**         | 3     | list, get, retry                                                                            |
| **Research**        | 3     | Threads content, user content, user profile                                                 |
| **Add-ons**         | 7     | TikTok trending music, Shopee products, WhatsApp and Telegram channels & groups, Pinterest boards, link metadata, addon allocation |

Tool availability follows the caller's Repliz plan; calling one above your tier
returns a clear error.

Every tool declares what it does, as MCP tool annotations: 48 only read
(`readOnlyHint`), 42 create or send something without touching what exists
(`destructiveHint: false`), and 15 change or delete (`destructiveHint: true`).
Clients use these to decide what to confirm first; ChatGPT asks before any tool
not marked read-only. A new tool must state its `effect` to compile.

---

## Development

```bash
npm run dev     # run from TypeScript source, no build step
npm test        # unit tests (no network, no credentials)
npm run smoke   # end-to-end: boots the real server against a stub Repliz API
npm run build   # compile to dist/
```

`npm test` covers the parts where a quiet mistake becomes a security hole: the
SSRF and upload-host guards, credential parsing, the session credential binding,
the limits that bound memory, and OAuth token sealing, single-use codes, and
client binding. `npm run smoke` covers the wiring — that two users stay apart,
that a session id alone opens nothing, that the full OAuth flow works over HTTP,
and that the upload guards are reachable through the tool surface. Neither needs
credentials or network access, and CI runs both.

```
src/
  index.ts     # entry point: load .env, start the server
  http.ts      # the HTTP transport, auth, and request lifecycle
  session.ts   # session store, credential fingerprints, rate limiter, auth cache
  server.ts    # createReplizServer(client)
  config.ts    # env config + per-request credential parsing
  client.ts    # Repliz API client (Basic auth, timeouts, errors)
  oauth.ts     # OAuth provider: sealed clients, codes, and tokens
  oauth-page.ts  # the sign-in page and its security headers
  net.ts       # upload-host allowlist and SSRF guards
  tools/       # one module per API domain, registered via tools/index.ts
test/          # unit tests for config, session, oauth, and net
scripts/
  smoke-test.mjs   # end-to-end check against a stub API
api.json           # the source OpenAPI spec, kept as reference
```

---

## Roadmap

- **Shared session store** (Redis) so several replicas can run behind a load
  balancer without sticky sessions.

## License

MIT — see [LICENSE](LICENSE).

TDQS

A3.7/5.0

Scored across 33 tools

Disambiguation5/5

Each tool targets a distinct resource and action (e.g., get_comment vs get_content_comments, list_chats vs get_chat). Descriptions clearly differentiate similar-sounding tools, leaving no ambiguity.

Naming Consistency5/5

All tools follow a consistent 'repliz_verb_noun' pattern with lowercase and underscores. Actions like create, get, list, delete, update are used uniformly across resources.

Tool Count4/5

33 tools is slightly high but appropriate for a comprehensive social media management server covering accounts, scheduling, content, comments, messaging, and search. Each tool has a clear purpose.

Completeness4/5

The tool set covers major workflows: account management, scheduling (CRUD + retry), content/comments, messaging, and platform-specific searches. Minor gaps like immediate publishing (vs scheduling) exist but are not critical.

Maintenance

ActivityActive
ResponsivenessNo issues