OpenDataMCP/cor/server.py
Christian Johannesen 96d527ecdf chore: opdater MCP servere (2026-10-22 20:47)
Synkronisering af 1 MCP server(e) fra OpenData.
2026-10-22 20:47:52 +02:00

306 lines
11 KiB
Python

"""
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")