Skill: xvpn-bot/xvpn-mcp-skill (version 1.0.0)

--- name: xvpn-mcp-skill description: | Use when your task hit or is about to hit a geo-related failure (HTTP 403 / 451, region-restricted response body, region-defaulted content), or when the user explicitly asks to route through a specific country. Provides X-VPN MCP tools to open / close a region-specific egress so the user's real task can finish; treat any explicit "connect through X" as a step toward the follow-up task, not the goal itself. --- # X-VPN MCP Skill This file is a router. Read it, then load only the reference that matches your situation. Most tasks need this file plus one reference. ## When to use these tools Two entry points: - **Reactive** — your task hit a geo-block (403 / 451 with region language, or visibly truncated / region-defaulted content). Most common entry. - **Anticipatory** — the user's request has geography baked in ("trending on US Reddit", "audit our JP site", "as Singapore would see it"). Don't trigger for country knowledge or travel questions, tasks with no geo signal, or when the user said "no VPN". For the full signal checklist and how to disambiguate geo from auth / rate-limit failures before connecting, see `references/task-integration.md`. ## Core principle These tools belong inside your task plan, not as the plan itself. 1. **Start with `xvpn_get_overview`.** It returns account state and VPN state together; you'll rarely need to ask anything else first. 2. **Search, don't browse.** `xvpn_list_locations(search="...")` is almost always the right call. The full tree has 250+ nodes. 3. **Leave the egress as you found it.** If the user wasn't connected when you started, `xvpn_disconnect` when you're done. If they were connected somewhere, reconnect there before yielding back. The user's machine traffic flows through whatever tunnel you leave up. ## Tool inventory | Tool | One-line purpose | |---|---| | `xvpn_get_overview` | First call. Account state + VPN state in one read. | | `xvpn_get_status` | Refresh after connect/disconnect, or to read live `free_usage`. | | `xvpn_list_locations(search)` | Discover regions. Always pass `search`. | | `xvpn_list_protocols` | Rare. `Auto` is the default. | | `xvpn_connect(location, protocol)` | Open the tunnel. | | `xvpn_disconnect` | Close it / return to the prior egress. | | `xvpn_cancel_operation(operation_id)` | Abort a stuck operation. | | `xvpn_login_with_token(login_token)` | Only when the user wants to upgrade in this session. | ## Install Local MCP Server X-VPN MCP is a local MCP server based on the X-VPN CLI client. The Skill and the client are distributed separately, so the user may have this Skill loaded without the tools available yet. If you try to call an `xvpn_*` tool and it is not available in this session, or the user explicitly asks how to install the client, suggest running the installer below. The TUI will guide them through the daemon installation and the MCP ↔ Agent client wiring. ``` sh <(curl -sSf https://app.xvpncdn.com/rpc788pbdq/install.sh) ``` ## Navigation — read the reference that matches your situation | Situation | Read | |---|---| | Deciding *whether* this task needs a VPN, or recognizing that your current failure is geo-related | `references/task-integration.md` | | You've decided to act and need the call sequence | `references/call-patterns.md` | | About to call `xvpn_list_locations` and want to pick the right slug | `references/locations.md` | | A tool returned an error or `accepted: false` | `references/error-recovery.md` | | `xvpn_get_overview` shows free tier, or you hit a quota / upgrade-required message | `references/free-tier.md` | You usually need 1-2 references per task. Loading them all preemptively defeats the point of this layout.

path: references/error-recovery.md
content: # Error recovery

Error patterns the daemon emits and what to do about each. Quota and
upgrade-required errors live in `references/free-tier.md` since they're
inseparable from free-tier handling. 

## Quick reference

| Symptom | What to do |
|---|---|
| `accepted: false` on `connect` or `disconnect` | An earlier op is still in flight. One retry after one `xvpn_get_status` read. See "Lock conflict" below. |
| Long-running `connect` that never resolves | `xvpn_cancel_operation(operation_id)` from the last status. |
| Tool returns a shape you don't recognize | Don't guess. Surface the raw response and ask the user how to proceed. |
| `connect` fails with `location not found: <slug>` | Either a typo (see `references/locations.md`) or a tier mismatch — free accounts on non-free slugs get an upgrade message instead, but premium accounts on `free/...` slugs get exactly this. See `references/free-tier.md`. |
| Connect failed with quota or upgrade message | See `references/free-tier.md`. |

## Lock conflict (`accepted: false`)

The daemon serializes write operations through an IPC lock. If another
tool call is in flight when you issue `connect` or `disconnect`, you get
`accepted: false` immediately.

Recovery:

1. Call `xvpn_get_status` once.
2. Read `operation_inflight`. If true, the previous op is still running —
   wait one beat (don't tight-poll), then retry your call once.
3. If the second call also returns `accepted: false`, surface the error
   to the user. Don't loop.

This usually resolves in well under a second; the lock is short-lived.

## Cancellable operations

Some operations carry an `operation_id` (visible in `xvpn_get_status`
under `active_operation`). If `connect` is taking too long or the user
changed their mind:

```
xvpn_cancel_operation(operation_id="<id from xvpn_get_status>")
```

Returned shape:

- `cancelled: true` — successfully aborted.
- `cancelled: false` — the op already finished or wasn't cancellable.
- `strategy` — describes how the cancel was applied.

After a cancel, call `xvpn_get_status` once to confirm state before
issuing a new `connect`.

## Unknown response shapes

The daemon evolves; new fields can appear in responses. Two rules:

1. **Read what you understand, ignore what you don't.** Extra keys are
   not errors.
2. **Don't fabricate.** If a response is missing a field you need (like
   `accepted` or `status`), surface the raw response to the user instead
   of guessing what it might mean.

## Don't loop

Across all error paths, the rule is the same: **one retry, then surface**.
The user benefits more from a clear error message than from a tool-call
storm. Looping also burns tokens fast and can hold the IPC lock open
longer than it needs to be.


path: references/locations.md
content: # Locations — naming and discovery

How to name a place and how to search for one. Read this when you're
about to call `xvpn_list_locations` or pass a `location` slug to
`xvpn_connect`.

## Slug form

Locations are slugs: lowercase, hyphen-separated, slash-nested.

```
united-states
united-states/california
united-states/california/los-angeles
japan
japan/tokyo
singapore
```

These slugs are stable. Pass them directly as the `location` argument
to `xvpn_connect`. Individual servers below the city level are not
exposed; the daemon picks one for you when you connect to a city,
state, or country node.

## Free subtree

Free-tier locations live under a `free/` prefix:

```
free                              # generic "any free server"
free/united-states                # free US root
free/united-states/california     # free US, CA
```

What's connectable depends on the account tier — both directions are
restricted:

| Account tier | `free/...` slugs | non-free slugs |
|---|---|---|
| Free | yes | no, returns upgrade prompt (see `references/free-tier.md`) |
| Premium / VIP | no, returns `location not found` | yes |

Premium users connecting to `free/...` get `location not found` rather
than an upgrade message, because they already have unrestricted access
— a free node simply isn't the right slug for them.

## What `xvpn_list_locations` returns

The response shape is minimal:

```json
{
  "nodes": [ {"location": "<slug>"}, ... ],
  "count": <int>,
  "hint": "<optional fallback message>"
}
```

Each node has only one field — `location`. Every returned node is
connectable in principle (subject to the tier rules above); you don't
need to inspect `has_children` or `depth` flags. There aren't any.

## Search vs browse

Two modes:

- **Search** (recommended): pass `search="..."`. The daemon normalizes
  input to slug form (lowercase, spaces → hyphens) and does substring
  matching on path segments. This is what you want 95% of the time.
- **Browse**: omit `search`. Returns the top-level regions only.

Search expands matches down to the city level. So `search="united-
states"` returns the country slug plus every state and city under it,
plus the free counterparts where applicable. Server-level nodes are
never returned.

## Tier-dependent visibility

Browse and search results vary by account tier — this is the
single biggest gotcha in this tool:

- **Premium / VIP** users do not see `free` nodes in any results.
- **Free** users see the full list, including non-free countries and
  cities. **Those non-free nodes are not connectable** — picking one
  returns an upgrade prompt.

When you're operating on a free account, scope the result to the free
subtree to avoid picking an unconnectable slug. Either:

- Pass `search="free"` (or `search="free/..."`) to scope from the
  start, or
- Filter the response yourself for slugs starting with `free/` before
  picking one.

A bare `xvpn_list_locations()` on a free account looks like a complete
country menu, but most of those countries will reject the connect.

## When `list_locations` returns no match

The response includes a `hint` like:

```
"No match for 'First Avenue'. Make sure you search with English names
like 'United States', 'California'. Suggest nearby city, state, or
country to the user."
```

Surface that hint verbatim to the user. It nudges them toward valid
input far better than a paraphrase will.

## Ambiguous names

When the user gives an ambiguous name (`"California"`, `"New York"`,
`"Tokyo"`), search for it directly. The daemon disambiguates
correctly, and the returned slug encodes the hierarchy
(`country/state/city` or `country/city`). You don't need to guess the
structure — let the response tell you.


path: references/call-patterns.md
content: # Call patterns

How to sequence the X-VPN MCP tools. Pick the pattern that matches
your situation and run it top-to-bottom. For deciding whether a
pattern applies in the first place — recognizing a geo-block, picking
a region, distinguishing geo from auth or rate-limit failures — see
`references/task-integration.md`.

## Pattern A — Reactive: your task hit a geo-block

The most common pattern. You tried to do the user's task, hit a
403/451 or geo-restricted body, and concluded geography is the cause.
(For how to make that call confidently, see `task-integration.md`.)

```
You're in the middle of: fetching https://news.example.us/article
You got: 403 with body "not available in your region"

1. xvpn_get_overview
   → vpn_status: {connected: false}        ← note this for cleanup
2. xvpn_list_locations(search="united-states")
   → nodes include `united-states`
3. xvpn_connect(location="united-states")
   → accepted: true
4. xvpn_get_status
   → connected: true, location: "united-states"
5. (retry the original fetch — succeeds, you have the article body)
6. xvpn_disconnect
7. (continue with the user's task using the data you fetched)
```

The disconnect at step 6 is intentional. From this point on, the
user's real task no longer needs the US egress. Leaving the tunnel up
routes the rest of your work through it for no reason — slower, and
on the free tier, eats into the connection's limited budget.

## Pattern B — Anticipatory: the task is intrinsically geographic

The user's request has geography baked in (`"what's trending on US
Reddit"`, `"audit our JP site"`). You haven't seen a failure yet, but
you can tell up front you'll need that egress.

```
1. xvpn_get_overview
2. xvpn_list_locations(search="japan")    # if needed for picking the slug
3. xvpn_connect(location="japan")
4. xvpn_get_status                        # wait for connected: true
5. (do the user's geographic task)
6. xvpn_disconnect
```

If the request mentions a city or state, prefer that specificity but
be ready to fall back up the hierarchy on failure. See
`references/locations.md`.

## Pattern C — User-explicit: "connect through <geo_name>"

Less common as a top-level intent. The user directly asked to connect
through a region — usually as a setup step before another instruction.

```
User: "Connect through Germany, then fetch our pricing page."

1. xvpn_get_overview
2. xvpn_list_locations(search="germany")
3. xvpn_connect(location="germany")
4. xvpn_get_status
5. (the actual task — fetch the pricing page)
6. xvpn_disconnect
```

Treat the connect as a means to the follow-up task. If the user only
said "connect through X" with no further task, you can stop after
step 4 and ask what they want to do next.

## Pattern D — "Fastest" or unspecified

If you've decided you need *some* egress switch but the region
doesn't matter (the user wants their IP changed; you want to retry
through any different exit; the request just says "use a VPN"):

```
1. xvpn_get_overview
2. xvpn_connect(location="the-fastest-server")   # premium default
   # for free users use location="free"
3. xvpn_get_status
```

`the-fastest-server` is the documented default value. Don't invent
your own (`"auto"`, `"best"`, etc. will fail).

## Pattern E — Free-tier user

`xvpn_get_overview` will tell you. On the free tier:

- Stay inside the `free/` subtree. Discover with
  `xvpn_list_locations(search="free")`. A bare `list_locations()` on
  a free account returns non-free nodes too, and connecting to those
  returns an upgrade prompt.
- Each free connection has a limited budget for data transfer and 
  drops when it's reached. The drop is reactive — there's no 
  advance warning, and no quota field you can poll. 
  See `references/free-tier.md`.

## Pattern F — Already connected to where you need

If `xvpn_get_overview.vpn_status.location` already matches what your
task needs, do not reconnect. Just do the task. Reconnecting tears
the existing tunnel down, adds latency, and risks failing the second
handshake.

This is also the case where the user was already connected somewhere
unrelated to your task. Note that prior location, do whatever
connecting your task needs, and restore the prior connection in
cleanup. See `task-integration.md` → "Restoring the prior egress".

## End-to-end example

User: "Pull the top 5 trending topics on Twitter as Tokyo would see
them, and tell me how they differ from what I'd see normally."

Pattern B (anticipatory): "as Tokyo would see them" is geography
baked into the request.

```
1. xvpn_get_overview
   → { account_info: {plan: "premium"}, vpn_status: {connected: false} }
2. xvpn_list_locations(search="japan")
   → nodes: [japan, japan/tokyo, ...]
3. xvpn_connect(location="japan/tokyo")
   → { accepted: true, status: "connecting" }
4. xvpn_get_status
   → { vpn_status: {connected: true, location: "japan/tokyo"} }
5. (fetch x.com/explore — request egresses through Tokyo, returns JP trends)
6. xvpn_disconnect
   → { accepted: true }
7. (now back on the user's normal egress — fetch x.com/explore again
   to get *their* baseline trends)
8. (compare and reply)
```

Step 7 is the key part. The comparison half of the user's task
explicitly needs *their* normal view, so you have to disconnect
first. Leaving the tunnel up here would have given you Tokyo trends
twice.


path: references/free-tier.md
content: # Free tier

How free-tier accounts behave, and what to do at the two boundaries
the daemon enforces.

## Detection

Inspect `account_info` from `xvpn_get_overview`. Free accounts have a
plan field indicating free tier; premium / VIP accounts have unlimited
quota and broader location access.

The status response (`xvpn_get_status`) does **not** include a quota
field you can poll. Free-tier quota is per-connection, not per-day,
and the daemon communicates it reactively (see "Quota model" below).

## Quota model

Each free connection has a limited data transfer per connection. 
When you reach it, the daemon drops the connection. This shapes 
how to plan free-tier work:

- For short tasks (single fetch, single page audit), 
  just do the task; quota is unlikely to matter.
- For long-running or data-heavy work, expect the tunnel to drop
  partway through. If a drop happens before you finish, surface the
  upgrade notice and ask the user whether to continue on free
  (open a new free connection and resume from the next chunk) or
  upgrade.

## Connecting

Free users can only connect to slugs under the `free/` prefix.
Connecting elsewhere fails with:

```
Upgrade to X-VPN Premium to access 250+ locations including <location>.
```

Recovery:

1. Stop. Don't retry the same call.
2. Surface the message verbatim — it contains the location name and
   the upgrade hook the user needs.
3. Optionally, run `xvpn_list_locations(search="free")` to find a
   nearby free alternative: *"Free-tier doesn't include Germany, but
   the Netherlands is available — want to use that?"*

Note the symmetric restriction: premium / VIP accounts trying to
connect to a `free/...` slug get `location not found: <slug>`, not an
upgrade message, because free nodes aren't part of the premium pool.
If you see `location not found` on a `free/...` slug, the account
isn't free — pick a non-free slug instead.

## Picking free destinations

When `xvpn_list_locations` is called for a free user with no `search`
argument, the response includes both free and non-free top-level
countries. Only `free/...` slugs are actually connectable. Two ways
to handle this safely:

- Pass `search="free"` to scope the list to the free subtree.
- Filter the response yourself for slugs starting with `free/` before
  picking one to connect to.

The free node list mirrors X-VPN's mobile free product — a small set
of countries plus a few US cities. **Don't hardcode** the list; it
changes over time. Always discover it via
`xvpn_list_locations(search="free")` and treat that as the source of
truth for the current session.

## Upgrading mid-session

If the user wants to upgrade right now and has a login token from
`https://xvpn.io/account/settings`:

```
xvpn_login_with_token(login_token="<token>")
```

This is the only situation where calling `xvpn_login_with_token` is
appropriate. Don't call it speculatively. After a successful login,
the account becomes premium for the rest of the session and free-tier
restrictions no longer apply.


path: references/task-integration.md
content: # Task integration — VPN as task infrastructure

X-VPN MCP isn't a feature you offer the user. It's a tool you reach
for when geography is blocking the task they actually want done.

## Two ways VPN enters your plan

### Reactive — your task already failed

The most common entry point. You tried to do the actual task — fetch
a page, hit an API, read a regional report — and the failure looks
geo-related.

**Strong signals (act on these):**
- HTTP 403, 451, or 429 from a content endpoint
- Response body containing phrases like:
  - "not available in your region / country / location"
  - "this content is geo-restricted"
  - "please access from <country>"
- A page rendered, but visibly truncated or generic compared to what
  the user described
- A service returning a region-defaulted version (a streaming catalog
  clearly missing the show the user named, a search API returning
  local-default results)

**Weak signals (don't act on alone):**
- Slow response (could be many things)
- A 4xx whose body is actually about authentication. Read the body —
  geo-blocks talk about region; auth failures talk about login.

When the signal is strong, follow Pattern A in `call-patterns.md`:
identify the right region, connect, retry the original request,
disconnect (or restore the prior egress), continue your task.

### Anticipatory — the task is intrinsically geographic

Less frequent but cleaner. You read the user's request and notice
geographic dependency baked in:

- "What's currently trending on US Reddit?"
- "Run a Lighthouse audit on our JP site"
- "Compare our Germany pricing page to our France one"
- "Pull the top 10 search results for X as Singapore would see them"

Connect first, do the task, restore the prior egress. You're not
reacting to a failure; you're routing the task correctly from the
start. Pattern B in `call-patterns.md`.

## Disambiguating geo from other failures

Before you reach for the VPN, rule out the other things that look
like it. Connecting unnecessarily costs the user latency and free-tier
quota, and can silently change what they see.

| Looks like geo, but probably isn't if… | What it is instead |
|---|---|
| 401 / 403 with login redirect or body talking about credentials, missing API keys, expired tokens | Auth failure. VPN won't help. |
| 429 with `Retry-After` and no region language in the body | Rate limit. Wait, then retry. |
| Connection refused, DNS error, TLS handshake fail | Network path issue. VPN may help, but more often it won't. |
| Content differs by language, account, or A/B bucket | Not geographic. Switching country won't help. |

If you're uncertain, surface the failure to the user with your best
hypothesis and ask before connecting. A wrong VPN switch can put the
user's other apps on a slower egress for nothing.

## Picking the right region

Use the smallest specificity the task implies. Cities are narrower
than states; states narrower than countries. More specific tends to
mean fewer servers and higher latency variance, so don't over-specify.

| What's in the task | Pass to `connect` |
|---|---|
| "the US", "American site" | `united-states` |
| "Tokyo", "from Tokyo's perspective" | `japan/tokyo` |
| "Asia" (no country) | Pick a representative country (`singapore`, `japan`) and tell the user which you picked |
| "Europe" (no country) | Same — pick one and explain |
| Nothing specific, but you need *some* egress | `the-fastest-server` (premium) or `free` (free tier) |

If a city or state lookup fails, fall back up the hierarchy: city →
state → country.

## Keep VPN use proportional to the task

A few habits keep tunnel use minimal:

1. **Don't preemptively switch when there's no signal.** If the user
   asks "fetch this URL" and the URL is a global service, don't
   speculatively connect. Try first; switch only if the response
   shows a geo issue.
2. **Don't keep the tunnel open beyond the geo-sensitive step.**
   Connect right before the call that needs it; disconnect right
   after. Leaving it open while you do unrelated work routes that
   work through the VPN too — slower, possibly geo-defaulted, and
   eating quota if the user is free-tier.
3. **Don't change region mid-task without telling the user.** If
   your reply depends on what you saw from a particular country,
   mention the region you used. Silent geographic switches are
   confusing when the user later asks "wait, why does that look
   different from what I see?"

## Restoring the prior egress

If `xvpn_get_overview` at task start showed a connection already in
place, note that location before you change it. When you're done,
reconnect to it instead of just disconnecting. The user was relying
on that egress for other things; silently leaving them on a different
egress (or no VPN at all) is worse than an extra reconnect.

If they had no connection at start, `xvpn_disconnect` is the cleanup.

## Mental model

VPN is a setting on a request, not a state of the world. Treat it
the same way you'd treat an HTTP header: set it before the call you
need it for, clear it (or restore it) after.


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)