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

+197
View File
@@ -0,0 +1,197 @@
# Builds dist\cursor\ from skills\: flat skill copies, mcp snippets, one plugin per skill.
# Usage: scripts\build-cursor.ps1 [-Name <skill-name>]
param([string]$Name)
. (Join-Path $PSScriptRoot 'lib.ps1')
. (Join-Path $PSScriptRoot 'cursor-deeplink.ps1')
$CursorDist = Join-Path $DistDir 'cursor'
$ServerKeys = @{
'wordcraft-mcp' = 'wordcraft'
'gridcraft-mcp' = 'gridcraft'
}
function Get-McpServerKey([string]$SkillName) {
if ($ServerKeys.ContainsKey($SkillName)) { return $ServerKeys[$SkillName] }
# fallback: strip trailing -mcp
if ($SkillName -match '^(.+)-mcp$') { return $Matches[1] }
return $SkillName
}
function Convert-ClaudeArgsToCursor([object[]]$CliArgs) {
$out = New-Object System.Collections.Generic.List[string]
foreach ($a in $CliArgs) {
$s = [string]$a
if ($s -eq '${user_config.author}') { $out.Add('${AUTHOR}'); continue }
if ($s -eq '${user_config.read_root}') { $out.Add('${READ_ROOT}'); continue }
if ($s -eq '${user_config.write_root}') { $out.Add('${WRITE_ROOT}'); continue }
$out.Add($s)
}
return , $out.ToArray()
}
function Convert-ArgsToUserHomeDefaults([object[]]$CliArgs) {
$out = New-Object System.Collections.Generic.List[string]
foreach ($a in $CliArgs) {
$s = [string]$a
if ($s -eq '${user_config.author}' -or $s -eq '${AUTHOR}') { $out.Add('Claude'); continue }
if ($s -eq '${user_config.read_root}' -or $s -eq '${READ_ROOT}') { $out.Add('${userHome}/Documents'); continue }
if ($s -eq '${user_config.write_root}' -or $s -eq '${WRITE_ROOT}') { $out.Add('${userHome}/Documents/Claude'); continue }
$out.Add($s)
}
return , $out.ToArray()
}
function Build-OneSkill([string]$N) {
$dir = Join-Path $SkillsDir $N
if (-not (Test-Path $dir -PathType Container)) {
Write-Host "skills/$N fehlt."
return $false
}
$ver = Get-SkillVersion $dir
if (-not $ver) {
Write-Host "CHANGELOG von $N hat keine Version."
return $false
}
$skillPath = Join-Path $dir 'SKILL.md'
$fm = Get-Frontmatter $skillPath
if ($null -eq $fm) {
Write-Host "${N}: kein Frontmatter."
return $false
}
$fmName = Get-FmValue $fm 'name'
$desc = Get-FmValue $fm 'description'
if ($fmName -cne $N) {
Write-Host "${N}: Frontmatter name '$fmName' != Ordner."
return $false
}
if (-not $desc -or $desc.Length -gt 1024) {
Write-Host "${N}: description fehlt oder > 1024."
return $false
}
# Flat skill copy
$flatDir = Join-Path (Join-Path $CursorDist 'skills') $N
New-Item -ItemType Directory -Force -Path $flatDir | Out-Null
Copy-Item -LiteralPath $skillPath -Destination (Join-Path $flatDir 'SKILL.md') -Force
$extManPath = Join-Path $dir 'extension\manifest.json'
$hasExt = Test-Path $extManPath
$pluginRoot = Join-Path (Join-Path $CursorDist 'plugins') $N
if (Test-Path $pluginRoot) { Remove-Item $pluginRoot -Recurse -Force }
New-Item -ItemType Directory -Force -Path (Join-Path $pluginRoot '.cursor-plugin') | Out-Null
$pluginSkillDir = Join-Path (Join-Path $pluginRoot 'skills') $N
New-Item -ItemType Directory -Force -Path $pluginSkillDir | Out-Null
Copy-Item -LiteralPath $skillPath -Destination (Join-Path $pluginSkillDir 'SKILL.md') -Force
$displayDesc = $desc
$authorName = 'Dominic Gundel'
$pluginVars = $null
$mcpForPlugin = $null
$mcpStandalone = $null
$serverKey = Get-McpServerKey $N
if ($hasExt) {
$man = (Read-Text $extManPath) | ConvertFrom-Json
if ($man.version -cne $ver) {
Write-Host "${N}: manifest version '$($man.version)' != CHANGELOG '$ver'."
return $false
}
$dn = [string]$man.display_name
if ($dn -match ' for Claude$') {
$displayDesc = ($dn -replace ' for Claude$', ' for Cursor') + '. ' + $desc
}
elseif ($man.description) {
$displayDesc = [string]$man.description
}
if ($man.author -and $man.author.name) { $authorName = [string]$man.author.name }
$cmd = [string]$man.server.mcp_config.command
$argsRaw = @($man.server.mcp_config.args)
$argsPlugin = Convert-ClaudeArgsToCursor $argsRaw
$argsHome = Convert-ArgsToUserHomeDefaults $argsRaw
$transportPlugin = @{ command = $cmd; args = @($argsPlugin) }
$transportHome = @{ command = $cmd; args = @($argsHome) }
$mcpForPlugin = @{ mcpServers = @{ $serverKey = $transportPlugin } }
$mcpStandalone = @{ mcpServers = @{ $serverKey = $transportHome } }
if ($man.user_config) {
$props = @{}
$required = @()
if ($man.user_config.read_root) {
$props['READ_ROOT'] = @{
type = 'string'
title = 'Lese-Ordner'
description = 'Weit fassen (Standard: Dokumente).'
default = '${userHome}/Documents'
}
$required += 'READ_ROOT'
}
if ($man.user_config.write_root) {
$props['WRITE_ROOT'] = @{
type = 'string'
title = 'Schreib-Ordner'
description = 'Eng halten (Standard: Dokumente/Claude).'
default = '${userHome}/Documents/Claude'
}
$required += 'WRITE_ROOT'
}
if ($man.user_config.author) {
$props['AUTHOR'] = @{
type = 'string'
title = 'Autorname'
description = 'Name fuer nachverfolgte Aenderungen.'
default = 'Claude'
}
}
if ($props.Count -gt 0) {
$pluginVars = @{
type = 'object'
properties = $props
}
if ($required.Count -gt 0) { $pluginVars['required'] = $required }
}
}
$mcpDir = Join-Path $CursorDist 'mcp'
New-Item -ItemType Directory -Force -Path $mcpDir | Out-Null
$snippetPath = Join-Path $mcpDir "$N.json"
Write-Text $snippetPath (($mcpStandalone | ConvertTo-Json -Depth 20) + "`n")
}
$pluginJson = @{
name = $N
version = $ver
description = $displayDesc
author = @{ name = $authorName }
license = 'MIT'
homepage = 'https://git.gundel.dev/dg/claude-skills'
repository = 'https://git.gundel.dev/dg/claude-skills.git'
}
if ($pluginVars) { $pluginJson['variables'] = $pluginVars }
Write-Text (Join-Path $pluginRoot '.cursor-plugin\plugin.json') (($pluginJson | ConvertTo-Json -Depth 20) + "`n")
if ($mcpForPlugin) {
Write-Text (Join-Path $pluginRoot 'mcp.json') (($mcpForPlugin | ConvertTo-Json -Depth 20) + "`n")
}
# Validate JSON round-trip
$null = (Read-Text (Join-Path $pluginRoot '.cursor-plugin\plugin.json')) | ConvertFrom-Json
if ($mcpForPlugin) {
$null = (Read-Text (Join-Path $pluginRoot 'mcp.json')) | ConvertFrom-Json
}
Write-Host "OK cursor plugin $N $ver"
return $true
}
if ($Name) { $names = @($Name) } else { $names = @(Get-SkillNames) }
New-Item -ItemType Directory -Force -Path $CursorDist | Out-Null
$ok = $true
foreach ($n in $names) {
if (-not (Build-OneSkill $n)) { $ok = $false }
}
if (-not $ok) { exit 1 }
exit 0
+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
+38
View File
@@ -0,0 +1,38 @@
# Builds a Cursor MCP install deeplink from a server name and config object (hashtable/JSON).
# Usage: . scripts\cursor-deeplink.ps1; New-CursorDeeplink -Name wordcraft -Config @{ command='...'; args=@(...) }
# Or: scripts\cursor-deeplink.ps1 -Name wordcraft -ConfigJson '{...}'
param(
[string]$Name,
[string]$ConfigJson
)
. (Join-Path $PSScriptRoot 'lib.ps1')
function New-CursorDeeplink([string]$ServerName, $Config) {
# Config = server transport object (command/args/...) OR { serverName = transport }.
# Deeplink uses name= + base64(JSON of the transport object keyed by name), per Cursor docs.
if ($Config -is [string]) {
$obj = $Config | ConvertFrom-Json
}
else {
$obj = $Config
}
# Normalize to { name: transport }
$props = @($obj.PSObject.Properties.Name)
if ($props.Count -eq 1 -and $props[0] -eq $ServerName) {
$payload = $obj
}
elseif ($obj.command -or $obj.PSObject.Properties['command']) {
$payload = @{ $ServerName = $obj }
}
else {
$payload = $obj
}
$json = ($payload | ConvertTo-Json -Compress -Depth 20)
$bytes = $Utf8NoBom.GetBytes($json)
$b64 = [Convert]::ToBase64String($bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_')
return "cursor://anysphere.cursor-deeplink/mcp/install?name=$([Uri]::EscapeDataString($ServerName))&config=$b64"
}
if ($Name -and $ConfigJson) {
Write-Output (New-CursorDeeplink -ServerName $Name -Config $ConfigJson)
}
+19
View File
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Prints a Cursor MCP install deeplink.
# Usage: scripts/cursor-deeplink.sh <server-name> '<transport-json>'
# transport-json = {"command":"...","args":[...]}
set -euo pipefail
name="${1:-}"
config="${2:-}"
if [[ -z "$name" || -z "$config" ]]; then
echo "Aufruf: scripts/cursor-deeplink.sh <server-name> '<transport-json>'" >&2
exit 1
fi
python3 -c '
import base64, json, sys, urllib.parse
name, raw = sys.argv[1], sys.argv[2]
transport = json.loads(raw)
payload = {name: transport}
b64 = base64.urlsafe_b64encode(json.dumps(payload, separators=(",", ":")).encode("utf-8")).decode("ascii").rstrip("=")
print(f"cursor://anysphere.cursor-deeplink/mcp/install?name={urllib.parse.quote(name)}&config={b64}")
' "$name" "$config"
+37 -1
View File
@@ -1,7 +1,10 @@
# Builds ZIP for every skill and .mcpb when skills\<name>\extension\ exists.
# Builds ZIP for every skill, .mcpb when extension/ exists, and Cursor plugin ZIPs.
# Usage: scripts\package-all.ps1
. (Join-Path $PSScriptRoot 'lib.ps1')
Add-Type -AssemblyName System.IO.Compression
Add-Type -AssemblyName System.IO.Compression.FileSystem
& (Join-Path $PSScriptRoot 'package.ps1')
if ($LASTEXITCODE -ne 0) { exit 1 }
@@ -12,3 +15,36 @@ foreach ($n in (Get-SkillNames)) {
if ($LASTEXITCODE -ne 0) { exit 1 }
}
}
& (Join-Path $PSScriptRoot 'build-cursor.ps1')
if ($LASTEXITCODE -ne 0) { exit 1 }
foreach ($n in (Get-SkillNames)) {
$pluginDir = Join-Path $DistDir "cursor\plugins\$n"
if (-not (Test-Path $pluginDir)) { continue }
$ver = Get-SkillVersion (Join-Path $SkillsDir $n)
$out = Join-Path $DistDir "$n-cursor-v$ver.zip"
if (Test-Path $out) { Remove-Item $out -Force }
$src = (Get-Item $pluginDir).FullName
$files = @(Get-ChildItem -Path $src -Recurse -File -Force | ForEach-Object { $_.FullName })
[Array]::Sort($files, [StringComparer]::Ordinal)
$zip = [IO.Compression.ZipFile]::Open($out, [IO.Compression.ZipArchiveMode]::Create)
try {
[void]$zip.CreateEntry("$n/")
foreach ($f in $files) {
$rel = $f.Substring($src.Length).TrimStart('\', '/') -replace '\\', '/'
[void][IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zip, $f, "$n/$rel")
}
}
finally { $zip.Dispose() }
Write-Host ''
Write-Host "dist/$n-cursor-v$ver.zip"
$read = [IO.Compression.ZipFile]::OpenRead($out)
try {
foreach ($e in $read.Entries) { Write-Host ('{0,9} {1}' -f $e.Length, $e.FullName) }
}
finally { $read.Dispose() }
}
+16 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Builds ZIP for every skill and .mcpb when skills/<name>/extension/ exists.
# Builds ZIP for every skill, .mcpb when extension/ exists, and Cursor plugin ZIPs.
# Usage: scripts/package-all.sh
set -euo pipefail
source "$(dirname "$0")/lib.sh"
@@ -12,3 +12,18 @@ while IFS= read -r name; do
bash "$(dirname "$0")/package-mcpb.sh" "$name"
fi
done < <(skill_names)
bash "$(dirname "$0")/build-cursor.sh"
while IFS= read -r name; do
[[ -z "$name" ]] && continue
plugin="$DIST_DIR/cursor/plugins/$name"
[[ -d "$plugin" ]] || continue
ver="$(skill_version "$SKILLS_DIR/$name")"
out="$DIST_DIR/$name-cursor-v$ver.zip"
rm -f "$out"
(cd "$DIST_DIR/cursor/plugins" && zip -r "$out" "$name")
echo
echo "dist/$name-cursor-v$ver.zip"
unzip -l "$out"
done < <(skill_names)
+31
View File
@@ -135,7 +135,38 @@ if (Test-Path (Join-Path (Join-Path $dir 'extension') 'manifest.json')) {
}
}
$cursorName = "$Name-cursor-v$ver.zip"
$cursorPath = Join-Path $DistDir $cursorName
$cursorAsset = $null
if (-not (Test-Path $cursorPath)) {
& (Join-Path $PSScriptRoot 'build-cursor.ps1') -Name $Name
if ($LASTEXITCODE -eq 0) {
$pluginDir = Join-Path $DistDir "cursor\plugins\$Name"
if (Test-Path $pluginDir) {
Add-Type -AssemblyName System.IO.Compression
Add-Type -AssemblyName System.IO.Compression.FileSystem
$src = (Get-Item $pluginDir).FullName
$files = @(Get-ChildItem -Path $src -Recurse -File -Force | ForEach-Object { $_.FullName })
[Array]::Sort($files, [StringComparer]::Ordinal)
$zip = [IO.Compression.ZipFile]::Open($cursorPath, [IO.Compression.ZipArchiveMode]::Create)
try {
[void]$zip.CreateEntry("$Name/")
foreach ($f in $files) {
$rel = $f.Substring($src.Length).TrimStart('\', '/') -replace '\\', '/'
[void][IO.Compression.ZipFileExtensions]::CreateEntryFromFile($zip, $f, "$Name/$rel")
}
}
finally { $zip.Dispose() }
}
}
}
if (Test-Path $cursorPath) {
$cursorUrl = "$RepoApi/releases/$($created.id)/assets?name=$([Uri]::EscapeDataString($cursorName))"
$cursorAsset = Invoke-ReleaseApi 'POST' $cursorUrl ([IO.File]::ReadAllBytes($cursorPath)) 'application/octet-stream' | ConvertFrom-Json
}
if ($created.html_url) { Write-Host $created.html_url }
if ($asset.browser_download_url) { Write-Host $asset.browser_download_url }
if ($mcpbAsset -and $mcpbAsset.browser_download_url) { Write-Host $mcpbAsset.browser_download_url }
if ($cursorAsset -and $cursorAsset.browser_download_url) { Write-Host $cursorAsset.browser_download_url }
Write-Host "Release $tag angelegt."
+12
View File
@@ -121,5 +121,17 @@ if [[ -f "$dir/extension/manifest.json" ]]; then
fi
fi
cursor_name="$name-cursor-v$version.zip"
cursor_path="$DIST_DIR/$cursor_name"
if [[ ! -f "$cursor_path" ]]; then
bash "$(dirname "$0")/package-all.sh" >/dev/null 2>&1 || bash "$(dirname "$0")/build-cursor.sh" "$name" || true
if [[ -d "$DIST_DIR/cursor/plugins/$name" ]]; then
(cd "$DIST_DIR/cursor/plugins" && zip -r "$cursor_path" "$name")
fi
fi
if [[ -f "$cursor_path" ]]; then
upload_asset "$cursor_path" "$cursor_name"
fi
[[ -n "$html" ]] && echo "$html"
echo "Release $tag angelegt."
+36 -3
View File
@@ -3,6 +3,7 @@
# Aufruf: scripts\update-index.ps1 [-Check]
param([switch]$Check)
. (Join-Path $PSScriptRoot 'lib.ps1')
. (Join-Path $PSScriptRoot 'cursor-deeplink.ps1')
$RepoWeb = 'https://git.gundel.dev/dg/claude-skills'
$readme = Join-Path $Root 'README.md'
@@ -15,7 +16,21 @@ if ($start -lt 0 -or $end -lt $start) {
exit 1
}
$table = @('| Skill | Kurzbeschreibung | Version | Doku | Download | Erweiterung (.mcpb) |', '|---|---|---|---|---|---|')
$ServerKeys = @{
'wordcraft-mcp' = 'wordcraft'
'gridcraft-mcp' = 'gridcraft'
}
function Get-McpServerKey([string]$SkillName) {
if ($ServerKeys.ContainsKey($SkillName)) { return $ServerKeys[$SkillName] }
if ($SkillName -match '^(.+)-mcp$') { return $Matches[1] }
return $SkillName
}
$table = @(
'| Skill | Kurzbeschreibung | Version | Doku | Download | Erweiterung (.mcpb) | Cursor-Plugin | Cursor 1-Klick |',
'|---|---|---|---|---|---|---|---|'
)
foreach ($n in (Get-SkillNames)) {
$dir = Join-Path $SkillsDir $n
$desc = ''
@@ -32,10 +47,28 @@ foreach ($n in (Get-SkillNames)) {
$tagEnc = [Uri]::EscapeDataString("$n/v$ver")
$zipLink = "[{0}-v{1}.zip]({2}/releases/download/{3}/{0}-v{1}.zip)" -f $n, $ver, $RepoWeb, $tagEnc
$mcpb = ''
if (Test-Path (Join-Path $dir 'extension\manifest.json')) {
$cursorZip = ''
$deeplink = ''
$extPath = Join-Path $dir 'extension\manifest.json'
if (Test-Path $extPath) {
$mcpb = "[{0}-v{1}.mcpb]({2}/releases/download/{3}/{0}-v{1}.mcpb)" -f $n, $ver, $RepoWeb, $tagEnc
$cursorZip = "[{0}-cursor-v{1}.zip]({2}/releases/download/{3}/{0}-cursor-v{1}.zip)" -f $n, $ver, $RepoWeb, $tagEnc
$man = (Read-Text $extPath) | ConvertFrom-Json
$serverKey = Get-McpServerKey $n
$cmd = [string]$man.server.mcp_config.command
$argsList = New-Object System.Collections.Generic.List[string]
foreach ($a in @($man.server.mcp_config.args)) {
$s = [string]$a
if ($s -eq '${user_config.author}') { $argsList.Add('Claude'); continue }
if ($s -eq '${user_config.read_root}') { $argsList.Add('${userHome}/Documents'); continue }
if ($s -eq '${user_config.write_root}') { $argsList.Add('${userHome}/Documents/Claude'); continue }
$argsList.Add($s)
}
$transport = @{ command = $cmd; args = @($argsList) }
$link = New-CursorDeeplink -ServerName $serverKey -Config $transport
$deeplink = "[Add to Cursor]($link)"
}
$table += ('| [{0}](skills/{0}/) | {1} | {2} | [README](skills/{0}/README.md) | {3} | {4} |' -f $n, $desc, $ver, $zipLink, $mcpb)
$table += ('| [{0}](skills/{0}/) | {1} | {2} | [README](skills/{0}/README.md) | {3} | {4} | {5} | {6} |' -f $n, $desc, $ver, $zipLink, $mcpb, $cursorZip, $deeplink)
}
$newLines = @()
+33 -5
View File
@@ -18,8 +18,8 @@ fi
table="$(mktemp)"
trap 'rm -f "$table"' EXIT
{
echo "| Skill | Kurzbeschreibung | Version | Doku | Download | Erweiterung (.mcpb) |"
echo "|---|---|---|---|---|---|"
echo '| Skill | Kurzbeschreibung | Version | Doku | Download | Erweiterung (.mcpb) | Cursor-Plugin | Cursor 1-Klick |'
echo '|---|---|---|---|---|---|---|---|'
while IFS= read -r name; do
[[ -z "$name" ]] && continue
desc=""
@@ -31,11 +31,40 @@ trap 'rm -f "$table"' EXIT
tag_enc="$(python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=""))' "$name/v$ver")"
zip_link="[${name}-v${ver}.zip](${REPO_WEB}/releases/download/${tag_enc}/${name}-v${ver}.zip)"
mcpb=""
cursor_zip=""
deeplink=""
if [[ -f "$SKILLS_DIR/$name/extension/manifest.json" ]]; then
mcpb="[${name}-v${ver}.mcpb](${REPO_WEB}/releases/download/${tag_enc}/${name}-v${ver}.mcpb)"
cursor_zip="[${name}-cursor-v${ver}.zip](${REPO_WEB}/releases/download/${tag_enc}/${name}-cursor-v${ver}.zip)"
deeplink="$(python3 - "$SKILLS_DIR/$name/extension/manifest.json" "$name" <<'PY'
import json, base64, sys, urllib.parse, re
man = json.load(open(sys.argv[1], encoding="utf-8"))
name = sys.argv[2]
sk = {"wordcraft-mcp":"wordcraft","gridcraft-mcp":"gridcraft"}.get(name)
if not sk:
m = re.match(r"^(.+)-mcp$", name)
sk = m.group(1) if m else name
cmd = man["server"]["mcp_config"]["command"]
args = []
for a in man["server"]["mcp_config"].get("args") or []:
s = str(a)
if s == "${user_config.author}":
args.append("Claude")
elif s == "${user_config.read_root}":
args.append("${userHome}/Documents")
elif s == "${user_config.write_root}":
args.append("${userHome}/Documents/Claude")
else:
args.append(s)
payload = {sk: {"command": cmd, "args": args}}
b64 = base64.urlsafe_b64encode(json.dumps(payload, separators=(",", ":")).encode()).decode().rstrip("=")
link = f"cursor://anysphere.cursor-deeplink/mcp/install?name={urllib.parse.quote(sk)}&config={b64}"
print(f"[Add to Cursor]({link})")
PY
)"
fi
printf '| [%s](skills/%s/) | %s | %s | [README](skills/%s/README.md) | %s | %s |\n' \
"$name" "$name" "$desc" "$ver" "$name" "$zip_link" "$mcpb"
printf '| [%s](skills/%s/) | %s | %s | [README](skills/%s/README.md) | %s | %s | %s | %s |\n' \
"$name" "$name" "$desc" "$ver" "$name" "$zip_link" "$mcpb" "$cursor_zip" "$deeplink"
done < <(skill_names)
} > "$table"
@@ -51,7 +80,6 @@ if [[ $check -eq 1 ]]; then
fi
echo "README.md ist aktuell."
else
# Explizit UTF-8 schreiben (Locale kann sonst die README verfaelschen).
python3 -c 'import sys; open(sys.argv[1],"w",encoding="utf-8",newline="\n").write(sys.stdin.read())' "$readme" <<< "$new"
echo "README.md aktualisiert."
fi