Module 09 — Building an MCP Server¶
Type 9 · Tool-Build — build a fastmcp MCP server exposing one tool, enrich_ip(ip) -> dict, backed by the module-04 threat-intel API, that speaks the MCP tool protocol and returns LLM-parseable results. (Secondary: Red-team-the-AI — treat every tool argument as untrusted and watch the prompt-injection / untrusted-arg boundary the capstone depends on.) Go to the hands-on lab →
Last reviewed: 2026-06
Python for Security — if you can describe a security operation as a function, you can hand it to an LLM.
In 60 seconds
MCP is the emerging standard for giving an LLM access to tools — it's JSON-RPC over stdio or HTTP,
where a server declares typed functions the model can call by name. fastmcp is to MCP what
FastAPI is to REST: decorate a typed Python function with @mcp.tool() and it generates the schema
and speaks the protocol. The security disciplines are non-negotiable: build read-only tools first,
and treat every tool argument as untrusted — an LLM can pass malformed input or values injected by
a prompt from an adversarial document.
Why this matters¶
The Model Context Protocol (MCP) is the emerging standard for giving LLMs access to tools and data sources. A security team that exposes their threat-intel enrichment, SIEM search, and asset lookup as MCP tools can let an AI assistant drive those operations while keeping a human in the loop for decisions. This module is the practical entry point: build a real MCP server that wraps the enrichment function you've already written.
Objective¶
Build a fastmcp MCP server that exposes one tool — enrich_ip(ip: str) -> dict — backed by
the local threat-intel API from module 04 (real abuse.ch feeds: Feodo Tracker + URLhaus). The server should start, respond to the MCP tool
protocol, and return enriched results in a format an LLM client can parse.
The core idea¶
MCP is a JSON-RPC protocol over standard I/O or HTTP. A server declares a set of tools —
functions with typed input schemas and structured return values — and a client (Claude, Cursor,
or any MCP-aware host) can call those tools by name. The fastmcp library is to MCP what
FastAPI is to REST: you write a Python function with type hints, decorate it with @mcp.tool(),
and fastmcp generates the schema and handles the protocol. From the LLM's perspective, the
tool looks like a typed function in the tool list it was given.
The mental model
A server declares tools — typed functions with input schemas and structured returns — and any
MCP-aware host calls them by name. @mcp.tool() turns a function signature into that schema
automatically, so from the model's side your enrichment function is just one more typed entry in
the tool list. You're not building an integration; you're publishing a callable contract.
The security use case is concrete: an analyst prompts Claude with "Is this IP malicious?" and
Claude calls enrich_ip("185.220.101.1"), gets back {"verdict": "malicious", "abuse_score":
95, "asn": "AS4444 TOR Exit"}, and explains the result in plain English. The analyst never
leaves the chat window; the enrichment is automatic; the human provides the judgment ("yes,
block it"). This is the "AI authors → you review → you own it" posture applied to tool use:
the AI does the API call, you decide what to do with the result.
sequenceDiagram
participant H as Host (Claude)
participant S as MCP server
participant API as Threat-intel API
H->>S: tools/call enrich_ip("185.220.101.1")
Note over S: validate arg<br/>(untrusted input)
S->>API: GET /ip/185.220.101.1
API-->>S: {abuse_score, asn, ...}
S-->>H: {"verdict": "malicious", ...}
Note over H: explains result;<br/>human decides
The operational discipline for MCP servers in security contexts: every tool should be read-only
unless the LLM is explicitly designed and scoped for write actions. An enrich_ip tool that
only queries is safe. An block_ip(ip: str) tool that writes a firewall rule is a tool that
can cause an outage if the LLM misunderstands a prompt. Build read-only first; add write tools
only with a human-approval step designed in from the start, not bolted on.
Input validation on every tool argument is not optional. An LLM will occasionally pass malformed input — a partial IP, a domain where an IP was expected, or a value injected by a prompt from an adversarial document (prompt injection). Validate the IP format before calling the API; return a clear error message rather than letting the API call fail with a cryptic status. The MCP server is a trust boundary: treat every tool argument as untrusted input.
The gotcha
An MCP tool argument is untrusted input twice over: the LLM can hallucinate a malformed value, and
a prompt-injection payload in a document the model read can steer what it passes you. A read-only
enrich_ip is safe; a block_ip tool that writes a firewall rule can cause an outage on a
misunderstood prompt. Validate every argument, return clear errors, and build write tools only
behind a designed-in human-approval step.
AI caveat
fastmcp is new enough that models sometimes get the decorator syntax slightly wrong. Write the
server function first, then have a model add the @mcp.tool() decorator and FastMCP init — the
last 20% (return-type annotation, error handling) is where its draft tends to slip. Test by running
the server and calling the tool: does it return valid JSON matching the schema?
Learn (~2 hrs)¶
MCP fundamentals (~1 hr) - Model Context Protocol — Introduction (modelcontextprotocol.io) — read the "What is MCP?", "Core concepts", and "Tools" sections; understand the tool schema format and how clients discover tools. - fastmcp — GitHub README — the library you'll use; read the "Quick start" and "Tools" sections; the decorator pattern is the entire API surface you need.
Security considerations for MCP (~1 hr) - MCP Security — Prompt Injection and Tool Safety (Simon Willison) — Simon Willison's writing on MCP security is the clearest current thinking; read carefully, especially the prompt-injection risk for data-reading tools. - Anthropic — Tool use (function calling) guide — understand the tool schema format from the client's perspective so you know what your server must produce.
Key concepts¶
- MCP tool schema: name, description, input schema (JSON Schema), return value
@mcp.tool()decorator: function signature → tool schema automatically- Read-only vs write tools: the security boundary and why it matters
- Input validation as a trust boundary: every argument is untrusted
- Prompt injection risk: data your tool reads can contain instructions to the LLM
AI acceleration¶
fastmcp is new enough that models sometimes get the decorator syntax slightly wrong. Write the
server function first, then ask a model to add the @mcp.tool() decorator and the fastmcp.FastMCP
initialization. Test it by running the server and calling the tool directly — does it respond
with valid JSON that matches the schema? The model will get you 80% there; the remaining 20% is
usually the return type annotation and the error handling.
Check yourself
- What does the
@mcp.tool()decorator generate from a typed function, and what does the LLM see? - Why build read-only tools before write tools — what's the failure mode of an unguarded
block_ip? - In what two distinct ways can a tool argument be "untrusted," and what does prompt injection have to do with it?
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).