Tilastokeskus StatFin MCP Server
Click 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., "@Tilastokeskus StatFin MCP ServerWhat is the current unemployment rate in Finland?"
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.
Tilastokeskus StatFin MCP Server
Production-grade Model Context Protocol (MCP) server for Statistics Finland's StatFin database. Enables AI assistants like Claude to browse, search, and query Finnish statistical data.
Purpose
This project makes Finland's official statistics accessible to AI assistants through the Model Context Protocol (MCP). It bridges the gap between natural language queries and the structured PxWeb API.
Why This Exists
The Problem: Statistics Finland maintains one of the world's most comprehensive national statistics databases with 4,500+ tables covering population, employment, housing, economy, and more. However, the PxWeb API requires:
Knowledge of table IDs, variable codes, and value codes
Understanding of the hierarchical data structure
Correct query formatting with specific filter types
Awareness of API rate limits and response sizes
This makes it difficult for users to answer simple questions like "What is Helsinki's population?" without significant technical knowledge.
The Solution: This MCP server provides 7 tools that allow AI assistants to:
Discover relevant tables through natural language search
Explore the database structure and available variables
Query specific data with proper filtering and pagination
Handle rate limiting, caching, and error recovery automatically
Use Cases
Journalists asking "How has unemployment changed since COVID?"
Researchers comparing regional population trends
Analysts tracking housing price divergence between Helsinki and other cities
Citizens curious about birth rates, migration, or energy consumption
Developers building applications that need Finnish statistical data
Design Philosophy
LLM-first: Tool descriptions, parameter hints, and output schemas are optimized for AI consumption
Guided workflow: Each tool guides the LLM to the next logical step
Fail-safe: Query size estimation prevents expensive API calls; rate limiting protects against quota exhaustion
Cache-smart: Historical data is cached until StatFin updates it, minimizing redundant API calls
Related MCP server: @nor-data/statfin-mcp
Features
7 MCP Tools for comprehensive data access
149 subject areas covering population, employment, education, economy, environment, and more
~4,500+ statistical tables with decades of historical data
Smart caching with timestamp validation - historical data stays cached until updated
Rate limiting (8 req/min per instance) to respect API limits
Multi-language support (Finnish, English, Swedish)
Documentation
ARCHITECTURE.md - Technical architecture and design decisions
BLOGS.md - Blog post ideas and data stories (English)
BLOGIT.md - Blog post ideas and data stories (Finnish)
Data Source & License
Statistics Finland StatFin Database
Official statistics of Finland
Data updated regularly (varies by table)
Free to use, no API key required
Data License: Statistics Finland data is licensed under CC BY 4.0. When using the data, provide attribution: "Source: Statistics Finland"
Installation
Local Installation
Clone and build the server to run on your machine.
git clone https://github.com/your-org/statfin-mcp.git
cd statfin-mcp
npm install
npm run buildAdd to your config file:
macOS:
~/Library/Application Support/Claude/claude_desktop_config.jsonWindows:
%APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"statfin": {
"command": "node",
"args": ["/absolute/path/to/statfin-mcp/dist/server.js"]
}
}
}claude mcp add statfin node /absolute/path/to/statfin-mcp/dist/server.jsVerify with:
claude mcp listAdd to ~/.codex/config.toml:
[mcp.statfin]
command = "node"
args = ["/absolute/path/to/statfin-mcp/dist/server.js"]
transport = "stdio"In Cursor settings, add MCP server with command:
Name:
statfinCommand:
nodeArgs:
/absolute/path/to/statfin-mcp/dist/server.js
Remote Server
Connect to a hosted instance without local installation.
claude mcp add --transport http statfin https://your-server.example.com/mcpVerify with:
claude mcp listAdd to your config file:
macOS:
~/Library/Application Support/Claude/claude_desktop_config.jsonWindows:
%APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"statfin": {
"type": "http",
"url": "https://your-server.example.com/mcp"
}
}
}Add to ~/.codex/config.toml:
[mcp.statfin]
url = "https://your-server.example.com/mcp"
transport = "http"In Cursor settings, add MCP server:
Name:
statfinURL:
https://your-server.example.com/mcpTransport: HTTP
Running the Server
# Development mode (stdio transport, watch mode)
npm run dev
# Production HTTP server
npm start
# With custom port
PORT=8080 npm start
# Docker
docker-compose up --buildMCP Tools
Tool | Description |
| Search for tables by keyword (primary discovery) |
| Browse all 149 topic areas |
| List tables in a subject area |
| Get table structure and variables |
| Get all codes for a variable (regions, years) |
| Execute data queries |
| Check server health and rate limits |
Example Workflows
⚠️ Variable codes changed in the 8 June 2026 PxWeb migration. They are now version-stamped and table-specific (e.g.
alue_23_20260101,timeperiod_y), so the variable codes shown in the advanced examples below are illustrative — always callget_table_metadatato get the exact codes for your table. Table IDs are now short (11re.px, notstatfin_vaerak_pxt_11re.px). Value codes (SSS,KU091,MK01…) are unchanged.
Basic: Helsinki Population Trend
// Codes below are the current codes for table 11re.px (verified).
query_table({
tableId: "11re.px",
selections: [
{ variable: "alue_23_20260101", filter: "item", values: ["KU091"] }, // Helsinki
{ variable: "ikaryhma_10_20180101", filter: "item", values: ["SSS"] }, // All ages
{ variable: "sukupuoli_9_20180101", filter: "item", values: ["SSS"] }, // Total
{ variable: "timeperiod_y", filter: "top", top: 10 }, // Last 10 years
{ variable: "contentscode", filter: "item", values: ["vaerak-vaesto"] } // Population
]
})
// Returns: Helsinki population trend over the last 10 yearsAdvanced Example Queries
The variable codes in these examples are conceptual (pre-migration names shown for readability). Resolve the real, current codes for each table via get_table_metadata before querying — only the short table IDs and the value codes are guaranteed current.
Education: University Student Employment by Field
Which fields of study have the highest employment rates for students?
// Table: Student employment by education level and field
query_table({
tableId: "13g2.px",
selections: [
{ variable: "Koulutusaste", filter: "item", values: ["7"] }, // University level
{ variable: "Sukupuoli", filter: "item", values: ["SSS"] }, // All genders
{ variable: "Maakunta", filter: "item", values: ["SSS"] }, // Whole country
{ variable: "Vuosi", filter: "top", top: 3 } // Last 3 years
]
})
// Analyze: Compare IT vs humanities employment ratesHousing: Rent Price Trends by Postal Code
Track rental market changes in Helsinki city center vs suburbs
// Table: Free-market rental prices by postal code, quarterly
query_table({
tableId: "13eb.px",
selections: [
{ variable: "Postinumero", filter: "item", values: [
"00100", // Helsinki center (Kruununhaka)
"00500", // Sörnäinen
"02100", // Espoo Tapiola
"01300" // Vantaa Tikkurila
]},
{ variable: "Huoneluku", filter: "item", values: ["02"] }, // 2-room apartments
{ variable: "Vuosineljännes", filter: "top", top: 20 } // 5 years quarterly
]
})
// Analyze: Which areas are gentrifying fastest?Migration: International Migration Flows
Analyze emigration vs immigration patterns over decades
// Table: Migration by month and type
query_table({
tableId: "119z.px",
selections: [
{ variable: "Sukupuoli", filter: "item", values: ["SSS"] },
{ variable: "Tapahtumakuukausi", filter: "item", values: ["SSS"] }, // Annual totals
{ variable: "Tiedot", filter: "item", values: [
"vm41", // Immigration
"vm42", // Emigration
"vm43" // Net migration
]},
{ variable: "Vuosi", filter: "top", top: 30 } // 30-year trend
]
})
// Analyze: How has Finland's migration balance changed since 1990?Crime: Monthly Crime Statistics Trends
Track reported crimes by category with seasonal patterns
// Table: Reported crimes by month (preliminary data)
query_table({
tableId: "13jt.px",
selections: [
{ variable: "Rikosryhmä ja teonkuvauksen tarkenne", filter: "item", values: [
"101T603", // All crimes total
"101T504X406", // Violent crimes
"101T161" // Property crimes
]},
{ variable: "Tiedot", filter: "item", values: ["rikokset_lkm"] },
{ variable: "Kuukausi", filter: "top", top: 60 } // 5 years monthly
]
})
// Analyze: Seasonal crime patterns, COVID impact on crime ratesElectricity: Power Generation Mix Evolution
How has Finland's electricity production evolved toward renewables?
// Table: Electricity supply and production by source
query_table({
tableId: "11sr.px",
selections: [
{ variable: "Tiedot", filter: "item", values: [
"sahkon_tuot", // Total production
"vesivoima", // Hydropower
"tuulivoima", // Wind power
"ydinvoima", // Nuclear
"fossiiliset" // Fossil fuels
]},
{ variable: "Vuosi", filter: "top", top: 25 } // 25-year transition
]
})
// Analyze: Nuclear vs wind growth, fossil phase-out trajectoryTraffic: Road Accident Hotspots by Municipality
Which municipalities have the highest traffic accident rates?
// Table: Traffic accidents with injuries by area and road type
query_table({
tableId: "12qh.px",
selections: [
{ variable: "Alue", filter: "item", values: [
"SSS", // Whole country (for comparison)
"KU091", // Helsinki
"KU092", // Vantaa
"KU049", // Espoo
"KU837", // Tampere
"KU853" // Turku
]},
{ variable: "Tielaji", filter: "item", values: ["SSS"] },
{ variable: "Osallinen", filter: "item", values: ["SSS"] },
{ variable: "Tiedot", filter: "item", values: [
"konn", // Total accidents
"kuolonn", // Fatal accidents
"loukonn" // Injury accidents
]},
{ variable: "Vuosi", filter: "top", top: 5 }
]
})
// Analyze: Per-capita accident rates, pedestrian vs vehicle involvementHousing: Household Size and Building Type Changes
How are Finnish living patterns changing over time?
// Table: Households by size and building type
query_table({
tableId: "116a.px",
selections: [
{ variable: "Talotyyppi", filter: "item", values: [
"1", // Detached houses
"2", // Attached houses
"3", // Apartment buildings
]},
{ variable: "Asuntokunnan koko", filter: "item", values: [
"1", // 1-person households
"2", // 2-person
"3", // 3-person
"4" // 4+ person
]},
{ variable: "Vuosi", filter: "top", top: 40 } // Since 1985
]
})
// Analyze: Rise of single-person households, apartment living trendsElectric Vehicles: Adoption Rate by Vehicle Type
Track the EV transition in Finland's vehicle fleet
// Table: New vehicle registrations by fuel type
query_table({
tableId: "11ck.px",
selections: [
{ variable: "Ajoneuvoluokka", filter: "item", values: ["01"] }, // Passenger cars
{ variable: "Käyttövoima", filter: "item", values: [
"00", // Total
"01", // Petrol
"02", // Diesel
"04", // Electric
"05", // Plug-in hybrid
]},
{ variable: "Vuosi", filter: "top", top: 15 }
]
})
// Analyze: EV market share growth, diesel decline post-2015Multi-Step Analysis Patterns
Cross-Domain Analysis: Education → Employment → Income
1. Find education completion rates by field
2. Get employment statistics for recent graduates
3. Query income data by education level
4. Compare: Which fields offer best ROI?Time-Series with Regional Breakdown
1. Get national trend (region variable, value "SSS")
2. Compare major cities (KU091, KU837, KU853)
3. Identify regional divergence patterns
4. Correlate with local economic indicatorsDemographic Shift Analysis
1. Query population by age groups (1990-2024)
2. Get migration data for same period
3. Query birth/death rates
4. Model: Aging population impact on workforceCommon Region Codes
Code | Description |
| Whole country (KOKO MAA) |
| Regions (maakunta) |
| Helsinki |
| Vantaa |
| Espoo |
Development
# Type checking
npm run typecheck
# Run unit tests
npm run test:run
# Run tests with coverage
npm run test:coverage
# Run integration tests (requires API access)
npm run test:integration
# Fetch fresh test fixtures
npm run test:fixtures:fetchEnvironment Variables
Variable | Default | Description |
| 8080 | HTTP server port |
| stdio | Set to "http" for HTTP transport |
| - | Optional authentication token |
| info | Logging level (debug, info, warn, error) |
API Rate Limits
StatFin API: 30 requests/minute total
This server: 8 requests/minute per instance
Designed for up to 3 concurrent Cloud Run instances
License
This project (code) is licensed under MIT. The statistical data accessed through this server is provided by Statistics Finland under CC BY 4.0.
Available Tools
7 toolsget_api_statusCheck API StatusARead-onlyIdempotent
Get server health, rate limit status, and cache statistics.
Use when:
Queries are failing or slow
Need to check remaining API quota
Debugging connection issues
Rate limit: 8 requests per minute per instance.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
| cache | Yes | Cache statistics per cache type |
| config | Yes | Server configuration |
| healthy | Yes | True if server is healthy and accepting requests |
| rateLimit | Yes | Current rate limit status |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint, idempotentHint, and destructiveHint=false, covering safety. The description adds value beyond that by stating the rate limit (8 requests per minute per instance) and the specific data returned (health, rate limit status, cache statistics). This is useful behavioral context that annotations do not provide. No contradictions with annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is compact and well-structured: first a one-line purpose, then a 'Use when' bulleted list, then a rate limit note. Every sentence earns its place – there is no fluff. Information is front-loaded, so an agent quickly understands what the tool does and when to use it.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a health-check tool with no parameters and an output schema (which likely defines the return structure), the description covers the purpose, use cases, and rate limit. The siblings are all sufficiently different that there is no ambiguity about selection. Nothing critical is missing for an agent to decide whether and when to call this tool.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
With zero parameters, the schema is trivially complete (100% coverage). The baseline for 0 params is 4, and the description adds no parameter-related meaning because none is needed. The description's mention of what is returned (health, rate limit, cache stats) provides context that would indirectly help understand any potential future parameters, but for now it's appropriately handled.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states a specific verb and resource: 'Get server health, rate limit status, and cache statistics.' This distinguishes it from all siblings, which deal with statistics, subject areas, metadata, and queries – none of which match a health check. The purpose is unambiguous and immediately actionable.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicit 'Use when:' list with three concrete triggers: queries failing/slow, needing to check remaining API quota, and debugging connection issues. This gives an agent clear conditions for invocation. While it doesn't explicitly say when not to use it, the context is specific enough that misuse is unlikely, and the distinct scope of siblings implicitly excludes alternatives.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_table_metadataGet Table StructureARead-onlyIdempotent
Get the structure of a table: what variables it has and what values are available.
REQUIRED before querying - shows you:
Variable codes (table-specific and version-stamped, e.g. "alue_23_20260101" for region, "timeperiod_y" for the time variable). Always read these here - never assume or reuse codes from another table.
Value codes (KU091=Helsinki, SSS=Total, 2024=year 2024)
Which variables are required vs optional
Total possible data combinations
Example: a region variable may have 300+ values, a year variable 50+.
After understanding the structure, use query_table with the exact codes from this output.
| Name | Required | Description | Default |
|---|---|---|---|
| tableId | Yes | Table ID from search_statistics. Example: "11re.px" | |
| language | No | Language for variable/value names. Default "fi". | fi |
| includeAllValues | No | If true, return ALL value codes (can be 300+ for regions). Default false shows first 20. |
Output Schema
| Name | Required | Description |
|---|---|---|
| title | Yes | Human-readable table title |
| tableId | Yes | The table ID |
| variables | Yes | All variables in this table |
| lastUpdated | Yes | When the data was last updated (ISO format) |
| queryGuidance | Yes | Tips for constructing an efficient query |
| totalCombinations | Yes | Total possible data cells (product of all value counts) |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true, idempotentHint=true, and destructiveHint=false, so the safety profile is covered. The description adds valuable behavioral context: it explains that variable codes are table-specific and version-stamped, that value codes can be numerous (300+), and that includeAllValues defaults to showing only the first 20. This goes beyond what annotations provide.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is well-structured with a clear opening sentence, a bulleted list of what the output shows, and a concrete example. It's slightly longer than strictly necessary but every section earns its place by conveying critical usage information. The 'REQUIRED before querying' callout is front-loaded and effective.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The description is complete for a read-only metadata tool. It explains the purpose, the prerequisite relationship to query_table, the nature of the output (codes, values, required/optional), and the scale of data. The output schema exists, so return values don't need to be described in detail. The only minor gap is not explaining what 'total possible data combinations' means precisely, but this is a minor omission given the overall completeness.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the schema already documents all three parameters (tableId, language, includeAllValues). The description adds context about the meaning of the output (variable codes, value codes) and the scale of values, but doesn't add much parameter-specific detail beyond the schema. Baseline 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool's purpose: retrieving table structure including variable codes, value codes, required vs optional variables, and data combinations. It explicitly distinguishes itself from query_table by positioning this as the prerequisite step before querying. The verb 'get' plus the specific resource 'table metadata/structure' is unambiguous.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description explicitly says this is REQUIRED before querying, and instructs the agent to use query_table afterward with the exact codes from this output. It also warns against assuming or reusing codes from another table, providing clear when-to-use guidance and routing to the correct sibling tool.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_variable_valuesGet All Values for a VariableARead-onlyIdempotent
Get the complete list of values for a variable when metadata only shows first 20.
Useful for:
Finding specific region codes (KU091=Helsinki, MK01=Uusimaa region)
Getting all available years (1972-2024)
Finding specific category codes
Common region codes:
SSS = Whole country (Finland)
MK01-MK19 = Regions (maakunta)
KU091 = Helsinki, KU049 = Espoo, KU837 = Tampere
Use search parameter to filter: search="Helsinki" returns only matching values.
| Name | Required | Description | Default |
|---|---|---|---|
| search | No | Filter values containing this text. Example: "Helsinki" returns only Helsinki-related codes. | |
| tableId | Yes | Table ID. Example: "11re.px" | |
| language | No | Language for value names. Default "fi". | fi |
| variable | Yes | Exact variable code from get_table_metadata (table-specific and version-stamped, e.g. "alue_23_20260101", "timeperiod_y"). The region variable usually starts with "alue"; the time variable is often "timeperiod_y". Do not guess. |
Output Schema
| Name | Required | Description |
|---|---|---|
| total | Yes | Total number of values for this variable |
| values | Yes | All values for this variable (filtered if search was used) |
| variable | Yes | The variable code that was queried |
| commonCodes | No | Common code patterns for region variables (wholeCountry, regions, municipalities) |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already cover readonly/idempotent/non-destructive behavior, so the description adds value by explaining that it returns a complete list beyond the metadata's first 20. It also provides concrete behavioral context (e.g., common region codes and search filter behavior) that helps set expectations about the output. No contradictions with annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is well-structured and front-loaded with the core purpose first. It uses bullet points for readability and groups related information (useful cases, region codes). It is a bit longer than strictly necessary, but every sentence adds practical value—examples and codes are not filler. The structure makes it scannable for an agent.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a read-only value-list tool with an output schema, the description is thorough. It covers the primary use cases, gives concrete examples of how to use the search filter, and provides domain-specific knowledge (region codes, year range 1972–2024) that an agent would otherwise lack. The existence of an output schema reduces the need to describe return structure. No critical information is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The schema already describes all parameters with 100% coverage (baseline 3). The description goes well beyond by explaining variable code patterns (e.g., 'alue_23_20260101', 'timeperiod_y'), providing common region codes (e.g., KU091, MK01), and showing the search parameter example with 'Helsinki'. This enriches the schema's bare definitions and helps the agent choose correct parameter values.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description opens with a specific, actionable purpose: 'Get the complete list of values for a variable when metadata only shows first 20.' This clearly distinguishes it from metadata retrieval (get_table_metadata) which provides a truncated list. The 'Useful for' bullets further clarify the intended use cases (region codes, year lists, category codes), leaving no ambiguity about what the tool does.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description states when to use the tool: when metadata only shows the first 20 values. This implicitly contrasts with get_table_metadata, but it does not name alternatives or explicitly say not to use other tools. It gives clear context for the typical scenarios (finding region codes, years, category codes) but lacks explicit 'when not to use' guidance or reference to sibling tools.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_subject_areasBrowse Statistical TopicsARead-onlyIdempotent
List all 149 subject areas (topics) in StatFin database.
Use this to explore what statistics are available when you don't have a specific search term.
Topic examples:
vaerak: Population structure
tyti: Labor force
ashi: Housing prices
synt: Births and deaths
muutl: Migration
After finding an area, use list_tables to see all tables in that topic.
| Name | Required | Description | Default |
|---|---|---|---|
| language | No | Language: fi=Finnish, en=English, sv=Swedish | fi |
Output Schema
| Name | Required | Description |
|---|---|---|
| areas | Yes | All available subject areas (topics) |
| total | Yes | Total number of subject areas (149) |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already cover the read-only, idempotent, and non-destructive profile. The description adds useful behavioral context by promising the complete set ('all 149') and giving representative codes and labels. It doesn't mention ordering or language-dependent output, but that is less critical given the output schema.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The core message is front-loaded in the first sentence, the examples earn their place by making return values concrete, and the last sentence is actionable. There is no filler or repetition.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a simple, parameterless, read-only list operation with an output schema and complete parameter docs, the description covers purpose, when to use it, and what to do next. Nothing required to invoke it correctly is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The only parameter, language, is fully documented in the input schema with enum values, default, and meaning. The description adds no parameter-level guidance, so it stays at the baseline expected when schema description coverage is high.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Opens with a specific verb and resource: 'List all 149 subject areas (topics) in StatFin database.' The topic examples make the resource concrete, and the final sentence distinguishes it from list_tables, so an agent can clearly tell what this tool produces.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly states when to use it: 'when you don't have a specific search term,' and gives a clear next step to list_tables. It doesn't name search_statistics as the counterpart for specific queries, but the contextual cue is strong enough to guide selection.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
list_tablesList Tables in TopicARead-onlyIdempotent
List all statistical tables within a subject area.
Each area typically has 20-40 tables with different data views.
Common subject areas:
"vaerak" → 30+ population tables (age, gender, region, etc.)
"tyti" → 35+ employment tables (employment rate, unemployment, etc.)
"ashi" → 15+ housing price tables
Use list_subject_areas first to find the area ID, or use search_statistics for direct search.
| Name | Required | Description | Default |
|---|---|---|---|
| language | No | Language: fi=Finnish, en=English, sv=Swedish | fi |
| subjectArea | Yes | Subject area ID (e.g., "vaerak", "tyti", "asas") |
Output Schema
| Name | Required | Description |
|---|---|---|
| total | Yes | Number of tables in this area |
| tables | Yes | All tables in this subject area |
| subjectArea | Yes | The subject area that was queried |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
The annotations already declare readOnlyHint, openWorldHint, idempotentHint, and destructiveHint=false, so the safety profile is well covered. The description adds useful context about typical table volume and data views, but it does not disclose further behavioral traits such as response size limits, pagination, or result variability.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is front-loaded with a one-sentence definition, followed by compact bullets and a short routing note. Every sentence contributes useful information, and there is no redundant restatement of the schema or title.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a two-parameter read-only tool with full schema coverage, an output schema, and strong sibling guidance, the description is complete. It explains how to discover the required argument, what kind of content to expect, and when to choose an alternative tool.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the baseline is 3. The description adds real value beyond the schema by mapping example subject-area IDs to content ('population tables', 'employment tables', 'housing price tables') and expected table counts, making the required subjectArea parameter more meaningful to an agent.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource: 'List all statistical tables within a subject area.' It also distinguishes itself from siblings by naming list_subject_areas and search_statistics and clarifying that the scope is subject-area-based, not a general search.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly tells the agent when to use this tool versus alternatives: 'Use list_subject_areas first to find the area ID, or use search_statistics for direct search.' It also gives concrete subject-area examples with expected table counts, which helps the agent decide whether this is the right entry point.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
query_tableQuery Statistical DataARead-onlyIdempotent
Execute a query to retrieve actual statistical data from a table.
WORKFLOW: search_statistics → get_table_metadata → query_table
Selection types:
filter: "item" + values: ["KU091", "2024"] → specific values
filter: "top" + top: 5 → latest 5 values (good for time variables)
filter: "all" → all values (use carefully, can be large!)
Example - Helsinki population for last 5 years (the variable codes below are from table 11re.px; YOUR table's codes WILL differ - always read them from get_table_metadata first, never reuse these): { "tableId": "11re.px", "selections": [ {"variable": "alue_23_20260101", "filter": "item", "values": ["KU091"]}, {"variable": "timeperiod_y", "filter": "top", "top": 5}, {"variable": "sukupuoli_9_20180101", "filter": "item", "values": ["SSS"]}, {"variable": "ikaryhma_10_20180101", "filter": "item", "values": ["SSS"]}, {"variable": "contentscode", "filter": "item", "values": ["vaerak-vaesto"]} ] }
IMPORTANT: Variable codes are table-specific; get them from get_table_metadata. Use VALUE CODES (KU091, SSS), not labels (Helsinki, Total).
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Max rows. If query exceeds this, it fails with an error suggesting more specific selections. | |
| tableId | Yes | Table ID from search_statistics or list_tables. Example: "11re.px" | |
| language | No | Label language in results. Default "fi". | fi |
| selections | Yes | One entry per variable. IMPORTANT: Include all non-optional variables or query will return too much data. |
Output Schema
| Name | Required | Description |
|---|---|---|
| data | No | Query results (only present if success=true) |
| error | No | Error message (only present if success=false) |
| success | Yes | True if query succeeded, false if error |
| metadata | No | Dataset metadata (source, last updated, label) |
| rowCount | No | Number of data rows returned |
| queryInfo | Yes | Query execution metadata |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already cover readOnly, idempotent, openWorld, and non-destructive behavior, lowering the burden. The description adds useful behavioral context beyond annotations: the 'all' filter can return large datasets, variable codes are table-specific, and the example shows how selections are structured. It does not contradict annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is long but every section earns its place: purpose, workflow, selection-type semantics, a concrete example, and critical warnings. It is front-loaded with the core action and workflow, and the example is highly instructive rather than filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the tool's complexity, the 100% schema coverage, and the presence of an output schema, the description is complete. It covers the required workflow, selection semantics, the need for table-specific codes, and caution around large result sets. Nothing essential for an agent to call this tool correctly is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the baseline is 3, but the description adds significant meaning beyond the schema by explaining the three selection types with concrete examples and emphasizing value codes over labels. The example JSON demonstrates exactly how to populate selections, which is valuable for correct invocation.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool executes a query to retrieve actual statistical data from a table, using a specific verb and resource. It also positions the tool within a workflow (search_statistics → get_table_metadata → query_table), which differentiates it from sibling tools like search_statistics and get_table_metadata.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The WORKFLOW line explicitly places query_table as the final step after search_statistics and get_table_metadata, giving clear sequencing. The description also warns to read variable codes from get_table_metadata first and never reuse codes, but it does not explicitly state when not to use query_table or name alternative tools for other needs.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
search_statisticsSearch Finnish StatisticsARead-onlyIdempotent
Search Statistics Finland's StatFin database for statistical tables by keyword.
USE THIS FIRST when looking for data. Returns ranked results with relevance scores.
Examples:
"väestö Helsinki" → population tables for Helsinki
"unemployment" → employment/labor market tables
"housing prices" → real estate statistics
Returns: tableId (needed for query_table), title, relevance score, publication date.
After finding a table, use get_table_metadata to see its structure before querying.
| Name | Required | Description | Default |
|---|---|---|---|
| limit | No | Max results (1-50). Default 10. Increase if first results are not relevant. | |
| query | Yes | Search term. Examples: "väestö Helsinki", "unemployment rate", "asuntojen hinnat". Finnish terms often work better. | |
| language | No | Response language. Default "fi" (Finnish). Use "en" for English labels. | fi |
Output Schema
| Name | Required | Description |
|---|---|---|
| query | Yes | The search query that was executed |
| total | Yes | Number of results returned |
| hasMore | Yes | True if more results available (increase limit) |
| results | Yes | Ranked search results |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint, idempotentHint, and destructiveHint=false. The description adds useful behavior beyond that: results are ranked by relevance, return fields are enumerated, and Finnish terms often work better. This gives the agent realistic expectations about search behavior without contradicting annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is well-structured and front-loaded with the most important instruction: 'USE THIS FIRST.' Every section earns its place: the one-line purpose, usage priority, examples, return fields, and next-step workflow. No filler or redundancy.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a read-only search tool, the description covers the essential agent workflow: search, interpret results, and proceed to metadata before querying. The output schema exists to document return structure, and the description still enumerates key return fields. Nothing critical is missing for correct invocation.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the schema already documents all three parameters. The description adds examples and notes about Finnish terms, but those complement rather than substantially extend the schema. Baseline 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states a specific verb and resource: 'Search Statistics Finland's StatFin database for statistical tables by keyword.' It also differentiates the tool from siblings by emphasizing keyword search and ranked results, which is distinct from list_tables, list_subject_areas, or query_table.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description gives explicit contextual guidance: 'USE THIS FIRST when looking for data.' It also provides a follow-up workflow: use get_table_metadata before querying. However, it does not explicitly state when not to use this tool or compare it against list_tables and list_subject_areas, so it stops short of full when/when-not coverage.
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.
7 tool updates
v1.0.0- First observed
get_api_status - First observed
get_table_metadata - First observed
get_variable_values - First observed
list_subject_areas - First observed
list_tables - First observed
query_table - First observed
search_statistics
TDQS
Scored across 7 tools
Each tool has a clearly distinct purpose: search, list topics, list tables, get metadata, get variable values, query data, and check status. No overlaps or ambiguous boundaries; the workflow is explicitly sequential and each step is unique.
All tool names follow a consistent verb_noun snake_case pattern: search_statistics, list_subject_areas, list_tables, get_table_metadata, get_variable_values, query_table, get_api_status. Predictable and uniform.
With 7 tools, the server is well-scoped for a statistical database access. Each tool is essential and earns its place—no redundant or missing operations. The count is comfortably within the optimal 3-15 range.
The toolset covers the entire lifecycle from discovery (search, list subject areas, list tables) to detailed exploration (metadata, variable values) to data retrieval (query) and system status. No significant gaps; the workflow is fully supported.
Maintenance
Related MCP Connectors
MCP server for Statistics Sweden (SCB) - 1200+ tables with population, economy, environment data
Hosted MCP server exposing US hospital procedure cost data to AI assistants
- UnifAPIOAuthcom.unifapi
Hosted MCP server for live public-data APIs and Skills for AI agents.
Related MCP Servers
- AlicenseBqualityFmaintenanceMCP server for accessing Japanese government statistics portal 'e-Stat' API, enabling language models to search and retrieve statistical data.590 PyPI20MIT
- AlicenseAqualityDmaintenanceMCP server for official statistics from Statistics Finland (Tilastokeskus) — the StatFin database, exposed through the PxWeb API. Search 3000+ tables, inspect their dimensions, and pull data as JSON-stat2.435 npmMIT
- AlicenseAqualityDmaintenanceMCP server for accessing statistical data via the PxWeb API v2.610 npm1MIT
- AlicenseAqualityBmaintenanceLocal MCP server for querying Polish socioeconomic statistics from the GUS Bank Danych Lokalnych (BDL), letting AI find variables and units and pull data.5MIT