Skip to content

Module 07 — LLM-Native Python & MCP

Type 9 · Tool-Build — expose sift to an LLM as an MCP server, and validate the LLM's own output with the same discipline you validate an API response. Go to the hands-on lab →  ·  Cheat sheet →

Last reviewed: 2026-08

Python for Securitythe same "parse, don't trust" you apply to a feed applies to the model — its arguments in, and its answers out.

In 60 seconds

Two moves make sift LLM-native, and both are the input edge discipline from Module 02 applied to the AI. First, expose sift's enrich/triage functions as an MCP server so an LLM or agent can call them — where every argument the model passes is untrusted input, validated exactly like Module 02 validates a feed. Second, when sift itself calls an LLM (to summarize or classify an alert), the model's output is also untrusted — use instructor to force it into a pydantic model, validated like an API response instead of parsed out of free text. Module 02 was the input edge; this is the AI edge; Module 09 is the measurement layer. Same discipline, three places.

Why this matters

"LLM-native" is where a lot of security tooling is heading, and where a lot of it is quietly unsafe. Two trust boundaries get missed. When you expose a tool to a model via MCP, people forget that the model is an untrusted caller — it can be prompt-injected into calling your enrich tool with a hostile argument, so the tool must validate its inputs as if they came from the internet (because, transitively, they did). And when your code consumes an LLM's output, people json.loads() the model's free text and hope — which breaks the moment the model wraps it in prose or hallucinates a field.

Both failures are the same root mistake this whole track fights: trusting input you didn't validate. The fix is the fix you already know — parse into a typed model at the boundary — applied to the AI on both sides. Get this right and sift becomes a tool an agent can safely use and a tool that safely uses an agent.

These two boundaries are not our invention — they are the two AI-specific entries of the OWASP Top 10 for LLM Applications: Prompt Injection (the model can be steered into calling your tool with a hostile argument) on the way in, and Insecure Output Handling (trusting the model's text as if it were structured, safe data) on the way out. "Parse, don't trust" is the same discipline that closes both — the AI edge of the through-line you opened in Module 02 and close in Module 09:

flowchart LR
    I["untrusted feed input<br/>Suricata eve.json · M02"] --> P["pydantic model<br/>validate at the boundary"]
    O["untrusted LLM output<br/>MCP tool arg + verdict · M07"] --> P
    Me["untrusted measurement<br/>eval metrics · M09"] --> P
    P --> Trust["typed, validated value<br/>the rest of sift can trust"]

The core idea

An MCP server is an API whose caller is an LLM — so validate every argument. Exposing sift's enrich/triage as MCP tools (with FastMCP, @mcp.tool()) is a few lines: the type hints and docstring become the schema the model sees. The tools operate over the real indicators sift already derives from validated AlertEvents — a src_ip/dest_ip or a signature — not some invented indicator blob. But the model can be manipulated into calling enrich with "1.1.1.1; drop table" or a path-traversal string, so the tool re-validates its arguments with Module 02's canonical EVE pydantic models (an IPvAnyAddress, an AlertEvent) before doing anything — the LLM's argument is untrusted input exactly like a raw eve.json line. Prefer read-only tools; if a tool changes state, gate it behind explicit human confirmation rather than letting the model trigger it.

sequenceDiagram
    participant H as LLM host<br/>(agent / assistant)
    participant C as MCP client
    participant S as sift MCP server<br/>(FastMCP)
    participant T as enrich / triage tool
    H->>C: "triage this alert / check 1.1.1.1"
    C->>S: call tool(argument)
    S->>T: validate arg — IPvAnyAddress / AlertEvent
    Note over T: hostile or out-of-range arg<br/>→ rejected at the boundary
    T-->>S: typed result (dict)
    S-->>C: structured response
    C-->>H: tool result

instructor makes the LLM's output a typed object, not a hope. When sift asks a model to classify an alert, you don't want a paragraph you regex — you want a validated Verdict(severity=..., is_tp=...). instructor patches the client so the model's response is coerced into (and re-asked until it satisfies) a pydantic model. That's the exact twin of Module 02: there you validated an untrusted API response into an Alert; here you validate an untrusted model response into a Verdict. The LLM is just another unreliable upstream you refuse to trust raw.

flowchart LR
    M["LLM reply<br/>(free text / JSON)"] --> G{"validate into<br/>Verdict (pydantic)"}
    G -->|fits the schema ✓| V["Verdict(severity,<br/>is_true_positive, rationale)"]
    G -->|missing field / bad enum ❌| R["reject — never<br/>json.loads() and pray"]
    R -.instructor re-asks the model.-> M

The shape, concretely. Both typed edges are a few lines — the API is the lesson here, so see it once:

from fastmcp import FastMCP
import instructor

mcp = FastMCP("sift")

@mcp.tool()                                  # argument-in edge: the caller is an LLM
def enrich(ip: IPvAnyAddress) -> EnrichResult:   # typed args → validated at the boundary
    ...                                      # your sift enrich core, unchanged

# output-out edge: the model's reply is parsed into a pydantic model, never trusted raw
client = instructor.from_openai(OpenAI())
verdict: Verdict = client.chat.completions.create(   # raises if the reply doesn't fit Verdict
    model=..., response_model=Verdict, messages=[...])

Pin the moving parts. MCP and instructor are newer and still evolving. Pin their versions (Module 01's lockfile), teach yourself the durable pattern (typed tool arguments; typed model output), and treat the specific API as replaceable — the discipline outlives the library.

Where this ends and Track 12 begins

This module is about building the tool well in Python — the typed MCP server, the validated LLM output. Operating and securing AI systems at large — RAG quality, a SOC copilot at volume, the broad red-team — is Track 12 (AI-Augmented Ops). The shared verbs (MCP, prompt injection) appear in both: here you learn the craft; there you learn the operation. Module 08 next bridges them by attacking the very server you build here.

Go deeper (~2–3 hrs · optional)

The core idea above teaches both moves — the typed MCP server whose caller is untrusted, and validating the model's output like an API response — and you can build the lab from it. These links go deeper on the exact MCP/instructor API and the primary source for the two trust boundaries; pull them when a step doesn't click, not as required reading.

MCP — the server whose caller is an LLM (start here)

Typed LLM output — validate the reply, don't trust it

The anchor — the two boundaries as OWASP entries

  • [primary source] OWASP Top 10 for LLM Applications (~20 min) — read Prompt Injection and Insecure Output Handling: the two AI-specific risks that are exactly the argument-in and output-out boundaries this module validates.

Key concepts

  • An MCP server's caller (the LLM) is untrusted — validate every tool argument with pydantic.
  • Type hints + docstring = the tool schema the model sees; write them for the model.
  • Read-only by default; gate state-changing tools behind human confirmation.
  • instructor validates LLM output into a pydantic model — the AI-edge twin of Module 02.
  • Pin MCP/instructor (they move fast); own the durable pattern, not the specific API.

AI acceleration

This is the module where the track's tool becomes usable by AI — so the review stakes rise. Have the copilot scaffold the MCP server and the instructor-typed classifier, then check the two boundaries: does the tool validate its arguments (or trust whatever the model passes)? Does the classifier return a validated model (or json.loads() free text and pray)? Both are the same bug you've been catching all track, now at the AI edge.

Check yourself

  • Why is an argument an LLM passes to your MCP tool "untrusted input," and what stops it being dangerous?
  • How is validating an instructor response into a Verdict the same discipline as validating a feed into an Alert?
  • Which of sift's tools should be read-only, and which (if any) deserve a human-confirmation gate?

Comments

Sign in with GitHub to comment. Choose the type: Feedback (errors or suggestions on this page) · Hints (help for fellow learners — no spoilers) · General (anything else).