Release wordcraft-mcp and gridcraft-mcp 0.3.0: client-neutral skills, Cursor plugins.

This commit is contained in:
Dominic Gundel committed 2026-10-19 23:27:06 +02:00
1 parent debdf92877
commit 4c7e3ff720
26 files changed
+1008 -236

No files matched your search

+199
View File
@@ -0,0 +1,199 @@
#!/usr/bin/env bash
# Builds dist/cursor/ from skills/: flat skill copies, mcp snippets, one plugin per skill.
# Usage: scripts/build-cursor.sh [skill-name]
set -euo pipefail
source "$(dirname "$0")/lib.sh"
name_filter="${1:-}"
CURSOR_DIST="$DIST_DIR/cursor"
python3 - "$ROOT" "$SKILLS_DIR" "$CURSOR_DIST" "$name_filter" <<'PY'
import json, os, re, shutil, sys
from pathlib import Path
root, skills_dir, cursor_dist, name_filter = Path(sys.argv[1]), Path(sys.argv[2]), Path(sys.argv[3]), sys.argv[4]
SERVER_KEYS = {"wordcraft-mcp": "wordcraft", "gridcraft-mcp": "gridcraft"}
def skill_names():
names = sorted([p.name for p in skills_dir.iterdir() if p.is_dir()], key=lambda s: s)
if name_filter:
names = [n for n in names if n == name_filter]
return names
def skill_version(d: Path) -> str:
p = d / "CHANGELOG.md"
if not p.exists():
return ""
for line in p.read_text(encoding="utf-8").splitlines():
m = re.match(r"^## (\d+\.\d+\.\d+) \(\d{4}-\d{2}-\d{2}\)\s*$", line)
if m:
return m.group(1)
return ""
def frontmatter(path: Path):
text = path.read_text(encoding="utf-8").replace("\r", "")
lines = text.split("\n")
if not lines or lines[0] != "---":
return None
out = []
for line in lines[1:]:
if line == "---":
return out
out.append(line)
return None
def fm_value(lines, key):
for line in lines:
m = re.match(rf"^{re.escape(key)}\s*:(.*)$", line)
if m:
v = m.group(1).strip()
if (v.startswith('"') and v.endswith('"')) or (v.startswith("'") and v.endswith("'")):
v = v[1:-1]
return v
return None
def server_key(name):
if name in SERVER_KEYS:
return SERVER_KEYS[name]
m = re.match(r"^(.+)-mcp$", name)
return m.group(1) if m else name
def map_args_plugin(args):
out = []
for a in args:
s = str(a)
if s == "${user_config.author}":
out.append("${AUTHOR}")
elif s == "${user_config.read_root}":
out.append("${READ_ROOT}")
elif s == "${user_config.write_root}":
out.append("${WRITE_ROOT}")
else:
out.append(s)
return out
def map_args_home(args):
out = []
for a in args:
s = str(a)
if s in ("${user_config.author}", "${AUTHOR}"):
out.append("Claude")
elif s in ("${user_config.read_root}", "${READ_ROOT}"):
out.append("${userHome}/Documents")
elif s in ("${user_config.write_root}", "${WRITE_ROOT}"):
out.append("${userHome}/Documents/Claude")
else:
out.append(s)
return out
ok = True
cursor_dist.mkdir(parents=True, exist_ok=True)
for name in skill_names():
d = skills_dir / name
ver = skill_version(d)
skill = d / "SKILL.md"
fm = frontmatter(skill)
if not ver or fm is None:
print(f"FEHLER {name}: Version/Frontmatter", file=sys.stderr)
ok = False
continue
if fm_value(fm, "name") != name:
print(f"FEHLER {name}: name mismatch", file=sys.stderr)
ok = False
continue
desc = fm_value(fm, "description") or ""
if not desc or len(desc) > 1024:
print(f"FEHLER {name}: description", file=sys.stderr)
ok = False
continue
flat = cursor_dist / "skills" / name
flat.mkdir(parents=True, exist_ok=True)
shutil.copy2(skill, flat / "SKILL.md")
plugin = cursor_dist / "plugins" / name
if plugin.exists():
shutil.rmtree(plugin)
(plugin / ".cursor-plugin").mkdir(parents=True)
(plugin / "skills" / name).mkdir(parents=True)
shutil.copy2(skill, plugin / "skills" / name / "SKILL.md")
display_desc = desc
author = "Dominic Gundel"
variables = None
man_path = d / "extension" / "manifest.json"
sk = server_key(name)
if man_path.exists():
man = json.loads(man_path.read_text(encoding="utf-8"))
if man.get("version") != ver:
print(f"FEHLER {name}: manifest version != CHANGELOG", file=sys.stderr)
ok = False
continue
dn = man.get("display_name") or ""
if dn.endswith(" for Claude"):
display_desc = dn.replace(" for Claude", " for Cursor") + ". " + desc
elif man.get("description"):
display_desc = man["description"]
if man.get("author", {}).get("name"):
author = man["author"]["name"]
cmd = man["server"]["mcp_config"]["command"]
args = list(man["server"]["mcp_config"].get("args") or [])
mcp_plugin = {"mcpServers": {sk: {"command": cmd, "args": map_args_plugin(args)}}}
mcp_home = {"mcpServers": {sk: {"command": cmd, "args": map_args_home(args)}}}
(cursor_dist / "mcp").mkdir(parents=True, exist_ok=True)
(cursor_dist / "mcp" / f"{name}.json").write_text(
json.dumps(mcp_home, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
)
(plugin / "mcp.json").write_text(
json.dumps(mcp_plugin, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
)
uc = man.get("user_config") or {}
props = {}
required = []
if "read_root" in uc:
props["READ_ROOT"] = {
"type": "string",
"title": "Lese-Ordner",
"description": "Weit fassen (Standard: Dokumente).",
"default": "${userHome}/Documents",
}
required.append("READ_ROOT")
if "write_root" in uc:
props["WRITE_ROOT"] = {
"type": "string",
"title": "Schreib-Ordner",
"description": "Eng halten (Standard: Dokumente/Claude).",
"default": "${userHome}/Documents/Claude",
}
required.append("WRITE_ROOT")
if "author" in uc:
props["AUTHOR"] = {
"type": "string",
"title": "Autorname",
"description": "Name fuer nachverfolgte Aenderungen.",
"default": "Claude",
}
if props:
variables = {"type": "object", "properties": props}
if required:
variables["required"] = required
plugin_json = {
"name": name,
"version": ver,
"description": display_desc,
"author": {"name": author},
"license": "MIT",
"homepage": "https://git.gundel.dev/dg/claude-skills",
"repository": "https://git.gundel.dev/dg/claude-skills.git",
}
if variables:
plugin_json["variables"] = variables
(plugin / ".cursor-plugin" / "plugin.json").write_text(
json.dumps(plugin_json, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
)
print(f"OK cursor plugin {name} {ver}")
sys.exit(0 if ok else 1)
PY