Picus Security MCP Server
README.md
<div align="center">
<img src="assets/picus-logo.png" alt="Picus" width="260" />
# Picus Security MCP Server
**Drive the [Picus](https://www.picussecurity.com/) Breach & Attack Simulation platform from Claude — and any MCP client.**
[](LICENSE)
[](pyproject.toml)
[](https://modelcontextprotocol.io)
[](https://apidocs.picussecurity.com/)
### 29 tools · 8 Picus API groups · 23 read-only + 6 write (opt-in)
</div>
A [Model Context Protocol](https://modelcontextprotocol.io) server that exposes the
Picus Security Breach & Attack Simulation (BAS) REST API as tools any
MCP-compatible client — [Claude Code](https://docs.claude.com/en/docs/claude-code),
Claude Desktop, and others — can call.
> Ask your assistant to *"list failed Picus simulations"*, *"show the latest run
> results for the DMZ agent"*, or *"search the threat library for ransomware
> threats affecting Windows"* — and it drives the Picus API for you.
## How it works
<div align="center">
<img src="assets/architecture.svg" width="820"
alt="An MCP client calls picus-mcp over stdio; picus-mcp calls the Picus REST API over HTTPS with a Bearer token, backed by an auto-refreshing token cache." />
</div>
You provide a long-lived **refresh token**; the server exchanges it for a
short-lived **access token**, caches it, and refreshes it automatically — so tool
calls never deal with auth.
---
## Features
- **All eight Picus API groups** — simulations, run results & analytics, threat
library, threat templates, agents, integrations, and vendor mitigations.
- **23 read-only tools** always available; **6 write tools** (create/update/
delete/cancel/simulate-now, create template) gated behind an explicit opt-in so
an assistant can't launch or destroy a simulation by accident.
- **Automatic auth** — you provide a long-lived refresh token; the server handles
the access-token exchange, caching, and refresh (including retry on a 401).
- **Portable** — speaks MCP over stdio and reads all config from the environment,
so it works with any MCP host and isn't tied to one machine.
- **Handles the API's quirks** — mixed `/v1` and `/v2` paths, inconsistent
pagination envelopes, and epoch-millisecond timestamps. Responses are returned
raw so nothing is lost in translation.
---
## Prerequisites
- Python 3.10 or newer
- A Picus Security account with REST API access
---
## Getting a Picus API token
1. Sign in to the Picus dashboard and go to
**[Settings → REST API Token](https://app.picussecurity.com/settings/rest-api-token)**.
2. Click **Generate Token**. Choose a name, description, expiration (up to 6
months), and the scopes you need.
3. **Copy the token immediately** — Picus shows it only once.
This is your *refresh* token (`PICUS_REFRESH_TOKEN`). The server exchanges it for
short-lived access tokens automatically.
---
## Install
```bash
git clone https://github.com/BeardedInfoSec/picus-security-mcp.git
cd picus-security-mcp
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e .
```
---
## Configure
Copy the example env file and paste in your token:
```bash
cp .env.example .env
```
```dotenv
# .env
PICUS_API_BASE="https://api.picussecurity.com/v1"
PICUS_REFRESH_TOKEN="paste-your-refresh-token-here"
```
`.env` is gitignored — your token never gets committed. The server auto-loads
`.env` from the working directory (real environment variables take precedence, so
a host that injects config still wins).
| Variable | Required | Default | Purpose |
| --- | --- | --- | --- |
| `PICUS_REFRESH_TOKEN` | **yes** | — | Picus REST API refresh token |
| `PICUS_API_BASE` | no | `https://api.picussecurity.com/v1` | API base; the host origin is derived from it (both `/v1` and `/v2` endpoints are used) |
| `PICUS_ENABLE_WRITE_TOOLS` | no | `false` | Register the mutating tools when `true` |
| `PICUS_REQUEST_TIMEOUT` | no | `30` | Per-request timeout (seconds) |
| `PICUS_TOKEN_LEEWAY_SECONDS` | no | `120` | Refresh the access token this many seconds before it expires |
---
## Verify it works
A smoke test builds the server, authenticates, and makes a couple of live calls:
```bash
python scripts/smoke_test.py
```
Expected output ends with your real agent and simulation counts, e.g.:
```
auth OK — access token acquired (len=185)
GET /v1/agents ...
agents returned: 42 -> ['Browser Agent', 'DMZ-client', ...]
```
---
## Use with Claude Code
Copy the example config and set the `command` path to your clone location, then
restart Claude Code (or run `/mcp`):
```bash
cp .mcp.json.example .mcp.json
# prints the exact absolute path to paste as "command" — no guessing:
echo "$(pwd)/.venv/bin/picus-mcp"
```
`.mcp.json` is gitignored (it holds a machine-specific absolute path); the tracked
`.mcp.json.example` is the template:
```json
{
"mcpServers": {
"picus": {
"command": "/absolute/path/to/picus-security-mcp/.venv/bin/picus-mcp",
"env": { "PICUS_ENABLE_WRITE_TOOLS": "false" }
}
}
}
```
No token in this file — the server reads it from `.env`. Once connected, ask
Claude to *"list my Picus simulations"* to confirm the end-to-end path.
## Use with Claude Desktop
Add to `claude_desktop_config.json` (macOS:
`~/Library/Application Support/Claude/claude_desktop_config.json`):
```json
{
"mcpServers": {
"picus": {
"command": "/absolute/path/to/picus-security-mcp/.venv/bin/picus-mcp",
"env": {
"PICUS_REFRESH_TOKEN": "paste-your-refresh-token-here"
}
}
}
}
```
Claude Desktop doesn't run from your project directory, so pass the token in the
`env` block (or export it in the launching environment).
## Use with any other MCP client
Run the server directly; it speaks MCP over stdio:
```bash
export PICUS_REFRESH_TOKEN="…" # or rely on .env
picus-mcp
```
---
## Response format
Every tool returns its result twice:
- a **text block** — markdown tables for list endpoints, an indented tree for
everything else, with epoch-millisecond timestamps decoded inline;
- **`structuredContent`** — the untouched API payload, exactly as Picus returned
it.
The text is what a model or a human reads, and it is a summary: long prose is
truncated and rows are reordered (dead agents last, and so on). Anything it
leaves out is still in `structuredContent`, so nothing is lost. On a typical
call the text runs ~65% smaller than the raw JSON dump it replaces.
Formatting lives in [`src/picus_mcp/format.py`](src/picus_mcp/format.py). The
high-volume list endpoints have hand-written formatters; everything else uses a
generic renderer that walks arbitrary JSON without dropping fields, so an
undocumented or newly-added API shape still renders readably. A formatter that
raises falls back to that generic renderer rather than failing the call.
---
## Enabling write tools
Write tools are **off by default** because they can start or delete real
simulations against your infrastructure. To enable them, set
`PICUS_ENABLE_WRITE_TOOLS=true` (in `.mcp.json`, the client `env` block, or your
shell). This adds:
| Tool | Endpoint |
| --- | --- |
| `picus_create_simulation` | `POST /v1/simulations` |
| `picus_update_simulation` | `PUT /v1/simulations/{id}` |
| `picus_delete_simulation` | `DELETE /v1/simulations/{id}` |
| `picus_cancel_simulation` | `PUT /v1/simulations/{id}/cancel` |
| `picus_simulate_now` | `POST /v1/simulations/{id}/simulate-now` |
| `picus_create_template` | `POST /v1/templates` |
---
## Tools reference (read-only)
| Tool | Endpoint |
| --- | --- |
| `picus_list_simulations` | `GET /v1/simulations` |
| `picus_get_simulation` | `GET /v1/simulations/{id}` |
| `picus_get_run_result` | `GET /v1/simulations/{id}/run/{runId}` |
| `picus_get_latest_run_result` | `GET /v1/simulations/{id}/run/latest` |
| `picus_get_run_threats` | `GET /v1/simulations/{id}/run/{runId}/threats` |
| `picus_get_action_alerts` | `GET …/run/latest/threats/{t}/actions/{a}/integrations/{i}/alerts` |
| `picus_get_run_frameworks` | `GET /v1/simulations/{id}/run/{runId}/frameworks` |
| `picus_get_latest_run_frameworks` | `GET /v1/simulations/{id}/run/latest/frameworks` |
| `picus_search_threats` | `GET /v1/threat-library/threats` |
| `picus_get_threat` | `GET /v1/threat-library/threats/{id}` |
| `picus_get_threat_action` | `GET /v1/threat-library/actions/{id}` |
| `picus_list_threat_actions` | `GET /v2/threat-library/actions` |
| `picus_list_threat_actors` | `GET /v1/threat-library/threat-actors` |
| `picus_list_templates` | `GET /v1/templates` |
| `picus_get_template` | `GET /v1/templates/{id}` |
| `picus_list_agents` | `GET /v1/agents` |
| `picus_get_agent` | `GET /v1/agents/{id}` |
| `picus_list_integrations` | `GET /v1/integrations` |
| `picus_list_integration_agents` | `GET /v1/integrations/agents` |
| `picus_list_mitigation_devices` | `GET /v2/mitigation/devices` |
| `picus_get_mitigation_device_stats` | `GET /v2/mitigation/devices/{id}` |
| `picus_get_device_signatures` | `GET /v1/mitigation/devices/{id}/signatures` |
| `picus_list_not_blocked_actions` | `GET /v1/mitigation/generic/not-blocked-actions` |
---
## Project layout
```
src/picus_mcp/
config.py # env-based configuration + .env auto-loading
client.py # async HTTP client: token exchange, caching, retry, v1/v2 routing
server.py # builds the FastMCP server and registers tool groups
tools/ # one module per Picus API group
scripts/
smoke_test.py # live end-to-end check
PICUS_API.md # auth flow + manual curl reference
```
---
## Troubleshooting
- **`configuration error: PICUS_REFRESH_TOKEN is not set`** — create `.env` from
`.env.example` and paste your token, or export the variable.
- **`Failed to obtain access token … refresh token may be expired`** — the
refresh token expired or was revoked; generate a new one in the dashboard.
- **Tools don't appear in Claude Code** — MCP servers load at startup; restart
Claude Code or run `/mcp`. Check the `command` path in `.mcp.json` is absolute
and points at your `.venv`.
- **Timestamps look wrong** — most Picus fields are epoch **milliseconds** (a few
threat-library fields are seconds). Values are returned raw; convert as needed.
---
## Notes on the Picus API
- List endpoints paginate with `limit`/`offset`. Most wrap results in a `pages`
object with `total_count`; `picus_list_not_blocked_actions` uses `pagination`
instead, and a few list endpoints return a bare array.
- The API mixes `/v1` and `/v2` paths, which is why the client is anchored at the
host origin and each tool carries its full versioned path.
See [PICUS_API.md](PICUS_API.md) for the auth flow and a manual curl example.
## License
MIT — see [LICENSE](LICENSE).
---
<sub>An independent, community-built integration. Not affiliated with, endorsed by, or sponsored by Picus Security. "Picus" and the Picus logo are trademarks of Picus Security, used here only to identify the API this project targets.</sub>
TDQS
A3.9/5.0
Scored across 23 tools
Disambiguation4/5
Most tools have distinct purposes, with minor overlap between 'latest' and specific run/framework variants. Descriptions clarify the difference, making confusion unlikely.
Naming Consistency5/5
All tools follow a consistent 'picus_verb_noun' pattern in snake_case, with verbs being 'list', 'get', or 'search'. No mixing or irregular naming.
Tool Count5/5
23 tools cover a broad domain (simulation, threats, agents, integrations, mitigation) without being excessive. Each tool serves a clear functional area.
Completeness3/5
The tool set provides comprehensive read access to all major entities (simulations, runs, threats, actions, etc.) but entirely lacks mutation (create/update/delete) capabilities, which limits its usefulness for active simulation management.
Maintenance
ActivityMaintained
ResponsivenessSyncing