Skip to main content
Glama
iNileshW

hmpo-mcp-server-practice

by iNileshW
README.md
# 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_status
```

`server.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=5001
```

## What you're doing, in order

1. **Run it and look at it.** Build the database, run `test_server.py`, and list the
   tools with the CLI above. Call `get_application_status` for `5001`, and for an id
   that does not exist.

2. **Add at least two new tools** over the dataset. Good candidates:
   - `list_applications_for_applicant(applicant_id)` — every application from one person
   - `count_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_number` or `date_of_birth`
     from the `applicants` table

3. **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`.** Serve `guidance.md` at a stable
  URI (for example `guidance://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 |
|---|---|
| `server.py` | The MCP server. One tool today: `get_application_status`. Add yours below the marker. |
| `seed.py` | Builds `passport.db` — the `applications` and `applicants` tables. |
| `test_server.py` | In-process FastMCP client check; a template for verifying your tools. |
| `guidance.md` | Processing-stages extract (for the resource stretch). |
| `requirements.txt` | `fastmcp`. |

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