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 Security — the 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)
- [core] Model Context Protocol — specification & concepts (~30 min) — read "Core concepts" (tools, resources) and the trust model; you're building a server, so focus on the tool interface.
- [core] FastMCP / the Python MCP SDK — quickstart
(~25 min) — the
@mcp.tool()decorator, type-hint-as-schema, and stdio transport.
Typed LLM output — validate the reply, don't trust it
- [core]
instructordocumentation — getting started (~30 min) — patching a client to return apydanticmodel, and how it re-asks on validation failure. - [reference] Anthropic API — tool use / structured output
(~20 min) — the underlying mechanism
instructorbuilds on; use a current model id likeclaude-sonnet-5(capable) orclaude-haiku-4-5-20251001(cheap/fast).
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.
instructorvalidates LLM output into apydanticmodel — 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
instructorresponse into aVerdictthe same discipline as validating a feed into anAlert? - 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).