io.github.malkreide/seco-labor-mcp
SECO labour market: unemployment, vacancies, workforce indicators
Open source Open in the app JSON README (API)
About
SECO labour market: unemployment, vacancies, workforce indicators
Details
- Kind
- MCP servers
- Topic
- No topic detected
- Publisher
- malkreide
- Origin
- official
- Category
- ferramentas
- Transport
- local
- Version
- 0.3.4
- Open pull requests
- 1
- Last push
- 2026-09-01T11:00:56Z
- Repository state
- ativo
- Language
- Python
- License
- MIT
- Added
- 2026-08-29 04:00:27
- Updated
- 2026-08-29 04:00:27
- Origin id
io.github.malkreide/seco-labor-mcp
README
> π¨π **Part of the [Swiss Public Data MCP Portfolio](https://github.com/malkreide)**
# SECO Labor Market MCP Server

[](https://github.com/malkreide/seco-labor-mcp/actions/workflows/ci.yml)
[](https://pypi.org/project/seco-labor-mcp/)
[](https://www.python.org/)
[](https://modelcontextprotocol.io/)
[](https://github.com/malkreide/seco-labor-mcp)
[](LICENSE)
π **English** | **[Deutsch](README.de.md)**
An MCP (Model Context Protocol) server for Swiss labor market data from **SECO** (Staatssekretariat fΓΌr Wirtschaft) and **AMSTAT** via opendata.swiss.
<p align="center">
<img src="assets/demo.png" alt="Demo: Claude queries youth unemployment via seco-labor-mcp tool call" width="720">
</p>
---
## Overview
This server connects AI models to Swiss labor market statistics β unemployment rates, job seekers, open positions, youth unemployment, and occupational breakdowns β all without requiring an API key.
**Primary audiences:**
- π« **Schulamt / Education planning** β youth unemployment, vocational guidance data
- π **Research & analysis** β labor market trends, cantonal comparisons
- π€ **AI agents** β automated labor market monitoring and reporting
**Anchor query:**
*"Welche Berufsgruppen haben im Kanton ZΓΌrich die hΓΆchste Jugendarbeitslosigkeit, und welche Lehrberufe unterliegen der Stellenmeldepflicht?"*
[β More use cases by audience β](EXAMPLES.md)
---
## Data Sources (Phase 1 β No Auth Required)
| Source | Description | Status |
|--------|-------------|--------|
| [opendata.swiss](https://opendata.swiss/de/dataset) | CKAN catalogue; the pinned BFS table `T3.3.0.1` carries the SECO annual series | β
Live |
| [arbeit.swiss](https://www.arbeit.swiss) | Monthly press reports (PDF, structured URL pattern) | β
Live |
| [amstat.ch](https://www.amstat.ch) | AMSTAT reference portal | β οΈ JavaScript SPA, no public REST API |
| [unfallstatistik.ch](https://www.unfallstatistik.ch) | Unfallstatistik UVG (SSUV/KSUV c/o Suva) β occupational accidents and diseases | β οΈ PDF only, no API (see below) |
---
## Architecture
```
βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β seco-labor-mcp β
β β
β βββββββββββββββ ββββββββββββββββββββββββββββ β
β β FastMCP β β 9 MCP Tools β β
β β Server βββββΊβ seco_search_datasets β β
β β (stdio / β β seco_get_dataset β β
β β SSE) β β seco_get_unemployment_* β β
β βββββββββββββββ β seco_get_youth_* β β
β β β seco_get_job_seekers β β
β βΌ β seco_get_open_positions β β
β βββββββββββββββ β seco_get_monthly_url β β
β β httpx β β seco_list_cantons β β
β β async β ββββββββββββββββββββββββββββ β
β ββββββββ¬βββββββ β
βββββββββββΌββββββββββββββββββββββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββ
β opendata.swiss CKAN API β
β https://opendata.swiss/api/3/ β
β action/package_search β
β action/package_show β
βββββββββββββ¬ββββββββββββββββββββββββ
β
βΌ
βββββββββββββββββββββββββββββββββββββ
β SECO Data Resources β
β CSV / XLSX / PDF Downloads β
β (monthly labor market data) β
βββββββββββββββββββββββββββββββββββββ
```
---
## Where the figures come from β and what is missing
**SECO is no longer a publisher on opendata.swiss.** Verified 2026-08-14:
`organization_show` returns 404, and none of the 176 entries in
`organization_list` is SECO. Until then the server filtered every search on
that organisation and therefore returned **nothing** β a name lookup that
misses looks exactly like an empty search.
The registered unemployed and job seekers are still SECO's figures: the **BFS
publishes them** in table `T3.3.0.1` and names SECO in the footer. The server
reads that table through a **pinned dataset id** (`sources.py`), checked
against the live source by a live test.
| Series | 2000 | 2025 |
|---|---|---|
| Registered job seekers (SECO) | 124.6 | 214.1 |
| Registered unemployed (SECO) | 72.0 | 133.7 |
| ILO unemployed (BFS) | 126.5 | 248.5 |
*thousands, annual average*
The three series do **not** measure the same thing: in 2000 the ILO figure is
1.76Γ the registered one. The server reports them separately and labelled, and
never converts one into the other.
### The cantonal layer: four cantons, four schemas
There is no national monthly series β but **four cantons publish their own
RAV figures**, each in its own portal with its own column names. For those,
`seco_get_unemployment_overview(canton=β¦)` returns real values:
| Canton | Granularity | from | Level | Note |
|---|---|---|---|---|
| **TG** | monthly | 2016-01 | canton | only series **by age class** β youth unemployment as a count |
| **FR** | monthly | 2004-01 | canton **and Switzerland** | carries the national monthly figure as a comparison row |
| **ZG** | monthly | 1993-01 | canton | youth unemployment only as a **rate**, not a count |
| **ZH** | **annual** | 1991 | **municipality** | no monthly values; districts and regions sit in the same column as municipalities and are separated out |
**The other 22 cantons get a named refusal** β no figure from another canton
and no national aggregate. Partial coverage that feels complete is worse than
none.
The four series are **not comparable with each other** and do not add up to a
Swiss figure: different time axes, different geographic levels, and in ZG's
case a rate rather than a count.
**Still not available:** unemployment by occupational group, open positions as
a national series, and youth unemployment for Switzerland or for 24 of the 26
cantons. The affected tools say so and return **no** substitute figure. These
values exist interactively on [amstat.ch](https://www.amstat.ch/v2/amstat_de.html),
which offers no interface a server could call.
---
## Tools
| Tool | Description | Key Use Case |
|------|-------------|--------------|
| `seco_search_datasets` | Search labour-market datasets on opendata.swiss (publisher shown per hit) | Discovery |
| `seco_get_dataset` | Full metadata + download links for a dataset | Data access |
| `seco_get_unemployment_overview` | Registered unemployed: national annual, cantonal for TG/FR/ZG/ZH | Labor market overview |
| `seco_get_youth_unemployment` | Youth unemployment (15β24) β **TG** (count) and **ZG** (rate) only | π Berufswahlberatung |
| `seco_get_job_seekers` | Registered job seekers, national, annual series from 2000 | Training demand |
| `seco_get_open_positions` | Open positions β **no national series available** | Sector analysis |
| `seco_get_unemployment_by_occupation` | Breakdown by Berufshauptgruppe β **no machine-readable source** | π Vocational guidance |
| `seco_get_monthly_report_url` | Generate/verify PDF report URL | Source access |
| `seco_list_cantons` | All 26 canton codes and names | Utility |
| `seco_get_uvg_overview` | UVG key figures on occupational accidents and diseases | Risk overview |
| `seco_get_uvg_by_branch` | Results per NOGA 2008 economic branch | π Vocational guidance |
| `seco_get_uvg_trends` | Ten-year accident time series per branch | Trend analysis |
12 of a maximum of 15 tools.
---
## Unfallstatistik UVG (SSUV)
The three `seco_get_uvg_*` tools cover the risk side of the same labour market
the unemployment tools describe: how many occupational accidents and diseases
occur per branch, and how that develops over ten years.
**The publisher is not SECO.** The Unfallstatistik UVG is issued by the
Koordinationsgruppe KSUV and the Sammelstelle SSUV c/o Suva, Lucerne. The
`seco_` prefix addresses this server, not the source; every response names the
actual publisher in its `source` field.
### Architecture decision: C (dump-first)
Verified live on 2026-08-05, full write-up in
[`PROBE_REPORT_UVG.md`](PROBE_REPORT_UVG.md).
The source has **no API**. A link scan across every data page returned 165 PDFs
and zero files with `.csv`, `.xlsx` or `.json`. opendata.swiss does not list the
source at all (`count=0` for six of seven search terms), and the BFS dam-api
silently ignores its filter parameters. What remains is machine-readable in
practice but not by design:
| Access | Format | Refresh |
|---|---|---|
| `schluesselzahlen_d.htm` | HTML table, 5 years, Switzerland-wide | annually |
| `Ts{YY}.pdf` | annual edition, tables 1.2 and 2.4 by NOGA | annually, June |
| `WirtKl_{BUV\|NBUV}_{NN}.pdf` | ten-year series per NOGA division | annually, January |
PDFs are cached for 24 h and fetched with 2s/4s/8s backoff.
### What every response tells you
- `source_freshness.data_year` β the **data** year, not the edition year. The
2026 edition reports 2024; that two-year lag is stated, not buried.
- `totals_check` β parsed rows are summed and compared against the total
printed in the same publication. A broken layout shows up here instead of
becoming a plausible wrong number.
- `significant` β the source marks statistically significant year-on-year
changes with an asterisk. That flag is preserved per data point, so a change
is only reported as significant where the source says so.
---
## Installation
### Claude Desktop (stdio)
Add to `claude_desktop_config.json`:
```json
{
"mcpServers": {
"seco-labor": {
"command": "uvx",
"args": ["seco-labor-mcp"]
}
}
}
```
### Cloud / SSE
```bash
pip install seco-labor-mcp
MCP_TRANSPORT=sse PORT=8000 seco-labor-mcp
```
The SSE server binds to **`127.0.0.1` (loopback) by default** to prevent
NeighborJack on shared networks. For container deployments where you actually
need to accept traffic from outside the container, set `HOST=0.0.0.0`
explicitly β ideally in your Dockerfile / orchestrator config, and only behind
an upstream proxy or firewall:
```bash
HOST=0.0.0.0 MCP_TRANSPORT=sse PORT=8000 seco-labor-mcp # container only
```
### Development
```bash
git clone https://github.com/malkreide/seco-labor-mcp.git
cd seco-labor-mcp
pip install -e ".[dev]"
pytest tests/ -m "not live" -v
```
---
## Usage Examples
### Search for youth unemployment data
```
Tool: seco_search_datasets
Input: { "query": "Jugendarbeitslosigkeit Alter", "limit": 5 }
```
### Get cantonal unemployment for ZΓΌrich
```
Tool: seco_get_unemployment_overview
Input: { "canton": "ZH", "response_format": "markdown" }
```
### Get monthly report URL
```
Tool: seco_get_monthly_report_url
Input: { "year": 2026, "month": 2, "language": "de" }
```
---
## Key Concepts
### Arbeitslose vs. Stellensuchende
> **EselsbrΓΌcke**: Arbeitslose β Stellensuchende β Arbeitslose sind eine Teilmenge.
| Term | Definition | Dec 2025 |
|------|-----------|----------|
| Arbeitslose | RAV-registered, immediately available | ~149'000 (3.2%) |
| Stellensuchende | All RAV-registered (incl. training programs) | ~233'900 |
### Youth Unemployment Seasonality
- **July/August**: Sharp increase (school leavers without placements)
- **September/October**: Decline (apprenticeship starts)
- The residual that remains after the autumn decline signals structural need for bridge programs (BrΓΌckenangebote)
### Stellenmeldepflicht (since 2020)
Occupations with β₯5% unemployment rate must be reported to the RAV before posting publicly. The list changes annually. This is directly relevant for vocational counseling β these professions have highest availability for Swiss job seekers.
---
## Portfolio Synergies
| Server | Synergy |
|--------|---------|
| `swiss-statistics-mcp` | BFS population/employment data for deeper context |
| `zurich-opendata-mcp` | City of Zurich-level education and social data |
| `swiss-snb-mcp` | Economic context (GDP, wages) for labor market interpretation |
| `fedlex-mcp` | ALV (Arbeitslosenversicherung) legislative framework |
---
## Known Limitations
- `amstat.arbeit.swiss` has no public REST API (JavaScript SPA) β workaround via CKAN
- Occupational/sectoral detail requires CSV download from SECO resources
- Monthly press report URL patterns may vary for older reports
- Cantonal sub-municipal data not available at this level
- UVG figures come from PDF parsing β the layout was stable across the 2025 and
2026 editions, but a redesign can break it. The `totals_check` in every
response is what makes such a break visible rather than silent.
- UVG data lags roughly two years (the 2026 edition reports 2024)
- UVG branch detail follows NOGA 2008 and groups some divisions (`41 β 42`,
`77, 79 β 82`); there is no cantonal breakdown at this level
- Detailed UVG data beyond the publications sits behind the SSUV closed user
group and is out of scope for this no-auth server
**Phase 2 roadmap:**
- Automatic CSV caching with 24h TTL
- Direct XLSX parsing for cantonal breakdowns
- Integration with `zh-education-mcp` for Schulamt-specific correlations
---
## Data License
Two different licences apply β the code of this server is MIT either way, but the
data is not covered by it.
**SECO / AMSTAT data** published on opendata.swiss is under **Creative Commons
CCZero** (public domain).
Source: Staatssekretariat fΓΌr Wirtschaft (SECO) β [seco.admin.ch](https://www.seco.admin.ch)
**Unfallstatistik UVG data** is **not** openly licensed. The publication states:
> Β«Abdruck β ausser fΓΌr kommerzielle Nutzung β mit Quellenangabe gestattet.Β»
> (Reproduction permitted, except for commercial use, with attribution.)
That is a non-commercial restriction with an attribution requirement. It belongs
to KSUV/SSUV and cannot be lifted by this repository's MIT licence: the MIT terms
cover the code, not the figures the code retrieves. **If you use this server
commercially, the UVG tools are not covered** β clarify directly with the
Sammelstelle (`unfallstatistik@suva.ch`). Every UVG response repeats this
restriction in its `source` field, because a README is not passed to the model.
---
## Safety & Limits
| Aspect | Details |
|--------|---------|
| **Access** | Read-only (`readOnlyHint: true`) β the server cannot modify or delete any data |
| **Personal data** | No personal data β all sources are aggregated, anonymous public statistics |
| **Rate limits** | No enforced external limits; server caps queries at 20 results by default; 30 s HTTP timeout |
| **Authentication** | No API keys required β opendata.swiss and arbeit.swiss are publicly accessible |
| **Licenses** | SECO data under [Creative Commons CCZero](https://creativecommons.org/publicdomain/zero/1.0/) (public domain) |
| **Terms of Service** | Subject to ToS of: [opendata.swiss](https://opendata.swiss/de/terms-of-use), [SECO](https://www.seco.admin.ch), [arbeit.swiss](https://www.arbeit.swiss) |
| **GDPR / DSG** | Fully compliant β no personal data transmitted or stored; all data is official public statistics |
---
## MCP Protocol Version
The protocol version is negotiated at the `initialize` handshake by the SDK,
not chosen by this server. The revision it is built and audited against is
**`2025-11-25`**, which is `LATEST_PROTOCOL_VERSION` in the pinned `mcp`
release that fastmcp brings in.
`tests/test_protocol_version.py` holds three things against each other: this
line, that SDK constant, and the revision a real handshake against the server
object actually returns. An SDK bump that changes the revision therefore fails
CI instead of drifting silently.
The sister servers in this portfolio pin a *pair* of revisions β a handshake
ceiling and a modern one β because `mcp` 2.x serves two protocol eras over the
same server. fastmcp 3.x pins `mcp` 1.x, where `mcp.types.version` does not
exist and one revision is the whole story. `test_das_sdk_kennt_hier_nur_eine_aera`
is tied to the SDK rather than to this paragraph and fails the day an upgrade
brings the two-era constants in.
---
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md) for development guidelines.
---
## Security
See [SECURITY.md](SECURITY.md) for the security posture and how to report a
vulnerability.
---
## License
Released under the [MIT License](LICENSE) β Copyright Β© 2026 Hayal Oezkan.
---
## Author
**Hayal Oezkan** Β· [github.com/malkreide](https://github.com/malkreide)
<!-- mcp-name: io.github.malkreide/seco-labor-mcp -->
<!-- BEGIN GENERATED: install -->
## Installation
Run via [`uv`](https://docs.astral.sh/uv/)'s `uvx` β no clone or manual install needed. Add to your MCP client config (`mcpServers` for Claude Desktop, Cursor and Windsurf; use a top-level `servers` key for VS Code in `.vscode/mcp.json`):
```json
{
"mcpServers": {
"seco-labor-mcp": {
"command": "uvx",
"args": [
"seco-labor-mcp"
]
}
}
}
```
<!-- END GENERATED: install -->