MCP server
MCP server
PinkGlasses has an MCP server, so an AI client — Claude Code, Claude Desktop, anything that speaks the Model Context Protocol — can do what the web UI does: read the inventory and findings, manage target groups, start, watch, pause, stop and rerun scans, and keep schedules.
How it is built, and why
It is a thin adapter over the HTTP API, nothing more. The API already holds
every rule that matters — roles, target authorization, the exit a scan needs,
the refusals written for a person — so the MCP server reimplements none of
them and cannot drift from them. It runs inside the api, served at /mcp
on the web app’s own address, where its calls reach the router in-process; the
same code is also a binary in the control-plane image (/usr/local/bin/mcp)
for a local client over stdio.
It authenticates with a PinkGlasses API token (Accounts → API tokens,
or POST /tokens). Two things follow: a viewer token gives a read-only MCP
server with no configuration, and every run started or group removed through
a model shows in the audit log under that token’s account, like any other API
call.
Tools are task-shaped, not route-shaped: about two dozen, named the way a
person would ask (search, get_host, start_scan, wait_for_run), rather
than one per HTTP route. Results are compact JSON with ids, so calls chain.
Destructive tools — remove_target_group, delete_run, remove_schedule —
refuse unless called with confirm: true. delete_company exists only when
ASM_MCP_ALLOW_DELETE_COMPANY=true is set — on the api service in compose, or
on the stdio binary — and then also demands the company’s exact name.
A scan started with a local exit names a VPN configuration, and that must be
one of the token owner’s own (list_vpn_configs shows exactly those):
configurations belong to accounts, not companies, so a token sees the same
list in every company and never another account’s.
Not exposed: worker enrolment and removal, VPN configuration bodies, alert
channel secrets, wordlist editing, API tokens and accounts. Those hand out
credentials, and a model has no business with them; the web UI and the API
remain for people. A test in internal/mcpserver walks the route table and
fails the build if a viewer or operator route is reachable through no tool or
resource and is not on that list with a reason.
Tools
| Group | Tools |
|---|---|
| Orientation | list_companies, company_summary, rename_company |
| Inventory | search (the query language, with facets), list_hosts, get_host, list_findings, update_finding |
| Targets | list_target_groups, add_targets, edit_target_group, remove_target_group |
| Scanning | start_scan (now, once at a time, or on a repeat), list_runs, get_run, wait_for_run, run_diff, pause_run, resume_run, stop_run, rerun, delete_run, list_schedules, edit_schedule, remove_schedule, scan_parameters |
| Infrastructure | list_workers (workers, pools, runs’ own fleets), list_vpn_configs (the token owner’s, names only, usable in any company), list_wordlists, alerts |
Resources for the read side: pinkglasses://companies,
pinkglasses://companies/{id}/summary, pinkglasses://hosts/{id},
pinkglasses://runs/{id}, pinkglasses://services/{id}/screenshot (the
PNG), pinkglasses://scan-parameters. Prompts: triage_changes,
explain_host, plan_scan.
In the app
MCP in the sidebar is the page for all of this: the server’s address on this install, a Create token form (viewer for a read-only server, operator to let it scan; the token is shown once and the snippets carry it while it is on screen), copy-ready snippets for Claude Code, Cursor and a stdio client, the skill download, and the list of tools this install exposes.
The skill
A skill is a folder an AI client reads to know how to use a tool well.
The page’s Download the skill gives a zip that unpacks into pinkglasses/:
| File | What it holds |
|---|---|
SKILL.md |
when to reach for which tool; the scan model in short; destructive-tool rules; how to answer |
reference/tools.md |
every tool with its parameters — generated from the server’s tool set, so it never drifts |
reference/search-query-language.md |
the search syntax |
reference/scanning.md |
companies, target groups, authorization, profiles, exits; starting and following a run |
Unpack it into ~/.claude/skills/ for Claude Code across every project, or
.claude/skills/ inside one project. Other clients that load SKILL.md
folders take the same. The API serves it at GET /api/v1/mcp/skill.zip.
Running it
Over HTTP, from the running app — the usual way. The api serves the MCP
server itself, at /mcp on the same address as the web app:
http://<host>:8080/mcp. There is nothing to enable and nothing else to
deploy; it is up whenever the web app is. Each request carries its own token as
Authorization: Bearer pgt_…; the server holds no credential of its own, and
a request without a token gets the API’s own refusal. It is reachable wherever
the web app is — the same address, port and hostname, plus /mcp — so put TLS
in front before exposing it beyond the host (the compose https profile does, see the
README’s “HTTPS”), and if a reverse proxy sits in
front, forward /mcp with the Authorization header and without response
buffering, as for the run events stream.
Both HTTP transports are answered on that one path: streamable HTTP (a POST per message, the current transport) and the older SSE transport (a hanging GET for the event stream, then POSTs to the session endpoint it names). Clients differ in which they speak, and some try the old one first, so a client needs no transport setting beyond the URL.
claude mcp add --transport http pinkglasses http://localhost:8080/mcp \
--header "Authorization: Bearer pgt_…"
Claude Desktop and other clients take the same URL and header in their MCP
servers configuration. In Cursor: Settings → MCP → Add new global MCP
server opens ~/.cursor/mcp.json (or .cursor/mcp.json in a project — keep
it out of version control, it holds the token):
{
"mcpServers": {
"pinkglasses": {
"url": "http://localhost:8080/mcp",
"headers": { "Authorization": "Bearer pgt_…" }
}
}
}
Locally over stdio, for a client that cannot speak HTTP or a laptop that reaches the api through a tunnel of its own. The binary is in the published image; the token is the only configuration:
claude mcp add pinkglasses -e ASM_API_URL=http://localhost:8080 -e ASM_MCP_TOKEN=pgt_… \
-- docker run -i --rm --network host -e ASM_API_URL -e ASM_MCP_TOKEN \
--entrypoint /usr/local/bin/mcp ghcr.io/cobbbex/pinkglasses:latest
Point ASM_API_URL at wherever the api answers. The same command in Cursor’s
mcp.json is "command": "docker" with those arguments under "args".
| Variable | Where | Meaning |
|---|---|---|
ASM_MCP_ALLOW_DELETE_COMPANY |
api service, or the binary | true adds the delete_company tool |
ASM_API_URL |
stdio binary | where the api answers (default http://localhost:8080) |
ASM_MCP_TOKEN |
stdio binary | the API token |
The binary’s own ASM_MCP_TRANSPORT=http / ASM_MCP_ADDR mode still exists
for running it apart from the api; the api’s /mcp is the same server and
needs none of that.
If a client cannot connect
- The page comes back instead of a server — the response to
/mcpis HTML. That install predates the built-in endpoint; redeploy it. Until then the endpoint does not exist there. - Connected, but every tool answers “sign in to continue” — the token is
missing or wrong. The connection itself needs no token; the calls do. Check
the
Authorization: Bearer pgt_…header, and that the token has not been revoked under Accounts → API tokens. - A reverse proxy in front must forward
/mcpas well as/api/, pass theAuthorizationheader through, and not buffer responses on that path (they are event streams). With those three in place the proxy’s address and TLS are what the client uses. -
What to try by hand — a bare request that needs no client:
curl -s -X POST http://localhost:8080/mcp \ -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'An
event: messageline with the server’s capabilities means the endpoint is up; anything else says what is in the way.