Short answers
What does MCP do here?
The Model Context Protocol is an open standard for giving a model tools. It is not a proxy protocol and it does not carry your traffic: the MCP server exposes account and connection operations as tools, and the agent then connects through the gateway with an ordinary HTTP or SOCKS5 proxy URL.
Which tools does the server expose?
get_traffic, list_countries, build_proxy_url, test_connection, buy_traffic, get_pricing and get_status. They are the same routes the dashboard uses, so anything an agent can do through them you can reproduce with curl.
How does the agent pick a country it can actually use?
list_countries reads the live per-country stock, per pool. An agent that calls it first never asks for a country with nothing online, which is the single most common avoidable failure: the gateway answers 502 with E_NO_STOCK_COUNTRY rather than substituting a neighbouring country.
Can an agent buy traffic on its own?
buy_traffic spends the account balance and needs two things before it will do anything: confirm: true and an idempotency key. The confirmation makes a purchase a deliberate step rather than a side effect, and the key means a retried tool call cannot buy twice.
Should the agent rotate its IP during a run?
Usually not. One sticky session for the length of a task keeps cookies, consent state and a shopping flow coherent; rotating mid-task throws away the state the agent just built. Use a new session name for the next task instead.
What does an agent run cost in GB?
It depends almost entirely on whether the agent renders pages. A tool-driven agent fetching HTML and JSON spends tens of kilobytes per step; the same agent driving a headless browser spends megabytes per page. Both are metered the same way, in both directions at the gateway.
Does the API key go to the model?
Give the MCP server the key through its environment, not through a prompt. The server holds it and the model calls tools; nothing about MCP requires a key to appear in the conversation, and nothing should put it there.
What an agent needs that a plain API key does not give it
Agents fail on the web for dull, geographic reasons. The page renders in the wrong language, prices arrive in the wrong currency, a country-restricted page refuses, or fifty parallel runs share one address and get rate limited together.
The fix is a country-accurate exit per run, chosen at the moment of the run. That is a decision the agent has to make while it works, which is exactly what a tool call is for.
The tools, and what each one answers
- list_countries
- Countries with devices online now, per pool. Call it before choosing, not once at startup.
- build_proxy_url
- Validates pool, country, rotation, session and protocol, and returns a usable proxy URL.
- test_connection
- Sends one request through the gateway from our side and reports whether it worked, the exit IP and the latency. It does not confirm the country. Rate limited to six a minute.
- get_traffic
- Balance and expiry:
gb_total,gb_used,gb_left, plus the credentials and the account limits. - buy_traffic
- Tops up from the account balance. Needs
confirm: trueand an idempotency key. - get_pricing
- The per-GB ladder, so an agent can reason about a top-up before making one.
- get_status
- Service status, for an agent deciding whether a failure is worth retrying.
Connecting the server
The server runs over stdio and takes two environment values: the API key and the API base URL. Most MCP clients read a block like this from their configuration file.
Keep the key in the client configuration or a secret store, never in a prompt. There is no sandbox: every key acts on your real balance, whatever its prefix.
{
"mcpServers": {
"portproof": {
"command": "npx",
"args": ["-y", "@portproof/mcp"],
"env": {
"PORTPROOF_API_KEY": "pk_...",
"PORTPROOF_API_URL": "https://api.portproof.org/v1"
}
}
}
}Every tool wraps a route you can call yourself, which is the fastest way to see what the agent will get back before you let it loose.
# what is online now, per pool: { "mobile": [ { "country": "US", "name": "...", "online": 33 } ], "residential": [ ... ] }
curl https://api.portproof.org/v1/traffic/countries -H "Authorization: Bearer $PORTPROOF_API_KEY"
# build and validate a URL for one run (the API takes mobile / residential / best)
curl -X POST https://api.portproof.org/v1/traffic/build-url \
-H "Authorization: Bearer $PORTPROOF_API_KEY" -H "Content-Type: application/json" \
-d '{"pool":"residential","country":"US","rotation":"sticky","session":"run_a1b2c3","protocol":"http"}'
# balance before and after the run
curl https://api.portproof.org/v1/traffic -H "Authorization: Bearer $PORTPROOF_API_KEY"One session per run
Give each run its own session name and hold it for the whole task: sid-<name> with rot-sticky. The name is lower-case letters, digits and underscores, and a run id or task id makes a good one because it is unique and already in your logs.
Holding a session keeps the device, not the address, so write the run to survive a change rather than asserting on the exit address. Why that distinction matters, and what to do when the address moves mid-task, is in why a sticky session changed its IP.
The stock check is the part worth copying: it turns an unavoidable 502 into a decision the agent makes with its eyes open. Note that the API names the pools mobile and residential, while the proxy username uses mbl and peer.
import os, uuid
import requests
API = "https://api.portproof.org/v1"
HEAD = {"Authorization": "Bearer " + os.environ["PORTPROOF_API_KEY"], "Content-Type": "application/json"}
WANT = ["US", "NL", "GB"] # in order of preference
def pick_country(pool: str, need: int = 1) -> str:
"""First preferred country with at least this many devices online in that pool."""
live = requests.get(API + "/traffic/countries", headers=HEAD, timeout=10).json()
online = {row["country"]: row["online"] for row in live[pool]}
for code in WANT:
if online.get(code, 0) >= need:
return code
raise RuntimeError("none of the preferred countries is online in " + pool)
country = pick_country("residential")
session = "run_" + uuid.uuid4().hex[:10]
built = requests.post(
API + "/traffic/build-url",
headers=HEAD,
json={"pool": "residential", "country": country, "rotation": "sticky", "session": session, "protocol": "http"},
timeout=10,
).json()
print(built["username"], built["url_masked"])
proxies = {"http": built["url"], "https": built["url"]}
print(requests.get("https://api.portproof.org/v1/echo-ip", proxies=proxies, timeout=30).json())mobile and residential, and the proxy username calls them mbl and peer. Both are on the setup docs. If the agent runs inside a workflow tool rather than an MCP client, the same job is done with environment variables, as in use a proxy in n8n.Topping up from inside a run
An agent that stops halfway because the balance ran out is worse than one that asks. buy_traffic exists for that, and it is deliberately awkward: without confirm: true it does nothing, and without an idempotency key the write is refused. A retried tool call with the same key is the same purchase, not a second one.
Keep the decision human where it matters. A sensible pattern is a hard ceiling in your own code, an alert at 80 percent of the balance, and an agent permitted to top up only up to a size you set. Buying from the API spends the account balance; every other payment method goes through checkout.
Budgeting a run, and reading it back
Size the run before it starts: steps times the weight of a step, times the retries you expect. A rendered page is the expensive unit, so an agent that can answer from an API or from HTML should not open a browser to do it.
Afterwards, read gb_used back and store it with the run id. Usage refreshes every five minutes, so compare across a wider window than a short run takes. What the meter counts, including the handshakes and the failed attempts, is in what counts as proxy traffic.
| Order size | Per GB | Saving |
|---|---|---|
| 1 to 4 GB | EUR 4.50 | |
| 5 to 24 GB | EUR 3.90−13 % | |
| 25 to 49 GB | EUR 3.50−22 % | |
| 50 to 99 GB | EUR 3.20−29 % | |
| 100 to 249 GB | EUR 2.90−36 % | |
| 250 GB and more | EUR 2.50−44 % |
GB never expire. What you buy stays on your balance until you use it; a new purchase adds to the same balance. The trial is 0.5 GB for EUR 2.90, once per customer.
Agent runs are metered like any other traffic: the same ladder, the same balance, no fee per session or per connection.
For the wider picture of what agents do with this, see proxies for AI agents, the two pools at mobile 4G/5G and residential, and the per-GB ladder on pricing.
What is not allowed
What is not allowed: an agent is held to the same rules as a person. Creating or farming accounts, solving or evading human checks, credential stuffing and anything else on the declined list are refused, whoever or whatever issues the request. Give an agent a task you would be happy to run yourself. See the acceptable-use policy.