Skip to main content
Glama
Graysonf12

fema-nfhl-mcp-server

by Graysonf12
README.md
# fema-nfhl-mcp-server

MCP server wrapping FEMA's public **National Flood Hazard Layer (NFHL)** ArcGIS REST service
(`https://hazards.fema.gov/arcgis/rest/services/public/NFHL/MapServer`) — the same live dataset
behind `msc.fema.gov`, which the Hutton SIR agent's standing doctrine treats as non-fetchable.
This server queries the NFHL directly, no API key required for the FEMA side.

## Transport: Streamable HTTP (stateless)

This server runs as a small Express HTTP server exposing **`POST /mcp`**, using the MCP
**Streamable HTTP** transport in **stateless mode** — every request gets its own short-lived
MCP server + transport instance, connected, used, and torn down. There is no in-memory session
state kept between calls. This is deliberate: it makes the server safe to run on a host that
spins the process down when idle (e.g. Render's free tier) — there's nothing to lose when the
process restarts, because nothing persists between requests anyway.

An earlier version of this server used **stdio** transport (local-process-only, no network).
That version is kept for reference in `reference/index_stdio_original.ts.txt` but is not part of
the deployable build — `src/index.ts` is now the HTTP entry point, and `src/createServer.ts`
holds the actual tool registration shared by both.

`GET /health` is also exposed for host health checks and uptime pingers.

## Why this exists (Hutton SIR context)

The SIR agent's Field Reference and Setback Routine both carry a standing rule: `msc.fema.gov/portal`
is non-fetchable, so flood zone/BFE/panel lookups always route to a manual coordinate search. This
server replaces that manual step with a live tool call — the agent already desk-answers lat/long for
every site (topography section), so it can pass those same coordinates straight through.

It does **not** replace the "carry coordinates with the route" discipline (Quality Rule 8) — the tool
output includes the queried coordinates in every result specifically so that discipline still holds
even when the lookup succeeds outright.

## Tools

### `fema_flood_zone_lookup`
The primary tool. Takes 1–8 labeled points (e.g. site centroid, or all four parcel corners + centroid)
and returns, per point: flood zone code, SFHA flag, Base Flood Elevation + datum, FIRM panel number,
effective date, and status. Flags a **split-zone condition** automatically if different points return
different zones — the exact case that matters for a site straddling a flood-zone boundary.

### `fema_identify_layer`
Lower-level identify against any NFHL layer(s) at a point — useful for checking Letters of Map Revision
(layer 1) or Letters of Map Amendment (layer 34) on a parcel, which the flood-zone tool doesn't surface.

## Deploying — Render (recommended, browser-only, free)

This repo includes `render.yaml`, so Render can configure itself from a Blueprint — you don't have
to manually fill in build/start commands.

1. Push/upload this repo to GitHub (a browser-only, no-git-CLI way to do this is via GitHub's
   web uploader: new repo → "Add file" → "Upload files").
2. On [render.com](https://render.com), sign up (GitHub login is easiest — no credit card required
   for the free tier).
3. **New > Blueprint**, connect your GitHub account, select this repo. Render will detect
   `render.yaml` and pre-fill everything: Node web service, free plan, `npm install && npm run build`
   build command, `npm start` start command, `/health` health check path.
4. Click **Apply** / **Deploy**. Render will generate a random `MCP_API_TOKEN` environment variable
   automatically (see `render.yaml` — `generateValue: true`). Copy that token from the service's
   **Environment** tab in the Render dashboard once deployed — you'll need it in step 6.
5. Your endpoint will be `https://<your-service-name>.onrender.com/mcp`.
6. In claude.ai: **Customize > Connectors > "+" > Add custom connector**. Paste the URL from step 5.
   Under **Advanced settings > Request headers**, add:
   ```
   Authorization: Bearer <the MCP_API_TOKEN value from step 4>
   ```
   Click **Add**.

**No auth needed?** If you'd rather leave the endpoint fully open (it's a read-only public-data
lookup, so the risk is low), delete the `MCP_API_TOKEN` env var in Render's dashboard after deploy —
the server checks for it at request time and runs open if it's unset. See the `requireAuth` middleware
in `src/index.ts`.

**Free-tier cold starts:** Render's free instances spin down after 15 minutes idle and take
30–60 seconds to wake on the next request. The first flood-zone lookup after a period of inactivity
may be slow or need a retry. Upgrade to Render's Starter plan (~$7/mo) to keep the service always-on
if this becomes annoying.

## Running locally (only if you have Node.js installed)

```bash
npm install
npm run build
PORT=3000 npm start
```

Then `POST http://localhost:3000/mcp` with a standard MCP JSON-RPC body, or point the
[MCP Inspector](https://github.com/modelcontextprotocol/inspector) at
`http://localhost:3000/mcp` for a browser test UI:

```bash
npx @modelcontextprotocol/inspector
```

Then in the Inspector UI, choose "Streamable HTTP" transport and enter the URL above.

No environment variables are required to run it — `MCP_API_TOKEN` is optional (see above).

## Verified so far

- Server builds clean under strict TypeScript.
- **Real protocol-level test performed:** started the compiled server, sent an actual HTTP
  `initialize` request and an actual `tools/list` request to `POST /mcp`, and got back correct,
  well-formed MCP responses (both tools present with valid JSON Schemas, matching the descriptions
  below) — this is a genuine test of the transport wiring, not a mock.
- `GET /health` confirmed working over real HTTP.
- Response parsing (`extractFloodZones` / `extractFirmPanels`, including the epoch-ms → ISO date
  conversion for `EFF_DATE`) is exercised against a mocked response shaped exactly like the live
  service's field schema (`FLD_ZONE`, `SFHA_TF`, `STATIC_BFE`, `V_DATUM`, `FIRM_PAN`, `EFF_DATE`, etc.),
  confirmed against FEMA's own layer metadata for layers 28 (`S_Fld_Haz_Ar`) and 3 (`S_FIRM_Pan`).
- Error handling confirmed to degrade gracefully (clear error message, no crash) on a simulated
  network failure.

## NOT yet verified — do this before relying on it live

The build/test environment used to build this server has no network route to `hazards.fema.gov`,
so **the actual live FEMA API call has not been executed or observed.** Before trusting this in a
real SIR:

1. Run `fema_flood_zone_lookup` against a known site (e.g. a parcel you already have a confirmed
   FIRM panel/zone for from a past live run — Indian River County or Jefferson County both have
   flood data on file) and diff the tool's output against the confirmed manual result.
2. Confirm the `identify` endpoint's exact response shape hasn't drifted from what's documented here —
   ArcGIS services occasionally change field aliasing or add/retire layers.
3. Watch for `SFHA_TF` values other than `"T"`/`"F"` (blank has been reported in edge cases) and confirm
   `extractFloodZones` handles that gracefully (it currently reports it as `null`/not-SFHA, worth
   double-checking against a real blank-value response).
4. Confirm rate-limit behavior under repeated calls (e.g. 5 points per SIR × multiple SIRs per day) —
   the public service doesn't publish a documented limit.
5. Confirm Render's free-tier cold start doesn't exceed claude.ai's MCP request timeout on a cold
   first call — if it does consistently, upgrade to a paid Render plan.

## Known layer IDs (from FEMA's own NFHL MapServer)

| ID | Layer |
|---|---|
| 0 | NFHL Availability |
| 1 | LOMRs |
| 3 | FIRM Panels (`S_FIRM_Pan`) |
| 14 | Cross-Sections |
| 20 | Water Lines |
| 22 | Political Jurisdictions |
| 28 | Flood Hazard Zones (`S_Fld_Haz_Ar`) |
| 34 | LOMAs |