v2 (Currently Only Available for Closed testing)

Getting started with the DZT MCP server (v2)

The DZT knowledge graph is reachable as a Model Context Protocol server: five search
tools over German tourism data — points of interest, trails, events and official tourism
regions.

Endpoint https://proxy.opendatagermany.io/api/its/mcp
Auth x-api-key: <your key> — request credentials from [email protected]

JSON-RPC 2.0 over POST. tools/list to discover, tools/call to invoke. Any MCP client
works without configuration; the API key is the only requirement.

What changed since v1

If you integrated against the first version, read this section before anything else.

v1v2
default responseJSON-LD structured contentGerman markdown, ready to answer from
output formatfixedchosen per request via x-output-format (markdown | jsonld | both)
duplicatesthe same entity could repeat within a pageone entity per result, guaranteed
paginationnone10 per page, page parameter, hasMore flag
multi-value inputscomma-separated stringsarrays
place filterslocality, region, postal_code separatelyone location covering all of them — the three old names are rejected with an error, not ignored
type filtercoarse classes onlyfull ODTA taxonomy, incl. subtypes
JSON-LD vocabularysome invented predicatesthe graph's own predicates

Breaking: locality and postal_code are gone — pass those values in location.
Multi-value parameters no longer accept comma-separated strings. The default response body no
longer contains JSON-LD; request it with x-output-format: jsonld

Your first call

curl -X POST https://proxy.opendatagermany.io/api/its/mcp \
  -H 'x-api-key: YOUR_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
       "params":{"name":"get_pois_by_criteria",
                 "arguments":{"type":["Museum"],"location":["Köln"]}}}'

The tool result is a JSON object inside result.content[0].text (and repeated as
result.structuredContent):

{
  "markdown": "Seite 1 – 10 Treffer. Weitere Treffer mit page=2.\n\n## 1. Domschatzkammer\n…",
  "page": 1,
  "pageSize": 10,
  "count": 10,
  "hasMore": true
}

markdown is written to be read: a heading per result, then its type, description and
fields. If you are feeding an LLM, pass it through unchanged — it needs no post-processing.

Choosing the response format

One header decides the body. Accept is not used for this; it belongs to the MCP
transport, and most clients will not let you override it per call.

x-output-formatbody contains
(omitted) or markdownmarkdown — the default
jsonldjsonld@graph in expanded JSON-LD
bothmarkdown and jsonld
anything elsefalls back to markdown

JSON-LD is expanded form — full IRI keys (https://schema.org/name), no @context, and
values as [{"@language":"de","@value":"…"}]. Predicates come straight from the graph, so
they are real: schema:servesCuisine, schema:amenityFeature, odta:length,
odta:uphillElevation. Compact it client-side with any JSON-LD processor if you prefer
CURIEs.

The five tools

Every tool takes language (two-letter code, default de) and page (1-based, 10 results
per page). All multi-value parameters are arrays.

Where these are searched, and how

parametersearched in the graphmatching
nameschema:namefuzzy (edit distance 2) and prefix
keywordsschema:name, schema:keywords, schema:description, schema:description/schema:textfuzzy on name and keywords; prefix only on description
typerdf:typeexact IRI
locationschema:address/schema:addressLocality, …/addressRegion, …/postalCode, schema:containedInPlace/schema:nameexact whole value, case-insensitive; prefix if location_prefix_match. City, tourism region, federal state and postal code are all this one field — matching is by NAME, never geographic containment
languageselects among language-tagged literalsrequested → deen → untagged

Keyword relevance is weighted: a hit in name outranks one in keywords, which outranks one
in the description.

get_pois_by_criteria

Places that exist permanently: museums, restaurants, castles, viewpoints, pools, wineries.

parametertypenotes
namestringa specific entity, e.g. Kölner Dom
keywordsarraytopical terms, matched across name, keywords and description
typearraysee Types below
locationarraycity, tourism region, federal state, postal code or containing place — see Places
location_prefix_matchbooleansee Places
near_pointstring"<radius>km,<lat>,<lon>", e.g. "10km,48.137,11.576"
cuisinearrayfree text, mostly German: italienisch, vegetarisch, regional
amenitiesarraya label (Sauna) or an ODTA IRI (odta:ParkingLots)
accessible_for_freebooleanfree entry only
parametersearched in the graphmatching
cuisineschema:servesCuisinephrase match, analyzed
amenitiesschema:amenityFeature/schema:name (labels) or schema:amenityFeature/schema:valueReference (IRIs)phrase match on labels; exact, case-insensitive on IRIs
accessible_for_freeschema:isAccessibleForFreeexact boolean
near_pointgeo:hasGeometry/geo:asWKTgeo radius

get_trails_by_criteria

Hiking, cycling and walking routes.

parametertypenotes
difficultyarrayleicht, mittel, schwer (English also accepted)
max_length_kmnumbersee Known limitations
max_ascent_mnumbersee Known limitations
max_duration_minnumbermatched to the hour; the source stores ISO-8601 durations
is_circularbooleancircular routes only
near_pointstringas above
near_modestringstart (default) — starts near the point; passes — passes near it (relevant for trails)

Plus name, keywords, type, location, location_prefix_match.

parametersearched in the graphmatching
difficultyodta:difficulty (vocabulary IRI) and odta:difficulty/schema:name (German label)exact IRI, or analyzed match on the label
max_length_kmodta:length/schema:valuenumeric range — see Known limitations
max_ascent_modta:uphillElevation/schema:valuenumeric range — see Known limitations
max_duration_minodta:estimatedDuration/schema:namematched to the hour; the source stores ISO-8601 (PT4H13M)
is_circularodta:circularTrailexact boolean
near_point + near_modeodta:startLocation/geo:hasGeometry/geo:asWKT (start) or geo:hasGeometry/geo:asWKT (passes)geo radius

For trails, location additionally covers the start and end points —
odta:startLocation/schema:address/schema:addressLocality, the same via odta:endLocation,
and the schema:containedInPlace/schema:name of either. So a town name finds trails that
start there, not only ones whose own address is there.

get_events_by_criteria

Things happening on dates: markets, festivals, guided tours.

parametertypenotes
dateRangeStartstringYYYY-MM-DD; any other format is rejected
dateRangeEndstringYYYY-MM-DD
attendance_modestringoffline, online, mixed
near_pointstringmatches the venue's location

Plus name, keywords, type, location, location_prefix_match.

A recurring event has many occurrences. When you pass a date range, the startDate returned
is the next occurrence inside that range — not the event's earliest date ever.

parametersearched in the graphmatching
dateRangeStart / dateRangeEndschema:startDate and schema:eventSchedule/schema:startDatedate range; a recurring event matches on any occurrence in the window
attendance_modeschema:eventAttendanceModeexact, case-insensitive
near_pointschema:location/geo:hasGeometry/geo:asWKT — the venue's geometrygeo radius

For events, location resolves through the venue:
schema:location/schema:address/schema:addressLocality and
schema:location/schema:containedInPlace/schema:name. An event has no address of its own.

get_touristic_regions_by_criteria

The 143 official German tourism regions (Reisegebiete). Takes name, keywords.
Use it to resolve a region's canonical spelling before filtering other tools by location.

parametersearched in the graphmatching
name, keywordsas abovefuzzy on the name, prefix on descriptions

The tool is scoped to the 143 entities whose schema:identifier/schema:value is
Reisegebiete (RG250).

get_entity_details

Everything known about one entity.

parametertypenotes
uristringrequired — take it from a previous result
languagestringoptional, defaults to de

Returns the full record rather than a search summary, including every image with its
copyright and licence. x-output-format applies here exactly as it does to the search
tools: markdown by default, JSON-LD only on request. Until 2026-08-18 this one tool ignored
the header and always returned both — the default body is now ~87 % smaller (4.2 KB instead
of 32.6 KB), so a client that read structuredContent["@graph"] without sending a header
must now send x-output-format: jsonld and read structuredContent.jsonld["@graph"].

uri is resolved directly — no search, no matching. language selects among language-tagged
literals with the same fallback as elsewhere (requested → deen → untagged).

Places: matching is exact

location matches the whole stored value, case-insensitively. Hyphen and space variants
are tried automatically, so Chiemsee-Alpenland also finds Chiemsee Alpenland.

Exactness is deliberate: it stops a search for one town returning a similarly-named one —
Erfurt will not return results from Querfurt.

The cost is that a short form misses a longer official name. Frankfurt does not match
Frankfurt am Main, and Erfurt does not match Erfurt - Molsdorf. Two options:

  • pass the full official name, or
  • set location_prefix_match: true, which also matches values beginning with your term

Prefix matching finds longer names, not neighbouring places. For "somewhere near here",
use near_point.

A worked example, on get_events_by_criteria — 6 events contain "erfurt" as a substring;
5 are genuinely in Erfurt and one is Querfurt:

queryreturns
{"location":["Erfurt"]}3 — those recorded exactly as Erfurt
{"location":["Erfurt"],"location_prefix_match":true}5 — adds two in Erfurt - Molsdorf
neitherthe Querfurt event, correctly excluded

The counts are tool- and data-specific: the same two queries against get_pois_by_criteria
both fill a page (count: 10, hasMore: true), because Erfurt has far more POIs than events.
What carries over is the behaviour — exact by default, prefix only when you ask, and never
a neighbouring town.

Types

type accepts a bare name (Museum), a prefixed name (odta:HikingTrail) or a full IRI
(https://odta.io/voc/HikingTrail) — always as an array. A bare name is tried against both
schema.org and ODTA.

The full ODTA taxonomy is filterable, including subtypes: odta:HikingTrail,
odta:BikeTourTrail, odta:ChristmasMarket, odta:FarmersMarket. (In v1 subtypes silently
returned nothing, because types resolved through
rdfs:label, which ODTA classes lack.)

If a very specific type returns nothing, retry with a broader one — Restaurant
FoodEstablishment.

Pagination

Ten results per page. hasMore tells you whether another page exists; to fetch it, repeat
the identical call with page incremented. Pages are disjoint and stable — ordering is by
relevance with a deterministic tiebreak, so an entity cannot appear on two pages.

count is the number of results on this page, not the total number of matches.

Invalid input is rejected, not ignored

A malformed filter fails the call with a message naming both what was expected and what
arrived — it is never silently dropped. Answering a question while quietly discarding the
user's constraint produces a confidently wrong result, which is worse than an error.

inputresponse
page: -3 or page: 1.5page must be a whole number >= 1, got: -3
near_point: "nahe München"near_point must be <radius>km,<lat>,<lon> — for example 10km,48.137,11.576
near_point: "10km,999,11.5"near_point latitude must be between -90 and 90
near_point: "0km,48.1,11.5"near_point radius must be greater than 0 km
dateRangeStart: "Dezember"dateRangeStart must be a date as YYYY-MM-DD, for example 2026-12-24
difficulty: ["ganz einfach"]difficulty must be leicht, mittel or schwer (or easy/medium/hard)
max_length_km: -5max_length_km must be greater than 0
near_mode: "irgendwas"near_mode must be start or passes

Type errors are caught by the input schema itself — passing type: "Museum" instead of
["Museum"] returns data/type must be array, and get_entity_details without a uri
returns data must have required property 'uri'. A uri that does not exist returns 404.

Errors arrive as an MCP tool error: HTTP 200 with result.isError: true, and the message in
result.structuredContent. The message is prefixed by an execution id, so read the tail.

Two values are deliberately not validated: attendance_mode and free-text fields like
cuisine and amenities. An unrecognised value there simply returns no results, which is an
honest answer rather than an error.

Known limitations

Trail length and ascent filters are unreliable. max_length_km and max_ascent_m
currently compare as text rather than numbers, so they return incorrect sets. Read the length
and ascent values from the results instead of relying on the filter. A fix is in progress.

Coverage is uneven across Germany. Some regions are densely described, others barely.
An empty result frequently means missing data rather than a missing attraction

Keyword search is fuzzy on names, exact-prefix on descriptions. A term is matched with
tolerance for typos against names and keywords, and as a prefix against descriptions. This
keeps long prose from matching almost anything.

Practical notes

  • Data follows ODTA Domain Specifications v2.2. Each entity carries compliesWith,
    naming the specification it was validated against.
  • Call tools/list rather than hard-coding schemas; parameters are extended over time.

Disclaimer

This documentation was created by AI and audited by a person.




Did this page help you?