> ## Documentation Index
> Fetch the complete documentation index at: https://docs.proxyjam.com/llms.txt
> Use this file to discover all available pages before exploring further.

# MCP Server

> Use ProxyJam from Claude, OpenAI agents, or any MCP client — without writing HTTP code.

The MCP server is a Model Context Protocol façade over the public API.
The catalog, order and wallet actions available to an `X-API-Key` holder
are exposed as 16 MCP **tools** — so LLM agents can list catalog offers,
place orders, manage proxies, and inspect wallet balances directly,
without you hand-writing HTTP calls.

The server is a thin proxy in front of `/public/v1`: same auth model,
same idempotency, same rate limits.

```
Endpoint:   https://mcp.proxyjam.com/mcp
Transport:  Streamable HTTP
Auth:       X-API-Key header on every request
```

<Note>
  The endpoint is the `/mcp` path, not the bare host: `https://mcp.proxyjam.com/`
  on its own returns `404`.
</Note>

## Get an API key

Generate one in your dashboard → API Keys, then keep the raw value
safe (it is shown only once). See
[Authentication](/overview/authentication) for the full flow.

```bash theme={null}
curl -X POST https://api.proxyjam.com/public/v1/api-keys \
  -H "Authorization: Bearer <JWT>" \
  -d '{"name": "my-mcp-client"}'
```

## Connect

### Claude Code

```bash theme={null}
claude mcp add --transport http proxyjam \
  https://mcp.proxyjam.com/mcp \
  --header "X-API-Key: pj_..."
```

Then in a Claude Code session: `/mcp` to see registered servers, or
just ask Claude something like *"list my proxy orders"* — it will
discover and call the right tool.

### Claude Desktop

Claude Desktop's config file launches local (stdio) servers only, and its
custom-connector dialog has no field for a request header. Bridge to the
remote endpoint with [`mcp-remote`](https://www.npmjs.com/package/mcp-remote)
(needs Node.js). Edit `claude_desktop_config.json`
(macOS: `~/Library/Application Support/Claude/`,
Windows: `%APPDATA%\Claude\`):

```json theme={null}
{
  "mcpServers": {
    "proxyjam": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://mcp.proxyjam.com/mcp",
        "--header",
        "X-API-Key:${PROXYJAM_API_KEY}"
      ],
      "env": {
        "PROXYJAM_API_KEY": "pj_..."
      }
    }
  }
}
```

The key goes through `env` rather than straight into `args` so the header
has no space in it — some platforms split arguments on spaces. Restart
Claude Desktop; the ProxyJam tools appear in the tools menu.

### OpenAI Agents SDK

```python theme={null}
from agents import Agent
from agents.mcp import MCPServerStreamableHttp

server = MCPServerStreamableHttp(
    name="proxyjam",
    params={
        "url": "https://mcp.proxyjam.com/mcp",
        "headers": {"X-API-Key": "pj_..."},
    },
)

agent = Agent(
    name="ProxyJamAssistant",
    instructions="Help the user manage their ProxyJam proxies.",
    mcp_servers=[server],
)
```

### Generic FastMCP client (Python)

```python theme={null}
from fastmcp import Client

async with Client(
    "https://mcp.proxyjam.com/mcp",
    headers={"X-API-Key": "pj_..."},
) as client:
    tools = await client.list_tools()
    result = await client.call_tool("list_offers", {"kind": "residential"})
    print(result.content[0].text)
```

## Tool catalog

<Info>
  **Money-spending tools default to dry-run.** `create_proxy_order`,
  `extend_proxy_period`, and `buy_proxy_bandwidth` require
  `confirm=true` to actually charge the wallet. Without it they
  return a preview with the offer, current balance, and what would
  happen. Always supply an `idempotency_key` (any unique string,
  typically a UUIDv4) so retries replay the stored answer instead of
  double-charging.
</Info>

### Catalog

#### `list_offers`

List available proxy offers, optionally filtered.

| Argument | Type | Default | Notes |
| - | - | - | - |
| `kind` | `residential` / `datacenter` / `mobile` | `null` | See `ref://offer-kinds`. `static` and `rotating` are `mode` values, not kinds. |
| `country` | `XX` (ISO-3166-1 alpha-2) | `null` | E.g. `US`, `GB`. |
| `region` | string | `null` | Exact match against a `locations` entry's region (max 100 chars). Combine with `city` to require the same location. Offers with an empty `locations` are excluded. |
| `city` | string | `null` | Exact match against a `locations` entry's city (max 100 chars). Combine with `region` to disambiguate — the same city name can appear under more than one region. Offers with an empty `locations` are excluded. |
| `response_format` | `csv` / `json` | `csv` | CSV is \~50% cheaper in LLM tokens. |

Returns a flat table — one row per offer, with `kind`, `id`, `name`,
`country_code`, `mode`, `ip_version`, `is_available`, `currency`,
`locations` (JSON-encoded list of `{region, city}`, empty when none is
known), and `options` (JSON-encoded list of tariff variants; each variant
carries a `price` in the row's `currency`). Use `id` as `offer_id` in
`create_proxy_order`.

<Note>
  A location narrows the choice of **ISP**, not a delivery guarantee: an
  order is placed against the whole pool of the matched ISP, so a specific
  city is not guaranteed. While location filtering is not enabled for your
  platform, passing `region` or `city` returns the same **403** the HTTP API
  would (check the `isp.city.enable` flag via `GET /flags` first).
</Note>

### Orders

#### Reads

| Tool | Arguments | Purpose |
| - | - | - |
| `list_my_orders` | `status?`, `limit=50` (max 100), `offset=0`, `response_format` | Paginated list of your orders. |
| `get_order` | `order_id` | Full detail of one order incl. proxy access. |
| `get_my_traffic_summary` | `response_format` | Aggregate traffic across all active proxies. |
| `get_order_traffic` | `order_id` | Traffic for a single order. |

#### Destructive — dry-run by default

These require `confirm=true` + an `idempotency_key` to actually
execute. Without `confirm=true` they return a preview and don't touch
the API mutation path.

| Tool | Arguments |
| - | - |
| `create_proxy_order` | `offer_id`, `quantity` (1-50), `idempotency_key`, `tarification_index?`, `period_days?` (1-360), `payment_method=wallet` (`wallet`/`cryptocloud`/`stripe`/`telegram_stars`/`rollypay`), `payment_currency?` (`USD`/`STAR`), `promo_code?`, `auto_renew=false`, `confirm=false` |
| `extend_proxy_period` | `order_id`, `idempotency_key`, exactly one of `months` (1-24) or `tarification_index`, `payment_currency?` (`USD`/`STAR`), `confirm=false` |
| `buy_proxy_bandwidth` | `order_id`, `bandwidth_gb` (1-5000), `idempotency_key`, `confirm=false` |

`idempotency_key` is 8–64 characters.

* **Picking a tariff.** `tarification_index` is an index into the offer's
  `options` from `list_offers` and is the primary way to choose a price and
  period for every offer kind; mobile rotating offers require it.
  `period_days` is the legacy alternative for residential and datacenter
  offers and must equal one of the offer's option periods exactly.
* **Extending.** Static (residential/datacenter) proxies extend by
  `months`; mobile rotating proxies extend by `tarification_index`, and
  only those can be paid with `payment_currency="STAR"`.
* **Bandwidth.** `buy_proxy_bandwidth` applies to static proxies only; a
  mobile rotating order rejects it.
* **External payment methods** (`cryptocloud`, `stripe`, `telegram_stars`,
  `rollypay`) return a payment URL instead of charging the wallet — see
  [Payment methods](/overview/payment-methods).

#### Non-destructive mutations

No dry-run because nothing is charged.

| Tool | Arguments |
| - | - |
| `set_auto_renew` | `order_id`, `enabled` |
| `set_proxy_whitelist` | `order_id`, `whitelisted_ips` (list of IPv4/IPv6) |
| `change_proxy_protocol` | `order_id`, `new_type` (`HTTP` / `SOCKS5`) |
| `sync_proxy_state` | `order_id` — force-pull state from the upstream provider |

### Wallet

| Tool | Arguments | Purpose |
| - | - | - |
| `list_my_wallets` | `response_format` | All currencies you hold. |
| `get_wallet_balance` | `currency` (`USD` / `STAR`) | Single-currency balance. |
| `list_wallet_transactions` | `currency`, `limit=100` (max 1000), `offset=0`, `response_format` | Transaction history. |
| `get_transaction` | `transaction_id` | Single transaction detail. |

## End-to-end example: place an order

Three calls — discover, preview, confirm.

```python theme={null}
# 1. Discover available offers
offers = await client.call_tool("list_offers", {"kind": "residential"})
# CSV with kind, id, name, country_code, currency, options, …
# `options` lists the tariffs; pick one by its position (0, 1, …)

# 2. Preview a purchase (no charge happens)
preview = await client.call_tool("create_proxy_order", {
    "offer_id": 42,
    "quantity": 5,
    "idempotency_key": "order-2026-05-25-abc",
    "tarification_index": 0,
    "payment_method": "wallet",
    # confirm omitted → defaults to false
})
# Returns: {"dry_run": true, "matched_offer": {...}, "your_wallets": [...]}

# 3. After the user approves, confirm with the SAME idempotency_key
order = await client.call_tool("create_proxy_order", {
    "offer_id": 42,
    "quantity": 5,
    "idempotency_key": "order-2026-05-25-abc",   # same string
    "tarification_index": 0,
    "payment_method": "wallet",
    "confirm": True,
})
# Returns: {"orders": [{"id": "...", "status": "paid", ...}, ...]}
```

If step 3 errors out (network hiccup, etc.) you can safely retry with
the same `idempotency_key` — the API returns the stored answer
instead of creating a second batch of orders.

## Reference resources

Static enums are exposed as MCP resources so they don't bloat tool
descriptions. Most clients let the agent fetch resources on demand via
`resources/read`.

| URI | Contents |
| - | - |
| `ref://order-statuses` | `new`, `waiting_payment`, `paid`, `provisioned`, `provisioning_failed`, `cancelled`, `refunded`, `expired`. |
| `ref://currencies` | `USD`, `STAR` (Telegram Stars). |
| `ref://proxy-types` | `HTTP`, `SOCKS5`. |
| `ref://offer-kinds` | `residential`, `datacenter`, `mobile`. |
| `ref://payment-methods` | `wallet`, `cryptocloud`, `stripe`, `telegram_stars`, `rollypay` (SBP). |

```python theme={null}
contents = await client.read_resource("ref://order-statuses")
print(contents[0].text)
```

## Response formats

Tools that return tabular data accept `response_format`:

* **`csv` (default)** — one header row, one data row per record;
  nested values are JSON-encoded inside a single cell. Roughly half
  the tokens of equivalent JSON.
* **`json`** — list of objects; use when you need to script over
  nested structures.

Non-list tools always return JSON.

### Paginated tools

`list_my_orders` and `list_wallet_transactions` always wrap the rows
in a pagination envelope so the LLM knows when more pages exist:

```json theme={null}
{
  "pagination": {
    "total": 120,        // total rows on the server (when known)
    "limit": 50,         // page size used
    "offset": 0,         // where this page started
    "returned": 50,      // rows actually returned
    "has_more": true     // call again with offset += returned
  },
  "rows_csv": "id,status,...\norder-1,paid,...\n..."   // csv mode
  // or
  "rows": [{...}, ...]                                  // json mode
}
```

If `has_more` is `true`, request the next page with
`offset = previous offset + returned`. Loop until `has_more` is
`false`.

## Errors

Tools surface upstream errors verbatim as `ToolError` with the full
HTTP response body and the `X-Correlation-ID`. Example:

```
HTTP 400 Bad Request (correlation_id=req_…): {
  "detail": "Insufficient wallet balance: have 5.00 USD, need 12.50"
}
```

The LLM sees the entire body, so it can either retry with a corrected
argument or pass the error back to the user with a clear explanation.

## Idempotency

Every destructive tool that hits a `POST` endpoint takes
`idempotency_key`. The MCP server forwards it as `Idempotency-Key` on
the HTTP request, and the API persists `(key, response)` pairs.
Replays return the original answer (so confirm=true on a previously
created order won't double-charge).

Use a fresh key per **logical** operation:

* Place an order → one key.
* A retry of the same order → same key.
* A *different* order → new key.

## Out of scope

The MCP server intentionally does **not** expose:

* `/users/*` and `/auth/*` — account management requires JWT, not an
  API key.
* `/payments/*` — payment intents are user-initiated through the
  dashboard.
* `/api-keys/*` — you cannot manage keys through a key (security).

If you need any of these, use the [HTTP API](/api/auth/sign-in)
directly.

## Troubleshooting

**Tool calls return `Missing API key`** — the `X-API-Key` header isn't
reaching the server. In Claude Desktop check the `headers` block in
`claude_desktop_config.json`; in Claude Code re-run `claude mcp add`
with `--header`.

**`HTTP 401 Invalid API key`** — the key is wrong, revoked, or
expired. Generate a new one via the dashboard.

**Tools list is empty** — the MCP client cached an old session.
Restart Claude Desktop / reconnect the Claude Code server.

**`HTTP 422`** on `list_offers` — only the values listed in
`ref://offer-kinds` are accepted for `kind`. Earlier docs mentioned
`mobile_rotating` — that's wrong, use `mobile`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.