An MCP (Model Context Protocol) server for querying the MEM (Metadata for Education Media) ontology, which models German school curricula across federal states (Bundesländer).
The server provides the following tools:
sparql_query— Execute arbitrary SPARQL SELECT queries against the MEM triple storelist_bundeslaender— List all German federal states available in the ontologylist_schulfaecher— List all school subjects for a given statelist_schularten— List all school types for a given statefind_lehrplaene— Find curricula by state, optionally filtered by subject, school type, or grade levelget_lehrplan_tree— Get the hierarchical structure of a Lehrplan (bounded bydepth, default 2, max 10)get_children— Get direct children of a specific node (for drilling down into a branch)get_kompetenzen— Flat catalogue of all descendant nodes (Kompetenzerwartungen, Lernbereiche, Inhalte, …) reachable from matching Lehrpläne, annotated with their class label. Takes the same filters asfind_lehrplaene.search— Full-text search across all Lehrplan nodes by keyword (uses Virtuosobif:contains), with optional Bundesland filter
npm install
npm run buildAll configuration is via a .env file in the project root (loaded automatically at startup). Copy the example and edit as needed:
cp .env.example .envThe server will fail at startup if required variables are missing.
Infrastructure graphs (required):
| Variable | Description |
|---|---|
SPARQL_ENDPOINT |
SPARQL endpoint URL |
GRAPH_ONTOLOGY |
Ontology graph URI |
GRAPH_SCHULART |
Schulart graph URI |
GRAPH_SCHULFACH |
Schulfach graph URI |
State graphs (optional, add one per state):
| Variable | Description |
|---|---|
GRAPH_STATE_<CODE> |
Graph URI for a state, e.g. GRAPH_STATE_SN, GRAPH_STATE_BY |
State graphs are discovered dynamically — adding a new state requires only a new GRAPH_STATE_<CODE> entry in .env.
Add a .mcp.json file in the project root:
{
"mcpServers": {
"mem-ontology": {
"type": "stdio",
"command": "node",
"args": ["build/index.js"]
}
}
}Add to your MCP settings configuration file:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Linux:
~/.config/Claude/claude_desktop_config.json
{
"mcpServers": {
"mem-ontology": {
"command": "node",
"args": ["/absolute/path/to/mem-ontologie-mcp/build/index.js"]
}
}
}Tool: find_lehrplaene
Arguments: { "bundesland": "SN", "schulfach": "Biologie", "schulart": "Gymnasium" }
Tool: get_lehrplan_tree
Arguments: { "lehrplanUri": "https://lp-sachsen.org/resource/522", "depth": 2 }
Tool: get_children
Arguments: { "nodeUri": "https://lp-sachsen.org/resource/7052" }
Tool: search
Arguments: { "query": "Wirbeltiere" }
Tool: search
Arguments: { "query": "Fische", "bundesland": "SN" }
If you want to consume the same ontology from a web frontend — typically a Bundesland → Schulart → Schulfach → Lehrplan dropdown cascade with a topic-node tree — see docs/curriculum-picker.md. It documents the query templates, when you need a server proxy (CORS, mostly), and the gotchas (transitive BFO_0000051 over-assertion, untagged labels, the optional Schulart filter).
A runnable reference implementation lives in examples/curriculum-picker-demo/ — Node proxy plus vanilla-JS frontend, no build step. node proxy.mjs, then open http://localhost:5174.
- BW — Baden-Württemberg
- BY — Bayern
- BE — Berlin
- BB — Brandenburg
- HB — Bremen
- HH — Hamburg
- HE — Hessen
- MV — Mecklenburg-Vorpommern
- NI — Niedersachsen
- NW — Nordrhein-Westfalen
- RP — Rheinland-Pfalz
- SL — Saarland
- SN — Sachsen
- ST — Sachsen-Anhalt
- SH — Schleswig-Holstein
- TH — Thüringen
Note: Currently, curriculum data is available for BY (Bayern), SN (Sachsen), RP (Rheinland-Pfalz), BB (Brandenburg), and BE (Berlin).
- Transport: stdio (default) or Streamable HTTP (
--httpflag) - SDK:
@modelcontextprotocol/sdkwithMcpServerandzodschemas - Data source: SPARQL endpoint (configurable via
SPARQL_ENDPOINTenv var) - Runtime: Node.js, TypeScript, ES modules
Source files:
src/index.ts— Main MCP server with all tool registrationssrc/http.ts— Streamable HTTP transport (Express server)src/sparql.ts— SPARQL query execution and result formatting
npm run build # Compile TypeScript
npm start # Run the server (stdio)
npm run start:http # Run the server (HTTP)Run with --http flag (or npm run start:http) to expose the MCP server as an HTTP service using Streamable HTTP transport.
Configuration (via .env or environment variables):
| Variable | Default | Description |
|---|---|---|
PORT |
3000 |
HTTP listen port |
API_KEY |
(unset) | Bearer token for auth. If unset, auth is disabled. |
Authentication: When API_KEY is set, all /mcp requests must include Authorization: Bearer <key>. Without API_KEY, the server is open access.
Client connection example:
# Test with MCP Inspector
npx @modelcontextprotocol/inspector --url http://localhost:3000/mcp
# With authentication
npx @modelcontextprotocol/inspector --url http://localhost:3000/mcp --header "Authorization: Bearer your-secret-key-here"A Dockerfile and docker-compose.yml are included for self-hosted deployment. The container runs the server in HTTP mode behind your own reverse proxy (TLS is expected to be terminated upstream).
Container images are built by Forgejo Actions on every push to main and every vX.Y.Z tag, then published to the Forgejo registry at git.edufeed.org/laoc/mem-mcp. Available tags: :latest, :main, :sha-<short>, :vX.Y.Z.
# One-time on the deploy host: authenticate to the Forgejo registry.
# Skip this step if the package is set to public in Forgejo.
docker login git.edufeed.org
# First-time setup: configure environment.
cp .env.example .env
# Set API_KEY to a random secret for production.
# Deploy / update.
docker compose pull
docker compose up -d
# Logs
docker compose logs -fBy default the container binds to 127.0.0.1:3000 on the host, so it's only reachable via a reverse proxy on the same machine. Adjust the ports: entry in docker-compose.yml if you need LAN exposure.
Before exposing the service publicly:
- Pick a hostname. Replace
mcp.example.orgin the reverse-proxy snippets below with your real FQDN and create the DNS record. - Set
API_KEY. When unset, the server is open access — fine for stdio or LAN, not for the public internet. Generate one withopenssl rand -hex 32and put it in.env. Clients then sendAuthorization: Bearer <key>. - Pin the image tag.
:latestmakes updates implicit. For deliberate rollouts, changeimage:indocker-compose.ymlto a release tag (:vX.Y.Z) or commit pin (:sha-<short>) and bump it on purpose. - Confirm registry access.
git.edufeed.org/laoc/mem-mcpmay be private. Ifdocker compose pullfails withunauthorized, rundocker login git.edufeed.orgonce on the host (or set the package to public in Forgejo). - Allow egress to the SPARQL endpoint. The container must reach
https://sparql.mem.edufeed.org/sparql/(or whateverSPARQL_ENDPOINTpoints at). If outbound traffic is firewalled, allow-list that host.
docker build -t mem-ontology-mcp:dev .
docker run --rm -p 127.0.0.1:3000:3000 --env-file .env mem-ontology-mcp:devmcp.example.org {
reverse_proxy 127.0.0.1:3000
}location /mcp {
proxy_pass http://127.0.0.1:3000/mcp;
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;
# Streamable HTTP uses SSE for server->client messages.
proxy_buffering off;
proxy_read_timeout 3600s;
}Clients then connect to https://mcp.example.org/mcp with Authorization: Bearer <API_KEY>.
Unlicense