Skip to main content
Glama
iNileshW

Licensing Hardening MCP Server

by iNileshW

Harden the local-authority licensing assistant's MCP server

Local authority — licensing team. The assistant answers questions about taxi and premises licences — "is licence 9003 active, and what did the last inspection say?" — so officers don't dig through the case system by hand. This MCP server is how it reaches the licensing database. A colleague "improved" it before they left; it works, it demos cleanly, and it carries every native MCP weakness from this afternoon. Work through it on your own.

Get it running

Plain Python — no Docker, no Git.

pip install -r requirements.txt
python seed.py            # builds licensing.db (includes a planted inspection note)

See the attacks land before you fix anything:

python exploit.py         # SQL injection + over-exposure of home addresses / DBS status
python poisoning_demo.py  # a tool whose DESCRIPTION carries hidden instructions

Related MCP server: Vulnerable MCP Server

Your task

Harden server.py so the native attacks fail while legitimate use still works. Work through them in this order:

  1. Result injectionread_inspection_note hands raw note text back to the model. Return it labelled as untrusted data, and rely on a system-prompt rule that labelled data is content, never command. (Licensee 3's note is an instruction to suspend a licence — the note-reader and suspend_licence, each harmless, compose into a suspension.)

  2. Tool poisoningformat_reference's description contains hidden instructions. Review every description; a server you do not trust should not reach the model's context. Neutralise it.

  3. Confused deputysuspend_licence is state-changing and unscoped. Put it behind a scope check, and reject any bearer token whose aud is not this server (the Week 5 discipline).

  4. Inherited — parameterise find_licensee and return only the columns the assistant needs (never home_address, dbs_status, or inspection_notes).

  5. Audit — append every tool call to a log.

You are done when poisoning_demo.py finds nothing in your descriptions and exploit.py's payloads are rejected, while a normal find_licensee("Aisha") still returns a result.

Stretch

  • Add a real aud check: decode the presented token and refuse a foreign audience.

  • With two servers connected to one host, how would a malicious second server reach this one's suspend_licence? Write down the control that stops it.

Files

File

Contents

server.py

The vulnerable MCP server — the thing you harden.

seed.py

Builds licensing.db — the licensees and licences tables.

exploit.py

Proves the inherited flaw (SQL injection + over-exposure).

poisoning_demo.py

Prints the model-facing descriptions; flags the poisoned one.

requirements.txt

fastmcp.

Questions worth asking yourself

  • Which of your fixes are MCP-specific, and which are ordinary secure coding?

  • A description you did not write reached your model. Whose job is it to have caught that — the server author, the host, or the person who approved it?

Related MCP Connectors

Related MCP Servers