as-mcp-server
A local MCP server that lets your own AI agent (Claude Code, Codex, Cursor, VS Code, OpenCode, Claude Desktop, ...) look at your OpenVPN Access Server through its Web API. This release is read-only: it answers questions such as "is the VPN healthy", "who is connected", "who failed to log in today", "what can user X reach" and never changes anything on the server.
It runs on your machine, is started by your agent, and talks to Access Server with your own admin credentials. Nothing is hosted by anyone else.
Copyright (c) 2026 OpenVPN Inc. Licensed under the MIT License, see LICENSE.
Requirements
- OpenVPN Access Server 3.1 or newer (Web API v0.2). Tested on 3.1.0 and 3.2.2.
- An admin account on that server. Use a dedicated one for the agent.
- uv on your machine. It downloads Python if needed; nothing else has to be installed.
Install (about two minutes)
These steps are written so you can hand them to your agent ("set this up for me") or follow them yourself.
-
Install uv.
- macOS / Linux:
curl -LsSf https://astral.sh/uv/install.sh | sh - Windows (PowerShell):
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" - or
brew install uv,winget install astral-sh.uv,pipx install uv.
- macOS / Linux:
-
Store the connection once. This asks for the server URL, the admin username and the password (typed hidden, never shown to the agent), checks them against the server, then saves URL and username to
~/.config/as-mcp-server/profiles.tomland the password in your OS keyring (macOS Keychain, Windows Credential Manager, Linux Secret Service):uvx as-mcp-server setupIf the account needs a second sign-in step you are asked for it as well, with the server's own prompt (the authenticator code, or for example a PIN from a RADIUS back end).
-
Register the server with your agent: run its command from the table in Agent setup, or add the entry to its configuration file. There is no secret in this configuration.
-
Ask the agent something: "Is my VPN healthy?" It will call
get_status_overview.
Pin a version if you want upgrades to be explicit: uvx as-mcp-server@0.1.0. uvx keeps
using the version it first cached until you pass @latest or run uv cache clean.
Agent setup
| Agent | Command | Configuration file |
|---|---|---|
| Claude Code | claude mcp add openvpn-as -- uvx as-mcp-server |
~/.claude.json (add --scope user to use it in every project) |
| Codex CLI | codex mcp add openvpn-as -- uvx as-mcp-server |
~/.codex/config.toml |
| Gemini CLI | gemini mcp add -s user openvpn-as uvx as-mcp-server (no --) |
~/.gemini/settings.json |
| OpenCode | opencode mcp add openvpn-as -- uvx as-mcp-server |
~/.config/opencode/opencode.json (or .jsonc) |
| VS Code | code --add-mcp '{"name":"openvpn-as","command":"uvx","args":["as-mcp-server"]}' |
the user profile's mcp.json ("MCP: Open User Configuration"), or .vscode/mcp.json in a project |
| Cursor | - | ~/.cursor/mcp.json, or .cursor/mcp.json in a project |
| Claude Desktop | - | macOS ~/Library/Application Support/Claude/claude_desktop_config.json, Windows %APPDATA%\Claude\claude_desktop_config.json (Settings, Developer, Edit Config) |
In Windows PowerShell type claude.cmd instead of claude: PowerShell drops the -- when it
runs the claude.ps1 wrapper, and the command then fails.
The entry has a different shape per agent. Claude Desktop, Cursor and Gemini CLI (and Claude
Code's project file .mcp.json):
{
"mcpServers": {
"openvpn-as": { "command": "uvx", "args": ["as-mcp-server"] }
}
}
VS Code:
{
"servers": {
"openvpn-as": { "type": "stdio", "command": "uvx", "args": ["as-mcp-server"] }
}
}
Codex CLI:
[mcp_servers.openvpn-as]
command = "uvx"
args = ["as-mcp-server"]
OpenCode 1.x (the command is one array, the server sits directly under mcp):
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"openvpn-as": { "type": "local", "command": ["uvx", "as-mcp-server"] }
}
}
OpenCode 2 (the server sits under mcp.servers):
{
"mcp": {
"servers": {
"openvpn-as": { "type": "local", "command": ["uvx", "as-mcp-server"] }
}
}
}
Both versions have the opencode mcp add openvpn-as -- uvx as-mcp-server command; 1.x writes
the first form.
Alternative: environment variables (headless Linux, CI, or by preference)
Skip setup and give the agent the settings as environment variables of the server entry.
With a command, add one flag per variable: --env KEY=value for Codex CLI and OpenCode, -e KEY=value for Gemini CLI and Claude Code. Claude Code reads every word after -e as another
variable, so put the server name first:
claude mcp add openvpn-as -e OPENVPN_AS_URL=https://vpn.example.com:943 \
-e OPENVPN_AS_USER=mcp-admin -e OPENVPN_AS_PASSWORD=... -- uvx as-mcp-server
In a configuration file the variables go into env next to command (Claude Desktop, Cursor,
Gemini CLI, VS Code, Claude Code), into environment for OpenCode, and into a
[mcp_servers.openvpn-as.env] table for Codex CLI:
{
"mcpServers": {
"openvpn-as": {
"command": "uvx",
"args": ["as-mcp-server"],
"env": {
"OPENVPN_AS_URL": "https://vpn.example.com:943",
"OPENVPN_AS_USER": "mcp-admin",
"OPENVPN_AS_PASSWORD": "..."
}
}
}
}
The password then lives in plain text in that file, like any API key in an MCP configuration. Environment variables override the stored profile field by field.
Check it works
uvx as-mcp-server check
prints the URL, user, where each value came from, TLS mode and the server version. Inside a
conversation the equivalent is the connection_info tool, which never fails and reports any
problem in its error field.
Tools
All tools are read-only except login, which only creates a session.
| Tool | What it answers | Parameters |
|---|---|---|
connection_info |
Which server, which user, TLS mode, session, server version; problems as text | - |
login |
Answer the second sign-in step: the authenticator code or the back end's prompt | totp_code |
get_status_overview |
One-call health check: version, EULA, DCO, chosen config values | config_names (optional) |
get_server_status |
Internal services and auth modules, last restart | - |
get_server_info |
Version, build, OS, architecture, hostname | - |
get_active_vpn_connections |
Connected clients and daemons | - |
get_log_reports |
Connection / authentication log with filters | page_size, offset, username, since, until, errors_only, active_only, search, order_by, sort |
get_license_info |
License type, concurrent connections, subscription state | - |
list_users |
Users and their properties (MFA secrets redacted) | page_size, offset, name_contains, group, admins_only, autologin_only, usernames, order_by, sort |
list_groups |
Groups, member counts or members | page_size, offset, name_contains, include_members, group_names, order_by, sort |
get_default_user |
The __DEFAULT__ profile everyone inherits from |
- |
list_access_rulesets |
Rulesets assigned to a user, a group or __DEFAULT__ |
owner, name_contains |
list_access_rules |
The rules (destination, action) inside rulesets | ruleset_ids, rule_type, match_data_contains |
Responses are the Access Server API's own JSON. since/until are relative durations such
as 30m, 24h, 7d, 15d 20m. "What can user X reach": rulesets for the user, for each of
the user's groups, and for __DEFAULT__, then list_access_rules with the collected ids.
Configuration reference
| Variable | Default | Meaning |
|---|---|---|
OPENVPN_AS_URL |
from setup |
https://host[:943] |
OPENVPN_AS_USER |
from setup |
admin username |
OPENVPN_AS_PASSWORD |
from the OS keyring | password |
OPENVPN_AS_CA_CERT |
- | PEM file of a private CA that signed the server certificate |
OPENVPN_AS_INSECURE |
false |
true disables TLS verification. Loud: a warning is logged and connection_info reports it. Test servers only. |
OPENVPN_AS_TIMEOUT |
30 |
request timeout in seconds |
OPENVPN_AS_LOG_LEVEL |
WARNING |
DEBUG, INFO, WARNING, ERROR; logs go to stderr and never contain secrets |
OPENVPN_AS_CONFIG_DIR |
~/.config/as-mcp-server |
where profiles.toml lives |
Reserved for later releases, not implemented yet: OPENVPN_AS_PROFILE, OPENVPN_AS_TOOLS,
OPENVPN_AS_READ_ONLY, OPENVPN_AS_EXTRA_HEADERS.
Remove stored credentials with uvx as-mcp-server setup --clear.
Logs and debugging
- The server logs to stderr only;
OPENVPN_AS_LOG_LEVEL=DEBUGadds one line per request (METHOD /path -> status in N ms) and the background token renewals. Bodies, headers, passwords and tokens are never logged. - The reliable way to read the log is a terminal:
OPENVPN_AS_LOG_LEVEL=DEBUG uvx as-mcp-server checkperforms the same login andGET /server/infoas theconnection_infotool and prints the log next to the result. - Inside an agent, where stderr ends up is the agent's business. Claude Code has no log view
in its
/mcppanel (it offers View tools, Reconnect and Disable); it writes each server's stderr asServer stderr: ...records into~/Library/Caches/claude-cli-nodejs/<project>/mcp-logs-<server>/*.jsonlon macOS (~/.cache/claude-cli-nodejs/...on Linux), one file per session.claude --debug(or--debug-file <path>) turns on Claude Code's own debug log, which includes its MCP connection messages. OPENVPN_AS_TIMEOUT=60if the server is slow (default 30 s).
MFA and other second sign-in steps
If the admin account needs a second step, the first tool call reports it and quotes the
server's own prompt. For Access Server's built-in authenticator that is the 6-digit TOTP code;
for a RADIUS, LDAP or PAM back end it is whatever the back end asks for (a PIN, a Duo passcode,
an answer to a question), passed through word for word. The agent shows you the prompt, asks
for the answer and calls login. The session then lasts for the lifetime of the MCP process:
Access Server issues short-lived tokens (10 minutes by default), and the server renews its
token in the background before it expires, so you are asked again only after the limit Access
Server puts on renewals (4 hours by default), or if the machine slept past a token's expiry.
The answer goes to the challenge the server already issued, so a push- or SMS-based back end is not triggered a second time, as long as you answer within about 90 seconds (Access Server forgets a challenge after two to three minutes). After a wrong answer or a longer pause the server issues a new challenge; if it asks a different question, the agent quotes it before anything is sent. Authenticator codes are single-use and must be exactly six digits (a mistyped code is refused locally and does not count as a failed attempt); five wrong answers lock the account for 15 minutes (Access Server default).
SAML-only accounts
as-mcp-server signs in with a username and password, plus the answer to a second step when
asked. It cannot
complete the browser-based SAML flow. When the Access Server's default authentication system is
SAML, an admin account that follows that default is refused with an error that says so, and no
MFA code is requested (a code could never succeed). Use an administrator whose authentication
method is set per user to something else: list_users shows it as auth_method (sacli calls
it user_auth_type), and the built-in openvpn administrator uses local.
Security notes
- Your password never enters the conversation:
setupis a terminal command. Only the answer to a second sign-in step passes through the agent: a 30-second, single-use authenticator code, or whatever a RADIUS, LDAP or PAM back end asks for. A static PIN or passcode given this way reaches the model provider like any other tool argument; if that is not acceptable, use an admin account whose second step is a one-time code. - Tool responses go to your agent's model provider. The server redacts TOTP secrets, which
Access Server otherwise includes in user listings, the subscription key from the license
information, and configuration values whose key name looks like a secret (passwords,
bind passwords, tokens, private keys, the subscription bundle; a trailing version or
index such as
.2.9.0does not hide the name) or that Access Server itself marks as redacted, inget_status_overview; everything else in a response (user names, addresses, log entries, other configuration values) is visible to the model. - Log records and the list of connected clients contain text that anyone on the internet
can choose: a failed login is logged with the username exactly as typed, and VPN clients
report their own platform and version. Such text can be written to look like log lines
or instructions for the model.
get_log_reportsandget_active_vpn_connectionsshow a client-chosen username, certificate name, platform, version or proxied address only when it looks like a plain value; anything else (for example newlines, control or invisible characters, sentence punctuation, more than three words, more than 64 characters or 16 East Asian characters) is replaced by{"withheld": true, "length": ..., "flags": [...], "fingerprint": ...}without the text. The same fingerprint means the same text. Read the full records in the Access Server Admin UI if you need them. This is a heuristic: a short plain-looking string such asignore_previous_instructionsstill gets through, so treat a model's security summary as a draft all the same. - Tool calls are rate limited: 30 in a row, then 30 per minute (one every two seconds). An agent stuck in a loop gets a tool error that tells it how long to wait instead of sending Access Server a request per call. Listing the tools and connecting are never limited.
- Use a dedicated admin account so its lockout or revocation affects only the agent.
- The keyring item is bound to the Python binary on macOS; after a
uvxupgrade the system may ask once whether the new binary may read it. - See
SECURITY.mdfor reporting.
Compatibility
| Access Server | Result |
|---|---|
| 3.2.2 | all tools verified |
| 3.1.0 | all tools verified |
| 3.0.x | not supported (Web API v0.1) |
The Web API is declared unstable by OpenVPN; an Access Server upgrade that changes it needs a
matching as-mcp-server release. Please open an issue with the server version if a tool
stops working.
Roadmap and feedback
This first release only reads. Later releases may add safe write operations (users, groups, access rules, profiles) and server administration, each behind explicit opt-in. Tell us what is missing with the Tool request issue template and report problems with Bug report.
Development
make install # uv sync
make check # ruff + pytest (no network)
make stand-up # two real Access Servers and FreeRADIUS in Docker, see dev/README.md
make live # tests against them
make release-build # the package as published: dependency versions pinned to uv.lock
Agent instructions: AGENTS.md.
Metadata
Release files for as-mcp-server 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| as_mcp_server-0.1.0.tar.gz | 36.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| as_mcp_server-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 78.3 kB
Release files / as_mcp_server-0.1.0.tar.gz
| Download URL | as_mcp_server-0.1.0.tar.gz |
|---|---|
| Size | 36.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a365d4393d339a4e9fb4694fc50f60a4a6bb46c4cf5107e87a97a9d427b87f77
|
|
BLAKE2b-256 checksum How to use checksums |
40b98a390bc8c17a9e36e9d6399d54073ccb49cc4a2f1269fcd3c43ec8a0eaf5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.
Transparency logRelease files / as_mcp_server-0.1.0-py3-none-any.whl
| Download URL | as_mcp_server-0.1.0-py3-none-any.whl |
|---|---|
| Size | 41.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9f5d0f7429a2149cc4414fd577db5795dce42d612a2b2b9e261ffeb73e86bf6d
|
|
BLAKE2b-256 checksum How to use checksums |
16fae81e12367bfe515e5370bc325318e86fa790300ce320b4f0539e31973a15
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.
Transparency log