Skip to main content
Glama
forticnapp-mcp

forticnapp-mcp

by svuillaume

๐Ÿ›ก๏ธ forticnapp-mcp โ€” FortiCNAPP MCP Server

Expose FortiCNAPP (formerly Lacework) API 2.0 as typed, auth-aware tools for any MCP client

Python MCP FortiCNAPP Docker Read-only

Overview โ€ข Install โ€ข Configure โ€ข stdio โ€ข HTTP โ€ข Architecture โ€ข Security โ€ข Tools โ€ข Quick Reference


๐Ÿ“‹ Overview

forticnapp-mcp is an MCP (Model Context Protocol) server for FortiCNAPP API 2.0. Tools are not hand-written โ€” they are generated at startup from the Lacework swagger spec (lw.yaml), so the tool surface tracks whichever spec you point it at.

The same tool registry ships behind two interchangeable transports. Pick the one that matches how you run the server.

#

Transport

Entry point

Process Model

Auth

Best For

1

stdio โ€” JSON-RPC over stdin/stdout

forticnapp-mcp

Spawned per client, dies with it

FortiCNAPP credentials from its own env

๐Ÿ’ป Claude Desktop, Claude Code (single client)

2

Streamable HTTP โ€” POST /mcp/

forticnapp-mcp-http

Long-running (bare metal or Docker)

FortiCNAPP credentials + bearer token gating the endpoint

โ˜๏ธ Shared server, multiple remote clients

TIP

Choosestdio to run locally on your own machine. Choose Streamable HTTP to share the server across users, deploy to the cloud, or require centralized authentication.

NOTE

Tenancy: each instance serves exactly one FortiCNAPP account in both modes. In HTTP mode, every caller holding the bearer token sees the same account โ€” there is no per-client credential isolation.

Both modes share every other module (config.py, openapi_loader.py, auth.py, http_client.py, tool_registry.py). http_server.py only adds a Starlette app, a bearer-token gate and a /healthz endpoint in front of the same mcp.server.lowlevel.Server that main.py runs over stdio.

๐Ÿค” Why This Exists

FortiCNAPP's API 2.0 spec makes naive OpenAPI-to-tool generation unreliable:

Spec quirk

How this server handles it

No OpenAPI securitySchemes declared

Hardcodes the real FortiCNAPP keyId + secret โ†’ bearer token handshake

No operationId on any of the 120+ operations

Derives deterministic tool names from method + path

Read-only "search" endpoints mixed in with mutations on same verbs

Classifies "is this mutating" from the path shape, not the HTTP verb

See CLAUDE.md for the full list of spec quirks the design accounts for.


Related MCP server: OpenAI-Compatible MCP Gateway

๐Ÿ“ฆ Installation

Requires Python 3.11+

pip install -e ".[dev]"

โš™๏ธ Configuration

Credentials come from the FortiCNAPP/Lacework console (Settings โ†’ API Keys), which issues a keyId and a secret โ€” both are required for the default auth mode.

Prompts for credentials, validates them against the real FortiCNAPP token endpoint, and writes .env, a portable .mcp.json and .gitignore.

forticnapp-mcp-setup

โœ๏ธ Option B โ€” Manual

cp .env.example .env
# then fill in FORTICNAPP_API_BASE_URL, FORTICNAPP_KEY_ID, FORTICNAPP_API_SECRET

๐Ÿ”‘ Key environment variables

Variable

Purpose

FORTICNAPP_API_BASE_URL

Tenant URL, e.g. https://YourAccount.lacework.net

FORTICNAPP_KEY_ID

API key ID

FORTICNAPP_API_SECRET

API secret

FORTICNAPP_OPENAPI_SPEC

Path to the spec (default ./lw.yaml)

FORTICNAPP_ENABLED_TAGS

Which OpenAPI tags become tools

ENABLE_MUTATION_TOOLS

Opt in to mutating tools โ€” off by default, only read-only tools are exposed

FORTICNAPP_MCP_HTTP_TOKEN

Bearer token protecting the HTTP endpoint (HTTP mode only)

FORTICNAPP_MCP_HTTP_HOST / FORTICNAPP_MCP_HTTP_PORT

HTTP bind address (default 0.0.0.0:8000)

See .env.example for every supported variable.


1๏ธโƒฃ stdio Mode (local)

The client (e.g. Claude Desktop) launches the server as a subprocess and talks JSON-RPC over its stdin/stdout.

forticnapp-mcp
# equivalently:
python -m forticnapp_mcp.main

The server validates configuration and loads the spec before it starts listening. On failure it exits with a clear one-line error on stderr โ€” it will not start with a broken configuration. This is the default mode.

๐Ÿ–ฅ๏ธ Claude Desktop

Add to claude_desktop_config.json:

{
  "mcpServers": {
    "forticnapp": {
      "command": "python",
      "args": ["-m", "forticnapp_mcp.main"],
      "cwd": "/absolute/path/to/mcp_forticnapp",
      "env": {
        "FORTICNAPP_API_BASE_URL": "https://YourAccount.lacework.net",
        "FORTICNAPP_KEY_ID": "YOUR_KEY_ID",
        "FORTICNAPP_API_SECRET": "YOUR_SECRET",
        "FORTICNAPP_OPENAPI_SPEC": "/absolute/path/to/mcp_forticnapp/lw.yaml"
      }
    }
  }
}
IMPORTANT

cwd must be the project root so the default FORTICNAPP_OPENAPI_SPEC=./lw.yaml resolves โ€” or set an absolute path as shown above.

WARNING

Prefer a real.env file over inlining secrets in this config wherever your setup allows it.


2๏ธโƒฃ HTTP Mode (shared/remote)

Use the forticnapp-mcp-http entry point for one server shared by multiple MCP clients. Every request to /mcp requires a bearer token (FORTICNAPP_MCP_HTTP_TOKEN) โ€” a separate secret from your FortiCNAPP credentials, protecting the HTTP endpoint itself.

Bare metal

cp .env.example .env
# fill in FORTICNAPP_API_BASE_URL / KEY_ID / API_SECRET as usual, plus:
# FORTICNAPP_MCP_HTTP_TOKEN=<a long random secret>

forticnapp-mcp-http
# equivalently:
python -m forticnapp_mcp.http_server

Docker

docker compose up --build

๐ŸŒ Endpoints

Endpoint

Method

Auth

Purpose

/mcp/

POST

Authorization: Bearer <token>

MCP Streamable HTTP (a bare /mcp without trailing slash 307-redirects)

/healthz

GET

None

Container HEALTHCHECK, load balancer and uptime checks

The server listens on 0.0.0.0:8000 by default.

Generic Streamable HTTP client config (shape varies by client)

{
  "mcpServers": {
    "forticnapp": {
      "type": "http",
      "url": "http://<host>:8000/mcp/",
      "headers": { "Authorization": "Bearer <FORTICNAPP_MCP_HTTP_TOKEN>" }
    }
  }
}

๐Ÿ—๏ธ Architecture

flowchart LR
    subgraph Clients["MCP Clients"]
        A["Claude Desktop<br/>Claude Code"]
        B["Remote MCP clients"]
    end

    subgraph Server["forticnapp-mcp"]
        M["main.py<br/>stdio"]
        H["http_server.py<br/>Streamable HTTP + bearer gate"]
        R["tool_registry.py<br/>OperationSpec โ†’ MCP Tools"]
        L["openapi_loader.py<br/>lw.yaml โ†’ OperationSpec"]
        C["http_client.py<br/>retries ยท 401 refresh ยท pagination"]
        U["auth.py<br/>keyId + secret โ†’ bearer token"]
    end

    API[("FortiCNAPP API 2.0")]

    A -->|stdio| M
    B -->|HTTPS + token| H
    M --> R
    H --> R
    L --> R
    R --> C
    C --> U
    C --> API

    style R fill:#DA291C,color:#fff,stroke:#DA291C
    style API fill:#24292f,color:#fff

๐Ÿงฉ Modules

Module

Responsibility

config.py

Env vars โ†’ validated Settings (pydantic-settings); fails fast

openapi_loader.py

lw.yaml/json โ†’ OperationSpec list; resolves $ref/allOf, builds a pydantic input model per operation, infers the token endpoint's field names

auth.py

ApiKeyAuthStrategy / BearerTokenStrategy / ApiKeyToTokenStrategy โ€” the last is FortiCNAPP's real keyId+secret โ†’ bearer token handshake

http_client.py

httpx.AsyncClient wrapper: builds requests from validated arguments, retries network/5xx errors, retries once on 401 after refreshing auth, follows cursor pagination

tool_registry.py

OperationSpec list โ†’ mcp.types.Tool list (resolving name collisions); dispatches call_tool requests to http_client

main.py

Wires the above into mcp.server.lowlevel.Server + stdio_server

http_server.py

Starlette app, bearer-token gate and /healthz in front of the same server

models.py

OperationSpec / OperationParameter (internal) and the ToolCallResult / RequestMeta / PaginationInfo models every tool returns

errors.py

ForticnappError hierarchy (auth / validation / api / network / spec), each carrying category, status_code, operation_id, retryable

logging_utils.py

Structured JSON logs to stderr with header/secret redaction

utils.py

Tool-name derivation, mutation/pagination classification, JSON Schema โ†’ Python type mapping

๐Ÿ“จ Tool response envelope

Every tool call returns the same structured JSON envelope:

{
  "success": true,
  "status_code": 200,
  "operation_id": "forticnapp_alerts_list",
  "request": {"method": "GET", "path": "/api/v2/Alerts", "query_keys": ["startTime"], "has_body": false},
  "data": { "...": "..." },
  "pagination": {"rows": 50, "total_rows": 400, "next_page_url": "https://...", "has_more": true},
  "error": null
}
TIP

Pagination: call the same tool again with page_url set to pagination.next_page_url from the previous response. Every other argument is ignored when page_url is set.

๐Ÿ”ง Customizing the token exchange

If your deployment's token endpoint differs from the documented contract (self-hosted, FedRAMP, a future API revision), there is exactly one place to change: ApiKeyToTokenStrategy._acquire_token in auth.py.

It builds the token request and parses the response using field names from a TokenOperationHint, inferred from the spec at startup (openapi_loader.discover_token_operation) with fallback defaults matching FortiCNAPP's current contract:

Direction

Fields

Request

keyId, expiryTime โ€” secret in X-LW-UAKS

Response

token, expiresAt

Token caching, proactive refresh and 401-triggered re-acquisition are wire-format-agnostic and live in the surrounding ApiKeyToTokenStrategy methods โ€” you shouldn't need to touch them.


๐Ÿ›ก๏ธ Security

Control

Behavior

๐Ÿง  In-memory secrets

Tokens and secrets are kept in memory only; nothing is persisted to disk

๐Ÿ™ˆ Log redaction

redact_headers() / redact_secret() wrap every header dict or secret reaching a log call โ€” Authorization, X-LW-UAKS and cookie headers are never logged in full

๐Ÿ”’ Read-only by default

Mutating operations (anything that isn't a GET or POST .../search) are excluded unless ENABLE_MUTATION_TOOLS=true

๐ŸŽซ Endpoint gate (HTTP)

Every /mcp request requires FORTICNAPP_MCP_HTTP_TOKEN

๐Ÿ‘ฅ Shared account (HTTP)

No per-client credential isolation โ€” all callers share the same FortiCNAPP account

CAUTION

Never commit API keys, secrets or the HTTP bearer token to Git. Keep .env out of source control:

.env
.mcp.json

๐Ÿงฐ MCP Tools

37 read-only tools across 9 categories.

Prefix: mcp__remote-devices__forticnapp__ โ€” each name below is prefixed with this. The server also appeared earlier under mcp__claude-device__lcl-forticnapp-.

Category

Tools

๐Ÿšจ Alerts

5

โ˜๏ธ Cloud accounts

4

๐Ÿ–ง Entities

14

๐Ÿ“… Events

1

๐Ÿ“‹ Inventory

2

๐Ÿ“œ Policies

4

๐Ÿ“Š Reports

1

๐Ÿž Vulnerabilities

3

๐Ÿ›‘ Vulnerability exceptions

3

Total

37

  • forticnapp_alerts_list

  • forticnapp_alerts_get

  • forticnapp_alerts_search

  • forticnapp_alerts_entities_get

  • forticnapp_alerts_entity_details_get

  • forticnapp_cloud_accounts_list

  • forticnapp_cloud_accounts_search

  • forticnapp_cloud_accounts_by_type_get

  • forticnapp_cloud_accounts_by_intg_guid_get

  • forticnapp_entities_machines_search

  • forticnapp_entities_machine_details_search

  • forticnapp_entities_containers_search

  • forticnapp_entities_images_search

  • forticnapp_entities_k8s_pods_search

  • forticnapp_entities_applications_search

  • forticnapp_entities_packages_search

  • forticnapp_entities_processes_search

  • forticnapp_entities_command_lines_search

  • forticnapp_entities_files_search

  • forticnapp_entities_new_file_hashes_search

  • forticnapp_entities_users_search

  • forticnapp_entities_network_interfaces_search

  • forticnapp_entities_internal_ip_addresses_search

  • forticnapp_events_search

  • forticnapp_inventory_search

  • forticnapp_inventory_scan_list

  • forticnapp_reports_list

  • forticnapp_policies_list

  • forticnapp_policies_get

  • forticnapp_policies_search

  • forticnapp_policies_tags_list

  • forticnapp_vulnerabilities_hosts_search

  • forticnapp_vulnerabilities_containers_search

  • forticnapp_vulnerabilities_containers_scan_get

  • forticnapp_vulnerability_exceptions_list

  • forticnapp_vulnerability_exceptions_get

  • forticnapp_vulnerability_exceptions_search


๐Ÿงช Development

ruff check src/
pytest

โšก Quick Reference

stdio (local)

pip install -e ".[dev]"
forticnapp-mcp-setup      # interactive credentials + .env + .mcp.json
forticnapp-mcp            # launched by the MCP client as a subprocess

HTTP (shared)

cp .env.example .env      # set FORTICNAPP_* and FORTICNAPP_MCP_HTTP_TOKEN
docker compose up --build
curl http://<host>:8000/healthz

Client call

POST http://<host>:8000/mcp/
Authorization: Bearer <FORTICNAPP_MCP_HTTP_TOKEN>

๐Ÿ”’ Keep secrets out of source control ยท Mutations are opt-in ยท Rotate API keys regularly

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelogโ€ฆ

    Related MCP Connectors

    Related MCP Servers

    • F
      license
      Not graded
      quality
      D
      maintenance
      A dual-transport MCP server that exposes your API as tools to LLM clients, supporting both stdio transport for local clients like Claude Desktop and HTTP/SSE transport for remote clients like OpenAI's Responses API.
      -
    • F
      license
      C
      quality
      D
      maintenance
      Schema-driven MCP server that exposes all Paperclip API operations as typed MCP tools over stdio or Streamable HTTP, supporting both authenticated and local_mode deployments.
      100
      -
    • A
      license
      Not graded
      quality
      C
      maintenance
      Enables AI tools to uniformly discover, inspect, and call tools, prompts, and resources from multiple upstream MCP servers through a small set of fixed MCP tools, over stdio or HTTP.
      2
      MIT