
# secobserve-mcp
*MCP server for SecObserve — triage, import and administration from an agent*
[](https://pypi.org/project/secobserve-mcp/)
[](https://pypi.org/project/secobserve-mcp/)
[](https://github.com/nh4ttruong/secobserve-mcp/actions/workflows/release.yml)
[](https://github.com/MaibornWolff/SecObserve)
[](LICENSE)
[Tools](#tools) • [Install](#install) • [Configure](#configure) • [Register with a client](#register-with-a-client) • [Design](#design) • [Evaluation](#evaluation)
`secobserve-mcp` exposes the [SecObserve](https://github.com/MaibornWolff/SecObserve) REST API to an LLM agent over the [Model Context Protocol](https://modelcontextprotocol.io): browse and triage observations, manage products, branches and rules, import scan reports and SBOMs, run scans and background jobs, generate VEX documents. Transport is stdio by default. Listed in the [official MCP Registry](https://registry.modelcontextprotocol.io/?q=secobserve-mcp) as `io.github.nh4ttruong/secobserve-mcp`.
## Tools
18 tools, not one per endpoint. SecObserve has ~50 REST resources and ~40 named actions; registering a tool for each would cost more context than the data ever returns, so the API is modelled as **data** and the tools are the interface to it.
| Tool | Purpose |
| --- | --- |
| `secobserve_list_resources` | The catalogue: every resource, its verbs, its actions. Makes no API call, so it is free to call first. |
| `secobserve_describe_resource` | Exact filters, fields and enums, read from the running instance's OpenAPI schema. |
| `secobserve_list` / `_get` | Read, with filters, sorting, pagination and projection. |
| `secobserve_create` / `_update` / `_delete` | CRUD over any resource in the catalogue. |
| `secobserve_call_action` | The long tail: `apply_rules`, `copy`, `simulate`, `license_overview`, exports. |
| `secobserve_assess_observation` | Triage one finding. Writes an observation log, honours the approval workflow. |
| `secobserve_bulk_assess_observations` | The same assessment across up to 250 findings. |
| `secobserve_approve_observation_log` | Approve or reject pending assessments (four-eyes). |
| `secobserve_product_metrics` | Pre-aggregated counts: current, timeline, and how stale they are. |
| `secobserve_upload_file` | Import a scan report, SBOM or VEX document from disk. |
| `secobserve_api_import` | Pull findings through a stored API configuration. |
| `secobserve_trigger_scan` | Run SecObserve's built-in OSV or VulnerableCode scan. |
| `secobserve_run_periodic_task` | Trigger a background job, or list the registered ones. |
| `secobserve_status` | Version, health, public settings, queue statistics, PURL types. |
| `secobserve_vex_document` | Generate or revise a CSAF / OpenVEX / CycloneDX document. |
## Install
```bash
uvx secobserve-mcp --help
```
`uvx` downloads and runs it without installing anything permanently, which is what
the client configurations below use. To put it on your `PATH` instead:
```bash
uv tool install secobserve-mcp
```
From a checkout, for development:
```bash
uv venv && uv pip install -e ".[dev]"
```
## Configure
| Variable | Default | Notes |
| --- | --- | --- |
| `SECOBSERVE_BASE_URL` | `http://localhost:8000` | Base URL **without** `/api`. |
| `SECOBSERVE_API_TOKEN` | — | User or product API token. Recommended. |
| `SECOBSERVE_JWT` | — | Alternative to an API token. |
| `SECOBSERVE_TIMEOUT` | `60` | Seconds. Raise it for imports and scans, which block. |
| `SECOBSERVE_VERIFY_SSL` | `true` | Set false only for a self-signed dev certificate. |
| `SECOBSERVE_READ_ONLY` | `false` | `true` refuses every non-GET call. |
| `SECOBSERVE_ALLOW_DELETE` | `false` | `secobserve_delete` is off until this is set. |
| `SECOBSERVE_IMPORT_DIR` | working directory | Uploads may only be read from this tree. |
| `SECOBSERVE_EXPORT_DIR` | `./secobserve-exports` | Exports and VEX documents are written here. |
Create a user API token:
```bash
curl -X POST "$SECOBSERVE_BASE_URL/api/authentication/create_user_api_token/" \
-H "Content-Type: application/json" \
-d '{"username": "you", "password": "...", "name": "mcp"}'
```
Check the wiring before handing it to a client:
```bash
uvx secobserve-mcp --check
```
It prints the instance version, the authenticated user, and whether read-only and delete are enabled.
## Run as a container
```bash
docker run --rm -p 8931:8931 \
-e SECOBSERVE_BASE_URL=https://secobserve.example.com \
-e SECOBSERVE_API_TOKEN=... \
ghcr.io/nh4ttruong/secobserve-mcp
```
The image is built for `linux/amd64` and `linux/arm64`, defaults to `--transport http --host 0.0.0.0`, runs as a non-root user, and answers `GET /healthz` with its version.
`/healthz` is liveness only and never calls SecObserve, so a backend outage does not get this server restarted.
One token is baked into the environment, so every caller of a container shares one SecObserve identity.
Keep it behind a gateway that does the authenticating, and off any public port.
For a single user, `uvx` over stdio is the better fit.
## Register with a client
The server prints its own registration snippet, so none of the blocks below have
to be copied by hand:
```bash
uvx secobserve-mcp --print-config claude # or: codex, vscode, json
```
It reads `SECOBSERVE_BASE_URL` from the environment and always leaves the token
as a placeholder — the snippet is meant to be pasted somewhere, and a token
should not travel with it. The hint goes to stderr, so `--print-config json >
mcp.json` writes a clean file.
### Claude Code
```bash
claude mcp add secobserve --env SECOBSERVE_BASE_URL=http://localhost:8000 --env SECOBSERVE_API_TOKEN=... -- uvx secobserve-mcp
```
### Codex CLI
In `~/.codex/config.toml`:
```toml
[mcp_servers.secobserve]
command = "uvx"
args = ["secobserve-mcp"]
env = { SECOBSERVE_BASE_URL = "http://localhost:8000", SECOBSERVE_API_TOKEN = "..." }
```
### Other MCP clients
Most clients take the same JSON shape:
```json
{
"mcpServers": {
"secobserve": {
"command": "uvx",
"args": ["secobserve-mcp"],
"env": {
"SECOBSERVE_BASE_URL": "http://localhost:8000",
"SECOBSERVE_API_TOKEN": "..."
}
}
}
}
```
For a shared deployment, run streamable HTTP with stateless JSON:
```bash
uvx secobserve-mcp --transport http --host 127.0.0.1 --port 8931
```
## Design
Three decisions worth knowing before reading the code:
**Responses are projected.** SecObserve's serializers return every model column; an Observation has around 100. Each resource carries a default field set, and every list result says what it dropped. Pass `fields=["*"]` to opt out.
**Resource help comes from the instance, not from this repo.** `secobserve_describe_resource` reads `/api/oa3/schema/` on the running backend, so filter names and enums cannot drift from the deployed version. The same schema is used to reject unknown filter names before a request is sent: django-filter **silently ignores** parameters it does not recognise, so `filters={"vulnerability_id": "CVE-2021-44228"}` would otherwise return the *entire* unfiltered list and the agent would report a confident wrong answer. It now fails with the list of filters that do exist.
**Validation lives in the schema wherever a rule exists.** An assessment with no comment, a rejection with no remark, a bulk call over 250 ids, or a create with neither `product_id` nor `product_name` is refused by the input model before any HTTP request. Everything else is passed through, and the API's 400 body — which names the offending field — is returned verbatim.
Expected failures come back as tool *text*, not as a raised exception: MCP reports a raised exception to the client as a bare `Error executing tool