hmpo-mcp-server-practice
Extend the HM Passport Office application-status assistant
HM Passport Office — examination support. HMPO processes millions of passport applications a year, and applicants (and MPs' offices) constantly ask "where is application 5001, and what happens next?". The assistant answers those questions directly instead of a caseworker looking each one up by hand. This MCP server is how it reaches the applications database: it exposes the database to the model as tools, over the Model Context Protocol.
Today it exposes one tool. Your job is to add the ones the assistant needs — read-only, parameterised, and returning nothing from which an applicant's identity could be rebuilt. Work through it on your own.
Get it running
Plain Python — no Docker, no Git.
pip install -r requirements.txt
python seed.py # builds passport.db (five applications, four applicants)
python test_server.py # in-process check: lists tools, calls get_application_statusserver.py speaks MCP over stdio — nothing to open in a browser. To poke it by hand,
use the MCP Inspector's command-line mode (its web UI does not come through the
training VM's port proxy):
# list the tools the model would see
npx @modelcontextprotocol/inspector --cli python server.py --method tools/list
# call one
npx @modelcontextprotocol/inspector --cli python server.py \
--method tools/call --tool-name get_application_status --tool-arg application_id=5001What you're doing, in order
Run it and look at it. Build the database, run
test_server.py, and list the tools with the CLI above. Callget_application_statusfor5001, and for an id that does not exist.Add at least two new tools over the dataset. Good candidates:
list_applications_for_applicant(applicant_id)— every application from one personcount_applications_by_status()— a queue summary (in examination, printed…)find_applications_by_service(service)— e.g. all "Renewal" applications
For each, keep the three disciplines from the starter:
parameterised query — bind arguments; never build SQL by string interpolation
open the database read-only (
mode=ro)return only application columns — never a
passport_numberordate_of_birthfrom theapplicantstable
Check your change worked. Re-list the tools and confirm the new ones appear, then call them:
npx @modelcontextprotocol/inspector --cli python server.py --method tools/list \ | python -c "import sys,json; print([t['name'] for t in json.load(sys.stdin)['tools']])" npx @modelcontextprotocol/inspector --cli python server.py \ --method tools/call --tool-name count_applications_by_status
Stretch
Expose the processing guidance as a
resource. Serveguidance.mdat a stable URI (for exampleguidance://passport-stages) so the assistant cites the live document rather than its training memory. Check it from the CLI:npx @modelcontextprotocol/inspector --cli python server.py --method resources/list npx @modelcontextprotocol/inspector --cli python server.py \ --method resources/read --uri "guidance://passport-stages"Add audit logging. Append every tool call — name, arguments, timestamp — to a log file.
Files
File | Contents |
| The MCP server. One tool today: |
| Builds |
| In-process FastMCP client check; a template for verifying your tools. |
| Processing-stages extract (for the resource stretch). |
|
|
Notes from the previous developer
Status: in a pilot with the examination team. The connection is opened read-only on
purpose — a status surface has no business writing to application data. Every query
is parameterised; the application id is bound, never formatted into the SQL. And
get_application_status returns only application columns — nothing from applicants.
That is deliberate: applicants carries passport numbers and dates of birth, and the
shortest join from an application to a passport number is a single hop. Whatever
tools you add, that hop must stay closed.
Questions worth asking yourself
Which of your tools would you leave connected to a live model overnight, unsupervised — and which would you not, and why?
What is the shortest join from an application to a passport number, and what stops the model asking for it?
A tool's description is read by the model as an instruction, not just documentation. Look at what you have written in yours — then ask what a malicious server could write in the description of a tool your agent connects to. (That question is where this afternoon begins.)