Skip to main content
Glama
N-Graves

x-mcp-server

by N-Graves
README.md
# x-mcp-server

A [Model Context Protocol](https://modelcontextprotocol.io) server for the **X API v2**.

**175 of X's 190 published operations** are reachable. The 15 that are not are the streaming endpoints, excluded for a reason no access tier changes.

MIT licensed.

## Install

```bash
npm install -g @nasdigitaluk/x-mcp
```

## Authentication

Two ways. Pick one.

**App-only** — simplest, cannot post:

```json
{ "env": { "X_BEARER_TOKEN": "your-bearer-token" } }
```

**User context** — can post, and refreshes itself:

```json
{
  "env": {
    "X_CLIENT_ID": "your-client-id",
    "X_CLIENT_SECRET": "your-client-secret",
    "X_CREDENTIALS_FILE": "~/.x-mcp-credentials"
  }
}
```

Seed the credentials file once from your authorisation flow:

```
X_ACCESS_TOKEN=...
X_REFRESH_TOKEN=...
X_TOKEN_EXPIRES_AT=1757260800000
```

## ⚠️ X's refresh tokens are single use

This is the part worth reading before you trust any X integration, including this one.

X access tokens last about **two hours**, and the refresh token is **single use** — every refresh returns a new one and invalidates the old. Persist the new one incorrectly and the chain breaks **permanently**; the only fix is re-authorising in a browser.

Running two MCP clients at once is completely normal. Both refresh, both spend the same token, and whichever writes second saves one X has already invalidated. The server this replaces did the write with a plain `writeFileSync` — no lock, no atomic write.

Here:

- the refresh happens inside an **exclusive lock**, so only one process spends the token;
- it **re-reads under the lock** before refreshing, because another process may have already done the work while this one waited;
- the write is **atomic** (temp file, `fsync`, rename), so a crash cannot truncate it;
- the file is **chmod'd on every write**, not only when created — a pre-existing `0644` credentials file used to stay world-readable while the code read as though it had secured it;
- a **stale lock** from a killed process is broken after 30s rather than wedging everything after it.

There is a test asserting that three concurrent callers cause exactly **one** refresh.

## What is excluded, and why

The 15 streaming endpoints: `/2/tweets/search/stream`, the firehose and sample streams, and the compliance streams.

These are not request/response endpoints. They hold the connection open and emit posts indefinitely — a stream that returns has *ended*, which is a failure rather than a result. An MCP tool call has to return, so pointing one at a stream means it hangs until the client's timeout and then reports a timeout, which reads as a broken server rather than a category error.

No access tier changes this. Consuming a stream needs a long-running process, not a tool call.

**The `/stream/rules` endpoints are NOT excluded.** They configure what a stream would deliver and are ordinary calls, so you can manage your rules here and consume the stream elsewhere.

## On paid tiers

Most of X's API needs a paid access tier. Those endpoints are **covered anyway**, because a tier is something you can buy — unlike an admin key you simply cannot obtain. Excluding them would deny the API to people who have paid for it. X reports clearly when your tier is insufficient, and that error passes through.

## Tools

Seven tools for 175 operations.

| Tool | |
|---|---|
| `x_list_operations` | Browse the catalogue. Start here. |
| `x_call` | Call any operation by id. |
| `x_get_me` | The authenticated user. Needs user context. |
| `x_create_post` | Post. Immediately public. |
| `x_delete_post` | Delete one of yours. Destructive. |
| `x_search_posts` | Search recent posts. Needs Basic or above. |
| `x_get_post` | One post by id. |

Posting is classified a **write**, not destructive, because a post can be deleted — but everyone who saw it still saw it, and the tool description says so.

Of 190 operations: **104 read, 57 write, 29 destructive**.

## Refreshing the catalogue

```bash
curl -o vendor/x-openapi.json https://api.twitter.com/2/openapi.json
npm run generate && npm test
```

## Testing

```bash
npm test                              # 15 tests, including the refresh-concurrency one
X_BEARER_TOKEN=x npm run smoke        # real MCP over stdio
```

## Built on

[`@nasdigitaluk/mcp-server-core`](https://github.com/N-Graves/mcp-server-core).

## Licence

MIT.

TDQS

B3.4/5.0

Scored across 7 tools

Disambiguation4/5

Each dedicated tool targets a clear action (create/delete/search/get/user), but x_call can execute the same operations generically, so an agent could occasionally choose between x_call and a wrapper. The descriptions are specific enough that this overlap is manageable.

Naming Consistency4/5

All tools share an x_ prefix and use snake_case, with imperative verb-first names like list_operations, create_post, and search_posts. x_call is a slight deviation from the verb_noun pattern, but it still reads consistently as the generic dispatch verb.

Tool Count5/5

Seven tools is a well-scoped set: five focused wrappers plus a discovery/generic-call pair cover common and long-tail X operations without bloating the server.

Completeness5/5

The server explicitly reaches 175 of 190 X API v2 operations via x_list_operations plus x_call, and dedicated wrappers cover the most common post/user workflows. The tiny uncovered remainder is not an obvious gap for practical use.

Maintenance

ActivityMaintained
ResponsivenessNo issues