Skip to main content
Glama
data-bs-mcp
by DCC-BS

data-bs-mcp

MCP server for any Huwise/Opendatasoft data portal — query open datasets from data.bs.ch (and other portals on the same platform) through the Explore 2.1 API.

It runs in two modes:

  • stdio — for local MCP clients (opencode, Cursor, Claude Desktop).

  • streamable HTTP — hosted as a container, so it can be wired into ChatGPT connectors and OpenWebUI. Also ships a skill as a no-server alternative.

Installation

The toolchain (uv only; Python version comes from pyproject.toml) and the task runner are managed by mise. Enter the project and trust the config once:

mise trust
mise install            # provisions uv, runs the postinstall hook

Then set up the virtual environment and dependencies:

mise run install        # alias: i — uv sync --locked

The postinstall hook runs install automatically on tool provisioning.

Related MCP server: mcp-data-strasbourg

Tasks

All standard DCC task names are available via mise run <task>; see mise.toml. In Docker the install task syncs --no-dev (driven by DOCKER_BUILD=1).

Task

Alias

Description

mise run install

i

Create venv and install deps (uv sync --locked)

mise run dev

d

MCP server over stdio (local clients)

mise run dev:http

dh

Streamable HTTP on :8000 for local testing

mise run check

c

Verify lockfile, format, and lint (ruff)

mise run test:unit

t

Run the unit test suite (pytest)

Selecting a catalog

The catalog is chosen by whoever runs the server. Configuration is read from the DATA_PORTAL_DOMAIN environment variable first, else a local .env file next to main.py. Copy the example and fill it in:

cp .env.example .env
# .env
DATA_PORTAL_DOMAIN=data.bs.ch
MCP_ALLOWED_HOSTS=mcp.bs.ch:*   # optional, see Hosting

The API base URL is built as https://<domain>/api/explore/v2.1. All Huwise/Opendatasoft portals share this path, so changing the domain targets a different portal.

Hosting (streamable HTTP)

Deploy as a container. The image is built against the DCC shared base image (ghcr.io/dcc-bs/dcc-docker-images/mise:13-slim); see Dockerfile.

The MCP endpoint is mounted at /mcp (streamable HTTP).

docker build -f Dockerfile -t mcp-data-bs .
docker run --rm -p 8000:8000 -e DATA_PORTAL_DOMAIN=data.bs.ch mcp-data-bs

Healthcheck: GET /healthz -> {"status":"ok"}.

API reference

The HTTP server exposes an OpenAPI description of the MCP endpoint with a rendered Swagger UI:

  • GET /docs — interactive reference (Swagger UI). Each tool appears as a tools/call request schema with a working example you can execute against the server via Try it out.

  • GET /openapi.json — the raw OpenAPI 3.1 spec.

The spec is generated from the live MCP tool registry at request time, so new tools appear automatically. It covers all endpoints (POST /mcp, GET /healthz, GET /). Responses are text/event-stream; each data: frame carries a JSON-RPC message. The server is stateless — requests need no initialize handshake and no session id.

Client compatibility: POST requests with incomplete Accept headers (e.g. only application/json or */*) are rewritten to application/json, text/event-stream by a middleware before the transport validates them, so plain JSON-RPC clients work unmodified. GET / returns a JSON status page (server, transport, endpoint links) instead of holding an SSE connection open — clients expecting a traditional SSE handshake get an immediate, meaningful response.

Remote hosting requires MCP_ALLOWED_HOSTS. The MCP HTTP endpoint has DNS-rebinding protection on by default and, without config, accepts only localhost Host headers — a remote deploy would get 421 Invalid Host header. When the server is reachable via a public hostname, list it (comma-separated, port-wildcard allowed): MCP_ALLOWED_HOSTS="mcp.bs.ch:*". When unset, protection is disabled so any Host header is accepted.

Docker Compose

compose.yml pulls the published GHCR image, sets the domain and allowed hostname(s), and healthchecks: docker compose up -d.

Publishing (CI)

This repo uses the DCC reusable workflows (ci-workflows):

  • .github/workflows/ci.yml runs mise run check and mise run test:unit automatically on push/PR (tasks absent from mise.toml are skipped).

  • .github/workflows/publish.yml (manual workflow_dispatch) builds and pushes to GHCR using publish-docker.yml@v2. Bump the version in pyproject.toml, then dispatch to tag the image <version> + latest.

Connecting clients

The snippets below are self-contained: stdio clients launch the server from GitHub via uvx (needs uv installed), HTTP clients point at the hosted deployment. Set DATA_PORTAL_DOMAIN to another portal domain to target a different catalog.

Antigravity CLI (agy) — one command:

agy mcp add --env DATA_PORTAL_DOMAIN=data.bs.ch databs -- \
  uvx --from git+https://github.com/DCC-BS/mcp-data-bs data-bs-mcp

Claude Desktop / Cursor — add to claude_desktop_config.json (Claude Desktop → Settings → Developer → Edit Config) or ~/.cursor/mcp.json:

{
  "mcpServers": {
    "data-bs": {
      "command": "uvx",
      "args": ["--from", "git+https://github.com/DCC-BS/mcp-data-bs", "data-bs-mcp"],
      "env": { "DATA_PORTAL_DOMAIN": "data.bs.ch" }
    }
  }
}

OpenWebUI / ChatGPT — point at the streamable HTTP endpoint https://mcp.data.bs.ch/mcp (no auth):

  • OpenWebUI: Settings → Tools → add https://mcp.data.bs.ch/mcp

  • ChatGPT (developer-mode connector): Settings → Connectors → Create → add https://mcp.data.bs.ch/mcp; all five tools are exposed and usable, and the interactive API reference lives at https://mcp.data.bs.ch/docs.

Other clients

  • opencode: add to OpenCode config:

    {
      "mcpServers": {
        "data-bs": {
          "command": "uv",
          "args": ["--directory", "/ABSOLUTE/PATH/TO/data-bs-mcp", "run", "main.py"]
        }
      }
    }
  • uvx (anywhere): set DATA_PORTAL_DOMAIN in your environment first (the .env is no longer committed). uvx runs the same code as a local checkout.

    DATA_PORTAL_DOMAIN=data.bs.ch \
      uvx --from git+https://github.com/DCC-BS/mcp-data-bs data-bs-mcp

Skills (no MCP needed)

Prefer not to run a server? Install the agent skill instead — it teaches an agent to hit the public REST API directly with plain HTTP (curl/httpx). See skills/data-bs/SKILL.md; copy or symlink it into your agent's skills directory (e.g. ~/.agents/skills/data-bs).

Tools

get_datasets

Search and list available datasets.

Two search modes:

  • semantic (default): ranks the catalog by meaning using the vector_similarity explore endpoint from Huwise. Best for natural-language / conceptual queries. Matches synonyms and other languages.

  • lexical: classic full-text match on the exact terms.

# semantic (default) — natural language, ranked by relevance
get_datasets(search="air quality measurements")

# lexical — exact full-text match
get_datasets(search="luft", search_mode="lexical")

# combine with facet filters
get_datasets(search="bevölkerung", refine="publisher:Statistisches Amt")

get_datasets also returns next_offset for paging (null when there is no next page).

get_dataset

Get detailed metadata for a specific dataset (fields with name, type, label, short description, unit, sortable/facet flags; publisher; url, title, modified).

get_dataset(dataset_id="100113")

get_records

Query records from a dataset with ODSQL filtering. The result is citable on its own: dataset_id, url, title, modified (the dataset's Stand; title and modified come from a cached catalog lookup and are omitted if it fails), plus total_count, next_offset (null on the last page) and results. Output is capped at about 12,000 characters; if rows were cut, truncated: true and a hint say so (use select, group_by, a smaller limit or next_offset).

get_records(dataset_id="100113", where="pm25 > 10", limit=100, order_by="time DESC")

get_facets

Get available facet values for filtering. Without facet, every facet is returned with at most 30 values and total_values; with a name, up to 100.

get_facets(facet="publisher")  # Options: publisher, keyword, theme, features, modified, language

export_dataset_url

Get download URL for dataset export. Returns an object {download_url, format, dataset_id, url, title, modified} (older versions returned the URL as a plain string).

export_dataset_url(dataset_id="100113", format="csv", where="sensornr=240")

Formats: csv, json, geojson, xlsx, shp, parquet, gpx, kml, rdfxml, jsonld, turtle

Debug

npx @modelcontextprotocol/inspector uv run main.py

Available Tools

5 tools
export_dataset_urlGet Export URLA

Generate a download URL for exporting a dataset in various formats (CSV, JSON, GeoJSON, XLSX, Shapefile, Parquet, etc.). Use this when you need to download or share dataset exports.

Inputs: dataset_id (required), where, format. Returns: result

get_datasetGet Dataset MetadataA

Get detailed metadata for a specific dataset including field definitions, schema, publisher info, and record count. Use this to understand a dataset's structure before querying records.

Inputs: dataset_id (required).

get_datasetsSearch DatasetsA

Search and list available open datasets from data.bs.ch. Two modes: 'semantic' (default) ranks the catalog by meaning using natural-language queries (handles synonyms and other languages); 'lexical' does a classic full-text match on the exact terms. Use semantic for conceptual discovery, lexical for precise term/name lookups.

Inputs: limit, offset, refine, search, +5 more.

get_facetsGet Facet ValuesB

Get available filter values for categorizing datasets. Useful for discovering publishers, keywords, themes, or other facets to refine dataset searches.

Inputs: facet.

get_recordsQuery Dataset RecordsA

Query and filter records from a dataset using ODSQL syntax. Use this to retrieve actual data from a dataset with optional WHERE clauses, ordering, and pagination.

Inputs: dataset_id (required), lang, limit, where, +8 more.

Tool Schema Changelog

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

Loading tool schema changelog…

    TDQS

    A3.9/5.0

    Scored across 5 tools

    Disambiguation4/5

    Each tool targets a distinct stage: search (get_datasets), metadata (get_dataset), facets (get_facets), records (get_records), and export (export_dataset_url). Minor potential confusion exists between get_datasets and get_facets for discovery, but descriptions clarify their different purposes.

    Naming Consistency5/5

    All tools follow a verb_noun pattern, with four using 'get_' and one using 'export_', both being clear verbs. The naming is consistent and predictable across the set.

    Tool Count5/5

    Five tools is well-scoped for a data portal: search, metadata, facets, records, and export cover the core read workflows without unnecessary bloat.

    Completeness5/5

    The set covers the full read lifecycle: discover datasets, understand their structure, query records, and export data. No obvious missing operations for a public read-only open data portal.

    Maintenance

    ActivityActive
    ResponsivenessLimited data

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      B
      maintenance
      MCP server for accessing Strasbourg Open Data (data.strasbourg.eu). Allows searching datasets, retrieving metadata, and querying records with ODSQL.
      109 npm
      MIT
    • A
      license
      A
      quality
      D
      maintenance
      An MCP server to find and fetch Norwegian open government data from data.norge.no. Enables search, metadata retrieval, and data download.
      5
      4
      MIT
    • A
      license
      Not graded
      quality
      B
      maintenance
      MCP server for the French open-data catalogue data.gouv.fr, enabling dataset search and retrieval, organization lookup, and reuse discovery via natural language queries.
      101 npm
      MIT