Skip to content

Cheat sheet — Building MCP Servers

Companion to Module 05 — Building MCP Servers · CC BY 4.0 — print it, pin it, share it.

Last reviewed: 2026-07

MCP (Model Context Protocol) lets an LLM call your tools over a standard protocol. The security rule that governs everything: every argument the model passes is untrusted input — validate it exactly as you would a web request parameter.

Minimal server with FastMCP (Python)

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("soc-tools")

@mcp.tool()
def lookup_ip(ip: str) -> dict:
    """Return geo/ASN for an IPv4 address. `ip` must be a dotted-quad."""
    validate_ipv4(ip)                 # reject anything that isn't an IP — do NOT trust the model
    return enrich(ip)

if __name__ == "__main__":
    mcp.run()                         # stdio transport by default
  • The docstring and type hints ARE the tool schema the model sees — write them for the model, and keep the description tight so it calls the tool correctly.
  • Prefer read-only tools. If a tool mutates state (blocks an IP, closes a ticket), gate it behind an explicit human confirmation, never let the model trigger it unattended.

Validate every argument (the whole game)

def lookup_ip(ip: str) -> dict:
    import ipaddress
    try:
        ipaddress.ip_address(ip)       # raises on anything not an IP
    except ValueError:
        raise ValueError("ip must be a valid IPv4/IPv6 address")
    # never interpolate `ip` into a shell/SQL string — parameterize or subprocess-list-args

Run and inspect it

# the MCP Inspector drives your server like a client would — test tools by hand
npx @modelcontextprotocol/inspector python server.py

# register with a client (e.g. Claude Code) via its MCP config, pointing at the command

Test the hostile path

def test_rejects_shell_metachars():
    with pytest.raises(ValueError):
        lookup_ip("1.1.1.1; rm -rf /")   # prove injection args are refused

Gotchas worth remembering

  • Every LLM-supplied argument is attacker-controlled. A model can be prompt-injected into calling your tool with "; drop table" or a path-traversal string. Validate/parameterize as if it came from the internet — because effectively it did.
  • Read-only by default; mutations need a human gate. The blast radius of a compromised prompt is exactly the set of state-changing tools you exposed. Keep that set tiny and confirmed.
  • The docstring is a prompt. A vague description makes the model misuse the tool; a precise one (args, units, constraints) is what makes it reliable. Treat it as part of the interface.
  • Least privilege for the server's own creds. The tool runs with its permissions, not the model's — scope its API keys/roles to exactly what its tools need.
  • Ship it like a tool, not a script: flags, README, tests. The deliverable is something others can run and trust, which means the hostile-argument test is non-negotiable.

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).