MCP Server

Ask BOMHort questions from an AI agent — the read-only Model Context Protocol tool surface.

The MCP server ships with BOMHort 0.8.0 and is read-only. It can answer questions about projects, packages, vulnerabilities and SBOMs. It cannot upload an SBOM, write a VEX statement or change a policy, and it never calls a language model itself.

What it is

mcp-server is the fifth BOMHort binary. It speaks the Model Context Protocol so an MCP-capable client — Claude Desktop, an IDE agent, a custom tool runner — can query a BOMHort instance without anyone writing a REST integration first.

It is a consumer of the REST API, not a second reader of the database. It holds no ClickHouse credentials. Everything it can tell an agent is something GET /api/v1/... already returns, which is the point: the MCP server is the first external consumer of the API contract that 1.0 freezes, and a consumer that reached around the contract would not exercise it.

The five tools

ToolAnswersBacked by
list_projects“What does this instance know about?”GET /api/v1/projects
get_project“What is the state of payment-service?”GET /api/v1/projects/{name} (+ /sboms)
search_packages“Who ships log4j-core, and in which versions?”GET /api/v1/packages/search
list_vulnerabilities“What is open, instance-wide or per project?”GET /api/v1/vulnerabilities, GET /api/v1/projects/{name}/vulnerabilities
get_sbom“What is in this document?”GET /api/v1/sboms/{id}/detail (+ /vulnerabilities)

All five are annotated readOnlyHint, idempotentHint and closed-world, so a client can call them without asking the user to confirm each one.

Two semantics are stated in the server’s instructions because a model cannot infer them from the schemas and otherwise gets them wrong:

  • Project counts are de-duplicated across versions. A package present in ten versions of a project counts once, and so does a finding. Summing per-version numbers to get a project total produces a wrong, larger number.
  • Suppressed findings are returned, not hidden. A finding that VEX marks not_affected appears in the list with its vex_status set. It is documented as not exploitable — not ignored, and not absent.

Paging and filters

list_projects, search_packages and the instance-wide list_vulnerabilities page server-side (page, page_size, default 25, max 500). Scoped to a project, list_vulnerabilities pages in the MCP server, because the project endpoint answers with the project’s distinct findings in one response — de-duplication across versions cannot be done a page at a time.

severity therefore only works together with project. Asking for severity: CRITICAL on the instance-wide listing returns an error rather than a number that only describes one page.

Running it on a laptop (stdio)

stdio is the default and needs no port, no token and no origin list, because nothing is listening. The client starts the binary and talks to it over the pipe.

// Claude Desktop: claude_desktop_config.json
{
  "mcpServers": {
    "bomhort": {
      "command": "/usr/local/bin/mcp-server",
      "env": {
        "MCP_API_BASE_URL": "https://bomhort.example.com",
        "MCP_API_KEY": "your-api-key"        // only if the API has AUTH_ENABLED
      }
    }
  }
}

With the container image instead of a local binary:

{
  "mcpServers": {
    "bomhort": {
      "command": "docker",
      "args": [
        "run", "--rm", "-i",
        "-e", "MCP_API_BASE_URL=https://bomhort.example.com",
        "ghcr.io/seebom-labs/bomhort/mcp-server:0.8.0"
      ]
    }
  }
}

From a checkout: make mcp.

MCP_API_BASE_URL accepts the URL as copied from a browser — a trailing /api/v1 is stripped rather than doubled.

Running it in the cluster (Streamable HTTP)

Only do this if agents outside your machine need to reach the instance. The HTTP transport is a remote tool-execution endpoint, and the chart treats it accordingly:

mcp:
  enabled: true
  transport: http
  httpToken: ""                                # or existingSecret
  allowedOrigins:
    - https://agent.example.com

helm template fails — not warns — if httpToken is missing, if allowedOrigins is empty, or if it contains *. The binary makes the same checks at startup. There is no flag to turn this off:

  • An unauthenticated MCP endpoint is remote tool execution for anyone who can reach the port.
  • * re-opens the cross-site hole that CVE-2026-33252 described — a Streamable HTTP transport accepting cross-site POSTs without validating Origin. A browser page a victim happens to have open must not be able to drive the server.

A CLI client sends no Origin header at all and is unaffected by the list; the list exists to stop browsers. Credentials are accepted as Authorization: Bearer <token> or X-API-Key, compared in constant time.

When apiGateway.auth.enabled is set, the MCP server automatically reads the release’s SERVICE_TOKEN from the same Secret — one token, one place.

Configuration reference

Environment variableHelm valueDefaultMeaning
MCP_TRANSPORTmcp.transportstdiostdio or http
MCP_HTTP_ADDRmcp.port127.0.0.1:8081Listen address (http only)
MCP_HTTP_TOKENmcp.httpToken—Inbound bearer token, required for http
MCP_ALLOWED_ORIGINSmcp.allowedOrigins—Explicit Origin allow-list, required for http
MCP_API_BASE_URLmcp.apiBaseURLhttp://localhost:8080Where the API gateway is
MCP_SERVICE_TOKENmcp.serviceTokenSERVICE_TOKENCredential sent to the API
MCP_API_KEYmcp.apiKeyfirst API_KEYS entryCredential sent to the API
MCP_REQUEST_TIMEOUT_SECONDSmcp.requestTimeoutSeconds30Per-upstream-request timeout

Not in scope

  • Write tools. No upload, no VEX authoring, no policy changes. BOMHort’s frontend is public and its policies are config files; there is no mutation this server could legitimately offer. Write tools are additive and can be added after 1.0 without breaking anything.
  • LLM calls. BOMHort never calls a model. The MCP server is a transport; automated reasoning over findings lives in the VEXViper sidecar.
  • A second data path. If a question cannot be answered by the REST API, it cannot be answered by a tool here either. That is a feature request against the API, and the right place to make one.