secobserve-mcp 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,56 @@
1
+ name: Release
2
+
3
+ # Publishing runs on a tag so the version that ships is the version that was tagged.
4
+ # No API token is stored anywhere: PyPI uses trusted publishing and the MCP Registry
5
+ # uses GitHub OIDC, both authenticated by this workflow's identity.
6
+ on:
7
+ push:
8
+ tags: ["v*"]
9
+ workflow_dispatch:
10
+
11
+ jobs:
12
+ release:
13
+ runs-on: ubuntu-latest
14
+ environment: release
15
+ permissions:
16
+ id-token: write
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+
20
+ - uses: astral-sh/setup-uv@v5
21
+ with:
22
+ enable-cache: true
23
+
24
+ - name: Check
25
+ run: |
26
+ uv sync --extra dev
27
+ uv run ruff check .
28
+ uv run ruff format --check .
29
+ uv run mypy src
30
+ uv run pytest -q
31
+
32
+ - name: Verify the tag matches the packaged version
33
+ run: |
34
+ tagged="${GITHUB_REF_NAME#v}"
35
+ packaged=$(uv version --short)
36
+ test "$tagged" = "$packaged" || {
37
+ echo "tag $tagged does not match pyproject version $packaged" >&2
38
+ exit 1
39
+ }
40
+ test "$tagged" = "$(python3 -c 'import json;print(json.load(open("server.json"))["version"])')" || {
41
+ echo "tag $tagged does not match server.json version" >&2
42
+ exit 1
43
+ }
44
+
45
+ - name: Build
46
+ run: uv build
47
+
48
+ - name: Publish to PyPI
49
+ run: uv publish --trusted-publishing always
50
+
51
+ - name: Publish to the MCP Registry
52
+ run: |
53
+ curl -sSL "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/').tar.gz" \
54
+ | tar xz mcp-publisher
55
+ ./mcp-publisher login github-oidc
56
+ ./mcp-publisher publish
@@ -0,0 +1,12 @@
1
+ .venv/
2
+ dist/
3
+ build/
4
+ *.egg-info/
5
+ __pycache__/
6
+ *.py[cod]
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ .ruff_cache/
10
+ .env
11
+ .env.local
12
+ secobserve-exports/
@@ -0,0 +1,187 @@
1
+ # Agent base memory — `secobserve-mcp`
2
+
3
+ Read this before changing code in this repository. It records the current state and the invariants that must hold. It is not a backlog or a design document.
4
+
5
+ ## Purpose of the repository
6
+
7
+ `secobserve-mcp` is an MCP server that exposes the SecObserve REST API to LLM agents. Current scope:
8
+
9
+ - Generic CRUD over the whole resource catalogue (~50 resources), with filtering, sorting, pagination and projection.
10
+ - Discovery: a static catalogue (`list_resources`) and the instance's live schema (`describe_resource`).
11
+ - An escape hatch for the ~40 named non-CRUD actions (`call_action`).
12
+ - Validated workflows: assessment, bulk assessment, approval, metrics, file upload (observations / SBOM / VEX), API import, scan triggering, periodic tasks, instance status, VEX document generation.
13
+
14
+ Project priorities, in order:
15
+
16
+ 1. Never return a wrong answer silently.
17
+ 2. Spend the agent's context sparingly.
18
+ 3. Error messages must be enough for the agent to fix the call and retry.
19
+ 4. Be safe about writes and deletes.
20
+ 5. Keep the tool surface small and stable.
21
+
22
+ Out of scope today:
23
+
24
+ - Changing the SecObserve backend.
25
+ - Duplicating `secobserve-cli` (multi-file Excel/CSV export, migration scripts, GitOps config apply). This server only calls the backend's own export endpoints.
26
+ - A per-endpoint resource layer: adding one tool per endpoint works against the design, see [Tool surface invariants](#tool-surface-invariants).
27
+ - Caching business data. Only the OpenAPI schema is cached, and only within one process.
28
+
29
+ Do not expand into these without the user asking.
30
+
31
+ ## Technical state
32
+
33
+ - Package: Python 3.11+ (uses `X | None`, `from __future__ import annotations` in every module).
34
+ - Entry point: `secobserve-mcp = secobserve_mcp.__main__:main`.
35
+ - MCP: **SDK 2.x** (`mcp>=2.2,<3`). The class is `MCPServer` in `mcp.server.mcpserver`, **not** `FastMCP` — that name is the 1.x API and is gone.
36
+ - HTTP: one `httpx.AsyncClient` shared for the process lifetime.
37
+ - Validation: Pydantic v2, one input model per tool.
38
+ - Tests: `pytest` + `pytest-asyncio` (`asyncio_mode = "auto"`) with `respx` mocking HTTP.
39
+ - Lint/types: `ruff` (line-length 120) and `mypy --strict`.
40
+ - Current version: `0.1.0`.
41
+ - Baseline when this file was updated: 47 tests passing, ruff and mypy strict clean, stdio handshake and streamable HTTP both verified against a live instance.
42
+
43
+ ## Code structure
44
+
45
+ ```text
46
+ src/secobserve_mcp/
47
+ ├── app.py # MCPServer instance + INSTRUCTIONS shown to the agent
48
+ ├── config.py # env vars, lru_cache, auth header
49
+ ├── client.py # httpx client, error translation, tool_errors decorator
50
+ ├── registry.py # resource catalogue: path, ops, list_fields, actions
51
+ ├── schema.py # reads and slices the live OpenAPI schema, cached once per process
52
+ ├── formatting.py # projection, markdown/json rendering, pagination envelope
53
+ ├── exports.py # writes export files, reads upload files (with confinement)
54
+ ├── types.py # enums mirrored from the backend (Severity, Status, VEX, ...)
55
+ ├── tools_crud.py # 8 generic and discovery tools
56
+ ├── tools_workflows.py # 10 validated workflow tools
57
+ └── __main__.py # argparse, --check, transport selection
58
+
59
+ evals/seed.py # seeds the dataset evaluation.xml asks about, via the server's own tools
60
+ evaluation.xml # 10 read-only questions with verified answers
61
+ ```
62
+
63
+ Layering rule: `tools_*` never calls `httpx` directly; every request goes through `client.request`. `client` knows nothing about resources; all resource knowledge lives in `registry` or is read from `schema`.
64
+
65
+ ## API contract in use
66
+
67
+ - The client's base URL is `<SECOBSERVE_BASE_URL>/api`. Every `path` passed to `request()` is relative, **with** a leading and a trailing slash (`/observations/`).
68
+ - Auth: header `Authorization: APIToken <token>`, or `JWT <token>`. Credentials never go in a query string.
69
+ - Pagination: `?page=N&page_size=M`, response `{count, next, previous, results}`. The backend does not cap `page_size`; this server caps it at 100.
70
+ - `MultipleChoiceFilter` takes repeated parameters. `httpx` encodes a list value as repeated parameters, so `filters={"current_status": ["Open", "In review"]}` works.
71
+ - `search` only exists on endpoints with a `SearchFilter` (observations search their title).
72
+ - Assessment: `PATCH /observations/{id}/assessment/`, `comment` mandatory, refused while the previous assessment is still in `Needs approval`.
73
+ - Bulk endpoints take at most 250 ids per call: `bulk_assessment`, `bulk_approval`, `bulk_delete`.
74
+ - File import: multipart, a `file` part plus form fields; two variants, `_by_id` and `_by_name`.
75
+ - OSV / VulnerableCode scans: POST with no body, and they **block until the scan finishes**, returning counters.
76
+ - `periodic_tasks/run`: queues rather than running inline, returns 409 while the task is already running, requires superuser.
77
+ - Deleting a product or product group requires a `name` query parameter matching the record's exact name.
78
+ - OpenAPI schema: `GET /api/oa3/schema/?format=json`; paths inside the schema carry the `/api` prefix.
79
+
80
+ When the backend changes its contract, update `registry.py` and the matching tests in the same change.
81
+
82
+ ## Tool surface invariants
83
+
84
+ - The surface is **18 tools** and must stay small. A new endpoint means a new entry in `registry.py` (a resource or an `Action`), not a new tool.
85
+ - Only add a dedicated tool when an endpoint carries **a rule the agent cannot infer from the path**: required fields, a precondition, multipart bodies, or a side effect worth warning about. Otherwise `call_action` already covers it.
86
+ - Tool names are always prefixed `secobserve_`, snake_case, and start with a verb or an action noun.
87
+ - Every tool declares `name`, `title` and `annotations=ToolAnnotations(...)`. **Use the snake_case field names** (`read_only_hint`, `destructive_hint`, `idempotent_hint`, `open_world_hint`); the camelCase aliases work at runtime but fail `mypy --strict`.
88
+ - Every tool takes exactly one `params` argument, a Pydantic model, and returns `str`.
89
+ - The docstring is the tool description the agent sees. It must carry: a one-line summary, Args with types and constraints, Returns with the schema of the JSON returned, Examples including "Don't use when", and Error Handling.
90
+ - Every tool carries `@tool_errors` directly below `@mcp.tool(...)`.
91
+
92
+ ## Context invariants
93
+
94
+ - SecObserve serializers return **every** column. An Observation has 99 fields plus nested `product_data` / `parser_data`; one raw page of 25 rows is tens of thousands of tokens.
95
+ - Every resource that returns many rows must have `list_fields` in `registry.py`. Having no default projection is a bug, not a choice.
96
+ - `fields=["*"]` is the only way to opt out of projection, and a list result must say what it dropped.
97
+ - Long string values are truncated in markdown with a note giving the real length and pointing at `secobserve_get`. Never truncate in the JSON format.
98
+ - Field projection uses dotted paths (`product_data.name`). Check field names against a real response instead of guessing: `license_components` uses `product_name`, while `observations` and the `vex_*` resources use `product_data.name`.
99
+
100
+ ## Correctness invariants
101
+
102
+ This is the most important invariant in the repository.
103
+
104
+ - django-filter **silently ignores** query parameters it does not recognise. A misspelled or invented filter returns the **unfiltered** list, and the agent reports a wrong number with full confidence.
105
+ - So `secobserve_list` validates filter names against the live schema before sending the request, and the error must list the filters that do exist.
106
+ - When the schema cannot be read, filters **pass through** rather than being blocked: the API remains the authority, and this server must not turn into a roadblock when the schema is unavailable.
107
+ - Never hardcode a filter list into this repo. Every filter, field and enum comes from the running instance's `/api/oa3/schema/`.
108
+ - `vulnerability_id` is **not** a filter on `observations`. This actually happened; do not add it to the catalogue.
109
+
110
+ ## Error invariants
111
+
112
+ - An exception raised to the client is reported by MCP as `Error executing tool <name>`, discarding the message. Expected failures are therefore **returned as text**, through the `tool_errors` decorator.
113
+ - `tool_errors` catches only `SecObserveError`, `ConfigError`, `ValueError` and `KeyError`. Anything else still raises, because it is a bug.
114
+ - Every error message must say **what to do next**: the offending field, the valid values, or the tool to use instead.
115
+ - DRF 400 bodies are returned verbatim, because they already name the wrong field. Do not swallow or paraphrase them.
116
+ - A 404 must mention that SecObserve hides records outside the caller's products, not just say "not found".
117
+ - Never log the token, the `Authorization` header, or request bodies. `httpx` logs URLs at INFO to stderr, which is acceptable because auth lives in the header.
118
+
119
+ ## Write and delete safety invariants
120
+
121
+ - `secobserve_delete` is disabled unless `SECOBSERVE_ALLOW_DELETE` is set. Deletion in SecObserve cascades and cannot be undone.
122
+ - Deleting `products` / `product_groups` additionally requires `confirm_name` to match the exact name; the API itself verifies this.
123
+ - `SECOBSERVE_READ_ONLY` is enforced in `client.request`, before any request is made, so it does not depend on each tool remembering to check.
124
+ - Uploads may only read files under `SECOBSERVE_IMPORT_DIR` (resolve, then compare `parents`). The reason is prompt injection: scan reports are third-party data, and without confinement an instruction planted in a report could talk an agent into uploading an unrelated local file to SecObserve.
125
+ - Exports are written to `SECOBSERVE_EXPORT_DIR` under a basename sanitised to a single segment. A caller can never supply a path.
126
+ - Never change an observation's severity or status with `secobserve_update`; it must go through `assess` so the observation log and the approval workflow stay intact.
127
+ - A tool with side effects must set `destructive_hint` correctly and state the side effect in its docstring.
128
+
129
+ ## Working process for agents
130
+
131
+ ### Before changing anything
132
+
133
+ 1. Read the module you are touching and its tests.
134
+ 2. Check `git status`; the repository may carry the user's own changes — do not overwrite anything outside your scope.
135
+ 3. For API-related changes, check against the real SecObserve source (`Development/SecObserve/backend/application/...`) or `/api/oa3/schema/`, and state which version you checked against.
136
+ 4. For projection changes, verify field names against a real response before editing `registry.py`.
137
+
138
+ ### While changing
139
+
140
+ - Keep Python 3.11 compatibility and `mypy --strict` clean.
141
+ - Never create a new `httpx.AsyncClient` per request.
142
+ - No business logic in `client.py`, no HTTP in `tools_*`.
143
+ - No new dependency for something a few lines of code can do.
144
+ - Comment only what cannot be derived from the code; no comments restating the line below.
145
+ - Never call mutating endpoints on a real SecObserve instance from tests.
146
+ - A new tool ships with tests for its guards, not just the happy path.
147
+
148
+ ### Required verification
149
+
150
+ ```bash
151
+ uv pip install -e ".[dev]"
152
+ uv run pytest -q
153
+ uv run ruff check .
154
+ uv run ruff format --check .
155
+ uv run mypy src
156
+ uv run secobserve-mcp --help
157
+ ```
158
+
159
+ For changes touching tool registration, the schema, or error paths, verify at the protocol level too, not just by calling the functions — only the protocol level exposes a swallowed message:
160
+
161
+ ```bash
162
+ SECOBSERVE_BASE_URL=... SECOBSERVE_API_TOKEN=... uv run secobserve-mcp --check
163
+ ```
164
+
165
+ then open a `ClientSession` over `stdio_client` and run `list_tools()` plus at least one successful and one failing `call_tool()`.
166
+
167
+ For changes touching the write paths (create, import, assessment, tasks), run `evals/seed.py` against a disposable **empty** instance.
168
+
169
+ ### When handing over
170
+
171
+ - Name the main files changed.
172
+ - State how many tests passed and which verification commands you ran.
173
+ - State whether you called a real instance, and which one.
174
+ - If you changed an answer in `evaluation.xml`, say how you re-verified it.
175
+
176
+ ## Known limitations
177
+
178
+ - Input schemas nest arguments under `params`, following the single-Pydantic-model convention of the `mcp-builder` skill. Clients render flat arguments better, but changing it breaks the interface of every tool at once.
179
+ - `registry.py` is a hand-written list and can fall behind when the backend adds resources. Filters and fields cannot drift, since they are read from the live schema, but **a new resource will not appear** until it is added by hand.
180
+ - `secobserve_trigger_scan` and `secobserve_api_import` block until the backend finishes. A timeout does not cancel the work in flight; check `vulnerability_checks` rather than retrying blind.
181
+ - No automated integration test runs in CI: verification against a real backend is still manual, via `--check` and `evals/seed.py`.
182
+ - CI runs only on a `v*` tag, in `.github/workflows/release.yml`. There is no per-push test workflow yet, so a broken commit is only caught at release time.
183
+ - Publishing is tokenless: PyPI trusted publishing plus GitHub OIDC for the registry. Both are configured on the provider side, not in this repo, so a fresh fork cannot release without setting them up.
184
+ - Version lives in three places that must agree: `pyproject.toml`, `server.json`, and the git tag. The release workflow fails the build when they diverge.
185
+ - The OpenAPI schema is cached per process with no TTL. If the backend is upgraded while the server runs, restart the server.
186
+
187
+ Do not hide these limitations in tool descriptions or documentation when making related changes.
@@ -0,0 +1,3 @@
1
+ # CLAUDE.md
2
+
3
+ The guidance for this repository lives in [AGENTS.md](AGENTS.md) — one source of truth for every agent (Claude Code, Codex, and others). Read it before changing code.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 nh4ttruong
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,219 @@
1
+ Metadata-Version: 2.5
2
+ Name: secobserve-mcp
3
+ Version: 0.1.0
4
+ Summary: MCP server for SecObserve vulnerability management
5
+ Project-URL: Homepage, https://github.com/nh4ttruong/secobserve-mcp
6
+ Project-URL: Repository, https://github.com/nh4ttruong/secobserve-mcp
7
+ Project-URL: Issues, https://github.com/nh4ttruong/secobserve-mcp/issues
8
+ Author: nh4ttruong
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: mcp,model-context-protocol,sbom,secobserve,vex,vulnerability-management
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: System Administrators
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Security
20
+ Classifier: Topic :: Software Development :: Quality Assurance
21
+ Classifier: Typing :: Typed
22
+ Requires-Python: >=3.11
23
+ Requires-Dist: httpx>=0.27
24
+ Requires-Dist: mcp<3,>=2.2
25
+ Requires-Dist: pydantic>=2.7
26
+ Provides-Extra: dev
27
+ Requires-Dist: mypy>=1.10; extra == 'dev'
28
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
29
+ Requires-Dist: pytest>=8.0; extra == 'dev'
30
+ Requires-Dist: respx>=0.21; extra == 'dev'
31
+ Requires-Dist: ruff>=0.5; extra == 'dev'
32
+ Description-Content-Type: text/markdown
33
+
34
+ <!-- mcp-name: io.github.nh4ttruong/secobserve-mcp -->
35
+
36
+ <!-- prettier-ignore -->
37
+ <div align="center">
38
+
39
+ # secobserve-mcp
40
+
41
+ *MCP server for SecObserve — triage, import and administration from an agent*
42
+
43
+ [![Python](https://img.shields.io/badge/Python-%3E%3D3.11-3776ab?style=flat-square&logo=python&logoColor=white)](https://www.python.org)
44
+ [![MCP SDK](https://img.shields.io/badge/MCP%20SDK-2.x-6b4fbb?style=flat-square)](https://github.com/modelcontextprotocol/python-sdk)
45
+ [![Version](https://img.shields.io/badge/version-0.1.0-blue?style=flat-square)](pyproject.toml)
46
+ [![SecObserve](https://img.shields.io/badge/SecObserve-1.59.0-0b7285?style=flat-square)](https://github.com/MaibornWolff/SecObserve)
47
+
48
+ [Tools](#tools) • [Install](#install) • [Configure](#configure) • [Register with a client](#register-with-a-client) • [Design](#design) • [Evaluation](#evaluation)
49
+
50
+ </div>
51
+
52
+ `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.
53
+
54
+ ## Tools
55
+
56
+ 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.
57
+
58
+ | Tool | Purpose |
59
+ | --- | --- |
60
+ | `secobserve_list_resources` | The catalogue: every resource, its verbs, its actions. Makes no API call, so it is free to call first. |
61
+ | `secobserve_describe_resource` | Exact filters, fields and enums, read from the running instance's OpenAPI schema. |
62
+ | `secobserve_list` / `_get` | Read, with filters, sorting, pagination and projection. |
63
+ | `secobserve_create` / `_update` / `_delete` | CRUD over any resource in the catalogue. |
64
+ | `secobserve_call_action` | The long tail: `apply_rules`, `copy`, `simulate`, `license_overview`, exports. |
65
+ | `secobserve_assess_observation` | Triage one finding. Writes an observation log, honours the approval workflow. |
66
+ | `secobserve_bulk_assess_observations` | The same assessment across up to 250 findings. |
67
+ | `secobserve_approve_observation_log` | Approve or reject pending assessments (four-eyes). |
68
+ | `secobserve_product_metrics` | Pre-aggregated counts: current, timeline, and how stale they are. |
69
+ | `secobserve_upload_file` | Import a scan report, SBOM or VEX document from disk. |
70
+ | `secobserve_api_import` | Pull findings through a stored API configuration. |
71
+ | `secobserve_trigger_scan` | Run SecObserve's built-in OSV or VulnerableCode scan. |
72
+ | `secobserve_run_periodic_task` | Trigger a background job, or list the registered ones. |
73
+ | `secobserve_status` | Version, health, public settings, queue statistics, PURL types. |
74
+ | `secobserve_vex_document` | Generate or revise a CSAF / OpenVEX / CycloneDX document. |
75
+
76
+ ## Install
77
+
78
+ ```bash
79
+ uvx secobserve-mcp --help
80
+ ```
81
+
82
+ `uvx` downloads and runs it without installing anything permanently, which is what
83
+ the client configurations below use. To put it on your `PATH` instead:
84
+
85
+ ```bash
86
+ uv tool install secobserve-mcp
87
+ ```
88
+
89
+ From a checkout, for development:
90
+
91
+ ```bash
92
+ uv venv && uv pip install -e ".[dev]"
93
+ ```
94
+
95
+ ## Configure
96
+
97
+ | Variable | Default | Notes |
98
+ | --- | --- | --- |
99
+ | `SECOBSERVE_BASE_URL` | `http://localhost:8000` | Base URL **without** `/api`. |
100
+ | `SECOBSERVE_API_TOKEN` | — | User or product API token. Recommended. |
101
+ | `SECOBSERVE_JWT` | — | Alternative to an API token. |
102
+ | `SECOBSERVE_TIMEOUT` | `60` | Seconds. Raise it for imports and scans, which block. |
103
+ | `SECOBSERVE_VERIFY_SSL` | `true` | Set false only for a self-signed dev certificate. |
104
+ | `SECOBSERVE_READ_ONLY` | `false` | `true` refuses every non-GET call. |
105
+ | `SECOBSERVE_ALLOW_DELETE` | `false` | `secobserve_delete` is off until this is set. |
106
+ | `SECOBSERVE_IMPORT_DIR` | working directory | Uploads may only be read from this tree. |
107
+ | `SECOBSERVE_EXPORT_DIR` | `./secobserve-exports` | Exports and VEX documents are written here. |
108
+
109
+ Create a user API token:
110
+
111
+ ```bash
112
+ curl -X POST "$SECOBSERVE_BASE_URL/api/authentication/create_user_api_token/" \
113
+ -H "Content-Type: application/json" \
114
+ -d '{"username": "you", "password": "...", "name": "mcp"}'
115
+ ```
116
+
117
+ Check the wiring before handing it to a client:
118
+
119
+ ```bash
120
+ uvx secobserve-mcp --check
121
+ ```
122
+
123
+ It prints the instance version, the authenticated user, and whether read-only and delete are enabled.
124
+
125
+ ## Register with a client
126
+
127
+ ### Claude Code
128
+
129
+ ```bash
130
+ claude mcp add secobserve --env SECOBSERVE_BASE_URL=http://localhost:8000 --env SECOBSERVE_API_TOKEN=... -- uvx secobserve-mcp
131
+ ```
132
+
133
+ ### Codex CLI
134
+
135
+ In `~/.codex/config.toml`:
136
+
137
+ ```toml
138
+ [mcp_servers.secobserve]
139
+ command = "uvx"
140
+ args = ["secobserve-mcp"]
141
+ env = { SECOBSERVE_BASE_URL = "http://localhost:8000", SECOBSERVE_API_TOKEN = "..." }
142
+ ```
143
+
144
+ ### Other MCP clients
145
+
146
+ Most clients take the same JSON shape:
147
+
148
+ ```json
149
+ {
150
+ "mcpServers": {
151
+ "secobserve": {
152
+ "command": "uvx",
153
+ "args": ["secobserve-mcp"],
154
+ "env": {
155
+ "SECOBSERVE_BASE_URL": "http://localhost:8000",
156
+ "SECOBSERVE_API_TOKEN": "..."
157
+ }
158
+ }
159
+ }
160
+ }
161
+ ```
162
+
163
+ For a shared deployment, run streamable HTTP with stateless JSON:
164
+
165
+ ```bash
166
+ uvx secobserve-mcp --transport http --host 127.0.0.1 --port 8931
167
+ ```
168
+
169
+ ## Design
170
+
171
+ Three decisions worth knowing before reading the code:
172
+
173
+ **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.
174
+
175
+ **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.
176
+
177
+ **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.
178
+
179
+ 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 <name>`, which would throw away exactly the guidance the agent needs to retry correctly.
180
+
181
+ ## Security
182
+
183
+ - Credentials come from the environment and are never returned by a tool.
184
+ - `secobserve_delete` is disabled by default; deleting a product or product group additionally requires `confirm_name` to match the record's exact name, which the API itself verifies. Deletion cascades and is irreversible.
185
+ - Uploads are confined to `SECOBSERVE_IMPORT_DIR`; exports are written to `SECOBSERVE_EXPORT_DIR` under a sanitised single-segment filename.
186
+ - HTTP transport binds to `127.0.0.1` by default.
187
+ - **Observation titles, descriptions, component names and scanner output are third-party data**, supplied by scanners and by whoever wrote the scanned code. The server states this in its MCP instructions and in the relevant tool descriptions. Treat that content as data, never as instructions.
188
+
189
+ > [!WARNING]
190
+ > This server has full write access to SecObserve. Prefer a product API token over
191
+ > a superuser token when the agent only needs to work on one product, and set
192
+ > `SECOBSERVE_READ_ONLY=true` for read-only sessions.
193
+
194
+ ## Tests
195
+
196
+ ```bash
197
+ uv run pytest
198
+ ```
199
+
200
+ The suite mocks the SecObserve API with `respx`: it covers projection, pagination metadata, error translation, the read-only and delete guards, upload path confinement, unknown-filter rejection, schema slicing, and the assessment/approval payload rules.
201
+
202
+ `ruff check`, `ruff format --check` and `mypy --strict` are clean.
203
+
204
+ ## Evaluation
205
+
206
+ `evaluation.xml` holds ten read-only questions for measuring how well an agent uses this server. Each needs several tool calls — resolving a name to an id, filtering a list, and correlating two resources — and each has one string-comparable answer.
207
+
208
+ The answers are verified against the dataset `evals/seed.py` creates: three products, two of them in a product group, four branches, 21 findings from five scanners, an SBOM with a license-policy verdict, and three assessments. Seed an **empty** instance, since the answers are counts:
209
+
210
+ ```bash
211
+ SECOBSERVE_BASE_URL=... SECOBSERVE_API_TOKEN=... SECOBSERVE_IMPORT_DIR=/tmp/so-seed \
212
+ uv run python evals/seed.py
213
+ ```
214
+
215
+ The seed script drives the server's own tools, so a clean run is also an end-to-end check of the create, import, assessment and background-task paths against a real backend.
216
+
217
+ ---
218
+
219
+ Repository conventions and invariants for agents working on this code: [AGENTS.md](AGENTS.md).