Merge pull request #615 from CoplayDev/beta

Update main for new workflow
main
Marcus Sanatan 2026-01-23 01:19:02 -04:00 committed by GitHub
commit 5acef27cd7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 1010 additions and 26 deletions

View File

@ -19,6 +19,14 @@ Save your change type
<!-- If applicable, add screenshots or recordings to demonstrate the changes -->
## Documentation Updates
<!-- Check if you updated documentation for changes to tools/resources -->
- [ ] I have added/removed/modified tools or resources
- [ ] If yes, I have updated all documentation files using:
- [ ] The LLM prompt at `tools/UPDATE_DOCS_PROMPT.md` (recommended)
- [ ] Manual updates following the guide at `tools/UPDATE_DOCS.md`
## Related Issues
<!-- Link any related issues using "Fixes #123" or "Relates to #123" -->

View File

@ -1,5 +1,9 @@
name: Release
concurrency:
group: release-main
cancel-in-progress: false
on:
workflow_dispatch:
inputs:
@ -19,13 +23,25 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
outputs:
new_version: ${{ steps.compute.outputs.new_version }}
tag: ${{ steps.tag.outputs.tag }}
bump_branch: ${{ steps.bump_branch.outputs.name }}
steps:
- name: Ensure workflow is running on main
shell: bash
run: |
set -euo pipefail
if [[ "${GITHUB_REF_NAME}" != "main" ]]; then
echo "This workflow must be run on the main branch. Current ref: ${GITHUB_REF_NAME}" >&2
exit 1
fi
- name: Checkout repository
uses: actions/checkout@v6
with:
ref: main
fetch-depth: 0
- name: Compute new version
@ -75,57 +91,102 @@ jobs:
run: |
set -euo pipefail
echo "Updating MCPForUnity/package.json to $NEW_VERSION"
jq ".version = \"${NEW_VERSION}\"" MCPForUnity/package.json > MCPForUnity/package.json.tmp
mv MCPForUnity/package.json.tmp MCPForUnity/package.json
echo "Updating all version references to $NEW_VERSION"
python3 tools/update_versions.py --version "$NEW_VERSION"
echo "Updating Server/pyproject.toml to $NEW_VERSION"
sed -i '0,/^version = ".*"/s//version = "'"$NEW_VERSION"'"/' "Server/pyproject.toml"
echo "Updating Server/README.md version references to v$NEW_VERSION"
sed -i 's|git+https://github.com/CoplayDev/unity-mcp@v[0-9]\+\.[0-9]\+\.[0-9]\+#subdirectory=Server|git+https://github.com/CoplayDev/unity-mcp@v'"$NEW_VERSION"'#subdirectory=Server|g' Server/README.md
echo "Updating root README.md fixed version examples to v$NEW_VERSION"
sed -i 's|https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity#v[0-9]\+\.[0-9]\+\.[0-9]\+|https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity#v'"$NEW_VERSION"'|g' README.md
echo "Updating docs/i18n/README-zh.md fixed version examples to v$NEW_VERSION"
sed -i 's|https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity#v[0-9]\+\.[0-9]\+\.[0-9]\+|https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity#v'"$NEW_VERSION"'|g' docs/i18n/README-zh.md
- name: Commit and push changes
- name: Commit version bump to a temporary branch
id: bump_branch
env:
NEW_VERSION: ${{ steps.compute.outputs.new_version }}
shell: bash
run: |
set -euo pipefail
BRANCH="release/v${NEW_VERSION}"
echo "name=$BRANCH" >> "$GITHUB_OUTPUT"
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git add MCPForUnity/package.json "Server/pyproject.toml" Server/README.md README.md docs/i18n/README-zh.md
git checkout -b "$BRANCH"
git add MCPForUnity/package.json manifest.json "Server/pyproject.toml" Server/README.md
if git diff --cached --quiet; then
echo "No version changes to commit."
else
git commit -m "chore: bump version to ${NEW_VERSION}"
fi
BRANCH="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}"
echo "Pushing to branch: $BRANCH"
echo "Pushing bump branch $BRANCH"
git push origin "$BRANCH"
- name: Create and push tag
- name: Create PR for version bump into main
id: bump_pr
env:
GH_TOKEN: ${{ github.token }}
NEW_VERSION: ${{ steps.compute.outputs.new_version }}
BRANCH: ${{ steps.bump_branch.outputs.name }}
shell: bash
run: |
set -euo pipefail
PR_URL=$(gh pr create \
--base main \
--head "$BRANCH" \
--title "chore: bump version to ${NEW_VERSION}" \
--body "Automated version bump to ${NEW_VERSION}.")
echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT"
PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$')
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
- name: Enable auto-merge and merge PR
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ steps.bump_pr.outputs.pr_number }}
shell: bash
run: |
set -euo pipefail
# Enable auto-merge (requires repo setting "Allow auto-merge")
gh pr merge "$PR_NUMBER" --merge --auto || true
# Wait for PR to be merged (poll up to 2 minutes)
for i in {1..24}; do
STATE=$(gh pr view "$PR_NUMBER" --json state -q '.state')
if [[ "$STATE" == "MERGED" ]]; then
echo "PR merged successfully."
exit 0
fi
echo "Waiting for PR to merge... (state: $STATE)"
sleep 5
done
echo "PR did not merge in time. Attempting direct merge..."
gh pr merge "$PR_NUMBER" --merge
- name: Fetch merged main and create tag
env:
TAG: ${{ steps.tag.outputs.tag }}
shell: bash
run: |
set -euo pipefail
git fetch origin main
git checkout main
git pull origin main
echo "Preparing to create tag $TAG"
if git ls-remote --tags origin | grep -q "refs/tags/$TAG$"; then
echo "Tag $TAG already exists on remote. Skipping tag creation."
exit 0
echo "Tag $TAG already exists on remote. Refusing to release." >&2
exit 1
fi
git tag -a "$TAG" -m "Version ${TAG#v}"
git push origin "$TAG"
- name: Clean up release branch
if: always()
env:
GH_TOKEN: ${{ github.token }}
BRANCH: ${{ steps.bump_branch.outputs.name }}
shell: bash
run: |
set -euo pipefail
git push origin --delete "$BRANCH" || true
- name: Create GitHub release
uses: softprops/action-gh-release@v2
with:
@ -133,6 +194,70 @@ jobs:
name: ${{ steps.tag.outputs.tag }}
generate_release_notes: true
sync_beta:
name: Merge main back into beta via PR
needs:
- bump
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout main
uses: actions/checkout@v6
with:
ref: main
fetch-depth: 0
- name: Create PR to merge main into beta
id: sync_pr
env:
GH_TOKEN: ${{ github.token }}
NEW_VERSION: ${{ needs.bump.outputs.new_version }}
shell: bash
run: |
set -euo pipefail
# Check if beta is behind main
git fetch origin beta
if git merge-base --is-ancestor origin/main origin/beta; then
echo "beta is already up to date with main. Skipping PR."
echo "skipped=true" >> "$GITHUB_OUTPUT"
exit 0
fi
PR_URL=$(gh pr create \
--base beta \
--head main \
--title "chore: sync main (v${NEW_VERSION}) into beta" \
--body "Automated sync of version bump from main into beta.")
echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT"
PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$')
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
echo "skipped=false" >> "$GITHUB_OUTPUT"
- name: Enable auto-merge and merge sync PR
if: steps.sync_pr.outputs.skipped != 'true'
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ steps.sync_pr.outputs.pr_number }}
shell: bash
run: |
set -euo pipefail
# Enable auto-merge (requires repo setting "Allow auto-merge")
gh pr merge "$PR_NUMBER" --merge --auto || true
# Wait for PR to be merged (poll up to 2 minutes)
for i in {1..24}; do
STATE=$(gh pr view "$PR_NUMBER" --json state -q '.state')
if [[ "$STATE" == "MERGED" ]]; then
echo "Sync PR merged successfully."
exit 0
fi
echo "Waiting for sync PR to merge... (state: $STATE)"
sleep 5
done
echo "Sync PR did not merge in time. Attempting direct merge..."
gh pr merge "$PR_NUMBER" --merge
publish_docker:
name: Publish Docker image
needs:
@ -194,3 +319,43 @@ jobs:
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: Server/dist/
publish_mcpb:
name: Generate and publish MCPB bundle
needs:
- bump
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Check out the repo
uses: actions/checkout@v6
with:
ref: ${{ needs.bump.outputs.tag }}
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20"
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Generate MCPB bundle
env:
NEW_VERSION: ${{ needs.bump.outputs.new_version }}
shell: bash
run: |
set -euo pipefail
python3 tools/generate_mcpb.py "$NEW_VERSION" \
--output "unity-mcp-${NEW_VERSION}.mcpb" \
--icon docs/images/coplay-logo.png
- name: Upload MCPB to release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.bump.outputs.tag }}
files: unity-mcp-${{ needs.bump.outputs.new_version }}.mcpb

82
.mcpbignore Normal file
View File

@ -0,0 +1,82 @@
# MCPB Ignore File
# This bundle uses uvx pattern - package downloaded from PyPI at runtime
# Only manifest.json, icon.png, README.md, and LICENSE are needed
# Server source code (downloaded via uvx from PyPI)
Server/
# Unity Client plugin (separate installation)
MCPForUnity/
# Test projects
TestProjects/
# Documentation folder
docs/
# Custom Tools (shipped separately)
CustomTools/
# Development scripts at root
scripts/
tools/
# Claude skill zip (separate distribution)
claude_skill_unity.zip
# Development batch files
deploy-dev.bat
restore-dev.bat
# Test files at root
test_unity_socket_framing.py
mcp_source.py
prune_tool_results.py
# Docker
docker-compose.yml
.dockerignore
Dockerfile
# Chinese README (keep English only)
README-zh.md
# GitHub and CI
.github/
.claude/
# IDE
.vscode/
.idea/
# Python artifacts
*.pyc
__pycache__/
.pytest_cache/
.mypy_cache/
*.egg-info/
dist/
build/
# Environment
.env*
*.local
.venv/
venv/
# Git
.git/
.gitignore
.gitattributes
# Package management
uv.lock
poetry.lock
requirements*.txt
pyproject.toml
# Logs and temp
*.log
*.tmp
.DS_Store
Thumbs.db

View File

@ -36,9 +36,9 @@ In Unity: `Window > Package Manager > + > Add package from git URL...`
> https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity
> ```
**Need a stable/fixed version?** Use a tagged URL (requires uninstall to update):
**Want the latest beta?** Use the beta branch:
```text
https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity#v9.1.0
https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity#beta
```
<details>

View File

@ -338,6 +338,36 @@ python3 tools/stress_mcp.py \
4. **迭代** - 按需重复 1-3
5. **Restore** 完成后用 `restore-dev.bat` 恢复原始文件
## 重要说明
### 更新工具和 Manifest
在 Unity 包中添加或修改 MCP 工具时:
- 工具定义位于仓库根目录的 manifest.json 文件中
- 在发布过程中manifest.json 版本会自动与 MCPForUnity/package.json 保持同步
- 如果在发布过程之外手动更新工具,请确保相应更新 manifest.json 版本
- 使用综合版本更新脚本:`python3 tools/update_versions.py` 来同步项目中所有版本引用
`update_versions.py` 脚本会更新:
- MCPForUnity/package.jsonUnity 包版本)
- manifest.jsonMCP bundle manifest
- Server/pyproject.tomlPython 包版本)
- Server/README.md版本引用
- README.md固定版本示例
- docs/i18n/README-zh.md固定版本示例
使用示例:
```bash
# 更新所有文件以匹配 package.json 版本
python3 tools/update_versions.py
# 更新所有文件到指定版本
python3 tools/update_versions.py --version 9.2.0
# 干运行以查看将要更新的内容
python3 tools/update_versions.py --dry-run
```
## Troubleshooting
### 运行 .bat 时出现 "Path not found"

View File

@ -328,6 +328,36 @@ We provide a CI job to run a Natural Language Editing suite against the Unity te
4. **Iterate** - repeat steps 1-3 as needed
5. **Restore** original files when done using `restore-dev.bat`
## Important Notes
### Updating Tools and Manifest
When adding or modifying MCP tools in the Unity package:
- Tool definitions are located in the manifest.json file at the repository root
- The manifest.json version is automatically kept in sync with MCPForUnity/package.json during releases
- If you manually update tools outside of the release process, ensure to update the manifest.json version accordingly
- Use the comprehensive version update script: `python3 tools/update_versions.py` to sync all version references across the project
The `update_versions.py` script updates:
- MCPForUnity/package.json (Unity package version)
- manifest.json (MCP bundle manifest)
- Server/pyproject.toml (Python package version)
- Server/README.md (version references)
- README.md (fixed version examples)
- docs/i18n/README-zh.md (fixed version examples)
Usage examples:
```bash
# Update all files to match package.json version
python3 tools/update_versions.py
# Update all files to a specific version
python3 tools/update_versions.py --version 9.2.0
# Dry run to see what would be updated
python3 tools/update_versions.py --dry-run
```
## Troubleshooting
### "Path not found" errors running the .bat file

111
docs/guides/RELEASING.md Normal file
View File

@ -0,0 +1,111 @@
# Releasing (Maintainers)
This repo uses a two-branch flow to keep `main` stable for users:
- `beta`: integration branch where feature PRs land
- `main`: stable branch that should match the latest release tag
## Release checklist
### 1) Promote `beta` to `main` via PR
- Create a PR with:
- base: `main`
- compare: `beta`
- Ensure required CI checks are green.
- Merge the PR.
Release note quality depends on how you merge:
- Squash-merging feature PRs into `beta` is OK.
- Avoid squash-merging the `beta -> main` promotion PR. Prefer a merge commit (or rebase merge) so GitHub can produce better auto-generated release notes.
### 2) Run the Release workflow (manual)
- Go to **GitHub → Actions → Release**
- Click **Run workflow**
- Select:
- `patch`, `minor`, or `major`
- Run it on branch: `main`
What the workflow does:
1. Creates a temporary `release/vX.Y.Z` branch with the version bump commit
2. Opens a PR from that branch into `main`
3. Auto-merges the PR (or waits for required checks, then merges)
4. Creates an annotated tag `vX.Y.Z` on the merged commit
5. Creates a GitHub Release for the tag
6. Publishes artifacts (Docker / PyPI / MCPB)
7. Opens a PR to merge `main` back into `beta` (so `beta` gets the bump)
8. Auto-merges the sync PR
9. Cleans up the temporary release branch
### 3) Verify release outputs
- Confirm a new tag exists: `vX.Y.Z`
- Confirm a GitHub Release exists for the tag
- Confirm artifacts:
- Docker image published with version `X.Y.Z`
- PyPI package published (if configured)
- `unity-mcp-X.Y.Z.mcpb` attached to the GitHub Release
## Required repo settings
### Branch protection (Rulesets)
The release workflow uses PRs instead of direct pushes, so it works with strict branch protection. No bypass actors are required.
Recommended ruleset for `main`:
- Require PR before merging
- Allowed merge methods: `merge`, `rebase` (no squash for promotion PRs)
- Required approvals: `0` (so automated PRs can merge without human review)
- Optionally require status checks
Recommended ruleset for `beta`:
- Require PR before merging
- Allowed merge methods: `squash` (for feature PRs)
- Required approvals: `0` (so the sync PR can auto-merge)
### Enable auto-merge (required)
The workflow uses `gh pr merge --auto` to automatically merge PRs once checks pass.
To enable:
1. Go to **Settings → General**
2. Scroll to **Pull Requests**
3. Check **Allow auto-merge**
Without this setting, the workflow will fall back to direct merge attempts, which may fail if branch protection requires checks.
## Failure modes and recovery
### Tag already exists
The workflow fails if the computed tag already exists. Pick a different bump type or investigate why a tag already exists for that version.
### Bump PR fails to merge
If the version bump PR cannot be merged (e.g., required checks fail):
- The workflow will fail before creating a tag.
- Fix the issue, then either:
- Manually merge the PR and create the tag/release, or
- Close the PR, delete the `release/vX.Y.Z` branch, and re-run the workflow.
### Sync PR (`main -> beta`) fails
If the sync PR has merge conflicts:
- The workflow will fail after the release is published (artifacts are already out).
- Manually resolve conflicts in the sync PR and merge it.
### Leftover release branch
If the workflow fails mid-run, a `release/vX.Y.Z` branch may remain. Delete it manually before re-running:
```bash
git push origin --delete release/vX.Y.Z
```

View File

@ -36,9 +36,9 @@
> https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity
> ```
**需要一个稳定/固定版本?** 使用带 tag 的 URL更新时需要卸载才能更新
**想要最新的 beta 版本?** 使用 beta 分支
```text
https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity#v9.1.0
https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity#beta
```
<details>

58
manifest.json Normal file
View File

@ -0,0 +1,58 @@
{
"manifest_version": "0.3",
"name": "Unity MCP",
"version": "9.1.0",
"description": "AI-powered Unity Editor automation via MCP - manage GameObjects, scripts, materials, scenes, prefabs, VFX, and run tests",
"author": {
"name": "Coplay",
"url": "https://www.coplay.dev"
},
"repository": {
"type": "git",
"url": "https://github.com/CoplayDev/unity-mcp"
},
"homepage": "https://www.coplay.dev",
"documentation": "https://github.com/CoplayDev/unity-mcp#readme",
"support": "https://github.com/CoplayDev/unity-mcp/issues",
"icon": "coplay-logo.png",
"server": {
"type": "python",
"entry_point": "Server/src/main.py",
"mcp_config": {
"command": "uvx",
"args": ["--from", "mcpforunityserver", "mcp-for-unity"],
"env": {}
}
},
"tools": [
{"name": "batch_execute", "description": "Execute multiple Unity operations in a single batch"},
{"name": "debug_request_context", "description": "Debug and inspect MCP request context"},
{"name": "execute_custom_tool", "description": "Execute custom Unity Editor tools registered by the project"},
{"name": "execute_menu_item", "description": "Execute Unity Editor menu items"},
{"name": "find_gameobjects", "description": "Find GameObjects in the scene by various criteria"},
{"name": "find_in_file", "description": "Search for content within Unity project files"},
{"name": "manage_asset", "description": "Create, modify, search, and organize Unity assets"},
{"name": "manage_components", "description": "Add, remove, and configure GameObject components"},
{"name": "manage_editor", "description": "Control Unity Editor state, play mode, and preferences"},
{"name": "manage_gameobject", "description": "Create, modify, transform, and delete GameObjects"},
{"name": "manage_material", "description": "Create and modify Unity materials and shaders"},
{"name": "manage_prefabs", "description": "Create, instantiate, unpack, and modify prefabs"},
{"name": "manage_scene", "description": "Load, save, query hierarchy, and manage Unity scenes"},
{"name": "manage_script", "description": "Create, read, and modify C# scripts"},
{"name": "manage_scriptable_object", "description": "Create and modify ScriptableObjects"},
{"name": "manage_shader", "description": "Work with Unity shaders"},
{"name": "manage_vfx", "description": "Manage Visual Effects, particle systems, and trails"},
{"name": "read_console", "description": "Read Unity Editor console output (logs, warnings, errors)"},
{"name": "refresh_unity", "description": "Refresh Unity Editor asset database"},
{"name": "run_tests", "description": "Run Unity Test Framework tests"},
{"name": "get_test_job", "description": "Get status of async test job"},
{"name": "script_apply_edits", "description": "Apply code edits to C# scripts with validation"},
{"name": "set_active_instance", "description": "Set the active Unity Editor instance for multi-instance workflows"},
{"name": "apply_text_edits", "description": "Apply text edits to script content"},
{"name": "create_script", "description": "Create new C# scripts"},
{"name": "delete_script", "description": "Delete C# scripts"},
{"name": "validate_script", "description": "Validate C# script syntax and compilation"},
{"name": "manage_script_capabilities", "description": "Query script management capabilities"},
{"name": "get_sha", "description": "Get SHA hash of script content"}
]
}

View File

@ -0,0 +1,66 @@
# LLM Prompt for Updating Documentation
Copy and paste this prompt into your LLM when you need to update documentation after adding/removing/modifying MCP tools or resources.
## Example Usage
After adding a new tool called "manage_new_feature" and a new resource called "feature_resource", you would:
1. Copy the prompt in the section below
2. Paste it into your LLM
3. The LLM will analyze the codebase and update all documentation files
4. Review the changes and run the check script to verify
This ensures all documentation stays in sync across the repository.
---
## Prompt
I've just made changes to MCP tools or resources in this Unity MCP repository. Please update all documentation files to keep them in sync.
Here's what you need to do:
1. **Check the current tools and resources** by examining:
- `Server/src/services/tools/` - Python tool implementations (look for @mcp_for_unity_tool decorators)
- `Server/src/services/resources/` - Python resource implementations (look for @mcp_for_unity_resource decorators)
2. **Update these files**:
a) **manifest.json** (root directory)
- Update the "tools" array (lines 27-57)
- Each tool needs: {"name": "tool_name", "description": "Brief description"}
- Keep tools in alphabetical order
- Note: Resources are not listed in manifest.json, only tools
b) **README.md** (root directory)
- Update "Available Tools" section (around line 78-79)
- Format: `tool1``tool2``tool3`
- Keep the same order as manifest.json
c) **README.md** - Resources section
- Update "Available Resources" section (around line 81-82)
- Format: `resource1``resource2``resource3`
- Resources come from Server/src/services/resources/ files
- Keep resources in alphabetical order
d) **docs/i18n/README-zh.md**
- Find and update the "可用工具" (Available Tools) section
- Find and update the "可用资源" (Available Resources) section
- Keep tool/resource names in English, but you can translate descriptions if helpful
3. **Important formatting rules**:
- Use backticks around tool/resource names
- Separate items with • (bullet point)
- Keep lists on single lines when possible
- Maintain alphabetical ordering
- Tools and resources are listed separately in documentation
4. **After updating**, run this check to verify:
```bash
python3 tools/check_docs_sync.py
```
It should show "All documentation is synchronized!"
Please show me the exact changes you're making to each file, and explain any discrepancies you find.
---

147
tools/generate_mcpb.py Executable file
View File

@ -0,0 +1,147 @@
#!/usr/bin/env python3
"""Generate MCPB bundle for Unity MCP.
This script creates a Model Context Protocol Bundle (.mcpb) file
for distribution as a GitHub release artifact.
Usage:
python3 tools/generate_mcpb.py VERSION [--output FILE] [--icon PATH]
Examples:
python3 tools/generate_mcpb.py 9.0.8
python3 tools/generate_mcpb.py 9.0.8 --output unity-mcp-9.0.8.mcpb
python3 tools/generate_mcpb.py 9.0.8 --icon docs/images/coplay-logo.png
"""
from __future__ import annotations
import argparse
import json
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
DEFAULT_ICON = REPO_ROOT / "docs" / "images" / "coplay-logo.png"
MANIFEST_TEMPLATE = REPO_ROOT / "manifest.json"
def create_manifest(version: str, icon_filename: str) -> dict:
"""Create manifest.json content with the specified version."""
if not MANIFEST_TEMPLATE.exists():
raise FileNotFoundError(f"Manifest template not found: {MANIFEST_TEMPLATE}")
manifest = json.loads(MANIFEST_TEMPLATE.read_text(encoding="utf-8"))
manifest["version"] = version
manifest["icon"] = icon_filename
return manifest
def generate_mcpb(
version: str,
output_path: Path,
icon_path: Path,
) -> Path:
"""Generate MCPB bundle file.
Args:
version: Semantic version string (e.g., "9.0.8")
output_path: Output path for the .mcpb file
icon_path: Path to the icon file
Returns:
Path to the generated .mcpb file
"""
if not icon_path.exists():
raise FileNotFoundError(f"Icon not found: {icon_path}")
with tempfile.TemporaryDirectory() as tmpdir:
build_dir = Path(tmpdir) / "mcpb-build"
build_dir.mkdir()
# Copy icon
icon_filename = icon_path.name
shutil.copy2(icon_path, build_dir / icon_filename)
# Create manifest with version
manifest = create_manifest(version, icon_filename)
manifest_path = build_dir / "manifest.json"
manifest_path.write_text(
json.dumps(manifest, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
# Copy LICENSE and README if they exist
for filename in ["LICENSE", "README.md"]:
src = REPO_ROOT / filename
if src.exists():
shutil.copy2(src, build_dir / filename)
# Pack using mcpb CLI
# Syntax: mcpb pack [directory] [output]
try:
result = subprocess.run(
["npx", "@anthropic-ai/mcpb", "pack", ".", str(output_path.absolute())],
cwd=build_dir,
capture_output=True,
text=True,
check=True,
)
print(result.stdout)
except subprocess.CalledProcessError as e:
print(f"MCPB pack failed:\n{e.stderr}", file=sys.stderr)
raise
except FileNotFoundError:
print(
"Error: npx not found. Please install Node.js and npm.",
file=sys.stderr,
)
raise
if not output_path.exists():
raise RuntimeError(f"MCPB file was not created: {output_path}")
print(f"Generated: {output_path} ({output_path.stat().st_size:,} bytes)")
return output_path
def main() -> int:
parser = argparse.ArgumentParser(
description="Generate MCPB bundle for Unity MCP",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument(
"version",
help="Version string for the bundle (e.g., 9.0.8)",
)
parser.add_argument(
"--output",
"-o",
type=Path,
help="Output path for the .mcpb file (default: unity-mcp-VERSION.mcpb)",
)
parser.add_argument(
"--icon",
type=Path,
default=DEFAULT_ICON,
help=f"Path to icon file (default: {DEFAULT_ICON.relative_to(REPO_ROOT)})",
)
args = parser.parse_args()
# Default output name
if args.output is None:
args.output = Path(f"unity-mcp-{args.version}.mcpb")
try:
generate_mcpb(args.version, args.output, args.icon)
return 0
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())

287
tools/update_versions.py Executable file
View File

@ -0,0 +1,287 @@
#!/usr/bin/env python3
"""Update version across all project files.
This script updates the version in all files that need it:
- MCPForUnity/package.json (Unity package version)
- manifest.json (MCP bundle manifest)
- Server/pyproject.toml (Python package version)
- Server/README.md (version references)
- README.md (fixed version examples)
- docs/i18n/README-zh.md (fixed version examples)
Usage:
python3 tools/update_versions.py [--dry-run] [--version VERSION]
Options:
--dry-run: Show what would be updated without making changes
--version: Specify version to use (auto-detected from package.json if not provided)
Examples:
# Update all files to match package.json version
python3 tools/update_versions.py
# Update all files to a specific version
python3 tools/update_versions.py --version 9.2.0
# Dry run to see what would be updated
python3 tools/update_versions.py --dry-run
"""
import argparse
import json
import re
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
PACKAGE_JSON = REPO_ROOT / "MCPForUnity" / "package.json"
MANIFEST_JSON = REPO_ROOT / "manifest.json"
PYPROJECT_TOML = REPO_ROOT / "Server" / "pyproject.toml"
SERVER_README = REPO_ROOT / "Server" / "README.md"
ROOT_README = REPO_ROOT / "README.md"
ZH_README = REPO_ROOT / "docs" / "i18n" / "README-zh.md"
def load_package_version() -> str:
"""Load version from package.json."""
if not PACKAGE_JSON.exists():
raise FileNotFoundError(f"Package file not found: {PACKAGE_JSON}")
package_data = json.loads(PACKAGE_JSON.read_text(encoding="utf-8"))
version = package_data.get("version")
if not version:
raise ValueError("No version found in package.json")
return version
def update_package_json(new_version: str, dry_run: bool = False) -> bool:
"""Update version in MCPForUnity/package.json."""
if not PACKAGE_JSON.exists():
print(f"Warning: {PACKAGE_JSON.relative_to(REPO_ROOT)} not found")
return False
package_data = json.loads(PACKAGE_JSON.read_text(encoding="utf-8"))
current_version = package_data.get("version", "unknown")
if current_version == new_version:
print(f"{PACKAGE_JSON.relative_to(REPO_ROOT)} already at v{new_version}")
return False
print(
f"Updating {PACKAGE_JSON.relative_to(REPO_ROOT)}: {current_version}{new_version}")
if not dry_run:
package_data["version"] = new_version
PACKAGE_JSON.write_text(
json.dumps(package_data, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
return True
def update_manifest_json(new_version: str, dry_run: bool = False) -> bool:
"""Update version in manifest.json."""
if not MANIFEST_JSON.exists():
print(f"Warning: {MANIFEST_JSON.relative_to(REPO_ROOT)} not found")
return False
manifest = json.loads(MANIFEST_JSON.read_text(encoding="utf-8"))
current_version = manifest.get("version", "unknown")
if current_version == new_version:
print(f"{MANIFEST_JSON.relative_to(REPO_ROOT)} already at v{new_version}")
return False
print(
f"Updating {MANIFEST_JSON.relative_to(REPO_ROOT)}: {current_version}{new_version}")
if not dry_run:
manifest["version"] = new_version
MANIFEST_JSON.write_text(
json.dumps(manifest, indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
return True
def update_pyproject_toml(new_version: str, dry_run: bool = False) -> bool:
"""Update version in Server/pyproject.toml."""
if not PYPROJECT_TOML.exists():
print(f"Warning: {PYPROJECT_TOML.relative_to(REPO_ROOT)} not found")
return False
content = PYPROJECT_TOML.read_text(encoding="utf-8")
# Find current version
version_match = re.search(r'^version = "([^"]+)"', content, re.MULTILINE)
if not version_match:
print(
f"Warning: Could not find version in {PYPROJECT_TOML.relative_to(REPO_ROOT)}")
return False
current_version = version_match.group(1)
if current_version == new_version:
print(f"{PYPROJECT_TOML.relative_to(REPO_ROOT)} already at v{new_version}")
return False
print(
f"Updating {PYPROJECT_TOML.relative_to(REPO_ROOT)}: {current_version}{new_version}")
if not dry_run:
# Replace only the first occurrence (the version field)
content = re.sub(
r'^version = ".*"', f'version = "{new_version}"', content, count=1, flags=re.MULTILINE)
PYPROJECT_TOML.write_text(content, encoding="utf-8")
return True
def update_server_readme(new_version: str, dry_run: bool = False) -> bool:
"""Update version references in Server/README.md."""
if not SERVER_README.exists():
print(f"Warning: {SERVER_README.relative_to(REPO_ROOT)} not found")
return False
content = SERVER_README.read_text(encoding="utf-8")
# Pattern to match git+https URLs with version tags
pattern = r'git\+https://github\.com/CoplayDev/unity-mcp@v[0-9]+\.[0-9]+\.[0-9]+#subdirectory=Server'
replacement = f'git+https://github.com/CoplayDev/unity-mcp@v{new_version}#subdirectory=Server'
if not re.search(pattern, content):
print(
f"{SERVER_README.relative_to(REPO_ROOT)} has no version references to update")
return False
print(
f"Updating version references in {SERVER_README.relative_to(REPO_ROOT)}")
if not dry_run:
content = re.sub(pattern, replacement, content)
SERVER_README.write_text(content, encoding="utf-8")
return True
def update_root_readme(new_version: str, dry_run: bool = False) -> bool:
"""Update fixed version examples in README.md."""
if not ROOT_README.exists():
print(f"Warning: {ROOT_README.relative_to(REPO_ROOT)} not found")
return False
content = ROOT_README.read_text(encoding="utf-8")
# Pattern to match git URLs with fixed version tags
pattern = r'https://github\.com/CoplayDev/unity-mcp\.git\?path=/MCPForUnity#v[0-9]+\.[0-9]+\.[0-9]+'
replacement = f'https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity#v{new_version}'
if not re.search(pattern, content):
print(
f"{ROOT_README.relative_to(REPO_ROOT)} has no version references to update")
return False
print(
f"Updating version references in {ROOT_README.relative_to(REPO_ROOT)}")
if not dry_run:
content = re.sub(pattern, replacement, content)
ROOT_README.write_text(content, encoding="utf-8")
return True
def update_zh_readme(new_version: str, dry_run: bool = False) -> bool:
"""Update fixed version examples in docs/i18n/README-zh.md."""
if not ZH_README.exists():
print(f"Warning: {ZH_README.relative_to(REPO_ROOT)} not found")
return False
content = ZH_README.read_text(encoding="utf-8")
# Pattern to match git URLs with fixed version tags
pattern = r'https://github\.com/CoplayDev/unity-mcp\.git\?path=/MCPForUnity#v[0-9]+\.[0-9]+\.[0-9]+'
replacement = f'https://github.com/CoplayDev/unity-mcp.git?path=/MCPForUnity#v{new_version}'
if not re.search(pattern, content):
print(
f"{ZH_README.relative_to(REPO_ROOT)} has no version references to update")
return False
print(f"Updating version references in {ZH_README.relative_to(REPO_ROOT)}")
if not dry_run:
content = re.sub(pattern, replacement, content)
ZH_README.write_text(content, encoding="utf-8")
return True
def main() -> int:
parser = argparse.ArgumentParser(
description="Update version across all project files",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Show what would be updated without making changes",
)
parser.add_argument(
"--version",
help="Version to set (auto-detected from package.json if not provided)",
)
args = parser.parse_args()
try:
# Determine version
if args.version:
version = args.version
print(f"Using specified version: {version}")
else:
version = load_package_version()
print(f"Auto-detected version from package.json: {version}")
# Update all files
updates_made = []
# Always update package.json if a version is specified
if args.version:
if update_package_json(version, args.dry_run):
updates_made.append("MCPForUnity/package.json")
if update_manifest_json(version, args.dry_run):
updates_made.append("manifest.json")
if update_pyproject_toml(version, args.dry_run):
updates_made.append("Server/pyproject.toml")
if update_server_readme(version, args.dry_run):
updates_made.append("Server/README.md")
# Summary
if args.dry_run:
print("\nDry run complete. No files were modified.")
else:
if updates_made:
print(
f"\nUpdated {len(updates_made)} files: {', '.join(updates_made)}")
else:
print("\nAll files already at the correct version.")
return 0
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
return 1
if __name__ == "__main__":
sys.exit(main())