chore: opdater MCP servere (2026-10-22 20:47)

Synkronisering af 1 MCP server(e) fra OpenData.
This commit is contained in:
Christian Johannesen 2026-10-22 20:47:52 +02:00
commit 96d527ecdf
7 changed files with 821 additions and 0 deletions

128
README.md Normal file
View file

@ -0,0 +1,128 @@
# OpenDataMCP – Model Context Protocol (MCP) Servere / AI Tools
[🇩🇰 Dansk](#dansk) | [🇬🇧 English](#english)
---
<a name="dansk"></a>
## 🇩🇰 Dansk
Velkommen til **OpenDataMCP**-repositoriet. Dette arkiv indeholder officielle, produktionsklare **Model Context Protocol (MCP)** servere udarbejdet til AI-assistenter, agenter og sprogmodeller (Claude Desktop, Antigravity, Cursor, m.fl.).
> **Type:** Færdige, selvstændige MCP-servere og SQLite-sprogdatabaser
> **Seneste synkronisering:** `2026-10-22 18:47:51 UTC`
> **Aktive MCP-servere:** `1` stk. (5 filer, 32.4 MB)
### 📂 Mappestruktur
Hver MCP-server er placeret i sin egen undermappe med serverkode, SQLite-database, bygge-script og test-suite:
```text
OpenDataMCP/
├── README.md # Denne centrale tosprogede dokumentation
├── mcp_manifest.json # SHA-256 hash og filmanifest
├── cor/
│ ├── server.py # MCP Server (JSON-RPC / stdio)
│ ├── build_db.py # Database builder
│ ├── test_tools.py # Værktøjstest og validering
│ └── README.md # Serverspecifik dokumentation og opsætning
```
### 🛠️ Tilgængelige MCP Servere
#### 🔹 [**`cor`**](cor/)
Dansk sprog- og ordregisterserver baseret på Det Centrale Ordregister (COR) og Dansk Sprognævn. Udstiller værktøjer til ordverifikation, ordspilssøgning (Wordle, Scrabble), stavefejlskontrol og grammatisk analyse med over 386.000 ordformer.
- **Server-fil:** [`cor/server.py`](cor/server.py)
- **Filer:** 5 filer (32.4 MB)
- **Vejledning:** Se [`cor/README.md`](cor/README.md) for detaljerede instruktioner og opsætning i Claude Desktop / Cursor.
### ⚡ Hurtig Start (Eksempel med `cor`)
1. Klon repositoriet:
```bash
git clone https://git.p20.dev/christian/OpenDataMCP.git
cd OpenDataMCP/cor
```
2. Installer MCP-biblioteket:
```bash
pip install "mcp[cli]"
```
3. Tilføj til din Claude Desktop konfiguration (`claude_desktop_config.json`):
```json
{
"mcpServers": {
"dansk-cor": {
"command": "python",
"args": ["/sti/til/OpenDataMCP/cor/server.py"]
}
}
}
```
### 📜 Licenser & Kildeangivelse
- **COR MCP Server:** Creative Commons CC0 1.0 Universal (Det Centrale Ordregister / Dansk Sprognævn).
- **Webportal:** [opendata.p20.dev](https://opendata.p20.dev)
---
<a name="english"></a>
## 🇬🇧 English
Welcome to **OpenDataMCP**. This repository distributes ready-to-run **Model Context Protocol (MCP)** servers tailored for AI assistants, agents, and LLMs (Claude Desktop, Cursor, Antigravity, and compatible MCP hosts).
> **Type:** Production-ready MCP servers with pre-indexed SQLite databases
> **Last synchronized:** `2026-10-22 18:47:51 UTC`
> **Active MCP servers:** `1` (5 files, 32.4 MB)
### 📂 Directory Layout
Each MCP server resides in an isolated directory containing the server logic, automated tests, local database assets, and instructions:
```text
OpenDataMCP/
├── README.md # Root bilingual documentation
├── mcp_manifest.json # SHA-256 release manifest
├── cor/
│ ├── server.py # MCP Server implementation
│ ├── test_tools.py # Automated tool test suite
│ └── README.md # Client configuration guide
```
### 🛠️ Server Registry
#### 🔹 [**`cor`**](cor/)
Danish language and lexicographic register server based on Det Centrale Ordregister (COR) and the Danish Language Council. Exposes AI tools for word verification, anagram/Wordle/Scrabble pattern matching, spell-checking, and grammar analysis across 386k+ words.
- **Server Entrypoint:** [`cor/server.py`](cor/server.py)
- **Payload:** 5 files (32.4 MB)
- **Guide:** Consult [`cor/README.md`](cor/README.md) for full tool definitions and client configs.
### ⚡ Quickstart (`cor` Server)
1. Clone the repository:
```bash
git clone https://git.p20.dev/christian/OpenDataMCP.git
cd OpenDataMCP/cor
```
2. Install dependencies:
```bash
pip install "mcp[cli]"
```
3. Configure your MCP client (e.g. `claude_desktop_config.json`):
```json
{
"mcpServers": {
"dansk-cor": {
"command": "python",
"args": ["/path/to/OpenDataMCP/cor/server.py"]
}
}
}
```
### 📜 Licenses & Attribution
- **COR MCP Server:** Creative Commons CC0 1.0 Universal (Det Centrale Ordregister / Danish Language Council).
- **Web Portal:** [opendata.p20.dev](https://opendata.p20.dev)

74
cor/README.md Normal file
View file

@ -0,0 +1,74 @@
# Dansk COR MCP Server (Model Context Protocol)
En officiel og lynhurtig **Model Context Protocol (MCP)** server for det danske sprog baseret på datasættene fra [Det Centrale Ordregister (COR)](https://ordregister.dk/) og Dansk Sprognævn i [OpenData](https://opendata.p20.dev).
Serveren gør det muligt for AI-agenter (såsom Claude Desktop, Antigravity, Cursor eller andre MCP-kompatible klienter) at udføre præcise ordbogsopslag, mønstersøgning (Wordle, krydsord, Scrabble), officiel stavekontrol og grammatisk analyse.
---
## ⚡ Funktioner og Værktøjer (Tools)
Serveren udstiller 4 specialiserede værktøjer:
| Værktøj | Beskrivelse | Eksempel på parametre |
| :--- | :--- | :--- |
| `lookup_word` | Slår et dansk ord op. Afgør om ordet er godkendt, henter ordklasse(r), grundform (lemma), køn, uregelmæssige bøjningsformer og stavefejlsrettelser. | `{"word": "cykler"}` |
| `find_words` | Fleksibel ordsøger med støtte for vildtegn (`*`, `?`), regex, ordlængde, ordklasse (`sb`, `vb`, `adj` osv.), Wordle-filtre og palindromer. | `{"pattern": "k?tte", "is_wordle": true}` |
| `check_spelling` | Kontrollerer et ord mod COR.SOEGEHJAELP (Dansk Sprognævns officielle samling af almindelige fejl og tastefejl) og returnerer korrekte autoriserede former. | `{"word": "accellerere"}` |
| `grammar_features` | Slår grammatiske særtræk op: uregelmæssige verber, substantiver med valgfrit køn (*en/et*), *plurale tantum* (kun flertal), *singulare tantum* (kun ental) og ordklasse-kamæleoner. | `{"category": "valgfrit_koen", "query": "folie"}` |
---
## 📦 Forudsætninger & Installation
Serveren kører på Python 3.10+ og anvender den officielle `mcp` pakke samt en optimeret SQLite-database (`cor.db`).
### 1. Installer afhængigheder
```bash
pip install "mcp[cli]"
```
### 2. Generer SQLite-databasen
Databasen opbygges lynhurtigt (< 2 sekunder) direkte fra OpenData-repositoriets datalister:
```bash
python mcp/cor/build_db.py
```
Dette genererer `mcp/cor/cor.db` med over **386.000 ordformer**, 61.000 lemmaer og knap 9.000 registrerede stavefejl.
### 3. Kør automatiseret test
```bash
python mcp/cor/test_tools.py
```
---
## 🚀 Konfiguration i MCP-klienter
### Claude Desktop
Tilføj følgende til din `claude_desktop_config.json`:
```json
{
"mcpServers": {
"dansk-cor": {
"command": "python",
"args": [
"/sti/til/OpenData/mcp/cor/server.py"
]
}
}
}
```
> **Tip:** Udskift `/sti/til/OpenData/` med den faktiske absolutte sti til repositoriet på dit system.
### Antigravity / Cursor
Tilføj serveren under MCP-indstillinger som en `stdio`-server:
- **Command**: `python` (eller sti til dit Python-miljø)
- **Args**: `["<sti-til-OpenData>/mcp/cor/server.py"]`
---
## 🌐 Data og Licens
- Data stammer fra **Det Centrale Ordregister (COR)** og **COR.SOEGEHJAELP** udgivet af Dansk Sprognævn under **CC0 1.0 Universal** (Public Domain).
- Webportal og datadokumentation: [opendata.p20.dev](https://opendata.p20.dev)

230
cor/build_db.py Normal file
View file

@ -0,0 +1,230 @@
import os
import sqlite3
import csv
import json
import time
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
PROJECT_ROOT = os.path.abspath(os.path.join(BASE_DIR, "..", ".."))
DATA_DIR = os.path.join(PROJECT_ROOT, "data")
DB_PATH = os.path.join(BASE_DIR, "cor.db")
def create_database():
print(f"Opretter/overskriver SQLite-database: {DB_PATH}")
if os.path.exists(DB_PATH):
os.remove(DB_PATH)
conn = sqlite3.connect(DB_PATH)
cur = conn.cursor()
cur.execute("PRAGMA synchronous = OFF;")
cur.execute("PRAGMA journal_mode = MEMORY;")
# 1. Tabel for alle godkendte ordformer (Scrabble / Wordfeud / Wordle)
cur.execute("""
CREATE TABLE words (
word TEXT PRIMARY KEY,
length INTEGER NOT NULL,
is_wordle BOOLEAN NOT NULL DEFAULT 0,
is_palindrome BOOLEAN NOT NULL DEFAULT 0
) WITHOUT ROWID;
""")
# 2. Tabel for lemmaer (grundformer) og deres ordklasser
cur.execute("""
CREATE TABLE lemmas (
lemma TEXT PRIMARY KEY,
pos TEXT NOT NULL,
length INTEGER NOT NULL
) WITHOUT ROWID;
""")
# 3. Tabel for stavefejl og autoriserede rettelser (COR.SOEGEHJAELP)
cur.execute("""
CREATE TABLE typos (
typo TEXT NOT NULL,
correct_word TEXT NOT NULL,
pos_tag TEXT,
PRIMARY KEY (typo, correct_word)
) WITHOUT ROWID;
""")
# 4. Tabel for uregelmæssige verber
cur.execute("""
CREATE TABLE irregular_verbs (
infinitive TEXT PRIMARY KEY,
present TEXT NOT NULL,
past TEXT NOT NULL,
past_participle TEXT NOT NULL
) WITHOUT ROWID;
""")
# 5. Tabel for grammatiske kuriositeter og særkategorier
cur.execute("""
CREATE TABLE grammar_features (
word TEXT NOT NULL,
category TEXT NOT NULL,
note TEXT,
PRIMARY KEY (word, category)
) WITHOUT ROWID;
""")
conn.commit()
return conn
def populate_words_and_lemmas(conn):
cur = conn.cursor()
print("Indlæser ordformer og grundformer...")
# Hent palindromer til et opslagssæt
palindromes_path = os.path.join(DATA_DIR, "cor_ordlister", "palindromer", "palindromer.txt")
palindromes = set()
if os.path.exists(palindromes_path):
with open(palindromes_path, "r", encoding="utf-8") as f:
palindromes = {line.strip().lower() for line in f if line.strip()}
print(f"Palindromer fundet: {len(palindromes)}")
# Indlæs Scrabble alle former
scrabble_path = os.path.join(DATA_DIR, "cor_ordlister", "scrabble", "scrabble_alle_former.txt")
if os.path.exists(scrabble_path):
words_data = []
with open(scrabble_path, "r", encoding="utf-8") as f:
for line in f:
w = line.strip().lower()
if not w:
continue
w_len = len(w)
is_w = 1 if w_len == 5 else 0
is_p = 1 if w in palindromes else 0
words_data.append((w, w_len, is_w, is_p))
print(f"Indsætter {len(words_data)} ord i 'words'...")
cur.executemany("INSERT OR IGNORE INTO words (word, length, is_wordle, is_palindrome) VALUES (?, ?, ?, ?)", words_data)
conn.commit()
# Indlæs grundformer / lemmaer
lemma_csv = os.path.join(DATA_DIR, "cor_ordlister", "grundformer", "grundformer_lemmaer.csv")
if os.path.exists(lemma_csv):
lemma_data = []
with open(lemma_csv, "r", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
l = row["lemma"].strip().lower()
pos = row["ordklasser"].strip()
if l:
lemma_data.append((l, pos, len(l)))
print(f"Indsætter {len(lemma_data)} grundformer i 'lemmas'...")
cur.executemany("INSERT OR IGNORE INTO lemmas (lemma, pos, length) VALUES (?, ?, ?)", lemma_data)
conn.commit()
def populate_typos(conn):
cur = conn.cursor()
typos_csv = os.path.join(DATA_DIR, "cor_ekstra", "stavefejl", "typiske_stavefejl_og_rettelser.csv")
if os.path.exists(typos_csv):
typo_data = []
with open(typos_csv, "r", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
t = row["taste_stavefejl"].strip().lower()
c = row["korrekt_form"].strip().lower()
p = row.get("ordklasse", "").strip()
if t and c:
typo_data.append((t, c, p))
print(f"Indsætter {len(typo_data)} stavefejl i 'typos'...")
cur.executemany("INSERT OR IGNORE INTO typos (typo, correct_word, pos_tag) VALUES (?, ?, ?)", typo_data)
conn.commit()
def populate_grammar(conn):
cur = conn.cursor()
gram_dir = os.path.join(DATA_DIR, "cor_ekstra", "grammatik")
kurio_dir = os.path.join(DATA_DIR, "cor_ekstra", "kuriositeter")
# Uregelmæssige verber
uv_csv = os.path.join(gram_dir, "uregelmaessige_verber.csv")
if os.path.exists(uv_csv):
uv_data = []
with open(uv_csv, "r", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
inf = row["infinitiv"].strip().lower()
pres = row["præsens"].strip().lower()
past = row["præteritum"].strip().lower()
part = row["perfektum_participium"].strip().lower()
if inf:
uv_data.append((inf, pres, past, part))
print(f"Indsætter {len(uv_data)} uregelmæssige verber...")
cur.executemany("INSERT OR IGNORE INTO irregular_verbs VALUES (?, ?, ?, ?)", uv_data)
# Simple kategorier
categories = [
(os.path.join(gram_dir, "substantiver_valgfrit_koen.txt"), "valgfrit_koen", "Fælleskøn eller intetkøn (en/et)"),
(os.path.join(gram_dir, "plurale_tantum_kun_flertal.txt"), "plurale_tantum", "Findes kun i flertal"),
(os.path.join(gram_dir, "singulare_tantum_kun_ental.txt"), "singulare_tantum", "Findes kun i ental"),
(os.path.join(kurio_dir, "isogrammer_uden_gentagne_bogstaver.txt"), "isogram", "Alle bogstaver er unikke"),
(os.path.join(kurio_dir, "paafolgende_dobbeltkonsonanter.txt"), "dobbeltkonsonanter", "Påfølgende dobbeltkonsonanter"),
(os.path.join(kurio_dir, "q_uden_u.txt"), "q_uden_u", "Indeholder Q uden efterfølgende U"),
(os.path.join(kurio_dir, "tre_ens_bogstaver_i_traek.txt"), "tre_ens_i_traek", "Tre ens bogstaver i træk"),
(os.path.join(kurio_dir, "scrabble_svaere_korte_ord.txt"), "scrabble_svaere_korte", "Korte ord med C, X, Z, W, Q")
]
gf_data = []
for filepath, cat_name, note in categories:
if os.path.exists(filepath):
with open(filepath, "r", encoding="utf-8") as f:
for line in f:
w = line.strip().lower()
if w:
gf_data.append((w, cat_name, note))
# Ordklasse kamæleoner (CSV med flere ordklasser)
kam_csv = os.path.join(gram_dir, "ordklasse_kamaeleoner.csv")
if os.path.exists(kam_csv):
with open(kam_csv, "r", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
w = row["lemma"].strip().lower()
ok = row.get("ordklasser", "")
antal = row.get("antal_ordklasser", "")
if w:
gf_data.append((w, "ordklasse_kamaeleon", f"{antal} ordklasser: {ok}"))
# Semordnilaps (venderord)
sem_csv = os.path.join(kurio_dir, "semordnilaps_venderord.csv")
if os.path.exists(sem_csv):
with open(sem_csv, "r", encoding="utf-8") as f:
reader = csv.DictReader(f)
for row in reader:
w1 = row.get("ord1", "").strip().lower()
w2 = row.get("ord2", "").strip().lower()
if w1 and w2:
gf_data.append((w1, "semordnilap", f"Omvendt: {w2}"))
print(f"Indsætter {len(gf_data)} grammatiske og kuriositets-træk...")
cur.executemany("INSERT OR IGNORE INTO grammar_features VALUES (?, ?, ?)", gf_data)
conn.commit()
def create_indexes(conn):
cur = conn.cursor()
print("Opretter optimerede indeks...")
cur.execute("CREATE INDEX IF NOT EXISTS idx_words_length ON words(length);")
cur.execute("CREATE INDEX IF NOT EXISTS idx_words_wordle ON words(is_wordle);")
cur.execute("CREATE INDEX IF NOT EXISTS idx_words_pal ON words(is_palindrome);")
cur.execute("CREATE INDEX IF NOT EXISTS idx_lemmas_pos ON lemmas(pos);")
cur.execute("CREATE INDEX IF NOT EXISTS idx_typos_typo ON typos(typo);")
cur.execute("CREATE INDEX IF NOT EXISTS idx_grammar_cat ON grammar_features(category);")
conn.commit()
def main():
start = time.time()
conn = create_database()
populate_words_and_lemmas(conn)
populate_typos(conn)
populate_grammar(conn)
create_indexes(conn)
conn.close()
elapsed = time.time() - start
print(f"Færdig! SQLite-databasen er klar på {elapsed:.2f} sekunder.")
if __name__ == "__main__":
main()

BIN
cor/cor.db Normal file

Binary file not shown.

306
cor/server.py Normal file
View file

@ -0,0 +1,306 @@
"""
COR MCP Server
--------------
Model Context Protocol server for Det Centrale Ordregister (COR) og Dansk Sprognævn.
Udstiller værktøjer til ordverifikation, ordbogssøgning (Wordle/Scrabble/regex),
stavekontrol (COR.SOEGEHJAELP) og grammatiske særtræk.
"""
import os
import re
import sqlite3
from typing import Optional, List, Dict, Any
from mcp.server.mcpserver import MCPServer
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DB_PATH = os.path.join(BASE_DIR, "cor.db")
server = MCPServer(
name="opendata-cor",
version="1.0.0",
description="Officiel dansk sprog- og ordregisterserver baseret på COR (Det Centrale Ordregister, Dansk Sprognævn)."
)
def get_db_connection():
if not os.path.exists(DB_PATH):
raise FileNotFoundError(f"Databasen '{DB_PATH}' blev ikke fundet. Kør build_db.py først.")
conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row
return conn
@server.tool(
name="lookup_word",
description="Slå et dansk ord op i COR. Undersøger om ordet er en godkendt form, henter grundform (lemma), ordklasse(r), grammatiske særtræk og bøjningsstatus."
)
def lookup_word(word: str) -> Dict[str, Any]:
"""
Slår et ord op.
:param word: Ordet der skal slås op (f.eks. 'cykler', 'betydning', 'gik').
"""
clean = word.strip().lower()
conn = get_db_connection()
cur = conn.cursor()
# 1. Tjek om ordet findes som gyldig godkendt form
cur.execute("SELECT word, length, is_wordle, is_palindrome FROM words WHERE word = ?", (clean,))
word_row = cur.fetchone()
# 2. Tjek om ordet er et lemma (grundform)
cur.execute("SELECT lemma, pos, length FROM lemmas WHERE lemma = ?", (clean,))
lemma_row = cur.fetchone()
# 3. Tjek grammatiske særtræk
cur.execute("SELECT category, note FROM grammar_features WHERE word = ?", (clean,))
grammar_rows = cur.fetchall()
features = [{"category": r["category"], "note": r["note"]} for r in grammar_rows]
# 4. Tjek uregelmæssigt verbum
cur.execute("SELECT * FROM irregular_verbs WHERE infinitive = ? OR present = ? OR past = ? OR past_participle = ?",
(clean, clean, clean, clean))
verb_row = cur.fetchone()
verb_info = None
if verb_row:
verb_info = {
"infinitiv": verb_row["infinitive"],
"praesens": verb_row["present"],
"praeteritum": verb_row["past"],
"perfektum_participium": verb_row["past_participle"]
}
# 5. Tjek eventuel stavefejl
cur.execute("SELECT correct_word, pos_tag FROM typos WHERE typo = ?", (clean,))
typo_rows = cur.fetchall()
corrections = [{"korrekt_form": r["correct_word"], "ordklasse": r["pos_tag"]} for r in typo_rows]
conn.close()
exists = word_row is not None or lemma_row is not None
return {
"word": clean,
"is_valid_danish_word": exists,
"is_lemma": lemma_row is not None,
"ordklasser": lemma_row["pos"].split("|") if lemma_row else [],
"length": len(clean),
"is_wordle_eligible": bool(word_row["is_wordle"]) if word_row else (len(clean) == 5),
"is_palindrome": bool(word_row["is_palindrome"]) if word_row else False,
"grammar_features": features,
"irregular_verb": verb_info,
"known_typo_corrections": corrections if not exists and corrections else corrections
}
@server.tool(
name="find_words",
description="Søg efter danske ord med avancerede filtre: længde, regex, vildtegn (* eller ?), ordklasse eller kun Wordle/palindromer."
)
def find_words(
pattern: Optional[str] = None,
length: Optional[int] = None,
pos: Optional[str] = None,
is_wordle: Optional[bool] = None,
is_palindrome: Optional[bool] = None,
limit: int = 50
) -> Dict[str, Any]:
"""
Søger i COR ordregisteret.
:param pattern: Mønster til søgning. Kan indeholde '?' (ét bogstav), '*' (nul eller flere) eller regex (f.eks. '^b.*ing$').
:param length: Nøjagtig ordlængde (f.eks. 5 til Wordle).
:param pos: Filtrér efter ordklasse for grundformer (f.eks. 'sb', 'vb', 'adj').
:param is_wordle: Filtrér specifikt på 5-bogstavsord.
:param is_palindrome: Kun palindromer (sandt/falsk).
:param limit: Maksimalt antal resultater (standard: 50, maks 500).
"""
max_limit = min(limit, 500)
conn = get_db_connection()
cur = conn.cursor()
conditions = []
params = []
# Hvis pos er angivet, søger vi i lemmas, ellers i words
search_lemmas = bool(pos)
table = "lemmas" if search_lemmas else "words"
word_col = "lemma" if search_lemmas else "word"
if length is not None:
conditions.append(f"{table}.length = ?")
params.append(length)
if is_wordle:
conditions.append(f"{table}.length = 5")
if is_palindrome and not search_lemmas:
conditions.append("words.is_palindrome = 1")
if pos:
# pos kan være f.eks. 'sb' eller 'vb'
conditions.append(f"(lemmas.pos = ? OR lemmas.pos LIKE ? OR lemmas.pos LIKE ? OR lemmas.pos LIKE ?)")
params.extend([pos, f"{pos}|%", f"%|{pos}", f"%|{pos}|%"])
# Regex / Pattern konvertering hvis nødvendigt
use_python_regex = False
compiled_regex = None
if pattern:
clean_pat = pattern.strip().lower()
# Hvis pattern indeholder regex specialtegn ud over ? og *
if any(c in clean_pat for c in ["^", "$", "[", "]", "(", ")", "+", "{", "}", "|", "\\"]):
use_python_regex = True
compiled_regex = re.compile(clean_pat)
elif "*" in clean_pat or "?" in clean_pat:
# Konverter simpelt glob mønster til SQL LIKE
sql_like = clean_pat.replace("*", "%").replace("?", "_")
conditions.append(f"{table}.{word_col} LIKE ?")
params.append(sql_like)
else:
# Eksakt eller delstreng
conditions.append(f"{table}.{word_col} LIKE ?")
params.append(f"%{clean_pat}%")
where_clause = " WHERE " + " AND ".join(conditions) if conditions else ""
sql = f"SELECT {word_col} as ord, length FROM {table} {where_clause} ORDER BY length ASC, ord ASC"
# Hvis vi bruger python regex filtrerer vi bagefter, ellers begrænser vi direkte i SQL
if not use_python_regex:
sql += f" LIMIT {max_limit}"
cur.execute(sql, params)
rows = cur.fetchall()
conn.close()
results = []
for r in rows:
w = r["ord"]
if use_python_regex and compiled_regex:
if not compiled_regex.search(w):
continue
results.append(w)
if len(results) >= max_limit:
break
return {
"total_matches_returned": len(results),
"limit": max_limit,
"filter": {
"pattern": pattern,
"length": length,
"pos": pos,
"is_wordle": is_wordle,
"is_palindrome": is_palindrome
},
"words": results
}
@server.tool(
name="check_spelling",
description="Kontrollér et ord eller slå typiske danske stavefejl op mod COR.SOEGEHJAELP (Dansk Sprognævns officielle samling af almindelige fejl og rettelser)."
)
def check_spelling(word: str) -> Dict[str, Any]:
"""
Tjekker stavemåde og finder officielle rettelser.
:param word: Ordet der skal kontrolleres (f.eks. 'accellerere', 'majestætisk', 'linje').
"""
clean = word.strip().lower()
conn = get_db_connection()
cur = conn.cursor()
# 1. Tjek om ordet findes direkte som godkendt ord
cur.execute("SELECT word FROM words WHERE word = ?", (clean,))
is_valid = cur.fetchone() is not None
# 2. Slå op i stavefejlsdatabasen
cur.execute("SELECT correct_word, pos_tag FROM typos WHERE typo = ?", (clean,))
typo_rows = cur.fetchall()
conn.close()
suggestions = []
for r in typo_rows:
suggestions.append({
"korrekt_form": r["correct_word"],
"ordklasse": r["pos_tag"]
})
is_known_typo = len(suggestions) > 0
status = "korrekt" if is_valid else ("kendt_tastefejl" if is_known_typo else "ukendt_ord")
return {
"input_word": clean,
"status": status,
"is_officially_approved": is_valid,
"is_known_spelling_error": is_known_typo,
"official_corrections": suggestions
}
@server.tool(
name="grammar_features",
description="Udforsk grammatiske særtræk i dansk fra COR: uregelmæssige verber, substantiver med valgfrit køn (en/et), plurale tantum (kun flertal), singulare tantum (kun ental) og ordklasse-kamæleoner."
)
def grammar_features(
category: str,
query: Optional[str] = None,
limit: int = 50
) -> Dict[str, Any]:
"""
Slår grammatiske særkategorier op.
:param category: Kategori at slå op. Gyldige værdier:
'uregelmaessige_verber' (bøjningsrækker),
'valgfrit_koen' (en/et substantiver),
'plurale_tantum' (kun flertal),
'singulare_tantum' (kun ental),
'ordklasse_kamaeleon' (ord i 3+ ordklasser),
'isogram' (unikke bogstaver),
'semordnilap' (venderord),
'q_uden_u' (ord med q uden u).
:param query: Valgfri søgestreng til at filtrere i ord eller bøjninger.
:param limit: Maksimalt antal resultater (standard: 50, maks: 200).
"""
cat = category.strip().lower()
max_limit = min(limit, 200)
conn = get_db_connection()
cur = conn.cursor()
if cat == "uregelmaessige_verber":
sql = "SELECT infinitive, present, past, past_participle FROM irregular_verbs"
params = []
if query:
q = f"%{query.strip().lower()}%"
sql += " WHERE infinitive LIKE ? OR present LIKE ? OR past LIKE ? OR past_participle LIKE ?"
params.extend([q, q, q, q])
sql += f" LIMIT {max_limit}"
cur.execute(sql, params)
rows = cur.fetchall()
results = [
{
"infinitiv": r["infinitive"],
"praesens": r["present"],
"praeteritum": r["past"],
"perfektum_participium": r["past_participle"]
}
for r in rows
]
else:
sql = "SELECT word, category, note FROM grammar_features WHERE category = ?"
params = [cat]
if query:
sql += " AND word LIKE ?"
params.append(f"%{query.strip().lower()}%")
sql += f" LIMIT {max_limit}"
cur.execute(sql, params)
rows = cur.fetchall()
results = [{"word": r["word"], "note": r["note"]} for r in rows]
conn.close()
return {
"category": cat,
"query": query,
"count": len(results),
"results": results
}
if __name__ == "__main__":
# Kør serveren over standard stdio transport
server.run(transport="stdio")

41
cor/test_tools.py Normal file
View file

@ -0,0 +1,41 @@
import sys
from server import lookup_word, find_words, check_spelling, grammar_features
def test_mcp_tools():
print("=== TEST 1: lookup_word ===")
res_ord = lookup_word("cykler")
print("Opslag 'cykler':", res_ord)
assert res_ord["is_valid_danish_word"] is True
res_uregel = lookup_word("afbrød")
print("Opslag 'afbrød':", res_uregel)
assert res_uregel["irregular_verb"] is not None
print("\n=== TEST 2: check_spelling ===")
res_typo = check_spelling("accellerere")
print("Stavefejl 'accellerere':", res_typo)
assert res_typo["is_known_spelling_error"] is True
assert res_typo["official_corrections"][0]["korrekt_form"] == "accelerere"
print("\n=== TEST 3: find_words ===")
res_find = find_words(pattern="b*ing", length=8, limit=5)
print("Find 'b*ing' (længde 8):", res_find)
assert len(res_find["words"]) > 0
res_wordle = find_words(pattern="k?tte", is_wordle=True)
print("Find Wordle 'k?tte':", res_wordle)
assert "katte" in res_wordle["words"]
print("\n=== TEST 4: grammar_features ===")
res_valgfrit = grammar_features("valgfrit_koen", query="folie")
print("Valgfrit køn 'folie':", res_valgfrit)
assert any("alufolie" in r["word"] for r in res_valgfrit["results"])
res_verb = grammar_features("uregelmaessige_verber", query="adlyde")
print("Uregelmæssigt verbum 'adlyde':", res_verb)
assert len(res_verb["results"]) > 0
print("\n>>> ALLE TESTS BESTÅET! <<<")
if __name__ == "__main__":
test_mcp_tools()

42
mcp_manifest.json Normal file
View file

@ -0,0 +1,42 @@
{
"generated_at": "2026-10-22T18:47:51.626865+00:00",
"mcp_repo": "https://git.p20.dev/christian/OpenDataMCP.git",
"servers": {
"cor": {
"total_files": 5,
"total_bytes": 33960130,
"files": [
{
"name": "build_db.py",
"rel_path": "cor/build_db.py",
"sha256": "5f9765da0254d67d93a3cbd360e8c93a443da38ea6b6ee8f9895277c2847c439",
"bytes": 9256
},
{
"name": "cor.db",
"rel_path": "cor/cor.db",
"sha256": "ca7eef09494967af3212449d8e9b8438df0d416f1ba9c36bbd557dba5dd24e2c",
"bytes": 33935360
},
{
"name": "README.md",
"rel_path": "cor/README.md",
"sha256": "80e3ad82218f6f9c9b1c98b9d2f9317151be1adcac61730c7aafa1f3897416a6",
"bytes": 3009
},
{
"name": "server.py",
"rel_path": "cor/server.py",
"sha256": "f9bdce1cb030a6fd493938eda5ca44ec23a5e0415bc17fa4fdc9ee49f75aba8f",
"bytes": 10972
},
{
"name": "test_tools.py",
"rel_path": "cor/test_tools.py",
"sha256": "eb0bd3cfddb8b50a22412da6087453fd14ea55bcf2668e0134650ec66f36e6c6",
"bytes": 1533
}
]
}
}
}