Skip to main content
Glama

SourceVahti

Reliable AI access to public health, government and research data.

SourceVahti is a Model Context Protocol (MCP) server that makes statistical definitions explicit before returning a number. Version 0.3 exposes four source-specific adapters and three read-only tools over local stdio:

  • search_indicators

  • get_observations

  • get_latest_observation

The product is the ambiguity guard. “Mortality rate” may mean a crude rate or one of several age-standardised rates, and two publishers may produce valid but non-interchangeable values. SourceVahti returns a structured ambiguous_indicator error with the valid candidates instead of silently choosing one.

Current sources

Source

Narrow supported scope

Role

Finnish Cancer Registry

Female Finnish lung/tracheal cancer mortality, 2024

Exact registry acceptance case

NORDCAN 9.6

Nordic lung-cancer mortality by country and rate definition, 2023–2024

Nordic cancer outcomes

WHO Global Health Observatory

Current tobacco-use prevalence, age 15+, Nordic countries, 2000–2025

Modelled risk-factor context

Eurostat

Lung-cancer mortality, C33-C34, Nordic countries, 2011–2023

Independent mortality source

All bundled data are frozen, reviewed snapshots. The MCP server makes no upstream requests during a tool call.

Finnish Cancer Registry

The snapshot contains the 2024 female lung and tracheal cancer mortality export. The public application exposes three rates for the same population and year:

Rate definition

Standard population

Value per 100,000 person-years

Crude

None

29.96

Age-standardised, world

World standard population (1966)

9.04

Age-standardised, Finland

Finland population 2014

23.28

It was retrieved on 2026-07-30 from the Finnish Cancer Registry statistics application. The latest official year was 2024 and the release date was 2026-04-24.

NORDCAN

The NORDCAN snapshot contains version 9.6 female lung-cancer mortality tables for 2023–2024. It covers Denmark, Finland, Greenland, Iceland, Norway, Sweden, the Faroe Islands' latest 2023 observation and two Nordic aggregates. Every geography retains crude, World, Nordic 2000, European 1976 and European 2013 rate definitions. NORDCAN 9.6 was released on 2026-06-30. See the database and statistical definitions.

WHO GHO

The WHO adapter contains 150 observations: female, male and both-sex modelled estimates for five Nordic countries at ten published years from 2000 through 2025. The indicator is current tobacco-use prevalence among people aged 15 years and over, standardised to the WHO standard population. Each value retains its 95% uncertainty bounds, modelled/projected status and source note.

This is contextual risk-factor data, not a cancer outcome or causal estimate. See the WHO indicator metadata.

Eurostat

The Eurostat adapter contains 195 annual observations for Denmark, Finland, Iceland, Norway and Sweden: female, male and both-sex rates from 2011 through 2023. It selects dataset HLTH_CD_ASDR2, unit RT, all ages and cause C33_C34. Rates are directly standardised to the European Standard Population 2013 and retain Eurostat status flags.

See the Eurostat causes-of-death metadata. Eurostat and NORDCAN are separate publishers; matching definitions make a source comparison possible but do not make their observations identical.

Related MCP server: Kolada MCP Server

Install and run

Requirements: Python 3.11 or newer and uv.

git clone https://github.com/heidihelena/sourcevahti.git
cd sourcevahti
uv sync --locked --all-extras --dev
uv run sourcevahti

sourcevahti starts a local stdio server. It should stay silent and wait for an MCP host because stdout carries the protocol.

For MCP Inspector:

uv run mcp dev src/sourcevahti/server.py:mcp

Generic MCP host configuration:

{
  "mcpServers": {
    "sourcevahti": {
      "command": "uv",
      "args": [
        "--directory",
        "/absolute/path/to/sourcevahti",
        "run",
        "sourcevahti"
      ]
    }
  }
}

Use an absolute path because an MCP host may start the server from a different working directory.

Examples

First search without selecting a Finnish rate definition:

{
  "query": "female lung cancer mortality rate",
  "source": "finnish_cancer_registry",
  "sex": "female",
  "unit": "per_100_000_person_years"
}

search_indicators returns three candidates. Retry get_latest_observation with the required definition:

{
  "query": "female lung cancer mortality rate",
  "source": "finnish_cancer_registry",
  "sex": "female",
  "rate_type": "age_standardised_finland_2014",
  "unit": "per_100_000_person_years"
}

The result includes the native source code, cancer definition, year, value, unit, sex, geography, rate type, standard population, observation status, citation URL, source version and release date, retrieval date and snapshot ID. The acceptance value is 23.28 per 100,000 person-years in 2024.

WHO risk-factor context:

{
  "query": "female Finland current tobacco use prevalence",
  "source": "who_gho",
  "sex": "female",
  "geography": "Finland",
  "unit": "percent"
}

This returns the 2025 projected estimate, 17.1%, with a 14.0–20.2% uncertainty interval and the WHO indicator definition.

Eurostat source comparison:

{
  "query": "female Finland lung cancer mortality European 2013",
  "source": "eurostat",
  "sex": "female",
  "geography": "Finland"
}

If source is omitted, a European-2013-standardised Nordic lung-cancer mortality query may correctly remain ambiguous between NORDCAN and Eurostat.

Tool contracts

search_indicators

Lexically searches indicator names, descriptions, definitions, native codes, health topic, cancer codes, source, geography, sex, unit, rate type and standard population. Recognised dimensional terms in the query become constraints. Explicit filters are validated and cannot contradict those terms. Epidemiologically distinct series are never collapsed.

get_observations

Returns one resolved series, optionally bounded by inclusive start_year and end_year. Supply an exact indicator_id, or a query plus sufficient dimensions to leave exactly one candidate.

get_latest_observation

Returns the highest-year observation in one resolved series. In addition to the shared provenance fields, an observation may include uncertainty bounds and a publisher note.

Supported canonical values:

  • source: finnish_cancer_registry, nordcan, who_gho, eurostat

  • sex: female, male, all

  • unit: per_100_000_person_years, percent

  • rate_type: crude, age_standardised_world, age_standardised_world_1966, age_standardised_finland_2014, age_standardised_nordic_2000, age_standardised_europe_1976, age_standardised_europe_2013, age_standardised_who

count remains reserved for future count indicators. Source-specific geography and semantic constraints are validated. Unknown categories and dimensional mismatches are rejected.

Architecture

MCP typed tools
    └── SourceCatalog
          ├── cross-source query resolution and ambiguity checks
          ├── FinnishCancerRegistryAdapter
          ├── NordcanAdapter
          ├── WhoGhoAdapter
          └── EurostatAdapter
                └── frozen source snapshots
                      └── strict Pydantic models + provenance

src/sourcevahti/models.py is the public schema. Version 0.3 adds a general health topic and indicator definition, measure type, percent units, uncertainty bounds, publisher notes, modelled/projected status and optional cancer fields. Cancer mortality observations still require an exact cancer definition and a per-100,000 rate.

The official MCP SDK derives JSON schemas from the typed functions and Pydantic models. Domain exceptions become MCP tool errors so a model can correct its request. Adapters remain source-specific; shared code handles normalised search, validation, duplicate detection and ambiguity.

Data refresh

Bundled snapshot files:

  • src/sourcevahti/data/finnish_cancer_registry_2024.csv

  • src/sourcevahti/data/nordcan_lung_mortality_9_6.csv

  • src/sourcevahti/data/who_gho_tobacco_use_2026_01_15.csv

  • src/sourcevahti/data/eurostat_lung_mortality_2026_06_08.csv

WHO and Eurostat have documented machine endpoints. Refresh their deliberately narrow matrices outside server startup:

uv run python scripts/refresh_context_snapshots.py

The script requests only the reviewed indicators/dimensions, rejects missing countries, short series, duplicate country-sex-year keys and negative values, then writes deterministic CSV files. Review diffs, metadata, terms and publisher flags before accepting them.

The Finnish Cancer Registry and NORDCAN interactive applications remain a manual trust boundary. Preserve raw exports outside the repository, verify all rate definitions and standard populations, update provenance with the normalised file, and confirm that ambiguity tests still expose every valid definition.

SourceVahti code is Apache-2.0. Source data retain publisher terms; every provenance object carries a separate license_note.

Deployment and DNS

The public endpoints are sourcevahti.vahtian.com for the hosted HTTP/MCP service and trends.ntog.org for the NTOG Shiny application. DNS records should only be created after each deployment supplies its canonical target hostname.

See docs/DNS.md for Cloudflare records, provider-side custom domain steps and verification commands.

Development

uv sync --locked --all-extras --dev
uv run --frozen ruff check .
uv run --frozen ruff format --check .
uv run --frozen mypy src
uv run --frozen pytest
uv build

Tests use frozen official responses and the SDK's in-memory MCP client. CI runs linting, type checking, tests and package builds on Python 3.11 and 3.13.

See CONTRIBUTING.md for source-update rules and SECURITY.md for responsible disclosure.

Licence

SourceVahti code is licensed under the Apache License, Version 2.0. No medical advice is provided. Always cite the source and preserve the returned epidemiological definition when reporting a value.

Available Tools

3 tools
get_latest_observationGet latest health observationB
Read-only

Return the latest observation with definition and full provenance.

ParametersJSON Schema
NameRequiredDescriptionDefault
sexNoOptional sex filter: female, male, or all.
unitNoOptional canonical unit filter.
queryNoIndicator query used when indicator_id is omitted.
sourceNoOptional source identifier.
geographyNoOptional published geography.
rate_typeNoExplicit epidemiological rate definition. Omission produces an ambiguity error when several definitions are valid.
indicator_idNoExact identifier returned by search_indicators.

Output Schema

ParametersJSON Schema
NameRequiredDescription
indicatorYes
observationYes

TDQS

B3.3/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description adds that the result includes definition and full provenance, but it does not mention error conditions, how 'latest' is determined, or what happens when no observation exists. The readOnlyHint=true annotation covers safety, so the description doesn't need to restate that, but more behavioral detail would be helpful.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single sentence that efficiently states the action, resource, and key added value ('definition and full provenance'). No fluff or unnecessary details are present.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness3/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The presence of an output schema and fully described parameters covers the return value and argument semantics. However, the description lacks usage context and does not differentiate from sibling tools, which is a notable gap for a tool with this many optional filters.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema covers 100% of parameters with descriptive text, so the baseline is 3. The description itself does not add parameter-level meaning, but the schema already provides clarity, including important warnings like rate_type's ambiguity error.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description uses a specific verb ('Return') and resource ('latest observation'), and adds 'with definition and full provenance' to clarify scope. It distinguishes from siblings by focusing on the singular latest result, though it doesn't explicitly contrast with get_observations.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no guidance on when to use this tool versus get_observations or search_indicators. It does not mention scenarios like retrieving a single most recent record versus a full time series, leaving the agent to infer the intended use from context.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

get_observationsGet health observationsB
Read-only

Return a normalised series only after resolving its full definition.

ParametersJSON Schema
NameRequiredDescriptionDefault
sexNoOptional sex filter: female, male, or all.
unitNoOptional canonical unit filter.
queryNoIndicator query used when indicator_id is omitted.
sourceNoOptional source identifier.
end_yearNoInclusive final year.
geographyNoOptional published geography.
rate_typeNoExplicit canonical rate definition returned by search_indicators. Required when several definitions match.
start_yearNoInclusive first year.
indicator_idNoExact identifier returned by search_indicators.

Output Schema

ParametersJSON Schema
NameRequiredDescription
countYes
indicatorYes
observationsYes

TDQS

B3.1/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint=true and openWorldHint=false, so the safety profile is known. The description adds the notion of normalization and the requirement to resolve the full definition, which provides some context about behavior. No contradiction found, but no additional details on rate limits, pagination, or response characteristics.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness4/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single concise sentence with no redundant words. It is front-loaded with the core function, but the second clause is somewhat cryptic, reducing clarity slightly.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The description is too minimal for a tool with 9 optional parameters. It does not explain the relationship with search_indicators (which supplies indicator_id and rate_type), nor does it mention any prerequisites or typical usage scenarios. The output schema covers return values, but usage context is missing.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so every parameter has an individual description. The tool description adds no parameter-level information beyond what the schema provides, keeping this at baseline.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description says 'Return a normalised series', which gives a specific verb and resource. It distinguishes from sibling get_latest_observation (which returns a single observation) by explicitly returning a series. However, the phrase 'only after resolving its full definition' is vague about what that entails.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No explicit when-to-use or alternative tool guidance. The phrase 'only after resolving its full definition' hints that some prerequisite must be met but does not explain how or when to use this tool vs search_indicators. Missing clear exclusions or alternatives.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

search_indicatorsSearch health indicatorsA
Read-only

Search precisely defined indicators and expose all valid rate variants.

ParametersJSON Schema
NameRequiredDescriptionDefault
sexNoOptional sex: female, male, or all.
unitNoOptional unit. Canonical values include per_100_000_person_years and percent.
limitNoMaximum matches to return.
queryYesPlain-language indicator query, for example 'female lung cancer mortality rate'.
sourceNoOptional source: finnish_cancer_registry, nordcan, who_gho, or eurostat. Source names in the query are also enforced.
geographyNoOptional published geography, for example Finland, Denmark, Norway, Sweden, Iceland, Greenland, or Faroe Islands.

Output Schema

ParametersJSON Schema
NameRequiredDescription
countYes
queryYes
matchesYes

TDQS

A4/5.0
Behavior4/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

The description discloses that the search is precise ('precisely defined indicators') and returns all valid rate variants, which goes beyond the readOnlyHint annotation. It does not contradict the annotation, and adds behavioral nuance about the comprehensiveness of results. However, it does not elaborate on additional behaviors like pagination or how results are ordered, but with the annotation providing the read-only profile, this is adequate.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, 11-word sentence that front-loads the action and purpose. It contains no filler or redundant information, and every word contributes to meaning. This is a model of concise writing.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness4/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

The tool has six parameters and an output schema, but the schema descriptions are comprehensive and the annotations supply the read-only safety context. The description covers the core purpose and a key behavioral quirk (exposing all rate variants), which is enough given the structured data. It could be slightly more explicit about how it fits into the workflow with observations, but overall it is complete.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The input schema descriptions cover 100% of the parameters, providing detailed explanations for each field such as examples for query and source. The tool description adds no additional parameter-specific semantics beyond the schema, so the baseline of 3 applies. The description's mention of 'valid rate variants' tangentially relates to the unit/geography filtering but does not clarify their syntax.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the tool performs a search over health indicators, using the specific verb 'Search' and resource 'precisely defined indicators.' It also adds a unique characteristic ('expose all valid rate variants') that distinguishes it from sibling tools like get_observations, which fetch observation data. This makes the purpose unambiguous and well-scoped.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines3/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description implies the tool is used for searching indicator definitions, but it does not explicitly contrast it with the sibling tools get_observations or get_latest_observation. There is no when-not-to-use guidance or alternative recommendations. The context is inferred from the verb 'Search' and the sibling names, but not explicitly stated.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

  1. 3 tool updatesv0.3.0
    • First observedget_latest_observation
    • First observedget_observations
    • First observedsearch_indicators

TDQS

A3.6/5.0

Scored across 3 tools

Disambiguation4/5

The three tools target distinct operations: searching indicators, retrieving full observation series, and fetching the latest observation. Search vs. retrieval is clear, and the latest observation is a specific convenience function, though it could be seen as a subset of get_observations.

Naming Consistency5/5

All tool names follow the verb_noun pattern consistently: search_indicators, get_observations, get_latest_observation. The verbs (search/get) and noun phrases are clear and predictable.

Tool Count4/5

With only 3 tools, the server is minimal but well-suited for a focused data-access purpose. It feels slightly thin but not under-scoped, and each tool serves a distinct and necessary function.

Completeness4/5

The domain is read-only indicator/observation data access. The tools cover searching for definitions, retrieving time series, and getting the latest value. A possible gap is a direct 'get_indicator' by exact ID, but search_indicators likely handles that. Core workflows appear covered.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables natural language access to Denmark's Statistics API (Danmarks Statistik), allowing users to query and analyze Danish statistical data without coding knowledge through AI-powered interactions.
    MIT
  • A
    license
    A
    quality
    D
    maintenance
    Provides access to 5,000+ Key Performance Indicators across 264 operating areas for all Swedish municipalities and regions, enabling statistical analysis, comparisons, and trend tracking of Swedish public sector data.
    21
    34 npm
    12
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Provides access to Sweden's comprehensive municipal and regional statistics database with semantic search capabilities. Enables natural language queries against thousands of Key Performance Indicators covering various aspects of Swedish public sector data.
    16
    Apache 2.0