netbird-mcp
by daniel100097
README.md
# netbird-mcp
A Bun/TypeScript MCP server that acts as a client for the NetBird REST API. Built with the [MCP TypeScript SDK v2](https://ts.sdk.modelcontextprotocol.io/v2/).
- **stdio** for MCP hosts that launch a process.
- **SSE** at `/sse` with messages posted to `/messages`.
- **Streamable HTTP** at `/mcp`, including SSE responses and SDK v2 protocol support.
- 90 schema-validated API tools, with environment flags for each feature group.
- Read-only by default, Bun tests, Docker development/runtime images, and GitHub Container Registry publishing.
## Run with Docker Compose
Docker with Compose v2.24+ is sufficient; Bun does not need to be installed on the host.
```sh
cp .env.example .env
# Edit .env: set NETBIRD_API_TOKEN and a separate MCP_AUTH_TOKEN.
# To generate MCP_AUTH_TOKEN: openssl rand -hex 32
docker compose up --build -d netbird-mcp
curl http://127.0.0.1:3000/healthz
```
Use a NetBird personal access token or service-user token. `NETBIRD_API_URL` defaults to `https://api.netbird.io/api`; for self-hosted NetBird, use your management URL **including `/api`**. A path prefix such as `https://vpn.example/management/api` is supported.
The MCP bearer token protects access to this server. It is separate from the NetBird token, which the server sends upstream as `Authorization: Token …`. Credentials are loaded at runtime; `.env` is excluded from Git and image builds.
Compose publishes port 3000 on localhost. To serve another machine, configure `MCP_BIND_ADDRESS`, add your public hostname to `MCP_ALLOWED_HOSTS`, and put TLS in front of the server. When proxying SSE, disable response buffering, allow long-lived streams, and forward `/mcp`, `/sse`, and `/messages` at the root of the same public origin. Legacy SSE sessions live in one process, so multiple replicas need sticky routing for `/sse` and `/messages`.
```sh
docker compose logs -f netbird-mcp
docker compose down
```
## Connect an MCP host
For a remote connection, use `http://127.0.0.1:3000/mcp` (Streamable HTTP) or `http://127.0.0.1:3000/sse` (SSE) and send `Authorization: Bearer <MCP_AUTH_TOKEN>`. For example, in clients that support `mcpServers` with URL and header settings:
```json
{
"mcpServers": {
"netbird": {
"url": "http://127.0.0.1:3000/mcp",
"headers": {
"Authorization": "Bearer REPLACE_WITH_MCP_AUTH_TOKEN"
}
}
}
}
```
Replace `/mcp` with `/sse` for an SSE client. Its authorization header must be sent on both the stream GET and message POST requests. The project uses the official v2 `@modelcontextprotocol/server-legacy` adapter for this transport; the SDK marks it deprecated. `/mcp` uses the current v2 transport.
For **stdio through Docker**, build the image, then configure your host to launch it:
```sh
docker build -t netbird-mcp:local .
```
```json
{
"mcpServers": {
"netbird": {
"command": "docker",
"args": [
"run",
"--rm",
"-i",
"--init",
"--env-file",
"/absolute/path/to/netbird-mcp/.env",
"-e",
"MCP_TRANSPORT=stdio",
"netbird-mcp:local"
]
}
}
}
```
Keep `-i` and omit `-t`: stdout carries only MCP messages. Logs go to stderr. Stdio needs only the NetBird token; it does not require the MCP bearer token or an exposed port. The image defaults to stdio, while Compose defaults to SSE/server mode.
If your Docker daemon requires `DOCKER_HOST` or `DOCKER_CONTEXT`, pass those variables in the MCP host's process environment too; some hosts do not inherit your shell environment.
You can also launch a stdio process through Compose:
```sh
docker compose run --rm --no-deps -T -e MCP_TRANSPORT=stdio netbird-mcp
```
## Feature flags
`NETBIRD_READ_ONLY=true` removes all write tools, including domain validation (a NetBird GET endpoint with side effects). Set it to `false` to expose writes for enabled feature groups.
Every feature flag below defaults to `true`. Setting a flag to `false` prevents its tools from being registered, so they are neither listed nor callable on any transport. Configuration is read at startup; restart after editing `.env`.
| Environment flag | Controls |
| ------------------------------- | --------------------------------------------------------------------- |
| `NETBIRD_ENABLE_PEERS` | Peer information, accessible peers, updates, deletion, and jobs |
| `NETBIRD_ENABLE_PEER_CONTROL` | Peer updates, deletion, and job creation; peer reads remain available |
| `NETBIRD_ENABLE_GROUPS` | Groups |
| `NETBIRD_ENABLE_POLICIES` | Access policies |
| `NETBIRD_ENABLE_ROUTES` | Routes |
| `NETBIRD_ENABLE_NETWORKS` | Networks, network resources, and routers |
| `NETBIRD_ENABLE_DNS` | Nameservers, settings, zones, and records |
| `NETBIRD_ENABLE_SETUP_KEYS` | Setup keys |
| `NETBIRD_ENABLE_USERS` | User listing, creation, updates, deletion, and current user |
| `NETBIRD_ENABLE_ACCOUNTS` | Account listing, updates, and deletion |
| `NETBIRD_ENABLE_EVENTS` | Audit, traffic, and reverse proxy access logs |
| `NETBIRD_ENABLE_POSTURE_CHECKS` | Posture checks |
| `NETBIRD_ENABLE_REVERSE_PROXY` | Reverse proxy services, domains, clusters, tokens, and access logs |
For example, allow management of groups, policies, and other enabled areas while disabling peer control and all reverse proxy tools:
```dotenv
NETBIRD_READ_ONLY=false
NETBIRD_ENABLE_PEER_CONTROL=false
NETBIRD_ENABLE_REVERSE_PROXY=false
```
Peer writes require both peer flags. Reverse proxy logs require both the events and reverse proxy flags. These switches control this server's tool exposure; NetBird token permissions still apply. A disabled peer-control flag does not prevent enabled policy/group tools from changing network access.
Tools use explicit endpoints and validated request schemas, with no unrestricted HTTP request tool. Path parameters are top-level arguments, filters are under `query`, and write payloads are under `body`:
```json
{ "name": "netbird_list_peers", "arguments": { "query": { "name": "laptop" } } }
```
```json
{
"name": "netbird_get_network_resource",
"arguments": { "networkId": "network-id", "resourceId": "resource-id" }
}
```
```json
{
"name": "netbird_create_group",
"arguments": { "body": { "name": "developers", "peers": ["peer-id"] } }
}
```
Results contain JSON text and `structuredContent: { "data": ... }`. API failures return MCP tool errors with HTTP status. Calls have a timeout, propagate cancellation, refuse redirects, and limit responses to 10 MiB. Writes are never automatically retried.
Tool coverage is an explicit allowlist, not the entire NetBird API. Billing, identity-provider integrations, user-token management, bootstrap/setup, and other unlisted APIs are not exposed. Newer or cloud-only endpoints may return an upstream error on older/self-hosted NetBird deployments. Read an existing object before using PUT; some APIs require the complete payload.
## Other environment settings
See [.env.example](.env.example) for a complete starting configuration.
| Variable | Default | Purpose |
| ---------------------- | ---------------------------- | -------------------------------------------------------------------------- |
| `NETBIRD_API_TOKEN` | Required | NetBird API credential |
| `NETBIRD_API_URL` | `https://api.netbird.io/api` | Management API base URL |
| `NETBIRD_TIMEOUT_MS` | `30000` | Per-call timeout, up to 300000 ms |
| `MCP_TRANSPORT` | `stdio` | `stdio`, `sse`, or `http`; both network modes serve both endpoints |
| `MCP_AUTH_TOKEN` | Required in network mode | Shared bearer token for MCP clients |
| `MCP_HOST` | `127.0.0.1` | Listener; Docker/Compose overrides to `0.0.0.0` |
| `MCP_PORT` | `3000` | Listening port and Compose published port |
| `MCP_BIND_ADDRESS` | `127.0.0.1` | Compose-only host interface for port publishing |
| `MCP_ALLOWED_HOSTS` | `localhost,127.0.0.1,[::1]` | Allowed request hostnames, without scheme or port |
| `MCP_ALLOWED_ORIGINS` | Empty | Exact comma-separated browser origins; requests without Origin are allowed |
| `MCP_MAX_SSE_SESSIONS` | `100` | Maximum simultaneous legacy SSE connections |
Boolean flags accept `true/false`, `1/0`, `yes/no`, and `on/off`, case-insensitively. Invalid configuration fails at startup. `/healthz` is a liveness endpoint and does not call NetBird. HTTP bodies are limited to 1 MiB. This is a shared-token service: all authenticated MCP clients use the same NetBird credential and feature settings.
## Development and tests
Application and catalog-generation code lives in `src/`; Bun tests live in `tests/`.
```sh
# Entire check suite in Docker; test execution needs no network or real credentials.
docker compose --profile test run --build --rm test
# Watch source changes in Docker (after configuring .env).
docker compose --profile dev up --build dev
# Run a single test file in Docker.
docker compose --profile test run --rm test bun test tests/transports.test.ts
```
For local Bun 1.4.2+ development:
```sh
bun install --frozen-lockfile
bun run check
bun run build
MCP_TRANSPORT=stdio bun run start
# Or use .env's network settings:
bun run dev
```
The tests exercise configuration, API authentication, path safety, error redaction, timeouts/cancellation, input validation, feature removal, real HTTP/SSE connections, real stdio subprocesses, and shutdown. NetBird responses are supplied by local fixtures; no live NetBird account is needed.
The checked-in [tool catalog](src/netbird/catalog.json) is generated from NetBird's OpenAPI schema at the revision recorded in [generate-catalog.ts](src/scripts/generate-catalog.ts). Regular builds are offline with respect to NetBird's schema. To update it, review the explicit route allowlist and pinned revision, then run:
```sh
bun run generate:catalog
# Or, after building the development image:
docker compose --profile dev run --rm --no-deps dev bun run generate:catalog
```
Review the generated diff and rerun checks before committing it. Attribution for the schema is in [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
## GitHub image publishing
[.github/workflows/image.yml](.github/workflows/image.yml) builds on branch pushes and pull requests. The Docker build runs formatting, TypeScript checks, and Bun tests before producing the runtime image.
Pushes to the repository's default branch publish `ghcr.io/<owner>/<repo>:latest` and a `sha-…` tag. Tags such as `v0.1.0` publish version tags (`0.1.0`, `0.1`) and a commit tag. Published images support `linux/amd64` and `linux/arm64`; pull requests build without publishing. Authentication uses GitHub's built-in `GITHUB_TOKEN` with `packages: write`.
After pushing this project to GitHub, replace `netbird-mcp:local` in your Docker client command with the GHCR image name. GitHub packages may initially be private; set the package visibility to public if anonymous pulls are desired. No registry credentials or NetBird secrets are needed in the workflow.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues