fema-mcp-server

v0.4.1 pre-1.0

Query FEMA disaster declarations, public assistance grants, housing aid, and NFIP flood insurance claims via MCP. STDIO or Streamable HTTP.

fema.caseyjhand.com/mcp
claude mcp add --transport http fema-mcp-server https://fema.caseyjhand.com/mcp
codex mcp add fema-mcp-server --url https://fema.caseyjhand.com/mcp
{
  "mcpServers": {
    "fema-mcp-server": {
      "url": "https://fema.caseyjhand.com/mcp"
    }
  }
}
gemini mcp add --transport http fema-mcp-server https://fema.caseyjhand.com/mcp
{
  "mcpServers": {
    "fema-mcp-server": {
      "command": "bunx",
      "args": [
        "mcp-remote",
        "https://fema.caseyjhand.com/mcp"
      ]
    }
  }
}
{
  "mcpServers": {
    "fema-mcp-server": {
      "type": "http",
      "url": "https://fema.caseyjhand.com/mcp"
    }
  }
}
curl -X POST https://fema.caseyjhand.com/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl","version":"1.0.0"}}}'

Tools

9

read 8

fema_search_disasters

open-world

Search federal disaster declarations by state, incident type, declaration type, date range, and county. Returns deduplicated declaration-level summaries — each disaster number appears once with a designated_area_count showing how many counties/municipalities were designated. The disaster number is the chain key for fema_get_disaster, fema_get_public_assistance, and fema_get_housing_assistance. Use declaration_type to filter: DR (major disaster, most common), EM (emergency), FM (fire management). Date filters apply to the declaration date. Use fema_get_disaster to retrieve all designated-area rows for a specific declaration. county selects the declarations that designated a matching area; their area counts and program flags still describe the whole declaration, and matched_areas lists the matching areas with their own program flags. A search reads the most recent 10,000 designated-area rows; when more match, it returns the declarations dated after the day that window ends, sets truncated, and its notice names the date_to that continues with older declarations. A page holds at most 100,000 characters of declarations; when declarations remain past it, next_offset is the offset to continue from.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "fema_search_disasters",
    "arguments": {}
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "state": {
      "description": "Two-letter US state/territory code (e.g., TX, CA, FL, PR). Filters by state.",
      "type": "string",
      "minLength": 2,
      "maxLength": 2
    },
    "incident_type": {
      "description": "Incident type filter (e.g., Flood, Hurricane, Tornado, Fire, Earthquake, Severe Storm). Case-insensitive substring match: storm also matches Snowstorm and Winter Storm.",
      "type": "string"
    },
    "declaration_type": {
      "description": "Declaration type: DR (major disaster declaration), EM (emergency declaration), FM (fire management assistance declaration).",
      "type": "string",
      "enum": [
        "DR",
        "EM",
        "FM"
      ]
    },
    "date_from": {
      "description": "Start of the declaration date range as a calendar date in YYYY-MM-DD format (e.g., 2024-01-01). Inclusive; not after date_to.",
      "anyOf": [
        {
          "type": "string",
          "const": ""
        },
        {
          "type": "string",
          "format": "date",
          "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$",
          "description": "Calendar date, YYYY-MM-DD."
        }
      ]
    },
    "date_to": {
      "description": "End of the declaration date range as a calendar date in YYYY-MM-DD format (e.g., 2024-12-31). Inclusive; equal to date_from for a single day.",
      "anyOf": [
        {
          "type": "string",
          "const": ""
        },
        {
          "type": "string",
          "format": "date",
          "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$",
          "description": "Calendar date, YYYY-MM-DD."
        }
      ]
    },
    "county": {
      "description": "Filter by designated area / county name (e.g., Harris, Los Angeles, dekalb). Case-insensitive substring match: harris also matches Harrison. Selects the declarations that designated a matching area; designated_area_count and the program flags still describe the whole declaration, and matched_areas lists the matching areas.",
      "type": "string"
    },
    "limit": {
      "default": 50,
      "description": "Maximum number of unique disaster declarations to return (1–1000, default 50). A page also stops at 100,000 characters of declarations (roughly 290 declarations, about 190 with county); when either bound leaves matching declarations out, next_offset names the offset to continue from.",
      "type": "integer",
      "minimum": 1,
      "maximum": 1000
    },
    "offset": {
      "default": 0,
      "description": "Pagination offset in declarations (default 0). Pass the previous page’s next_offset to continue.",
      "type": "integer",
      "minimum": 0,
      "maximum": 9007199254740991
    }
  },
  "required": [
    "limit",
    "offset"
  ],
  "additionalProperties": false
}
view source ↗

fema_get_disaster

Fetch up to 1,000 designated-area rows for a specific FEMA disaster by disaster number (e.g., 4781). Discloses partial area and program coverage when more rows exist. Returns counties/municipalities along with programs observed, incident period, and state info. Use fema_search_disasters to find disaster numbers. The returned disaster_number chains to fema_get_public_assistance and fema_get_housing_assistance.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "fema_get_disaster",
    "arguments": {
      "disaster_number": "<disaster_number>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "disaster_number": {
      "type": "integer",
      "exclusiveMinimum": 0,
      "maximum": 9007199254740991,
      "description": "FEMA disaster number (e.g., 4781). Obtain from fema_search_disasters."
    }
  },
  "required": [
    "disaster_number"
  ],
  "additionalProperties": false
}
view source ↗

fema_get_public_assistance

open-world

Retrieve Public Assistance (PA) funded project records for a disaster or state — shows where federal recovery money was obligated. Returns applicant, damage category, project size and status, federal share obligated, and total obligated amounts. Either disaster_number or state must be provided. Use disaster_number (from fema_search_disasters) to scope to a single declaration, or state to browse all PA projects for a state. PA projects are created only when the PA program is declared (pa_declared: true on the disaster). A page holds at most 100,000 characters of projects; when projects remain past it, next_offset is the offset to continue from.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "fema_get_public_assistance",
    "arguments": {}
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "disaster_number": {
      "description": "FEMA disaster number to scope results to a single declaration. Numbers above 32767 match no declaration.",
      "type": "integer",
      "exclusiveMinimum": 0,
      "maximum": 9007199254740991
    },
    "state": {
      "description": "Two-letter state code to browse all PA projects for a state.",
      "type": "string",
      "minLength": 2,
      "maxLength": 2
    },
    "county": {
      "description": "Filter by county name (e.g., Harris, dekalb). Case-insensitive substring match: harris also matches Harrison.",
      "type": "string"
    },
    "limit": {
      "default": 100,
      "description": "Maximum number of projects to return (1–10000, default 100). A page also stops at 100,000 characters of projects (roughly 220 projects); when either bound leaves matching projects out, next_offset names the offset to continue from.",
      "type": "integer",
      "minimum": 1,
      "maximum": 10000
    },
    "offset": {
      "default": 0,
      "description": "Pagination offset (default 0). Pass the previous page’s next_offset to continue.",
      "type": "integer",
      "minimum": 0,
      "maximum": 9007199254740991
    }
  },
  "required": [
    "limit",
    "offset"
  ],
  "additionalProperties": false
}
view source ↗

fema_get_housing_assistance

open-world

Retrieve Individual Assistance (IA) housing data for a disaster by disaster number. Returns owner and/or renter records aggregated by county, city, and ZIP code — valid registrations, total approved IHP amounts, repair/rental/other-needs amounts, and inspection counts (owners also carry inspected damage in USD). Narrow with state, county, or zip_code; owners_count, renters_count, and offset then count the filtered records. Use type to select owners, renters, or both (default). A page holds at most 100,000 characters of records, owners and renters cut at the same row count so one offset continues both; when records remain past it, next_offset is the offset to continue from. Use disaster_number from fema_search_disasters.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "fema_get_housing_assistance",
    "arguments": {
      "disaster_number": "<disaster_number>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "disaster_number": {
      "type": "integer",
      "exclusiveMinimum": 0,
      "maximum": 9007199254740991,
      "description": "FEMA disaster number. Obtain from fema_search_disasters. Numbers above 32767 match no declaration."
    },
    "state": {
      "description": "Two-letter state code to narrow results when a disaster spans multiple states.",
      "type": "string",
      "minLength": 2,
      "maxLength": 2
    },
    "county": {
      "description": "Filter by county name (e.g., Harris, dekalb). Case-insensitive substring match: harris also matches Harrison. OpenFEMA stores the county with its type, such as Harris (County) or Evangeline (Parish), and as the applicant reported it.",
      "type": "string"
    },
    "zip_code": {
      "description": "Filter by 5-digit ZIP code, as a string with leading zeros kept (e.g., 77084, 00609). ZIP+4 is not accepted. One ZIP can return several records, one per county and city spelling.",
      "anyOf": [
        {
          "type": "string",
          "const": ""
        },
        {
          "type": "string",
          "pattern": "^\\d{5}$",
          "description": "5-digit ZIP code."
        }
      ]
    },
    "type": {
      "default": "both",
      "description": "Which housing assistance dataset to query: owners, renters, or both (default).",
      "type": "string",
      "enum": [
        "owners",
        "renters",
        "both"
      ]
    },
    "limit": {
      "default": 100,
      "description": "Maximum records per dataset to return (1–10000, default 100). A page also stops at 100,000 characters of owner and renter records together, cutting both datasets at the same row count; when either bound leaves matching records out, next_offset names the offset to continue from.",
      "type": "integer",
      "minimum": 1,
      "maximum": 10000
    },
    "offset": {
      "default": 0,
      "description": "Pagination offset into each queried dataset (default 0). Pass the previous page’s next_offset to continue both.",
      "type": "integer",
      "minimum": 0,
      "maximum": 9007199254740991
    }
  },
  "required": [
    "disaster_number",
    "type",
    "limit",
    "offset"
  ],
  "additionalProperties": false
}
view source ↗

fema_search_nfip

open-world

Search National Flood Insurance Program (NFIP) claims data by state, county, ZIP code, and year range. Returns the matching claim count and claim records — amounts paid on building and contents, damage estimates, flood zones, cause and occupancy codes, and loss dates — newest loss first. state is required — the full NFIP dataset is 2.7 million rows; unfiltered access is prohibited. Page the inline claims with limit and offset; a page holds at most 100,000 characters of claims, and while claims remain past it, next_offset is the offset to continue from. When DataCanvas is enabled (CANVAS_PROVIDER_TYPE=duckdb) and the match exceeds that inline budget, a call at offset 0 stages the match (up to 50,000 claims) on a canvas: inspect the staged table with fema_dataframe_describe, then aggregate it with SQL via fema_dataframe_query.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "fema_search_nfip",
    "arguments": {
      "state": "<state>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "state": {
      "type": "string",
      "minLength": 2,
      "maxLength": 2,
      "description": "Two-letter US state code (required). NFIP dataset is 2.7M rows — state filter is mandatory."
    },
    "county_code": {
      "description": "County FIPS code to narrow results within the state: the full 5-digit state+county code (e.g., 48201 for Harris County TX), or the 3-digit county part (e.g., 201), to which the server prepends the state FIPS. A shorter county part is zero-padded to 3 digits (1 → 001), and a 4-digit code is zero-padded to 5 (1001 → 01001), the form a numeric 01001 arrives in. Any other value is rejected before the search runs.",
      "type": "string"
    },
    "zip_code": {
      "description": "ZIP code to narrow results to a specific area (5-digit, e.g., 77002).",
      "type": "string",
      "pattern": "^\\d{5}$"
    },
    "year_from": {
      "description": "Start year of loss, inclusive (e.g., 2020). Not after year_to.",
      "type": "integer",
      "minimum": 1970,
      "maximum": 2100
    },
    "year_to": {
      "description": "End year of loss, inclusive (e.g., 2023). Equal to year_from for a single year.",
      "type": "integer",
      "minimum": 1970,
      "maximum": 2100
    },
    "limit": {
      "default": 1000,
      "description": "Maximum claims to return inline (1–10000, default 1000). A page also stops at 100,000 characters of claims (roughly 330 claims); when either bound leaves matching claims out, next_offset names the offset to continue from. When DataCanvas is enabled and the match exceeds that character budget, a call at offset 0 stages the match on a canvas regardless of this value.",
      "type": "integer",
      "minimum": 1,
      "maximum": 10000
    },
    "offset": {
      "default": 0,
      "description": "Pagination offset into the matching claims, ordered newest date of loss first (default 0). Pass the previous page’s next_offset to continue. Only a call at offset 0 stages a match on a canvas; a later offset returns just its inline page. After a spill (spilled=true), read rows past the inline claims from the canvas table with fema_dataframe_query.",
      "type": "integer",
      "minimum": 0,
      "maximum": 9007199254740991
    },
    "canvas_id": {
      "description": "Optional canvas ID from a prior call, to stage this match as another table on that canvas. Used only when the match is staged; omit it to stage on a fresh canvas. Pass the returned canvas_id to fema_dataframe_describe for the table columns, then to fema_dataframe_query for SQL.",
      "type": "string",
      "pattern": "^[A-Za-z0-9_-]{10}$"
    }
  },
  "required": [
    "state",
    "limit",
    "offset"
  ],
  "additionalProperties": false
}
view source ↗

fema_dataframe_query

Run a read-only SQL SELECT against a DataCanvas table staged by fema_search_nfip. Enables aggregation, GROUP BY, SUM/COUNT, time-series, and filtered analysis over the full NFIP claims result without re-fetching from the API. Call fema_dataframe_describe first to get the exact table name and column names needed for valid SQL. Only SELECT statements are allowed — DDL, DML, COPY, and file-reading functions are blocked. A response holds at most 100,000 characters of rows; when it holds fewer rows than the query returned, truncated and shown are set and the notice says how to page with OFFSET.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "fema_dataframe_query",
    "arguments": {
      "canvas_id": "<canvas_id>",
      "query": "<query>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "canvas_id": {
      "type": "string",
      "pattern": "^[A-Za-z0-9_-]{10}$",
      "description": "Canvas ID from the fema_search_nfip response (the canvas_id field)."
    },
    "query": {
      "type": "string",
      "description": "SQL SELECT statement to run against the staged table. Use the table name from fema_dataframe_describe. Results are capped at the canvas row limit and at 100,000 characters of rows; append LIMIT/OFFSET, under an ORDER BY, to page through additional rows, or aggregate (GROUP BY, COUNT, SUM) to summarize the full set in one query. Example: \"SELECT year_of_loss, COUNT(*) AS claims, SUM(amount_paid_building) AS total_building_paid FROM df_nfip_abc123 GROUP BY year_of_loss ORDER BY year_of_loss\""
    }
  },
  "required": [
    "canvas_id",
    "query"
  ],
  "additionalProperties": false
}
view source ↗

fema_dataframe_describe

List tables and column schemas on a DataCanvas staged by fema_search_nfip. Call this before fema_dataframe_query to discover the exact table name, column names, and DuckDB data types needed to write valid SQL. Row count reflects what was actually staged — check truncated in the fema_search_nfip response to know whether the canvas holds the full matching set.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "fema_dataframe_describe",
    "arguments": {
      "canvas_id": "<canvas_id>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "canvas_id": {
      "type": "string",
      "pattern": "^[A-Za-z0-9_-]{10}$",
      "description": "Canvas ID from the fema_search_nfip response."
    }
  },
  "required": [
    "canvas_id"
  ],
  "additionalProperties": false
}
view source ↗

fema_query_dataset

open-world

Generic OData query against any dataset in the OpenFEMA dataset catalog — the escape hatch for datasets the convenience tools do not cover (e.g., NfipPolicies, IndividualAssistanceHousingRegistrantsLargeDisasters, FemaWebDeclarationAreas, PublicAssistanceApplicants). Accepts raw OData filter, select, orderby, and pagination parameters. For NfipPolicies, use propertyState (not state) for the state and reportedZipCode for the ZIP code — it has no countyCode; always include a ZIP filter to avoid timeout. The dataset name must match the exact OpenFEMA entity name (case-sensitive, e.g., NfipClaims). Names missing from the catalog return an unknown_dataset error; a listed dataset OpenFEMA does not serve through the API, or a retired version, returns dataset_not_served. A page holds at most 100,000 characters of records; when records remain past it, next_offset is the offset to continue from.

read
invocation
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "fema_query_dataset",
    "arguments": {
      "dataset": "<dataset>"
    }
  }
}
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "dataset": {
      "type": "string",
      "minLength": 1,
      "description": "OpenFEMA dataset entity name (case-sensitive, e.g., NfipPolicies, FemaWebDeclarationAreas, PublicAssistanceApplicants)."
    },
    "filter": {
      "description": "OData $filter expression (e.g., \"state eq 'TX' and declarationDate ge '2024-01-01T00:00:00.000Z'\"). String values in single quotes. For a list of values use field in ('a','b'): OpenFEMA rejects filters nested more than 100 levels deep (about 90 chained and/or comparisons) and request URLs past about 8,900 characters.",
      "type": "string"
    },
    "select": {
      "description": "Comma-separated field names to return (e.g., \"disasterNumber,state,declarationDate\").",
      "type": "string"
    },
    "orderby": {
      "description": "OData $orderby expression (e.g., \"declarationDate desc\").",
      "type": "string"
    },
    "limit": {
      "default": 100,
      "description": "Maximum records to return (1–10000, default 100). A page also stops at 100,000 characters of records; when either bound leaves matching records out, next_offset names the offset to continue from.",
      "type": "integer",
      "minimum": 1,
      "maximum": 10000
    },
    "offset": {
      "default": 0,
      "description": "Pagination offset (default 0). Pass the previous page’s next_offset to continue.",
      "type": "integer",
      "minimum": 0,
      "maximum": 9007199254740991
    }
  },
  "required": [
    "dataset",
    "limit",
    "offset"
  ],
  "additionalProperties": false
}
view source ↗

disabled 1

fema_dataframe_drop

Permanently remove one table or view from a DataCanvas staged by fema_search_nfip. This only deletes staged analytical data; it never changes FEMA source data. Call fema_dataframe_describe first to get the exact table or view name.

disabledwould be destructive

Disabled. DataCanvas deletion is disabled in this deployment.

FEMA_ENABLE_CANVAS_DROP=true
schema
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "canvas_id": {
      "type": "string",
      "pattern": "^[A-Za-z0-9_-]{10}$",
      "description": "Canvas ID from the fema_search_nfip response."
    },
    "table_name": {
      "type": "string",
      "description": "Exact table or view name returned by fema_dataframe_describe."
    }
  },
  "required": [
    "canvas_id",
    "table_name"
  ],
  "additionalProperties": false
}
view source ↗

Resources

1

Summary for a specific FEMA disaster declaration — title, state, incident type, programs observed (IA/PA/HM), incident period, and fetched designated area count (up to 500 rows), with disclosure when coverage is partial. Read-once context injection for agents already holding a disaster number.

uri fema://disaster/{disasterNumber}