as-mcp-server 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 OpenVPN Inc
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,394 @@
1
+ Metadata-Version: 2.4
2
+ Name: as-mcp-server
3
+ Version: 0.1.0
4
+ Summary: Local MCP server that lets AI agents monitor an OpenVPN Access Server through its Web API (read-only)
5
+ Keywords: openvpn,access-server,mcp,model-context-protocol,vpn
6
+ Author: OpenVPN Inc
7
+ Author-email: OpenVPN Inc <as-mcp-maintainers@openvpn.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Environment :: Console
12
+ Classifier: Intended Audience :: System Administrators
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: System :: Networking
19
+ Classifier: Topic :: System :: Systems Administration
20
+ Requires-Dist: aiofile==3.12.3
21
+ Requires-Dist: annotated-types==0.8.0
22
+ Requires-Dist: anyio==4.15.1
23
+ Requires-Dist: attrs==26.1.0
24
+ Requires-Dist: authlib==1.8.0
25
+ Requires-Dist: beartype==0.22.9
26
+ Requires-Dist: cachetools==7.1.8
27
+ Requires-Dist: caio==0.12.4
28
+ Requires-Dist: certifi==2026.7.22
29
+ Requires-Dist: cffi==2.1.1 ; platform_python_implementation != 'PyPy'
30
+ Requires-Dist: click==8.5.0
31
+ Requires-Dist: cryptography==50.0.1
32
+ Requires-Dist: cyclopts==4.25.2
33
+ Requires-Dist: dnspython==2.8.0
34
+ Requires-Dist: docstring-parser==0.18.0
35
+ Requires-Dist: email-validator==2.3.0
36
+ Requires-Dist: exceptiongroup==1.3.1
37
+ Requires-Dist: fastmcp==4.0.3
38
+ Requires-Dist: fastmcp-slim==4.0.3
39
+ Requires-Dist: griffelib==2.3.0
40
+ Requires-Dist: h11==0.16.0
41
+ Requires-Dist: httpcore==1.0.9
42
+ Requires-Dist: httpcore2==2.12.0 ; sys_platform != 'emscripten'
43
+ Requires-Dist: httpx==0.28.1
44
+ Requires-Dist: httpx2==2.12.0
45
+ Requires-Dist: httpx2-jsfetch==1.0 ; sys_platform == 'emscripten'
46
+ Requires-Dist: idna==3.19
47
+ Requires-Dist: jaraco-classes==3.4.0
48
+ Requires-Dist: jaraco-context==6.1.2
49
+ Requires-Dist: jaraco-functools==4.6.0
50
+ Requires-Dist: jeepney==0.9.0 ; sys_platform == 'linux'
51
+ Requires-Dist: joserfc==1.7.5
52
+ Requires-Dist: jsonref==1.1.0
53
+ Requires-Dist: jsonschema==4.26.0
54
+ Requires-Dist: jsonschema-path==0.5.0
55
+ Requires-Dist: jsonschema-specifications==2025.9.1
56
+ Requires-Dist: keyring==25.7.0
57
+ Requires-Dist: markdown-it-py==4.2.0
58
+ Requires-Dist: mcp==2.2.0
59
+ Requires-Dist: mcp-types==2.2.0
60
+ Requires-Dist: mdurl==0.1.2
61
+ Requires-Dist: more-itertools==11.1.0
62
+ Requires-Dist: openapi-pydantic==0.5.1
63
+ Requires-Dist: opentelemetry-api==1.44.0
64
+ Requires-Dist: packaging==26.3
65
+ Requires-Dist: pathable==0.6.0
66
+ Requires-Dist: platformdirs==4.11.8
67
+ Requires-Dist: py-key-value-aio==0.4.5
68
+ Requires-Dist: pycparser==3.0 ; implementation_name != 'PyPy' and platform_python_implementation != 'PyPy'
69
+ Requires-Dist: pydantic==2.13.5
70
+ Requires-Dist: pydantic-core==2.46.5
71
+ Requires-Dist: pydantic-settings==2.15.0
72
+ Requires-Dist: pygments==2.21.0
73
+ Requires-Dist: pyjwt==2.13.0
74
+ Requires-Dist: pyperclip==1.11.0
75
+ Requires-Dist: python-dotenv==1.2.3
76
+ Requires-Dist: python-multipart==0.0.32
77
+ Requires-Dist: pywin32==312 ; sys_platform == 'win32'
78
+ Requires-Dist: pywin32-ctypes==0.2.3 ; sys_platform == 'win32'
79
+ Requires-Dist: pyyaml==6.0.3
80
+ Requires-Dist: referencing==0.37.0
81
+ Requires-Dist: rich==15.0.0
82
+ Requires-Dist: rich-rst==2.1.0
83
+ Requires-Dist: rpds-py==2026.6.3
84
+ Requires-Dist: secretstorage==3.5.0 ; sys_platform == 'linux'
85
+ Requires-Dist: sse-starlette==3.4.11
86
+ Requires-Dist: starlette==1.6.0
87
+ Requires-Dist: truststore==0.10.4 ; sys_platform != 'emscripten'
88
+ Requires-Dist: typing-extensions==4.16.0
89
+ Requires-Dist: typing-inspection==0.4.4
90
+ Requires-Dist: uncalled-for==0.4.0
91
+ Requires-Dist: uvicorn==0.52.4
92
+ Requires-Dist: watchfiles==1.2.0
93
+ Requires-Dist: websockets==17.1
94
+ Requires-Python: >=3.12
95
+ Project-URL: Homepage, https://github.com/OpenVPN/as-mcp-server
96
+ Project-URL: Issues, https://github.com/OpenVPN/as-mcp-server/issues
97
+ Project-URL: Changelog, https://github.com/OpenVPN/as-mcp-server/blob/main/CHANGELOG.md
98
+ Description-Content-Type: text/markdown
99
+
100
+ # as-mcp-server
101
+
102
+ A local [MCP](https://modelcontextprotocol.io) server that lets your own AI agent (Claude Code,
103
+ Codex, Cursor, VS Code, OpenCode, Claude Desktop, ...) look at your **OpenVPN Access Server**
104
+ through its Web API. This release is **read-only**: it answers questions such as "is the VPN
105
+ healthy", "who is connected", "who failed to log in today", "what can user X reach" and never
106
+ changes anything on the server.
107
+
108
+ It runs on your machine, is started by your agent, and talks to Access Server with your own
109
+ admin credentials. Nothing is hosted by anyone else.
110
+
111
+ Copyright (c) 2026 OpenVPN Inc. Licensed under the MIT License, see `LICENSE`.
112
+
113
+ ## Requirements
114
+
115
+ - OpenVPN Access Server **3.1 or newer** (Web API v0.2). Tested on 3.1.0 and 3.2.2.
116
+ - An **admin** account on that server. Use a dedicated one for the agent.
117
+ - [uv](https://docs.astral.sh/uv/) on your machine. It downloads Python if needed; nothing
118
+ else has to be installed.
119
+
120
+ ## Install (about two minutes)
121
+
122
+ These steps are written so you can hand them to your agent ("set this up for me") or follow
123
+ them yourself.
124
+
125
+ 1. Install uv.
126
+ - macOS / Linux: `curl -LsSf https://astral.sh/uv/install.sh | sh`
127
+ - Windows (PowerShell): `powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"`
128
+ - or `brew install uv`, `winget install astral-sh.uv`, `pipx install uv`.
129
+ 2. Store the connection once. This asks for the server URL, the admin username and the
130
+ password (typed hidden, never shown to the agent), checks them against the server, then
131
+ saves URL and username to `~/.config/as-mcp-server/profiles.toml` and the password in your
132
+ OS keyring (macOS Keychain, Windows Credential Manager, Linux Secret Service):
133
+
134
+ uvx as-mcp-server setup
135
+
136
+ If the account needs a second sign-in step you are asked for it as well, with the server's
137
+ own prompt (the authenticator code, or for example a PIN from a RADIUS back end).
138
+ 3. Register the server with your agent: run its command from the table in
139
+ [Agent setup](#agent-setup), or add the entry to its configuration file. There is no
140
+ secret in this configuration.
141
+ 4. Ask the agent something: "Is my VPN healthy?" It will call `get_status_overview`.
142
+
143
+ Pin a version if you want upgrades to be explicit: `uvx as-mcp-server@0.1.0`. `uvx` keeps
144
+ using the version it first cached until you pass `@latest` or run `uv cache clean`.
145
+
146
+ ### Agent setup
147
+
148
+ | Agent | Command | Configuration file |
149
+ | --- | --- | --- |
150
+ | Claude Code | `claude mcp add openvpn-as -- uvx as-mcp-server` | `~/.claude.json` (add `--scope user` to use it in every project) |
151
+ | Codex CLI | `codex mcp add openvpn-as -- uvx as-mcp-server` | `~/.codex/config.toml` |
152
+ | Gemini CLI | `gemini mcp add -s user openvpn-as uvx as-mcp-server` (no `--`) | `~/.gemini/settings.json` |
153
+ | OpenCode | `opencode mcp add openvpn-as -- uvx as-mcp-server` | `~/.config/opencode/opencode.json` (or `.jsonc`) |
154
+ | 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 |
155
+ | Cursor | - | `~/.cursor/mcp.json`, or `.cursor/mcp.json` in a project |
156
+ | Claude Desktop | - | macOS `~/Library/Application Support/Claude/claude_desktop_config.json`, Windows `%APPDATA%\Claude\claude_desktop_config.json` (Settings, Developer, Edit Config) |
157
+
158
+ In Windows PowerShell type `claude.cmd` instead of `claude`: PowerShell drops the `--` when it
159
+ runs the `claude.ps1` wrapper, and the command then fails.
160
+
161
+ The entry has a different shape per agent. Claude Desktop, Cursor and Gemini CLI (and Claude
162
+ Code's project file `.mcp.json`):
163
+
164
+ {
165
+ "mcpServers": {
166
+ "openvpn-as": { "command": "uvx", "args": ["as-mcp-server"] }
167
+ }
168
+ }
169
+
170
+ VS Code:
171
+
172
+ {
173
+ "servers": {
174
+ "openvpn-as": { "type": "stdio", "command": "uvx", "args": ["as-mcp-server"] }
175
+ }
176
+ }
177
+
178
+ Codex CLI:
179
+
180
+ [mcp_servers.openvpn-as]
181
+ command = "uvx"
182
+ args = ["as-mcp-server"]
183
+
184
+ OpenCode 1.x (the command is one array, the server sits directly under `mcp`):
185
+
186
+ {
187
+ "$schema": "https://opencode.ai/config.json",
188
+ "mcp": {
189
+ "openvpn-as": { "type": "local", "command": ["uvx", "as-mcp-server"] }
190
+ }
191
+ }
192
+
193
+ OpenCode 2 (the server sits under `mcp.servers`):
194
+
195
+ {
196
+ "mcp": {
197
+ "servers": {
198
+ "openvpn-as": { "type": "local", "command": ["uvx", "as-mcp-server"] }
199
+ }
200
+ }
201
+ }
202
+
203
+ Both versions have the `opencode mcp add openvpn-as -- uvx as-mcp-server` command; 1.x writes
204
+ the first form.
205
+
206
+ ### Alternative: environment variables (headless Linux, CI, or by preference)
207
+
208
+ Skip `setup` and give the agent the settings as environment variables of the server entry.
209
+ With a command, add one flag per variable: `--env KEY=value` for Codex CLI and OpenCode, `-e
210
+ KEY=value` for Gemini CLI and Claude Code. Claude Code reads every word after `-e` as another
211
+ variable, so put the server name first:
212
+
213
+ claude mcp add openvpn-as -e OPENVPN_AS_URL=https://vpn.example.com:943 \
214
+ -e OPENVPN_AS_USER=mcp-admin -e OPENVPN_AS_PASSWORD=... -- uvx as-mcp-server
215
+
216
+ In a configuration file the variables go into `env` next to `command` (Claude Desktop, Cursor,
217
+ Gemini CLI, VS Code, Claude Code), into `environment` for OpenCode, and into a
218
+ `[mcp_servers.openvpn-as.env]` table for Codex CLI:
219
+
220
+ {
221
+ "mcpServers": {
222
+ "openvpn-as": {
223
+ "command": "uvx",
224
+ "args": ["as-mcp-server"],
225
+ "env": {
226
+ "OPENVPN_AS_URL": "https://vpn.example.com:943",
227
+ "OPENVPN_AS_USER": "mcp-admin",
228
+ "OPENVPN_AS_PASSWORD": "..."
229
+ }
230
+ }
231
+ }
232
+ }
233
+
234
+ The password then lives in plain text in that file, like any API key in an MCP configuration.
235
+ Environment variables override the stored profile field by field.
236
+
237
+ ### Check it works
238
+
239
+ uvx as-mcp-server check
240
+
241
+ prints the URL, user, where each value came from, TLS mode and the server version. Inside a
242
+ conversation the equivalent is the `connection_info` tool, which never fails and reports any
243
+ problem in its `error` field.
244
+
245
+ ## Tools
246
+
247
+ All tools are read-only except `login`, which only creates a session.
248
+
249
+ | Tool | What it answers | Parameters |
250
+ | --- | --- | --- |
251
+ | `connection_info` | Which server, which user, TLS mode, session, server version; problems as text | - |
252
+ | `login` | Answer the second sign-in step: the authenticator code or the back end's prompt | `totp_code` |
253
+ | `get_status_overview` | One-call health check: version, EULA, DCO, chosen config values | `config_names` (optional) |
254
+ | `get_server_status` | Internal services and auth modules, last restart | - |
255
+ | `get_server_info` | Version, build, OS, architecture, hostname | - |
256
+ | `get_active_vpn_connections` | Connected clients and daemons | - |
257
+ | `get_log_reports` | Connection / authentication log with filters | `page_size`, `offset`, `username`, `since`, `until`, `errors_only`, `active_only`, `search`, `order_by`, `sort` |
258
+ | `get_license_info` | License type, concurrent connections, subscription state | - |
259
+ | `list_users` | Users and their properties (MFA secrets redacted) | `page_size`, `offset`, `name_contains`, `group`, `admins_only`, `autologin_only`, `usernames`, `order_by`, `sort` |
260
+ | `list_groups` | Groups, member counts or members | `page_size`, `offset`, `name_contains`, `include_members`, `group_names`, `order_by`, `sort` |
261
+ | `get_default_user` | The `__DEFAULT__` profile everyone inherits from | - |
262
+ | `list_access_rulesets` | Rulesets assigned to a user, a group or `__DEFAULT__` | `owner`, `name_contains` |
263
+ | `list_access_rules` | The rules (destination, action) inside rulesets | `ruleset_ids`, `rule_type`, `match_data_contains` |
264
+
265
+ Responses are the Access Server API's own JSON. `since`/`until` are relative durations such
266
+ as `30m`, `24h`, `7d`, `15d 20m`. "What can user X reach": rulesets for the user, for each of
267
+ the user's groups, and for `__DEFAULT__`, then `list_access_rules` with the collected ids.
268
+
269
+ ## Configuration reference
270
+
271
+ | Variable | Default | Meaning |
272
+ | --- | --- | --- |
273
+ | `OPENVPN_AS_URL` | from `setup` | `https://host[:943]` |
274
+ | `OPENVPN_AS_USER` | from `setup` | admin username |
275
+ | `OPENVPN_AS_PASSWORD` | from the OS keyring | password |
276
+ | `OPENVPN_AS_CA_CERT` | - | PEM file of a private CA that signed the server certificate |
277
+ | `OPENVPN_AS_INSECURE` | `false` | `true` disables TLS verification. Loud: a warning is logged and `connection_info` reports it. Test servers only. |
278
+ | `OPENVPN_AS_TIMEOUT` | `30` | request timeout in seconds |
279
+ | `OPENVPN_AS_LOG_LEVEL` | `WARNING` | `DEBUG`, `INFO`, `WARNING`, `ERROR`; logs go to stderr and never contain secrets |
280
+ | `OPENVPN_AS_CONFIG_DIR` | `~/.config/as-mcp-server` | where `profiles.toml` lives |
281
+
282
+ Reserved for later releases, not implemented yet: `OPENVPN_AS_PROFILE`, `OPENVPN_AS_TOOLS`,
283
+ `OPENVPN_AS_READ_ONLY`, `OPENVPN_AS_EXTRA_HEADERS`.
284
+
285
+ Remove stored credentials with `uvx as-mcp-server setup --clear`.
286
+
287
+ ## Logs and debugging
288
+
289
+ - The server logs to stderr only; `OPENVPN_AS_LOG_LEVEL=DEBUG` adds one line per request
290
+ (`METHOD /path -> status in N ms`) and the background token renewals. Bodies, headers,
291
+ passwords and tokens are never logged.
292
+ - The reliable way to read the log is a terminal: `OPENVPN_AS_LOG_LEVEL=DEBUG uvx
293
+ as-mcp-server check` performs the same login and `GET /server/info` as the
294
+ `connection_info` tool and prints the log next to the result.
295
+ - Inside an agent, where stderr ends up is the agent's business. Claude Code has no log view
296
+ in its `/mcp` panel (it offers View tools, Reconnect and Disable); it writes each server's
297
+ stderr as `Server stderr: ...` records into
298
+ `~/Library/Caches/claude-cli-nodejs/<project>/mcp-logs-<server>/*.jsonl` on macOS
299
+ (`~/.cache/claude-cli-nodejs/...` on Linux), one file per session. `claude --debug` (or
300
+ `--debug-file <path>`) turns on Claude Code's own debug log, which includes its MCP
301
+ connection messages.
302
+ - `OPENVPN_AS_TIMEOUT=60` if the server is slow (default 30 s).
303
+
304
+ ## MFA and other second sign-in steps
305
+
306
+ If the admin account needs a second step, the first tool call reports it and quotes the
307
+ server's own prompt. For Access Server's built-in authenticator that is the 6-digit TOTP code;
308
+ for a RADIUS, LDAP or PAM back end it is whatever the back end asks for (a PIN, a Duo passcode,
309
+ an answer to a question), passed through word for word. The agent shows you the prompt, asks
310
+ for the answer and calls `login`. The session then lasts for the lifetime of the MCP process:
311
+ Access Server issues short-lived tokens (10 minutes by default), and the server renews its
312
+ token in the background before it expires, so you are asked again only after the limit Access
313
+ Server puts on renewals (4 hours by default), or if the machine slept past a token's expiry.
314
+
315
+ The answer goes to the challenge the server already issued, so a push- or SMS-based back end
316
+ is not triggered a second time, as long as you answer within about 90 seconds (Access Server
317
+ forgets a challenge after two to three minutes). After a wrong answer or a longer pause the
318
+ server issues a new challenge; if it asks a different question, the agent quotes it before
319
+ anything is sent. Authenticator codes are single-use and must be exactly six digits (a
320
+ mistyped code is refused locally and does not count as a failed attempt); five wrong answers
321
+ lock the account for 15 minutes (Access Server default).
322
+
323
+ ### SAML-only accounts
324
+
325
+ as-mcp-server signs in with a username and password, plus the answer to a second step when
326
+ asked. It cannot
327
+ complete the browser-based SAML flow. When the Access Server's default authentication system is
328
+ SAML, an admin account that follows that default is refused with an error that says so, and no
329
+ MFA code is requested (a code could never succeed). Use an administrator whose authentication
330
+ method is set per user to something else: `list_users` shows it as `auth_method` (`sacli` calls
331
+ it `user_auth_type`), and the built-in `openvpn` administrator uses `local`.
332
+
333
+ ## Security notes
334
+
335
+ - Your password never enters the conversation: `setup` is a terminal command. Only the
336
+ answer to a second sign-in step passes through the agent: a 30-second, single-use
337
+ authenticator code, or whatever a RADIUS, LDAP or PAM back end asks for. A static PIN or
338
+ passcode given this way reaches the model provider like any other tool argument; if that
339
+ is not acceptable, use an admin account whose second step is a one-time code.
340
+ - Tool responses go to your agent's model provider. The server redacts TOTP secrets, which
341
+ Access Server otherwise includes in user listings, the subscription key from the license
342
+ information, and configuration values whose key name looks like a secret (passwords,
343
+ bind passwords, tokens, private keys, the subscription bundle; a trailing version or
344
+ index such as `.2.9.0` does not hide the name) or that Access Server itself marks as
345
+ redacted, in `get_status_overview`; everything else in a response (user names, addresses,
346
+ log entries, other configuration values) is visible to the model.
347
+ - Log records and the list of connected clients contain text that anyone on the internet
348
+ can choose: a failed login is logged with the username exactly as typed, and VPN clients
349
+ report their own platform and version. Such text can be written to look like log lines
350
+ or instructions for the model. `get_log_reports` and `get_active_vpn_connections` show a
351
+ client-chosen username, certificate name, platform, version or proxied address only when
352
+ it looks like a plain value; anything else (for example newlines, control or invisible
353
+ characters, sentence punctuation, more than three words, more than 64 characters or
354
+ 16 East Asian characters) is replaced by
355
+ `{"withheld": true, "length": ..., "flags": [...], "fingerprint": ...}` without the text.
356
+ The same fingerprint means the same text. Read the full records in the Access Server
357
+ Admin UI if you need them. This is a heuristic: a short plain-looking string such as
358
+ `ignore_previous_instructions` still gets through, so treat a model's security summary
359
+ as a draft all the same.
360
+ - Tool calls are rate limited: 30 in a row, then 30 per minute (one every two seconds). An
361
+ agent stuck in a loop gets a tool error that tells it how long to wait instead of sending
362
+ Access Server a request per call. Listing the tools and connecting are never limited.
363
+ - Use a dedicated admin account so its lockout or revocation affects only the agent.
364
+ - The keyring item is bound to the Python binary on macOS; after a `uvx` upgrade the system
365
+ may ask once whether the new binary may read it.
366
+ - See `SECURITY.md` for reporting.
367
+
368
+ ## Compatibility
369
+
370
+ | Access Server | Result |
371
+ | --- | --- |
372
+ | 3.2.2 | all tools verified |
373
+ | 3.1.0 | all tools verified |
374
+ | 3.0.x | not supported (Web API v0.1) |
375
+
376
+ The Web API is declared unstable by OpenVPN; an Access Server upgrade that changes it needs a
377
+ matching `as-mcp-server` release. Please open an issue with the server version if a tool
378
+ stops working.
379
+
380
+ ## Roadmap and feedback
381
+
382
+ This first release only reads. Later releases may add safe write operations (users, groups, access
383
+ rules, profiles) and server administration, each behind explicit opt-in. Tell us what is
384
+ missing with the **Tool request** issue template and report problems with **Bug report**.
385
+
386
+ ## Development
387
+
388
+ make install # uv sync
389
+ make check # ruff + pytest (no network)
390
+ make stand-up # two real Access Servers and FreeRADIUS in Docker, see dev/README.md
391
+ make live # tests against them
392
+ make release-build # the package as published: dependency versions pinned to uv.lock
393
+
394
+ Agent instructions: `AGENTS.md`.