Skip to main content
Glama
README.md
# twingate-mcp

A **read-only** [MCP](https://modelcontextprotocol.io) server that lets Claude (or any MCP client)
answer questions about a Twingate network: users, groups, resources, remote networks, connectors,
devices, and who can access what. It never creates, changes, or deletes anything.

One server process talks to one Twingate network with one API token.

## 1. Create a read-only API token

In the Twingate Admin Console go to **Settings → API**, generate a token, and choose the
**Read-only** permission. Treat it like a password.

## 2. Install

```sh
git clone <this repo> && cd twingate-mcp
uv sync
cp .env.example .env   # then fill in TWINGATE_NETWORK and TWINGATE_API_KEY
uv run twingate-mcp version
```

`TWINGATE_NETWORK` is everything before `.twingate.com`: for `acme.twingate.com` use `acme`; for a regional tenant such as `acme.us1.twingate.com` use `acme.us1`.

| Variable | Default | Notes |
|---|---|---|
| `TWINGATE_NETWORK` | required | Host before `.twingate.com` (letters, digits, `-`, `.`), e.g. `acme` or `acme.us1` |
| `TWINGATE_API_KEY` | required | Read-only token recommended |
| `TWINGATE_PAGE_SIZE` | `100` | 1–100 |
| `TWINGATE_DEFAULT_LIMIT` | `100` | Items per tool call when `limit` is omitted |
| `TWINGATE_MAX_LIMIT` | `1000` | Hard cap per tool call |
| `TWINGATE_TIMEOUT_SECONDS` | `30` | Per request |
| `TWINGATE_RETRY_ATTEMPTS` | `5` | |
| `TWINGATE_MCP_HTTP_TOKEN` | unset | Required for non-loopback HTTP |
| `TWINGATE_MCP_ALLOWED_HOSTS` | unset | Extra comma-separated hostnames accepted in `Host`/`Origin` for HTTP, e.g. a Compose service name |
| `TWINGATE_LOG_LEVEL` | `INFO` | `DEBUG`, `INFO`, `WARNING`, `ERROR` or `CRITICAL` (any case); logs go to stderr |

## 3. Use with Claude Desktop (stdio)

Add this to `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "twingate": {
      "command": "uv",
      "args": ["run", "--directory", "/path/to/twingate-mcp", "twingate-mcp", "serve", "--transport", "stdio"],
      "env": {
        "TWINGATE_NETWORK": "acme",
        "TWINGATE_API_KEY": "<your read-only token>"
      }
    }
  }
}
```

## 4. Streamable HTTP

```sh
uv run twingate-mcp serve --transport http            # 127.0.0.1:8000, endpoint /mcp
```

Binding to anything other than a loopback address fails unless `TWINGATE_MCP_HTTP_TOKEN` is set.
With it set, every request must send `Authorization: Bearer <token>`. DNS-rebinding protection only
accepts `Host` headers matching the bind address.

> **Security caveat:** the bearer token is a single shared secret, not user authentication. Anyone
> holding it can read everything the Twingate API token can read. Use HTTPS in front of it (a reverse
> proxy) and don't expose it to the internet.

## 5. Docker

The image runs as a non-root user and contains only the runtime virtualenv. `.env` and the schema
are excluded from the build context.

```sh
docker build -t twingate-mcp:local .
```

**stdio (Claude Desktop):** use `docker` as the command:

```json
"twingate": {
  "command": "docker",
  "args": ["run", "-i", "--rm", "--env-file", "/path/to/twingate-mcp/.env", "twingate-mcp:local"]
}
```

**HTTP (Compose):** add `TWINGATE_MCP_HTTP_TOKEN` to `.env` (generate one with
`openssl rand -hex 32`), then:

```sh
docker compose up -d     # http://localhost:8000/mcp, published on 127.0.0.1 only
```

Inside the container the server binds `0.0.0.0`, so the bearer token is mandatory. It accepts
`localhost` and `127.0.0.1` Host headers; put a TLS reverse proxy in front before exposing it beyond
the local machine.

## Tools

All tools are read-only. List tools take an optional `limit` (default `TWINGATE_DEFAULT_LIMIT`,
capped at `TWINGATE_MAX_LIMIT`) and return `{items, total_returned, truncated}`.

| Tool | Input | Returns |
|---|---|---|
| `list_users` | `limit` | id, email, first/last name, role, state |
| `get_user` | `email` | One user or null |
| `list_groups` | `limit` | id, name, type, active, member count |
| `list_resources` | `limit` | id, name, address, remote network name, active |
| `list_remote_networks` | `limit` | id, name, location, active, connector count |
| `list_connectors` | `limit` | id, name, remote network, state, last heartbeat |
| `list_devices` | `limit` | id, name, OS, user email, trust status, last seen |
| `get_resource_access` | `resource_id` | Groups and service accounts with access |
| `get_user_access` | `email` | The user's groups and resources reachable through them |

From a terminal: `uv run twingate-mcp call list_users --arg limit=5`.

## Contributing: data-safety rules

This repo must never contain company or tenant data.

1. Test data is synthetic: seeded polyfactory/Faker factories (`tests/factories.py`) using `safe_*`
   providers, or small hand-written JSON in `tests/fixtures/` with values like `alice@example.com`
   and the `acme-test` network. Never use Faker's plain `email()`, `domain_name()`, or `hostname()`.
2. Never record or paste real API responses. No VCR/cassette/record-mode libraries.
3. No real network names, emails, hostnames, or IDs anywhere: code, tests, docs, commit messages.
4. `.env` and `schema/twingate.graphql` are gitignored. Pull the schema from a personal or trial
   tenant, never an employer tenant.
5. Log operation names, durations, and status codes only, never bodies or headers.
6. `gitleaks` runs in pre-commit (`uv run pre-commit install`). `tests/test_data_safety.py` fails on
   non-`example.*` domains or non-allowlisted network names.
7. Only `query` operations are allowed in `queries/`; a test enforces it.

Quality gates: `uv lock --check`, `uv run ruff check .`, `uv run ruff format --check .`,
`uv run pyright`, `uv run pytest --cov=twingate_mcp --cov-fail-under=85`, the `pip-licenses`
command in the spec, and `gitleaks detect --no-banner`.

## License

MIT, see [LICENSE](LICENSE).

TDQS

A4.2/5.0

Scored across 9 tools

Disambiguation5/5

Each tool targets a distinct resource or a distinct direction of an access query: list_* tools each cover one entity type, get_user is the single-email lookup complement to list_users, and get_user_access (user→resources) is clearly the inverse of get_resource_access (resource→principals). Descriptions explicitly cross-reference each other to steer selection, leaving no realistic misselection risk.

Naming Consistency5/5

Uniform verb_noun snake_case throughout: list_* for enumerations (groups, resources, users, remote_networks, connectors, devices) and get_* for targeted lookups (user, user_access, resource_access). No camelCase or vague verbs intrude.

Tool Count5/5

Nine tools is well within the ideal 3-15 range and each earns its place by covering a distinct Twingate entity or access relationship. Nothing looks padded or redundant.

Completeness3/5

The read-only/audit surface is coherent and covers the main entities plus access relationships, but it is entirely queries: there are no create, update, or delete operations for users, groups, resources, or connectors, and no get_group/get_resource/get_connector single-item lookups. An agent can investigate but cannot remediate anything it discovers.

Maintenance

ActivityMaintained
ResponsivenessNo issues