threat-model-mcp

threat-model-mcp

Serves any STRIDE threat model to MCP-capable LLMs, enabling natural language querying of threat registers and export to the Microsoft Threat Modeling Tool.

Category
Visit Server

README

threat-model-mcp

Serve any STRIDE threat model to any MCP-capable LLM — Claude Code, Gemini CLI, a local Ollama / LM Studio client, or a custom agent — and export it to the Microsoft Threat Modeling Tool.

Point the server at your threat register. Then ask an agent "what are the critical threats?", "how do I mitigate the auth bypass?", or "which threats touch the database?" and it answers from your register instead of guessing.

pip install -e .
threat-model-mcp --data ./my-threat-model.json

The server ships with no threat data of its own. Your model is yours; nothing is bundled, inferred, or phoned home. A worked example is included so you have something to point at on day one.


Why this exists

Threat models live in spreadsheets that nobody reads. An LLM that can query the register — rather than being handed a 40-page PDF and asked to remember it — turns the model into something you consult during code review, incident response, or design.

And because the register is structured, the same data can be exported straight into the Microsoft Threat Modeling Tool, so it stays usable by people who don't work in a chat window.


Install

Requires Python >= 3.10. The only runtime dependency is the mcp SDK.

git clone <this-repo> threat-model-mcp
cd threat-model-mcp
pip install -e .

Verify:

python -m pytest                                              # the full test suite
threat-model-mcp --data threat_model_mcp/examples/example-model.json --check

--check validates the dataset, prints a summary, and exits without serving. Use it in CI to catch a malformed register before an agent ever sees it.


Point it at your data

The dataset is required — there is no implicit default, because silently serving somebody else's threat model would be worse than a startup failure.

threat-model-mcp --data ./my-model.json      # explicit flag
THREAT_MODEL_PATH=./my-model.csv threat-model-mcp   # or the environment

Both .json and .csv work. A CSV register — the natural output of a spreadsheet review — is normalised on load, so there is no build step and no generated artifact to keep in sync.

The smallest dataset that works:

{
  "meta": { "name": "My Service" },
  "threats": [
    {
      "id": "T-1",
      "stride": "Spoofing/EoP",
      "component": "Auth service",
      "description": "Tokens are decoded but never verified",
      "severity": "Critical",
      "mitigation": "Verify the signature against the IdP JWKS"
    }
  ]
}

Or the same thing as CSV:

ID,STRIDE,Element,Threat,Risk,Mitigation
T-1,Spoofing/EoP,Auth service,Tokens are decoded but never verified,Critical,Verify the signature against the IdP JWKS

Only id, stride, and one of title/description are required. See docs/DATA-SCHEMA.md for the full contract, and threat_model_mcp/examples/ for a complete worked model.

Your domain fields survive

Any column or key the schema doesn't recognise is preserved in the threat's extra dict, searchable and returned by the tools. A medical-device team can carry FDA_Objective, IEC62443_FR and Safety_14971; an appsec team carries OWASP and CWE; nothing is hardcoded and nothing is dropped.

Boolean-ish values (yes/no/true/false) become real booleans, and coverage_summary() counts them automatically:

"by_extra": { "handles_pii": 3 }

The server has never heard of handles_pii. It counted it because it is a flag.


Register it with an LLM

The server speaks MCP over stdio, so any compliant client can spawn it.

Claude Code

claude mcp add threat-model \
  --env THREAT_MODEL_PATH=/abs/path/to/my-model.json \
  -- python -m threat_model_mcp.server

Or drop a .mcp.json in your project (an example ships in this repo) and run claude in that directory:

{
  "mcpServers": {
    "threat-model": {
      "command": "python",
      "args": ["-m", "threat_model_mcp.server"],
      "env": { "THREAT_MODEL_PATH": "./my-model.json" }
    }
  }
}

Gemini CLI

Add the same block to ~/.gemini/settings.json (or a project .gemini/settings.json), then run gemini.

Any other MCP client

{
  "command": "python",
  "args": ["-m", "threat_model_mcp.server", "--data", "/abs/path/to/my-model.json"]
}

The client performs the standard handshake, calls tools/list, and invokes any tool via tools/call. The server's name and its instructions — the description the LLM reads to decide when to reach for these tools — are generated from your dataset's own metadata, including any caveats you attached to it.


Tool reference

Tool Description
list_threats() Compact list of every threat (id, title, STRIDE, severity, component, status).
get_threat(threat_id) Full record for one id, including its extra fields. Unknown id → {"error": ...}.
threats_by_stride(category) Threats whose STRIDE set contains the category. Accepts full names or shorthand (EoP, DoS, I, ...).
threats_by_severity(level) Threats at a severity level (case-insensitive).
threats_by_component(component) Threats whose component matches a substring.
search_threats(query) Free-text search across the core fields and every extra value.
get_mitigations(threat_id) Mitigation, severity, priority, and the threat's whole extra bag.
coverage_summary() Counts by STRIDE, severity, priority, group, status; the highest-severity ids; and by_extra flag counts.
export(fmt) Serialise the register: json | csv | md | tm7.
model_info() Your meta block: name, description, scope, method, standards, status, caveats.

STRIDE is multi-valued. A threat can carry several categories, so it answers to each of them, and the per-STRIDE counts in coverage_summary() sum to more than the total. That is correct, not a bug.

Every tool is read-only. The server never writes to your dataset.


Microsoft Threat Modeling Tool export

export("tm7") emits a .tm7 model — the data-flow diagram plus every threat as a threat instance — for the official Microsoft Threat Modeling Tool.

The diagram is data, not code. Declare it in your dataset's optional meta.dfd block:

"dfd": {
  "elements": [
    { "id": "EI1", "kind": "external", "label": "Browser client", "x": 40, "y": 40 },
    { "id": "P1",  "kind": "process",  "label": "P1 API gateway" },
    { "id": "DS1", "kind": "store",    "label": "DS1 Notes database" }
  ],
  "flows": [
    { "source": "EI1", "target": "P1", "label": "HTTPS request" },
    { "source": "P1", "target": "DS1", "label": "read/write" }
  ]
}

Coordinates are optional — anything without x/y is auto-laid-out on a grid, banded by kind (externals on top, processes in the middle, stores at the bottom). If you omit the dfd block entirely, one Process element is inferred per distinct component value, so the export still works on a bare register.

Each threat is attached to an element by matching its component against the element ids and labels; set extra.dfd_element to route it explicitly.

Caveat. The Threat Modeling Tool is a Windows desktop app with no CLI or API, and the .tm7 format is DataContractSerializer XML whose stencil GUIDs reference the loaded KnowledgeBase. This emitter produces a well-formed model of the correct shape using the stock SDL generic type ids, but it is authored from the schema and not tool-validated. Open the output in TMT and Save As to let the tool normalise it before relying on it.


Use it as a library

ThreatModel has no dependency on the mcp SDK, so you can query a register from any Python program:

from threat_model_mcp import ThreatModel

model = ThreatModel.from_path("my-model.csv")
print(model.coverage_summary())
print(model.threats_by_stride("EoP"))
open("model.tm7", "w").write(model.export("tm7"))

Contributing

See CONTRIBUTING.md. The short version: query logic goes in model.py, server.py stays a thin wrapper, and nothing writes to stdout — that is the MCP stdio channel.

License

MIT.

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured