diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 302258d..cd13fde 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -19,6 +19,14 @@ Save your change type +## Documentation Updates + +- [ ] 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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aff446f..198652e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 diff --git a/.mcpbignore b/.mcpbignore new file mode 100644 index 0000000..cd911c3 --- /dev/null +++ b/.mcpbignore @@ -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 diff --git a/README.md b/README.md index b9d55b8..6ed1be3 100644 --- a/README.md +++ b/README.md @@ -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 ```
diff --git a/docs/development/README-DEV-zh.md b/docs/development/README-DEV-zh.md index 269e09a..c79ba55 100644 --- a/docs/development/README-DEV-zh.md +++ b/docs/development/README-DEV-zh.md @@ -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.json(Unity 包版本) +- manifest.json(MCP bundle manifest) +- Server/pyproject.toml(Python 包版本) +- 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" diff --git a/docs/development/README-DEV.md b/docs/development/README-DEV.md index c9b3d45..0772947 100644 --- a/docs/development/README-DEV.md +++ b/docs/development/README-DEV.md @@ -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 diff --git a/docs/guides/RELEASING.md b/docs/guides/RELEASING.md new file mode 100644 index 0000000..8911992 --- /dev/null +++ b/docs/guides/RELEASING.md @@ -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 +``` diff --git a/docs/i18n/README-zh.md b/docs/i18n/README-zh.md index 8bad03d..c92d673 100644 --- a/docs/i18n/README-zh.md +++ b/docs/i18n/README-zh.md @@ -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 ```
diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..fac5861 --- /dev/null +++ b/manifest.json @@ -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"} + ] +} diff --git a/tools/UPDATE_DOCS_PROMPT.md b/tools/UPDATE_DOCS_PROMPT.md new file mode 100644 index 0000000..0ccdc60 --- /dev/null +++ b/tools/UPDATE_DOCS_PROMPT.md @@ -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. + +--- diff --git a/tools/generate_mcpb.py b/tools/generate_mcpb.py new file mode 100755 index 0000000..7194218 --- /dev/null +++ b/tools/generate_mcpb.py @@ -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()) diff --git a/tools/update_versions.py b/tools/update_versions.py new file mode 100755 index 0000000..979be4e --- /dev/null +++ b/tools/update_versions.py @@ -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())