commit 96d527ecdf346c2b4a1fae523c7ddc285699078b Author: Christian Johannesen Date: Thu Oct 22 20:47:52 2026 +0200 chore: opdater MCP servere (2026-10-22 20:47) Synkronisering af 1 MCP server(e) fra OpenData. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d3eafba --- /dev/null +++ b/README.md @@ -0,0 +1,128 @@ +# OpenDataMCP – Model Context Protocol (MCP) Servere / AI Tools + +[🇩🇰 Dansk](#dansk) | [🇬🇧 English](#english) + +--- + + +## 🇩🇰 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) + +--- + + +## 🇬🇧 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) \ No newline at end of file diff --git a/cor/README.md b/cor/README.md new file mode 100644 index 0000000..fa8d9d0 --- /dev/null +++ b/cor/README.md @@ -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**: `["/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) diff --git a/cor/build_db.py b/cor/build_db.py new file mode 100644 index 0000000..46f4e28 --- /dev/null +++ b/cor/build_db.py @@ -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() diff --git a/cor/cor.db b/cor/cor.db new file mode 100644 index 0000000..1e21f3d Binary files /dev/null and b/cor/cor.db differ diff --git a/cor/server.py b/cor/server.py new file mode 100644 index 0000000..0e88bde --- /dev/null +++ b/cor/server.py @@ -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") diff --git a/cor/test_tools.py b/cor/test_tools.py new file mode 100644 index 0000000..ddd7cfb --- /dev/null +++ b/cor/test_tools.py @@ -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() diff --git a/mcp_manifest.json b/mcp_manifest.json new file mode 100644 index 0000000..86283bc --- /dev/null +++ b/mcp_manifest.json @@ -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 + } + ] + } + } +} \ No newline at end of file