Skip to main content
Glama
BeardedInfoSec

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: MIT](https://img.shields.io/badge/License-MIT-green.svg?style=flat-square)](LICENSE)
[![Python 3.10+](https://img.shields.io/badge/Python-3.10%2B-3776AB.svg?style=flat-square&logo=python&logoColor=white)](pyproject.toml)
[![MCP](https://img.shields.io/badge/Model_Context_Protocol-server-8A2BE2.svg?style=flat-square)](https://modelcontextprotocol.io)
[![Picus API](https://img.shields.io/badge/Picus_API-v1_%2B_v2-005EB8.svg?style=flat-square)](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