chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
Server/build
|
||||
.git
|
||||
.venv
|
||||
__pycache__
|
||||
*.pyc
|
||||
.DS_Store
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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/
|
||||
@@ -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 -->
|
||||
Executable
+119
@@ -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))
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
@@ -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/
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 31e7fac5858840340a75cc6df0ad3d9e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("MCPForUnityTests.EditMode")]
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: be61633e00d934610ac1ff8192ffbe3d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
@@ -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
Reference in New Issue
Block a user