Skip to main content
Glama
thodel

HGB Basel MCP Server

by thodel

Königsfelden — MCP Server

An MCP server that exposes the Königsfelden corpus — the records of the Cistercian convent and the Hofmeisterei Königsfelden (1300–1658), edited by Die Urkunden und Akten des Klosters und der Hofmeisterei Königsfelden — to Claude and other MCP-compatible clients.

Architecture

data/docs/*.xml       (TEI, one file per register entry)  ─┐
data/registers/                                            ├─► build_db.py ──► kf.db
  people.xml  places.xml  organizations.xml               ─┘                  (SQLite + FTS5)
                                                                                   │
                                                                              server.py
                                                              (mcp 2.0 MCPServer,
                                                               streamable HTTP)
                                                                                   │
                                                                    http://<host>:8001/mcp

The TEI sources are parsed once into a SQLite database with two FTS5 indexes. The server then runs stateless read-only queries against it (PRAGMA query_only).

The server targets mcp 2.0, which renamed the high-level server class (FastMCPMCPServer), removed mcp.server.fastmcp, and moved the bind address from the constructor into run(); requirements.txt pins the major version accordingly.

The transport is streamable HTTP (/mcp), not the legacy HTTP+SSE (/sse) this server used previously. SSE is deprecated, and — more practically — its handshake hands the client an absolute /messages/ path computed from the app's own mount point, which a client cannot reach when the server sits under a reverse-proxy sub-path. Streamable HTTP has one endpoint and no such handshake. Existing clients pointed at /sse must be repointed at the new endpoint; it is a deliberate cutover, not a compatible change.

Entity identifiers come from the TEI xml:id attributes — persons perXXXXXX, places locXXXXXX, organisations orgXXXX. Person and place records additionally carry HLS identifiers (and GND, for places) where the edition supplies them.

Related MCP server: barracuda-mcp

Setup

1. Install dependencies

pip install -r requirements.txt

2. Build the database

python build_db.py --docs ../data/docs --registers ../data/registers --db kf.db

--docs is a directory of per-entry TEI files; the entry id is the filename without its extension. --registers must contain people.xml, places.xml, and organizations.xml. Both default to ../data/docs and ../data/registers; --batch (default 200) controls the commit batch size.

Run it once, and again whenever the TEI changes. Rebuilding is destructive: the five tables the script owns (entries, spans, persons, places, orgs) and both FTS indexes are cleared and repopulated, so a rebuild always mirrors the current sources rather than accumulating duplicates. It prints Existing database: clearing N entries when it does this. Nothing else in the file is touched.

Malformed records are skipped individually and reported on stderr as WARNING: N record(s) skipped — check for that line, since the build otherwise completes normally.

3. Start the server

python server.py --db kf.db --host 0.0.0.0 --port 8001

Each flag also has an environment variable — KF_DB, KF_HOST, KF_PORT, KF_HTTP_PATH — which the flags override. Importing server.py never reads sys.argv, so it is safe to import from tests or an ASGI loader.

--http-path (default /mcp) is the path the MCP endpoint is served at. Behind a reverse proxy, set it to the public path — see Reverse proxy.

4. Connect a client

Claude Code — the name and URL are positional; there is no --url flag:

claude mcp add --transport http kf http://<server-ip>:8001/mcp -s user

-s user makes the server available in every project; -s project writes it to .mcp.json to share with a repository; the default local scope is just you, in the current project. claude mcp list then reports the connection status.

Claude Desktop, Cowork, claude.ai — Customize → Connectors → +Add custom connector, and paste the same URL. These clients connect from Anthropic's cloud rather than from your machine, so the server has to be reachable over the public internet; claude_desktop_config.json only configures local stdio servers, not remote URLs.

Project-scoped .mcp.json:

{
  "mcpServers": {
    "kf": {
      "type": "http",
      "url": "http://<server-ip>:8001/mcp"
    }
  }
}

type is required, and streamable-http is accepted as an alias for http. An entry with a url but no type is read as a stdio server and skipped with an error.


Docker deployment

Build image

docker compose build

First-time: build the database

Copy the TEI sources onto the server (the compose file mounts /home/dh/kf_data as /data), then:

docker run --rm -v /home/dh/kf_data:/data kf-mcp python build_db.py --docs /data/kf_raw/docs --registers /data/kf_raw/registers --db /data/kf.db

Run

docker compose up -d

The container serves on port 8001 and expects kf.db at /data/kf.db. Adjust the volume path in docker-compose.yml if your data lives elsewhere.

Reverse proxy (nginx)

Serving under a sub-path (https://tei.example.ch/mcp/kf/mcp) has exactly one rule: the app's --http-path and the nginx location must be the same string. The endpoint is one path that answers POST (requests), GET (the server→client stream), and DELETE (session teardown); it builds no URLs of its own, so all nginx has to do is forward the path unchanged.

server {
    listen 443 ssl;
    server_name tei.example.ch;

    # KF_HTTP_PATH=/mcp/kf/mcp — same string, no trailing slash on proxy_pass,
    # so the path reaches the app unrewritten.
    location /mcp/kf/mcp {
        proxy_pass         http://127.0.0.1:8001;
        proxy_http_version 1.1;
        proxy_set_header   Host $host;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto $scheme;
        # The GET stream must not be buffered or timed out mid-session.
        proxy_set_header   Connection '';
        proxy_buffering    off;
        proxy_cache        off;
        proxy_read_timeout 3600s;
        chunked_transfer_encoding on;
    }
}

Two failure modes worth knowing, both of which return a bare Not Found or 405:

  • A trailing slash on proxy_pass (http://127.0.0.1:8001/) strips the location prefix, so the app sees / and no route matches.

  • location and --http-path disagree — the app 404s every request. Check the startup line, which prints the exact path being served: Starting KF MCP server on 0.0.0.0:8001/mcp/kf/mcp.

Verify from outside before wiring up a client:

curl -sS -o /dev/null -w '%{http_code}\n' -X POST https://tei.example.ch/mcp/kf/mcp -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"1"}}}'

200 means the endpoint is live. 404 is a path mismatch, 405 means nginx is not passing POST to the app (a static location or a limit_except is shadowing it).

Note: the server has no authentication. By default docker-compose.yml publishes port 8001 on all interfaces; if a proxy fronts it, bind it to loopback instead so the corpus is not reachable directly:

KF_BIND=127.0.0.1 docker compose up -d

Otherwise restrict access at the firewall.


Available tools

Tool

Description

corpus_stats()

Entry/span/person/place/org counts and year range

list_entries(limit=50, offset=0)

Paginated list of register entries, ordered by year

get_entry(entry_id)

Full entry: title, year, source, pages, transcription, all spans

search_persons(query, limit=50)

Person authority file by name (substring match)

get_person(pid)

Person record with HLS id, occupation, life dates, and mentions

search_places(query, limit=50)

Place authority file by name (German or French)

get_place(pid)

Place record with geo, HLS id, GND id, and mentions

search_orgs(query, limit=50)

Organisation authority file by name or description

search_fulltext(query, limit=20)

Full-text search over transcriptions, with snippets

get_entries_for_person(pid, limit=50)

Entries mentioning a person, by authority id

get_entries_for_place(pid, limit=50)

Entries mentioning a place, by authority id

get_entries_by_year(year_from, year_to, limit=100)

Entries in a year range (max span 300 years)

Available resources

URI

Description

kf://stats

Corpus statistics (JSON)

kf://persons

Person index — {total, returned, truncated, persons: [...]}, capped at 1000 rows and flagged when truncated

kf://entry/{entry_id}

Single entry (JSON)

Query behaviour

Limits. Every limit is clamped to at most 500; a negative, zero, or non-numeric value falls back to that tool's own default rather than returning the whole table. Use list_entries(limit, offset) to page through the full corpus.

Result size. Claude.ai and Claude Desktop truncate a tool or resource result at roughly 150,000 characters. kf://persons is capped at 1000 rows (about 100 KB) for that reason and reports its own truncation; the 500-row tool ceiling stays comfortably under the limit too.

Full-text search. search_fulltext passes the query to FTS5, so operators work — Brugg OR Königsfelden, Heinr*, NEAR(...). If the query isn't valid FTS5 syntax (a stray quote, a dangling AND), it silently falls back to a literal word search instead of erroring. Only a query with no usable words returns {"error": ...}.

Name search. search_persons, search_places, and search_orgs do a plain case-insensitive substring match. SQL wildcards in the query are escaped, so searching for 100% finds a literal "100%" rather than matching every record.

Missing records. get_entry, get_person, and get_place return {"error": "... not found."} rather than raising.

Spans. get_entry returns every span in the entry — persName, placeName, orgName, date, measure. The ref field holds the authority id and is empty for unlinked mentions and for dates/measures; norm holds the normalised @when or @quantity value.

How entry years are assigned. The year comes from the first <date when="..."> in the document <body>. If the body has no date, the <sourceDesc> in the header is used as a fallback. Dates in publicationStmt or revisionDesc are never used — they describe the edition, not the charter. Years outside 1000–1800 are ignored, and entries with no usable date have year = NULL.

Database schema

Table

Contents

entries

id, title, short_id, year, source, pages, text_raw

spans

entry_id, span_id, class, ref, text, norm

persons

id, forename, surname, full_name, main_name, occupation, birth, death, org_ref, hls_id, note

places

id, name_de, name_fr, country, region, geo, hls_id, gnd_id, place_type

orgs

id, name, desc_de, desc_fr

fts_entries, fts_spans

FTS5 indexes (external content, populated by AFTER INSERT triggers at build time — there are no update/delete triggers, which is why a rebuild clears and repopulates)

Deployment

This server runs on tei.dh.unibe.ch at https://tei.dh.unibe.ch/mcp/kf/mcp, alongside four sibling MCP servers: SSRQ, HLS, HBLS, EOS / HGB Basel.

What they share — the nginx routing, the landing pages, and the deploy sequence — lives in tei_mcp_ops. Start there for anything that spans the fleet; in particular, the app's --http-path and the nginx location have to be the same string, which is the rule a sub-path deployment turns on.

Tests

pip install -r requirements-dev.txt
pytest test_kf_mcp.py

Unit tests (TEI parsing, authority registers, rebuild idempotency) run with no setup. The DB and server tests skip unless you point them at a built database and a running server:

KF_DB=/home/dh/kf_data/kf.db KF_SERVER=http://localhost:8001 pytest test_kf_mcp.py

The suite also runs standalone, with grouped output and a non-zero exit on failure:

python test_kf_mcp.py --unit --db /home/dh/kf_data/kf.db --server http://localhost:8001

Note that the DB tests assert corpus-size floors (≥1550 entries, ≥5000 persons, ≥1300 places, ≥2000 orgs) — they will fail against a small sample database.

F
license - not found
-
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

  • A
    license
    A
    quality
    A
    maintenance
    Enables AI-native access to Swiss cultural heritage data from SIK-ISEA (artists), Nationalmuseum (collections), and Nationalbibliothek (bibliography) via MCP tools, without authentication.
    11
    MIT
  • A
    license
    -
    quality
    D
    maintenance
    Enables searching OpenAleph entities and documents using natural language queries through the MCP protocol.
    12
    1
    MIT
  • A
    license
    A
    quality
    A
    maintenance
    MCP server for Switzerland's national metadata catalogue, enabling AI agents to discover datasets, APIs, public services, and publishers through free-text search and structured queries.
    13
    MIT

View all related MCP servers

Related MCP Connectors

  • Agent-native MCP server over the public saagarpatel.dev corpus. Read-only, stateless.

  • Search your AI chat history (ChatGPT, Claude, Codex) from any MCP client. Remote, private, read-only

  • Agentic search over your Dewey document collections from any MCP-compatible client.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/thodel/kf_mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server