Metadata-Version: 2.5
Name: bigfix-root-mcp
Version: 1.0.0
Summary: A minimal read-only MCP server around the besapi library (BigFix REST API)
Project-URL: Homepage, https://github.com/jgstew/bigfix-root-mcp
Project-URL: Repository, https://github.com/jgstew/bigfix-root-mcp
Author-email: JGStew <admin@jgstew.com>
License-Expression: MIT
License-File: LICENSE
Keywords: besapi,bigfix,mcp,rest-api
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: System Administrators
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Requires-Python: >=3.11
Requires-Dist: besapi>=4.1.5
Requires-Dist: bigfix-relevance-analyzer<2,>=1.8.3
Requires-Dist: fastmcp==4.0.0b1
Requires-Dist: lxml
Requires-Dist: pydantic
Requires-Dist: requests
Provides-Extra: qna
Requires-Dist: bigfix-remote-client-relevance<2,>=1.1.2; extra == 'qna'
Description-Content-Type: text/markdown

# bigfix-root-mcp

A minimal, read-only MCP server around the [besapi](https://github.com/jgstew/besapi)
library, exposing the HCL BigFix root server REST API to MCP clients. Built on
[FastMCP](https://gofastmcp.com) 4 (stateless).

Focus: **session relevance** queries (data the server already has) and
**client fast query** (live questions answered by BigFix agents), plus a few
read-only helpers.

Strongly recommend using the official BigFix Platform MCP server instead: https://help.hcl-software.com/bigfix/11.0/platform/Platform/MCP/c_introduction.html

The capabilities of this MCP server are intentionally limited, where as the official one is not.

## Tools

| Tool | Purpose |
| --- | --- |
| `session_relevance_query` | Evaluate session relevance on the root server; returns the JSON envelope (`result`, `evaltime_ms`). |
| `analyze_relevance` | Statically analyze a relevance expression offline - parse, types, dialect, unknown inspectors with suggestions, complexity. Never evaluates it. |
| `search_inspectors` | Fuzzy-search the offline inspector reference by name or phrase. |
| `client_query_submit` | Submit a client fast query, return its `query_id` immediately. |
| `client_query_results` | Fetch current (cumulative) results for a query ID; safe to call repeatedly. |
| `client_query` | Submit + poll in one call with progress notifications; stops on expected count reached, results stable, or timeout. |
| `get_server_info` | Root server version info (`/api/serverinfo`). |
| `list_sites` | Sites visible to the configured operator. |
| `get_computer_group` | Look up a group by name - requires an explicit `site_path`. |
| `get_operator` | Look up a console operator by name. |
| `get_dashboard_variable` | Read a dashboard datastore variable. |
| `whoami` | Configured user/root server, main-operator status, write-gate state; connectivity smoke test. |
| `api_get` | Read-only escape hatch: GET any `/api/` path (try `help` for discovery). |
| `get_computer` | One computer's full record (`/api/computer/{id}`). |
| `find_computers` | Find computers by case-insensitive name substring. |
| `applicable_fixlets` | Content currently relevant to one computer. |
| `get_action` / `get_action_status` | An action's definition, and its per-computer execution state. |
| `list_actions` | Actions visible to the configured operator. |
| `find_content` | Search fixlets/tasks/analyses/baselines by name across sites; resolves the `site_path` needed by `get_content`. |
| `get_content` | One fixlet, task, analysis or baseline by site path and ID. |
| `list_operators` / `list_roles` | Console operators and roles (master operator only). |
| `validate_bes_xml` | Validate BES XML against the BigFix schemas. No server call. |

### Relevance analysis (advisory)

The relevance-taking tools statically analyze their relevance with
[bigfix-relevance-analyzer](https://github.com/jgstew/bigfix-relevance-analyzer)
before sending it (dialect pinned per tool). The analysis is **advisory
only**: even a query the analyzer rejects still goes to the server, because
the analyzer's inspector table is a snapshot - an unknown inspector there may
still exist on the live server. When there are findings, the response carries
an `analysis` key (findings, suggestions, dialect mismatch); a clean query
adds nothing. Server-side relevance errors gain a `Static analysis:` appendix
with the findings and did-you-mean suggestions, so a typo'd inspector is a
one-round-trip fix.

### qna fast evaluation (opt-in)

Absent unless the `[qna]` extra is installed - see [qna evaluation](#qna-evaluation).

| Tool | Purpose |
| --- | --- |
| `evaluate_client_relevance_qna` | Evaluate client relevance with the `qna` binary on containers or admin-inventoried hosts - about a second per warm run. Never touches the BigFix deployment. |
| `list_qna_targets` | List the admin-configured inventory targets and whether container images are allowed. |

### Write tools (opt-in)

Absent unless `BIGFIX_ALLOW_WRITES` is set - see [Writes](#writes).

| Tool | Purpose |
| --- | --- |
| `stop_action` | Stop an in-flight action (`POST /api/action/{id}/stop`). |
| `set_dashboard_variable` | Set a dashboard datastore variable. |
| `import_bes_content` | Create/update custom content in a site. Does **not** deploy it. |

### Result bounding

Every tool that can return an unbounded payload is windowed and says so.
List-shaped tools take `limit`/`offset` and report `returned`,
`total_available` and `truncated`; blob-shaped tools report `truncated` and
`total_chars`, and drop an oversized payload rather than cut it into something
that looks complete.

This is not optional politeness: BigFix relevance has **no row-limiting
operator** (`first`, `firsts`, `items`, `elements` are all undefined), so
bounding the response is the only way to bound a result. `find_content` on the
reference deployment matches 12,395 fixlets.

## Resources and prompts

Relevance is the hard part, so the server ships reference material clients can
pull on demand rather than repeating it in every tool description:

| Resource | Contents |
| --- | --- |
| `bigfix://relevance/session-cookbook` | Session relevance that works - every expression verified against a live root server - plus the operators that don't exist. |
| `bigfix://relevance/client-cookbook` | Client (fast query) relevance, targeting forms, reading cumulative results. |
| `bigfix://guide/tools` | Which tool answers which question, how to read bounded responses, what operator scope means. |
| `bigfix://relevance/reference/dialects` | Client versus session relevance: where each is evaluated, which one a tool expects. |
| `bigfix://relevance/reference/client-relevance` | Client relevance language reference (from bigfix-relevance-analyzer). |
| `bigfix://relevance/reference/session-relevance` | Session relevance language reference (from bigfix-relevance-analyzer). |

Prompts: `diagnose_computer`, `patch_status`, `find_stale_agents`,
`troubleshoot_relevance`.

Relevance errors also carry a cause hint: the server recognizes the common
failure shapes (a non-existent limiting operator, client relevance in a
session query, singular-vs-plural) and appends what to do instead, so a bad
expression is a retry rather than a dead end.

### Client fast query semantics

Client queries are answered by live agents: results accumulate at
`/api/clientqueryresults/{id}` over seconds to minutes as clients report in,
and there is **no completion flag**. The `client_query` tool polls with three
termination heuristics (reported in `stop_reason`):

1. `expected_count_reached` - as many distinct computers reported as targeted;
2. `results_stable` - no new computers for `stable_polls` consecutive polls;
3. `timeout` - partial results at timeout are a *normal* outcome (offline
   agents never report), not an error.

For long waits, use `client_query_submit` then `client_query_results`
repeatedly instead of a single blocking call.

## Configuration

Environment variables win over config files:

| Setting | Env var / `[besapi]` config key | Default |
| --- | --- | --- |
| Root server URL | `BES_ROOT_SERVER` (e.g. `https://bes.example.com:52311`) | - |
| REST operator | `BES_USER_NAME` | - |
| Password | `BES_PASSWORD` | - |
| Write tools | `BIGFIX_ALLOW_WRITES`: `true` to register them | off |
| TLS verification | `BES_SSL_VERIFY`: `false`, `true`, or a CA bundle path | `false` (besapi default) |
| qna inventory | `BIGFIX_QNA_INVENTORY`: path to a hosts.toml (see [qna evaluation](#qna-evaluation)) | unset |
| qna containers | `BIGFIX_QNA_CONTAINERS`: `0` to forbid container-image targets | on when `[qna]` installed |
| Transport | `BIGFIX_MCP_TRANSPORT`: `stdio` or `http` (streamable HTTP on 127.0.0.1:8000/mcp, banner on). No auth layer of its own - put authentication in front before binding beyond localhost. | `stdio` |

Config files are searched in besapi's order: `/etc/besapi.conf`,
`~/besapi.conf`, `~/.besapi.conf`, `./besapi.conf` - same
`[besapi]` section format as besapi/bescli, so an existing config just works.
Prefer keeping credentials in `~/besapi.conf` over MCP client config files.

Example MCP client config (see [.mcp.json](.mcp.json)):

```json
{
  "mcpServers": {
    "bigfix-root": {
      "command": "uvx",
      "args": ["bigfix-root-mcp"]
    }
  }
}
```

## Install / run

Published on [PyPI](https://pypi.org/project/bigfix-root-mcp/). Requires Python 3.11+.

### With uv (preferred)

No install step - `uvx` fetches and runs it in a throwaway environment, which is
also what the MCP client configuration above uses:

```bash
uvx bigfix-root-mcp
```

To keep it on `PATH` instead, as a managed tool:

```bash
uv tool install bigfix-root-mcp
bigfix-root-mcp
```

### With pip

```bash
pip install bigfix-root-mcp
bigfix-root-mcp               # or: python -m bigfix_root_mcp
```

### From a checkout

[uv](https://docs.astral.sh/uv/) 0.12+ required:

```bash
uv sync
uv run bigfix-root-mcp        # or: uv run python -m bigfix_root_mcp
```

### Verifying a download

Every [release](https://github.com/jgstew/bigfix-root-mcp/releases) since v0.1.3
also attaches the wheel and sdist with a `SHA256SUMS.txt`, checkable with
`sha256sum -c SHA256SUMS.txt`.

Smoke test against a live root server with MCP Inspector:

```bash
npx @modelcontextprotocol/inspector bigfix-root-mcp
```

then call `whoami`, `session_relevance_query` with `number of bes computers`,
and `client_query` targeting a known computer ID.

### Operator scope

Every result is limited to what the configured REST operator can see. Only a
**master operator** has full visibility; a regular operator can never be
certain its view is complete, and cannot distinguish "does not exist" from
"outside my scope". So `number of bes computers` returning 35 means *35
computers visible to this operator* - a lower bound, not the BigFix total.

`whoami` reports `is_main_operator` for exactly this reason: check it before
treating any result as the full state of BigFix. The tool descriptions carry
this caveat so LLM clients don't overstate scoped results.

## Safety and design notes

- **Read-only by default**: with `BIGFIX_ALLOW_WRITES` unset, only read tools
  are registered - the write tools do not exist as far as any client can tell.
  One nuance: submitting a client query does create a query object
  server-side, but agents only *evaluate* relevance against it - no
  managed-endpoint state changes.
- **Client fast query is a powerful read.** `client_query` with `target_all`
  evaluates arbitrary client relevance on every agent the operator can see,
  and the BigFix agent runs as SYSTEM/root. That can read file contents,
  registry values and process lists fleet-wide, and the results come back in
  the tool response. Scope the configured operator to the smallest useful set
  of computers; `whoami.is_main_operator` tells you which you have.
- **Explicit site paths**: this server never uses besapi's mutable
  "current site path" connection state (`set_current_site_path` /
  `get_current_site_path` - a bescli convenience); tools that need a site take
  a required `site_path` parameter.
- **Stdout hygiene**: stdout belongs to the MCP stdio transport; all logging
  goes to stderr, and config loading avoids besapi helpers that print.
- **TLS**: verification is off by default to match besapi; set
  `BES_SSL_VERIFY=true` (or a CA bundle path) for anything beyond a lab.
- Generic BigFix logic here is written to be upstreamed into besapi - see
  [docs/besapi-proposals.md](docs/besapi-proposals.md).

## qna evaluation

`pip install "bigfix-root-mcp[qna]"` adds
[bigfix-remote-client-relevance](https://github.com/jgstew/bigfix-remote-client-relevance)
and registers `evaluate_client_relevance_qna` / `list_qna_targets` (with the
extra absent, the tools do not exist). It evaluates **client** relevance with
the BigFix `qna` binary on lab targets in about a second per warm run - the
fast half of the feedback loop next to the advisory analysis.

**What it is not:** qna runs on the target machine, *not* through the BigFix
deployment. No operator scope, no site subscriptions, no client settings -
inspectors that depend on deployment state will differ from `client_query`.
Iterate here, confirm on real agents with `client_query`. (The package's
Fast Query transport is an upstream stub; this server does not expose it.)

Targets are deliberately constrained:

- **Container images** (`ubuntu:22.04`, ...) may be passed by the caller;
  needs docker or podman on the server host. First use of an image is slow
  (agent artifact download + derived image build); later runs are ~1s. Set
  `BIGFIX_QNA_CONTAINERS=0` to forbid these.
- **Inventory hosts** come only from an admin-configured hosts.toml named by
  `BIGFIX_QNA_INVENTORY` (the package's inventory format: ssh/local/container
  entries). The MCP client can pick entries by name but can never supply ssh
  hosts, users, or `become` flags of its own.

Security notes: qna executes on the targets; the controller downloads BigFix
agent artifacts from support.bigfix.com (SHA256-verified against the release
site's sums) on first use of a version, so the server host makes outbound
requests; every run leaves a `BIGFIX QNA` audit line on stderr.

## Writes

Set `BIGFIX_ALLOW_WRITES=true` to register the three write tools. The flag
controls *registration*, so with it off there is nothing to call.

Two guardrails apply to all of them:

- **`dry_run` defaults to true.** The response describes the call that would
  be made and nothing is sent. A write only happens on an explicit
  `dry_run=false`.
- **Every attempt is audit-logged** to stderr as one `BIGFIX WRITE` line with
  the operator, target, dry-run flag and outcome.

The set is limited on purpose to operations whose blast radius is reversible
or nil. `import_bes_content` *creates* content; it does not run it - a fixlet
imported this way does nothing until somebody deploys an action against it in
the console.

**Not implemented, and not to be added without their own design round:**
deploying actions (`POST /api/actions`), any `DELETE`, creating sites or
operators, and file upload. Deploying an action is arbitrary code execution as
root across the fleet, which is a different category of risk from anything
here.

## Documentation

| Doc | Contents |
| --- | --- |
| [client-query.md](docs/client-query.md) | Client fast query protocol reference: endpoints, payloads, live-captured result schema, termination heuristics and their tradeoffs. |
| [besapi-notes.md](docs/besapi-notes.md) | besapi behaviors this wrapper depends on or works around (error surfacing, connection lifecycle, return shapes, site-path state). |
| [design-decisions.md](docs/design-decisions.md) | Why the server is shaped this way, plus FastMCP 4 beta specifics. |
| [besapi-proposals.md](docs/besapi-proposals.md) | Proposed upstream besapi changes that would let this project shrink. |
| [rest-endpoints.md](docs/rest-endpoints.md) | Live-verified REST paths, site-path rules, and relevance findings (including the operators that don't exist). |
| [security-review.md](docs/security-review.md) | Threat model and findings for the tool surface. |

## Development

Requires Python 3.11+ and [uv](https://docs.astral.sh/uv/) 0.12+. `uv sync`
creates `.venv` from the committed `uv.lock` and installs the `dev` dependency
group; no manual venv or `pip install -e` step.

```bash
uv sync
uv run pytest
```

Linting, formatting, type checking and the packaging checks all run through
pre-commit (ruff, mypy, bandit, slyp, zizmor, and the uv lock/build checks).
Install both hook types - some checks are deliberately deferred to `pre-push`
because they are too slow to want on every commit:

```bash
uv run pre-commit install --hook-type pre-commit --hook-type pre-push
```

Useful invocations:

```bash
uv run pre-commit run --all-files                    # the commit-stage hooks
uv run pre-commit run --all-files --hook-stage manual  # everything, as CI runs it
```

Tests run entirely offline against a scripted fake `BESConnection`, including
in-memory end-to-end MCP calls via `fastmcp.Client`.
