edp-mcp
OfficialClick on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@edp-mcp Show total public school enrollment by state for 2022."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
education-data-mcp
An MCP server over the Urban Institute's Education Data Portal — harmonized federal education data (CCD, CRDC, IPEDS, EdFacts, SAIPE, College Scorecard, MEPS, PSEO) for schools, school districts, and colleges.
Queries the live EDP API. No API key required. Read-only.
Prerequisites
Python 3.11+
Related MCP server: data-utah
Installation
git clone https://github.com/UrbanInstitute/education-data-mcp.git
cd education-data-mcp
uv syncTools
Six tools, following a discovery-first workflow:
Tool | Purpose |
| Find available datasets by level, source, topic, or keyword |
| Inspect a dataset's variables, filters, and coded value formats |
| Fetch raw data records with human-readable labels (by default) |
| Get aggregated statistics (counts, sums, averages) by group |
| Translate between codes and labels (e.g., FIPS 6 = California) |
| Find a school/district/college ID by name (e.g., "Harvard" → unitid 166027) |
Typical workflow: search_datasets → describe_dataset → get_summary or get_data, with lookup_codes and resolve_entity as needed.
Tool reference
search_datasets
Find available datasets.
Parameter | Type | Description |
| string (optional) |
|
| string (optional) |
|
| string (optional) |
|
| string (optional) | Keyword to match against dataset URLs and descriptions |
describe_dataset
Inspect a dataset's variables, filters, and coded value formats.
Parameter | Type | Description |
| string | Dataset path, raw template or filled in: |
| boolean (default: false) | Only show variables usable as query filters |
get_data
Fetch education data. Returns human-readable labels by default.
Parameter | Type | Description |
| string | A path with every |
| string (optional) | Query filters: |
| string (optional) | Columns to return: |
| boolean (default: true) | Decode coded values to human-readable labels |
| boolean (default: false) | Return a small labelled sample instead of a complete result |
get_summary
Get aggregated statistics from the Education Data Portal. Use for counts, totals, or averages across groups. Much faster than fetching raw data and computing yourself.
Parameter | Type | Description |
| string | The dataset path WITHOUT |
| string | Variable to aggregate: |
| string | Statistic: |
| string | Grouping variables (comma-separated): |
| string (optional) | Query filters: |
Results are always grouped by year in addition to the specified groupings.
Example: Total enrollment by state: get_summary(path="schools/ccd/enrollment", var="enrollment", stat="sum", by="fips")
lookup_codes
Look up code-to-label mappings. Use to find filter values (e.g., "California" = FIPS 6) or understand coded results.
Parameter | Type | Description |
| string | Format name from |
| string (optional) | Comma-separated codes: |
Coded values are field-specific. Code meanings come from each variable's API metadata, not a fixed table, and get_data/get_summary decode them to labels automatically. Negative codes often mean -1 = Missing/not reported, -2 = Not applicable, -3 = Suppressed for privacy — but not always: for grade, -1 means Pre-K (0 = Kindergarten). Use lookup_codes to see a variable's full, authoritative code list.
resolve_entity
Resolve a school, district, or college name to its ID for filtering. Use when you need to filter data by a specific entity.
Parameter | Type | Description |
| string | Name to search for (case-insensitive substring match) |
| string |
|
| integer (optional) | State FIPS code (e.g., 6 for California). Required for schools and districts |
| integer (default: 15) | Maximum number of results to return |
Returns matching entities with their IDs (ncessch for schools, leaid for districts, unitid for colleges) for use in get_data filters.
Running it
MCP Inspector (interactive testing)
uv run mcp dev src/edp_mcp/server.pyOpens a browser at http://localhost:6274 — connect, open Tools, and run
any tool with parameters.
Claude Desktop / Claude Code / VS Code / Copilot CLI
{
"mcpServers": {
"edp-mcp": {
"command": "uv",
"args": ["run", "--directory", "/absolute/path/to/education-data-mcp", "edp-mcp"]
}
}
}Client | Where it goes |
Claude Desktop |
|
Claude Code |
|
VS Code (Copilot) |
|
stdio (direct)
uv run edp-mcpStreamable HTTP (hosted)
MCP_TRANSPORT=streamable-http PORT=8080 uv run edp-mcpMCP is served at POST /mcp; GET /health is a plain unauthenticated health
check for a load balancer or orchestrator.
Variable | Default | Purpose |
|
|
|
|
| Listen port |
|
| Bind address |
| (unset) | Comma-separated |
| (unset) | Comma-separated |
Set MCP_ALLOWED_HOSTS to the public hostname before exposing this beyond a
private network. Both are unset by default, which leaves the SDK's
DNS-rebinding protection off — setting either turns it on. Note that enabling it
with an allow-list that omits the real hostname rejects every request.
Hosted
Also available as a hosted streamable-HTTP server: https://educationdata.urban.org/mcp/edp
Tests
uv run pytest -qTests use saved API fixtures (tests/fixtures/) with mocked HTTP, so no network
is needed. scripts/smoke_live.py exercises the real API end to end.
Available Tools
6 toolsdescribe_datasetDescribe datasetARead-onlyIdempotent
Inspect one dataset: every variable with its definition, data type, coded-value format, and whether it is filterable. Also returns the years covered, the source description, and equivalent R/Stata code.
This is where the [FILTER] marks come from — filtering on anything else returns UNFILTERED data rather than an error.
Args: path: Dataset path, template or filled in — both resolve to the same dataset: "schools/ccd/enrollment/{year}/{grade}/race/" or "schools/ccd/enrollment/2022/grade-99/race/" filterable_only: Show only the variables usable as filters
Inputs: path (required), filterable_only. Returns: result
get_dataGet data recordsARead-onlyIdempotent
Fetch raw data records from one dataset path. For totals or averages across groups use get_summary instead — it aggregates server-side.
Results are COMPLETE or refused, never truncated, so counting and averaging over the rows returned is valid. A query matching over 10,000 rows is refused with its true size and how to narrow it. When one is too big, aggregate with get_summary, narrow to a state or district, or hand the user the bulk CSV link from the refusal or the R/Stata snippet from describe_dataset — for whole-country or long multi-year analysis those beat paging through calls.
COST: this hits a live API and a broad query can take 30s+. Filter before fetching rather than issuing many wide calls in parallel.
Args: path: A dataset path from search_datasets with every {placeholder} filled in — "schools/ccd/enrollment/2022/grade-99/race/". One year per call; a leading "/api/v1/" is optional. filters: "fips=11&charter=1". Only fields marked [FILTER] in describe_dataset work; others are rejected here rather than silently returning unfiltered data. Also takes "ordering=" to rank server-side: "ordering=-enrollment" largest first, "ordering=enrollment" smallest. Ranking sorts the whole result before paging, so a ranked query answers where an unranked one is refused as too large. fields: Comma-separated columns — "ncessch,school_name,enrollment". ALWAYS PASS THIS. These tables are 50-96 columns wide; requesting only what you need is typically an 8-17x reduction and is usually the difference between an answer and a refusal. add_labels: Decode coded values to labels (default True). Decoding is per variable, so meanings are FIELD-SPECIFIC — trust the decoded label over any assumption about what a raw code means. preview: Return a small labelled SAMPLE rather than a complete result. The rows are the API's first N by ID, NOT a random sample — never count, rank or average over them.
Inputs: path (required), fields, filters, preview, +1 more. Returns: result
get_summarySummarize dataARead-onlyIdempotent
Aggregate a dataset server-side: counts, sums, averages by group. Use this rather than get_data whenever the question is about totals ("how many schools per state", "total enrollment by race").
EVERY year comes back in one call — results are grouped by year on top of
by, so a trend needs one call, never one per year. Filter years only to
narrow a large result. Results are COMPLETE or refused. The API cannot rank
aggregates, and refuses groupings that produce too many groups (by=leaid
nationally, for instance).
var and by are drawn from two different pools — describe_dataset lists
both for any dataset. Guessing costs a slow round trip; reading them does not.
COST: this hits a live API and a broad aggregation can take 30s+.
NO COUNTY AGGREGATION for schools or districts. Several datasets return
county_code as a column, but it is neither filterable nor groupable, so
county totals cannot be computed here — fetch the rows with get_data and
aggregate them yourself, or say the portal does not support it. Do not
retry with county in by.
Args: path: The SUMMARY path — no year, no {placeholders}, e.g. "schools/ccd/enrollment". describe_dataset prints the right one for any dataset. Some differ from the data path ("college-university/ipeds/fall-enrollment/race"). var: The measure to aggregate — a numeric, non-filter variable stat: sum, count, avg, min, max, median, stddev, or variance by: Comma-separated groupings — "fips", "fips,race". May also use variables from the source's directory ("school_level", "sector"). filters: "fips=6" — omit year to get every year; "year=2018,2019,2020" to restrict the range
Inputs: by (required), var (required), path (required), stat (required), +1 more. Returns: result
lookup_codesLook up coded valuesARead-onlyIdempotent
Look up the code-to-label mapping for a coded variable — mainly to find a filter value for get_data ("California" -> fips=6).
Args: format_name: The "Format" shown for a variable in describe_dataset — "fips", "race", "sex", "school_level", "charter", … codes: Comma-separated codes to look up — "6,48". Omit for all values.
Inputs: format_name (required), codes. Returns: result
resolve_entityResolve entity name to IDARead-onlyIdempotent
Resolve a school/district/college NAME to the ID used in get_data filters.
Returns CANDIDATES with their city and level, not one authoritative answer — names repeat, so the right row is the one whose city and level fit what the user meant. Type words ("Elementary", "High", "ISD") are ignored when matching, which is what lets natural phrasings reach the directory's abbreviations ("Lakewood Elementary" finds "LAKEWOOD EL").
Schools and districts require fips: without it this asks which state rather
than guessing, since "Homestead High" exists in CA, WI, IN and FL.
COST: downloads a whole state's directory, so the first call for a state often takes 10-30s and later ones are cached. Resolve names in the same state together.
Args: name: Name to search for; distinctive words matter most. entity_type: "school", "district", or "college" fips: State FIPS code (6 = California). Required for schools and districts. See lookup_codes(format_name="fips"). max_results: Maximum candidates to return (default 15)
Inputs: name (required), entity_type (required), fips, max_results. Returns: result
search_datasetsSearch datasetsARead-onlyIdempotent
Find datasets in the Education Data Portal. Returns each one's path template, description and years available; pass a template to describe_dataset for its variables and its summary call. All arguments are optional filters.
NOT COVERED by this portal at all: NAEP scores (see the NAEP Data Explorer), teacher salaries (BLS), private K-12 (limited; CRDC covers some), and curriculum data. Say so rather than searching repeatedly.
Args: level: "schools", "school-districts", or "college-university" source: "ccd", "ipeds", "crdc", "edfacts", "saipe", "scorecard", … topic: "enrollment", "directory", "finance", "discipline", … search: Words matched per-token against dataset paths and descriptions, not as a phrase — so drop specific terms like grade numbers ("8th") if a query returns nothing, since datasets rarely spell those out verbatim.
Inputs: level, topic, search, source. Returns: result
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 6 tools
Each tool has a clearly distinct role: discover datasets, inspect schema, fetch raw rows, aggregate, decode codes, and resolve names to IDs. The potentially confusing get_data/get_summary pair is explicitly separated and cross-referenced.
All tool names follow a consistent snake_case verb_noun convention: describe_dataset, search_datasets, get_data, get_summary, lookup_codes, resolve_entity. No mixed casing or vague generic verbs appear.
Six tools form a compact, non-redundant set that covers the read-only data-portal workflow well. Each tool earns its place, and the count is well within the ideal range for an agent to navigate comfortably.
The tool surface covers the full query lifecycle: find datasets, inspect variables and filter fields, resolve codes and entity names, fetch raw data, and aggregate server-side. Known unsupported operations are explicitly documented with workarounds, leaving no obvious dead ends.
Maintenance
Related MCP Connectors
Education Data MCP — US K-12 schools, districts, funding and child poverty.
Search and query government open-data portals (Socrata SODA API).
Access US federal award, recipient, agency, and spending analytics data from USAspending.gov.
Search, compare, and analyze U.S. college data — costs, earnings, programs, and outcomes.
Related MCP Servers
- AlicenseCqualityDmaintenanceProvides access to Urban Institute's Education Data API through Claude, enabling users to query and analyze detailed educational data from schools, districts, and universities.22MIT
- AlicenseNot gradedqualityCmaintenanceEnables searching and querying Utah Open Data datasets via the Socrata SoQL API, including datasets, metadata, and row-level data retrieval.2 npmMIT
- AlicenseNot gradedqualityDmaintenanceFederated MCP server that routes K-12 education data requests to public sources like Urban Institute, EDC/Zelma, NAEP, and MA E2C, handling comparability and provenance automatically.MIT
- AlicenseBqualityBmaintenanceEnables discovery and summarization of French education data, including school directories, geocoded establishments, IPS, Parcoursup, and exam datasets.5MIT