One Canadian data server to rule them all.
Canadian data lives in dozens of places. MapleStats puts 175 of them behind one connection, so your AI agent can find, fetch and cite the numbers while you focus on what they mean.
One connection
MapleStats puts Canada's scattered agencies and portals behind one connection, in English or French, with the source on every number.
Scripts you can rerun
Most data calls can come back as an R, Python, Stata or Julia script (or an Excel Power Query) that fetches the same numbers straight from the source, so the analysis runs without the agent.
The easiest way: ask your agent One prompt connects MapleStats to the hosted server: nothing to install, no account, no key. The agent reads the setup steps from the repository and does the rest.
Connect the MapleStats MCP server to this agent. Follow the setup steps in https://github.com/dsanchezp18/maplestats-mcp- Copy the prompt.
- Paste it into Claude Code, Codex, Cursor or any agent that can run commands on your computer.
- Restart the agent when it says so, then ask for data.
Or add the hosted server yourselfOr run it on your machineBrowse the 358 tools
3
MCPs for the statisticians, keepers of the weights and the margins,
7
for the demographers, counting who comes to stay,
9
for the financial analysts, doomed to explain the rate…
But MapleStats is the one MCP to rule them all.
Too many places to look, too many interfaces to learn.
MCPs made it easier to connect AI agents to data, and people started building narrow servers for narrow pieces of the Canadian data ecosystem: one for Statistics Canada, another for the Bank of Canada, others for open-data portals. Each can be useful. Together they recreate an old Canadian data problem: too many choices before asking a relatively simple question. I built MapleStats to take the opposite approach: one MCP for Canadian open data.
Centralize the interface, not the data.
The server keeps no dataset of its own, only short-lived caches. It calls each source directly, so every number comes from its publisher.
One discovery layer, many adaptors.
Each agency keeps its own adaptor underneath. The agent sees one search across all of them.
Start from the question, not the portal.
The agent does not need to know whether the answer is a StatCan table, a Valet series, a CMHC dataset or a CKAN resource.
Demos
What you can do with it
Nine questions, from nine kinds of work, each answered with MapleStats tool calls. Here is one of them: a macroeconomist reading the yield curve.
Macroeconomists
What bond markets expect, in one line
The gap between the 10-year and 2-year Government of Canada bond yields, every business day since 2001. When it drops below zero, markets expect rates to fall, usually because they expect the economy to slow. It inverted in 2007, 2019 to 2020 and 2022 to 2024.
Show the data
| Year | Average | Lowest month | Highest month |
|---|---|---|---|
| 2001 | 117 bp | 41 bp | 213 bp |
| 2002 | 169 bp | 139 bp | 225 bp |
| 2003 | 155 bp | 124 bp | 188 bp |
| 2004 | 165 bp | 126 bp | 199 bp |
| 2005 | 88 bp | 23 bp | 131 bp |
| 2006 | 18 bp | 6 bp | 32 bp |
| 2007 | 9 bp | −5 bp | 32 bp |
| 2008 | 95 bp | 54 bp | 171 bp |
| 2009 | 200 bp | 167 bp | 218 bp |
| 2010 | 169 bp | 139 bp | 221 bp |
| 2011 | 141 bp | 110 bp | 158 bp |
| 2012 | 76 bp | 65 bp | 100 bp |
| 2013 | 115 bp | 76 bp | 157 bp |
| 2014 | 118 bp | 84 bp | 150 bp |
| 2015 | 97 bp | 77 bp | 117 bp |
| 2016 | 69 bp | 49 bp | 96 bp |
| 2017 | 69 bp | 34 bp | 95 bp |
| 2018 | 29 bp | 8 bp | 51 bp |
| 2019 | 0 bp | −18 bp | 14 bp |
| 2020 | 24 bp | −12 bp | 49 bp |
| 2021 | 88 bp | 47 bp | 124 bp |
| 2022 | −13 bp | −87 bp | 58 bp |
| 2023 | −91 bp | −123 bp | −71 bp |
| 2024 | −33 bp | −69 bp | 17 bp |
| 2025 | 63 bp | 36 bp | 81 bp |
| 2026 | 70 bp | 61 bp | 84 bp |
The other eight
- Statisticians A bachelor's degree, province by province
- Demographers Who comes to stay: Edmonton and Calgary
- Urban planners Canada is building up, not out
- Microeconomists Low income across immigrant generations
- Marketers What a rewards card costs
- Wealth managers Who funds the RRSP
- Scientists Machine learning at the patent office
- Financial analysts The policy rate since 2015
Sources
Where the data comes from
Federal agencies answer most national questions; provincial agencies and open-data catalogues add the local detail. Every result comes straight from its publisher.
- 42
- federal and national sources
- 133
- provincial, territorial and city sources
- 12/13
- provinces and territories with local data
- 175
- sources, one connection
How it works
From a question to a cited number
A connected agent sees three tools: plan_query, search_tools and call_tool. The other 357 stay out of its context until a search finds them. Every output below is real. The server produced the plan, the search and the scripts when this page was built; the data call was recorded on 27 September 2026.
-
01
Plan
plan_queryThe planner reads the question and lists the agencies to ask, in order, with the caveats on combining them. Places it recognizes add their local portals.
plan_queryRun when this page was builtQuestion
How have rents and interest rates moved in Calgary since 2020?
Plan
Housing: starts, rents, prices, mortgages
cmhc_CMHC starts, completions, rents and vacancy: category_level_1 and _2list_ categories cmhc_column_field and row_field options for that category and geographyget_ table_ options cmhc_the table: category_level_1, category_level_2, column_field and row_field (row_field='TIMESERIES' for a series), geography_type and geography_idget_ table_ data cmhc_CMHC's published Excel tables: category='rental-market' or 'household-characteristics'dt_ list_ tables wds_StatCan New Housing Price Index, building permitssearch_ cubes boc_Bank of Canada mortgage and policy ratessearch_ series crea_CREA MLS® Home Price Index (resale prices): download link, attribution and terms only; no values, since CREA's terms forbid publishing themget_ hpi_ links statcan_census shelter cost and tenure: level, geography_codes (DGUIDs from statcan_census_profile_search_geography) and characteristic_codescensus_ profile_ get_ data
Caveat CMHC reports by census metropolitan area and centre; StatCan price indexes are by CMA too, but the two define some areas differently, so name the geography each figure uses.
Interest rates, exchange rates and markets
boc_find the Valet series (e.g. FXUSDCAD, V39079)search_ series boc_pull the series for the periodget_ observations
Calgary (city)
socrata_search with portal='calgary'search_ datasets
-
02
Find
search_toolsSearch ranks the catalogue against a plain-language query and returns the top 5. French works because every tool carries French keywords. The panel runs the server's own index in your browser, so type your own query.
search_toolsRuns in your browserTry
- 1boc_
get_ Bank of CanadaGet the label and description for one Bank of Canada Valet series.series - 2boc_
search_ Bank of CanadaSearch or list Bank of Canada Valet's ~16,000 statistical series.series - 3boc_
get_ Bank of CanadaGet observations for one or more Bank of Canada Valet series.observations - 4boc_
get_ Bank of CanadaGet a Bank of Canada Valet group's description and member series.group - 5worldbank_
get_ World Bank WDIGet the definition and original source of one World Bank WDI indicator.indicator
Top 5 of 357 searchable tools, ranked by the same BM25 index search_tools uses.
- 1boc_
-
03
Fetch
call_toolThe agent calls the tool by name. The result is a typed object, and its provenance block, marked here, gives the exact upstream URL and the time of the query.
call_toolRecorded 27 September 2026Prompt
What is the Bank of Canada's overnight rate?
Result
Target for the overnight rate (business daily) V39079Date Value 24 September 2026 2.25 23 September 2026 2.25 22 September 2026 2.25 Provenance
- Queried
- 27 September 2026, 17:00 UTC
- Result type
boc.ObservationsResult
Show the full response as JSON
{ "series": { "V39079": {"name": "V39079", "label": "Target for the overnight rate (business daily)", "description": "Also called the policy interest rate, the average rate that the Bank of Canada wants to see in the market for overnight money market financing. (V39079)"} }, "observations": [ {"ref_date": "2026-09-24", "values": {"V39079": 2.25}}, {"ref_date": "2026-09-23", "values": {"V39079": 2.25}}, {"ref_date": "2026-09-22", "values": {"V39079": 2.25}} ], "provenance": { "source": "boc", "url": "https://www.bankofcanada.ca/valet/observations/V39079/json", "queried_at": "2026-09-27T17:00:48.227537Z", "as_of": null, "freshness": null, "coverage": null, "limits": null, "cached": false, "schema_name": "boc.ObservationsResult", "reproduce": "For R, Python, Stata and Julia scripts that fetch and clean this data, call reproduce_code with this tool's name and arguments." }} -
04
Reproduce
reproduce_codeThe same call becomes an R, Python, Stata or Julia script, or an Excel Power Query, that downloads, checks and cleans the data, so the analysis does not depend on the agent.
reproduce_codeRun when this page was builtPrompt
Write me a script that fetches this, so I can rerun it myself.
R
# ============================================================ # Bank of Canada Valet: V39079 # Purpose: Fetch the data behind MapleStats MCP's boc_get_observations # (exact: Valet request rebuilt from the tool's arguments) # Inputs: https://www.bankofcanada.ca/valet/observations/V39079/json?recent=3 # Outputs: data/raw/valet_observations.json; the prepared table as `data` # ============================================================ # 0. Setup ---- library(dplyr) library(httr2) library(janitor) library(jsonlite) library(lubridate) library(readr) library(stringr) library(tibble) library(tidyr) dir.create("data/raw", recursive = TRUE, showWarnings = FALSE) # 1. Read inputs ---- response <- request("https://www.bankofcanada.ca/valet/observations/V39079/json?recent=3") |> req_perform() writeLines(resp_body_string(response), "data/raw/valet_observations.json") payload <- fromJSON("data/raw/valet_observations.json", flatten = TRUE) data <- as_tibble(payload[["observations"]]) # 2. Check inputs ---- stopifnot("https://www.bankofcanada.ca/valet/observations/V39079/json?recent=3 returned no rows" = nrow(data) > 0) # 3. Prepare data ---- data <- data |> clean_names() # Valet nests each series as <series>.v; make one row per date and series. data <- data |> pivot_longer(-d, names_to = "series", values_to = "value") |> mutate( series = str_remove(series, "_v$") |> str_to_upper(), value = as.numeric(value), date = ymd(d) ) |> select(date, series, value) # Standard cleaning: trimmed text, empty strings as missing, and numbers # stored as text converted to numbers. data <- data |> mutate(across(where(is.character), \(x) na_if(str_trim(x), ""))) |> type_convert()Python
# ============================================================ # Bank of Canada Valet: V39079 # Purpose: Fetch the data behind MapleStats MCP's boc_get_observations # (exact: Valet request rebuilt from the tool's arguments) # Inputs: https://www.bankofcanada.ca/valet/observations/V39079/json?recent=3 # Outputs: data/raw/valet_observations.json; the prepared table as `data` # ============================================================ # %% 0. Setup import json import re import unicodedata from pathlib import Path import httpx import polars as pl RAW_DIR = Path("data/raw") RAW_DIR.mkdir(parents=True, exist_ok=True) raw_path = RAW_DIR / "valet_observations.json" # %% 1. Read inputs with httpx.Client( http2=True, follow_redirects=True, timeout=300, headers={"User-Agent": "Mozilla/5.0 (compatible; research script)"}, ) as client: response = client.get('https://www.bankofcanada.ca/valet/observations/V39079/json?recent=3') response.raise_for_status() raw_path.write_bytes(response.content) payload = json.loads(raw_path.read_text(encoding="utf-8")) records = payload['observations'] data = pl.json_normalize(records, strict=False) # %% 2. Check inputs assert data.height > 0, "https://www.bankofcanada.ca/valet/observations/V39079/json?recent=3 returned no rows" # %% 3. Prepare data # Valet nests each series as <series>.v; make one row per date and series. data = data.unpivot(index="d", variable_name="series", value_name="value") data = data.with_columns( pl.col("d").str.to_date().alias("date"), pl.col("series").str.replace(r"\.v$", ""), pl.col("value").cast(pl.Float64, strict=False), ).select("date", "series", "value") # Standard cleaning: snake_case names without accents (PÉRIODE -> periode, # referenceNumber -> reference_number, as janitor does in R), trimmed text, # empty strings as missing. Names that clean alike are numbered as janitor # numbers them (Indicator, indicator -> indicator, indicator_2). clean_names = [ re.sub( r"[^0-9a-z]+", "_", re.sub( r"([a-z0-9])([A-Z])", r"\1_\2", unicodedata.normalize("NFKD", column).encode("ascii", "ignore").decode(), ).lower(), ).strip("_") for column in data.columns ] while len(set(clean_names)) < len(clean_names): name_counts = {} numbered = [] for name in clean_names: name_counts[name] = name_counts.get(name, 0) + 1 count = name_counts[name] numbered.append(name if count == 1 else f"{name}_{count}") clean_names = numbered data = data.rename(dict(zip(data.columns, clean_names))) data = data.with_columns(pl.col(pl.Utf8).str.strip_chars().replace("", None))Stata
* ============================================================ * Bank of Canada Valet: V39079 * Purpose: Fetch the data behind MapleStats MCP's boc_get_observations * (exact: Valet request rebuilt from the tool's arguments) * Inputs: https://www.bankofcanada.ca/valet/observations/V39079/json?recent=3 * Outputs: data/raw/valet_observations.json; the prepared table as `data` * ============================================================ version 18 clear all set more off * 0. Setup capture mkdir "logs" capture log close log using "logs/boc_get_observations.log", replace capture mkdir "data" capture mkdir "data/raw" * 1. Read inputs * Stata reads no JSON or HTML and truncates long column names, so its * built-in Python (Stata 16+) fetches, filters and writes a CSV. Point * Stata at a Python with these packages first: python set exec <path>. python: import json from pathlib import Path import httpx import polars as pl RAW_DIR = Path("data/raw") RAW_DIR.mkdir(parents=True, exist_ok=True) raw_path = RAW_DIR / "valet_observations.json" with httpx.Client(http2=True, follow_redirects=True, timeout=300, headers={"User-Agent": "Mozilla/5.0 (compatible; research script)"}) as client: response = client.get('https://www.bankofcanada.ca/valet/observations/V39079/json?recent=3') response.raise_for_status() raw_path.write_bytes(response.content) payload = json.loads(raw_path.read_text(encoding="utf-8")) records = payload['observations'] data = pl.json_normalize(records, strict=False) nested = [name for name, dtype in data.schema.items() if dtype.is_nested()] data = data.with_columns(pl.col(name).map_elements(lambda value: json.dumps(value.to_list() if isinstance(value, pl.Series) else value, default=str), return_dtype=pl.Utf8) for name in nested) data.write_csv(RAW_DIR / "valet_observations_prepared.csv") end import delimited "data/raw/valet_observations_prepared.csv", clear varnames(1) encoding("utf-8") * 2. Check inputs assert _N > 0 * 3. Prepare data * One column per series (<series>_v), one row per date. generate date = date(d, "YMD") format date %td drop d * Standard cleaning: lower-case names, trimmed text, and numbers stored as * text converted (destring leaves genuinely non-numeric text alone). * rename *, lower stops at a clash (Indicator next to indicator), so names * that lower-case alike are numbered as janitor numbers them (indicator, * indicator_2), then renamed in one group rename, which allows swaps. local names foreach var of varlist _all { local names `names' `=strlower("`var'")' } local dups : list dups names while "`dups'" != "" { local numbered local before foreach name of local names { local count 1 foreach earlier of local before { if "`earlier'" == "`name'" local ++count } local before `before' `name' if `count' > 1 { local name = substr("`name'", 1, 32 - strlen("_`count'")) + "_`count'" } local numbered `numbered' `name' } local names `numbered' local dups : list dups names } local old_names local new_names local i 0 foreach var of varlist _all { local ++i local name : word `i' of `names' if "`name'" != "`var'" { local old_names `old_names' `var' local new_names `new_names' `name' } } if "`old_names'" != "" { rename (`old_names') (`new_names') } quietly ds, has(type string) local text_vars `r(varlist)' foreach var of local text_vars { replace `var' = strtrim(`var') } destring, replace log closeJulia
# ============================================================ # Bank of Canada Valet: V39079 # Purpose: Fetch the data behind MapleStats MCP's boc_get_observations # (exact: Valet request rebuilt from the tool's arguments) # Inputs: https://www.bankofcanada.ca/valet/observations/V39079/json?recent=3 # Outputs: data/raw/valet_observations.json; the prepared table as `data` # ============================================================ # 0. Setup using DataFrames using Downloads using JSON3 using Tables using TidierData mkpath("data/raw") # 1. Read inputs Downloads.download("https://www.bankofcanada.ca/valet/observations/V39079/json?recent=3", "data/raw/valet_observations.json") payload = JSON3.read(read("data/raw/valet_observations.json", String)) records = payload["observations"] data = DataFrame(Tables.dictrowtable(records)) # 2. Check inputs @assert nrow(data) > 0 "https://www.bankofcanada.ca/valet/observations/V39079/json?recent=3 returned no rows" # 3. Prepare data # Standard cleaning: snake_case names, trimmed text, empty strings as missing. data = @chain data begin @clean_names end data = mapcols( col -> eltype(col) <: Union{Missing, AbstractString} ? [ismissing(x) || isempty(strip(x)) ? missing : strip(x) for x in col] : col, data, )
Connect
Add it to your client
Your client starts the server with uv when it needs it. There is nothing to host and no port to open.
Claude Code
Run this once in a terminal. --scope user makes the server available in every project; leave it out to add it to the current project only.
claude mcp add --scope user maplestats -- uvx maplestats-mcp
Claude Desktop
In Claude Desktop, open Settings, then Developer, then Edit Config. Add the server to the file that opens, save it and restart Claude.
{
"mcpServers": {
"maplestats": {
"command": "uvx",
"args": ["maplestats-mcp"]
}
}
}
The file lives at ~/Library/Application Support/Claude/claude_desktop_config.json on macOS and %APPDATA%\Claude\claude_desktop_config.json on Windows.
Cursor
The link opens Cursor with the entry filled in. To add it by hand, put it in ~/.cursor/mcp.json for every project, or in .cursor/mcp.json for one project.
{
"mcpServers": {
"maplestats": {
"command": "uvx",
"args": ["maplestats-mcp"]
}
}
}
VS Code
To add it by hand, note that VS Code uses a servers key, not mcpServers. Put this in .vscode/mcp.json in a workspace, or run MCP: Open User Configuration from the Command Palette to add it for every workspace.
{
"servers": {
"maplestats": {
"type": "stdio",
"command": "uvx",
"args": ["maplestats-mcp"]
}
}
}
Codex CLI
Add it from the terminal, or write the same entry into ~/.codex/config.toml yourself.
codex mcp add maplestats -- uvx maplestats-mcp
[mcp_servers.maplestats]
command = "uvx"
args = ["maplestats-mcp"]
Gemini CLI
Add it from the terminal (-s user for every project), or write the entry into ~/.gemini/settings.json next to any settings already there.
gemini mcp add -s user maplestats uvx maplestats-mcp
{
"mcpServers": {
"maplestats": {
"command": "uvx",
"args": ["maplestats-mcp"]
}
}
}
Other clients
Most MCP clients accept this mcpServers entry. If you installed the command with uv tool install or pip, use "command": "maplestats-mcp" and drop args.
{
"mcpServers": {
"maplestats": {
"command": "uvx",
"args": ["maplestats-mcp"]
}
}
}
Every source. One server.
Show the data
| Subject | Tools |
|---|---|
| Provinces and cities: open-data catalogues, provincial, municipal | 117 |
| Statistics: statistics and census | 92 |
| Land and energy: agriculture and food, environment and hazards, energy and mining, geography, transport and safety | 55 |
| People: health, housing, immigration | 40 |
| Money and business: money, prices and public finance, business, IP and competition | 30 |
| Parliament: parliament, law and elections | 21 |