Skip to main content
Glama
claude-marie

pathways-mcp-server

by claude-marie
README.md
# Pathways MCP Server

A [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) server that exposes the **Pathways** health segmentation platform as structured tools. Pathways provides woman-centered data and insights to help global health organizations design targeted interventions.

## What it does

This server connects to the Pathways Strapi CMS API and provides tools that let Claude (or any MCP client) explore population segmentation data:

| Tool | Description |
|---|---|
| `list_segmentations` | Discover available country studies (Senegal, Kenya, Nigeria, etc.) |
| `get_segmentation` | Full details and segments for a specific study |
| `list_segments` | Filter segments by vulnerability level or stratum (urban/rural) |
| `get_segment_profile` | Comprehensive "who are these women?" view with metrics by theme/domain |
| `get_segment_metrics` | Quantitative indicators for a segment, filterable by health theme |
| `search_variables` | Search indicators by name, theme, domain, or data type |
| `list_themes_and_domains` | Reference list of health themes and vulnerability domains |
| `list_regions` | Sub-national regions for a country |
| `get_geographic_distribution` | Geographic distribution of segments across regions |

**Example query this enables**: _"What is the best way to reach out to women in R4 in Tambacounda to improve family planning outcomes?"_

## Prerequisites

- Python 3.10+
- A Pathways API token (read-only Bearer token for the Strapi CMS)

## Installation

```bash
cd pathways-mcp-server
python3 -m venv .venv
source .venv/bin/activate
pip install -e .
```

## Configuration

Copy the example env file and add your token:

```bash
cp .env.example .env
# Edit .env and set PATHWAYS_API_TOKEN
```

| Variable | Default | Description |
|---|---|---|
| `PATHWAYS_API_TOKEN` | _(required)_ | Strapi read-only API token |
| `PATHWAYS_API_URL` | `https://api.staging.withpathways.org` | Strapi API base URL |

## Running standalone

```bash
PATHWAYS_API_TOKEN=<your-token> python -m pathways_mcp.server
```

The server communicates over stdio using the MCP protocol. To test interactively, use the MCP Inspector:

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

## Using with Claude

### Claude Desktop

Add this to `~/Library/Application Support/Claude/claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "pathways": {
      "command": "<path-to-repo>/pathways-mcp-server/.venv/bin/python",
      "args": ["-m", "pathways_mcp.server"],
      "cwd": "<path-to-repo>/pathways-mcp-server",
      "env": {
        "PATHWAYS_API_TOKEN": "<your-token>",
        "PATHWAYS_API_URL": "https://api.staging.withpathways.org"
      }
    }
  }
}
```

## Project structure

```
pathways-mcp-server/
├── pyproject.toml
├── requirements.txt
├── .env.example
├── .gitignore
├── README.md
└── src/
    └── pathways_mcp/
        ├── __init__.py
        ├── __main__.py         # python -m entry point
        ├── server.py           # FastMCP server + tool registration
        ├── api.py              # Strapi API client (httpx, auth, pagination)
        └── tools/
            ├── __init__.py
            ├── segmentations.py
            ├── segments.py
            ├── metrics.py
            ├── variables.py
            ├── reference.py
            └── geography.py
```

## The Pathways Data Model

Understanding this hierarchy is key to understanding all the tools.

```
Geography (e.g., Senegal)
  └── Segmentation (e.g., SEN_2019DHS8_v1 — "Senegal 2019 DHS study")
        ├── Segments (e.g., R1, R2, R3, R4, U1, U2... — distinct groups of women)
        │     └── Metrics (a segment × variable pair = one data point)
        │
        └── Variables (the indicators measured, e.g., "Modern contraceptive use")
              ├── linked to Themes   → describe Health Outcomes
              └── linked to Domains  → describe Vulnerability Factors

Themes  = categories of Health Outcomes  (e.g., Maternal Health, Nutrition)
Domains = categories of Vulnerability Factors (e.g., Household Economics, Social Support)

Regions = sub-national administrative areas within a Geography
Geographic Distributions = what % of each region's population belongs to each segment
```

## The API Client (`api.py`)

This is the most important infrastructure file. It handles all HTTP communication.

### The `StrapiClient` class

When instantiated, it reads two environment variables — both are now required:
- `PATHWAYS_API_TOKEN` — the Bearer token (raises an error immediately if missing)
- `PATHWAYS_API_URL` — the base URL (raises an error immediately if missing; there is no default fallback)

```python
self._headers = {"Authorization": f"Bearer {token}"}
```

Every request includes this header, which Strapi uses to verify access.

### `fetch_collection` — one page of results

This is the main method. It:
1. Builds the query string parameters (filters, pagination, populate, fields)
2. Makes an async HTTP GET request using `httpx`
3. Returns the parsed JSON

```python
async with httpx.AsyncClient(...) as client:
    resp = await client.get(url, params=params)
    resp.raise_for_status()
    return resp.json()
```

`resp.raise_for_status()` checks the HTTP status code. If the server returned a 4xx or 5xx error, it raises a Python exception immediately rather than silently returning broken data. Before calling that, the code also checks for specific codes to give actionable error messages:

```python
if resp.status_code == 403:
    raise RuntimeError("Access denied... Check your PATHWAYS_API_TOKEN.")
if resp.status_code == 404:
    raise RuntimeError(f"Endpoint '{endpoint}' not found on the Strapi API.")
```

A 403 means the token is wrong or expired. A 404 means the endpoint path itself doesn't exist — usually a typo in the collection name.

### `fetch_all` — auto-pagination

Some tools need all records, not just a page. `fetch_all` calls `fetch_collection` in a loop, advancing the page number on each iteration:

```python
while True:
    result = await self.fetch_collection(..., page=page, ...)
    data = result.get("data", [])
    all_data.extend(data)

    page_count = result["meta"]["pagination"]["pageCount"]

    if page >= page_count or len(all_data) >= max_records:
        break
    page += 1
```

Strapi tells you how many pages exist in the `meta.pagination.pageCount` field. The loop stops when you've fetched the last page, or when you've hit `max_records` (a safety cap to prevent fetching thousands of records if the data grows unexpectedly).

This is used by `get_segment_profile` for both its metrics fetch and variables fetch — both can be very large datasets.

### The `populate` parameter — what Strapi relations are

In relational databases, a **foreign key** is when one table stores only the ID of a record from another table — not the full data. For example, a `metrics` record stores a `variable_id: 42` rather than copying all the variable's fields.

Strapi works the same way. By default, when you fetch a metric, you get:

```json
{ "id": 1, "percentage": 0.34, "variable": null }
```

To get the actual variable data embedded in the response, you pass `populate`:

```python
populate=["variable", "categorical_level"]
```

Strapi then does a database JOIN behind the scenes and returns:

```json
{
  "id": 1,
  "percentage": 0.34,
  "variable": { "code": "fp.mod.use", "name_en": "Modern FP use", ... },
  "categorical_level": { "name_en": "Yes", ... }
}
```

Without populate, the tool code would have to make a separate API call for every variable — which would be hundreds of extra requests. Populate fetches them all in one go.

### The singleton pattern

```python
_client: StrapiClient | None = None

def get_client() -> StrapiClient:
    global _client
    if _client is None:
        _client = StrapiClient()
    return _client
```

The client is created once and reused across all tool calls. This avoids re-reading environment variables and re-allocating memory on every request.

### `RESPONSE_CHAR_LIMIT`

Set to 25,000 characters. All tool responses are truncated to this length before being returned to Claude:

```python
return json.dumps(output, indent=2)[:RESPONSE_CHAR_LIMIT]
```

This is a practical guard: MCP responses that are too large can cause problems for the AI client or hit context limits.

---

TDQS

A4.1/5.0

Scored across 5 tools

Disambiguation5/5

Each tool targets a distinct operation: listing segmentations, getting segmentation details, listing segments, getting segment profile, and geographic distribution. Descriptions clearly differentiate them, preventing functional overlap.

Naming Consistency5/5

All tools follow a consistent 'verb_noun' pattern with 'list_' for enumeration and 'get_' for retrieval. No mixing of conventions.

Tool Count5/5

5 tools is well-scoped for exploring population segmentation data. It covers the necessary query operations without being excessive or too sparse.

Completeness3/5

Core read operations are present, but the description of 'get_segment_profile' references a non-existent 'get_segment_metrics' tool for sample baselines, indicating a gap. Additionally, there is no tool to list regions, though geographic distribution relies on region codes.

Maintenance

ActivityInactive
ResponsivenessNo issues