An MCP server that never learns where you are

SanAlert gives an unofficial threat level from the air for places in Poland. Its server for AI assistants answers for one place without the place reaching us, and it won't let a model turn "no data" into "all quiet".

SanAlert shows an unofficial threat level from the air for places in Poland: drones and missiles tracked across the border, a level for the place you pick, and the official RCB and RSO messages in their original wording. It runs as a website, an Android and Wear OS app and a Home Assistant integration, with the iPhone app in review.

The obvious question to put to an AI assistant is "is it calm in Przemyśl right now?". Since 6 October the API has had a remote MCP server for questions like that. It has six read-only tools for the whole country: the state summarized, official messages from the last 24 hours, night reports, source cards and the reliability record. None of them takes a place. A tool that takes a place on our server means our server learns where people are, and I left that open.

Working it out on the user's computer

The new server runs on the user's computer instead, next to Claude Code or Claude Desktop, and it has one tool: sanalert_level. It takes one of the 44 towns from the website's list, or a latitude and longitude.

It works the way the Home Assistant integration already does: it downloads the same public state document the website and the apps read, at most once every five seconds, plus the grid of Poland once per run. Then it finds the place's square on that grid and reads the level locally. No request it makes carries the place.

It also refuses to geocode. A town that isn't on the list is an error that lists the towns and asks for coordinates, because looking a name up online would send the place to whoever runs the lookup. That's less convenient, and it's the point.

One limit is outside the server's reach: the question and the answer still go to the assistant's provider, as anything you type into it does. The privacy page says that in the same paragraph that says the place stays on your computer.

What a model must not do with a warning

A model rewrites what it's given, and handed "Aktywność" with a line about an alarm across the border, it can easily come back with "nothing to worry about in Przemyśl". SanAlert has two rules for every surface: never narrow a warning, and no data is never quiet. The server has to hold both, even with a model in between.

Every answer carries say, the sentence the website shows under the level, in Polish, English or Ukrainian. The server's instructions tell the assistant to repeat the level and that sentence as they are, never to soften or narrow them, and never to call a place safe.

When the state document is missing, can't be read or is older than 60 seconds, the level is no_data with the reasons, never quiet. A point outside Poland's grid is out_of_range, so Paris doesn't come back calm.

Every answer also carries the notice, the attribution and the link to NEPTUN, whose data the level partly comes from. Those are the conditions of using the data, and the server passes them on unchanged.

One set of rules

The level isn't computed by new code; the server uses a byte-for-byte copy of the Home Assistant integration's core.py, and CI fails if the two ever differ. The notice and attribution texts are copies of the API's, and a test in the API fails if those drift. If the rules change, they change in one place.

The bug the review found

A Claude Code session wrote the server, under the same rules as the rest of SanAlert. Before I merged it, a separate review session read the diff and found a real bug.

The age of the state document came from the computer's clock. With the clock two hours behind and our API down, the last "quiet" the server had seen would look fresh for two hours. Now the age is never less than the time the server has held the document, counted on both the monotonic and the wall clock, the larger winning. The website already worked that way. The Home Assistant integration, which shares the same core, had the same gap; its fix went out in version 1.1.1 on 21 October.

On 20 October, run against the live API, it gave Przemyśl "Aktywność" because of an alarm across the border, with two official messages in force for the voivodeship. Lublin and Warsaw came back quiet, Paris out_of_range.

Using it

The code is at github.com/plrojek/sanalert-mcp, under MIT. In Claude Code the easiest route is the plugin, which adds both servers, the local one and the remote one for the whole country: run /plugin marketplace add plrojek/agent-skills, then /plugin install sanalert@plrojek.

It isn't an official warning system and doesn't replace sirens, Alert RCB or instructions from the authorities. The server says so in every answer too.

← All writing Get in touch