A self-hosted MCP gateway is one small service on a VPS that every AI agent connects to instead of talking to MCP servers directly. Claude Code, Codex, Cursor or Open WebUI get one URL and one key each, and the gateway decides which tools that key may see and call, writes every call to a log and slows down an agent that loops. In our lab on Debian 13, agentgateway 1.6.0 did this in 37 MB of RAM and added about 0.7 ms per tool call, and AISIX 1.6.0 did the same job in about 160 MB. Both refused the tools we had blocked, and both run fine on a CPU-only server, because a gateway only forwards small JSON messages and never runs a model.
Key facts, checked on 16 October 2026:
- agentgateway 1.6.0 was released on 2 October 2026 under the Apache 2.0 licence. It ships as one 94 MB Linux binary and can start MCP servers itself over stdio, or reach them over SSE or Streamable HTTP.
- AISIX, API7's Rust gateway, is also Apache 2.0. Version 1.6.0 came out on 12 October 2026 and the last code push to its repository was on 14 October. Its MCP gateway serves registered servers on one
/mcpendpoint, but upstream servers must speak Streamable HTTP. - Tuskira AI Agent Gateway v0.4.0 was published on 1 October 2026 (Apache 2.0). Its own README calls it pre-1.0 alpha, and it needs PostgreSQL.
- MetaMCP (MIT) last tagged a release, v2.4.22, on 19 December 2025. IBM ContextForge (Apache 2.0) reached v1.0.11 on 28 September 2026.
- In our test, one MCP session cost about five HTTP requests (initialize, a confirmation, the event stream, the call, the close). Keep that in mind when you set a rate limit in requests per minute.
What an MCP gateway does, in plain words
MCP, the Model Context Protocol, is the standard way for an AI agent to use tools. An MCP server offers tools such as "read a file" or "create an issue", and the agent calls them. Our guide to running your own MCP server on a VPS covers one server. A gateway is the next step, once you have several servers and several agents.
Without a gateway, every agent holds its own list of servers, its own tokens and its own idea of what it may do. With a gateway, the agent knows a single address. Behind it, the gateway does four jobs:
- It checks who is calling. Each agent or person gets its own key, so you can revoke the CI runner without touching your laptop.
- It filters tools. A key sees only the tools on its allow list, and a call to anything else is refused before it reaches the server.
- It logs every call: which key, which server, which tool, how long it took, and whether it was refused.
- It limits the rate, so a confused agent stuck in a loop cannot hammer a server or a paid API behind it.
This is a different job from the LLM gateway in our LiteLLM and OmniRoute article. That one routes model requests ("which model answers this prompt"). An MCP gateway routes tool calls ("may this agent delete that file"). Some products, AISIX and agentgateway included, do both.
Why agents should not reach tools directly
The official MCP test server, @modelcontextprotocol/server-everything, has a tool called get-env. When we called it directly in our lab, it returned the server's environment variables: PATH, HOME, the user name. On a real server that list can hold database passwords and API tokens. An agent does not need malice to leak them. A prompt hidden in a web page it reads can ask it to call the tool and paste the result somewhere.
The fix is least privilege: each agent can use only the tools its job needs. A coding agent may read the project folder; it has no reason to write outside it, print the environment or move files. A gateway turns that rule into configuration you can read in one file, instead of hoping every agent's settings are right.
Open-source MCP gateways compared
We checked each project's repository and release page on 16 October 2026. We installed and tested the first two.
| Gateway | Licence and version | Install | Per-tool control | Auth and limits | Tested by us |
|---|---|---|---|---|---|
| agentgateway | Apache 2.0, v1.6.0 (2 Oct 2026) | One binary, YAML file; Kubernetes optional | CEL rules per tool, per key or per JWT claim | API keys, JWT, OAuth, basic auth; local and remote rate limits | Yes, stdio and HTTP servers |
| AISIX | Apache 2.0, v1.6.0 (12 Oct 2026) | Docker image, one resources.yaml | Allow and deny globs per caller key | Hashed caller keys, OIDC/JWT; per key and per server limits | Yes, Streamable HTTP servers |
| Tuskira AI Agent Gateway | Apache 2.0, v0.4.0 (1 Oct 2026), alpha | Docker Compose built from source, PostgreSQL | Agent profiles checked on every tool call | Gateway API keys bound to profiles; stored credentials injected | No, see below |
| MetaMCP | MIT, v2.4.22 (19 Dec 2025) | Docker Compose with PostgreSQL | Namespaces with tools switched on or off | API key or OAuth per endpoint; SSE and Streamable HTTP | No |
| IBM ContextForge | Apache 2.0, v1.0.11 (28 Sep 2026) | pip, uvx or Docker | "Virtual servers" that bundle chosen tools | RBAC, rate limiting, admin UI | No |
We wanted to test Tuskira too. On 16 October 2026 its container image on GitHub's registry answered "unauthorized" to an anonymous pull, and the documented quickstart builds the gateway and its web console from source, which needs Go and Node.js toolchains and at least 4 GB free for Docker by the project's own README. What it describes is attractive: profiles enforced on tools/call, not only hidden from the tool list, and credentials injected on the way out so agents never hold them. Its README also warns that a key not bound to a profile lets the caller choose its own profile, so bind every key.
If you start today and want something small, agentgateway is the easiest to run on a VPS. If you also want an OpenAI-compatible model gateway with guardrails in the same box, AISIX is worth a look. Both fit on a small VPS.
Our lab: what we built
We used a Debian 13 container limited to 2 vCPUs and 3 GB of RAM, on our Amsterdam lab host with an AMD EPYC 7742 whose 8 cores were shared with other tests. Your plan's numbers can differ. Behind the gateways we put two harmless MCP servers:
files: the official filesystem server, version 1.0.0, allowed to touch only/srv/mcp-test.demo: the official "everything" test server, which includes the riskyget-envtool.
The rule we wanted: the claude key may read files and use two demo tools; the ci key may also write files; nobody may call get-env. As the client we used a short Python script on the official MCP SDK. We first tried the MCP Inspector CLI, but version 2.11.0 needs Node.js 22.19 or newer and Debian 13 ships 20.19.2.
Setup 1: agentgateway in front of two MCP servers
Step 1: packages, a user and a test folder
# as root
apt update && apt install -y curl ca-certificates openssl nodejs npm python3-venv
adduser --disabled-password --gecos "" mcp
mkdir -p /srv/mcp-test
echo "hello from the lab" > /srv/mcp-test/readme.txt
chown -R mcp:mcp /srv/mcp-test
npm install -g @modelcontextprotocol/server-filesystem@1.0.0 @modelcontextprotocol/server-everything@1.0.0
This took 47 seconds in our lab. node -v should print v20.19.2.
Step 2: run the demo server as a service
# as root
cat > /etc/systemd/system/mcp-demo.service <<'EOF'
[Unit]
Description=MCP demo server (official "everything" test server)
After=network-online.target
[Service]
User=mcp
Environment=PORT=3001
ExecStart=/usr/local/bin/mcp-server-everything streamableHttp
Restart=on-failure
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now mcp-demo
ss -ltnp | grep 3001
You should see LISTEN ... *:3001 ... node. This server listens on every interface, so your firewall must keep port 3001 closed to the internet (see the checklist below).
Step 3: install agentgateway and check the download
# as root
cd /root
curl -fsSLO https://github.com/agentgateway/agentgateway/releases/download/v1.6.0/agentgateway-linux-amd64
curl -fsSLO https://github.com/agentgateway/agentgateway/releases/download/v1.6.0/agentgateway-linux-amd64.sha256
sha256sum agentgateway-linux-amd64
cat agentgateway-linux-amd64.sha256
install -m 755 agentgateway-linux-amd64 /usr/local/bin/agentgateway
agentgateway --version
The two hashes must be the same. Ours were both 496ed27e...f079d, and the version check printed "version": "1.6.0".
Step 4: keys, rules and the service
Two random keys, one per agent, then the config. The mcpAuthorization rules are the allow list: a tool not matched by any rule is hidden and refused.
# as root
CLAUDE_KEY=$(openssl rand -hex 24)
CI_KEY=$(openssl rand -hex 24)
printf 'CLAUDE_KEY=%s\nCI_KEY=%s\n' "$CLAUDE_KEY" "$CI_KEY" > /root/gateway-keys.txt
chmod 600 /root/gateway-keys.txt
mkdir -p /etc/agentgateway
cat > /etc/agentgateway/config.yaml <<EOF
config:
adminAddr: "127.0.0.1:15000"
statsAddr: "127.0.0.1:15020"
readinessAddr: "127.0.0.1:15021"
frontendPolicies:
accessLog:
add:
agent: 'apiKey.agent'
tool: 'mcp.tool.name'
server: 'mcp.tool.target'
binds:
- port: 3000
listeners:
- routes:
- policies:
apiKey:
mode: strict
keys:
- key: "$CLAUDE_KEY"
metadata: { agent: claude }
- key: "$CI_KEY"
metadata: { agent: ci }
localRateLimit:
- maxTokens: 120
tokensPerFill: 120
fillInterval: 60s
key: 'apiKey.agent'
mcpAuthorization:
rules:
# every key: read-only file tools, only inside /srv/mcp-test
- allow: 'mcp.tool.target == "files" && mcp.tool.name in ["list_allowed_directories", "list_directory", "read_text_file", "search_files"]'
# every key: two harmless demo tools
- allow: 'mcp.tool.target == "demo" && mcp.tool.name in ["echo", "get-sum"]'
# only the ci key may write files
- allow: 'apiKey.agent == "ci" && mcp.tool.target == "files" && mcp.tool.name == "write_file"'
backends:
- mcp:
targets:
- name: files
stdio:
cmd: /usr/local/bin/mcp-server-filesystem
args: ["/srv/mcp-test"]
- name: demo
mcp:
host: http://127.0.0.1:3001/mcp
EOF
chown root:mcp /etc/agentgateway/config.yaml
chmod 640 /etc/agentgateway/config.yaml
cat > /etc/systemd/system/agentgateway.service <<'EOF'
[Unit]
Description=agentgateway MCP gateway
After=network-online.target mcp-demo.service
[Service]
User=mcp
ExecStart=/usr/local/bin/agentgateway -f /etc/agentgateway/config.yaml
Restart=on-failure
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now agentgateway
systemctl is-active agentgateway
Expected: active. ss -ltnp | grep agentgateway showed the admin UI, stats and readiness ports on 127.0.0.1 only, and the gateway itself on *:3000, which means every interface. Your firewall or a reverse proxy has to guard that port.
Proving the block works
Now the client. It runs as the mcp user; on a real setup it would be your agent on another machine.
# as root: give the test user the keys
install -o mcp -m 600 /root/gateway-keys.txt /home/mcp/gateway-keys.txt
# as the mcp user
python3 -m venv ~/venv
~/venv/bin/pip install -q mcp==2.3.0
cat > ~/mcpcall.py <<'EOF'
# mcpcall.py - tiny MCP client: list the tools, or call one tool, through a gateway
import asyncio, json, logging, os, sys, time
import httpx2
from mcp import ClientSession
from mcp.client.streamable_http import streamable_http_client
logging.disable(logging.CRITICAL) # hide SDK noise when the session closes
def root_cause(e):
while isinstance(e, BaseExceptionGroup):
e = e.exceptions[0]
return e
async def main():
url, action = sys.argv[1], sys.argv[2]
headers = {}
if os.environ.get("MCP_TOKEN"):
headers["Authorization"] = "Bearer " + os.environ["MCP_TOKEN"]
async with httpx2.AsyncClient(headers=headers, timeout=30) as http:
async with streamable_http_client(url, http_client=http) as (read, write):
async with ClientSession(read, write) as s:
await s.initialize()
if action == "list":
tools = await s.list_tools()
print("tools:", ", ".join(t.name for t in tools.tools))
return
args = json.loads(sys.argv[3]) if len(sys.argv) > 3 else {}
t0 = time.perf_counter()
r = await s.call_tool(action, args)
ms = (time.perf_counter() - t0) * 1000
text = " ".join(getattr(c, "text", "") for c in r.content)
print(f"error={r.is_error} time={ms:.1f}ms -> {text.strip()[:200]}")
try:
asyncio.run(main())
except BaseException as e:
e = root_cause(e)
print(f"refused: {type(e).__name__}: {e}"[:300])
sys.exit(1)
EOF
set -a; . ~/gateway-keys.txt; set +a
GW=http://127.0.0.1:3000/mcp
MCP_TOKEN=$CLAUDE_KEY ~/venv/bin/python mcpcall.py $GW list
MCP_TOKEN=$CLAUDE_KEY ~/venv/bin/python mcpcall.py $GW files_read_text_file '{"path": "/srv/mcp-test/readme.txt"}'
MCP_TOKEN=$CLAUDE_KEY ~/venv/bin/python mcpcall.py $GW files_write_file '{"path": "/srv/mcp-test/new.txt", "content": "hi"}'
MCP_TOKEN=$CLAUDE_KEY ~/venv/bin/python mcpcall.py $GW demo_get-env
MCP_TOKEN=$CI_KEY ~/venv/bin/python mcpcall.py $GW files_write_file '{"path": "/srv/mcp-test/new.txt", "content": "hi"}'
What we saw, line by line:
# output, as the mcp user
tools: files_read_text_file, files_list_directory, files_search_files, files_list_allowed_directories, demo_echo, demo_get-sum
error=False time=87.0ms -> hello from the lab
refused: MCPError: Unknown tool: files_write_file
refused: MCPError: Unknown tool: demo_get-env
error=False time=78.8ms -> Successfully wrote to /srv/mcp-test/new.txt
The claude key never even sees write_file or get-env in its tool list, and a direct call is refused. The ci key can write. Without any key, the gateway answers HTTP 401 with api key authentication failure: no API Key found. The filesystem server adds its own fence: a read of /etc/shadow came back as Access denied - path outside allowed directories. Two locks are better than one.
The audit log
# as root
journalctl -u agentgateway --no-pager | grep 'tools/call' | tail -3
Each line carries the key's agent name, the server and the tool. Shortened, a refused and an allowed call look like this:
# output (shortened)
http.status=400 mcp.method.name=tools/call mcp.target=files gen_ai.tool.name=write_file error="mcp: Unknown tool: files_write_file" reason=MCP agent="claude" tool="write_file" server="files"
http.status=200 mcp.method.name=tools/call mcp.target=files gen_ai.tool.name=write_file duration=8ms agent="ci" tool="write_file" server="files"
The rate limit
We ran 30 sessions in a row with the claude key against a limit of 120 requests per minute. Nineteen went through and eleven were refused with rate limit exceeded, logged with reason=RateLimit. Our first try used a limit of 30 a minute and tripped after six sessions, because one session uses about five HTTP requests. Set limits per key, so one runaway agent does not lock out the others.
Setup 2: AISIX with allow and deny lists
AISIX reaches upstream MCP servers only over Streamable HTTP, so the stdio filesystem server needs a bridge. We used supergateway 4.2.0 and Debian's Docker package.
# as root
apt install -y docker.io
npm install -g supergateway@4.2.0
cat > /etc/systemd/system/mcp-files.service <<'EOF'
[Unit]
Description=Filesystem MCP server over Streamable HTTP (via supergateway)
After=network-online.target
[Service]
User=mcp
ExecStart=/usr/local/bin/supergateway --stdio "/usr/local/bin/mcp-server-filesystem /srv/mcp-test" --outputTransport streamableHttp --stateful --host 127.0.0.1 --port 3002
Restart=on-failure
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now mcp-files
Next, the two AISIX files. Keys are never written into them; key_env names an environment variable, and AISIX stores only a hash.
# as root
mkdir -p /opt/aisix
cat > /opt/aisix/config.yaml <<'EOF'
resources_file: /etc/aisix/resources.yaml
proxy:
addr: "127.0.0.1:4000"
admin:
enabled: false
observability:
metrics:
prometheus:
enabled: true
addr: "127.0.0.1:9090"
EOF
cat > /opt/aisix/resources.yaml <<'EOF'
_format_version: "1"
mcp_servers:
- name: files
url: http://127.0.0.1:3002/mcp
- name: demo
url: http://127.0.0.1:3001/mcp
api_keys:
- display_name: claude-laptop
key_env: CLAUDE_KEY
allowed_models: []
mcp_access:
allow: ["files__read_text_file", "files__list_directory", "files__list_allowed_directories", "demo__echo", "demo__get-sum"]
mcp_rate_limits:
files: { rpm: 60 }
- display_name: ci-runner
key_env: CI_KEY
allowed_models: []
mcp_access:
allow: ["files__*", "demo__echo"]
deny: ["files__move_file", "files__edit_file"]
EOF
chmod 644 /opt/aisix/config.yaml /opt/aisix/resources.yaml
docker run --rm --env-file /root/gateway-keys.txt -v /opt/aisix:/etc/aisix:ro --entrypoint aisix ghcr.io/api7/aisix:1.6.0 validate --resources /etc/aisix/resources.yaml
docker run -d --name aisix --restart unless-stopped --network host \
--env-file /root/gateway-keys.txt \
-v /opt/aisix:/etc/aisix:ro \
ghcr.io/api7/aisix:1.6.0
curl -s http://127.0.0.1:4000/readyz
The validate step printed OK: /etc/aisix/resources.yaml loaded 4 resource(s) and the ready check printed ok. The 141 MB image downloaded in 3 seconds. The same client script against http://127.0.0.1:4000/mcp gave:
# output, as the mcp user
tools: demo__echo, demo__get-sum, files__read_text_file, files__list_directory, files__list_allowed_directories
error=False time=979.1ms -> hello from the lab
refused: MCPError: tool 'files__write_file' is not available
refused: MCPError: tool 'demo__get-env' is not available
refused: MCPError: tool 'files__move_file' is not available
The last line is the ci key hitting its deny list. Seventy quick reads from claude on the files server ended with 52 allowed and 18 refused with HTTP 429 rate_limit_exceeded, while the demo server stayed open to the same key. The access log (docker logs aisix) records the tool and the key's ID for every call, including refused ones.
Two lessons from this run. First, the 979 ms: AISIX opens a fresh upstream session for each call, and supergateway starts a new filesystem process for each session. Calls to a native HTTP server (the demo) took about 11 ms instead. Use AISIX with MCP servers that speak HTTP themselves. Second, mount the folder, not a single file. With -v resources.yaml:..., editing the file with sed -i created a new file on disk; docker kill -s HUP aisix logged a reload, but the container still read the old copy. Mounting /opt/aisix as a folder fixed it.
Speed and memory: our numbers
| Measurement (our lab, 2 vCPU) | agentgateway 1.6.0 | AISIX 1.6.0 |
|---|---|---|
| RAM of the gateway, idle | 37 MB | 158 to 183 MB |
| Disk | 94 MB binary | 141 MB image, plus Docker |
| Echo tool, median of 300 calls | 5.6 to 5.9 ms (direct: 4.9 to 5.2 ms) | 11.3 to 11.8 ms |
| 2,000 calls from 4 clients at once | 9.3 s, about 1 CPU second | 15.7 s, about 3 CPU seconds |
| Upstreams it can start or reach | stdio, SSE, Streamable HTTP | Streamable HTTP, OpenAPI |
The gateways are not the heavy part. Each Node.js MCP server used 70 to 120 MB, and agentgateway starts one copy of a stdio server per client session: four parallel sessions raised the filesystem server count from 2 to 8 processes, which all exited when the sessions closed. The whole stack, both gateways, three Node processes and Docker, used 414 MB of the container's 3 GB. Nothing here touches a GPU: 2,000 calls through agentgateway cost about one second of CPU time.
Security checklist before agents connect from outside
- Put HTTPS in front. Keys travel in a header, so never expose port 3000 or 4000 over plain HTTP. Our Caddy, nginx and Traefik guide shows a reverse proxy with a free certificate.
- Close every other port. The demo server and agentgateway listen on all interfaces; only 22, 80 and 443 should be open. Start with our Debian 13 hardening steps.
- One key per agent and per person, never shared. Revoke by deleting one line.
- Allow lists, not block lists. A new tool added by a server update then stays hidden until you add it. AISIX's deny list is a second layer, not the first.
- Keep upstream secrets in the gateway or the MCP server's own service file, not in each agent's config. Tuskira's README describes injecting upstream credentials on the way out so agents never hold them.
- Give MCP servers their own Linux user and the smallest folder, as we did with
/srv/mcp-test. - Read the log weekly. A refused
get-envcall you did not expect tells you something about the prompts your agent read.
Pointing an agent at the gateway is one setting. Claude Code, for example, documents a claude mcp add command with an HTTP transport and an Authorization header in its MCP documentation; we did not run it here because it needs a paid account. If your agents live on a server too, see running Claude Code and Codex 24/7 on a VPS, and our claude-mem sync server guide for sharing agent memory.
Which plan and which city
| Your situation | Plan | Why |
|---|---|---|
| agentgateway plus two or three MCP servers, a few agents | VPS Micro: 2 shared vCPU, 2 GB RAM, 40 GB NVMe | Our whole agentgateway stack fit in well under 1 GB; Linux only, which is all you need |
| AISIX or another Docker gateway, five or more MCP servers, a small team | VPS Mini: 4 shared vCPU, 4 GB RAM, 80 GB NVMe | Room for Docker, several Node servers and many stdio sessions at once |
| Gateway plus the agents themselves, or plus an LLM gateway and a database | VDS Small: 4 dedicated vCPU, 8 GB RAM, 240 GB NVMe | Dedicated cores keep tool latency steady when agents compile code next to the gateway |
| A company with many teams, Tuskira or ContextForge with PostgreSQL and analytics | VDS Medium: 8 dedicated vCPU, 16 GB RAM | The database and log storage grow with every call you record |
You can move to a bigger plan later from the client area with a short reboot. For the city, put the gateway near the tools it calls and the people using it. In our own ping.pe test on 6 October 2026, probes reached our Amsterdam server in 6.6 ms from Frankfurt, 6.7 ms from London and 9.3 ms from Paris; our Prishtina server answered Vienna in 18.6 ms and Frankfurt in 27.8 ms, and Skopje is about 3 ms away on our own network. Dublin suits teams in Ireland and the UK. Availability by city is on the plans page: Amsterdam, Dublin, Prishtina.
When is a VPS the wrong answer? If you use one agent on one laptop with two local tools, a gateway adds moving parts for little gain. A managed gateway from a cloud vendor also makes sense if your company already pays for one and needs its single sign-on. The VPS wins once several agents or people share tools, or when logs and keys must stay on a server you control in the EU. Compare every size on the VPS and VDS plans page.
Frequently asked questions
What is an MCP gateway?
An MCP gateway is a proxy between AI agents and MCP servers. Agents connect to one endpoint with their own key; the gateway lists only the tools each key may use, refuses other calls, logs every call and applies rate limits. agentgateway, AISIX, MetaMCP, IBM ContextForge and Tuskira AI Agent Gateway are open-source examples.
Is an MCP gateway the same as an LLM gateway?
No. An LLM gateway such as LiteLLM routes prompts to language models and tracks tokens. An MCP gateway controls tool calls: which agent may read files, query a database or open tickets. Some products, including AISIX and agentgateway, offer both in one binary or container.
Does an MCP gateway need a GPU?
No. It forwards small JSON messages and runs no model. In our lab, agentgateway handled 2,000 tool calls with about one second of CPU time and 37 MB of RAM, and AISIX used about 160 MB, both on 2 CPU cores without a graphics card.
How much latency does an MCP gateway add?
In our Debian 13 lab, agentgateway 1.6.0 added about 0.7 ms per call (median 5.6 to 5.9 ms against 4.9 to 5.2 ms direct). AISIX 1.6.0 answered in about 11 ms because it opens a new upstream session per call. Network distance to your agents usually matters more than either.
How do I block one MCP tool for one agent?
Give the agent its own key and list only the tools it needs. In agentgateway, write a CEL rule such as mcp.tool.name in ["read_text_file"] under mcpAuthorization. In AISIX, set mcp_access.allow on that key, with deny as an extra guard. Blocked tools disappear from the list and calls to them are refused.
Which is the best open-source MCP gateway for a small VPS?
For one small server, agentgateway is the lightest we tested: a single 94 MB binary, 37 MB of RAM, stdio and HTTP servers, per-key rules and logs. Choose AISIX if you also want an OpenAI-compatible model gateway with guardrails, and look at ContextForge or Tuskira when you need a web console and a database-backed registry.
One door for every agent
Start with agentgateway on a VPS Micro, two keys and an allow list you can read in one minute. Add HTTPS, close the other ports, and check the log after the first week. If you would like us to install the gateway and your MCP servers for you, that setup work is quoted per job: message us on Telegram, or pick a plan and a city on the VPS plans page.