Add Claude Desktop extensions (.mcpb) for WordCraft and GridCraft.

Ship unsigned MCPB packages built from the ArtCraft Launcher CLIs, with install docs and skill 0.2.0 updates.
This commit is contained in:
Dominic Gundel committed 2026-10-19 16:07:21 +02:00
1 parent 3ac6cd5a29
commit 2fd4164ee7
36 files changed
+1412 -116

No files matched your search

+90
View File
@@ -0,0 +1,90 @@
# Copies the ArtCraft-Launcher CLI into skills\<name>\extension\server\
# and writes upstream.json plus LICENSES from the matching upstream tag.
# No GitHub binary download. Requires WordCraft/GridCraft installed via ArtCraft Launcher.
# Usage: scripts\fetch-cli.ps1 -Name <skill-name> [-CliPath <path>]
param(
[Parameter(Mandatory = $true)][string]$Name,
[string]$CliPath
)
. (Join-Path $PSScriptRoot 'lib.ps1')
$dir = Join-Path $SkillsDir $Name
$ext = Join-Path $dir 'extension'
if (-not (Test-Path (Join-Path $ext 'manifest.json'))) {
Write-Host "skills/$Name/extension/manifest.json fehlt."
exit 1
}
$defaults = @{
'wordcraft-mcp' = @{
ExeName = 'wordcraft-cli.exe'
Default = 'C:\Program Files\WordCraft\wordcraft-cli.exe'
Repo = 'https://github.com/storytold/wordcraft'
RepoSlug = 'storytold/wordcraft'
}
'gridcraft-mcp' = @{
ExeName = 'gridcraft-cli.exe'
Default = 'C:\Program Files\GridCraft\gridcraft-cli.exe'
Repo = 'https://github.com/storytold/gridcraft'
RepoSlug = 'storytold/gridcraft'
}
}
if (-not $defaults.ContainsKey($Name)) {
Write-Host "Kein CLI-Mapping fuer Skill '$Name'."
exit 1
}
$meta = $defaults[$Name]
$src = if ($CliPath) { $CliPath } else { $meta.Default }
if (-not (Test-Path -LiteralPath $src)) {
Write-Host "CLI fehlt: $src"
Write-Host "Bitte WordCraft/GridCraft ueber den ArtCraft-Launcher installieren, oder -CliPath angeben."
exit 1
}
$serverDir = Join-Path $ext 'server'
New-Item -ItemType Directory -Force -Path $serverDir | Out-Null
$dest = Join-Path $serverDir $meta.ExeName
Copy-Item -LiteralPath $src -Destination $dest -Force
$verOut = & $dest --version 2>&1 | Out-String
if ($LASTEXITCODE -ne 0 -and $Name -eq 'gridcraft-mcp') {
$verOut = & $dest version 2>&1 | Out-String
}
$ver = ''
if ($verOut -match '(\d+\.\d+\.\d+)') { $ver = $Matches[1] }
if (-not $ver) {
Write-Host "Version der CLI nicht lesbar: $verOut"
exit 1
}
$hash = (Get-FileHash -LiteralPath $dest -Algorithm SHA256).Hash.ToLowerInvariant()
$upstream = @{
source = 'ArtCraft-Launcher'
path = $src
exe = $meta.ExeName
version = $ver
sha256 = $hash
repository = $meta.Repo
tag = "v$ver"
} | ConvertTo-Json -Compress
Write-Text (Join-Path $ext 'upstream.json') ($upstream + "`n")
$licDir = Join-Path $ext 'LICENSES'
New-Item -ItemType Directory -Force -Path $licDir | Out-Null
$tag = "v$ver"
foreach ($f in @('LICENSE-MIT', 'LICENSE-APACHE', 'NOTICE')) {
$url = "https://raw.githubusercontent.com/$($meta.RepoSlug)/$tag/$f"
$out = Join-Path $licDir $f
try {
Invoke-WebRequest -Uri $url -OutFile $out -UseBasicParsing
}
catch {
Write-Host "Lizenzdatei fehlgeschlagen: $url"
Write-Host $_
exit 1
}
}
Write-Host "OK $Name $ver $hash"
Write-Host " $dest"
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env bash
# Copies the ArtCraft-Launcher CLI into skills/<name>/extension/server/
# and writes upstream.json plus LICENSES from the matching upstream tag.
# No GitHub binary download. Requires WordCraft/GridCraft via ArtCraft Launcher.
# Usage: scripts/fetch-cli.sh <skill-name> [cli-path]
set -euo pipefail
source "$(dirname "$0")/lib.sh"
name="${1:-}"
cli_path="${2:-}"
if [[ -z "$name" ]]; then
echo "Aufruf: scripts/fetch-cli.sh <skill-name> [cli-path]" >&2
exit 1
fi
ext="$SKILLS_DIR/$name/extension"
if [[ ! -f "$ext/manifest.json" ]]; then
echo "skills/$name/extension/manifest.json fehlt." >&2
exit 1
fi
case "$name" in
wordcraft-mcp)
exe_name=wordcraft-cli.exe
default='/mnt/c/Program Files/WordCraft/wordcraft-cli.exe'
[[ -f "$default" ]] || default='C:/Program Files/WordCraft/wordcraft-cli.exe'
repo='https://github.com/storytold/wordcraft'
repo_slug='storytold/wordcraft'
;;
gridcraft-mcp)
exe_name=gridcraft-cli.exe
default='/mnt/c/Program Files/GridCraft/gridcraft-cli.exe'
[[ -f "$default" ]] || default='C:/Program Files/GridCraft/gridcraft-cli.exe'
repo='https://github.com/storytold/gridcraft'
repo_slug='storytold/gridcraft'
;;
*)
echo "Kein CLI-Mapping fuer Skill '$name'." >&2
exit 1
;;
esac
src="${cli_path:-$default}"
if [[ ! -f "$src" ]]; then
echo "CLI fehlt: $src" >&2
echo "Bitte WordCraft/GridCraft ueber den ArtCraft-Launcher installieren, oder Pfad als Argument angeben." >&2
exit 1
fi
mkdir -p "$ext/server"
dest="$ext/server/$exe_name"
cp -f "$src" "$dest"
chmod +x "$dest" 2>/dev/null || true
ver_out="$("$dest" --version 2>&1 || true)"
if [[ ! "$ver_out" =~ [0-9]+\.[0-9]+\.[0-9]+ ]] && [[ "$name" == gridcraft-mcp ]]; then
ver_out="$("$dest" version 2>&1 || true)"
fi
if [[ ! "$ver_out" =~ ([0-9]+\.[0-9]+\.[0-9]+) ]]; then
echo "Version der CLI nicht lesbar: $ver_out" >&2
exit 1
fi
ver="${BASH_REMATCH[1]}"
if command -v sha256sum >/dev/null 2>&1; then
hash="$(sha256sum "$dest" | awk '{print $1}')"
elif command -v shasum >/dev/null 2>&1; then
hash="$(shasum -a 256 "$dest" | awk '{print $1}')"
else
hash="$(python3 -c "import hashlib,sys; print(hashlib.sha256(open(sys.argv[1],'rb').read()).hexdigest())" "$dest")"
fi
python3 -c "
import json, sys
print(json.dumps({
'source': 'ArtCraft-Launcher',
'path': sys.argv[1],
'exe': sys.argv[2],
'version': sys.argv[3],
'sha256': sys.argv[4],
'repository': sys.argv[5],
'tag': 'v' + sys.argv[3],
}, separators=(',', ':')))
" "$src" "$exe_name" "$ver" "$hash" "$repo" > "$ext/upstream.json"
printf '\n' >> "$ext/upstream.json" || true
mkdir -p "$ext/LICENSES"
tag="v$ver"
for f in LICENSE-MIT LICENSE-APACHE NOTICE; do
url="https://raw.githubusercontent.com/$repo_slug/$tag/$f"
if ! curl -fsSL "$url" -o "$ext/LICENSES/$f"; then
echo "Lizenzdatei fehlgeschlagen: $url" >&2
exit 1
fi
done
echo "OK $name $ver $hash"
echo " $dest"
+14
View File
@@ -0,0 +1,14 @@
# Builds ZIP for every skill and .mcpb when skills\<name>\extension\ exists.
# Usage: scripts\package-all.ps1
. (Join-Path $PSScriptRoot 'lib.ps1')
& (Join-Path $PSScriptRoot 'package.ps1')
if ($LASTEXITCODE -ne 0) { exit 1 }
foreach ($n in (Get-SkillNames)) {
$ext = Join-Path (Join-Path $SkillsDir $n) 'extension\manifest.json'
if (Test-Path $ext) {
& (Join-Path $PSScriptRoot 'package-mcpb.ps1') -Name $n
if ($LASTEXITCODE -ne 0) { exit 1 }
}
}
+14
View File
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# Builds ZIP for every skill and .mcpb when skills/<name>/extension/ exists.
# Usage: scripts/package-all.sh
set -euo pipefail
source "$(dirname "$0")/lib.sh"
bash "$(dirname "$0")/package.sh"
while IFS= read -r name; do
[[ -z "$name" ]] && continue
if [[ -f "$SKILLS_DIR/$name/extension/manifest.json" ]]; then
bash "$(dirname "$0")/package-mcpb.sh" "$name"
fi
done < <(skill_names)
+57
View File
@@ -0,0 +1,57 @@
# Builds dist\<name>-v<version>.mcpb from skills\<name>\extension\
# Steps: fetch-cli, mcpb validate, mcpb pack. Unsigned (no cert).
# Usage: scripts\package-mcpb.ps1 -Name <skill-name> [-CliPath <path>]
param(
[Parameter(Mandatory = $true)][string]$Name,
[string]$CliPath
)
. (Join-Path $PSScriptRoot 'lib.ps1')
$dir = Join-Path $SkillsDir $Name
$ext = Join-Path $dir 'extension'
if (-not (Test-Path (Join-Path $ext 'manifest.json'))) {
Write-Host "skills/$Name/extension/ fehlt."
exit 1
}
$ver = Get-SkillVersion $dir
if (-not $ver) {
Write-Host "CHANGELOG von $Name hat keine Version."
exit 1
}
$fetchArgs = @{ Name = $Name }
if ($CliPath) { $fetchArgs.CliPath = $CliPath }
& (Join-Path $PSScriptRoot 'fetch-cli.ps1') @fetchArgs
if ($LASTEXITCODE -ne 0) { exit 1 }
$manPath = Join-Path $ext 'manifest.json'
$man = Read-Text $manPath | ConvertFrom-Json
if ($man.version -cne $ver) {
Write-Host "manifest.json version '$($man.version)' != CHANGELOG '$ver'."
exit 1
}
New-Item -ItemType Directory -Force -Path $DistDir | Out-Null
$out = Join-Path $DistDir "$Name-v$ver.mcpb"
if (Test-Path $out) { Remove-Item $out -Force }
Push-Location $ext
try {
npx --yes @anthropic-ai/mcpb validate .
if ($LASTEXITCODE -ne 0) { exit 1 }
npx --yes @anthropic-ai/mcpb pack . $out
if ($LASTEXITCODE -ne 0) { exit 1 }
}
finally { Pop-Location }
Write-Host ''
Write-Host "dist/$Name-v$ver.mcpb"
Add-Type -AssemblyName System.IO.Compression.FileSystem
$zip = [IO.Compression.ZipFile]::OpenRead($out)
try {
foreach ($e in $zip.Entries) {
Write-Host ('{0,12} {1}' -f $e.Length, $e.FullName)
}
}
finally { $zip.Dispose() }
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# Builds dist/<name>-v<version>.mcpb from skills/<name>/extension/
# Steps: fetch-cli, mcpb validate, mcpb pack. Unsigned (no cert).
# Usage: scripts/package-mcpb.sh <skill-name> [cli-path]
set -euo pipefail
source "$(dirname "$0")/lib.sh"
name="${1:-}"
cli_path="${2:-}"
if [[ -z "$name" ]]; then
echo "Aufruf: scripts/package-mcpb.sh <skill-name> [cli-path]" >&2
exit 1
fi
dir="$SKILLS_DIR/$name"
ext="$dir/extension"
[[ -f "$ext/manifest.json" ]] || { echo "skills/$name/extension/ fehlt." >&2; exit 1; }
ver="$(skill_version "$dir")"
[[ -n "$ver" ]] || { echo "CHANGELOG von $name hat keine Version." >&2; exit 1; }
if [[ -n "$cli_path" ]]; then
bash "$(dirname "$0")/fetch-cli.sh" "$name" "$cli_path"
else
bash "$(dirname "$0")/fetch-cli.sh" "$name"
fi
man_ver="$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1],encoding="utf-8"))["version"])' "$ext/manifest.json")"
if [[ "$man_ver" != "$ver" ]]; then
echo "manifest.json version '$man_ver' != CHANGELOG '$ver'." >&2
exit 1
fi
mkdir -p "$DIST_DIR"
out="$DIST_DIR/$name-v$ver.mcpb"
rm -f "$out"
(
cd "$ext"
npx --yes @anthropic-ai/mcpb validate .
npx --yes @anthropic-ai/mcpb pack . "$out"
)
echo
echo "dist/$name-v$ver.mcpb"
unzip -l "$out"
+5 -1
View File
@@ -23,8 +23,12 @@ foreach ($n in $names) {
$zipPath = Join-Path $DistDir "$n-v$version.zip"
if (Test-Path $zipPath) { Remove-Item $zipPath }
$extPrefix = (Join-Path $src 'extension') + [IO.Path]::DirectorySeparatorChar
$files = @(Get-ChildItem -Path $src -Recurse -File -Force |
Where-Object { $exclude -notcontains $_.Name } |
Where-Object {
$exclude -notcontains $_.Name -and
(-not $_.FullName.StartsWith($extPrefix, [StringComparison]::OrdinalIgnoreCase))
} |
ForEach-Object { $_.FullName })
[Array]::Sort($files, [StringComparer]::Ordinal)
+1 -1
View File
@@ -23,7 +23,7 @@ for name in "${names[@]}"; do
tmp="$(mktemp -d)"
mkdir "$tmp/$name"
(cd "$SKILLS_DIR/$name" && tar cf - --exclude=CHANGELOG.md --exclude=README.md --exclude=.DS_Store --exclude=Thumbs.db .) \
(cd "$SKILLS_DIR/$name" && tar cf - --exclude=CHANGELOG.md --exclude=README.md --exclude=.DS_Store --exclude=Thumbs.db --exclude=extension .) \
| (cd "$tmp/$name" && tar xf -)
(cd "$tmp" && zip -qrX "$zip_path" "$name")
rm -rf "$tmp"
+15
View File
@@ -104,6 +104,21 @@ catch {
$assetUrl = "$api/releases/$($created.id)/assets?name=$([Uri]::EscapeDataString($zipName))"
$asset = Invoke-Gitea 'POST' $assetUrl ([IO.File]::ReadAllBytes($zipPath)) 'application/octet-stream' | ConvertFrom-Json
$mcpbName = "$Name-v$ver.mcpb"
$mcpbPath = Join-Path $DistDir $mcpbName
$mcpbAsset = $null
if (Test-Path (Join-Path (Join-Path $dir 'extension') 'manifest.json')) {
if (-not (Test-Path $mcpbPath)) {
& (Join-Path $PSScriptRoot 'package-mcpb.ps1') -Name $Name
if ($LASTEXITCODE -ne 0) { exit 1 }
}
if (Test-Path $mcpbPath) {
$mcpbUrl = "$api/releases/$($created.id)/assets?name=$([Uri]::EscapeDataString($mcpbName))"
$mcpbAsset = Invoke-Gitea 'POST' $mcpbUrl ([IO.File]::ReadAllBytes($mcpbPath)) '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 }
Write-Host "Release $tag angelegt."
+30 -13
View File
@@ -73,21 +73,38 @@ rm -f "$resp"
rel_id="$(CREATED="$created" python3 -c 'import json,os; print(json.loads(os.environ["CREATED"])["id"])')"
html="$(CREATED="$created" python3 -c 'import json,os; print(json.loads(os.environ["CREATED"]).get("html_url",""))')"
asset_resp="$(mktemp)"
code="$(curl -sS -o "$asset_resp" -w '%{http_code}' -X POST "$api/releases/$rel_id/assets?name=$zip_name" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary @"$zip_path")"
if [[ "$code" != "201" ]]; then
echo "API $code" >&2
cat "$asset_resp" >&2
upload_asset() {
local file_path="$1" file_name="$2"
local asset_resp code asset download
asset_resp="$(mktemp)"
code="$(curl -sS -o "$asset_resp" -w '%{http_code}' -X POST "$api/releases/$rel_id/assets?name=$file_name" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary @"$file_path")"
if [[ "$code" != "201" ]]; then
echo "API $code" >&2
cat "$asset_resp" >&2
rm -f "$asset_resp"
exit 1
fi
asset="$(cat "$asset_resp")"
rm -f "$asset_resp"
exit 1
download="$(ASSET="$asset" python3 -c 'import json,os; print(json.loads(os.environ["ASSET"]).get("browser_download_url",""))')"
[[ -n "$download" ]] && echo "$download"
}
upload_asset "$zip_path" "$zip_name"
mcpb_name="$name-v$version.mcpb"
mcpb_path="$DIST_DIR/$mcpb_name"
if [[ -f "$dir/extension/manifest.json" ]]; then
if [[ ! -f "$mcpb_path" ]]; then
bash "$(dirname "$0")/package-mcpb.sh" "$name"
fi
if [[ -f "$mcpb_path" ]]; then
upload_asset "$mcpb_path" "$mcpb_name"
fi
fi
asset="$(cat "$asset_resp")"
rm -f "$asset_resp"
download="$(ASSET="$asset" python3 -c 'import json,os; print(json.loads(os.environ["ASSET"]).get("browser_download_url",""))')"
[[ -n "$html" ]] && echo "$html"
[[ -n "$download" ]] && echo "$download"
echo "Release $tag angelegt."
+8 -2
View File
@@ -15,7 +15,7 @@ if ($start -lt 0 -or $end -lt $start) {
exit 1
}
$table = @('| Skill | Kurzbeschreibung | Version | Doku | Download |', '|---|---|---|---|---|')
$table = @('| Skill | Kurzbeschreibung | Version | Doku | Download | Erweiterung (.mcpb) |', '|---|---|---|---|---|---|')
foreach ($n in (Get-SkillNames)) {
$dir = Join-Path $SkillsDir $n
$desc = ''
@@ -29,7 +29,13 @@ foreach ($n in (Get-SkillNames)) {
}
$desc = $desc.Replace('|', '\|')
$ver = Get-SkillVersion $dir
$table += ('| [{0}](skills/{0}/) | {1} | {2} | [README](skills/{0}/README.md) | [{0}-v{2}.zip]({3}/releases/tag/{0}/v{2}) |' -f $n, $desc, $ver, $RepoWeb)
$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')) {
$mcpb = "[{0}-v{1}.mcpb]({2}/releases/download/{3}/{0}-v{1}.mcpb)" -f $n, $ver, $RepoWeb, $tagEnc
}
$table += ('| [{0}](skills/{0}/) | {1} | {2} | [README](skills/{0}/README.md) | {3} | {4} |' -f $n, $desc, $ver, $zipLink, $mcpb)
}
$newLines = @()
+10 -4
View File
@@ -18,8 +18,8 @@ fi
table="$(mktemp)"
trap 'rm -f "$table"' EXIT
{
echo "| Skill | Kurzbeschreibung | Version | Doku | Download |"
echo "|---|---|---|---|---|"
echo "| Skill | Kurzbeschreibung | Version | Doku | Download | Erweiterung (.mcpb) |"
echo "|---|---|---|---|---|---|"
while IFS= read -r name; do
[[ -z "$name" ]] && continue
desc=""
@@ -28,8 +28,14 @@ trap 'rm -f "$table"' EXIT
desc="$(fm_value "$fm" description || true)"
fi
desc="${desc//|/\\|}"
printf '| [%s](skills/%s/) | %s | %s | [README](skills/%s/README.md) | [%s-v%s.zip](%s/releases/tag/%s/v%s) |\n' \
"$name" "$name" "$desc" "$ver" "$name" "$name" "$ver" "$REPO_WEB" "$name" "$ver"
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=""
if [[ -f "$SKILLS_DIR/$name/extension/manifest.json" ]]; then
mcpb="[${name}-v${ver}.mcpb](${REPO_WEB}/releases/download/${tag_enc}/${name}-v${ver}.mcpb)"
fi
printf '| [%s](skills/%s/) | %s | %s | [README](skills/%s/README.md) | %s | %s |\n' \
"$name" "$name" "$desc" "$ver" "$name" "$zip_link" "$mcpb"
done < <(skill_names)
} > "$table"