chore: opdater MCP servere (2026-10-22 20:47)
Synkronisering af 1 MCP server(e) fra OpenData.
This commit is contained in:
commit
96d527ecdf
7 changed files with 821 additions and 0 deletions
128
README.md
Normal file
128
README.md
Normal 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
74
cor/README.md
Normal 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
230
cor/build_db.py
Normal 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
BIN
cor/cor.db
Normal file
Binary file not shown.
306
cor/server.py
Normal file
306
cor/server.py
Normal 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
41
cor/test_tools.py
Normal 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
42
mcp_manifest.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue