# mcp-xray-pilot

[![npm version](https://img.shields.io/npm/v/mcp-xray-pilot.svg)](https://www.npmjs.com/package/mcp-xray-pilot)
[![npm downloads](https://img.shields.io/npm/dm/mcp-xray-pilot.svg)](https://www.npmjs.com/package/mcp-xray-pilot)
[![node](https://img.shields.io/node/v/mcp-xray-pilot.svg)](https://nodejs.org)
[![license](https://img.shields.io/npm/l/mcp-xray-pilot.svg)](./LICENSE)

[Русская версия ниже / Russian version below](#mcp-xray-pilot-ru)

A Model Context Protocol (MCP) server that gives an LLM offline access to the
official **xray-core** documentation, plus deep structural validation,
best-practice lint, a protocol/transport/security compatibility matrix, a
**full v2fly geosite catalogue (~1500 categories)**, an alternative-stack
suggester, REALITY keypair / shortId generators, a live SNI target validator,
a curated SNI suggester per exit-country and a multi-config merge helper.

## What it does

- **Bundles ~60 docs pages** from the upstream
  [`XTLS/Xray-docs-next`](https://github.com/XTLS/Xray-docs-next) repo as
  raw markdown (no html→md mess).
- **Refreshes on demand**: `xray_fetch_topic` tries the network first and
  silently overwrites the bundled cache on success. If you're offline (or
  upstream is down), it falls back to the packaged copy and surfaces a
  `warning` field.
- **Searches the corpus** with a tiny title/body relevance scorer.
- **Validates** xray JSON config: required top-level fields, per-protocol
  Zod schemas (vless / vmess / trojan / shadowsocks / socks / http /
  wireguard / hysteria / freedom / blackhole / dns / loopback / dokodemo /
  tun), per-transport `*Settings` schemas (raw / xhttp / grpc / ws / mkcp /
  httpupgrade / hysteria), TLS / REALITY security blocks, routing tag
  cross-references.
- **Lints** ~22 best-practice rules: VLESS `decryption: "none"`, REALITY
  pubkey/shortId/target syntax, XTLS vision flow compatibility, TLS
  fingerprint enum, ALPN collisions, geosite/geoip typo catcher, protocol
  × transport × security incompatibilities, `xhttp.path` leading slash,
  `geoip:private` block rule, sniffing on 80/443, **DNS-over-proxy leaks
  on `.ru` direct routing** (v0.12), **`geosite:` categories absent from
  xray-core release `geosite.dat`** (v0.12) etc.
- **Geo catalogue**: search ~1500 known geoip/geosite tags by substring (full
  v2fly/domain-list-community catalogue, hydrated from `data/geocatalogue.json`).
- **REALITY toolbelt**: `xray_generate_reality_keypair` (drop-in replacement
  for `xray x25519`), `xray_generate_short_ids` (cryptographically random,
  legacy-empty prefix), `xray_validate_sni_target` (live TLS 1.3 + h2 probe),
  `xray_suggest_sni_for_country` (curated REALITY fronts per exit-country).
- **Compares protocols**: side-by-side table of vless/vmess/trojan/ss/
  hysteria2/wireguard on transports, security, anti-DPI, mobile, battery.
- **Recommends a stack** for a stated goal (anti-DPI in RU/IR/CN, low
  latency, mobile battery, high throughput, stealth-CDN, getting started).
- **Merges configs**: joins inbounds/outbounds/routing.rules from N JSON
  configs, auto-resolves tag collisions, warns on port collisions.

## Tools

| Tool                       | What it does                                                                          |
| -------------------------- | ------------------------------------------------------------------------------------- |
| `xray_list_topics`         | List doc topics, grouped by category. Use first to discover slugs.                    |
| `xray_fetch_topic`         | Fetch one topic as markdown. Network → fall back to bundled cache → update cache.     |
| `xray_search`              | Full-text search over all cached docs. Returns ranked hits + snippets.                |
| `xray_validate_config`     | Structural+schema validation of an xray JSON config (Zod under the hood).             |
| `xray_lint`                | ~22 best-practice lint rules. Returns issues with severity, rule id, JSON-pointer.    |
| `xray_geo_search`          | Substring search over the embedded geosite/geoip catalogue (~1500 tags).              |
| `xray_diff_protocols`      | Side-by-side feature table for two protocols.                                         |
| `xray_suggest_alternative` | Recommend protocol+transport+security for a goal (anti-DPI / battery / latency / …). |
| `xray_generate_short_ids`  | Cryptographically random REALITY shortIds (default `[4,8,16]` bytes, legacy empty prefix). |
| `xray_generate_reality_keypair` | Fresh REALITY X25519 keypair, base64url 43 chars — drop-in for `xray x25519`. |
| `xray_validate_sni_target` | Live TLS 1.3 + ALPN h2 + HTTP probe of a candidate REALITY target host.               |
| `xray_test_reality_live`   | Spin up real local xray server+client, run a full REALITY handshake against the target, probe HTTPS through the cascade. Strictly stronger than `xray_validate_sni_target`. |
| `xray_whitelist_sni_candidates` | Pull a public RU-traffic whitelist (default: hxehex/russia-mobile-internet-whitelist) and TLS-validate the top N hosts as REALITY SNI front candidates. Returns ranked, sorted by ok desc, latency asc. |
| `xray_suggest_sni_for_country` | Curated REALITY SNI/target hosts per exit-country (DE/PL/NL/FR/LV/SE/FI/US/UK/JP/SG/AU/CA). |
| `xray_merge_configs`       | Merge N xray configs with tag-collision resolution and conflict warnings.             |
| `xray_github_search`       | Search issues/PRs/discussions across XTLS GitHub repos (Xray-core/REALITY/docs).      |
| `xray_github_fetch_issue`  | Fetch one issue/PR/discussion with full body + top comments.                          |
| `xray_refresh_cache`       | Bulk re-fetch cached docs (`scope: all/stale/category`). Optional `discover` for new upstream slugs. Optional `refresh_geocatalogue: true` to also re-pull the v2fly category list. |

Total: **18 tools**.

### Quick toolbelt

Generate a REALITY keypair (server-side priv + client-side pub):

```jsonc
// xray_generate_reality_keypair {}
{
  "privateKey": "MNCibkT-h5bCF6iknJG0rJdHdfUjT7VugHgSX9BRWUY",
  "publicKey": "ffvf0eNwnLhgK-axt3rajJIAoHKv0rX4xkw5KyImn38",
  "note": "Server: paste privateKey into inbound realitySettings.privateKey. Clients: paste publicKey into outbound realitySettings.publicKey. Format matches `xray x25519` output exactly."
}
```

Live-check a candidate REALITY SNI:

```jsonc
// xray_validate_sni_target { "host": "www.onet.pl" }
{
  "host": "www.onet.pl", "port": 443, "ok": true,
  "tls_version": "TLSv1.3", "alpn": "h2", "http_status": 200,
  "cert_subject": "www.onet.pl", "cert_san_count": 4,
  "latency_ms": 312, "issues": []
}
```

Live REALITY handshake (catches what `xray_validate_sni_target` cannot):

```jsonc
// xray_test_reality_live { "target_host": "2gis.ru" }
{
  "ok": true,
  "target": "2gis.ru:443",
  "reality_handshake_complete": true,
  "client_received_real_cert": false,
  "http_probe_status": 200,
  "latency_ms": 824,
  "issues": [],
  "used_keypair": { "privateKey": "...", "publicKey": "...", "shortId": "a1b2c3d4" },
  "cached": false,
  "cached_at": "2026-05-06T12:30:00.000Z"
}
```

> Verdicts are persisted to `data/reality-verdicts.json` (LRU cap 50, TTL 24h,
> key `host:port`). Subsequent calls with the same target return instantly with
> `cached: true`. Pass `force_refresh: true` to bypass the cache and rerun xray.

Compare a list of candidates in one shot — runs sequentially, each entry hits
the cache, results are sorted by `ok desc, latency_ms asc`:

```jsonc
// xray_test_reality_live {
//   "multi_targets": ["2gis.ru", "yandex.ru", "vk.com", "mail.ru"]
// }
{
  "results": [
    { "target": "2gis.ru:443",   "ok": true,  "latency_ms": 824, "cached": true,  "cached_at": "..." },
    { "target": "mail.ru:443",   "ok": true,  "latency_ms": 1102, "cached": false, "cached_at": "..." },
    { "target": "yandex.ru:443", "ok": false, "latency_ms": 0,    "cached": false, "cached_at": "...", "issues": ["client received a real certificate ..."] },
    { "target": "vk.com:443",    "ok": false, "latency_ms": 0,    "cached": false, "cached_at": "...", "issues": ["target unreachable"] }
  ],
  "summary": { "ok_count": 2, "total": 4 }
}
```

Pull a public RU-traffic whitelist and rank live SNI candidates for an
inbound RU-relay node:

```jsonc
// xray_whitelist_sni_candidates { "max_candidates": 10 }
{
  "source_url": "https://raw.githubusercontent.com/hxehex/russia-mobile-internet-whitelist/main/whitelist.txt",
  "fetched_at": "2026-05-06T12:34:00.000Z",
  "total_domains": 412,
  "tested": 10,
  "candidates": [
    { "host": "ya.ru",      "ok": true,  "tls_version": "TLSv1.3", "alpn": "h2", "http_status": 200, "latency_ms": 41,  "cert_subject": "ya.ru",      "issues": [] },
    { "host": "vk.com",     "ok": true,  "tls_version": "TLSv1.3", "alpn": "h2", "http_status": 200, "latency_ms": 78,  "cert_subject": "*.vk.com",   "issues": [] },
    { "host": "ozon.ru",    "ok": false, "tls_version": "TLSv1.3", "alpn": "h2", "http_status": 200, "latency_ms": 134, "cert_subject": "*.ozon.ru",  "issues": [] }
  ],
  "summary": { "ok_count": 8, "failed_count": 2 },
  "cache":   { "used": false, "age_seconds": null },
  "notes":   ["latency_ms is measured from the MCP host (your laptop), not from the relay node — re-test from the node for geo-accurate values"]
}
```

> The whitelist body is cached on disk in `data/whitelist-cache.json`
> (default TTL 24h, override via `cache_ttl_hours`). Verdicts are NOT cached
> — every call re-probes its slice live so latency numbers stay fresh.
> NB: the probe runs from your laptop, not from the РФ relay; for
> geo-accurate latency, use `xray_test_reality_live` from the actual
> node (or ssh + curl on it).

It also exposes a single MCP **resource**: `xray://docs/index` — the raw
`_index.json` of cached topics.

## Examples

### Example 1 — lint a broken cascade config

Prompt:

> Lint this xray config and tell me what's wrong.
>
> ```json
> {
>   "inbounds":[{"tag":"in1","port":443,"protocol":"vless","settings":{"clients":[{"id":"00000000-0000-4000-8000-000000000000","flow":"xtls-rprx-vision"}]},"streamSettings":{"network":"ws","security":"reality","realitySettings":{"target":"yandex.com","privateKey":"short","shortIds":["zz"],"serverNames":["yandex.com"],"fingerprint":"netscape"}}}],
>   "outbounds":[{"tag":"out","protocol":"freedom"}],
>   "routing":{"rules":[{"type":"field","outboundTag":"missing","domain":["geosite:tinkoff-bank"]}]}
> }
> ```

`xray_lint` response (excerpt):

```json
{
  "summary": { "error_count": 6, "warn_count": 5, "info_count": 2 },
  "issues": [
    { "rule": "reality_pubkey_format", "severity": "error",
      "path": "/inbounds/0/streamSettings/realitySettings/privateKey",
      "message": "REALITY privateKey must be 43 base64url chars, got 'short'" },
    { "rule": "reality_shortid_format", "severity": "error",
      "path": "/inbounds/0/streamSettings/realitySettings/shortIds/0",
      "message": "shortIds[0] 'zz' must be hex (0..16 chars, even length)" },
    { "rule": "flow_requires_specific_transport", "severity": "error",
      "path": "/inbounds/0/streamSettings/network",
      "message": "flow=xtls-rprx-vision requires raw/tcp transport, got 'ws'" },
    { "rule": "routing_dangling_outbound", "severity": "error",
      "path": "/routing/rules/0/outboundTag",
      "message": "outboundTag 'missing' does not match any outbound tag" },
    { "rule": "tls_fingerprint_enum", "severity": "warn",
      "path": "/inbounds/0/streamSettings/realitySettings/fingerprint",
      "message": "'netscape' is not a known fingerprint (chrome/firefox/safari/ios/android/edge/360/qq/random/randomized)" },
    { "rule": "geo_unknown_category", "severity": "warn",
      "path": "/routing/rules/0/domain/0",
      "message": "geosite:tinkoff-bank is not in the bundled catalogue (typo? try geosite:category-ru)" }
  ]
}
```

### Example 2 — research RKN bypass via GitHub

Prompt:

> Find recent xray issues about RKN/TSPU and suggest an anti-DPI stack for Russia.

Step 1 — `xray_github_search`:

```jsonc
// args
{ "query": "RKN", "type": "issue", "repo": "all", "sort": "updated" }
// response (excerpt)
{
  "total_count": 47,
  "items": [
    { "repo": "XTLS/Xray-core", "number": 5747, "state": "open",
      "title": "REALITY blocked by TSPU after recent RKN update",
      "comments": 38, "reactions": { "+1": 21 },
      "snippet": "Starting last week our REALITY inbound on :443 stops responding after ~30s of traffic from RU clients. xhttp+REALITY survives longer than raw+vision…" },
    { "repo": "XTLS/Xray-core", "number": 5332, "state": "closed",
      "title": "RKN: shortId enumeration probe",
      "comments": 22,
      "snippet": "TSPU appears to brute-force shortIds. Recommendation: rotate, keep list >=4 entries, do not include empty string…" }
  ]
}
```

Step 2 — `xray_suggest_alternative`:

```jsonc
// args
{ "goal": "anti-dpi-russia" }
// response (excerpt)
{
  "recommendation": {
    "protocol": "vless",
    "transport": "xhttp",
    "security": "reality",
    "flow": null,
    "rationale": [
      "xhttp survives TSPU active probing better than raw/tcp+vision in 2025",
      "REALITY hides SNI; pick a target popular in RU (yandex.com, mail.ru)",
      "Rotate shortIds[] (>=4 entries, hex, no empty string)",
      "Keep packet padding default; do not enable kcp on top of REALITY"
    ]
  },
  "see_also": ["xray_fetch_topic transports/xhttp", "xray_fetch_topic features/reality"]
}
```

## Install

```bash
npm i -g mcp-xray-pilot
```

Or run from source:

```bash
git clone https://github.com/beekamai/mcp-xray-pilot.git
cd mcp-xray-pilot
npm install
npm run build
npm run fetch-docs    # fills data/docs/ if you cloned without it
```

## Add to Claude Code

```bash
claude mcp add xray-pilot --scope user -- npx -y mcp-xray-pilot
```

With a GitHub PAT (raises `xray_github_*` rate limit, enables discussions):

```bash
claude mcp add xray-pilot --scope user --env GITHUB_TOKEN=ghp_xxx -- npx -y mcp-xray-pilot
```

Or, from a local clone:

```bash
claude mcp add xray-pilot --scope user -- node /absolute/path/to/mcp-xray-pilot/dist/index.js
```

## Add to Cursor / Windsurf / Cline

```jsonc
{
  "mcpServers": {
    "xray-pilot": {
      "command": "npx",
      "args": ["-y", "mcp-xray-pilot"]
    }
  }
}
```

## Offline cache vs. online refresh

The `data/docs/` directory ships with the package. Each call to
`xray_fetch_topic`:

1. If `force_offline=true` → reads only the packaged copy.
2. Otherwise → tries the upstream raw markdown URL (10s timeout). On HTTP
   200, the response body replaces the on-disk markdown and the index
   entry's `fetched_at` is updated. Subsequent calls in the same process
   serve from in-memory cache.
3. On any network failure → falls back to the packaged copy, returning the
   markdown plus `warning: "network fetch failed: …"`.

To refresh everything in bulk, run `npm run fetch-docs -- --refresh`. To
discover newly-added pages upstream without writing them: `npm run
fetch-docs -- --discover`.

### Keeping cache fresh

There are three complementary ways to keep `data/docs/` aligned with
upstream:

1. **On-demand per page** — every `xray_fetch_topic` call already tries
   the network first and silently overwrites the on-disk copy on success.
   No action needed.
2. **Bulk via MCP tool** — call `xray_refresh_cache` from the LLM:
   - `{ "scope": "stale", "max_age_days": 30 }` (default) re-fetches only
     entries older than N days.
   - `{ "scope": "all" }` re-fetches every page (~60).
   - `{ "scope": "category", "category": "transports" }` restricts to one
     category.
   - Add `"discover": true` to also report slugs that exist upstream but
     are missing from `DOCS_CATALOGUE` in `src/docs.ts`.
3. **CI weekly cron** — `.github/workflows/refresh-docs.yml` runs
   `npm run fetch-docs -- --refresh` every Monday 06:00 UTC and opens a
   PR if anything changed (also triggerable manually via `workflow_dispatch`).

## Optional `GITHUB_TOKEN` env variable

`xray_github_search` and `xray_github_fetch_issue` work anonymously, but
the GitHub API caps unauthenticated requests at **60/hour**. Setting
`GITHUB_TOKEN` to any classic or fine-grained PAT (no scopes needed for
public repos) raises the limit to **5000/hour** and additionally enables
the **discussions** endpoint (GraphQL), which has no anonymous access.

```bash
export GITHUB_TOKEN=ghp_xxx           # Linux / macOS
$env:GITHUB_TOKEN = "ghp_xxx"         # PowerShell
```

When `X-RateLimit-Remaining` drops below 10, the tool surfaces an inline
warning in the response.

## Security model

This package runs entirely in the MCP host's process and the only
"interesting" thing it does on disk/network is `xray_test_reality_live`,
which downloads the official **xray-core** binary from
[XTLS/Xray-core releases](https://github.com/XTLS/Xray-core/releases/latest)
into `~/.cache/mcp-xray-pilot/xray-bin/` and spawns it locally on
ephemeral ports.

**Approvals.** There is no in-package permission prompt by design — all
tool invocations are gated by the MCP host (Claude Code / Claude Desktop
/ Cursor). Each host has its own permission UX (per-call confirm,
allowlist, etc.). Pin a specific version in your MCP config
(`mcp-xray-pilot@0.15.0`, not `mcp-xray-pilot`) so a malicious npm
release cannot silently roll in.

**Integrity of the xray binary.** Since v0.15:

1. The matching `<asset>.zip.dgst` from the same GitHub release is
   downloaded alongside the zip, and the zip is verified against its
   `SHA2-256=` line **before** extraction. Mismatched payloads are
   renamed to `xray.rejected-<timestamp>.zip` for inspection and the
   tool aborts.
2. If you want a stronger guarantee than "trust whatever XTLS ships
   right now", set `XRAY_PILOT_PINNED_HASH` to a known SHA-256 — the
   `.dgst` is then ignored and the pinned value is enforced. This
   defends against a hypothetical compromised XTLS release that ships a
   matching `.dgst`.

   ```bash
   # Linux / macOS — read the hash once, then pin it.
   curl -sL https://github.com/XTLS/Xray-core/releases/latest/download/Xray-linux-64.zip.dgst \
     | awk '/^SHA2-256=/ {print $2}'
   export XRAY_PILOT_PINNED_HASH=23cd9af9...
   ```

   ```powershell
   $env:XRAY_PILOT_PINNED_HASH = "23cd9af9..."
   ```

**What this does NOT do.** XTLS does not GPG-sign `.dgst` files, so an
attacker with write access to the releases page can ship a coherent
zip+dgst pair. The pinned-hash path is your defense against that. The
package does not sandbox the spawned xray binary — it runs with the
permissions of the MCP host process. If you need stronger isolation,
run the MCP host inside a container or a dedicated user account.

**`GITHUB_TOKEN`.** Optional, used only by `xray_github_search` /
`xray_github_fetch_issue`. A **fine-grained PAT with read-only public
repo scope** is recommended over classic PATs. The token is read from
the process env at call time and never persisted by the package.

## Roadmap

See [ROADMAP.md](./ROADMAP.md). All v0.1–v0.11 milestones are checked off.

## License

MIT.

---

<a id="mcp-xray-pilot-ru"></a>

# mcp-xray-pilot (RU)

MCP-сервер, дающий LLM офлайн-доступ к официальной документации **xray-core**,
плюс глубокую валидацию по схемам, lint best-practice, матрицу
совместимости протоколов/транспортов/security, каталог geosite/geoip,
рекомендатор альтернативного стека и helper для слияния конфигов.

## Что делает

- **Упаковывает ~60 страниц** документации из upstream-репозитория
  [`XTLS/Xray-docs-next`](https://github.com/XTLS/Xray-docs-next) как
  raw markdown (без html→md мусора).
- **Обновляется по запросу**: `xray_fetch_topic` сначала идёт в сеть и при
  успехе молча перезаписывает упакованный кеш. Если оффлайн (или upstream
  лёг), возвращает упакованную копию и выставляет `warning`.
- **Поиск по корпусу** простым title/body relevance scorer.
- **Валидирует** xray JSON: обязательные top-level поля, per-protocol
  Zod-схемы (vless / vmess / trojan / shadowsocks / socks / http /
  wireguard / hysteria / freedom / blackhole / dns / loopback / dokodemo /
  tun), per-transport `*Settings` (raw / xhttp / grpc / ws / mkcp /
  httpupgrade / hysteria), security блоки TLS/REALITY, routing tag
  cross-references.
- **Lint** ~22 правил: VLESS `decryption: "none"`, REALITY pubkey/shortId/
  target syntax, XTLS vision flow compatibility, TLS fingerprint enum,
  ALPN collisions, geo typo catcher, protocol × transport × security
  несовместимости, `xhttp.path` слеш, `geoip:private` block, sniffing на
  80/443, **утечка DNS через прокси ломает `.ru` direct routing** (v0.12),
  **`geosite:` категории, которых нет в xray-core release `geosite.dat`**
  (v0.12) и т.д.
- **Geo catalogue**: поиск по ~1500 известным geoip/geosite тегам (полный
  v2fly/domain-list-community каталог, гидратируется из `data/geocatalogue.json`).
- **REALITY toolbelt**: `xray_generate_reality_keypair` (drop-in замена
  `xray x25519`), `xray_generate_short_ids` (криптослучайные, с empty-prefix
  для легаси), `xray_validate_sni_target` (live TLS 1.3 + h2 проба),
  `xray_suggest_sni_for_country` (курируемые REALITY-фронты по стране exit'а).
- **Сравнение протоколов**: таблица vless/vmess/trojan/ss/hysteria2/
  wireguard по transports, security, anti-DPI, mobile, battery.
- **Рекомендует стек** под цель (anti-DPI в РФ/Иране/КНР, low-latency,
  mobile-battery, high-throughput, stealth-CDN, getting started).
- **Сливает конфиги**: объединяет inbounds/outbounds/routing.rules из N
  JSON конфигов, авто-резолвит коллизии тегов, варнит на коллизиях портов.

## Тулы

| Тул                        | Что делает                                                                              |
| -------------------------- | --------------------------------------------------------------------------------------- |
| `xray_list_topics`         | Список тем по категориям. Дёргать первым.                                               |
| `xray_fetch_topic`         | Получить тему как markdown. Сеть → fallback на кеш → обновление кеша.                   |
| `xray_search`              | Полнотекстовый поиск по докам. Хиты + сниппеты.                                         |
| `xray_validate_config`     | Структурная валидация + Zod-схемы по протоколу/transport/security.                      |
| `xray_lint`                | ~20 правил best-practice. Issues с severity, rule id, JSON-pointer.                     |
| `xray_geo_search`          | Поиск по embedded каталогу geosite/geoip по подстроке (~1500 тегов).                    |
| `xray_diff_protocols`      | Side-by-side таблица фич двух протоколов.                                               |
| `xray_suggest_alternative` | Рекомендация protocol+transport+security под цель.                                      |
| `xray_generate_short_ids`  | Криптослучайные REALITY shortIds (default `[4,8,16]` байт, с empty-prefix для легаси).  |
| `xray_generate_reality_keypair` | Свежая X25519 пара REALITY, base64url 43 chars — drop-in для `xray x25519`.        |
| `xray_validate_sni_target` | Live проба TLS 1.3 + ALPN h2 + HTTP кандидата на REALITY target.                        |
| `xray_test_reality_live`   | Поднимает локальную пару xray (server+client), реально гоняет REALITY handshake к target, probe HTTPS сквозь каскад. Строго сильнее `xray_validate_sni_target`. |
| `xray_whitelist_sni_candidates` | Тянет публичный whitelist РФ-трафика (default: hxehex/russia-mobile-internet-whitelist), TLS-валидирует топ-N как REALITY SNI-кандидаты. Сортировка ok desc, latency asc. |
| `xray_suggest_sni_for_country` | Курируемые REALITY-фронты по стране exit'а (DE/PL/NL/FR/LV/SE/FI/US/UK/JP/SG/AU/CA). |
| `xray_merge_configs`       | Слить N конфигов с разрешением коллизий тегов.                                          |
| `xray_github_search`       | Поиск issues/PR/discussions по XTLS GitHub репозиториям.                                |
| `xray_github_fetch_issue`  | Получить одну issue/PR/discussion с полным body + топ комментариев.                     |
| `xray_refresh_cache`       | Bulk перезатяжка кеша доков (`scope: all/stale/category`). Опц. `discover` + `refresh_geocatalogue: true` (заодно перетянет v2fly категории). |

Всего: **18 тулов**.

### Quick toolbelt

Сгенерить REALITY keypair (priv для сервера, pub для клиентов):

```jsonc
// xray_generate_reality_keypair {}
{
  "privateKey": "MNCibkT-h5bCF6iknJG0rJdHdfUjT7VugHgSX9BRWUY",
  "publicKey": "ffvf0eNwnLhgK-axt3rajJIAoHKv0rX4xkw5KyImn38",
  "note": "Format matches `xray x25519` output exactly."
}
```

Live-проверить кандидата на REALITY SNI:

```jsonc
// xray_validate_sni_target { "host": "www.onet.pl" }
{
  "host": "www.onet.pl", "port": 443, "ok": true,
  "tls_version": "TLSv1.3", "alpn": "h2", "http_status": 200,
  "cert_subject": "www.onet.pl", "cert_san_count": 4,
  "latency_ms": 312, "issues": []
}
```

> ⚠️ Проба идёт с локальной машины, где запущен `mcp-xray-pilot`. Для решения «работает ли SNI из РФ» — гоняй с РФ-IP отдельно.

Реальный REALITY handshake (ловит то, что `xray_validate_sni_target` пропускает):

```jsonc
// xray_test_reality_live { "target_host": "2gis.ru" }
{
  "ok": true,
  "target": "2gis.ru:443",
  "reality_handshake_complete": true,
  "client_received_real_cert": false,
  "http_probe_status": 200,
  "latency_ms": 824,
  "issues": [],
  "used_keypair": { "privateKey": "...", "publicKey": "...", "shortId": "a1b2c3d4" },
  "cached": false,
  "cached_at": "2026-05-06T12:30:00.000Z"
}
```

> При первом вызове скачивает xray-binary в `~/.cache/mcp-xray-pilot/xray-bin/` (~30MB), дальше переиспользует. Если REALITY несовместим с target (как `outlook.live.com` или `www.ozon.ru` — реальные кейсы Flare VPN), `client_received_real_cert: true` и `ok: false`.

> Вердикты пишутся на диск в `data/reality-verdicts.json` (LRU 50, TTL 24h, ключ `host:port`). Повторный вызов на тот же таргет возвращается мгновенно с `cached: true`. Чтобы прогнать заново — `force_refresh: true`.

Сравнить пачку кандидатов одним вызовом — запускаются последовательно, каждый через кэш, сорт `ok desc, latency_ms asc`:

```jsonc
// xray_test_reality_live {
//   "multi_targets": ["2gis.ru", "yandex.ru", "vk.com", "mail.ru"]
// }
{
  "results": [
    { "target": "2gis.ru:443",   "ok": true,  "latency_ms": 824, "cached": true },
    { "target": "mail.ru:443",   "ok": true,  "latency_ms": 1102, "cached": false },
    { "target": "yandex.ru:443", "ok": false, "issues": ["client received a real certificate ..."] },
    { "target": "vk.com:443",    "ok": false, "issues": ["target unreachable"] }
  ],
  "summary": { "ok_count": 2, "total": 4 }
}
```

Затянуть публичный whitelist РФ-трафика и проранжировать кандидатов на REALITY SNI для inbound РФ-relay-ноды:

```jsonc
// xray_whitelist_sni_candidates { "max_candidates": 10 }
{
  "source_url": "https://raw.githubusercontent.com/hxehex/russia-mobile-internet-whitelist/main/whitelist.txt",
  "fetched_at": "2026-05-06T12:34:00.000Z",
  "total_domains": 412,
  "tested": 10,
  "candidates": [
    { "host": "ya.ru",   "ok": true,  "tls_version": "TLSv1.3", "alpn": "h2", "http_status": 200, "latency_ms": 41,  "cert_subject": "ya.ru",     "issues": [] },
    { "host": "vk.com",  "ok": true,  "tls_version": "TLSv1.3", "alpn": "h2", "http_status": 200, "latency_ms": 78,  "cert_subject": "*.vk.com",  "issues": [] }
  ],
  "summary": { "ok_count": 8, "failed_count": 2 },
  "cache":   { "used": false, "age_seconds": null },
  "notes":   ["latency_ms is measured from the MCP host (your laptop), not from the relay node — re-test from the node for geo-accurate values"]
}
```

> Тело whitelist кешируется на диск в `data/whitelist-cache.json` (TTL по
> умолчанию 24h, переопределение через `cache_ttl_hours`). Verdicts НЕ
> кешируются — каждый вызов делает свежие пробы на своём срезе. NB:
> latency меряется с тачки где запущен MCP, а НЕ с РФ-relay-ноды; для
> гео-релевантной задержки используй `xray_test_reality_live` прямо с
> ноды (или ssh + curl на ней).

Также один MCP **ресурс**: `xray://docs/index`.

## Примеры

### Пример 1 — линт сломанного каскадного конфига

Промпт:

> Прогони линт по этому xray конфигу и скажи что не так.
>
> ```json
> {
>   "inbounds":[{"tag":"in1","port":443,"protocol":"vless","settings":{"clients":[{"id":"00000000-0000-4000-8000-000000000000","flow":"xtls-rprx-vision"}]},"streamSettings":{"network":"ws","security":"reality","realitySettings":{"target":"yandex.com","privateKey":"short","shortIds":["zz"],"serverNames":["yandex.com"],"fingerprint":"netscape"}}}],
>   "outbounds":[{"tag":"out","protocol":"freedom"}],
>   "routing":{"rules":[{"type":"field","outboundTag":"missing","domain":["geosite:tinkoff-bank"]}]}
> }
> ```

Ответ `xray_lint` (выжимка):

```json
{
  "summary": { "error_count": 6, "warn_count": 5, "info_count": 2 },
  "issues": [
    { "rule": "reality_pubkey_format", "severity": "error",
      "path": "/inbounds/0/streamSettings/realitySettings/privateKey",
      "message": "REALITY privateKey must be 43 base64url chars, got 'short'" },
    { "rule": "reality_shortid_format", "severity": "error",
      "path": "/inbounds/0/streamSettings/realitySettings/shortIds/0",
      "message": "shortIds[0] 'zz' must be hex (0..16 chars, even length)" },
    { "rule": "flow_requires_specific_transport", "severity": "error",
      "path": "/inbounds/0/streamSettings/network",
      "message": "flow=xtls-rprx-vision requires raw/tcp transport, got 'ws'" },
    { "rule": "routing_dangling_outbound", "severity": "error",
      "path": "/routing/rules/0/outboundTag",
      "message": "outboundTag 'missing' does not match any outbound tag" },
    { "rule": "tls_fingerprint_enum", "severity": "warn",
      "path": "/inbounds/0/streamSettings/realitySettings/fingerprint",
      "message": "'netscape' is not a known fingerprint (chrome/firefox/safari/ios/android/edge/360/qq/random/randomized)" },
    { "rule": "geo_unknown_category", "severity": "warn",
      "path": "/routing/rules/0/domain/0",
      "message": "geosite:tinkoff-bank is not in the bundled catalogue (typo? try geosite:category-ru)" }
  ]
}
```

### Пример 2 — ресёрч обхода РКН через GitHub

Промпт:

> Найди свежие xray issues про РКН/ТСПУ и предложи anti-DPI стек под Россию.

Шаг 1 — `xray_github_search`:

```jsonc
// args
{ "query": "RKN", "type": "issue", "repo": "all", "sort": "updated" }
// response (выжимка)
{
  "total_count": 47,
  "items": [
    { "repo": "XTLS/Xray-core", "number": 5747, "state": "open",
      "title": "REALITY blocked by TSPU after recent RKN update",
      "comments": 38, "reactions": { "+1": 21 },
      "snippet": "Starting last week our REALITY inbound on :443 stops responding after ~30s of traffic from RU clients. xhttp+REALITY survives longer than raw+vision…" },
    { "repo": "XTLS/Xray-core", "number": 5332, "state": "closed",
      "title": "RKN: shortId enumeration probe",
      "comments": 22,
      "snippet": "TSPU appears to brute-force shortIds. Recommendation: rotate, keep list >=4 entries, do not include empty string…" }
  ]
}
```

Шаг 2 — `xray_suggest_alternative`:

```jsonc
// args
{ "goal": "anti-dpi-russia" }
// response (выжимка)
{
  "recommendation": {
    "protocol": "vless",
    "transport": "xhttp",
    "security": "reality",
    "flow": null,
    "rationale": [
      "xhttp survives TSPU active probing better than raw/tcp+vision in 2025",
      "REALITY hides SNI; pick a target popular in RU (yandex.com, mail.ru)",
      "Rotate shortIds[] (>=4 entries, hex, no empty string)",
      "Keep packet padding default; do not enable kcp on top of REALITY"
    ]
  },
  "see_also": ["xray_fetch_topic transports/xhttp", "xray_fetch_topic features/reality"]
}
```

## Установка

```bash
npm i -g mcp-xray-pilot
```

Или из исходников:

```bash
git clone https://github.com/beekamai/mcp-xray-pilot.git
cd mcp-xray-pilot
npm install
npm run build
npm run fetch-docs
```

## Подключить к Claude Code

```bash
claude mcp add xray-pilot --scope user -- npx -y mcp-xray-pilot
```

С GitHub PAT (поднимает rate-limit `xray_github_*`, включает discussions):

```bash
claude mcp add xray-pilot --scope user --env GITHUB_TOKEN=ghp_xxx -- npx -y mcp-xray-pilot
```

Или из локального клона:

```bash
claude mcp add xray-pilot --scope user -- node /absolute/path/to/mcp-xray-pilot/dist/index.js
```

## Офлайн-кеш vs онлайн-обновление

Папка `data/docs/` едет в пакете. Каждый вызов `xray_fetch_topic`:

1. При `force_offline=true` → читает только упакованную копию.
2. Иначе → пробует upstream raw URL (10s таймаут). При HTTP 200 ответ
   перезаписывает markdown на диске, `fetched_at` обновляется. Последующие
   вызовы в том же процессе отдаются из in-memory кеша.
3. При любой сетевой ошибке → fallback на упакованную копию, возвращает
   markdown плюс `warning: "network fetch failed: …"`.

Bulk-обновление — `npm run fetch-docs -- --refresh`. Discover новых
страниц в upstream без записи — `npm run fetch-docs -- --discover`.

### Поддержание кеша актуальным

Три способа держать `data/docs/` в синхроне с upstream:

1. **На каждый запрос** — `xray_fetch_topic` сам ходит в сеть и при HTTP
   200 перезаписывает on-disk копию. Ничего делать не надо.
2. **Bulk через MCP-тул** — позови `xray_refresh_cache`:
   - `{ "scope": "stale", "max_age_days": 30 }` (default) — только
     устаревшие старше N дней.
   - `{ "scope": "all" }` — все ~60 страниц.
   - `{ "scope": "category", "category": "transports" }` — одна категория.
   - `"discover": true` — дополнительно вернёт список slug'ов, которые
     появились upstream, но отсутствуют в `DOCS_CATALOGUE` (`src/docs.ts`).
3. **CI weekly cron** — `.github/workflows/refresh-docs.yml` каждый
   понедельник 06:00 UTC гоняет `npm run fetch-docs -- --refresh` и
   открывает PR если что-то поменялось (есть `workflow_dispatch` для
   ручного триггера).

## Опциональный `GITHUB_TOKEN`

`xray_github_search` и `xray_github_fetch_issue` работают анонимно, но
GitHub API лимитирует unauth запросы **60/час**. Установка `GITHUB_TOKEN`
(любой classic или fine-grained PAT, для публичных репо scope не нужен)
поднимает лимит до **5000/час** и дополнительно включает поиск/чтение
**discussions** (GraphQL, у него нет anon-доступа).

```bash
export GITHUB_TOKEN=ghp_xxx           # Linux / macOS
$env:GITHUB_TOKEN = "ghp_xxx"         # PowerShell
```

Когда `X-RateLimit-Remaining` падает ниже 10, тул возвращает inline-warning
в ответе.

## Модель безопасности

Пакет крутится внутри процесса MCP-хоста, и единственное «опасное»
действие на диск/сеть — `xray_test_reality_live`, который скачивает
официальный бинарь **xray-core** из
[XTLS/Xray-core releases](https://github.com/XTLS/Xray-core/releases/latest)
в `~/.cache/mcp-xray-pilot/xray-bin/` и запускает его локально на
ephemeral портах.

**Approvals.** Встроенного permission-prompt'а в пакете нет — это by
design. Все тулколлы гейтятся MCP-хостом (Claude Code / Claude Desktop /
Cursor) — у каждого хоста свой UX (per-call confirm, allowlist и т.д.).
Пиньте конкретную версию в MCP-конфиге (`mcp-xray-pilot@0.15.0`, а не
просто `mcp-xray-pilot`), чтобы вредоносный npm-релиз не подкатился
тихо.

**Целостность бинаря xray.** Начиная с v0.15:

1. Рядом со скачиванием zip-а тянется `<asset>.zip.dgst` из того же
   GitHub-релиза, и **до** распаковки zip проверяется против строки
   `SHA2-256=`. Если хэши не совпали — payload переименовывается в
   `xray.rejected-<timestamp>.zip` для разбора, и тул abort'ится.
2. Если хочется жёстче чем «доверяю тому что XTLS залил прямо сейчас» —
   задайте `XRAY_PILOT_PINNED_HASH` с известным SHA-256. Тогда `.dgst`
   игнорируется, проверяется ваше захардкоженное значение. Это защищает
   от гипотетического compromised XTLS-релиза, где злоумышленник залил
   согласованную пару zip+dgst.

   ```bash
   # Linux / macOS — снять хэш один раз, потом пиннуть.
   curl -sL https://github.com/XTLS/Xray-core/releases/latest/download/Xray-linux-64.zip.dgst \
     | awk '/^SHA2-256=/ {print $2}'
   export XRAY_PILOT_PINNED_HASH=23cd9af9...
   ```

   ```powershell
   $env:XRAY_PILOT_PINNED_HASH = "23cd9af9..."
   ```

**Чего пакет НЕ делает.** XTLS не GPG-подписывает `.dgst`, поэтому
атакер с правами на releases-страницу может выкатить согласованную пару
zip+dgst. Защита от этого — pinned-hash. Sandbox для запущенного xray
пакет не делает — бинарь работает с правами процесса MCP-хоста. Хотите
жёстче — запускайте MCP-хост в контейнере или под отдельным юзером.

**`GITHUB_TOKEN`.** Опционально, используется только
`xray_github_search` / `xray_github_fetch_issue`. Рекомендуется
**fine-grained PAT с read-only scope для публичных репо** вместо
classic PAT. Токен читается из env при вызове и пакетом не сохраняется.

## Roadmap

См. [ROADMAP.md](./ROADMAP.md) — все вехи v0.1–v0.11 закрыты.

## Лицензия

MIT.
