chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:49:17 +08:00
commit 7243d5823b
2201 changed files with 257291 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
Server/build
.git
.venv
__pycache__
*.pyc
.DS_Store
+122
View File
@@ -0,0 +1,122 @@
name: Bug report
description: Something isn't working as expected
title: "[Bug]: "
labels: ["bug", "needs-triage"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to file a bug. The more specific you can be, the faster we can help.
**Before filing:**
- Search [existing issues](https://github.com/CoplayDev/unity-mcp/issues?q=is%3Aissue) to avoid duplicates
- Check the [troubleshooting docs](https://coplaydev.github.io/unity-mcp/guides/troubleshooting) and [setup wiki](https://github.com/CoplayDev/unity-mcp/wiki)
- type: textarea
id: what-happened
attributes:
label: What happened?
description: Clear, concrete description. What did you expect? What did you get?
placeholder: When I run X, I expect Y, but I see Z.
validations:
required: true
- type: textarea
id: reproduce
attributes:
label: Reproduction steps
description: Numbered steps. Include the exact prompt or command if relevant.
placeholder: |
1. Open Window > MCP for Unity
2. Click Start Server
3. In Claude Desktop, prompt: "create a red cube"
4. Observe error in console: ...
validations:
required: true
- type: input
id: unity-version
attributes:
label: Unity version
placeholder: "6000.0.34f1, 2022.3.42f1, etc."
validations:
required: true
- type: input
id: package-version
attributes:
label: MCP for Unity package version
description: Found under Window > Package Manager → MCP for Unity.
placeholder: "9.6.3"
validations:
required: true
- type: input
id: server-version
attributes:
label: Python server version
description: Run `uv pip show mcpforunityserver` or check the bundled wheel.
placeholder: "9.6.3"
validations:
required: false
- type: dropdown
id: client
attributes:
label: MCP client
options:
- Claude Desktop
- Claude Code
- Cursor
- VS Code (Copilot)
- Windsurf
- Cline
- GitHub Copilot CLI
- Codex
- Qwen Code
- Gemini CLI
- OpenClaw
- Other (specify in description)
validations:
required: true
- type: dropdown
id: transport
attributes:
label: Transport
options:
- HTTP (default)
- stdio
- Don't know
validations:
required: true
- type: dropdown
id: os
attributes:
label: OS
options:
- macOS
- Windows
- Linux
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant logs / console output
description: Editor console, MCP for Unity log, client log. Strip secrets.
render: text
validations:
required: false
- type: checkboxes
id: terms
attributes:
label: Checks
options:
- label: I searched existing issues and did not find a duplicate
required: true
- label: I included logs / steps to reproduce
required: true
+14
View File
@@ -0,0 +1,14 @@
blank_issues_enabled: false
contact_links:
- name: Discord — chat with maintainers
url: https://discord.gg/y4p8KfzrN4
about: Best for quick questions, troubleshooting, and design discussion.
- name: Documentation
url: https://coplaydev.github.io/unity-mcp/
about: Getting Started, guides, tool reference, and architecture notes.
- name: Security vulnerability
url: https://github.com/CoplayDev/unity-mcp/security/advisories/new
about: Report privately via GitHub Security Advisories or email security@coplay.dev. Do NOT open a public issue.
- name: Discussion / design idea
url: https://github.com/CoplayDev/unity-mcp/discussions
about: Broad questions, design conversations, show-and-tell.
@@ -0,0 +1,63 @@
name: Feature request
description: Suggest a new tool, capability, or quality-of-life improvement
title: "[Feature]: "
labels: ["enhancement", "needs-triage"]
body:
- type: markdown
attributes:
value: |
Got an idea? Great. The more concrete you can be about *who* this helps and *what* they're trying to do, the easier it is to scope.
For tool requests: please name the Unity API/system you'd be wrapping and what your MCP client would actually say.
- type: textarea
id: problem
attributes:
label: What problem are you trying to solve?
description: Frame the user need first, not the implementation.
placeholder: |
When I'm prototyping with the LLM, I can't get it to build a NavMesh because there's no MCP tool for the AI navigation system. I have to flip back to the Editor manually.
validations:
required: true
- type: textarea
id: proposal
attributes:
label: What would you like to see?
description: Be specific. New tool? New action on an existing tool? New resource? Workflow change?
placeholder: |
A new `manage_navigation` tool with actions: bake, clear, set_agent_radius, query_nearest_edge, ...
validations:
required: true
- type: textarea
id: prior-art
attributes:
label: Prior art / how do you work around this today?
placeholder: I currently bake manually via Window > AI > Navigation, then ask the LLM to reason about it.
validations:
required: false
- type: dropdown
id: scope
attributes:
label: Scope hint
options:
- New tool (substantial)
- New action on an existing tool
- New resource
- UX / editor window
- CLI improvement
- Documentation
- Other
validations:
required: true
- type: checkboxes
id: contribute
attributes:
label: Would you be willing to help build this?
options:
- label: I can open a PR if a maintainer sketches the approach
- label: I can help test
- label: I can write the docs
+111
View File
@@ -0,0 +1,111 @@
name: Publish Docker image
description: Build and push the Docker image to Docker Hub
inputs:
docker_username:
required: true
description: Docker Hub username
docker_password:
required: true
description: Docker Hub password
image:
required: true
description: Docker image name (e.g. user/repo)
version:
required: false
default: ""
description: Optional version string (e.g. 1.2.3). If provided, tags are computed from this version instead of the GitHub ref.
include_branch_tags:
required: false
default: "true"
description: Whether to also publish a branch tag when running on a branch ref (e.g. manual runs).
context:
required: false
default: .
description: Docker build context
dockerfile:
required: false
default: Server/Dockerfile
description: Path to Dockerfile
platforms:
required: false
default: linux/amd64
description: Target platforms
runs:
using: composite
steps:
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ inputs.docker_username }}
password: ${{ inputs.docker_password }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
if: ${{ inputs.version == '' && inputs.include_branch_tags == 'true' }}
with:
images: ${{ inputs.image }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=ref,event=branch
- name: Extract metadata (tags, labels) for Docker
id: meta_nobranch
uses: docker/metadata-action@v5
if: ${{ inputs.version == '' && inputs.include_branch_tags != 'true' }}
with:
images: ${{ inputs.image }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Compute Docker tags from version
id: version_tags
if: ${{ inputs.version != '' }}
shell: bash
run: |
set -euo pipefail
IFS='.' read -r MA MI PA <<< "${{ inputs.version }}"
echo "major=$MA" >> "$GITHUB_OUTPUT"
echo "minor=$MI" >> "$GITHUB_OUTPUT"
- name: Extract metadata (tags, labels) for Docker
id: meta_version
uses: docker/metadata-action@v5
if: ${{ inputs.version != '' && inputs.include_branch_tags == 'true' }}
with:
images: ${{ inputs.image }}
tags: |
type=raw,value=v${{ inputs.version }}
type=raw,value=v${{ steps.version_tags.outputs.major }}.${{ steps.version_tags.outputs.minor }}
type=raw,value=v${{ steps.version_tags.outputs.major }}
type=ref,event=branch
- name: Extract metadata (tags, labels) for Docker
id: meta_version_nobranch
uses: docker/metadata-action@v5
if: ${{ inputs.version != '' && inputs.include_branch_tags != 'true' }}
with:
images: ${{ inputs.image }}
tags: |
type=raw,value=v${{ inputs.version }}
type=raw,value=v${{ steps.version_tags.outputs.major }}.${{ steps.version_tags.outputs.minor }}
type=raw,value=v${{ steps.version_tags.outputs.major }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: ${{ inputs.context }}
file: ${{ inputs.dockerfile }}
platforms: ${{ inputs.platforms }}
push: true
tags: ${{ steps.meta.outputs.tags || steps.meta_nobranch.outputs.tags || steps.meta_version.outputs.tags || steps.meta_version_nobranch.outputs.tags }}
labels: ${{ steps.meta.outputs.labels || steps.meta_nobranch.outputs.labels || steps.meta_version.outputs.labels || steps.meta_version_nobranch.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
+22
View File
@@ -0,0 +1,22 @@
name: Publish Python distribution to PyPI
description: Build and publish the Python package from Server/ to PyPI
runs:
using: composite
steps:
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "latest"
enable-cache: true
cache-dependency-glob: "Server/uv.lock"
- name: Build a binary wheel and a source tarball
shell: bash
run: uv build
working-directory: ./Server
- name: Publish distribution to PyPI
# Pin to v1.12.4 to avoid Docker container name issue with uppercase repo names in v1.13.0+
uses: pypa/gh-action-pypi-publish@v1.12.4
with:
packages-dir: Server/dist/
+43
View File
@@ -0,0 +1,43 @@
## Description
<!-- Provide a brief description of your changes -->
## Type of Change
<!-- Check all that apply -->
- [ ] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
- [ ] Documentation update
- [ ] Refactoring (no functional changes)
- [ ] Test update
## Changes Made
<!-- List the specific changes in this PR -->
## Compatibility / Package Source
<!-- Especially important for Unity API compatibility, package-source, or transport changes -->
- Unity version(s) tested:
- Package source used (`#beta`, `#main`, tag, branch, or `file:`):
- Resolved commit hash from `Packages/packages-lock.json` (if using a Git package URL):
## Testing/Screenshots/Recordings
<!-- If applicable, add screenshots or recordings to demonstrate the changes -->
- [ ] Python tests (`cd Server && uv run pytest tests/ -v`)
- [ ] Unity EditMode tests
- [ ] Unity PlayMode tests
- [ ] Package import/compile check
- [ ] Not applicable (explain why in Additional Notes)
## 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 review of the generated changes
## Related Issues
<!-- Link any related issues using "Fixes #123" or "Relates to #123" -->
## Additional Notes
<!-- Any other information that reviewers should know -->
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env python3
"""
Post-processes a JUnit XML so that "expected"/environmental failures
(e.g., permission prompts, empty MCP resources, or schema hiccups)
are converted to <skipped/>. Leaves real failures intact.
Usage:
python .github/scripts/mark_skipped.py reports/claude-nl-tests.xml
"""
from __future__ import annotations
import sys
import os
import re
import xml.etree.ElementTree as ET
PATTERNS = [
r"\bpermission\b",
r"\bpermissions\b",
r"\bautoApprove\b",
r"\bapproval\b",
r"\bdenied\b",
r"requested\s+permissions",
r"^MCP resources list is empty$",
r"No MCP resources detected",
r"aggregator.*returned\s*\[\s*\]",
r"Unknown resource:\s*mcpforunity://",
r"Input should be a valid dictionary.*ctx",
r"validation error .* ctx",
]
def should_skip(msg: str) -> bool:
if not msg:
return False
msg_l = msg.strip()
for pat in PATTERNS:
if re.search(pat, msg_l, flags=re.IGNORECASE | re.MULTILINE):
return True
return False
def summarize_counts(ts: ET.Element):
tests = 0
failures = 0
errors = 0
skipped = 0
for case in ts.findall("testcase"):
tests += 1
if case.find("failure") is not None:
failures += 1
if case.find("error") is not None:
errors += 1
if case.find("skipped") is not None:
skipped += 1
return tests, failures, errors, skipped
def main(path: str) -> int:
if not os.path.exists(path):
print(f"[mark_skipped] No JUnit at {path}; nothing to do.")
return 0
try:
tree = ET.parse(path)
except ET.ParseError as e:
print(f"[mark_skipped] Could not parse {path}: {e}")
return 0
root = tree.getroot()
suites = root.findall("testsuite") if root.tag == "testsuites" else [root]
changed = False
for ts in suites:
for case in list(ts.findall("testcase")):
nodes = [n for n in list(case) if n.tag in ("failure", "error")]
if not nodes:
continue
# If any node matches skip patterns, convert the whole case to skipped.
first_match_text = None
to_skip = False
for n in nodes:
msg = (n.get("message") or "") + "\n" + (n.text or "")
if should_skip(msg):
first_match_text = (
n.text or "").strip() or first_match_text
to_skip = True
if to_skip:
for n in nodes:
case.remove(n)
reason = "Marked skipped: environment/permission precondition not met"
skip = ET.SubElement(case, "skipped")
skip.set("message", reason)
skip.text = first_match_text or reason
changed = True
# Recompute tallies per testsuite
tests, failures, errors, skipped = summarize_counts(ts)
ts.set("tests", str(tests))
ts.set("failures", str(failures))
ts.set("errors", str(errors))
ts.set("skipped", str(skipped))
if changed:
tree.write(path, encoding="utf-8", xml_declaration=True)
print(
f"[mark_skipped] Updated {path}: converted environmental failures to skipped.")
else:
print(f"[mark_skipped] No environmental failures detected in {path}.")
return 0
if __name__ == "__main__":
target = (
sys.argv[1]
if len(sys.argv) > 1
else os.environ.get("JUNIT_OUT", "reports/junit-nl-suite.xml")
)
raise SystemExit(main(target))
+241
View File
@@ -0,0 +1,241 @@
name: Beta Release (PyPI Pre-release)
concurrency:
group: beta-release
cancel-in-progress: true
on:
push:
branches:
- beta
paths:
- "Server/**"
- "MCPForUnity/**"
jobs:
unity_tests:
name: Unity tests gate
if: github.actor != 'github-actions[bot]'
uses: ./.github/workflows/unity-tests.yml
secrets: inherit
python_tests:
name: Python tests gate
if: github.actor != 'github-actions[bot]'
uses: ./.github/workflows/python-tests.yml
update_unity_beta_version:
name: Update Unity package to beta version
runs-on: ubuntu-latest
needs: [unity_tests, python_tests]
# Avoid running when the workflow's own automation merges the PR
# created by this workflow (prevents a version-bump loop).
if: github.actor != 'github-actions[bot]'
permissions:
contents: write
pull-requests: write
outputs:
unity_beta_version: ${{ steps.version.outputs.unity_beta_version }}
version_updated: ${{ steps.commit.outputs.updated }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
ref: beta
- name: Generate beta version for Unity package
id: version
shell: bash
run: |
set -euo pipefail
# Read current Unity package version
CURRENT_VERSION=$(jq -r '.version' MCPForUnity/package.json)
echo "Current Unity package version: $CURRENT_VERSION"
# Check if already a beta version - increment beta number
if [[ "$CURRENT_VERSION" =~ ^([0-9]+\.[0-9]+\.[0-9]+)-beta\.([0-9]+)$ ]]; then
BASE_VERSION="${BASH_REMATCH[1]}"
BETA_NUM="${BASH_REMATCH[2]}"
NEXT_BETA=$((BETA_NUM + 1))
BETA_VERSION="${BASE_VERSION}-beta.${NEXT_BETA}"
echo "Incrementing beta number: $CURRENT_VERSION -> $BETA_VERSION"
elif [[ "$CURRENT_VERSION" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
# Stable version - bump patch and add -beta.1 suffix
# This ensures beta is "newer" than stable (9.3.2-beta.1 > 9.3.1)
# The release workflow decides final bump type (patch/minor/major)
MAJOR="${BASH_REMATCH[1]}"
MINOR="${BASH_REMATCH[2]}"
PATCH="${BASH_REMATCH[3]}"
NEXT_PATCH=$((PATCH + 1))
BETA_VERSION="${MAJOR}.${MINOR}.${NEXT_PATCH}-beta.1"
echo "Converting stable to beta: $CURRENT_VERSION -> $BETA_VERSION"
else
echo "Error: Could not parse version '$CURRENT_VERSION'" >&2
exit 1
fi
# Always output the computed version
echo "unity_beta_version=$BETA_VERSION" >> "$GITHUB_OUTPUT"
# Only skip update if computed version matches current (no change needed)
if [[ "$BETA_VERSION" == "$CURRENT_VERSION" ]]; then
echo "Version unchanged, skipping update"
echo "needs_update=false" >> "$GITHUB_OUTPUT"
else
echo "Version will be updated: $CURRENT_VERSION -> $BETA_VERSION"
echo "needs_update=true" >> "$GITHUB_OUTPUT"
fi
- name: Update Unity package.json with beta version
if: steps.version.outputs.needs_update == 'true'
env:
BETA_VERSION: ${{ steps.version.outputs.unity_beta_version }}
shell: bash
run: |
set -euo pipefail
# Update package.json version
jq --arg v "$BETA_VERSION" '.version = $v' MCPForUnity/package.json > tmp.json
mv tmp.json MCPForUnity/package.json
echo "Updated MCPForUnity/package.json:"
jq '.version' MCPForUnity/package.json
- name: Commit to temporary branch and create PR
id: commit
if: steps.version.outputs.needs_update == 'true'
env:
GH_TOKEN: ${{ github.token }}
BETA_VERSION: ${{ steps.version.outputs.unity_beta_version }}
shell: bash
run: |
set -euo pipefail
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
if git diff --quiet MCPForUnity/package.json; then
echo "No changes to commit"
echo "updated=false" >> "$GITHUB_OUTPUT"
exit 0
fi
# Create a temporary branch for the version update
BRANCH="beta-version-${BETA_VERSION}-${GITHUB_RUN_ID}"
echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"
git checkout -b "$BRANCH"
git add MCPForUnity/package.json
git commit -m "chore: update Unity package to beta version ${BETA_VERSION}"
git push origin "$BRANCH"
# Check if PR already exists
if gh pr view "$BRANCH" >/dev/null 2>&1; then
echo "PR already exists for $BRANCH"
PR_NUMBER=$(gh pr view "$BRANCH" --json number -q '.number')
else
PR_URL=$(gh pr create \
--base beta \
--head "$BRANCH" \
--title "chore: update Unity package to beta version ${BETA_VERSION}" \
--body "Automated beta version bump for the Unity package.")
echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT"
PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$')
fi
echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT"
echo "updated=true" >> "$GITHUB_OUTPUT"
- name: Auto-merge version bump PR
if: steps.commit.outputs.updated == 'true' && steps.commit.outputs.pr_number != ''
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ steps.commit.outputs.pr_number }}
shell: bash
run: |
set -euo pipefail
gh pr merge "$PR_NUMBER" --merge --delete-branch
publish_pypi_prerelease:
name: Publish beta to PyPI (pre-release)
runs-on: ubuntu-latest
needs: [unity_tests, python_tests]
# Avoid double-publish when the bot merges the version bump PR
if: github.actor != 'github-actions[bot]'
environment:
name: pypi
url: https://pypi.org/p/mcpforunityserver
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
ref: beta
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "latest"
enable-cache: true
cache-dependency-glob: "Server/uv.lock"
- name: Generate beta version
id: version
shell: bash
run: |
set -euo pipefail
RAW_VERSION=$(grep -oP '(?<=version = ")[^"]+' Server/pyproject.toml)
echo "Raw version: $RAW_VERSION"
# Check if already a beta/prerelease version
if [[ "$RAW_VERSION" =~ (a|b|rc|\.dev|\.post)[0-9]+$ ]]; then
IS_PRERELEASE=true
# Strip the prerelease suffix to get base version
BASE_VERSION=$(echo "$RAW_VERSION" | sed -E 's/(a|b|rc|\.dev|\.post)[0-9]+$//')
else
IS_PRERELEASE=false
BASE_VERSION="$RAW_VERSION"
fi
# Validate we have a proper X.Y.Z format
if ! [[ "$BASE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: Could not parse version '$RAW_VERSION' -> '$BASE_VERSION'" >&2
exit 1
fi
IFS='.' read -r MAJOR MINOR PATCH <<< "$BASE_VERSION"
# Only bump patch if coming from stable; keep same base if already prerelease
if [[ "$IS_PRERELEASE" == "true" ]]; then
# Already on a beta series - keep the same base version
NEXT_PATCH="$PATCH"
echo "Already prerelease, keeping base: $BASE_VERSION"
else
# Stable version - bump patch to ensure beta is "newer"
NEXT_PATCH=$((PATCH + 1))
echo "Stable version, bumping patch: $PATCH -> $NEXT_PATCH"
fi
BETA_NUMBER="$(date +%Y%m%d%H%M%S)"
BETA_VERSION="${MAJOR}.${MINOR}.${NEXT_PATCH}b${BETA_NUMBER}"
echo "Base version: $BASE_VERSION"
echo "Beta version: $BETA_VERSION"
echo "beta_version=$BETA_VERSION" >> "$GITHUB_OUTPUT"
- name: Update version for beta release
env:
BETA_VERSION: ${{ steps.version.outputs.beta_version }}
shell: bash
run: |
set -euo pipefail
sed -i "s/^version = .*/version = \"${BETA_VERSION}\"/" Server/pyproject.toml
echo "Updated pyproject.toml:"
grep "^version" Server/pyproject.toml
- name: Build a binary wheel and a source tarball
shell: bash
run: uv build
working-directory: ./Server
- name: Publish distribution to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: Server/dist/
File diff suppressed because it is too large Load Diff
+97
View File
@@ -0,0 +1,97 @@
name: Docs — Build & Deploy
# Builds the Docusaurus site under /website and, on push to beta, deploys
# to GitHub Pages at https://coplaydev.github.io/unity-mcp/.
#
# PR runs only build (no deploy) as a preview-validation step.
# Re-deploys also fire when the Python tool/resource registry changes,
# so M3's auto-generated reference pages stay fresh.
on:
push:
branches: [beta]
paths:
- website/**
- docs/**
- Server/src/services/tools/**
- Server/src/services/resources/**
- Server/src/services/registry/**
- .github/workflows/docs-deploy.yml
pull_request:
branches: [beta, main]
paths:
- website/**
- docs/**
- Server/src/services/tools/**
- Server/src/services/resources/**
- Server/src/services/registry/**
- .github/workflows/docs-deploy.yml
workflow_dispatch: {}
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
jobs:
build:
name: Build site
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
# Full history so Docusaurus's showLastUpdateTime / showLastUpdateAuthor
# can resolve real per-file commit metadata. Shallow clones make every
# page report the latest commit instead.
fetch-depth: 0
# No push back from this workflow — the deploy uses the Pages
# OIDC token issued by actions/deploy-pages, not the repo token.
persist-credentials: false
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
cache-dependency-path: website/package-lock.json
- name: Install dependencies
working-directory: website
run: npm ci
- name: Setup Pages
if: github.event_name == 'push' && github.ref == 'refs/heads/beta'
uses: actions/configure-pages@v5
- name: Build
working-directory: website
env:
# Inject the cookieless GoatCounter beacon when provisioned. The site
# gates on process.env.GOATCOUNTER_CODE, which Actions does NOT
# auto-expose — the Actions variable must be mapped in explicitly.
GOATCOUNTER_CODE: ${{ vars.GOATCOUNTER_CODE }}
run: npm run build
- name: Upload Pages artifact
if: github.event_name == 'push' && github.ref == 'refs/heads/beta'
uses: actions/upload-pages-artifact@v3
with:
path: website/build
deploy:
name: Deploy to GitHub Pages
needs: build
if: github.event_name == 'push' && github.ref == 'refs/heads/beta'
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy
id: deployment
uses: actions/deploy-pages@v4
+72
View File
@@ -0,0 +1,72 @@
name: Docs — Reference Drift Check
# Fails a PR if the committed /website/docs/reference/ output disagrees
# with what tools/generate_docs_reference.py would produce from the live
# Python tool/resource registry. Contributors should regenerate locally:
#
# cd Server && uv run python ../tools/generate_docs_reference.py
#
# or install the pre-commit hook to make this automatic:
#
# tools/install-hooks.sh
on:
pull_request:
branches: [beta, main]
paths:
- Server/src/services/tools/**
- Server/src/services/resources/**
- Server/src/services/registry/**
- website/docs/reference/**
- tools/generate_docs_reference.py
- .github/workflows/docs-generate.yml
push:
branches: [beta]
paths:
- Server/src/services/tools/**
- Server/src/services/resources/**
- Server/src/services/registry/**
- website/docs/reference/**
- tools/generate_docs_reference.py
- .github/workflows/docs-generate.yml
workflow_dispatch: {}
jobs:
check:
name: Check docs reference is fresh
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: latest
- name: Set up Python
run: uv python install 3.10
- name: Sync Server deps
working-directory: Server
run: uv sync
- name: Drift check
working-directory: Server
run: uv run python ../tools/generate_docs_reference.py --check
- name: Tool-count sanity
run: |
set -euo pipefail
# Match the decorator at start-of-line (skips imports and docstring mentions).
decorator_count=$(grep -rhE "^@mcp_for_unity_tool\(" Server/src/services/tools/ | wc -l | tr -d ' ')
# md_count excludes group landing pages (index.md) and the catalog root.
md_count=$(find website/docs/reference/tools -name '*.md' -not -name 'index.md' | wc -l | tr -d ' ')
echo "decorator_count=$decorator_count, md_count=$md_count"
if [[ "$decorator_count" != "$md_count" ]]; then
echo "Mismatch: $decorator_count @mcp_for_unity_tool decorators vs $md_count reference pages." >&2
echo "Run: cd Server && uv run python ../tools/generate_docs_reference.py" >&2
exit 1
fi
+194
View File
@@ -0,0 +1,194 @@
name: E2E Bridge Smoke (deterministic, no LLM)
# Boots a headless Unity Editor, starts the Python MCP server's wire path, and
# drives a fixed sequence of real tool calls with exact assertions
# (Server/tests/e2e/bridge_smoke.py). Unlike claude-nl-suite.yml this needs
# NO Anthropic API key -- it is deterministic and cheap, so it can gate PRs and
# releases. It still needs Unity license secrets to boot the Editor.
on:
workflow_dispatch:
pull_request:
paths:
- "MCPForUnity/Editor/**"
- "MCPForUnity/Runtime/**"
- "Server/src/**"
- "Server/tests/e2e/**"
- "tools/local_harness.py"
- ".github/workflows/e2e-bridge.yml"
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
UNITY_IMAGE: unityci/editor:ubuntu-2021.3.45f2-linux-il2cpp-3
jobs:
e2e-bridge:
runs-on: ubuntu-24.04
timeout-minutes: 40
steps:
- name: Detect Unity license secrets
id: detect
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
run: |
set -e
if [ -n "$UNITY_LICENSE" ] || { [ -n "$UNITY_EMAIL" ] && [ -n "$UNITY_PASSWORD" ] && [ -n "$UNITY_SERIAL" ]; }; then
echo "unity_ok=true" >> "$GITHUB_OUTPUT"
else
echo "unity_ok=false" >> "$GITHUB_OUTPUT"
echo "::warning::Unity license secrets absent; E2E bridge smoke will be skipped (not failed)."
fi
- uses: actions/checkout@v4
if: steps.detect.outputs.unity_ok == 'true'
with:
fetch-depth: 0
- uses: astral-sh/setup-uv@v4
if: steps.detect.outputs.unity_ok == 'true'
with:
python-version: "3.11"
- name: Install MCP server
if: steps.detect.outputs.unity_ok == 'true'
run: |
set -eux
uv venv
echo "VIRTUAL_ENV=$GITHUB_WORKSPACE/.venv" >> "$GITHUB_ENV"
echo "$GITHUB_WORKSPACE/.venv/bin" >> "$GITHUB_PATH"
uv pip install -e Server
# --- License staging (mirrors claude-nl-suite.yml) ---
- name: Decide license sources
if: steps.detect.outputs.unity_ok == 'true'
id: lic
shell: bash
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
run: |
set -eu
use_ulf=false; use_ebl=false
[[ -n "${UNITY_LICENSE:-}" ]] && use_ulf=true
[[ -n "${UNITY_EMAIL:-}" && -n "${UNITY_PASSWORD:-}" && -n "${UNITY_SERIAL:-}" ]] && use_ebl=true
echo "use_ulf=$use_ulf" >> "$GITHUB_OUTPUT"
echo "use_ebl=$use_ebl" >> "$GITHUB_OUTPUT"
- name: Stage Unity .ulf license (from secret)
if: steps.detect.outputs.unity_ok == 'true' && steps.lic.outputs.use_ulf == 'true'
id: ulf
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
shell: bash
run: |
set -eu
mkdir -p "$RUNNER_TEMP/unity-license-ulf" "$RUNNER_TEMP/unity-local/Unity"
f="$RUNNER_TEMP/unity-license-ulf/Unity_lic.ulf"
if printf "%s" "$UNITY_LICENSE" | base64 -d - >/dev/null 2>&1; then
printf "%s" "$UNITY_LICENSE" | base64 -d - > "$f"
else
printf "%s" "$UNITY_LICENSE" > "$f"
fi
chmod 600 "$f" || true
if grep -qi '<Signature>' "$f"; then
cp -f "$f" "$RUNNER_TEMP/unity-local/Unity/Unity_lic.ulf"
echo "ok=true" >> "$GITHUB_OUTPUT"
else
echo "ok=false" >> "$GITHUB_OUTPUT"
fi
- name: Activate Unity (EBL via container)
if: steps.detect.outputs.unity_ok == 'true' && steps.lic.outputs.use_ebl == 'true'
shell: bash
env:
UNITY_IMAGE: ${{ env.UNITY_IMAGE }}
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
run: |
set -euo pipefail
mkdir -p "$RUNNER_TEMP/unity-config" "$RUNNER_TEMP/unity-local"
docker run --rm --network host \
-e HOME=/root -e UNITY_EMAIL -e UNITY_PASSWORD -e UNITY_SERIAL \
-v "$RUNNER_TEMP/unity-config:/root/.config/unity3d" \
-v "$RUNNER_TEMP/unity-local:/root/.local/share/unity3d" \
"$UNITY_IMAGE" bash -lc '
set -euxo pipefail
/opt/unity/Editor/Unity -batchmode -nographics -logFile - \
-username "$UNITY_EMAIL" -password "$UNITY_PASSWORD" -serial "$UNITY_SERIAL" -quit || true
'
- name: Warm up project (import Library once)
if: steps.detect.outputs.unity_ok == 'true'
shell: bash
env:
UNITY_IMAGE: ${{ env.UNITY_IMAGE }}
ULF_OK: ${{ steps.ulf.outputs.ok }}
run: |
set -euxo pipefail
manual_args=()
if [[ "${ULF_OK:-false}" == "true" ]]; then
manual_args=(-manualLicenseFile "/root/.local/share/unity3d/Unity/Unity_lic.ulf")
fi
docker run --rm --network host \
-e HOME=/root \
-v "${{ github.workspace }}:${{ github.workspace }}" -w "${{ github.workspace }}" \
-v "$RUNNER_TEMP/unity-config:/root/.config/unity3d" \
-v "$RUNNER_TEMP/unity-local:/root/.local/share/unity3d" \
-v "$RUNNER_TEMP/unity-cache:/root/.cache/unity3d" \
"$UNITY_IMAGE" /opt/unity/Editor/Unity -batchmode -nographics -logFile - \
-projectPath "${{ github.workspace }}/TestProjects/UnityMCPTests" \
"${manual_args[@]}" -quit
- name: Clean old MCP status
if: steps.detect.outputs.unity_ok == 'true'
run: |
set -eux
mkdir -p "$GITHUB_WORKSPACE/.unity-mcp"
rm -f "$GITHUB_WORKSPACE/.unity-mcp"/unity-mcp-status-*.json || true
- name: Run headless bridge harness (boot + wait + smoke/editmode/playmode)
if: steps.detect.outputs.unity_ok == 'true'
shell: bash
env:
UNITY_IMAGE: ${{ env.UNITY_IMAGE }}
ULF_OK: ${{ steps.ulf.outputs.ok }}
run: |
set -euxo pipefail
# In --ci mode the harness drives the DockerLauncher: it runs the same
# docker container (repo .unity-mcp status dir, docker liveness/teardown,
# log redaction), waits on the status file, derives the instance, then
# runs the smoke + EditMode + PlayMode legs over the bridge.
license_args=()
if [[ "${ULF_OK:-false}" == "true" ]]; then
license_args=(--editor-arg -manualLicenseFile \
--editor-arg "/root/.local/share/unity3d/Unity/Unity_lic.ulf")
fi
python3 tools/local_harness.py --ci \
--legs smoke,editmode,playmode \
--project-path TestProjects/UnityMCPTests \
--reports reports \
"${license_args[@]}"
- name: Unity logs on failure
if: failure() && steps.detect.outputs.unity_ok == 'true'
run: docker logs unity-mcp --tail 200 | sed -E 's/((email|serial|license|password|token)[^[:space:]]*)/[REDACTED]/Ig' || true
- name: Upload E2E report
if: always() && steps.detect.outputs.unity_ok == 'true'
uses: actions/upload-artifact@v4
with:
name: e2e-bridge-report
path: reports/junit-*.xml
if-no-files-found: ignore
+20
View File
@@ -0,0 +1,20 @@
name: github-repo-stats
on:
# schedule:
# Run this once per day, towards the end of the day for keeping the most
# recent data point most meaningful (hours are interpreted in UTC).
#- cron: "0 23 * * *"
workflow_dispatch: # Allow for running this manually.
jobs:
j1:
if: github.repository == 'CoplayDev/unity-mcp'
name: github-repo-stats
runs-on: ubuntu-latest
steps:
- name: run-ghrs
# Use latest release.
uses: jgehrcke/github-repo-stats@RELEASE
with:
ghtoken: ${{ secrets.ghrs_github_api_token }}
+81
View File
@@ -0,0 +1,81 @@
name: Python Tests
on:
push:
# Exclude beta and main: those branches re-trigger this workflow via
# workflow_call from beta-release.yml / release.yml. Without the exclusion,
# a push to beta that touches Server/** fires this workflow twice for the
# same SHA, and GitHub auto-cancels the duplicate.
branches-ignore: [beta, main]
paths:
- Server/**
- tools/**
- .github/workflows/python-tests.yml
pull_request:
branches: [main, beta]
paths:
- Server/**
- tools/**
- .github/workflows/python-tests.yml
workflow_dispatch: {}
workflow_call:
inputs:
ref:
description: "Git ref to test (defaults to the triggering ref)."
type: string
required: false
default: ""
jobs:
test:
name: Run Python Tests
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.ref }}
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: "latest"
- name: Set up Python
run: uv python install 3.10
- name: Install dependencies
run: |
cd Server
uv sync
uv pip install -e ".[dev]"
- name: Run tests with coverage
run: |
cd Server
uv run pytest tests/ -v --tb=short --cov --cov-report=xml --cov-report=html --cov-report=term
- name: Run local harness unit tests (hermetic, no Unity)
run: |
cd Server
uv run python -m pytest "$GITHUB_WORKSPACE/tools/tests/" -v --tb=short
- name: Upload coverage reports
uses: codecov/codecov-action@v4
if: always()
with:
files: ./Server/coverage.xml
flags: python
name: python-coverage
fail_ci_if_error: false
- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: pytest-results
path: |
Server/.pytest_cache/
Server/tests/
Server/coverage.xml
Server/htmlcov/
+557
View File
@@ -0,0 +1,557 @@
name: Release
concurrency:
group: release-main
cancel-in-progress: false
on:
workflow_dispatch:
inputs:
version_bump:
description: "Version bump type (none = release beta version as-is)"
type: choice
options:
- patch
- minor
- major
- none
default: patch
required: true
jobs:
unity_tests:
name: Unity tests gate
uses: ./.github/workflows/unity-tests.yml
with:
ref: beta
secrets: inherit
python_tests:
name: Python tests gate
uses: ./.github/workflows/python-tests.yml
with:
ref: beta
bump:
name: Bump version, tag, and create release
runs-on: ubuntu-latest
needs: [unity_tests, python_tests]
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: Show current versions
id: preview
shell: bash
run: |
set -euo pipefail
echo "============================================"
echo "CURRENT VERSION STATUS"
echo "============================================"
# Get main version
MAIN_VERSION=$(jq -r '.version' "MCPForUnity/package.json")
MAIN_PYPI=$(grep -oP '(?<=version = ")[^"]+' Server/pyproject.toml)
echo "Main branch:"
echo " Unity package: $MAIN_VERSION"
echo " PyPI server: $MAIN_PYPI"
echo ""
# Get beta version
git fetch origin beta
BETA_VERSION=$(git show origin/beta:MCPForUnity/package.json | jq -r '.version')
BETA_PYPI=$(git show origin/beta:Server/pyproject.toml | grep -oP '(?<=version = ")[^"]+')
echo "Beta branch:"
echo " Unity package: $BETA_VERSION"
echo " PyPI server: $BETA_PYPI"
echo ""
# Compute stripped version (used for "none" bump option)
STRIPPED=$(echo "$BETA_VERSION" | sed -E 's/-[a-zA-Z]+\.[0-9]+$//')
echo "stripped_version=$STRIPPED" >> "$GITHUB_OUTPUT"
# Show what will happen
BUMP="${{ inputs.version_bump }}"
echo "Selected bump type: $BUMP"
echo "After stripping beta suffix: $STRIPPED"
if [[ "$BUMP" == "none" ]]; then
echo "Release version will be: $STRIPPED"
else
IFS='.' read -r MA MI PA <<< "$STRIPPED"
case "$BUMP" in
major) ((MA+=1)); MI=0; PA=0 ;;
minor) ((MI+=1)); PA=0 ;;
patch) ((PA+=1)) ;;
esac
echo "Release version will be: $MA.$MI.$PA"
fi
echo "============================================"
- name: Merge beta into main
shell: bash
run: |
set -euo pipefail
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
# Fetch beta branch
git fetch origin beta
# Check if beta has changes not in main
if git merge-base --is-ancestor origin/beta HEAD; then
echo "beta is already merged into main. Nothing to merge."
else
echo "Merging beta into main..."
git merge origin/beta --no-edit -m "chore: merge beta into main for release"
echo "Beta merged successfully."
fi
- name: Strip beta suffix from version if present
shell: bash
run: |
set -euo pipefail
CURRENT_VERSION=$(jq -r '.version' "MCPForUnity/package.json")
echo "Current version: $CURRENT_VERSION"
# Strip beta/alpha/rc suffix if present (e.g., "9.4.0-beta.1" -> "9.4.0")
if [[ "$CURRENT_VERSION" == *"-"* ]]; then
STABLE_VERSION=$(echo "$CURRENT_VERSION" | sed -E 's/-[a-zA-Z]+\.[0-9]+$//')
# Validate we have a proper X.Y.Z format after stripping
if ! [[ "$STABLE_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: Could not parse version '$CURRENT_VERSION' -> '$STABLE_VERSION'" >&2
exit 1
fi
echo "Stripping prerelease suffix: $CURRENT_VERSION -> $STABLE_VERSION"
jq --arg v "$STABLE_VERSION" '.version = $v' MCPForUnity/package.json > tmp.json
mv tmp.json MCPForUnity/package.json
# Also update pyproject.toml
sed -i "s/^version = .*/version = \"${STABLE_VERSION}\"/" Server/pyproject.toml
else
echo "Version is already stable: $CURRENT_VERSION"
fi
- name: Compute new version
id: compute
env:
PREVIEWED_STRIPPED: ${{ steps.preview.outputs.stripped_version }}
shell: bash
run: |
set -euo pipefail
BUMP="${{ inputs.version_bump }}"
CURRENT_VERSION=$(jq -r '.version' "MCPForUnity/package.json")
echo "Current version: $CURRENT_VERSION"
# Sanity check: ensure current version matches what was previewed
if [[ "$CURRENT_VERSION" != "$PREVIEWED_STRIPPED" ]]; then
echo "Warning: Current version ($CURRENT_VERSION) differs from previewed ($PREVIEWED_STRIPPED)"
echo "This may indicate an unexpected merge result. Proceeding with current version."
fi
if [[ "$BUMP" == "none" ]]; then
# Use the previewed stripped version to ensure consistency with what user saw
NEW_VERSION="$PREVIEWED_STRIPPED"
else
IFS='.' read -r MA MI PA <<< "$CURRENT_VERSION"
case "$BUMP" in
major)
((MA+=1)); MI=0; PA=0
;;
minor)
((MI+=1)); PA=0
;;
patch)
((PA+=1))
;;
*)
echo "Unknown version_bump: $BUMP" >&2
exit 1
;;
esac
NEW_VERSION="$MA.$MI.$PA"
fi
echo "New version: $NEW_VERSION"
echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
echo "current_version=$CURRENT_VERSION" >> "$GITHUB_OUTPUT"
- name: Compute tag
id: tag
env:
NEW_VERSION: ${{ steps.compute.outputs.new_version }}
shell: bash
run: |
set -euo pipefail
echo "tag=v${NEW_VERSION}" >> "$GITHUB_OUTPUT"
- name: Update files to new version
env:
NEW_VERSION: ${{ steps.compute.outputs.new_version }}
shell: bash
run: |
set -euo pipefail
echo "Updating all version references to $NEW_VERSION"
python3 tools/update_versions.py --version "$NEW_VERSION"
- 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 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
echo "Pushing bump branch $BRANCH"
git push origin "$BRANCH"
- 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. 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:
tag_name: ${{ steps.tag.outputs.tag }}
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 beta
uses: actions/checkout@v6
with:
ref: beta
fetch-depth: 0
- name: Prepare sync branch from beta with merged main
id: sync_branch
env:
NEW_VERSION: ${{ needs.bump.outputs.new_version }}
shell: bash
run: |
set -euo pipefail
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
# Fetch both branches so we can build a merge commit in CI.
git fetch origin main beta
if git merge-base --is-ancestor origin/main origin/beta; then
echo "beta is already up to date with main. Skipping sync."
echo "skipped=true" >> "$GITHUB_OUTPUT"
exit 0
fi
SYNC_BRANCH="sync/main-v${NEW_VERSION}-into-beta-${GITHUB_RUN_ID}"
echo "name=$SYNC_BRANCH" >> "$GITHUB_OUTPUT"
echo "skipped=false" >> "$GITHUB_OUTPUT"
git checkout -b "$SYNC_BRANCH" origin/beta
if git merge origin/main --no-ff --no-commit; then
echo "main merged cleanly into sync branch."
else
echo "Merge conflicts detected. Attempting expected conflict resolution for beta version files."
CONFLICTS=$(git diff --name-only --diff-filter=U || true)
if [[ -n "$CONFLICTS" ]]; then
echo "$CONFLICTS"
fi
# Keep beta-side prerelease versions if these files conflict.
for file in MCPForUnity/package.json Server/pyproject.toml; do
if git ls-files -u -- "$file" | grep -q .; then
echo "Keeping beta version for $file"
git checkout --ours -- "$file"
git add "$file"
fi
done
REMAINING=$(git diff --name-only --diff-filter=U || true)
if [[ -n "$REMAINING" ]]; then
echo "Unexpected unresolved conflicts remain:"
echo "$REMAINING"
exit 1
fi
fi
git commit -m "chore: sync main (v${NEW_VERSION}) into beta"
# After releasing X.Y.Z on main, beta should move to X.Y.(Z+1)-beta.1.
IFS='.' read -r MAJOR MINOR PATCH <<< "$NEW_VERSION"
NEXT_PATCH=$((PATCH + 1))
NEXT_BETA_VERSION="${MAJOR}.${MINOR}.${NEXT_PATCH}-beta.1"
echo "beta_version=$NEXT_BETA_VERSION" >> "$GITHUB_OUTPUT"
echo "Setting beta version to $NEXT_BETA_VERSION"
CURRENT_BETA_VERSION=$(jq -r '.version' MCPForUnity/package.json)
if [[ "$CURRENT_BETA_VERSION" != "$NEXT_BETA_VERSION" ]]; then
jq --arg v "$NEXT_BETA_VERSION" '.version = $v' MCPForUnity/package.json > tmp.json
mv tmp.json MCPForUnity/package.json
git add MCPForUnity/package.json
git commit -m "chore: set beta version to ${NEXT_BETA_VERSION} after release v${NEW_VERSION}"
else
echo "Beta version already at target: $NEXT_BETA_VERSION"
fi
echo "Pushing sync branch $SYNC_BRANCH"
git push origin "$SYNC_BRANCH"
- name: Create PR to merge sync branch into beta
if: steps.sync_branch.outputs.skipped != 'true'
id: sync_pr
env:
GH_TOKEN: ${{ github.token }}
NEW_VERSION: ${{ needs.bump.outputs.new_version }}
NEXT_BETA_VERSION: ${{ steps.sync_branch.outputs.beta_version }}
SYNC_BRANCH: ${{ steps.sync_branch.outputs.name }}
shell: bash
run: |
set -euo pipefail
PR_URL=$(gh pr create \
--base beta \
--head "$SYNC_BRANCH" \
--title "chore: sync main (v${NEW_VERSION}) into beta" \
--body "Automated sync of main back into beta after release v${NEW_VERSION}, including beta version set to ${NEXT_BETA_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: Merge sync PR
if: steps.sync_branch.outputs.skipped != 'true'
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ steps.sync_pr.outputs.pr_number }}
shell: bash
run: |
set -euo pipefail
# Best effort: auto-merge if repository settings allow it.
gh pr merge "$PR_NUMBER" --merge --auto --delete-branch || true
# Retry direct merge for up to 2 minutes while checks settle.
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
if gh pr merge "$PR_NUMBER" --merge --delete-branch >/dev/null 2>&1; then
echo "Sync PR merged successfully."
exit 0
fi
echo "Waiting for sync PR to become mergeable... (state: $STATE)"
sleep 5
done
echo "Sync PR did not merge in time."
gh pr view "$PR_NUMBER" --json state,mergeStateStatus,isDraft -q '{state: .state, mergeStateStatus: .mergeStateStatus, isDraft: .isDraft}'
exit 1
publish_docker:
name: Publish Docker image
needs:
- bump
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Check out the repo
uses: actions/checkout@v6
with:
ref: ${{ needs.bump.outputs.tag }}
fetch-depth: 0
- name: Build and push Docker image
uses: ./.github/actions/publish-docker
with:
docker_username: ${{ secrets.DOCKER_USERNAME }}
docker_password: ${{ secrets.DOCKER_PASSWORD }}
image: ${{ secrets.DOCKER_USERNAME }}/mcp-for-unity-server
version: ${{ needs.bump.outputs.new_version }}
include_branch_tags: "false"
context: .
dockerfile: Server/Dockerfile
platforms: linux/amd64
publish_pypi:
name: Publish Python distribution to PyPI
needs:
- bump
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/mcpforunityserver
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.bump.outputs.tag }}
fetch-depth: 0
# Inlined from .github/actions/publish-pypi to avoid nested composite action issue
# with pypa/gh-action-pypi-publish (see https://github.com/pypa/gh-action-pypi-publish/issues/338)
- name: Install uv
uses: astral-sh/setup-uv@v7
with:
version: "latest"
enable-cache: true
cache-dependency-glob: "Server/uv.lock"
- name: Build a binary wheel and a source tarball
shell: bash
run: uv build
working-directory: ./Server
- name: Publish distribution to PyPI
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
+25
View File
@@ -0,0 +1,25 @@
name: Adoption stats (maintainer-only)
# Posts unified PyPI install + docs-traffic numbers to the workflow run summary,
# which is visible ONLY to repo collaborators. Nothing is published publicly.
on:
schedule:
- cron: '0 6 * * *'
workflow_dispatch: {}
permissions:
contents: read
jobs:
stats:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- name: Fetch + summarize stats (private to collaborators)
env:
# public stars/forks work with the default token; unique cloners/viewers
# need a maintainer PAT with Administration: read (STATS_GITHUB_TOKEN).
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
STATS_GITHUB_TOKEN: ${{ secrets.STATS_GITHUB_TOKEN }}
GOATCOUNTER_TOKEN: ${{ secrets.GOATCOUNTER_TOKEN }}
GOATCOUNTER_SITE: ${{ secrets.GOATCOUNTER_SITE }}
run: node website/scripts/fetch-stats.mjs >> "$GITHUB_STEP_SUMMARY"
+78
View File
@@ -0,0 +1,78 @@
name: Docs — Sync Release Notes
# Keeps website/docs/releases.md and the README's "Recent Updates" block
# in sync with the GitHub Releases API. Source of truth is GitHub Releases.
#
# Triggers (intentionally narrow):
# - release.published / edited / unpublished / deleted: the only time
# the synced files can legitimately go stale. Fires on every release
# event so the docs reflect the new version within ~30 seconds.
# - workflow_dispatch: manual escape hatch (re-run after a one-off
# GitHub UI edit to a release body, or to backfill after a downtime).
#
# Why no pull_request / schedule triggers:
# - PR-time drift-check would fail outsider PRs that touched README for
# unrelated reasons (e.g. typo fixes) and the contributor has no push
# access to fix it. The synced files are maintained by the release
# pipeline, not by PR authors — drift can't logically be introduced
# by a PR that the workflow couldn't already handle on release.
# - A daily cron would mask the source-of-truth (release events) and
# produce mystery commits unattached to a release. Better to let
# workflow_dispatch handle the rare out-of-band UI edit.
#
# Behavior:
# - Commits directly to `beta` with [skip ci] in the message — the
# change is mechanical (re-render from API output), pre-validated,
# and confined to two well-known files.
on:
release:
types: [published, edited, unpublished, deleted]
workflow_dispatch: {}
permissions:
contents: read
concurrency:
group: docs-sync-releases
cancel-in-progress: false
jobs:
sync:
name: Sync release notes
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout beta
uses: actions/checkout@v4
with:
ref: beta
# Full history so the commit lands on a fresh ref tip.
fetch-depth: 0
# `sync` needs to push back, so the token must persist here.
persist-credentials: true
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Sync release notes
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: python tools/sync_release_notes.py
- name: Commit & push (if anything changed)
run: |
set -euo pipefail
if git diff --quiet -- website/docs/releases.md README.md; then
echo "No drift — release notes already up to date."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add website/docs/releases.md README.md
git commit -m "docs: sync release notes from GitHub Releases [skip ci]"
git push origin beta
+265
View File
@@ -0,0 +1,265 @@
name: Unity Tests
on:
workflow_dispatch: {}
workflow_call:
inputs:
ref:
description: "Git ref to test (defaults to the triggering ref)."
type: string
required: false
default: ""
push:
# Exclude beta and main: those branches re-trigger this workflow via
# workflow_call from beta-release.yml / release.yml. Without the exclusion,
# a push to beta that touches MCPForUnity/** fires this workflow twice
# for the same SHA, and GitHub's auto-generated concurrency group
# (`unity-tests-refs/heads/beta`) cancels the older run with the noisy
# "higher priority waiting request" annotation.
branches-ignore: [beta, main]
paths:
- TestProjects/UnityMCPTests/**
- MCPForUnity/Editor/**
- MCPForUnity/Runtime/**
- .github/workflows/unity-tests.yml
# Same-repo PRs get a unity-tests status check on every open / push via this trigger
# (mirrors python-tests.yml). Fork PRs ALSO fire this trigger but run in the fork's
# context without secrets — the detect step downstream writes unity_ok=false and the
# job exits clean with a "missing license secrets" notice so the status check still
# appears. Maintainers apply 'safe-to-test' to invoke pull_request_target below for
# a real fork-PR test run.
pull_request:
branches: [main, beta]
paths:
- TestProjects/UnityMCPTests/**
- MCPForUnity/Editor/**
- MCPForUnity/Runtime/**
- .github/workflows/unity-tests.yml
# Fork PRs: maintainer applies the 'safe-to-test' label after reviewing
# the diff. The workflow runs with UNITY_LICENSE in scope against the
# PR's head SHA. Re-pushed commits do NOT auto-trigger — maintainer must
# remove and re-apply the label to re-run after additional review.
pull_request_target:
types: [labeled]
branches: [main, beta]
paths:
- TestProjects/UnityMCPTests/**
- MCPForUnity/Editor/**
- MCPForUnity/Runtime/**
- .github/workflows/unity-tests.yml
# Dedup runs for the same branch across push / pull_request / pull_request_target / workflow_call.
# Same-repo PRs would otherwise fire both push (on the branch SHA) AND pull_request (on the PR);
# concurrency keeps only the newer in-flight run per branch.
concurrency:
group: unity-tests-${{ github.head_ref || github.ref }}
cancel-in-progress: true
jobs:
matrix:
name: Compute Unity version matrix
runs-on: ubuntu-latest
permissions:
contents: read
# Gate (mirrored by testAllModes below):
# - Always run for non-PR triggers (push / workflow_call / workflow_dispatch).
# - Fork PRs: require 'safe-to-test' to be applied (existing secret-safety gate);
# 'full-matrix' may be added on top to opt into the full 4-version matrix.
# - In-repo PRs: only re-run via pull_request_target when 'full-matrix' is the
# label that just fired (the push-event run already covered the default leg).
if: >
github.event_name != 'pull_request_target' ||
(
github.event.pull_request.head.repo.full_name != github.repository &&
contains(github.event.pull_request.labels.*.name, 'safe-to-test') &&
(github.event.label.name == 'safe-to-test' || github.event.label.name == 'full-matrix')
) ||
(
github.event.pull_request.head.repo.full_name == github.repository &&
github.event.label.name == 'full-matrix'
)
outputs:
versions: ${{ steps.set.outputs.versions }}
steps:
- name: Checkout version manifest
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref || github.event.pull_request.head.sha || github.ref }}
sparse-checkout: tools/unity-versions.json
sparse-checkout-cone-mode: false
persist-credentials: false
- name: Select versions for this trigger
id: set
env:
EVENT_NAME: ${{ github.event_name }}
GH_REF: ${{ github.ref }}
FULL_MATRIX_LABEL: ${{ contains(github.event.pull_request.labels.*.name, 'full-matrix') }}
run: |
set -euo pipefail
# Full matrix on: beta push, workflow_call (release pipelines), workflow_dispatch,
# or any PR (pull_request OR pull_request_target) labeled with 'full-matrix'.
# Default (single defaultVersion from tools/unity-versions.json) otherwise — fast PR feedback.
if [[ "$EVENT_NAME" == "workflow_dispatch" ]] || \
[[ "$EVENT_NAME" == "workflow_call" ]] || \
{ [[ "$EVENT_NAME" == "push" ]] && [[ "$GH_REF" == "refs/heads/beta" ]]; } || \
{ { [[ "$EVENT_NAME" == "pull_request" ]] || [[ "$EVENT_NAME" == "pull_request_target" ]]; } && [[ "$FULL_MATRIX_LABEL" == "true" ]]; }; then
versions=$(jq -c '[.versions[].id]' tools/unity-versions.json)
echo "Trigger '$EVENT_NAME' on ref '$GH_REF' (full_matrix_label=$FULL_MATRIX_LABEL) → full matrix: $versions"
else
versions=$(jq -c '[.defaultVersion]' tools/unity-versions.json)
echo "Trigger '$EVENT_NAME' on ref '$GH_REF' → default only: $versions"
fi
echo "versions=$versions" >> "$GITHUB_OUTPUT"
testAllModes:
name: Test in ${{ matrix.testMode }} on Unity ${{ matrix.unityVersion }}
needs: matrix
runs-on: ubuntu-latest
permissions:
contents: read
if: >
github.event_name != 'pull_request_target' ||
(
github.event.pull_request.head.repo.full_name != github.repository &&
contains(github.event.pull_request.labels.*.name, 'safe-to-test') &&
(github.event.label.name == 'safe-to-test' || github.event.label.name == 'full-matrix')
) ||
(
github.event.pull_request.head.repo.full_name == github.repository &&
github.event.label.name == 'full-matrix'
)
strategy:
fail-fast: false
matrix:
projectPath:
- TestProjects/UnityMCPTests
testMode:
- editmode
unityVersion: ${{ fromJson(needs.matrix.outputs.versions) }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
lfs: true
ref: ${{ inputs.ref || github.event.pull_request.head.sha || github.ref }}
persist-credentials: false
- name: Detect Unity license secrets
id: detect
env:
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
run: |
set -e
if [ -n "$UNITY_LICENSE" ] || { [ -n "$UNITY_EMAIL" ] && [ -n "$UNITY_PASSWORD" ] && [ -n "$UNITY_SERIAL" ]; }; then
echo "unity_ok=true" >> "$GITHUB_OUTPUT"
else
echo "unity_ok=false" >> "$GITHUB_OUTPUT"
fi
- name: Skip Unity tests (missing license secrets)
if: steps.detect.outputs.unity_ok != 'true'
run: |
echo "Unity license secrets missing; skipping Unity tests."
- uses: actions/cache@v4
with:
path: ${{ matrix.projectPath }}/Library
key: Library-${{ matrix.projectPath }}-${{ matrix.unityVersion }}
restore-keys: |
Library-${{ matrix.projectPath }}-
Library-
# Run domain reload tests first (they're [Explicit] so need explicit category)
- name: Run domain reload tests
if: steps.detect.outputs.unity_ok == 'true'
uses: game-ci/unity-test-runner@v4
id: domain-tests
env:
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
with:
projectPath: ${{ matrix.projectPath }}
unityVersion: ${{ matrix.unityVersion }}
testMode: ${{ matrix.testMode }}
customParameters: -testCategory domain_reload
- name: Run tests
if: steps.detect.outputs.unity_ok == 'true'
uses: game-ci/unity-test-runner@v4
id: tests
continue-on-error: true
env:
UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }}
UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }}
UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }}
UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }}
with:
projectPath: ${{ matrix.projectPath }}
unityVersion: ${{ matrix.unityVersion }}
testMode: ${{ matrix.testMode }}
- name: Check test results
if: steps.detect.outputs.unity_ok == 'true'
env:
ARTIFACTS_PATH: ${{ steps.tests.outputs.artifactsPath }}
run: |
set -euo pipefail
# `|| true` so a missing $ARTIFACTS_PATH (Unity crashed before producing any) doesn't trip
# `pipefail` and skip the explicit empty-check diagnostic below.
RESULTS_XML=$(find "$ARTIFACTS_PATH" -name '*.xml' 2>/dev/null | head -1 || true)
if [ -z "$RESULTS_XML" ]; then
echo "::error::No test results XML found — Unity may have crashed"
exit 1
fi
python3 - "$RESULTS_XML" <<'PY'
import sys, xml.etree.ElementTree as ET
# Escape workflow-command payloads so test-controlled XML (under pull_request_target this
# is fork-supplied) can't break annotation rendering or inject extra workflow commands.
# https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions
def esc_data(s):
return s.replace("%", "%25").replace("\r", "%0D").replace("\n", "%0A")
def esc_prop(s):
return esc_data(s).replace(":", "%3A").replace(",", "%2C")
root = ET.parse(sys.argv[1]).getroot()
totals = root.attrib
passed = totals.get("passed", "?")
failed = totals.get("failed", "?")
total = totals.get("total", "?")
incon = totals.get("inconclusive", "?")
skipped = totals.get("skipped", "?")
print(f"Results: {passed} passed, {failed} failed, {incon} inconclusive, {skipped} skipped (total: {total})")
fails = [tc for tc in root.iter("test-case") if tc.attrib.get("result") == "Failed"]
if not fails:
sys.exit(0)
# Surface every failure inline so a CI watcher doesn't need to download the NUnit XML artifact.
for tc in fails:
name = tc.attrib.get("fullname") or tc.attrib.get("name") or "<unknown>"
f = tc.find("failure")
msg = (f.findtext("message") or "").strip() if f is not None else ""
stack = (f.findtext("stack-trace") or "").strip() if f is not None else ""
# First line of the message becomes the GitHub annotation title.
first_line = msg.splitlines()[0] if msg else "(no message)"
# GitHub annotations don't render multi-line bodies, so emit the full failure inside a collapsible group.
print(f"::error title=Failed: {esc_prop(name)}::{esc_data(first_line)}")
print(f"::group::Failure details — {esc_data(name)}")
if msg:
print("Message:")
print(msg)
if stack:
print("Stack trace:")
print(stack)
print("::endgroup::")
print(f"::error::{len(fails)} test(s) failed")
sys.exit(1)
PY
- uses: actions/upload-artifact@v4
if: always() && steps.detect.outputs.unity_ok == 'true' && steps.tests.outcome != 'skipped'
with:
name: Test results for ${{ matrix.testMode }} on Unity ${{ matrix.unityVersion }}
path: ${{ steps.tests.outputs.artifactsPath }}
+75
View File
@@ -0,0 +1,75 @@
# AI-related files (user-specific)
.cursorrules
.cursorignore
.windsurf
.codeiumignore
.kiro
# Local worktrees for parallel feature work
.worktrees/
# Code-copy related files
.clipignore
# Python-generated files
__pycache__/
__pycache__.meta
build/
!MCPForUnity/**/Build/
dist/
wheels/
*.egg-info
# Test coverage
.coverage
.coverage.*
htmlcov/
coverage.xml
*.cover
# Virtual environments
.venv
# Environment files (API keys)
.env
# Unity Editor
*.unitypackage
*.asset
LICENSE.meta
CONTRIBUTING.md.meta
# IDE
.idea/
.vscode/
.aider*
.DS_Store*
# Unity test project lock files
TestProjects/UnityMCPTests/Packages/packages-lock.json
# UnityMCPTests stress-run artifacts (these are created by tests/tools and should never be committed)
TestProjects/UnityMCPTests/Assets/Temp/
# Backup artifacts
*.backup
*.backup.meta
.wt-origin-main/
# CI test reports (generated during test runs)
reports/
# Local testing harness
scripts/local-test/
.claude/settings.local.json
# Per-version logs from tools/check-unity-versions.{sh,ps1}
tools/.unity-check-logs/
# Ignore the .claude directory, since it might contain local/project-level setting such as deny and allowlist.
/.claude
.mcp.json
# Superpowers skill working artifacts (specs, plans, SDD ledger)
docs/superpowers/
.superpowers/
+82
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
+200
View File
@@ -0,0 +1,200 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## What This Project Is
**MCP for Unity** is a bridge that lets AI assistants (Claude, Cursor, Windsurf, etc.) control the Unity Editor through the Model Context Protocol (MCP). It enables AI-driven game development workflows - creating GameObjects, editing scripts, managing assets, running tests, and more.
## Architecture
```text
AI Assistant (Claude/Cursor)
↓ MCP Protocol (stdio/HTTP)
Python Server (Server/src/)
↓ WebSocket + HTTP
Unity Editor Plugin (MCPForUnity/)
↓ Unity Editor API
Scene, Assets, Scripts
```
**Two codebases, one system:**
- `Server/` - Python MCP server using FastMCP
- `MCPForUnity/` - Unity C# Editor package
### Three Layers on the Python Side
The Python server has three distinct layers. These are **not** auto-generated from each other:
| Layer | Location | Framework | Purpose |
|-------|----------|-----------|---------|
| **MCP Tools** | `Server/src/services/tools/` | FastMCP (`@mcp_for_unity_tool`) | Exposed to AI assistants via MCP protocol |
| **CLI Commands** | `Server/src/cli/commands/` | Click (`@click.command`) | Terminal interface for developers |
| **Resources** | `Server/src/services/resources/` | FastMCP (`@mcp_for_unity_resource`) | Read-only state exposed to AI assistants |
MCP tools call Unity via WebSocket (`send_with_unity_instance`). CLI commands call Unity via HTTP (`run_command`). Both route to the same C# `HandleCommand` methods.
### Transport Modes
- **Stdio**: Single-agent only. Separate Python process per client. Legacy TCP bridge to Unity. New connections stomp old ones.
- **HTTP**: Multi-agent ready. Single shared Python server. WebSocket hub at `/hub/plugin`. Session isolation via `client_id`.
## Code Philosophy
### 1. Domain Symmetry
Python MCP tools mirror C# Editor tools. Each domain exists in both:
- `Server/src/services/tools/manage_material.py``MCPForUnity/Editor/Tools/ManageMaterial.cs`
- CLI commands (`Server/src/cli/commands/`) also mirror these but are a separate implementation.
### 2. Minimal Abstraction
Avoid premature abstraction. Three similar lines of code is better than a helper that's used once. Only abstract when you have 3+ genuine use cases.
### 3. Delete Rather Than Deprecate
When removing functionality, delete it completely. No `_unused` renames, no `// removed` comments, no backwards-compatibility shims for internal code.
### 4. Test Coverage Required
Every new feature needs tests. Run them before PRs.
### 5. Keep Tools Focused
Each MCP tool does one thing well. Resist the urge to add "convenient" parameters that bloat the API surface.
### 6. Use Resources for Reading
Keep them smart and focused rather than "read everything" type resources. Resources should be quick and LLM-friendly.
## Key Patterns
### Python MCP Tool Registration
Tools in `Server/src/services/tools/` are auto-discovered. Use the `@mcp_for_unity_tool` decorator:
```python
from services.registry import mcp_for_unity_tool
@mcp_for_unity_tool(
description="Does something in Unity.",
group="core", # core (default), vfx, animation, ui, scripting_ext, testing, probuilder, profiling, docs
)
async def manage_something(
ctx: Context,
action: Annotated[Literal["create", "delete"], "Action to perform"],
) -> dict[str, Any]:
unity_instance = await get_unity_instance_from_context(ctx)
params = {"action": action}
response = await send_with_unity_instance(async_send_command_with_retry, unity_instance, "manage_something", params)
return response
```
The `group` parameter controls tool visibility. Only `"core"` is enabled by default. Non-core groups (vfx, animation, etc.) start disabled and are toggled via `manage_tools`.
### Python CLI Error Handling
CLI commands (not MCP tools) use the `@handle_unity_errors` decorator:
```python
@handle_unity_errors
async def my_command(ctx, ...):
result = await call_unity_tool(...)
```
### C# Tool Registration
Tools are auto-discovered by `CommandRegistry` via reflection. Use the `[McpForUnityTool]` attribute:
```csharp
[McpForUnityTool("manage_something", AutoRegister = false, Group = "core")]
public static class ManageSomething
{
// Sync handler (most tools):
public static object HandleCommand(JObject @params)
{
var p = new ToolParams(@params);
// ...
return new SuccessResponse("Done.", new { data = result });
}
// OR async handler (for long-running operations like play-test, refresh, batch):
public static async Task<object> HandleCommand(JObject @params)
{
// CommandRegistry detects Task return type automatically
await SomeAsyncOperation();
return new SuccessResponse("Done.");
}
}
```
Async handlers use `EditorApplication.update` polling with `TaskCompletionSource` — see `RefreshUnity.cs` for the canonical pattern.
### C# Parameter Handling
Use `ToolParams` for consistent parameter validation:
```csharp
var p = new ToolParams(parameters);
var pageSize = p.GetInt("page_size", "pageSize") ?? 50;
var name = p.RequireString("name");
```
### C# Resources
Resources use `[McpForUnityResource]` and follow the same `HandleCommand` pattern as tools. They provide read-only state to AI assistants.
### Paging Large Results
Always page results that could be large (hierarchies, components, search results):
- Use `page_size` and `cursor` parameters
- Return `next_cursor` when more results exist
### Composing Tools Internally (C#)
Use `CommandRegistry.InvokeCommandAsync` to call other tools from within a handler:
```csharp
var result = await CommandRegistry.InvokeCommandAsync("read_console", consoleParams);
```
### Unity API Compatibility Shims
We support a wide Unity version range (2021+ → 6.x → CoreCLR 6.8). When an API is renamed, deprecated, or removed across versions, **don't sprinkle `#if UNITY_x_y_OR_NEWER` at every call site** — add a shim in `MCPForUnity/Runtime/Helpers/Unity*Compat.cs` and route every caller through it.
The catalog of active shims, the policy for when to add one, what does NOT belong in a shim, and the reflection-cache pattern all live in **`MCPForUnity/Runtime/Helpers/UnityCompatShims.cs`** — the XML doc on that empty marker class is the source of truth and ships inside the UPM package, so end-users can `F12`/Go-to-definition into it. Sources for current deprecations: Unity 6.x upgrade guides and the [CoreCLR 2026 thread](https://discussions.unity.com/t/path-to-coreclr-2026-upgrade-guide/1714279).
When you touch a shim or anything else gated by `#if UNITY_*_OR_NEWER`, run `tools/check-unity-versions.sh` to compile-check across the CI matrix locally before committing — the matrix lives in `tools/unity-versions.json`.
## Commands
### Running Tests
```bash
# Python (all tests)
cd Server && uv run pytest tests/ -v
# Python (single test file)
cd Server && uv run pytest tests/test_manage_material.py -v
# Python (single test by name)
cd Server && uv run pytest tests/ -k "test_create_material" -v
# Unity - open TestProjects/UnityMCPTests in Unity, use Test Runner window
# Local multi-version compile check (parity with CI matrix, see tools/unity-versions.json)
tools/check-unity-versions.sh # compile-only across installed Unity Hub editors
tools/check-unity-versions.sh --full # full EditMode test run
```
#### Local headless test harness
One command boots a headless Hub-licensed Editor against `TestProjects/UnityMCPTests` and runs the smoke + EditMode + PlayMode legs over the bridge — the same entrypoint CI uses (`.github/workflows/e2e-bridge.yml`):
```bash
python tools/local_harness.py
```
Key flags: `--legs smoke,editmode,playmode` (subset to run), `--project-path` (target project, default `TestProjects/UnityMCPTests`), `--reuse` (attach to an already-resident bridge instead of booting one), `--keep-alive` (leave the Editor running after the legs), `--no-warmup` (skip the warm-up import phase).
Exit codes: `0` pass, `1` blocking-leg regression, `2` bridge unreachable / setup failure, `3` project does not compile, `4` no Unity license / Hub seat, `5` Editor binary/version not found. Requires a Hub-activated Editor locally (no ULF/serial).
### Local Development
1. Set **Server Source Override** in MCP for Unity Advanced Settings to your local `Server/` path
2. Enable **Dev Mode** checkbox to force fresh installs
3. Use `mcp_source.py` to switch Unity package sources
4. Test on Windows and Mac if possible, and multiple clients (Claude Desktop and Claude Code are tricky for configuration as of this writing)
### Adding a New Tool
1. Add Python MCP tool in `Server/src/services/tools/manage_<domain>.py` using `@mcp_for_unity_tool`
2. Add Python CLI commands in `Server/src/cli/commands/<domain>.py` using Click
3. Add C# implementation in `MCPForUnity/Editor/Tools/Manage<Domain>.cs` with `[McpForUnityTool]`
4. Add Python tests in `Server/tests/test_manage_<domain>.py`
5. Add Unity tests in `TestProjects/UnityMCPTests/Assets/Tests/`
## What Not To Do
- Don't add features without tests
- Don't create helper functions for one-time operations
- Don't add error handling for scenarios that can't happen
- Don't commit to `main` directly - branch off `beta` for PRs
- Don't add docstrings/comments to code you didn't change
+75
View File
@@ -0,0 +1,75 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in MCP for Unity a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes
- Focusing on what is best for the community as a whole
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information (physical or email address) without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting
## Enforcement Responsibilities
Project maintainers are responsible for clarifying and enforcing these standards and will take appropriate and fair corrective action in response to any behavior they deem inappropriate, threatening, offensive, or harmful.
Maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned with this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces — GitHub repository, Discord, official communications channels — and also when an individual is officially representing the community in public spaces.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the maintainers at **conduct@coplay.dev**. All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the reporter of any incident.
## Enforcement Guidelines
Project maintainers will follow these guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact:** Use of inappropriate language or other behavior deemed unprofessional or unwelcome.
**Consequence:** A private, written warning, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact:** A violation through a single incident or series of actions.
**Consequence:** A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time.
### 3. Temporary Ban
**Community Impact:** A serious violation of community standards, including sustained inappropriate behavior.
**Consequence:** A temporary ban from any sort of interaction or public communication with the community for a specified period of time.
### 4. Permanent Ban
**Community Impact:** Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
**Consequence:** A permanent ban from any sort of public interaction within the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html.
[homepage]: https://www.contributor-covenant.org
+71
View File
@@ -0,0 +1,71 @@
# Contributing to MCP for Unity
Thanks for wanting to help! MCP for Unity is community-maintained and PRs of any size are welcome — bug fixes, new tools, docs improvements, tests.
## Quick Start
1. **Fork** this repo and **clone** your fork.
2. Branch off `beta` (not `main`):
```bash
git checkout -b feat/your-idea upstream/beta
```
3. Install the dev environment (see [Dev Setup](https://coplaydev.github.io/unity-mcp/contributing/dev-setup)).
4. Make your change with tests.
5. Open a PR against `beta`. PRs against `main` will be redirected.
## What We Look For
- **Tests for new behavior.** Python tests live in `Server/tests/`; Unity EditMode tests live in `TestProjects/UnityMCPTests/Assets/Tests/`.
- **Domain symmetry.** New tools live in *both* `Server/src/services/tools/manage_<domain>.py` (Python MCP tool) and `MCPForUnity/Editor/Tools/Manage<Domain>.cs` (C# implementation). See [Adding a New Tool](https://coplaydev.github.io/unity-mcp/contributing/dev-setup).
- **Minimal abstraction.** Three similar lines of code is better than a helper that's only used once.
- **Documentation as code.** Tool reference pages under `website/docs/reference/` are auto-generated — never hand-edit them outside the `<!-- examples:start --><!-- examples:end -->` blocks.
## Before You Push
```bash
# Python tests
cd Server && uv run pytest tests/ -v
# Unity multi-version compile check (matches CI)
tools/check-unity-versions.sh
# Pre-commit hook for docs reference (one-time setup)
tools/install-hooks.sh
```
The pre-commit hook regenerates `website/docs/reference/` whenever you touch a tool/resource module — saves you a CI round trip.
## Pull Request Checklist
- [ ] Branched off `beta`
- [ ] New or updated tests
- [ ] Docs updated (the auto-gen handles the tool reference; narrative docs under `website/docs/` are hand-written)
- [ ] No commented-out code, no `// removed for X` markers, no `_unused` renames
- [ ] PR description explains the **why**, not just the **what**
## Code Style
- **Python:** type hints required; follow the patterns in existing `manage_*.py` files.
- **C#:** match existing namespace conventions under `MCPForUnity.Editor.*`; route Unity API differences through `MCPForUnity/Runtime/Helpers/Unity*Compat.cs` shims rather than `#if UNITY_*_OR_NEWER` blocks.
- **Markdown:** wrap at sensible widths; use sentence case in headings.
## Areas That Need Help
- Examples in tool reference pages (`website/docs/reference/tools/**/*.md` — add inside the `<!-- examples:start --><!-- examples:end -->` blocks).
- Net-new guide content (multi-instance routing, tool groups, transport modes).
- Translations beyond Chinese.
- Cross-platform shell testing for the CLI.
## Reporting Bugs / Requesting Features
Use the issue templates under [`.github/ISSUE_TEMPLATE/`](.github/ISSUE_TEMPLATE/). For security concerns, see [SECURITY.md](SECURITY.md) — do **not** open a public issue.
## Code of Conduct
See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md). Be excellent to each other.
## Questions?
- [GitHub Issues](https://github.com/CoplayDev/unity-mcp/issues) — bugs, features
- [Discord](https://discord.gg/y4p8KfzrN4) — chat with maintainers and other contributors
- [Discussions](https://github.com/CoplayDev/unity-mcp/discussions) — design ideas, broad questions
@@ -0,0 +1,528 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using Newtonsoft.Json.Linq;
using UnityEngine;
using UnityEditor;
using MCPForUnity.Editor.Helpers;
#if USE_ROSLYN
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
#endif
namespace MCPForUnity.Editor.Tools
{
/// <summary>
/// Runtime compilation tool for MCP Unity.
/// Compiles and loads C# code at runtime without triggering domain reload via Roslyn Runtime Compilation, where in traditional Unity workflow it would take seconds to reload assets and reset script states for each script change.
/// </summary>
[McpForUnityTool(
name:"runtime_compilation",
Description = "Enable runtime compilation of C# code within Unity without domain reload via Roslyn.")]
public static class ManageRuntimeCompilation
{
private static readonly Dictionary<string, LoadedAssemblyInfo> LoadedAssemblies = new Dictionary<string, LoadedAssemblyInfo>();
private static string DynamicAssembliesPath => Path.Combine(Application.temporaryCachePath, "DynamicAssemblies");
private class LoadedAssemblyInfo
{
public string Name;
public Assembly Assembly;
public string DllPath;
public DateTime LoadedAt;
public List<string> TypeNames;
}
public static object HandleCommand(JObject @params)
{
string action = @params["action"]?.ToString()?.ToLower();
if (string.IsNullOrEmpty(action))
{
return new ErrorResponse("Action parameter is required. Valid actions: compile_and_load, list_loaded, get_types, execute_with_roslyn, get_history, save_history, clear_history");
}
switch (action)
{
case "compile_and_load":
return CompileAndLoad(@params);
case "list_loaded":
return ListLoadedAssemblies();
case "get_types":
return GetAssemblyTypes(@params);
case "execute_with_roslyn":
return ExecuteWithRoslyn(@params);
case "get_history":
return GetCompilationHistory();
case "save_history":
return SaveCompilationHistory();
case "clear_history":
return ClearCompilationHistory();
default:
return new ErrorResponse($"Unknown action '{action}'. Valid actions: compile_and_load, list_loaded, get_types, execute_with_roslyn, get_history, save_history, clear_history");
}
}
private static object CompileAndLoad(JObject @params)
{
#if !USE_ROSLYN
return new ErrorResponse(
"Runtime compilation requires Roslyn. Please install Microsoft.CodeAnalysis.CSharp NuGet package and add USE_ROSLYN to Scripting Define Symbols. " +
"See ManageScript.cs header for installation instructions."
);
#else
try
{
string code = @params["code"]?.ToString();
string assemblyName = @params["assembly_name"]?.ToString() ?? $"DynamicAssembly_{DateTime.Now.Ticks}";
string attachTo = @params["attach_to"]?.ToString();
bool loadImmediately = @params["load_immediately"]?.ToObject<bool>() ?? true;
if (string.IsNullOrEmpty(code))
{
return new ErrorResponse("'code' parameter is required");
}
// Ensure unique assembly name
if (LoadedAssemblies.ContainsKey(assemblyName))
{
assemblyName = $"{assemblyName}_{DateTime.Now.Ticks}";
}
// Create output directory
Directory.CreateDirectory(DynamicAssembliesPath);
string dllPath = Path.Combine(DynamicAssembliesPath, $"{assemblyName}.dll");
// Parse code
var syntaxTree = CSharpSyntaxTree.ParseText(code);
// Get references
var references = GetDefaultReferences();
// Create compilation
var compilation = CSharpCompilation.Create(
assemblyName,
new[] { syntaxTree },
references,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
.WithOptimizationLevel(OptimizationLevel.Debug)
.WithPlatform(Platform.AnyCpu)
);
// Emit to file
EmitResult emitResult;
using (var stream = new FileStream(dllPath, FileMode.Create))
{
emitResult = compilation.Emit(stream);
}
// Check for compilation errors
if (!emitResult.Success)
{
var errors = emitResult.Diagnostics
.Where(d => d.Severity == DiagnosticSeverity.Error)
.Select(d => new
{
line = d.Location.GetLineSpan().StartLinePosition.Line + 1,
column = d.Location.GetLineSpan().StartLinePosition.Character + 1,
message = d.GetMessage(),
id = d.Id
})
.ToList();
return new ErrorResponse("Compilation failed", new
{
errors = errors,
error_count = errors.Count
});
}
// Load assembly if requested
Assembly loadedAssembly = null;
List<string> typeNames = new List<string>();
if (loadImmediately)
{
loadedAssembly = Assembly.LoadFrom(dllPath);
typeNames = loadedAssembly.GetTypes().Select(t => t.FullName).ToList();
// Store info
LoadedAssemblies[assemblyName] = new LoadedAssemblyInfo
{
Name = assemblyName,
Assembly = loadedAssembly,
DllPath = dllPath,
LoadedAt = DateTime.Now,
TypeNames = typeNames
};
Debug.Log($"[MCP] Runtime compilation successful: {assemblyName} ({typeNames.Count} types)");
}
// Optionally attach to GameObject
GameObject attachedTo = null;
Type attachedType = null;
if (!string.IsNullOrEmpty(attachTo) && loadedAssembly != null)
{
var go = GameObject.Find(attachTo);
if (go == null)
{
// Try hierarchical path search
go = FindGameObjectByPath(attachTo);
}
if (go != null)
{
// Find first MonoBehaviour type
var behaviourType = loadedAssembly.GetTypes()
.FirstOrDefault(t => t.IsSubclassOf(typeof(MonoBehaviour)) && !t.IsAbstract);
if (behaviourType != null)
{
go.AddComponent(behaviourType);
attachedTo = go;
attachedType = behaviourType;
Debug.Log($"[MCP] Attached {behaviourType.Name} to {go.name}");
}
else
{
Debug.LogWarning($"[MCP] No MonoBehaviour types found in {assemblyName} to attach");
}
}
else
{
Debug.LogWarning($"[MCP] GameObject '{attachTo}' not found");
}
}
return new SuccessResponse("Runtime compilation completed successfully", new
{
assembly_name = assemblyName,
dll_path = dllPath,
loaded = loadImmediately,
type_count = typeNames.Count,
types = typeNames,
attached_to = attachedTo != null ? attachedTo.name : null,
attached_type = attachedType != null ? attachedType.FullName : null
});
}
catch (Exception ex)
{
return new ErrorResponse($"Runtime compilation failed: {ex.Message}", new
{
exception = ex.GetType().Name,
stack_trace = ex.StackTrace
});
}
#endif
}
private static object ListLoadedAssemblies()
{
var assemblies = LoadedAssemblies.Values.Select(info => new
{
name = info.Name,
dll_path = info.DllPath,
loaded_at = info.LoadedAt.ToString("o"),
type_count = info.TypeNames.Count,
types = info.TypeNames
}).ToList();
return new SuccessResponse($"Found {assemblies.Count} loaded dynamic assemblies", new
{
count = assemblies.Count,
assemblies = assemblies
});
}
private static object GetAssemblyTypes(JObject @params)
{
string assemblyName = @params["assembly_name"]?.ToString();
if (string.IsNullOrEmpty(assemblyName))
{
return new ErrorResponse("'assembly_name' parameter is required");
}
if (!LoadedAssemblies.TryGetValue(assemblyName, out var info))
{
return new ErrorResponse($"Assembly '{assemblyName}' not found in loaded assemblies");
}
var types = info.Assembly.GetTypes().Select(t => new
{
full_name = t.FullName,
name = t.Name,
@namespace = t.Namespace,
is_class = t.IsClass,
is_abstract = t.IsAbstract,
is_monobehaviour = t.IsSubclassOf(typeof(MonoBehaviour)),
base_type = t.BaseType?.FullName
}).ToList();
return new SuccessResponse($"Retrieved {types.Count} types from {assemblyName}", new
{
assembly_name = assemblyName,
type_count = types.Count,
types = types
});
}
/// <summary>
/// Execute code using RoslynRuntimeCompiler with full GUI tool integration
/// Supports MonoBehaviours, static methods, and coroutines
/// </summary>
private static object ExecuteWithRoslyn(JObject @params)
{
try
{
string code = @params["code"]?.ToString();
string className = @params["class_name"]?.ToString() ?? "AIGenerated";
string methodName = @params["method_name"]?.ToString() ?? "Run";
string targetObjectName = @params["target_object"]?.ToString();
bool attachAsComponent = @params["attach_as_component"]?.ToObject<bool>() ?? false;
if (string.IsNullOrEmpty(code))
{
return new ErrorResponse("'code' parameter is required");
}
// Get or create the RoslynRuntimeCompiler instance
var compiler = GetOrCreateRoslynCompiler();
// Find target GameObject if specified
GameObject targetObject = null;
if (!string.IsNullOrEmpty(targetObjectName))
{
targetObject = GameObject.Find(targetObjectName);
if (targetObject == null)
{
targetObject = FindGameObjectByPath(targetObjectName);
}
if (targetObject == null)
{
return new ErrorResponse($"Target GameObject '{targetObjectName}' not found");
}
}
// Use the RoslynRuntimeCompiler's CompileAndExecute method
bool success = compiler.CompileAndExecute(
code,
className,
methodName,
targetObject,
attachAsComponent,
out string errorMessage
);
if (success)
{
return new SuccessResponse($"Code compiled and executed successfully", new
{
class_name = className,
method_name = methodName,
target_object = targetObject != null ? targetObject.name : "compiler_host",
attached_as_component = attachAsComponent,
diagnostics = compiler.lastCompileDiagnostics
});
}
else
{
return new ErrorResponse($"Execution failed: {errorMessage}", new
{
diagnostics = compiler.lastCompileDiagnostics
});
}
}
catch (Exception ex)
{
return new ErrorResponse($"Failed to execute with Roslyn: {ex.Message}", new
{
exception = ex.GetType().Name,
stack_trace = ex.StackTrace
});
}
}
/// <summary>
/// Get compilation history from RoslynRuntimeCompiler
/// </summary>
private static object GetCompilationHistory()
{
try
{
var compiler = GetOrCreateRoslynCompiler();
var history = compiler.CompilationHistory;
var historyData = history.Select(entry => new
{
timestamp = entry.timestamp,
type_name = entry.typeName,
method_name = entry.methodName,
success = entry.success,
diagnostics = entry.diagnostics,
execution_target = entry.executionTarget,
source_code_preview = entry.sourceCode.Length > 200
? entry.sourceCode.Substring(0, 200) + "..."
: entry.sourceCode
}).ToList();
return new SuccessResponse($"Retrieved {historyData.Count} history entries", new
{
count = historyData.Count,
history = historyData
});
}
catch (Exception ex)
{
return new ErrorResponse($"Failed to get history: {ex.Message}");
}
}
/// <summary>
/// Save compilation history to JSON file
/// </summary>
private static object SaveCompilationHistory()
{
try
{
var compiler = GetOrCreateRoslynCompiler();
if (compiler.SaveHistoryToFile(out string savedPath, out string error))
{
return new SuccessResponse($"History saved successfully", new
{
path = savedPath,
entry_count = compiler.CompilationHistory.Count
});
}
else
{
return new ErrorResponse($"Failed to save history: {error}");
}
}
catch (Exception ex)
{
return new ErrorResponse($"Failed to save history: {ex.Message}");
}
}
/// <summary>
/// Clear compilation history
/// </summary>
private static object ClearCompilationHistory()
{
try
{
var compiler = GetOrCreateRoslynCompiler();
int count = compiler.CompilationHistory.Count;
compiler.ClearHistory();
return new SuccessResponse($"Cleared {count} history entries");
}
catch (Exception ex)
{
return new ErrorResponse($"Failed to clear history: {ex.Message}");
}
}
#if USE_ROSLYN
private static List<MetadataReference> GetDefaultReferences()
{
var references = new List<MetadataReference>();
// Add core .NET references
references.Add(MetadataReference.CreateFromFile(typeof(object).Assembly.Location));
references.Add(MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location));
// Add Unity references
var unityEngine = typeof(UnityEngine.Object).Assembly.Location;
references.Add(MetadataReference.CreateFromFile(unityEngine));
// Add UnityEditor if available
try
{
var unityEditor = typeof(UnityEditor.Editor).Assembly.Location;
references.Add(MetadataReference.CreateFromFile(unityEditor));
}
catch { /* Editor assembly not always needed */ }
// Add Assembly-CSharp (user scripts)
try
{
var assemblyCSharp = AppDomain.CurrentDomain.GetAssemblies()
.FirstOrDefault(a => a.GetName().Name == "Assembly-CSharp");
if (assemblyCSharp != null)
{
references.Add(MetadataReference.CreateFromFile(assemblyCSharp.Location));
}
}
catch { /* User assembly not always needed */ }
return references;
}
#endif
private static GameObject FindGameObjectByPath(string path)
{
// Handle hierarchical paths like "Canvas/Panel/Button"
var parts = path.Split('/');
GameObject current = null;
foreach (var part in parts)
{
if (current == null)
{
// Find root object
current = GameObject.Find(part);
}
else
{
// Find child
var transform = current.transform.Find(part);
if (transform == null)
return null;
current = transform.gameObject;
}
}
return current;
}
/// <summary>
/// Get or create a RoslynRuntimeCompiler instance for GUI integration
/// This allows MCP commands to leverage the existing GUI tool
/// </summary>
private static RoslynRuntimeCompiler GetOrCreateRoslynCompiler()
{
var existing = UnityEngine.Object.FindFirstObjectByType<RoslynRuntimeCompiler>();
if (existing != null)
{
return existing;
}
var go = new GameObject("MCPRoslynCompiler");
var compiler = go.AddComponent<RoslynRuntimeCompiler>();
compiler.enableHistory = true; // Enable history tracking for MCP operations
if (!Application.isPlaying)
{
go.hideFlags = HideFlags.HideAndDontSave;
}
return compiler;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 1c3b2419382faa04481f4a631c510ee6
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 97f1198c66ce56043a3c8a5e05ba0150
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 CoplayDev
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 31e7fac5858840340a75cc6df0ad3d9e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+3
View File
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("MCPForUnityTests.EditMode")]
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: be61633e00d934610ac1ff8192ffbe3d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c9d47f01d06964ee7843765d1bd71205
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 59ff83375c2c74c8385c4a22549778dd
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Models;
using UnityEditor;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class AntigravityConfigurator : JsonFileMcpConfigurator
{
// Antigravity 2.x migrated its MCP config from ~/.gemini/antigravity/mcp_config.json
// (where Antigravity also stores its own runtime state — conversations, scratch, etc.)
// to a dedicated ~/.gemini/config/mcp_config.json. The migration drops a `.migrated`
// marker in the new location and renames the previous folder to `antigravity-backup`.
// The old path is no longer read by Antigravity at all, so writing there silently
// fails to register UnityMCP on every modern install.
public AntigravityConfigurator() : base(new McpClient
{
name = "Antigravity 2.0",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "config", "mcp_config.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "config", "mcp_config.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "config", "mcp_config.json"),
HttpUrlProperty = "serverUrl",
DefaultUnityFields = { { "disabled", false } },
StripEnvWhenNotRequired = true
})
{ }
// Detect Antigravity itself, not just its config dir. ~/.gemini/config/ is created on
// first launch of Antigravity 2.x, so the inherited ParentDirectoryExists check
// false-negatives on a fresh install where the user hasn't opened Antigravity yet.
// ~/.antigravity/ is created by the installer (VS-Code-style support dir) and the
// macOS app bundle is dropped by the installer; either is conclusive evidence that
// Antigravity is installed, regardless of whether it has been launched.
public override bool IsInstalled
{
get
{
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
if (Directory.Exists(Path.Combine(home, ".antigravity"))) return true;
if (Directory.Exists(Path.Combine(home, ".gemini", "config"))) return true;
if (Directory.Exists(Path.Combine(home, ".gemini", "antigravity"))) return true;
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
return Directory.Exists("/Applications/Antigravity.app");
return false;
}
}
public override IList<string> GetInstallationSteps() => new List<string>
{
"Open Antigravity 2.0",
"Click the more_horiz menu in the Agent pane > MCP Servers",
"Select 'Install' for Unity MCP or use the Configure button above",
"Restart Antigravity if necessary"
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 331b33961513042e3945d0a1d06615b5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,52 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Models;
using UnityEditor;
namespace MCPForUnity.Editor.Clients.Configurators
{
/// <summary>
/// Antigravity IDE — the separate IDE build that ships its own ~/.gemini/antigravity-ide/
/// runtime dir and reads its MCP config from that same folder. It did NOT migrate to
/// ~/.gemini/config/ the way Antigravity 2.0 did, so it still uses the legacy in-folder
/// mcp_config.json layout. The two apps coexist on the same machine, so we expose them
/// as separate clients rather than trying to autodetect which one to write to.
/// </summary>
public class AntigravityIdeConfigurator : JsonFileMcpConfigurator
{
public AntigravityIdeConfigurator() : base(new McpClient
{
name = "Antigravity IDE",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "antigravity-ide", "mcp_config.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "antigravity-ide", "mcp_config.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "antigravity-ide", "mcp_config.json"),
HttpUrlProperty = "serverUrl",
DefaultUnityFields = { { "disabled", false } },
StripEnvWhenNotRequired = true
})
{ }
// ~/.gemini/antigravity-ide/ is created by the IDE on first launch and holds both
// its runtime state (annotations/, brain/, conversations/, ...) and its mcp_config.json
// — presence of the dir is the canonical "Antigravity IDE has been installed and
// launched at least once" signal.
public override bool IsInstalled
{
get
{
string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
return Directory.Exists(Path.Combine(home, ".gemini", "antigravity-ide"));
}
}
public override IList<string> GetInstallationSteps() => new List<string>
{
"Open Antigravity IDE",
"Click the more_horiz menu in the Agent pane > MCP Servers",
"Select 'Install' for Unity MCP or use the Configure button above",
"Restart Antigravity IDE if necessary"
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c74a24c69bb0481682d5e35731cba6b1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Models;
using MCPForUnity.Editor.Services;
using UnityEditor;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class CherryStudioConfigurator : JsonFileMcpConfigurator
{
public const string ClientName = "Cherry Studio";
public CherryStudioConfigurator() : base(new McpClient
{
name = ClientName,
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Cherry Studio", "config"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Cherry Studio", "config"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Cherry Studio", "config"),
SupportsHttpTransport = false
})
{ }
public override bool SupportsAutoConfigure => false;
public override IList<string> GetInstallationSteps() => new List<string>
{
"Open Cherry Studio",
"Go to Settings (⚙️) → MCP Server",
"Click 'Add Server' button",
"For STDIO mode (recommended):",
" - Name: unity-mcp",
" - Type: STDIO",
" - Command: uvx",
" - Arguments: Copy from the Manual Configuration JSON below",
"Click Save and restart Cherry Studio",
"",
"Note: Cherry Studio uses UI-based configuration.",
"Use the manual snippet below as reference for the values to enter."
};
public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
{
client.SetStatus(McpStatus.NotConfigured, "Cherry Studio requires manual UI configuration");
return client.status;
}
public override void Configure()
{
throw new InvalidOperationException(
"Cherry Studio uses UI-based configuration. " +
"Please use the Manual Configuration snippet and Installation Steps to configure manually."
);
}
public override string GetManualSnippet()
{
bool useHttp = EditorConfigurationCache.Instance.UseHttpTransport;
if (useHttp)
{
return "# Cherry Studio does not support WebSocket transport.\n" +
"# Cherry Studio supports STDIO and SSE transports.\n" +
"# \n" +
"# To use Cherry Studio:\n" +
"# 1. Switch transport to 'Stdio' in Advanced Settings below\n" +
"# 2. Return to this configuration screen\n" +
"# 3. Copy the STDIO configuration snippet that will appear\n" +
"# \n" +
"# OPTION 2: SSE mode (future support)\n" +
"# Note: Unity MCP does not currently have an SSE endpoint.\n" +
"# This may be added in a future update.";
}
return base.GetManualSnippet() + "\n\n" +
"# Cherry Studio Configuration Instructions:\n" +
"# Cherry Studio uses UI-based configuration, not a JSON file.\n" +
"# \n" +
"# To configure:\n" +
"# 1. Open Cherry Studio\n" +
"# 2. Go to Settings (⚙️) → MCP Server\n" +
"# 3. Click 'Add Server'\n" +
"# 4. Enter the following values from the JSON above:\n" +
"# - Name: unity-mcp\n" +
"# - Type: STDIO\n" +
"# - Command: (copy 'command' value from JSON)\n" +
"# - Arguments: (copy 'args' array values, space-separated or as individual entries)\n" +
"# - Active: true\n" +
"# 5. Click Save\n" +
"# 6. Restart Cherry Studio";
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6de06c6bb0399154d840a1e4c84be869
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
/// <summary>
/// Claude Code configurator using the CLI-based registration (claude mcp add/remove).
/// This integrates with Claude Code's native MCP management.
/// </summary>
public class ClaudeCodeConfigurator : ClaudeCliMcpConfigurator
{
public ClaudeCodeConfigurator() : base(new McpClient
{
name = "Claude Code",
SupportsHttpTransport = true,
})
{ }
public override bool SupportsSkills => true;
public override string GetSkillInstallPath()
{
var userHome = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
return Path.Combine(userHome, ".claude", "skills", "unity-mcp-skill");
}
public override IList<string> GetInstallationSteps() => new List<string>
{
"Ensure Claude CLI is installed (comes with Claude Code)",
"Click Configure to add UnityMCP via 'claude mcp add'",
"The server will be automatically available in Claude Code",
"Use Unregister to remove via 'claude mcp remove'"
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d0d22681fc594475db1c189f2d9abdf7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
using UnityEditor;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class ClaudeDesktopConfigurator : JsonFileMcpConfigurator
{
public const string ClientName = "Claude Desktop";
public ClaudeDesktopConfigurator() : base(new McpClient
{
name = ClientName,
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Claude", "claude_desktop_config.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Claude", "claude_desktop_config.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Claude", "claude_desktop_config.json"),
SupportsHttpTransport = false,
StripEnvWhenNotRequired = true
})
{ }
public override bool SupportsSkills => true;
public override string GetSkillInstallPath()
{
var userHome = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
return Path.Combine(userHome, ".claude", "skills", "unity-mcp-skill");
}
public override IList<string> GetInstallationSteps() => new List<string>
{
"Open Claude Desktop",
"Go to Settings > Developer > Edit Config\nOR open the config path",
"Paste the configuration JSON",
"Save and restart Claude Desktop"
};
private static readonly ConfiguredTransport[] StdioOnly = { ConfiguredTransport.Stdio };
public override IReadOnlyList<ConfiguredTransport> SupportedTransports => StdioOnly;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d5e5d87c9db57495f842dc366f1ebd65
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class ClineConfigurator : JsonFileMcpConfigurator
{
public ClineConfigurator() : base(new McpClient
{
name = "Cline",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", "cline_mcp_settings.json"),
HttpTypeValue = "streamableHttp",
DefaultUnityFields = { { "disabled", false }, { "autoApprove", new object[] { } } }
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Open Cline in VS Code",
"Click the MCP Servers icon in the Cline pane",
"Go to Configure tab and click 'Configure MCP Servers'\nOR open the config file at the path above",
"Paste the configuration JSON into the mcpServers object",
"Save and restart VS Code"
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6b8abf0951c7413d9ff97a053b0adf2d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
/// <summary>
/// Configures the CodeBuddy CLI (~/.codebuddy.json) MCP settings.
/// </summary>
public class CodeBuddyCliConfigurator : JsonFileMcpConfigurator
{
public CodeBuddyCliConfigurator() : base(new McpClient
{
name = "CodeBuddy CLI",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codebuddy.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codebuddy.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codebuddy.json"),
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Install CodeBuddy CLI and ensure '~/.codebuddy.json' exists",
"Click Configure to add the UnityMCP entry (or manually edit the file above)",
"Restart your CLI session if needed"
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 923728a98c8c74cfaa6e9203c408f34e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class CodexConfigurator : CodexMcpConfigurator
{
public CodexConfigurator() : base(new McpClient
{
name = "Codex",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codex", "config.toml"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codex", "config.toml"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codex", "config.toml")
})
{ }
public override bool SupportsSkills => true;
public override string GetSkillInstallPath()
{
var userHome = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
return Path.Combine(userHome, ".codex", "skills", "unity-mcp-skill");
}
public override IList<string> GetInstallationSteps() => new List<string>
{
"Run 'codex config edit' in a terminal\nOR open the config file at the path above",
"Paste the configuration TOML",
"Save and restart Codex"
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c7037ef8b168e49f79247cb31c3be75a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class CopilotCliConfigurator : JsonFileMcpConfigurator
{
public CopilotCliConfigurator() : base(new McpClient
{
name = "GitHub Copilot CLI",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".copilot", "mcp-config.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".copilot", "mcp-config.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".copilot", "mcp-config.json")
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Install GitHub Copilot CLI (https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli)",
"Open or create mcp-config.json at the path above",
"Paste the configuration JSON (or use /mcp add in the CLI)",
"Restart your Copilot CLI session"
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 14a4b9a7f749248d496466c2a3a53e56
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class CursorConfigurator : JsonFileMcpConfigurator
{
public CursorConfigurator() : base(new McpClient
{
name = "Cursor",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cursor", "mcp.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cursor", "mcp.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".cursor", "mcp.json")
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Open Cursor",
"Go to File > Preferences > Cursor Settings > MCP > Add new global MCP server\nOR open the config file at the path above",
"Paste the configuration JSON",
"Save and restart Cursor"
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b708eda314746481fb8f4a1fb0652b03
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Models;
using UnityEditor;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class GeminiCliConfigurator : JsonFileMcpConfigurator
{
public GeminiCliConfigurator() : base(new McpClient
{
name = "Gemini CLI",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "settings.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "settings.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".gemini", "settings.json"),
HttpUrlProperty = "httpUrl",
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Ensure Gemini CLI is installed (see https://geminicli.com/docs/get-started/installation/)",
"Click Register to add UnityMCP via 'gemini mcp add'",
"The server will be automatically available in Gemini CLI",
"Use Unregister to remove via 'gemini mcp remove'"
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c5e9bbb45e552453ab5cb557a22d43e7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class KiloCodeConfigurator : JsonFileMcpConfigurator
{
public KiloCodeConfigurator() : base(new McpClient
{
name = "Kilo Code",
// Kilo Code v7.0.33+ moved MCP config out of the VS Code extension's
// globalStorage/mcp_settings.json to a CLI-style kilo.jsonc under ~/.config/kilo.
// The new schema (https://app.kilo.ai/config.json) uses an "mcp" container,
// type:"remote" for HTTP servers, type:"local" for stdio, and an "enabled" flag.
// ~/.config/kilo/kilo.jsonc on every OS (UserProfile resolves to C:\Users\<user> on Windows).
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "kilo", "kilo.jsonc"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "kilo", "kilo.jsonc"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "kilo", "kilo.jsonc"),
IsVsCodeLayout = false,
ServerContainerKey = "mcp",
HttpTypeValue = "remote",
StdioTypeValue = "local",
SchemaUrl = "https://app.kilo.ai/config.json",
DefaultUnityFields = { { "enabled", true } }
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Install or update Kilo Code (v7.0.33 or newer)",
"Open the Kilo Code MCP Servers view\nOR edit the config file at the path above (~/.config/kilo/kilo.jsonc)",
"Paste the configuration JSON into the \"mcp\" object",
"Save and restart Kilo Code"
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3286d62ffe5644f5ea60488fd7e6513d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
/// <summary>
/// Kimi Code CLI MCP client configurator.
/// Kimi Code uses a JSON-based configuration file with mcpServers section.
/// Config path: ~/.kimi/mcp.json
///
/// Kimi Code supports both stdio (uvx) and HTTP transport modes.
/// Default: stdio mode (works without Unity Editor for basic operations)
/// HTTP mode: requires Unity Editor running with MCP HTTP server started
/// </summary>
public class KimiCodeConfigurator : JsonFileMcpConfigurator
{
public KimiCodeConfigurator() : base(new McpClient
{
name = "Kimi Code",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".kimi", "mcp.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".kimi", "mcp.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".kimi", "mcp.json"),
SupportsHttpTransport = true,
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Ensure Kimi Code CLI is installed (pip install kimi-cli or see https://github.com/MoonshotAI/kimi-cli)",
"Click 'Auto Configure' to automatically add UnityMCP to ~/.kimi/mcp.json",
"OR click 'Manual Setup' to copy the configuration JSON",
"Open ~/.kimi/mcp.json and paste the configuration",
"Save and restart Kimi Code CLI",
"Use 'kimi mcp list' to verify Unity MCP is connected",
"Note: For full functionality, open Unity Editor and start HTTP server"
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 562758226f6844438b5b1ae03a2cc7b7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class KiroConfigurator : JsonFileMcpConfigurator
{
public KiroConfigurator() : base(new McpClient
{
name = "Kiro",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".kiro", "settings", "mcp.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".kiro", "settings", "mcp.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".kiro", "settings", "mcp.json"),
EnsureEnvObject = true,
DefaultUnityFields = { { "disabled", false } }
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Open Kiro",
"Go to File > Settings > Settings > Search for \"MCP\" > Open Workspace MCP Config\nOR open the config file at the path above",
"Paste the configuration JSON",
"Save and restart Kiro"
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e9b73ff071a6043dda1f2ec7d682ef71
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,398 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Constants;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Models;
using MCPForUnity.Editor.Services;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using UnityEditor;
namespace MCPForUnity.Editor.Clients.Configurators
{
/// <summary>
/// Configurator for OpenClaw via the openclaw-mcp-bridge plugin.
/// OpenClaw stores config at ~/.openclaw/openclaw.json.
/// </summary>
public class OpenClawConfigurator : McpClientConfiguratorBase
{
private const string PluginName = "openclaw-mcp-bridge";
private const string ServerName = "unityMCP";
private const string HttpTransportName = "http";
private const string StdioTransportName = "stdio";
private const string StdioUrl = "stdio://local";
public OpenClawConfigurator() : base(new McpClient
{
name = "OpenClaw",
windowsConfigPath = BuildConfigPath(),
macConfigPath = BuildConfigPath(),
linuxConfigPath = BuildConfigPath()
})
{ }
private static string BuildConfigPath()
{
return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".openclaw",
"openclaw.json");
}
public override string GetConfigPath() => CurrentOsPath();
public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
{
try
{
string path = GetConfigPath();
if (!File.Exists(path))
{
client.SetStatus(McpStatus.NotConfigured);
client.configuredTransport = ConfiguredTransport.Unknown;
return client.status;
}
JObject root = LoadConfig(path);
JObject pluginEntry = root["plugins"]?["entries"]?[PluginName] as JObject;
JObject unityServer = FindUnityServer(pluginEntry?["config"]?["servers"]);
if (pluginEntry == null || unityServer == null)
{
client.SetStatus(McpStatus.MissingConfig);
client.configuredTransport = ConfiguredTransport.Unknown;
return client.status;
}
if (!IsEnabled(pluginEntry) || !IsEnabled(unityServer))
{
client.SetStatus(McpStatus.NotConfigured);
client.configuredTransport = ConfiguredTransport.Unknown;
return client.status;
}
bool matches = ServerMatchesCurrentEndpoint(unityServer);
if (matches)
{
client.SetStatus(McpStatus.Configured);
client.configuredTransport = ResolveTransport(unityServer);
return client.status;
}
if (attemptAutoRewrite)
{
Configure();
}
else
{
client.SetStatus(McpStatus.IncorrectPath);
client.configuredTransport = ConfiguredTransport.Unknown;
}
}
catch (Exception ex)
{
client.SetStatus(McpStatus.Error, ex.Message);
client.configuredTransport = ConfiguredTransport.Unknown;
}
return client.status;
}
public override void Configure()
{
if (EditorPrefs.GetBool(EditorPrefKeys.LockCursorConfig, false))
return;
string path = GetConfigPath();
McpConfigurationHelper.EnsureConfigDirectoryExists(path);
JObject root = File.Exists(path) ? LoadConfig(path) : new JObject();
JObject plugins = root["plugins"] as JObject ?? new JObject();
root["plugins"] = plugins;
JObject entries = plugins["entries"] as JObject ?? new JObject();
plugins["entries"] = entries;
JObject pluginEntry = entries[PluginName] as JObject ?? new JObject();
entries[PluginName] = pluginEntry;
pluginEntry["enabled"] = true;
JObject pluginConfig = pluginEntry["config"] as JObject ?? new JObject();
pluginEntry["config"] = pluginConfig;
pluginConfig.Remove("timeout"); // removed in openclaw-mcp-bridge v2+
pluginConfig.Remove("retries"); // removed in openclaw-mcp-bridge v2+
pluginConfig["servers"] = UpsertUnityServer(pluginConfig["servers"]);
McpConfigurationHelper.WriteAtomicFile(path, root.ToString(Formatting.Indented));
client.SetStatus(McpStatus.Configured);
client.configuredTransport = HttpEndpointUtility.GetCurrentServerTransport();
}
public override string GetManualSnippet()
{
JObject snippet = new JObject
{
["plugins"] = new JObject
{
["entries"] = new JObject
{
[PluginName] = new JObject
{
["enabled"] = true,
["config"] = new JObject
{
["servers"] = new JObject
{
[ServerName] = BuildUnityServerEntry()
}
}
}
}
}
};
return snippet.ToString(Formatting.Indented);
}
public override IList<string> GetInstallationSteps() => new List<string>
{
"Install OpenClaw",
"Install the bridge plugin: npm install -g openclaw-mcp-bridge (or pnpm add -g openclaw-mcp-bridge)",
"In MCP for Unity, choose OpenClaw and click Configure",
"OpenClaw uses the currently selected MCP for Unity transport (HTTP or stdio)",
"OpenClaw exposes a proxy tool such as unityMCP__call for Unity MCP access",
"Restart OpenClaw if the plugin does not hot-reload the new config"
};
private JObject LoadConfig(string path)
{
string text = File.ReadAllText(path);
if (string.IsNullOrWhiteSpace(text))
{
return new JObject();
}
try
{
return JsonConvert.DeserializeObject<JObject>(text) ?? new JObject();
}
catch (JsonException ex)
{
throw new InvalidOperationException(
$"OpenClaw config contains non-JSON content and cannot be safely auto-edited: {ex.Message}");
}
}
private JObject FindUnityServer(JToken serversToken)
{
if (serversToken is JObject serverMap)
{
return serverMap[ServerName] as JObject;
}
if (serversToken is JArray legacyServers)
{
foreach (JToken token in legacyServers)
{
JObject server = token as JObject;
if (server == null)
{
continue;
}
string name = server["name"]?.ToString();
if (string.Equals(name, ServerName, StringComparison.OrdinalIgnoreCase))
{
return server;
}
}
}
return null;
}
private JObject UpsertUnityServer(JToken serversToken)
{
JObject servers = NormalizeServers(serversToken);
JObject entry = servers[ServerName] as JObject ?? new JObject();
JObject desiredEntry = BuildUnityServerEntry();
entry.Remove("name");
entry.Remove("prefix");
entry.Remove("healthCheck");
entry.Remove("command");
entry.Remove("args");
entry.Remove("env");
entry.Remove("connectTimeoutMs");
foreach (var property in desiredEntry.Properties())
{
entry[property.Name] = property.Value.DeepClone();
}
servers[ServerName] = entry;
return servers;
}
private static JObject NormalizeServers(JToken serversToken)
{
if (serversToken is JObject serverMap)
{
return serverMap;
}
var normalized = new JObject();
if (!(serversToken is JArray legacyServers))
{
return normalized;
}
foreach (JToken token in legacyServers)
{
if (!(token is JObject legacyServer))
{
continue;
}
string name = legacyServer["name"]?.ToString();
if (string.IsNullOrWhiteSpace(name))
{
continue;
}
normalized[name] = legacyServer;
}
return normalized;
}
private static JObject BuildUnityServerEntry()
{
ConfiguredTransport transport = HttpEndpointUtility.GetCurrentServerTransport();
if (transport == ConfiguredTransport.Stdio)
{
var (uvxPath, _, packageName) = AssetPathUtility.GetUvxCommandParts();
if (string.IsNullOrWhiteSpace(uvxPath))
{
throw new InvalidOperationException("uvx not found. Install uv/uvx or set the override in Advanced Settings.");
}
var args = new JArray();
foreach (string value in AssetPathUtility.GetUvxDevFlagsList())
{
args.Add(value);
}
foreach (string value in AssetPathUtility.GetBetaServerFromArgsList())
{
args.Add(value);
}
args.Add(packageName);
args.Add("--transport");
args.Add("stdio");
return new JObject
{
["enabled"] = true,
["url"] = StdioUrl,
["transport"] = StdioTransportName,
["command"] = uvxPath,
["args"] = args,
["toolPrefix"] = ServerName,
["requestTimeoutMs"] = 60000,
["connectTimeoutMs"] = 15000
};
}
return new JObject
{
["enabled"] = true,
["url"] = HttpEndpointUtility.GetMcpRpcUrl(),
["transport"] = HttpTransportName,
["toolPrefix"] = ServerName,
["requestTimeoutMs"] = 30000
};
}
private bool ServerMatchesCurrentEndpoint(JObject server)
{
if (server == null)
{
return false;
}
ConfiguredTransport expectedTransport = HttpEndpointUtility.GetCurrentServerTransport();
ConfiguredTransport configuredTransport = ResolveTransport(server);
if (configuredTransport != expectedTransport)
{
return false;
}
if (configuredTransport == ConfiguredTransport.Stdio)
{
string configuredUrl = server["url"]?.ToString();
string command = server["command"]?.ToString();
if (!UrlsEqual(configuredUrl, StdioUrl) || string.IsNullOrWhiteSpace(command))
{
return false;
}
// Validate the --from package source hasn't drifted (e.g. stable vs prerelease switch)
string[] args = (server["args"] as JArray)?.ToObject<string[]>();
string configuredSource = McpConfigurationHelper.ExtractUvxUrl(args);
string expectedSource = GetExpectedPackageSourceForValidation();
if (!string.IsNullOrEmpty(configuredSource) && !string.IsNullOrEmpty(expectedSource) &&
!McpConfigurationHelper.PathsEqual(configuredSource, expectedSource))
{
return false;
}
}
else
{
string configuredUrl = server["url"]?.ToString();
if (string.IsNullOrWhiteSpace(configuredUrl) ||
(!UrlsEqual(configuredUrl, HttpEndpointUtility.GetLocalMcpRpcUrl()) &&
!UrlsEqual(configuredUrl, HttpEndpointUtility.GetRemoteMcpRpcUrl())))
{
return false;
}
}
string toolPrefix = server["toolPrefix"]?.ToString();
return string.IsNullOrWhiteSpace(toolPrefix) ||
string.Equals(toolPrefix, ServerName, StringComparison.OrdinalIgnoreCase);
}
private static bool IsEnabled(JObject entry)
{
JToken enabledToken = entry["enabled"];
return enabledToken == null || enabledToken.Type != JTokenType.Boolean || enabledToken.Value<bool>();
}
private ConfiguredTransport ResolveTransport(JObject server)
{
string configuredTransport = server?["transport"]?.ToString();
string configuredUrl = server?["url"]?.ToString();
if (string.Equals(configuredTransport, StdioTransportName, StringComparison.OrdinalIgnoreCase) ||
UrlsEqual(configuredUrl, StdioUrl))
{
return ConfiguredTransport.Stdio;
}
if (UrlsEqual(configuredUrl, HttpEndpointUtility.GetRemoteMcpRpcUrl()))
{
return ConfiguredTransport.HttpRemote;
}
if (UrlsEqual(configuredUrl, HttpEndpointUtility.GetLocalMcpRpcUrl()))
{
return ConfiguredTransport.Http;
}
return ConfiguredTransport.Unknown;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 19f29c88761345158fc766d24e7c18f3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,224 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Helpers;
using MCPForUnity.Editor.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace MCPForUnity.Editor.Clients.Configurators
{
/// <summary>
/// Configurator for OpenCode (opencode.ai) - a Go-based terminal AI coding assistant.
/// OpenCode uses ~/.config/opencode/opencode.json with a custom "mcp" format.
/// </summary>
public class OpenCodeConfigurator : McpClientConfiguratorBase
{
private const string ServerName = "unityMCP";
private const string SchemaUrl = "https://opencode.ai/config.json";
private const string RemoteType = "remote";
private const string LocalType = "local";
public OpenCodeConfigurator() : base(new McpClient
{
name = "OpenCode",
windowsConfigPath = BuildConfigPath(),
macConfigPath = BuildConfigPath(),
linuxConfigPath = BuildConfigPath()
})
{ }
private static string BuildConfigPath()
{
string xdgConfigHome = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME");
string configBase = !string.IsNullOrEmpty(xdgConfigHome)
? xdgConfigHome
: Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config");
return Path.Combine(configBase, "opencode", "opencode.json");
}
public override string GetConfigPath() => CurrentOsPath();
/// <summary>
/// Attempts to load and parse the config file.
/// Returns null if file doesn't exist or cannot be read.
/// Returns parsed JObject if valid JSON found.
/// Logs warning if file exists but contains malformed JSON.
/// </summary>
private JObject TryLoadConfig(string path)
{
if (!File.Exists(path))
return null;
string content;
try
{
content = File.ReadAllText(path);
}
catch (Exception ex)
{
UnityEngine.Debug.LogWarning($"[OpenCodeConfigurator] Failed to read config file {path}: {ex.Message}");
return null;
}
try
{
return JsonConvert.DeserializeObject<JObject>(content) ?? new JObject();
}
catch (JsonException ex)
{
// Malformed JSON - log warning and return null.
// When Configure() receives null, it will do: TryLoadConfig(path) ?? new JObject()
// This creates a fresh empty JObject, which replaces the entire file with only the unityMCP section.
// Existing config sections are lost. To preserve sections, a different recovery strategy
// (e.g., line-by-line parsing, JSON repair, or manual user intervention) would be needed.
UnityEngine.Debug.LogWarning($"[OpenCodeConfigurator] Malformed JSON in {path}: {ex.Message}");
return null;
}
}
public override McpStatus CheckStatus(bool attemptAutoRewrite = true)
{
try
{
string path = GetConfigPath();
var config = TryLoadConfig(path);
if (config == null)
{
client.SetStatus(McpStatus.NotConfigured);
return client.status;
}
var unityMcp = config["mcp"]?[ServerName] as JObject;
if (unityMcp == null)
{
client.SetStatus(McpStatus.NotConfigured);
return client.status;
}
if (EntryMatchesCurrentTransport(unityMcp))
{
client.SetStatus(McpStatus.Configured);
}
else if (attemptAutoRewrite)
{
Configure();
}
else
{
client.SetStatus(McpStatus.IncorrectPath);
}
}
catch (Exception ex)
{
client.SetStatus(McpStatus.Error, ex.Message);
}
return client.status;
}
public override void Configure()
{
try
{
string path = GetConfigPath();
McpConfigurationHelper.EnsureConfigDirectoryExists(path);
// Load existing config or start fresh, preserving all other properties and MCP servers
var config = TryLoadConfig(path) ?? new JObject();
// Only add $schema if creating a new file
if (!File.Exists(path))
{
config["$schema"] = SchemaUrl;
}
// Preserve existing mcp section and only update our server entry
var mcpSection = config["mcp"] as JObject ?? new JObject();
config["mcp"] = mcpSection;
mcpSection[ServerName] = BuildServerEntry();
McpConfigurationHelper.WriteAtomicFile(path, JsonConvert.SerializeObject(config, Formatting.Indented));
client.SetStatus(McpStatus.Configured);
}
catch (Exception ex)
{
client.SetStatus(McpStatus.Error, ex.Message);
}
}
public override string GetManualSnippet()
{
var snippet = new JObject
{
["mcp"] = new JObject { [ServerName] = BuildServerEntry() }
};
return JsonConvert.SerializeObject(snippet, Formatting.Indented);
}
public override IList<string> GetInstallationSteps() => new List<string>
{
"Install OpenCode (https://opencode.ai)",
"Click Configure to add Unity MCP to ~/.config/opencode/opencode.json",
"Restart OpenCode",
"The Unity MCP server should be detected automatically"
};
private static JObject BuildServerEntry()
{
if (HttpEndpointUtility.GetCurrentServerTransport() == ConfiguredTransport.Stdio)
{
var (uvxPath, _, packageName) = AssetPathUtility.GetUvxCommandParts();
if (string.IsNullOrWhiteSpace(uvxPath))
{
throw new InvalidOperationException("uvx not found. Install uv/uvx or set the override in Advanced Settings.");
}
var command = new JArray { uvxPath };
foreach (string value in AssetPathUtility.GetUvxDevFlagsList())
{
command.Add(value);
}
foreach (string value in AssetPathUtility.GetBetaServerFromArgsList())
{
command.Add(value);
}
command.Add(packageName);
command.Add("--transport");
command.Add("stdio");
return new JObject
{
["type"] = LocalType,
["command"] = command,
["enabled"] = true
};
}
return new JObject
{
["type"] = RemoteType,
["url"] = HttpEndpointUtility.GetMcpRpcUrl(),
["enabled"] = true
};
}
private bool EntryMatchesCurrentTransport(JObject entry)
{
string entryType = entry["type"]?.ToString();
ConfiguredTransport expected = HttpEndpointUtility.GetCurrentServerTransport();
if (expected == ConfiguredTransport.Stdio)
{
return string.Equals(entryType, LocalType, StringComparison.OrdinalIgnoreCase)
&& entry["command"] is JArray;
}
return string.Equals(entryType, RemoteType, StringComparison.OrdinalIgnoreCase)
&& UrlsEqual(entry["url"]?.ToString(), HttpEndpointUtility.GetMcpRpcUrl());
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 489f99ffb7e6743e88e3203552c8b37b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
/// <summary>
/// Qwen Code MCP client configurator.
/// Qwen Code uses a JSON-based configuration file with mcpServers section.
/// Config path: ~/.qwen/settings.json
///
/// Qwen Code supports both stdio (uvx) and HTTP transport modes.
/// Default: stdio mode (works without Unity Editor for basic operations)
/// HTTP mode: requires Unity Editor running with MCP HTTP server started
/// </summary>
public class QwenCodeConfigurator : JsonFileMcpConfigurator
{
public QwenCodeConfigurator() : base(new McpClient
{
name = "Qwen Code",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".qwen", "settings.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".qwen", "settings.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".qwen", "settings.json"),
SupportsHttpTransport = true,
// Default to stdio transport for Qwen Code (like Cursor)
// User can switch to HTTP in Unity: Window > MCP for Unity > Settings
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Ensure Qwen Code is installed (npm install -g @qwen-code/qwen-code or download from https://github.com/QwenLM/qwen-code)",
"Open Qwen Code",
"Click 'Auto Configure' to automatically add UnityMCP to settings.json",
"OR click 'Manual Setup' to copy the configuration JSON",
"Open ~/.qwen/settings.json and paste the configuration",
"Save and restart Qwen Code",
"Use /mcp command in Qwen Code to verify Unity MCP is connected",
"Note: For full functionality, open Unity Editor and start HTTP server"
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 46891bcdb00e468cbd04afbfb8f3095e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class RiderConfigurator : JsonFileMcpConfigurator
{
public RiderConfigurator() : base(new McpClient
{
name = "Rider GitHub Copilot",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "github-copilot", "intellij", "mcp.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "github-copilot", "intellij", "mcp.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "github-copilot", "intellij", "mcp.json"),
IsVsCodeLayout = true
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Install GitHub Copilot plugin in Rider",
"Open or create mcp.json at the path above",
"Paste the configuration JSON",
"Save and restart Rider"
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2511b0d05271d486bb61f8cc9fd11363
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class TraeConfigurator : JsonFileMcpConfigurator
{
public TraeConfigurator() : base(new McpClient
{
name = "Trae",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Trae", "mcp.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Trae", "mcp.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Trae", "mcp.json"),
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Open Trae and go to Settings > MCP",
"Select Add Server > Add Manually",
"Paste the JSON or point to the mcp.json file\n"+
"Windows: %AppData%\\Trae\\mcp.json\n" +
"macOS: ~/Library/Application Support/Trae/mcp.json\n" +
"Linux: ~/.config/Trae/mcp.json\n",
"Save and restart Trae"
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b3ab39e22ae0948ab94beae307f9902e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class VSCodeConfigurator : JsonFileMcpConfigurator
{
public VSCodeConfigurator() : base(new McpClient
{
name = "VSCode GitHub Copilot",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Code", "User", "mcp.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Code", "User", "mcp.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Code", "User", "mcp.json"),
IsVsCodeLayout = true
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Install GitHub Copilot extension",
"Open or create mcp.json at the path above",
"Paste the configuration JSON",
"Save and restart VSCode"
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bcc7ead475a4d4ea2978151c217757b8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class VSCodeInsidersConfigurator : JsonFileMcpConfigurator
{
public VSCodeInsidersConfigurator() : base(new McpClient
{
name = "VSCode Insiders GitHub Copilot",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Code - Insiders", "User", "mcp.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Application Support", "Code - Insiders", "User", "mcp.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".config", "Code - Insiders", "User", "mcp.json"),
IsVsCodeLayout = true
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Install GitHub Copilot extension in VS Code Insiders",
"Open or create mcp.json at the path above",
"Paste the configuration JSON",
"Save and restart VS Code Insiders"
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2c4a1b0d3b34489cbf0f8c40c49c4f3b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.IO;
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients.Configurators
{
public class WindsurfConfigurator : JsonFileMcpConfigurator
{
public WindsurfConfigurator() : base(new McpClient
{
name = "Windsurf",
windowsConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codeium", "windsurf", "mcp_config.json"),
macConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codeium", "windsurf", "mcp_config.json"),
linuxConfigPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".codeium", "windsurf", "mcp_config.json"),
HttpUrlProperty = "serverUrl",
DefaultUnityFields = { { "disabled", false } },
StripEnvWhenNotRequired = true
})
{ }
public override IList<string> GetInstallationSteps() => new List<string>
{
"Open Windsurf",
"Go to File > Preferences > Windsurf Settings > MCP > Manage MCPs > View raw config\nOR open the config file at the path above",
"Paste the configuration JSON",
"Save and restart Windsurf"
};
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b528971e189f141d38db577f155bd222
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,77 @@
using MCPForUnity.Editor.Models;
namespace MCPForUnity.Editor.Clients
{
/// <summary>
/// Contract for MCP client configurators. Each client is responsible for
/// status detection, auto-configure, and manual snippet/steps.
/// </summary>
public interface IMcpClientConfigurator
{
/// <summary>Stable identifier (e.g., "cursor").</summary>
string Id { get; }
/// <summary>Display name shown in the UI.</summary>
string DisplayName { get; }
/// <summary>Current status cached by the configurator.</summary>
McpStatus Status { get; }
/// <summary>
/// The transport type the client is currently configured for.
/// Returns Unknown if the client is not configured or the transport cannot be determined.
/// </summary>
ConfiguredTransport ConfiguredTransport { get; }
/// <summary>True if this client supports auto-configure.</summary>
bool SupportsAutoConfigure { get; }
/// <summary>
/// True if this client appears installed on the user's machine. Used to filter
/// "configure all detected" so we don't write configs for apps the user doesn't have.
/// Implementations should be cheap (filesystem stat or cached path lookup).
/// </summary>
bool IsInstalled { get; }
/// <summary>
/// Transports this client can be configured with. Order is "preference if user has no opinion";
/// the configure path picks the user's global preference if present in this list, else falls back to the first entry.
/// </summary>
System.Collections.Generic.IReadOnlyList<ConfiguredTransport> SupportedTransports { get; }
/// <summary>Label to show on the configure button for the current state.</summary>
string GetConfigureActionLabel();
/// <summary>Returns the platform-specific config path (or message for CLI-managed clients).</summary>
string GetConfigPath();
/// <summary>Checks and updates status; returns current status.</summary>
McpStatus CheckStatus(bool attemptAutoRewrite = true);
/// <summary>Runs auto-configuration (register/write file/CLI etc.). Always idempotent
/// — calling twice with the same settings is safe and is what the bulk "Configure All"
/// path relies on to refresh transport / server-version drift across every detected
/// client.</summary>
void Configure();
/// <summary>
/// Removes UnityMCP from this client's config (JSON entry, CLI registration, etc.).
/// Default is a no-op for client types that don't yet implement removal (Codex TOML);
/// callers should treat this as best-effort. The UI's per-client button routes here
/// when <see cref="Status"/> is <see cref="McpStatus.Configured"/>.
/// </summary>
void Unregister();
/// <summary>Returns the manual configuration snippet (JSON/TOML/commands).</summary>
string GetManualSnippet();
/// <summary>Returns ordered human-readable installation steps.</summary>
System.Collections.Generic.IList<string> GetInstallationSteps();
/// <summary>True if this client supports skill installation/sync.</summary>
bool SupportsSkills { get; }
/// <summary>Returns the absolute path where skills should be installed, or null if unsupported.</summary>
string GetSkillInstallPath();
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f5a5078d9e6e14027a1abfebf4018634
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8d408fd7733cb4a1eb80f785307db2ff
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Linq;
using MCPForUnity.Editor.Helpers;
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Clients
{
/// <summary>
/// Central registry that auto-discovers configurators via TypeCache.
/// </summary>
public static class McpClientRegistry
{
private static List<IMcpClientConfigurator> cached;
public static IReadOnlyList<IMcpClientConfigurator> All
{
get
{
if (cached == null)
{
cached = BuildRegistry();
}
return cached;
}
}
private static List<IMcpClientConfigurator> BuildRegistry()
{
var configurators = new List<IMcpClientConfigurator>();
foreach (var type in TypeCache.GetTypesDerivedFrom<IMcpClientConfigurator>())
{
if (type.IsAbstract || !type.IsClass || !type.IsPublic)
continue;
// Require a public parameterless constructor
if (type.GetConstructor(Type.EmptyTypes) == null)
continue;
try
{
if (Activator.CreateInstance(type) is IMcpClientConfigurator instance)
{
configurators.Add(instance);
}
}
catch (Exception ex)
{
McpLog.Warn($"UnityMCP: Failed to instantiate configurator {type.Name}: {ex.Message}");
}
}
// Alphabetical order by display name
configurators = configurators.OrderBy(c => c.DisplayName, StringComparer.OrdinalIgnoreCase).ToList();
return configurators;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4ce08555f995e4e848a826c63f18cb35
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f7e009cbf3e74f6c987331c2b438ec59
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,10 @@
namespace MCPForUnity.Editor.Constants
{
/// <summary>
/// Protocol-level constants for API key authentication.
/// </summary>
internal static class AuthConstants
{
internal const string ApiKeyHeader = "X-API-Key";
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 96844bc39e9a94cf18b18f8127f3854f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,85 @@
namespace MCPForUnity.Editor.Constants
{
/// <summary>
/// Centralized list of EditorPrefs keys used by the MCP for Unity package.
/// Keeping them in one place avoids typos and simplifies migrations.
/// </summary>
internal static class EditorPrefKeys
{
internal const string UseHttpTransport = "MCPForUnity.UseHttpTransport";
internal const string HttpTransportScope = "MCPForUnity.HttpTransportScope"; // "local" | "remote"
internal const string LastLocalHttpServerPid = "MCPForUnity.LocalHttpServer.LastPid";
internal const string LastLocalHttpServerPort = "MCPForUnity.LocalHttpServer.LastPort";
internal const string LastLocalHttpServerStartedUtc = "MCPForUnity.LocalHttpServer.LastStartedUtc";
internal const string LastLocalHttpServerPidArgsHash = "MCPForUnity.LocalHttpServer.LastPidArgsHash";
internal const string LastLocalHttpServerPidFilePath = "MCPForUnity.LocalHttpServer.LastPidFilePath";
internal const string LastLocalHttpServerInstanceToken = "MCPForUnity.LocalHttpServer.LastInstanceToken";
internal const string DebugLogs = "MCPForUnity.DebugLogs";
internal const string ValidationLevel = "MCPForUnity.ValidationLevel";
internal const string UnitySocketPort = "MCPForUnity.UnitySocketPort";
internal const string ResumeStdioAfterReload = "MCPForUnity.ResumeStdioAfterReload";
internal const string UvxPathOverride = "MCPForUnity.UvxPath";
internal const string ClaudeCliPathOverride = "MCPForUnity.ClaudeCliPath";
internal const string ClientProjectDirOverride = "MCPForUnity.ClientProjectDir";
internal const string HttpBaseUrl = "MCPForUnity.HttpUrl";
internal const string HttpRemoteBaseUrl = "MCPForUnity.HttpRemoteUrl";
internal const string SessionId = "MCPForUnity.SessionId";
internal const string WebSocketUrlOverride = "MCPForUnity.WebSocketUrl";
internal const string GitUrlOverride = "MCPForUnity.GitUrlOverride";
internal const string DevModeForceServerRefresh = "MCPForUnity.DevModeForceServerRefresh";
internal const string ProjectScopedToolsLocalHttp = "MCPForUnity.ProjectScopedTools.LocalHttp";
internal const string AllowLanHttpBind = "MCPForUnity.Security.AllowLanHttpBind";
internal const string AllowInsecureRemoteHttp = "MCPForUnity.Security.AllowInsecureRemoteHttp";
internal const string PackageDeploySourcePath = "MCPForUnity.PackageDeploy.SourcePath";
internal const string PackageDeployLastBackupPath = "MCPForUnity.PackageDeploy.LastBackupPath";
internal const string PackageDeployLastTargetPath = "MCPForUnity.PackageDeploy.LastTargetPath";
internal const string PackageDeployLastSourcePath = "MCPForUnity.PackageDeploy.LastSourcePath";
internal const string ServerSrc = "MCPForUnity.ServerSrc";
internal const string UseEmbeddedServer = "MCPForUnity.UseEmbeddedServer";
internal const string LockCursorConfig = "MCPForUnity.LockCursorConfig";
internal const string AutoRegisterEnabled = "MCPForUnity.AutoRegisterEnabled";
internal const string ToolEnabledPrefix = "MCPForUnity.ToolEnabled.";
internal const string ToolFoldoutStatePrefix = "MCPForUnity.ToolFoldout.";
internal const string ResourceEnabledPrefix = "MCPForUnity.ResourceEnabled.";
internal const string ResourceFoldoutStatePrefix = "MCPForUnity.ResourceFoldout.";
internal const string EditorWindowActivePanel = "MCPForUnity.EditorWindow.ActivePanel";
internal const string LastSelectedClientId = "MCPForUnity.LastSelectedClientId";
internal const string ClientDetailsFoldoutOpen = "MCPForUnity.ClientConfig.DetailsFoldoutOpen";
internal const string SetupCompleted = "MCPForUnity.SetupCompleted";
internal const string SetupDismissed = "MCPForUnity.SetupDismissed";
internal const string CustomToolRegistrationEnabled = "MCPForUnity.CustomToolRegistrationEnabled";
internal const string LastUpdateCheck = "MCPForUnity.LastUpdateCheck";
internal const string LatestKnownVersion = "MCPForUnity.LatestKnownVersion";
internal const string LastAssetStoreUpdateCheck = "MCPForUnity.LastAssetStoreUpdateCheck";
internal const string LatestKnownAssetStoreVersion = "MCPForUnity.LatestKnownAssetStoreVersion";
internal const string LastStdIoUpgradeVersion = "MCPForUnity.LastStdIoUpgradeVersion";
internal const string TelemetryDisabled = "MCPForUnity.TelemetryDisabled";
internal const string CustomerUuid = "MCPForUnity.CustomerUUID";
internal const string ApiKey = "MCPForUnity.ApiKey";
internal const string AutoStartOnLoad = "MCPForUnity.AutoStartOnLoad";
internal const string HttpServerLaunchConfirmed = "MCPForUnity.HttpServerLaunchConfirmed";
internal const string BatchExecuteMaxCommands = "MCPForUnity.BatchExecute.MaxCommands";
internal const string LogRecordEnabled = "MCPForUnity.LogRecordEnabled";
internal const string ExecuteCodeCompiler = "MCPForUnity.ExecuteCode.Compiler";
// AI Asset Generation — NON-SECRET config only. Provider API keys live in the OS
// secure store (MCPForUnity.Editor.Security.SecureKeyStore), never in EditorPrefs.
internal const string AssetGenSelectedModelProvider = "MCPForUnity.AssetGen.ModelProvider";
internal const string AssetGenSelectedImageProvider = "MCPForUnity.AssetGen.ImageProvider";
internal const string AssetGenDefaultFormat = "MCPForUnity.AssetGen.Format";
internal const string AssetGenOutputRoot = "MCPForUnity.AssetGen.OutputRoot";
internal const string AssetGenAutoNormalize = "MCPForUnity.AssetGen.AutoNormalize";
internal const string AssetGenProviderEnabledPrefix = "MCPForUnity.AssetGen.Enabled.";
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7317786cfb9304b0db20ca73a774b9fa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,14 @@
namespace MCPForUnity.Editor.Constants
{
/// <summary>
/// Constants for health check status values.
/// Used for coordinating health state between Connection and Advanced sections.
/// </summary>
public static class HealthStatus
{
public const string Unknown = "Unknown";
public const string Healthy = "Healthy";
public const string PingFailed = "Ping Failed";
public const string Unhealthy = "Unhealthy";
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c15ed2426f43860479f1b8a99a343d16
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,9 @@
namespace MCPForUnity.Editor.Constants
{
/// <summary>Canonical user-facing product identity strings.</summary>
public static class ProductInfo
{
public const string ProductName = "MCP for Unity";
public const string MenuRoot = "Window/MCP for Unity";
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1df5e80156ac4f1ebe343e537eaa9da4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 221a4d6e595be6897a5b17b77aedd4d0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,143 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using MCPForUnity.Editor.Dependencies.Models;
using MCPForUnity.Editor.Dependencies.PlatformDetectors;
using MCPForUnity.Editor.Helpers;
using UnityEditor;
using UnityEngine;
namespace MCPForUnity.Editor.Dependencies
{
/// <summary>
/// Main orchestrator for dependency validation and management
/// </summary>
public static class DependencyManager
{
private static readonly List<IPlatformDetector> _detectors = new List<IPlatformDetector>
{
new WindowsPlatformDetector(),
new MacOSPlatformDetector(),
new LinuxPlatformDetector()
};
private static IPlatformDetector _currentDetector;
/// <summary>
/// Get the platform detector for the current operating system
/// </summary>
public static IPlatformDetector GetCurrentPlatformDetector()
{
if (_currentDetector == null)
{
_currentDetector = _detectors.FirstOrDefault(d => d.CanDetect);
if (_currentDetector == null)
{
throw new PlatformNotSupportedException($"No detector available for current platform: {RuntimeInformation.OSDescription}");
}
}
return _currentDetector;
}
/// <summary>
/// Perform a comprehensive dependency check
/// </summary>
public static DependencyCheckResult CheckAllDependencies()
{
var result = new DependencyCheckResult();
try
{
var detector = GetCurrentPlatformDetector();
McpLog.Info($"Checking dependencies on {detector.PlatformName}...", always: false);
// Check Python
var pythonStatus = detector.DetectPython();
result.Dependencies.Add(pythonStatus);
// Check uv
var uvStatus = detector.DetectUv();
result.Dependencies.Add(uvStatus);
// Generate summary and recommendations
result.GenerateSummary();
GenerateRecommendations(result, detector);
McpLog.Info($"Dependency check completed. System ready: {result.IsSystemReady}", always: false);
}
catch (Exception ex)
{
McpLog.Error($"Error during dependency check: {ex.Message}");
result.Summary = $"Dependency check failed: {ex.Message}";
result.IsSystemReady = false;
}
return result;
}
/// <summary>
/// Get installation recommendations for the current platform
/// </summary>
public static string GetInstallationRecommendations()
{
try
{
var detector = GetCurrentPlatformDetector();
return detector.GetInstallationRecommendations();
}
catch (Exception ex)
{
return $"Error getting installation recommendations: {ex.Message}";
}
}
/// <summary>
/// Get platform-specific installation URLs
/// </summary>
public static (string pythonUrl, string uvUrl) GetInstallationUrls()
{
try
{
var detector = GetCurrentPlatformDetector();
return (detector.GetPythonInstallUrl(), detector.GetUvInstallUrl());
}
catch
{
return ("https://python.org/downloads/", "https://docs.astral.sh/uv/getting-started/installation/");
}
}
private static void GenerateRecommendations(DependencyCheckResult result, IPlatformDetector detector)
{
var missing = result.GetMissingDependencies();
if (missing.Count == 0)
{
result.RecommendedActions.Add("All dependencies are available. You can start using MCP for Unity.");
return;
}
foreach (var dep in missing)
{
if (dep.Name == "Python")
{
result.RecommendedActions.Add($"Install Python 3.10+ from: {detector.GetPythonInstallUrl()}");
}
else if (dep.Name == "uv Package Manager")
{
result.RecommendedActions.Add($"Install uv package manager from: {detector.GetUvInstallUrl()}");
}
else if (dep.Name == "MCP Server")
{
result.RecommendedActions.Add("MCP Server will be installed automatically when needed.");
}
}
if (result.GetMissingRequired().Count > 0)
{
result.RecommendedActions.Add("Use the Setup Window (Window > MCP for Unity > Local Setup Window) for guided installation.");
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4a6d2236d370b4f1db4d0e3d3ce0dcac
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4c0f2e87395b4c6c9df8c21b6d0fae13
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace MCPForUnity.Editor.Dependencies.Models
{
/// <summary>
/// Result of a comprehensive dependency check
/// </summary>
[Serializable]
public class DependencyCheckResult
{
/// <summary>
/// List of all dependency statuses checked
/// </summary>
public List<DependencyStatus> Dependencies { get; set; }
/// <summary>
/// Overall system readiness for MCP operations
/// </summary>
public bool IsSystemReady { get; set; }
/// <summary>
/// Whether all required dependencies are available
/// </summary>
public bool AllRequiredAvailable => Dependencies?.Where(d => d.IsRequired).All(d => d.IsAvailable) ?? false;
/// <summary>
/// Whether any optional dependencies are missing
/// </summary>
public bool HasMissingOptional => Dependencies?.Where(d => !d.IsRequired).Any(d => !d.IsAvailable) ?? false;
/// <summary>
/// Summary message about the dependency state
/// </summary>
public string Summary { get; set; }
/// <summary>
/// Recommended next steps for the user
/// </summary>
public List<string> RecommendedActions { get; set; }
/// <summary>
/// Timestamp when this check was performed
/// </summary>
public DateTime CheckedAt { get; set; }
public DependencyCheckResult()
{
Dependencies = new List<DependencyStatus>();
RecommendedActions = new List<string>();
CheckedAt = DateTime.UtcNow;
}
/// <summary>
/// Get dependencies by availability status
/// </summary>
public List<DependencyStatus> GetMissingDependencies()
{
return Dependencies?.Where(d => !d.IsAvailable).ToList() ?? new List<DependencyStatus>();
}
/// <summary>
/// Get missing required dependencies
/// </summary>
public List<DependencyStatus> GetMissingRequired()
{
return Dependencies?.Where(d => d.IsRequired && !d.IsAvailable).ToList() ?? new List<DependencyStatus>();
}
/// <summary>
/// Generate a user-friendly summary of the dependency state
/// </summary>
public void GenerateSummary()
{
var missing = GetMissingDependencies();
var missingRequired = GetMissingRequired();
if (missing.Count == 0)
{
Summary = "All dependencies are available and ready.";
IsSystemReady = true;
}
else if (missingRequired.Count == 0)
{
Summary = $"System is ready. {missing.Count} optional dependencies are missing.";
IsSystemReady = true;
}
else
{
Summary = $"System is not ready. {missingRequired.Count} required dependencies are missing.";
IsSystemReady = false;
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f6df82faa423f4e9ebb13a3dcee8ba19
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,65 @@
using System;
namespace MCPForUnity.Editor.Dependencies.Models
{
/// <summary>
/// Represents the status of a dependency check
/// </summary>
[Serializable]
public class DependencyStatus
{
/// <summary>
/// Name of the dependency being checked
/// </summary>
public string Name { get; set; }
/// <summary>
/// Whether the dependency is available and functional
/// </summary>
public bool IsAvailable { get; set; }
/// <summary>
/// Version information if available
/// </summary>
public string Version { get; set; }
/// <summary>
/// Path to the dependency executable/installation
/// </summary>
public string Path { get; set; }
/// <summary>
/// Additional details about the dependency status
/// </summary>
public string Details { get; set; }
/// <summary>
/// Error message if dependency check failed
/// </summary>
public string ErrorMessage { get; set; }
/// <summary>
/// Whether this dependency is required for basic functionality
/// </summary>
public bool IsRequired { get; set; }
/// <summary>
/// Suggested installation method or URL
/// </summary>
public string InstallationHint { get; set; }
public DependencyStatus(string name, bool isRequired = true)
{
Name = name;
IsRequired = isRequired;
IsAvailable = false;
}
public override string ToString()
{
var status = IsAvailable ? "✓" : "✗";
var version = !string.IsNullOrEmpty(Version) ? $" ({Version})" : "";
return $"{status} {Name}{version}";
}
}
}

Some files were not shown because too many files have changed in this diff Show More