Skip to main content
Glama
README.md
# education-to-workforce-mcp

An MCP server over the Education-to-Workforce (E-W) Indicator Framework — 20
essential questions and 99 indicators covering how people progress from early
education through the workforce, for the US, states, counties, and school
districts.

The **framework** is [Mathematica's](https://educationtoworkforce.org/), written
with the Bill & Melinda Gates Foundation. The **data tool** that compiles federal
data against it is the [Urban Institute's](https://apps.urban.org/features/education-workforce-framework-data/).
This server ships a compiled, pinned snapshot of that data — 11.8M observations
in ~24 MB of Parquet — so it answers locally and makes no network calls.

No API key required. Read-only.

## Prerequisites

- Python 3.11+
- [uv](https://docs.astral.sh/uv/getting-started/installation/)

## Installation

```bash
git clone https://github.com/UrbanInstitute/education-to-workforce-mcp.git
cd education-to-workforce-mcp
uv sync
```

## Tools

| Tool | Purpose |
|------|---------|
| `search` | Browse essential questions, or find an indicator/metric by concept |
| `describe` | Explain one metric, indicator, essential question, or disaggregate — or the framework itself |
| `resolve_place` | Find the state/county/district geoid used by the data, from a name |
| `get_data` | Fetch metric values for places, or rank places on a metric |

**Typical workflow**: `search` → `describe` → `resolve_place` → `get_data`.

## Tool reference

### search

Browse the framework, or find a metric by concept.

| Parameter | Type | Description |
|-----------|------|-------------|
| `query` | string (optional) | A concept — `"gifted"`, `"student debt"`, `"chronic absence"`. Omit to list the 20 essential questions |

### describe

Explain one metric, indicator, essential question, disaggregate, or the framework overview.

| Parameter | Type | Description |
|-----------|------|-------------|
| `target` | string | `"m47"` (metric), `"i5"` (indicator), `"eq12"` (essential question), `"disaggregates"`, or `"framework"` |

### resolve_place

Find the geoid for a state, county, or school district, from a name.

| Parameter | Type | Description |
|-----------|------|-------------|
| `name` | string | Place name — `"Cook County"`, `"Illinois"`, `"Chicago Public Schools"` |
| `geo_level` | string (optional) | `"state"`, `"county"`, or `"district"` |

### get_data

Fetch metric values for places, or rank places on a metric.

| Parameter | Type | Description |
|-----------|------|-------------|
| `metric_ids` | string | Comma-separated ids: `"47"` or `"47,51"` |
| `geo_level` | string | `"national"`, `"state"`, `"county"`, or `"district"` |
| `geo_ids` | string (optional) | Comma-separated geoids from `resolve_place` |
| `state` | string (optional) | 2-digit state FIPS — every place at `geo_level` in that state |
| `years` | string (optional) | Comma-separated years: `"2022"` or `"2013,2022"`. Omit for all |
| `disaggregate` | string (optional) | A dimension (`"race"`, `"gender"`, `"disability"`, `"income"`) or one group code (`"d1_hispanic"`) |
| `rank` | string (optional) | `"highest"` or `"lowest"` to rank places instead of listing values |
| `limit` | integer (default: 10) | How many places to return when ranking |

---

## Running it

### MCP Inspector (interactive testing)

```bash
uv run mcp dev src/ew_mcp/server.py
```

Opens a browser at `http://localhost:6274` — connect, open **Tools**, and run
any tool with parameters.

### Claude Desktop / Claude Code / VS Code / Copilot CLI

```json
{
  "mcpServers": {
    "ew-mcp": {
      "command": "uv",
      "args": ["run", "--directory", "/absolute/path/to/education-to-workforce-mcp", "ew-mcp"]
    }
  }
}
```

| Client | Where it goes |
|---|---|
| Claude Desktop | `claude_desktop_config.json` — macOS: `~/Library/Application Support/Claude/`; Windows: `%APPDATA%\Claude\` |
| Claude Code | `.claude/settings.json`, or `claude mcp add ew-mcp -- uv run --directory /absolute/path/to/education-to-workforce-mcp NAME` |
| VS Code (Copilot) | `.vscode/settings.json`, nested as `{"mcp": {"servers": {...}}}` |

### stdio (direct)

```bash
uv run ew-mcp
```

### Streamable HTTP (hosted)

```bash
MCP_TRANSPORT=streamable-http PORT=8080 uv run ew-mcp
```

MCP is served at `POST /mcp`; `GET /health` is a plain unauthenticated health
check for a load balancer or orchestrator.

| Variable | Default | Purpose |
|---|---|---|
| `MCP_TRANSPORT` | `stdio` | `stdio` or `streamable-http` |
| `PORT` | `8080` | Listen port |
| `MCP_HOST` | `0.0.0.0` | Bind address |
| `MCP_ALLOWED_HOSTS` | _(unset)_ | Comma-separated `Host` allow-list |
| `MCP_ALLOWED_ORIGINS` | _(unset)_ | Comma-separated `Origin` allow-list |

**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/ew](https://educationdata.urban.org/mcp/ew)

## Tests

```bash
uv run pytest -q
```

Tests run against the compiled data in `src/ew_mcp/data/` — no network. If the
store is absent they skip rather than fail.

TDQS

A4.3/5.0

Scored across 4 tools

Disambiguation4/5

The four tools form a clear pipeline: search finds metrics by concept, describe explains a specific metric/indicator/essential question, resolve_place turns a name into a geoid, and get_data fetches values. There is mild overlap between search and describe (both surface framework content), but the args and outputs make the boundary clear in practice.

Naming Consistency3/5

Two tools use bare verbs (search, describe) while two use a verb_noun pattern (resolve_place, get_data). Individually readable and unambiguous, but the conventions are mixed rather than a single predictable pattern.

Tool Count4/5

Four tools is a tight, well-scoped read-only set for a data framework: discover, explain, geocode, fetch. Nothing feels redundant, though it sits near the thin end where one more operation (e.g. an explicit metric enumeration) might round it out.

Completeness4/5

The surface covers the full read lifecycle for the domain — find a metric, understand it, resolve a place, retrieve or rank values, including automatic benchmarks and disaggregation options. Being a read-only data server, missing write ops are not gaps; the main absence is a direct way to enumerate all available metrics/geographies without a query.

Maintenance

ActivityMaintained
ResponsivenessNo issues