Compare commits
16 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
8c6cefdd3f | |
|
|
6f3b869f3d | |
|
|
a8e478a42a | |
|
|
ebe0296b51 | |
|
|
c21b65efb5 | |
|
|
b7ed1f3494 | |
|
|
b52d1563b0 | |
|
|
7aa539c9fe | |
|
|
20e822b60f | |
|
|
74236b6f66 | |
|
|
2671f2091a | |
|
|
b5a77feb9e | |
|
|
862c7fa4c3 | |
|
|
a126ed6c3f | |
|
|
cb08b0c59b | |
|
|
ac7f4a3099 |
|
|
@ -10,11 +10,128 @@ on:
|
|||
- beta
|
||||
paths:
|
||||
- "Server/**"
|
||||
- "MCPForUnity/**"
|
||||
|
||||
jobs:
|
||||
update_unity_beta_version:
|
||||
name: Update Unity package to beta version
|
||||
runs-on: ubuntu-latest
|
||||
# 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"
|
||||
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"
|
||||
fi
|
||||
echo "updated=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
publish_pypi_prerelease:
|
||||
name: Publish beta to PyPI (pre-release)
|
||||
runs-on: ubuntu-latest
|
||||
# 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
|
||||
|
|
@ -25,6 +142,7 @@ jobs:
|
|||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: beta
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
|
|
@ -39,21 +157,39 @@ jobs:
|
|||
run: |
|
||||
set -euo pipefail
|
||||
RAW_VERSION=$(grep -oP '(?<=version = ")[^"]+' Server/pyproject.toml)
|
||||
# Strip any existing pre-release suffix (a, b, rc, dev, post) for safe parsing
|
||||
# e.g., "9.2.0b1" -> "9.2.0", "9.2.0.dev1" -> "9.2.0"
|
||||
BASE_VERSION=$(echo "$RAW_VERSION" | sed -E 's/(a|b|rc|\.dev|\.post)[0-9]+$//')
|
||||
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
|
||||
# Bump minor version and use beta suffix (PEP 440 compliant: X.Y+1.0bN)
|
||||
# This ensures beta is "newer" than the stable release
|
||||
|
||||
IFS='.' read -r MAJOR MINOR PATCH <<< "$BASE_VERSION"
|
||||
NEXT_MINOR=$((MINOR + 1))
|
||||
|
||||
# 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}.${NEXT_MINOR}.0b${BETA_NUMBER}"
|
||||
echo "Raw version: $RAW_VERSION"
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -8,12 +8,13 @@ on:
|
|||
workflow_dispatch:
|
||||
inputs:
|
||||
version_bump:
|
||||
description: "Version bump type"
|
||||
description: "Version bump type (none = release beta version as-is)"
|
||||
type: choice
|
||||
options:
|
||||
- patch
|
||||
- minor
|
||||
- major
|
||||
- none
|
||||
default: patch
|
||||
required: true
|
||||
|
||||
|
|
@ -44,8 +45,102 @@ jobs:
|
|||
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
|
||||
|
|
@ -53,24 +148,35 @@ jobs:
|
|||
CURRENT_VERSION=$(jq -r '.version' "MCPForUnity/package.json")
|
||||
echo "Current version: $CURRENT_VERSION"
|
||||
|
||||
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
|
||||
# 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
|
||||
|
||||
NEW_VERSION="$MA.$MI.$PA"
|
||||
echo "New version: $NEW_VERSION"
|
||||
echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "current_version=$CURRENT_VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
|
@ -244,7 +350,8 @@ jobs:
|
|||
run: |
|
||||
set -euo pipefail
|
||||
# Enable auto-merge (requires repo setting "Allow auto-merge")
|
||||
gh pr merge "$PR_NUMBER" --merge --auto || true
|
||||
# Use --no-delete-branch to prevent deleting main (the head branch)
|
||||
gh pr merge "$PR_NUMBER" --merge --auto --no-delete-branch || 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')
|
||||
|
|
@ -256,7 +363,7 @@ jobs:
|
|||
sleep 5
|
||||
done
|
||||
echo "Sync PR did not merge in time. Attempting direct merge..."
|
||||
gh pr merge "$PR_NUMBER" --merge
|
||||
gh pr merge "$PR_NUMBER" --merge --no-delete-branch
|
||||
|
||||
publish_docker:
|
||||
name: Publish Docker image
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ namespace MCPForUnity.Editor.Clients.Configurators
|
|||
public override IList<string> GetInstallationSteps() => new List<string>
|
||||
{
|
||||
"Ensure Claude CLI is installed (comes with Claude Code)",
|
||||
"Click Register to add UnityMCP via 'claude mcp add'",
|
||||
"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'"
|
||||
};
|
||||
|
|
|
|||
|
|
@ -78,6 +78,74 @@ namespace MCPForUnity.Editor.Clients
|
|||
string Normalize(string value) => value.Trim().TrimEnd('/');
|
||||
return string.Equals(Normalize(a), Normalize(b), StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the expected package source for validation, accounting for beta mode.
|
||||
/// This should match what Configure() would actually use for the --from argument.
|
||||
/// MUST be called from the main thread due to EditorPrefs access.
|
||||
/// </summary>
|
||||
protected static string GetExpectedPackageSourceForValidation()
|
||||
{
|
||||
// Check for explicit override first
|
||||
string gitUrlOverride = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, "");
|
||||
if (!string.IsNullOrEmpty(gitUrlOverride))
|
||||
{
|
||||
return gitUrlOverride;
|
||||
}
|
||||
|
||||
// Check beta mode using the same logic as GetUseBetaServerWithDynamicDefault
|
||||
// (bypass cache to ensure fresh read)
|
||||
bool useBetaServer;
|
||||
bool hasPrefKey = EditorPrefs.HasKey(EditorPrefKeys.UseBetaServer);
|
||||
if (hasPrefKey)
|
||||
{
|
||||
useBetaServer = EditorPrefs.GetBool(EditorPrefKeys.UseBetaServer, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Dynamic default based on package version
|
||||
useBetaServer = AssetPathUtility.IsPreReleaseVersion();
|
||||
}
|
||||
|
||||
if (useBetaServer)
|
||||
{
|
||||
return "mcpforunityserver>=0.0.0a0";
|
||||
}
|
||||
|
||||
// Standard mode uses exact version from package.json
|
||||
return AssetPathUtility.GetMcpServerPackageSource();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a package source string represents a beta/prerelease version.
|
||||
/// Beta versions include:
|
||||
/// - PyPI beta: "mcpforunityserver==9.4.0b20250203..." (contains 'b' before timestamp)
|
||||
/// - PyPI prerelease range: "mcpforunityserver>=0.0.0a0" (used when beta mode is enabled)
|
||||
/// - Git beta branch: contains "@beta" or "-beta"
|
||||
/// </summary>
|
||||
protected static bool IsBetaPackageSource(string packageSource)
|
||||
{
|
||||
if (string.IsNullOrEmpty(packageSource))
|
||||
return false;
|
||||
|
||||
// PyPI beta format: mcpforunityserver==X.Y.Zb<timestamp>
|
||||
// The 'b' suffix before numbers indicates a PEP 440 beta version
|
||||
if (System.Text.RegularExpressions.Regex.IsMatch(packageSource, @"==\d+\.\d+\.\d+b\d+"))
|
||||
return true;
|
||||
|
||||
// PyPI prerelease range: >=0.0.0a0 (used when "Use Beta Server" is enabled in Unity settings)
|
||||
if (packageSource.Contains(">=0.0.0a0", StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
|
||||
// Git-based beta references
|
||||
if (packageSource.Contains("@beta", StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
|
||||
if (packageSource.Contains("-beta", StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>JSON-file based configurator (Cursor, Windsurf, VS Code, etc.).</summary>
|
||||
|
|
@ -174,12 +242,44 @@ namespace MCPForUnity.Editor.Clients
|
|||
}
|
||||
|
||||
bool matches = false;
|
||||
bool hasVersionMismatch = false;
|
||||
string mismatchReason = null;
|
||||
|
||||
if (args != null && args.Length > 0)
|
||||
{
|
||||
string expectedUvxUrl = AssetPathUtility.GetMcpServerPackageSource();
|
||||
// Use beta-aware expected package source for comparison
|
||||
string expectedUvxUrl = GetExpectedPackageSourceForValidation();
|
||||
string configuredUvxUrl = McpConfigurationHelper.ExtractUvxUrl(args);
|
||||
matches = !string.IsNullOrEmpty(configuredUvxUrl) &&
|
||||
McpConfigurationHelper.PathsEqual(configuredUvxUrl, expectedUvxUrl);
|
||||
|
||||
if (!string.IsNullOrEmpty(configuredUvxUrl) && !string.IsNullOrEmpty(expectedUvxUrl))
|
||||
{
|
||||
if (McpConfigurationHelper.PathsEqual(configuredUvxUrl, expectedUvxUrl))
|
||||
{
|
||||
matches = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Check for beta/stable mismatch
|
||||
bool configuredIsBeta = IsBetaPackageSource(configuredUvxUrl);
|
||||
bool expectedIsBeta = IsBetaPackageSource(expectedUvxUrl);
|
||||
|
||||
if (configuredIsBeta && !expectedIsBeta)
|
||||
{
|
||||
hasVersionMismatch = true;
|
||||
mismatchReason = "Configured for beta server, but 'Use Beta Server' is disabled in Advanced settings.";
|
||||
}
|
||||
else if (!configuredIsBeta && expectedIsBeta)
|
||||
{
|
||||
hasVersionMismatch = true;
|
||||
mismatchReason = "Configured for stable server, but 'Use Beta Server' is enabled in Advanced settings.";
|
||||
}
|
||||
else
|
||||
{
|
||||
hasVersionMismatch = true;
|
||||
mismatchReason = "Server version doesn't match the plugin. Re-configure to update.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(configuredUrl))
|
||||
{
|
||||
|
|
@ -194,7 +294,27 @@ namespace MCPForUnity.Editor.Clients
|
|||
return client.status;
|
||||
}
|
||||
|
||||
if (attemptAutoRewrite)
|
||||
if (hasVersionMismatch)
|
||||
{
|
||||
if (attemptAutoRewrite)
|
||||
{
|
||||
var result = McpConfigurationHelper.WriteMcpConfiguration(path, client);
|
||||
if (result == "Configured successfully")
|
||||
{
|
||||
client.SetStatus(McpStatus.Configured);
|
||||
client.configuredTransport = HttpEndpointUtility.GetCurrentServerTransport();
|
||||
}
|
||||
else
|
||||
{
|
||||
client.SetStatus(McpStatus.VersionMismatch, mismatchReason);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
client.SetStatus(McpStatus.VersionMismatch, mismatchReason);
|
||||
}
|
||||
}
|
||||
else if (attemptAutoRewrite)
|
||||
{
|
||||
var result = McpConfigurationHelper.WriteMcpConfiguration(path, client);
|
||||
if (result == "Configured successfully")
|
||||
|
|
@ -300,6 +420,9 @@ namespace MCPForUnity.Editor.Clients
|
|||
}
|
||||
|
||||
bool matches = false;
|
||||
bool hasVersionMismatch = false;
|
||||
string mismatchReason = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(url))
|
||||
{
|
||||
// Match against the active scope's URL
|
||||
|
|
@ -307,10 +430,39 @@ namespace MCPForUnity.Editor.Clients
|
|||
}
|
||||
else if (args != null && args.Length > 0)
|
||||
{
|
||||
string expected = AssetPathUtility.GetMcpServerPackageSource();
|
||||
// Use beta-aware expected package source for comparison
|
||||
string expected = GetExpectedPackageSourceForValidation();
|
||||
string configured = McpConfigurationHelper.ExtractUvxUrl(args);
|
||||
matches = !string.IsNullOrEmpty(configured) &&
|
||||
McpConfigurationHelper.PathsEqual(configured, expected);
|
||||
|
||||
if (!string.IsNullOrEmpty(configured) && !string.IsNullOrEmpty(expected))
|
||||
{
|
||||
if (McpConfigurationHelper.PathsEqual(configured, expected))
|
||||
{
|
||||
matches = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Check for beta/stable mismatch
|
||||
bool configuredIsBeta = IsBetaPackageSource(configured);
|
||||
bool expectedIsBeta = IsBetaPackageSource(expected);
|
||||
|
||||
if (configuredIsBeta && !expectedIsBeta)
|
||||
{
|
||||
hasVersionMismatch = true;
|
||||
mismatchReason = "Configured for beta server, but 'Use Beta Server' is disabled in Advanced settings.";
|
||||
}
|
||||
else if (!configuredIsBeta && expectedIsBeta)
|
||||
{
|
||||
hasVersionMismatch = true;
|
||||
mismatchReason = "Configured for stable server, but 'Use Beta Server' is enabled in Advanced settings.";
|
||||
}
|
||||
else
|
||||
{
|
||||
hasVersionMismatch = true;
|
||||
mismatchReason = "Server version doesn't match the plugin. Re-configure to update.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matches)
|
||||
|
|
@ -318,6 +470,22 @@ namespace MCPForUnity.Editor.Clients
|
|||
client.SetStatus(McpStatus.Configured);
|
||||
return client.status;
|
||||
}
|
||||
|
||||
if (hasVersionMismatch)
|
||||
{
|
||||
if (attemptAutoRewrite)
|
||||
{
|
||||
string result = McpConfigurationHelper.ConfigureCodexClient(path, client);
|
||||
if (result == "Configured successfully")
|
||||
{
|
||||
client.SetStatus(McpStatus.Configured);
|
||||
client.configuredTransport = HttpEndpointUtility.GetCurrentServerTransport();
|
||||
return client.status;
|
||||
}
|
||||
}
|
||||
client.SetStatus(McpStatus.VersionMismatch, mismatchReason);
|
||||
return client.status;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -394,7 +562,7 @@ namespace MCPForUnity.Editor.Clients
|
|||
public ClaudeCliMcpConfigurator(McpClient client) : base(client) { }
|
||||
|
||||
public override bool SupportsAutoConfigure => true;
|
||||
public override string GetConfigureActionLabel() => client.status == McpStatus.Configured ? "Unregister" : "Register";
|
||||
public override string GetConfigureActionLabel() => client.status == McpStatus.Configured ? "Unregister" : "Configure";
|
||||
|
||||
public override string GetConfigPath() => "Managed via Claude CLI";
|
||||
|
||||
|
|
@ -409,18 +577,26 @@ namespace MCPForUnity.Editor.Clients
|
|||
bool useHttpTransport = EditorConfigurationCache.Instance.UseHttpTransport;
|
||||
// Resolve claudePath on the main thread (EditorPrefs access)
|
||||
string claudePath = MCPServiceLocator.Paths.GetClaudeCliPath();
|
||||
return CheckStatusWithProjectDir(projectDir, useHttpTransport, claudePath, attemptAutoRewrite);
|
||||
RuntimePlatform platform = Application.platform;
|
||||
bool isRemoteScope = HttpEndpointUtility.IsRemoteScope();
|
||||
// Get expected package source considering beta mode (matches what Register() would use)
|
||||
string expectedPackageSource = GetExpectedPackageSourceForValidation();
|
||||
return CheckStatusWithProjectDir(projectDir, useHttpTransport, claudePath, platform, isRemoteScope, expectedPackageSource, attemptAutoRewrite);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal thread-safe version of CheckStatus.
|
||||
/// Can be called from background threads because all main-thread-only values are passed as parameters.
|
||||
/// projectDir, useHttpTransport, and claudePath are REQUIRED (non-nullable) to enforce thread safety at compile time.
|
||||
/// projectDir, useHttpTransport, claudePath, platform, isRemoteScope, and expectedPackageSource are REQUIRED
|
||||
/// (non-nullable where applicable) to enforce thread safety at compile time.
|
||||
/// NOTE: attemptAutoRewrite is NOT fully thread-safe because Configure() requires the main thread.
|
||||
/// When called from a background thread, pass attemptAutoRewrite=false and handle re-registration
|
||||
/// on the main thread based on the returned status.
|
||||
/// </summary>
|
||||
internal McpStatus CheckStatusWithProjectDir(string projectDir, bool useHttpTransport, string claudePath, bool attemptAutoRewrite = false)
|
||||
internal McpStatus CheckStatusWithProjectDir(
|
||||
string projectDir, bool useHttpTransport, string claudePath, RuntimePlatform platform,
|
||||
bool isRemoteScope, string expectedPackageSource,
|
||||
bool attemptAutoRewrite = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
@ -437,135 +613,134 @@ namespace MCPForUnity.Editor.Clients
|
|||
throw new ArgumentNullException(nameof(projectDir), "Project directory must be provided for thread-safe execution");
|
||||
}
|
||||
|
||||
string pathPrepend = null;
|
||||
if (Application.platform == RuntimePlatform.OSXEditor)
|
||||
// Read Claude Code config directly from ~/.claude.json instead of using slow CLI
|
||||
// This is instant vs 15+ seconds for `claude mcp list` which does health checks
|
||||
var configResult = ReadClaudeCodeConfig(projectDir);
|
||||
if (configResult.error != null)
|
||||
{
|
||||
pathPrepend = "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin";
|
||||
}
|
||||
else if (Application.platform == RuntimePlatform.LinuxEditor)
|
||||
{
|
||||
pathPrepend = "/usr/local/bin:/usr/bin:/bin";
|
||||
client.SetStatus(McpStatus.NotConfigured, configResult.error);
|
||||
client.configuredTransport = Models.ConfiguredTransport.Unknown;
|
||||
return client.status;
|
||||
}
|
||||
|
||||
try
|
||||
if (configResult.serverConfig == null)
|
||||
{
|
||||
string claudeDir = Path.GetDirectoryName(claudePath);
|
||||
if (!string.IsNullOrEmpty(claudeDir))
|
||||
// UnityMCP not found in config
|
||||
client.SetStatus(McpStatus.NotConfigured);
|
||||
client.configuredTransport = Models.ConfiguredTransport.Unknown;
|
||||
return client.status;
|
||||
}
|
||||
|
||||
// UnityMCP is registered - check transport and version
|
||||
bool currentUseHttp = useHttpTransport;
|
||||
var serverConfig = configResult.serverConfig;
|
||||
|
||||
// Determine registered transport type
|
||||
string registeredType = serverConfig["type"]?.ToString()?.ToLowerInvariant() ?? "";
|
||||
bool registeredWithHttp = registeredType == "http";
|
||||
bool registeredWithStdio = registeredType == "stdio";
|
||||
|
||||
// Set the configured transport based on what we detected
|
||||
if (registeredWithHttp)
|
||||
{
|
||||
client.configuredTransport = isRemoteScope
|
||||
? Models.ConfiguredTransport.HttpRemote
|
||||
: Models.ConfiguredTransport.Http;
|
||||
}
|
||||
else if (registeredWithStdio)
|
||||
{
|
||||
client.configuredTransport = Models.ConfiguredTransport.Stdio;
|
||||
}
|
||||
else
|
||||
{
|
||||
client.configuredTransport = Models.ConfiguredTransport.Unknown;
|
||||
}
|
||||
|
||||
// Check for transport mismatch
|
||||
bool hasTransportMismatch = (currentUseHttp && registeredWithStdio) || (!currentUseHttp && registeredWithHttp);
|
||||
|
||||
// For stdio transport, also check package version
|
||||
bool hasVersionMismatch = false;
|
||||
string configuredPackageSource = null;
|
||||
string mismatchReason = null;
|
||||
if (registeredWithStdio)
|
||||
{
|
||||
configuredPackageSource = ExtractPackageSourceFromConfig(serverConfig);
|
||||
if (!string.IsNullOrEmpty(configuredPackageSource) && !string.IsNullOrEmpty(expectedPackageSource))
|
||||
{
|
||||
pathPrepend = string.IsNullOrEmpty(pathPrepend)
|
||||
? claudeDir
|
||||
: $"{claudeDir}:{pathPrepend}";
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
// Check if UnityMCP exists (handles both "UnityMCP" and legacy "unityMCP")
|
||||
if (ExecPath.TryRun(claudePath, "mcp list", projectDir, out var listStdout, out var listStderr, 10000, pathPrepend))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(listStdout) && listStdout.IndexOf("UnityMCP", StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
// UnityMCP is registered - now verify transport mode matches
|
||||
// useHttpTransport parameter is required (non-nullable) to ensure thread safety
|
||||
bool currentUseHttp = useHttpTransport;
|
||||
|
||||
// Get detailed info about the registration to check transport type
|
||||
// Try both "UnityMCP" and "unityMCP" (legacy naming)
|
||||
string getStdout = null, getStderr = null;
|
||||
bool gotInfo = ExecPath.TryRun(claudePath, "mcp get UnityMCP", projectDir, out getStdout, out getStderr, 7000, pathPrepend)
|
||||
|| ExecPath.TryRun(claudePath, "mcp get unityMCP", projectDir, out getStdout, out getStderr, 7000, pathPrepend);
|
||||
if (gotInfo)
|
||||
// Check for exact match first
|
||||
if (!string.Equals(configuredPackageSource, expectedPackageSource, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Parse the output to determine registered transport mode
|
||||
// The CLI output format contains "Type: http" or "Type: stdio"
|
||||
bool registeredWithHttp = getStdout.Contains("Type: http", StringComparison.OrdinalIgnoreCase);
|
||||
bool registeredWithStdio = getStdout.Contains("Type: stdio", StringComparison.OrdinalIgnoreCase);
|
||||
hasVersionMismatch = true;
|
||||
|
||||
// Set the configured transport based on what we detected
|
||||
// For HTTP, we can't distinguish local/remote from CLI output alone,
|
||||
// so infer from the current scope setting when HTTP is detected.
|
||||
if (registeredWithHttp)
|
||||
// Provide more specific mismatch reason for beta/stable differences
|
||||
bool configuredIsBeta = IsBetaPackageSource(configuredPackageSource);
|
||||
bool expectedIsBeta = IsBetaPackageSource(expectedPackageSource);
|
||||
|
||||
if (configuredIsBeta && !expectedIsBeta)
|
||||
{
|
||||
client.configuredTransport = HttpEndpointUtility.IsRemoteScope()
|
||||
? Models.ConfiguredTransport.HttpRemote
|
||||
: Models.ConfiguredTransport.Http;
|
||||
mismatchReason = "Configured for beta server, but 'Use Beta Server' is disabled in Advanced settings.";
|
||||
}
|
||||
else if (registeredWithStdio)
|
||||
else if (!configuredIsBeta && expectedIsBeta)
|
||||
{
|
||||
client.configuredTransport = Models.ConfiguredTransport.Stdio;
|
||||
mismatchReason = "Configured for stable server, but 'Use Beta Server' is enabled in Advanced settings.";
|
||||
}
|
||||
else
|
||||
{
|
||||
client.configuredTransport = Models.ConfiguredTransport.Unknown;
|
||||
}
|
||||
|
||||
// Check for transport mismatch (3-way: Stdio, Http, HttpRemote)
|
||||
bool hasTransportMismatch = (currentUseHttp && registeredWithStdio) || (!currentUseHttp && registeredWithHttp);
|
||||
|
||||
// For stdio transport, also check package version
|
||||
bool hasVersionMismatch = false;
|
||||
string configuredPackageSource = null;
|
||||
string expectedPackageSource = null;
|
||||
if (registeredWithStdio)
|
||||
{
|
||||
expectedPackageSource = AssetPathUtility.GetMcpServerPackageSource();
|
||||
configuredPackageSource = ExtractPackageSourceFromCliOutput(getStdout);
|
||||
hasVersionMismatch = !string.IsNullOrEmpty(configuredPackageSource) &&
|
||||
!string.Equals(configuredPackageSource, expectedPackageSource, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
// If there's any mismatch and auto-rewrite is enabled, re-register
|
||||
if (hasTransportMismatch || hasVersionMismatch)
|
||||
{
|
||||
// Configure() requires main thread (accesses EditorPrefs, Application.dataPath)
|
||||
// Only attempt auto-rewrite if we're on the main thread
|
||||
bool isMainThread = System.Threading.Thread.CurrentThread.ManagedThreadId == 1;
|
||||
if (attemptAutoRewrite && isMainThread)
|
||||
{
|
||||
string reason = hasTransportMismatch
|
||||
? $"Transport mismatch (registered: {(registeredWithHttp ? "HTTP" : "stdio")}, expected: {(currentUseHttp ? "HTTP" : "stdio")})"
|
||||
: $"Package version mismatch (registered: {configuredPackageSource}, expected: {expectedPackageSource})";
|
||||
McpLog.Info($"{reason}. Re-registering...");
|
||||
try
|
||||
{
|
||||
// Force re-register by ensuring status is not Configured (which would toggle to Unregister)
|
||||
client.SetStatus(McpStatus.IncorrectPath);
|
||||
Configure();
|
||||
return client.status;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Warn($"Auto-reregister failed: {ex.Message}");
|
||||
client.SetStatus(McpStatus.IncorrectPath, $"Configuration mismatch. Click Configure to re-register.");
|
||||
return client.status;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (hasTransportMismatch)
|
||||
{
|
||||
string errorMsg = $"Transport mismatch: Claude Code is registered with {(registeredWithHttp ? "HTTP" : "stdio")} but current setting is {(currentUseHttp ? "HTTP" : "stdio")}. Click Configure to re-register.";
|
||||
client.SetStatus(McpStatus.Error, errorMsg);
|
||||
McpLog.Warn(errorMsg);
|
||||
}
|
||||
else
|
||||
{
|
||||
client.SetStatus(McpStatus.IncorrectPath, $"Package version mismatch: registered with '{configuredPackageSource}' but current version is '{expectedPackageSource}'.");
|
||||
}
|
||||
return client.status;
|
||||
}
|
||||
mismatchReason = "Server version doesn't match the plugin. Re-configure to update.";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client.SetStatus(McpStatus.Configured);
|
||||
// If there's any mismatch and auto-rewrite is enabled, re-register
|
||||
if (hasTransportMismatch || hasVersionMismatch)
|
||||
{
|
||||
// Configure() requires main thread (accesses EditorPrefs, Application.dataPath)
|
||||
// Only attempt auto-rewrite if we're on the main thread
|
||||
bool isMainThread = System.Threading.Thread.CurrentThread.ManagedThreadId == 1;
|
||||
if (attemptAutoRewrite && isMainThread)
|
||||
{
|
||||
string reason = hasTransportMismatch
|
||||
? $"Transport mismatch (registered: {(registeredWithHttp ? "HTTP" : "stdio")}, expected: {(currentUseHttp ? "HTTP" : "stdio")})"
|
||||
: mismatchReason ?? $"Package version mismatch";
|
||||
McpLog.Info($"{reason}. Re-registering...");
|
||||
try
|
||||
{
|
||||
// Force re-register by ensuring status is not Configured (which would toggle to Unregister)
|
||||
client.SetStatus(McpStatus.IncorrectPath);
|
||||
Configure();
|
||||
return client.status;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Warn($"Auto-reregister failed: {ex.Message}");
|
||||
client.SetStatus(McpStatus.IncorrectPath, $"Configuration mismatch. Click Configure to re-register.");
|
||||
return client.status;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (hasTransportMismatch)
|
||||
{
|
||||
string errorMsg = $"Transport mismatch: Claude Code is registered with {(registeredWithHttp ? "HTTP" : "stdio")} but current setting is {(currentUseHttp ? "HTTP" : "stdio")}. Click Configure to re-register.";
|
||||
client.SetStatus(McpStatus.Error, errorMsg);
|
||||
McpLog.Warn(errorMsg);
|
||||
}
|
||||
else
|
||||
{
|
||||
client.SetStatus(McpStatus.VersionMismatch, mismatchReason);
|
||||
}
|
||||
return client.status;
|
||||
}
|
||||
}
|
||||
|
||||
client.SetStatus(McpStatus.NotConfigured);
|
||||
client.configuredTransport = Models.ConfiguredTransport.Unknown;
|
||||
client.SetStatus(McpStatus.Configured);
|
||||
return client.status;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
McpLog.Warn($"[Claude Code] CheckStatus exception: {ex.GetType().Name}: {ex.Message}");
|
||||
client.SetStatus(McpStatus.Error, ex.Message);
|
||||
client.configuredTransport = Models.ConfiguredTransport.Unknown;
|
||||
}
|
||||
|
|
@ -592,7 +767,7 @@ namespace MCPForUnity.Editor.Clients
|
|||
public void ConfigureWithCapturedValues(
|
||||
string projectDir, string claudePath, string pathPrepend,
|
||||
bool useHttpTransport, string httpUrl,
|
||||
string uvxPath, string gitUrl, string packageName, bool shouldForceRefresh,
|
||||
string uvxPath, string fromArgs, string packageName, bool shouldForceRefresh,
|
||||
string apiKey,
|
||||
Models.ConfiguredTransport serverTransport)
|
||||
{
|
||||
|
|
@ -603,7 +778,7 @@ namespace MCPForUnity.Editor.Clients
|
|||
else
|
||||
{
|
||||
RegisterWithCapturedValues(projectDir, claudePath, pathPrepend,
|
||||
useHttpTransport, httpUrl, uvxPath, gitUrl, packageName, shouldForceRefresh,
|
||||
useHttpTransport, httpUrl, uvxPath, fromArgs, packageName, shouldForceRefresh,
|
||||
apiKey, serverTransport);
|
||||
}
|
||||
}
|
||||
|
|
@ -614,7 +789,7 @@ namespace MCPForUnity.Editor.Clients
|
|||
private void RegisterWithCapturedValues(
|
||||
string projectDir, string claudePath, string pathPrepend,
|
||||
bool useHttpTransport, string httpUrl,
|
||||
string uvxPath, string gitUrl, string packageName, bool shouldForceRefresh,
|
||||
string uvxPath, string fromArgs, string packageName, bool shouldForceRefresh,
|
||||
string apiKey,
|
||||
Models.ConfiguredTransport serverTransport)
|
||||
{
|
||||
|
|
@ -627,27 +802,28 @@ namespace MCPForUnity.Editor.Clients
|
|||
if (useHttpTransport)
|
||||
{
|
||||
// Only include API key header for remote-hosted mode
|
||||
// Use --scope local to register in the project-local config, avoiding conflicts with user-level config (#664)
|
||||
if (serverTransport == Models.ConfiguredTransport.HttpRemote && !string.IsNullOrEmpty(apiKey))
|
||||
{
|
||||
string safeKey = SanitizeShellHeaderValue(apiKey);
|
||||
args = $"mcp add --transport http UnityMCP {httpUrl} --header \"{AuthConstants.ApiKeyHeader}: {safeKey}\"";
|
||||
args = $"mcp add --scope local --transport http UnityMCP {httpUrl} --header \"{AuthConstants.ApiKeyHeader}: {safeKey}\"";
|
||||
}
|
||||
else
|
||||
{
|
||||
args = $"mcp add --transport http UnityMCP {httpUrl}";
|
||||
args = $"mcp add --scope local --transport http UnityMCP {httpUrl}";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Note: --reinstall is not supported by uvx, use --no-cache --refresh instead
|
||||
string devFlags = shouldForceRefresh ? "--no-cache --refresh " : string.Empty;
|
||||
args = $"mcp add --transport stdio UnityMCP -- \"{uvxPath}\" {devFlags}--from \"{gitUrl}\" {packageName}";
|
||||
// Use --scope local to register in the project-local config, avoiding conflicts with user-level config (#664)
|
||||
args = $"mcp add --scope local --transport stdio UnityMCP -- \"{uvxPath}\" {devFlags}{fromArgs} {packageName}";
|
||||
}
|
||||
|
||||
// Remove any existing registrations - handle both "UnityMCP" and "unityMCP" (legacy)
|
||||
McpLog.Info("Removing any existing UnityMCP registrations before adding...");
|
||||
ExecPath.TryRun(claudePath, "mcp remove UnityMCP", projectDir, out _, out _, 7000, pathPrepend);
|
||||
ExecPath.TryRun(claudePath, "mcp remove unityMCP", projectDir, out _, out _, 7000, pathPrepend);
|
||||
// Remove any existing registrations from ALL scopes to prevent stale config conflicts (#664)
|
||||
McpLog.Info("Removing any existing UnityMCP registrations from all scopes before adding...");
|
||||
RemoveFromAllScopes(claudePath, projectDir, pathPrepend);
|
||||
|
||||
// Now add the registration
|
||||
if (!ExecPath.TryRun(claudePath, args, projectDir, out var stdout, out var stderr, 15000, pathPrepend))
|
||||
|
|
@ -670,10 +846,9 @@ namespace MCPForUnity.Editor.Clients
|
|||
throw new InvalidOperationException("Claude CLI not found. Please install Claude Code first.");
|
||||
}
|
||||
|
||||
// Remove both "UnityMCP" and "unityMCP" (legacy naming)
|
||||
McpLog.Info("Removing all UnityMCP registrations...");
|
||||
ExecPath.TryRun(claudePath, "mcp remove UnityMCP", projectDir, out _, out _, 7000, pathPrepend);
|
||||
ExecPath.TryRun(claudePath, "mcp remove unityMCP", projectDir, out _, out _, 7000, pathPrepend);
|
||||
// Remove from ALL scopes to ensure complete cleanup (#664)
|
||||
McpLog.Info("Removing all UnityMCP registrations from all scopes...");
|
||||
RemoveFromAllScopes(claudePath, projectDir, pathPrepend);
|
||||
|
||||
McpLog.Info("MCP server successfully unregistered from Claude Code.");
|
||||
client.SetStatus(McpStatus.NotConfigured);
|
||||
|
|
@ -696,31 +871,34 @@ namespace MCPForUnity.Editor.Clients
|
|||
{
|
||||
string httpUrl = HttpEndpointUtility.GetMcpRpcUrl();
|
||||
// Only include API key header for remote-hosted mode
|
||||
// Use --scope local to register in the project-local config, avoiding conflicts with user-level config (#664)
|
||||
if (HttpEndpointUtility.IsRemoteScope())
|
||||
{
|
||||
string apiKey = EditorPrefs.GetString(EditorPrefKeys.ApiKey, string.Empty);
|
||||
if (!string.IsNullOrEmpty(apiKey))
|
||||
{
|
||||
string safeKey = SanitizeShellHeaderValue(apiKey);
|
||||
args = $"mcp add --transport http UnityMCP {httpUrl} --header \"{AuthConstants.ApiKeyHeader}: {safeKey}\"";
|
||||
args = $"mcp add --scope local --transport http UnityMCP {httpUrl} --header \"{AuthConstants.ApiKeyHeader}: {safeKey}\"";
|
||||
}
|
||||
else
|
||||
{
|
||||
args = $"mcp add --transport http UnityMCP {httpUrl}";
|
||||
args = $"mcp add --scope local --transport http UnityMCP {httpUrl}";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
args = $"mcp add --transport http UnityMCP {httpUrl}";
|
||||
args = $"mcp add --scope local --transport http UnityMCP {httpUrl}";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var (uvxPath, gitUrl, packageName) = AssetPathUtility.GetUvxCommandParts();
|
||||
var (uvxPath, _, packageName) = AssetPathUtility.GetUvxCommandParts();
|
||||
// Use central helper that checks both DevModeForceServerRefresh AND local path detection.
|
||||
// Note: --reinstall is not supported by uvx, use --no-cache --refresh instead
|
||||
string devFlags = AssetPathUtility.ShouldForceUvxRefresh() ? "--no-cache --refresh " : string.Empty;
|
||||
args = $"mcp add --transport stdio UnityMCP -- \"{uvxPath}\" {devFlags}--from \"{gitUrl}\" {packageName}";
|
||||
string fromArgs = AssetPathUtility.GetBetaServerFromArgs(quoteFromPath: true);
|
||||
// Use --scope local to register in the project-local config, avoiding conflicts with user-level config (#664)
|
||||
args = $"mcp add --scope local --transport stdio UnityMCP -- \"{uvxPath}\" {devFlags}{fromArgs} {packageName}";
|
||||
}
|
||||
|
||||
string projectDir = Path.GetDirectoryName(Application.dataPath);
|
||||
|
|
@ -747,10 +925,9 @@ namespace MCPForUnity.Editor.Clients
|
|||
}
|
||||
catch { }
|
||||
|
||||
// Remove any existing registrations - handle both "UnityMCP" and "unityMCP" (legacy)
|
||||
McpLog.Info("Removing any existing UnityMCP registrations before adding...");
|
||||
ExecPath.TryRun(claudePath, "mcp remove UnityMCP", projectDir, out _, out _, 7000, pathPrepend);
|
||||
ExecPath.TryRun(claudePath, "mcp remove unityMCP", projectDir, out _, out _, 7000, pathPrepend);
|
||||
// Remove any existing registrations from ALL scopes to prevent stale config conflicts (#664)
|
||||
McpLog.Info("Removing any existing UnityMCP registrations from all scopes before adding...");
|
||||
RemoveFromAllScopes(claudePath, projectDir, pathPrepend);
|
||||
|
||||
// Now add the registration with the current transport mode
|
||||
if (!ExecPath.TryRun(claudePath, args, projectDir, out var stdout, out var stderr, 15000, pathPrepend))
|
||||
|
|
@ -787,10 +964,9 @@ namespace MCPForUnity.Editor.Clients
|
|||
pathPrepend = "/usr/local/bin:/usr/bin:/bin";
|
||||
}
|
||||
|
||||
// Remove both "UnityMCP" and "unityMCP" (legacy naming)
|
||||
McpLog.Info("Removing all UnityMCP registrations...");
|
||||
ExecPath.TryRun(claudePath, "mcp remove UnityMCP", projectDir, out _, out _, 7000, pathPrepend);
|
||||
ExecPath.TryRun(claudePath, "mcp remove unityMCP", projectDir, out _, out _, 7000, pathPrepend);
|
||||
// Remove from ALL scopes to ensure complete cleanup (#664)
|
||||
McpLog.Info("Removing all UnityMCP registrations from all scopes...");
|
||||
RemoveFromAllScopes(claudePath, projectDir, pathPrepend);
|
||||
|
||||
McpLog.Info("MCP server successfully unregistered from Claude Code.");
|
||||
client.SetStatus(McpStatus.NotConfigured);
|
||||
|
|
@ -813,9 +989,11 @@ namespace MCPForUnity.Editor.Clients
|
|||
headerArg = !string.IsNullOrEmpty(apiKey) ? $" --header \"{AuthConstants.ApiKeyHeader}: {SanitizeShellHeaderValue(apiKey)}\"" : "";
|
||||
}
|
||||
return "# Register the MCP server with Claude Code:\n" +
|
||||
$"claude mcp add --transport http UnityMCP {httpUrl}{headerArg}\n\n" +
|
||||
"# Unregister the MCP server:\n" +
|
||||
"claude mcp remove UnityMCP\n\n" +
|
||||
$"claude mcp add --scope local --transport http UnityMCP {httpUrl}{headerArg}\n\n" +
|
||||
"# Unregister the MCP server (from all scopes to clean up any stale configs):\n" +
|
||||
"claude mcp remove --scope local UnityMCP\n" +
|
||||
"claude mcp remove --scope user UnityMCP\n" +
|
||||
"claude mcp remove --scope project UnityMCP\n\n" +
|
||||
"# List registered servers:\n" +
|
||||
"claude mcp list";
|
||||
}
|
||||
|
|
@ -825,15 +1003,17 @@ namespace MCPForUnity.Editor.Clients
|
|||
return "# Error: Configuration not available - check paths in Advanced Settings";
|
||||
}
|
||||
|
||||
string packageSource = AssetPathUtility.GetMcpServerPackageSource();
|
||||
// Use central helper that checks both DevModeForceServerRefresh AND local path detection.
|
||||
// Note: --reinstall is not supported by uvx, use --no-cache --refresh instead
|
||||
string devFlags = AssetPathUtility.ShouldForceUvxRefresh() ? "--no-cache --refresh " : string.Empty;
|
||||
string fromArgs = AssetPathUtility.GetBetaServerFromArgs(quoteFromPath: true);
|
||||
|
||||
return "# Register the MCP server with Claude Code:\n" +
|
||||
$"claude mcp add --transport stdio UnityMCP -- \"{uvxPath}\" {devFlags}--from \"{packageSource}\" mcp-for-unity\n\n" +
|
||||
"# Unregister the MCP server:\n" +
|
||||
"claude mcp remove UnityMCP\n\n" +
|
||||
$"claude mcp add --scope local --transport stdio UnityMCP -- \"{uvxPath}\" {devFlags}{fromArgs} mcp-for-unity\n\n" +
|
||||
"# Unregister the MCP server (from all scopes to clean up any stale configs):\n" +
|
||||
"claude mcp remove --scope local UnityMCP\n" +
|
||||
"claude mcp remove --scope user UnityMCP\n" +
|
||||
"claude mcp remove --scope project UnityMCP\n\n" +
|
||||
"# List registered servers:\n" +
|
||||
"claude mcp list";
|
||||
}
|
||||
|
|
@ -841,10 +1021,32 @@ namespace MCPForUnity.Editor.Clients
|
|||
public override IList<string> GetInstallationSteps() => new List<string>
|
||||
{
|
||||
"Ensure Claude CLI is installed",
|
||||
"Use Register to add UnityMCP (or run claude mcp add UnityMCP)",
|
||||
"Use Configure to add UnityMCP (or run claude mcp add UnityMCP)",
|
||||
"Restart Claude Code"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Removes UnityMCP registration from all Claude Code configuration scopes (local, user, project).
|
||||
/// This ensures no stale or conflicting configurations remain across different scopes.
|
||||
/// Also handles legacy "unityMCP" naming convention.
|
||||
/// </summary>
|
||||
private static void RemoveFromAllScopes(string claudePath, string projectDir, string pathPrepend)
|
||||
{
|
||||
// Remove from all three scopes to prevent stale configs causing connection issues.
|
||||
// See GitHub issue #664 - conflicting configs at different scopes can cause
|
||||
// Claude Code to connect with outdated/incorrect configuration.
|
||||
string[] scopes = { "local", "user", "project" };
|
||||
string[] names = { "UnityMCP", "unityMCP" }; // Include legacy naming
|
||||
|
||||
foreach (var scope in scopes)
|
||||
{
|
||||
foreach (var name in names)
|
||||
{
|
||||
ExecPath.TryRun(claudePath, $"mcp remove --scope {scope} {name}", projectDir, out _, out _, 5000, pathPrepend);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sanitizes a value for safe inclusion inside a double-quoted shell argument.
|
||||
/// Escapes characters that are special within double quotes (", \, `, $, !)
|
||||
|
|
@ -921,5 +1123,134 @@ namespace MCPForUnity.Editor.Clients
|
|||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads Claude Code configuration directly from ~/.claude.json file.
|
||||
/// This is much faster than running `claude mcp list` which does health checks on all servers.
|
||||
/// </summary>
|
||||
private static (JObject serverConfig, string error) ReadClaudeCodeConfig(string projectDir)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Find the Claude config file
|
||||
string homeDir = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
|
||||
string configPath = Path.Combine(homeDir, ".claude.json");
|
||||
|
||||
if (!File.Exists(configPath))
|
||||
{
|
||||
// Missing config file is "not configured", not an error
|
||||
// (Claude Code may not be installed or just hasn't been configured yet)
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
string configJson = File.ReadAllText(configPath);
|
||||
var config = JObject.Parse(configJson);
|
||||
|
||||
var projects = config["projects"] as JObject;
|
||||
if (projects == null)
|
||||
{
|
||||
return (null, null); // No projects configured
|
||||
}
|
||||
|
||||
// Build a dictionary of normalized paths for quick lookup
|
||||
// Use last entry for duplicates (forward/backslash variants) as it's typically more recent
|
||||
var normalizedProjects = new Dictionary<string, JObject>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var project in projects.Properties())
|
||||
{
|
||||
string normalizedPath = NormalizePath(project.Name);
|
||||
normalizedProjects[normalizedPath] = project.Value as JObject;
|
||||
}
|
||||
|
||||
// Walk up the directory tree to find a matching project config
|
||||
// Claude Code may be configured at a parent directory (e.g., repo root)
|
||||
// while Unity project is in a subdirectory (e.g., TestProjects/UnityMCPTests)
|
||||
string currentDir = NormalizePath(projectDir);
|
||||
while (!string.IsNullOrEmpty(currentDir))
|
||||
{
|
||||
if (normalizedProjects.TryGetValue(currentDir, out var projectConfig))
|
||||
{
|
||||
var mcpServers = projectConfig?["mcpServers"] as JObject;
|
||||
if (mcpServers != null)
|
||||
{
|
||||
// Look for UnityMCP (case-insensitive)
|
||||
foreach (var server in mcpServers.Properties())
|
||||
{
|
||||
if (string.Equals(server.Name, "UnityMCP", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return (server.Value as JObject, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Found the project but no UnityMCP - don't continue walking up
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
// Move up one directory
|
||||
int lastSlash = currentDir.LastIndexOf('/');
|
||||
if (lastSlash <= 0)
|
||||
break;
|
||||
currentDir = currentDir.Substring(0, lastSlash);
|
||||
}
|
||||
|
||||
return (null, null); // Project not found in config
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return (null, $"Error reading Claude config: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes a file path for comparison (handles forward/back slashes, trailing slashes).
|
||||
/// </summary>
|
||||
private static string NormalizePath(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
return path;
|
||||
|
||||
// Replace backslashes with forward slashes and remove trailing slashes
|
||||
return path.Replace('\\', '/').TrimEnd('/');
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the package source from Claude Code JSON config.
|
||||
/// For stdio servers, this is in the args array after "--from".
|
||||
/// </summary>
|
||||
private static string ExtractPackageSourceFromConfig(JObject serverConfig)
|
||||
{
|
||||
if (serverConfig == null)
|
||||
return null;
|
||||
|
||||
var args = serverConfig["args"] as JArray;
|
||||
if (args == null)
|
||||
return null;
|
||||
|
||||
// Look for --from argument (either "--from VALUE" or "--from=VALUE" format)
|
||||
bool foundFrom = false;
|
||||
foreach (var arg in args)
|
||||
{
|
||||
string argStr = arg?.ToString();
|
||||
if (argStr == null)
|
||||
continue;
|
||||
|
||||
if (foundFrom)
|
||||
{
|
||||
// This is the package source following --from
|
||||
return argStr;
|
||||
}
|
||||
|
||||
if (argStr == "--from")
|
||||
{
|
||||
foundFrom = true;
|
||||
}
|
||||
else if (argStr.StartsWith("--from=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Handle --from=VALUE format
|
||||
return argStr.Substring(7).Trim('"', '\'');
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ namespace MCPForUnity.Editor.Constants
|
|||
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 SetupCompleted = "MCPForUnity.SetupCompleted";
|
||||
internal const string SetupDismissed = "MCPForUnity.SetupDismissed";
|
||||
|
|
|
|||
|
|
@ -248,21 +248,38 @@ namespace MCPForUnity.Editor.Helpers
|
|||
/// Handles beta server mode (prerelease from PyPI) vs standard mode (pinned version or override).
|
||||
/// Centralizes the prerelease logic to avoid duplication between HTTP and stdio transports.
|
||||
/// Priority: explicit fromUrl override > beta server mode > default package.
|
||||
/// NOTE: This overload reads from EditorPrefs/cache and MUST be called from the main thread.
|
||||
/// For background threads, use the overload that accepts pre-captured parameters.
|
||||
/// </summary>
|
||||
/// <param name="quoteFromPath">Whether to quote the --from path (needed for command-line strings, not for arg lists)</param>
|
||||
/// <returns>The package source arguments (e.g., "--prerelease explicit --from mcpforunityserver>=0.0.0a0")</returns>
|
||||
public static string GetBetaServerFromArgs(bool quoteFromPath = false)
|
||||
{
|
||||
// Read values from cache/EditorPrefs on main thread
|
||||
bool useBetaServer = Services.EditorConfigurationCache.Instance.UseBetaServer;
|
||||
string gitUrlOverride = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, "");
|
||||
string packageSource = GetMcpServerPackageSource();
|
||||
return GetBetaServerFromArgs(useBetaServer, gitUrlOverride, packageSource, quoteFromPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Thread-safe overload that accepts pre-captured values.
|
||||
/// Use this when calling from background threads.
|
||||
/// </summary>
|
||||
/// <param name="useBetaServer">Pre-captured value from EditorConfigurationCache.Instance.UseBetaServer</param>
|
||||
/// <param name="gitUrlOverride">Pre-captured value from EditorPrefs GitUrlOverride</param>
|
||||
/// <param name="packageSource">Pre-captured value from GetMcpServerPackageSource()</param>
|
||||
/// <param name="quoteFromPath">Whether to quote the --from path</param>
|
||||
public static string GetBetaServerFromArgs(bool useBetaServer, string gitUrlOverride, string packageSource, bool quoteFromPath = false)
|
||||
{
|
||||
// Explicit override (local path, git URL, etc.) always wins
|
||||
string fromUrl = GetMcpServerPackageSource();
|
||||
string overrideUrl = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, "");
|
||||
if (!string.IsNullOrEmpty(overrideUrl))
|
||||
if (!string.IsNullOrEmpty(gitUrlOverride))
|
||||
{
|
||||
return $"--from {fromUrl}";
|
||||
string fromValue = quoteFromPath ? $"\"{gitUrlOverride}\"" : gitUrlOverride;
|
||||
return $"--from {fromValue}";
|
||||
}
|
||||
|
||||
// Beta server mode: use prerelease from PyPI
|
||||
bool useBetaServer = EditorPrefs.GetBool(EditorPrefKeys.UseBetaServer, true);
|
||||
if (useBetaServer)
|
||||
{
|
||||
// Use --prerelease explicit with version specifier to only get prereleases of our package,
|
||||
|
|
@ -272,9 +289,10 @@ namespace MCPForUnity.Editor.Helpers
|
|||
}
|
||||
|
||||
// Standard mode: use pinned version from package.json
|
||||
if (!string.IsNullOrEmpty(fromUrl))
|
||||
if (!string.IsNullOrEmpty(packageSource))
|
||||
{
|
||||
return $"--from {fromUrl}";
|
||||
string fromValue = quoteFromPath ? $"\"{packageSource}\"" : packageSource;
|
||||
return $"--from {fromValue}";
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
|
|
@ -283,24 +301,39 @@ namespace MCPForUnity.Editor.Helpers
|
|||
/// <summary>
|
||||
/// Builds the uvx package source arguments as a list (for JSON config builders).
|
||||
/// Priority: explicit fromUrl override > beta server mode > default package.
|
||||
/// NOTE: This overload reads from EditorPrefs/cache and MUST be called from the main thread.
|
||||
/// For background threads, use the overload that accepts pre-captured parameters.
|
||||
/// </summary>
|
||||
/// <returns>List of arguments to add to uvx command</returns>
|
||||
public static System.Collections.Generic.IList<string> GetBetaServerFromArgsList()
|
||||
{
|
||||
// Read values from cache/EditorPrefs on main thread
|
||||
bool useBetaServer = Services.EditorConfigurationCache.Instance.UseBetaServer;
|
||||
string gitUrlOverride = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, "");
|
||||
string packageSource = GetMcpServerPackageSource();
|
||||
return GetBetaServerFromArgsList(useBetaServer, gitUrlOverride, packageSource);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Thread-safe overload that accepts pre-captured values.
|
||||
/// Use this when calling from background threads.
|
||||
/// </summary>
|
||||
/// <param name="useBetaServer">Pre-captured value from EditorConfigurationCache.Instance.UseBetaServer</param>
|
||||
/// <param name="gitUrlOverride">Pre-captured value from EditorPrefs GitUrlOverride</param>
|
||||
/// <param name="packageSource">Pre-captured value from GetMcpServerPackageSource()</param>
|
||||
public static System.Collections.Generic.IList<string> GetBetaServerFromArgsList(bool useBetaServer, string gitUrlOverride, string packageSource)
|
||||
{
|
||||
var args = new System.Collections.Generic.List<string>();
|
||||
|
||||
// Explicit override (local path, git URL, etc.) always wins
|
||||
string fromUrl = GetMcpServerPackageSource();
|
||||
string overrideUrl = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, "");
|
||||
if (!string.IsNullOrEmpty(overrideUrl))
|
||||
if (!string.IsNullOrEmpty(gitUrlOverride))
|
||||
{
|
||||
args.Add("--from");
|
||||
args.Add(fromUrl);
|
||||
args.Add(gitUrlOverride);
|
||||
return args;
|
||||
}
|
||||
|
||||
// Beta server mode: use prerelease from PyPI
|
||||
bool useBetaServer = EditorPrefs.GetBool(EditorPrefKeys.UseBetaServer, true);
|
||||
if (useBetaServer)
|
||||
{
|
||||
args.Add("--prerelease");
|
||||
|
|
@ -311,10 +344,10 @@ namespace MCPForUnity.Editor.Helpers
|
|||
}
|
||||
|
||||
// Standard mode: use pinned version from package.json
|
||||
if (!string.IsNullOrEmpty(fromUrl))
|
||||
if (!string.IsNullOrEmpty(packageSource))
|
||||
{
|
||||
args.Add("--from");
|
||||
args.Add(fromUrl);
|
||||
args.Add(packageSource);
|
||||
}
|
||||
|
||||
return args;
|
||||
|
|
@ -426,5 +459,31 @@ namespace MCPForUnity.Editor.Helpers
|
|||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the installed package version is a prerelease (beta, alpha, rc, etc.).
|
||||
/// Used to auto-enable beta server mode for beta package users.
|
||||
/// </summary>
|
||||
public static bool IsPreReleaseVersion()
|
||||
{
|
||||
try
|
||||
{
|
||||
string version = GetPackageVersion();
|
||||
if (string.IsNullOrEmpty(version) || version == "unknown")
|
||||
return false;
|
||||
|
||||
// Check for common prerelease indicators in semver format
|
||||
// e.g., "9.3.0-beta.1", "9.3.0-alpha", "9.3.0-rc.2", "9.3.0-preview"
|
||||
return version.Contains("-beta", StringComparison.OrdinalIgnoreCase) ||
|
||||
version.Contains("-alpha", StringComparison.OrdinalIgnoreCase) ||
|
||||
version.Contains("-rc", StringComparison.OrdinalIgnoreCase) ||
|
||||
version.Contains("-preview", StringComparison.OrdinalIgnoreCase) ||
|
||||
version.Contains("-pre", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ namespace MCPForUnity.Editor.Models
|
|||
McpStatus.UnsupportedOS => "Unsupported OS",
|
||||
McpStatus.MissingConfig => "Missing MCPForUnity Config",
|
||||
McpStatus.Error => configStatus?.StartsWith("Error:") == true ? configStatus : "Error",
|
||||
McpStatus.VersionMismatch => "Version Mismatch",
|
||||
_ => "Unknown",
|
||||
};
|
||||
}
|
||||
|
|
@ -44,9 +45,9 @@ namespace MCPForUnity.Editor.Models
|
|||
{
|
||||
status = newStatus;
|
||||
|
||||
if (newStatus == McpStatus.Error && !string.IsNullOrEmpty(errorDetails))
|
||||
if ((newStatus == McpStatus.Error || newStatus == McpStatus.VersionMismatch) && !string.IsNullOrEmpty(errorDetails))
|
||||
{
|
||||
configStatus = $"Error: {errorDetails}";
|
||||
configStatus = errorDetails;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ namespace MCPForUnity.Editor.Models
|
|||
MissingConfig, // Config file exists but missing required elements
|
||||
UnsupportedOS, // OS is not supported
|
||||
Error, // General error state
|
||||
VersionMismatch, // Configuration version doesn't match expected version
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -124,6 +124,23 @@ namespace MCPForUnity.Editor.Services
|
|||
/// </summary>
|
||||
public int UnitySocketPort => _unitySocketPort;
|
||||
|
||||
/// <summary>
|
||||
/// Gets UseBetaServer value with dynamic default based on package version.
|
||||
/// If the pref hasn't been explicitly set, defaults to true for prerelease packages
|
||||
/// (beta, alpha, rc, etc.) and false for stable releases.
|
||||
/// </summary>
|
||||
private static bool GetUseBetaServerWithDynamicDefault()
|
||||
{
|
||||
// If user has explicitly set the pref, use that value
|
||||
if (EditorPrefs.HasKey(EditorPrefKeys.UseBetaServer))
|
||||
{
|
||||
return EditorPrefs.GetBool(EditorPrefKeys.UseBetaServer, false);
|
||||
}
|
||||
|
||||
// Otherwise, default based on whether this is a prerelease package
|
||||
return Helpers.AssetPathUtility.IsPreReleaseVersion();
|
||||
}
|
||||
|
||||
private EditorConfigurationCache()
|
||||
{
|
||||
Refresh();
|
||||
|
|
@ -137,7 +154,7 @@ namespace MCPForUnity.Editor.Services
|
|||
{
|
||||
_useHttpTransport = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true);
|
||||
_debugLogs = EditorPrefs.GetBool(EditorPrefKeys.DebugLogs, false);
|
||||
_useBetaServer = EditorPrefs.GetBool(EditorPrefKeys.UseBetaServer, true);
|
||||
_useBetaServer = GetUseBetaServerWithDynamicDefault();
|
||||
_devModeForceServerRefresh = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false);
|
||||
_uvxPathOverride = EditorPrefs.GetString(EditorPrefKeys.UvxPathOverride, string.Empty);
|
||||
_gitUrlOverride = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, string.Empty);
|
||||
|
|
@ -312,7 +329,7 @@ namespace MCPForUnity.Editor.Services
|
|||
_debugLogs = EditorPrefs.GetBool(EditorPrefKeys.DebugLogs, false);
|
||||
break;
|
||||
case nameof(UseBetaServer):
|
||||
_useBetaServer = EditorPrefs.GetBool(EditorPrefKeys.UseBetaServer, true);
|
||||
_useBetaServer = GetUseBetaServerWithDynamicDefault();
|
||||
break;
|
||||
case nameof(DevModeForceServerRefresh):
|
||||
_devModeForceServerRefresh = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
|
@ -21,13 +20,6 @@ using UnityEngine;
|
|||
|
||||
namespace MCPForUnity.Editor.Services.Transport.Transports
|
||||
{
|
||||
class Outbound
|
||||
{
|
||||
public byte[] Payload;
|
||||
public string Tag;
|
||||
public int? ReqId;
|
||||
}
|
||||
|
||||
class QueuedCommand
|
||||
{
|
||||
public string CommandJson;
|
||||
|
|
@ -44,7 +36,6 @@ namespace MCPForUnity.Editor.Services.Transport.Transports
|
|||
private static readonly object startStopLock = new();
|
||||
private static readonly object clientsLock = new();
|
||||
private static readonly HashSet<TcpClient> activeClients = new();
|
||||
private static readonly BlockingCollection<Outbound> _outbox = new(new ConcurrentQueue<Outbound>());
|
||||
private static CancellationTokenSource cts;
|
||||
private static Task listenerTask;
|
||||
private static int processingCommands = 0;
|
||||
|
|
@ -61,7 +52,6 @@ namespace MCPForUnity.Editor.Services.Transport.Transports
|
|||
private const ulong MaxFrameBytes = 64UL * 1024 * 1024;
|
||||
private const int FrameIOTimeoutMs = 30000;
|
||||
|
||||
private static long _ioSeq = 0;
|
||||
private static void IoInfo(string s) { McpLog.Info(s, always: false); }
|
||||
|
||||
private static bool IsDebugEnabled()
|
||||
|
|
@ -123,30 +113,6 @@ namespace MCPForUnity.Editor.Services.Transport.Transports
|
|||
static StdioBridgeHost()
|
||||
{
|
||||
try { mainThreadId = Thread.CurrentThread.ManagedThreadId; } catch { mainThreadId = 0; }
|
||||
try
|
||||
{
|
||||
var writerThread = new Thread(() =>
|
||||
{
|
||||
foreach (var item in _outbox.GetConsumingEnumerable())
|
||||
{
|
||||
try
|
||||
{
|
||||
long seq = Interlocked.Increment(ref _ioSeq);
|
||||
IoInfo($"[IO] ➜ write start seq={seq} tag={item.Tag} len={(item.Payload?.Length ?? 0)} reqId={(item.ReqId?.ToString() ?? "?")}");
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
sw.Stop();
|
||||
IoInfo($"[IO] ✓ write end tag={item.Tag} len={(item.Payload?.Length ?? 0)} reqId={(item.ReqId?.ToString() ?? "?")} durMs={sw.Elapsed.TotalMilliseconds:F1}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
IoInfo($"[IO] ✗ write FAIL tag={item.Tag} reqId={(item.ReqId?.ToString() ?? "?")} {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
})
|
||||
{ IsBackground = true, Name = "MCP-Writer" };
|
||||
writerThread.Start();
|
||||
}
|
||||
catch { }
|
||||
|
||||
if (Application.isBatchMode && string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("UNITY_MCP_ALLOW_BATCH")))
|
||||
{
|
||||
|
|
@ -633,12 +599,10 @@ namespace MCPForUnity.Editor.Services.Transport.Transports
|
|||
{
|
||||
try { McpLog.Info("[MCP] sending framed response", always: false); } catch { }
|
||||
}
|
||||
long seq = Interlocked.Increment(ref _ioSeq);
|
||||
byte[] responseBytes;
|
||||
try
|
||||
{
|
||||
responseBytes = System.Text.Encoding.UTF8.GetBytes(response);
|
||||
IoInfo($"[IO] ➜ write start seq={seq} tag=response len={responseBytes.Length} reqId=?");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
@ -646,12 +610,9 @@ namespace MCPForUnity.Editor.Services.Transport.Transports
|
|||
throw;
|
||||
}
|
||||
|
||||
var swDirect = System.Diagnostics.Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
await WriteFrameAsync(stream, responseBytes);
|
||||
swDirect.Stop();
|
||||
IoInfo($"[IO] ✓ write end tag=response len={responseBytes.Length} reqId=? durMs={swDirect.Elapsed.TotalMilliseconds:F1}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@ namespace MCPForUnity.Editor.Windows.Components.Advanced
|
|||
McpLog.SetDebugLoggingEnabled(debugEnabled);
|
||||
|
||||
devModeForceRefreshToggle.value = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false);
|
||||
useBetaServerToggle.value = EditorPrefs.GetBool(EditorPrefKeys.UseBetaServer, true);
|
||||
useBetaServerToggle.value = EditorConfigurationCache.Instance.UseBetaServer;
|
||||
UpdatePathOverrides();
|
||||
UpdateDeploymentSection();
|
||||
}
|
||||
|
|
@ -185,7 +185,7 @@ namespace MCPForUnity.Editor.Windows.Components.Advanced
|
|||
|
||||
useBetaServerToggle.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
EditorPrefs.SetBool(EditorPrefKeys.UseBetaServer, evt.newValue);
|
||||
EditorConfigurationCache.Instance.SetUseBetaServer(evt.newValue);
|
||||
OnHttpServerCommandUpdateRequested?.Invoke();
|
||||
OnBetaModeChanged?.Invoke(evt.newValue);
|
||||
});
|
||||
|
|
@ -292,7 +292,7 @@ namespace MCPForUnity.Editor.Windows.Components.Advanced
|
|||
gitUrlOverride.value = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, "");
|
||||
debugLogsToggle.value = EditorPrefs.GetBool(EditorPrefKeys.DebugLogs, false);
|
||||
devModeForceRefreshToggle.value = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false);
|
||||
useBetaServerToggle.value = EditorPrefs.GetBool(EditorPrefKeys.UseBetaServer, true);
|
||||
useBetaServerToggle.value = EditorConfigurationCache.Instance.UseBetaServer;
|
||||
UpdateDeploymentSection();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -53,6 +53,12 @@ namespace MCPForUnity.Editor.Windows.Components.ClientConfig
|
|||
/// </summary>
|
||||
public event Action<string, ConfiguredTransport> OnClientTransportDetected;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when a config mismatch is detected (e.g., version mismatch).
|
||||
/// The parameter contains the client name and the mismatch message (null if no mismatch).
|
||||
/// </summary>
|
||||
public event Action<string, string> OnClientConfigMismatch;
|
||||
|
||||
public VisualElement Root { get; private set; }
|
||||
|
||||
public McpClientConfigSection(VisualElement root)
|
||||
|
|
@ -95,7 +101,22 @@ namespace MCPForUnity.Editor.Windows.Components.ClientConfig
|
|||
clientDropdown.choices = clientNames;
|
||||
if (clientNames.Count > 0)
|
||||
{
|
||||
clientDropdown.index = 0;
|
||||
// Restore last selected client from EditorPrefs
|
||||
string lastClientId = EditorPrefs.GetString(EditorPrefKeys.LastSelectedClientId, string.Empty);
|
||||
int restoredIndex = 0;
|
||||
if (!string.IsNullOrEmpty(lastClientId))
|
||||
{
|
||||
for (int i = 0; i < configurators.Count; i++)
|
||||
{
|
||||
if (string.Equals(configurators[i].Id, lastClientId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
restoredIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
clientDropdown.index = restoredIndex;
|
||||
selectedClientIndex = restoredIndex;
|
||||
}
|
||||
|
||||
claudeCliPathRow.style.display = DisplayStyle.None;
|
||||
|
|
@ -111,6 +132,11 @@ namespace MCPForUnity.Editor.Windows.Components.ClientConfig
|
|||
clientDropdown.RegisterValueChangedCallback(evt =>
|
||||
{
|
||||
selectedClientIndex = clientDropdown.index;
|
||||
// Persist the selected client so it's restored on next window open
|
||||
if (selectedClientIndex >= 0 && selectedClientIndex < configurators.Count)
|
||||
{
|
||||
EditorPrefs.SetString(EditorPrefKeys.LastSelectedClientId, configurators[selectedClientIndex].Id);
|
||||
}
|
||||
UpdateClientStatus();
|
||||
UpdateManualConfiguration();
|
||||
UpdateClaudeCliPathVisibility();
|
||||
|
|
@ -147,6 +173,7 @@ namespace MCPForUnity.Editor.Windows.Components.ClientConfig
|
|||
McpStatus.UnsupportedOS => "Unsupported OS",
|
||||
McpStatus.MissingConfig => "Missing MCPForUnity Config",
|
||||
McpStatus.Error => "Error",
|
||||
McpStatus.VersionMismatch => "Version Mismatch",
|
||||
_ => "Unknown",
|
||||
};
|
||||
}
|
||||
|
|
@ -266,14 +293,15 @@ namespace MCPForUnity.Editor.Windows.Components.ClientConfig
|
|||
|
||||
statusRefreshInFlight.Add(client);
|
||||
bool isCurrentlyConfigured = client.Status == McpStatus.Configured;
|
||||
ApplyStatusToUi(client, showChecking: true, customMessage: isCurrentlyConfigured ? "Unregistering..." : "Registering...");
|
||||
ApplyStatusToUi(client, showChecking: true, customMessage: isCurrentlyConfigured ? "Unregistering..." : "Configuring...");
|
||||
|
||||
// Capture ALL main-thread-only values before async task
|
||||
string projectDir = Path.GetDirectoryName(Application.dataPath);
|
||||
bool useHttpTransport = EditorConfigurationCache.Instance.UseHttpTransport;
|
||||
string claudePath = MCPServiceLocator.Paths.GetClaudeCliPath();
|
||||
string httpUrl = HttpEndpointUtility.GetMcpRpcUrl();
|
||||
var (uvxPath, gitUrl, packageName) = AssetPathUtility.GetUvxCommandParts();
|
||||
var (uvxPath, _, packageName) = AssetPathUtility.GetUvxCommandParts();
|
||||
string fromArgs = AssetPathUtility.GetBetaServerFromArgs(quoteFromPath: true);
|
||||
bool shouldForceRefresh = AssetPathUtility.ShouldForceUvxRefresh();
|
||||
string apiKey = EditorPrefs.GetString(EditorPrefKeys.ApiKey, string.Empty);
|
||||
|
||||
|
|
@ -301,7 +329,7 @@ namespace MCPForUnity.Editor.Windows.Components.ClientConfig
|
|||
cliConfigurator.ConfigureWithCapturedValues(
|
||||
projectDir, claudePath, pathPrepend,
|
||||
useHttpTransport, httpUrl,
|
||||
uvxPath, gitUrl, packageName, shouldForceRefresh,
|
||||
uvxPath, fromArgs, packageName, shouldForceRefresh,
|
||||
apiKey, serverTransport);
|
||||
}
|
||||
return (success: true, error: (string)null);
|
||||
|
|
@ -458,6 +486,10 @@ namespace MCPForUnity.Editor.Windows.Components.ClientConfig
|
|||
string projectDir = Path.GetDirectoryName(Application.dataPath);
|
||||
bool useHttpTransport = EditorConfigurationCache.Instance.UseHttpTransport;
|
||||
string claudePath = MCPServiceLocator.Paths.GetClaudeCliPath();
|
||||
RuntimePlatform platform = Application.platform;
|
||||
bool isRemoteScope = HttpEndpointUtility.IsRemoteScope();
|
||||
// Get expected package source considering beta mode (bypass cache for fresh read)
|
||||
string expectedPackageSource = GetExpectedPackageSourceForBetaMode();
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
|
|
@ -466,7 +498,7 @@ namespace MCPForUnity.Editor.Windows.Components.ClientConfig
|
|||
if (client is ClaudeCliMcpConfigurator claudeConfigurator)
|
||||
{
|
||||
// Use thread-safe version with captured main-thread values
|
||||
claudeConfigurator.CheckStatusWithProjectDir(projectDir, useHttpTransport, claudePath, attemptAutoRewrite: false);
|
||||
claudeConfigurator.CheckStatusWithProjectDir(projectDir, useHttpTransport, claudePath, platform, isRemoteScope, expectedPackageSource, attemptAutoRewrite: false);
|
||||
}
|
||||
}).ContinueWith(t =>
|
||||
{
|
||||
|
|
@ -556,6 +588,8 @@ namespace MCPForUnity.Editor.Windows.Components.ClientConfig
|
|||
case McpStatus.IncorrectPath:
|
||||
case McpStatus.CommunicationError:
|
||||
case McpStatus.NoResponse:
|
||||
case McpStatus.Error:
|
||||
case McpStatus.VersionMismatch:
|
||||
clientStatusIndicator.AddToClassList("warning");
|
||||
break;
|
||||
default:
|
||||
|
|
@ -569,6 +603,55 @@ namespace MCPForUnity.Editor.Windows.Components.ClientConfig
|
|||
|
||||
// Notify listeners about the client's configured transport
|
||||
OnClientTransportDetected?.Invoke(client.DisplayName, client.ConfiguredTransport);
|
||||
|
||||
// Notify listeners about version mismatch if applicable
|
||||
if (client.Status == McpStatus.VersionMismatch && client is McpClientConfiguratorBase baseConfigurator)
|
||||
{
|
||||
// Get the mismatch reason from the configStatus field
|
||||
string mismatchReason = baseConfigurator.Client.configStatus;
|
||||
OnClientConfigMismatch?.Invoke(client.DisplayName, mismatchReason);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Clear any previous mismatch warning
|
||||
OnClientConfigMismatch?.Invoke(client.DisplayName, null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the expected package source for validation, accounting for beta mode.
|
||||
/// Uses the same logic as registration to ensure validation matches what was registered.
|
||||
/// MUST be called from the main thread due to EditorPrefs access.
|
||||
/// </summary>
|
||||
private static string GetExpectedPackageSourceForBetaMode()
|
||||
{
|
||||
// Check for explicit override first
|
||||
string gitUrlOverride = EditorPrefs.GetString(EditorPrefKeys.GitUrlOverride, "");
|
||||
if (!string.IsNullOrEmpty(gitUrlOverride))
|
||||
{
|
||||
return gitUrlOverride;
|
||||
}
|
||||
|
||||
// Check beta mode using the same logic as GetUseBetaServerWithDynamicDefault
|
||||
// (bypass cache to ensure fresh read)
|
||||
bool useBetaServer;
|
||||
if (EditorPrefs.HasKey(EditorPrefKeys.UseBetaServer))
|
||||
{
|
||||
useBetaServer = EditorPrefs.GetBool(EditorPrefKeys.UseBetaServer, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Dynamic default based on package version
|
||||
useBetaServer = AssetPathUtility.IsPreReleaseVersion();
|
||||
}
|
||||
|
||||
if (useBetaServer)
|
||||
{
|
||||
return "mcpforunityserver>=0.0.0a0";
|
||||
}
|
||||
|
||||
// Standard mode uses exact version from package.json
|
||||
return AssetPathUtility.GetMcpServerPackageSource();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ namespace MCPForUnity.Editor.Windows.Components.Connection
|
|||
private EnumField transportDropdown;
|
||||
private VisualElement transportMismatchWarning;
|
||||
private Label transportMismatchText;
|
||||
private VisualElement versionMismatchWarning;
|
||||
private Label versionMismatchText;
|
||||
private VisualElement httpUrlRow;
|
||||
private VisualElement httpServerControlRow;
|
||||
private Foldout manualCommandFoldout;
|
||||
|
|
@ -86,6 +88,8 @@ namespace MCPForUnity.Editor.Windows.Components.Connection
|
|||
transportDropdown = Root.Q<EnumField>("transport-dropdown");
|
||||
transportMismatchWarning = Root.Q<VisualElement>("transport-mismatch-warning");
|
||||
transportMismatchText = Root.Q<Label>("transport-mismatch-text");
|
||||
versionMismatchWarning = Root.Q<VisualElement>("version-mismatch-warning");
|
||||
versionMismatchText = Root.Q<Label>("version-mismatch-text");
|
||||
httpUrlRow = Root.Q<VisualElement>("http-url-row");
|
||||
httpServerControlRow = Root.Q<VisualElement>("http-server-control-row");
|
||||
manualCommandFoldout = Root.Q<Foldout>("manual-command-foldout");
|
||||
|
|
@ -1023,6 +1027,35 @@ namespace MCPForUnity.Editor.Windows.Components.Connection
|
|||
transportMismatchWarning?.RemoveFromClassList("visible");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the version mismatch warning banner based on the client's configuration status.
|
||||
/// Shows a warning if the client is registered with a different package version than expected.
|
||||
/// </summary>
|
||||
/// <param name="clientName">The display name of the client being checked.</param>
|
||||
/// <param name="mismatchMessage">The mismatch message, or null if no mismatch.</param>
|
||||
public void UpdateVersionMismatchWarning(string clientName, string mismatchMessage)
|
||||
{
|
||||
if (versionMismatchWarning == null || versionMismatchText == null)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrEmpty(mismatchMessage))
|
||||
{
|
||||
versionMismatchWarning.RemoveFromClassList("visible");
|
||||
return;
|
||||
}
|
||||
|
||||
versionMismatchText.text = $"⚠ {clientName}: {mismatchMessage}";
|
||||
versionMismatchWarning.AddToClassList("visible");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the version mismatch warning banner.
|
||||
/// </summary>
|
||||
public void ClearVersionMismatchWarning()
|
||||
{
|
||||
versionMismatchWarning?.RemoveFromClassList("visible");
|
||||
}
|
||||
|
||||
private static string TransportDisplayName(ConfiguredTransport transport)
|
||||
{
|
||||
return transport switch
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@
|
|||
<ui:VisualElement name="transport-mismatch-warning" class="warning-banner">
|
||||
<ui:Label name="transport-mismatch-text" class="warning-banner-text" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement name="version-mismatch-warning" class="warning-banner">
|
||||
<ui:Label name="version-mismatch-text" class="warning-banner-text" />
|
||||
</ui:VisualElement>
|
||||
<ui:VisualElement class="setting-row" name="http-url-row">
|
||||
<ui:Label text="HTTP URL:" class="setting-label" />
|
||||
<ui:TextField name="http-url" class="url-field" />
|
||||
|
|
|
|||
|
|
@ -92,6 +92,9 @@ namespace MCPForUnity.Editor.Windows
|
|||
|
||||
public void CreateGUI()
|
||||
{
|
||||
// Clear search filter on GUI recreation to avoid stale filtered results
|
||||
searchFilter = "";
|
||||
|
||||
string basePath = AssetPathUtility.GetMcpPackageRootPath();
|
||||
|
||||
// Load UXML
|
||||
|
|
@ -245,24 +248,27 @@ namespace MCPForUnity.Editor.Windows
|
|||
// Check if we know the type of this pref
|
||||
if (knownPrefTypes.TryGetValue(key, out var knownType))
|
||||
{
|
||||
// Check if the key actually exists
|
||||
item.IsUnset = !EditorPrefs.HasKey(key);
|
||||
|
||||
// Use the known type
|
||||
switch (knownType)
|
||||
{
|
||||
case EditorPrefType.Bool:
|
||||
item.Type = EditorPrefType.Bool;
|
||||
item.Value = EditorPrefs.GetBool(key, false).ToString();
|
||||
item.Value = item.IsUnset ? "Unset. Default: False" : EditorPrefs.GetBool(key, false).ToString();
|
||||
break;
|
||||
case EditorPrefType.Int:
|
||||
item.Type = EditorPrefType.Int;
|
||||
item.Value = EditorPrefs.GetInt(key, 0).ToString();
|
||||
item.Value = item.IsUnset ? "Unset. Default: 0" : EditorPrefs.GetInt(key, 0).ToString();
|
||||
break;
|
||||
case EditorPrefType.Float:
|
||||
item.Type = EditorPrefType.Float;
|
||||
item.Value = EditorPrefs.GetFloat(key, 0f).ToString();
|
||||
item.Value = item.IsUnset ? "Unset. Default: 0" : EditorPrefs.GetFloat(key, 0f).ToString();
|
||||
break;
|
||||
case EditorPrefType.String:
|
||||
item.Type = EditorPrefType.String;
|
||||
item.Value = EditorPrefs.GetString(key, "");
|
||||
item.Value = item.IsUnset ? "Unset. Default: (empty)" : EditorPrefs.GetString(key, "");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -324,6 +330,14 @@ namespace MCPForUnity.Editor.Windows
|
|||
// Buttons
|
||||
var saveButton = itemElement.Q<Button>("save-button");
|
||||
|
||||
// Style unset items
|
||||
if (item.IsUnset)
|
||||
{
|
||||
valueField.SetEnabled(false);
|
||||
valueField.style.opacity = 0.6f;
|
||||
saveButton.SetEnabled(false);
|
||||
}
|
||||
|
||||
// Callbacks
|
||||
saveButton.clicked += () => SavePref(item, valueField.value, (EditorPrefType)typeDropdown.index);
|
||||
|
||||
|
|
@ -389,6 +403,7 @@ namespace MCPForUnity.Editor.Windows
|
|||
public string Value { get; set; }
|
||||
public EditorPrefType Type { get; set; }
|
||||
public bool IsKnown { get; set; }
|
||||
public bool IsUnset { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ namespace MCPForUnity.Editor.Windows
|
|||
}
|
||||
|
||||
// Initialize version label
|
||||
UpdateVersionLabel(EditorPrefs.GetBool(EditorPrefKeys.UseBetaServer, true));
|
||||
UpdateVersionLabel(EditorConfigurationCache.Instance.UseBetaServer);
|
||||
|
||||
SetupTabs();
|
||||
|
||||
|
|
@ -229,6 +229,11 @@ namespace MCPForUnity.Editor.Windows
|
|||
// update the connection section's warning banner if there's a mismatch
|
||||
clientConfigSection.OnClientTransportDetected += (clientName, transport) =>
|
||||
connectionSection?.UpdateTransportMismatchWarning(clientName, transport);
|
||||
|
||||
// Wire up version mismatch detection: when client status is checked,
|
||||
// update the connection section's warning banner if there's a version mismatch
|
||||
clientConfigSection.OnClientConfigMismatch += (clientName, mismatchMessage) =>
|
||||
connectionSection?.UpdateVersionMismatchWarning(clientName, mismatchMessage);
|
||||
}
|
||||
|
||||
// Load and initialize Validation section
|
||||
|
|
@ -263,6 +268,7 @@ namespace MCPForUnity.Editor.Windows
|
|||
await connectionSection.VerifyBridgeConnectionAsync();
|
||||
};
|
||||
advancedSection.OnBetaModeChanged += UpdateVersionLabel;
|
||||
advancedSection.OnBetaModeChanged += _ => clientConfigSection?.RefreshSelectedClient(forceImmediate: true);
|
||||
|
||||
// Wire up health status updates from Connection to Advanced
|
||||
connectionSection?.SetHealthStatusUpdateCallback((isHealthy, statusText) =>
|
||||
|
|
@ -327,7 +333,7 @@ namespace MCPForUnity.Editor.Windows
|
|||
}
|
||||
|
||||
string version = AssetPathUtility.GetPackageVersion();
|
||||
versionLabel.text = useBetaServer ? $"v{version} β" : $"v{version}";
|
||||
versionLabel.text = $"v{version}";
|
||||
versionLabel.tooltip = useBetaServer
|
||||
? "Beta server mode - fetching pre-release server versions from PyPI"
|
||||
: $"MCP For Unity v{version}";
|
||||
|
|
@ -552,6 +558,8 @@ namespace MCPForUnity.Editor.Windows
|
|||
{
|
||||
case ActivePanel.Clients:
|
||||
if (clientsPanel != null) clientsPanel.style.display = DisplayStyle.Flex;
|
||||
// Refresh client status when switching to Connect tab (e.g., after changing beta mode in Advanced)
|
||||
clientConfigSection?.RefreshSelectedClient(forceImmediate: true);
|
||||
break;
|
||||
case ActivePanel.Validation:
|
||||
if (validationPanel != null) validationPanel.style.display = DisplayStyle.Flex;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "com.coplaydev.unity-mcp",
|
||||
"version": "9.4.0",
|
||||
"version": "9.4.1-beta.1",
|
||||
"displayName": "MCP for Unity",
|
||||
"description": "A bridge that connects AI assistants to Unity via the MCP (Model Context Protocol). Allows AI clients like Claude Code, Cursor, and VSCode to directly control your Unity Editor for enhanced development workflows.\n\nFeatures automated setup wizard, cross-platform support, and seamless integration with popular AI development tools.\n\nJoin Our Discord: https://discord.gg/y4p8KfzrN4",
|
||||
"unity": "2021.3",
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
# MCP for Unity Server
|
||||
|
|
@ -256,9 +256,10 @@ class TestUnityInstanceMiddlewareInjection:
|
|||
async def mock_call_next(_ctx):
|
||||
return {"status": "ok"}
|
||||
|
||||
# Mock PluginHub as unavailable - this is sufficient for auto-select to fail
|
||||
# Mock PluginHub as unavailable AND legacy connection pool to prevent fallback discovery
|
||||
with patch("transport.unity_instance_middleware.PluginHub.is_configured", return_value=False):
|
||||
await middleware.on_call_tool(middleware_ctx, mock_call_next)
|
||||
with patch("transport.legacy.unity_connection.get_unity_connection_pool", return_value=None):
|
||||
await middleware.on_call_tool(middleware_ctx, mock_call_next)
|
||||
|
||||
# set_state should not be called for unity_instance if no instance found
|
||||
calls = [c for c in mock_context.set_state.call_args_list
|
||||
|
|
@ -329,9 +330,10 @@ class TestAutoSelectInstance:
|
|||
|
||||
with patch("transport.unity_instance_middleware.PluginHub.is_configured", return_value=True):
|
||||
with patch("transport.unity_instance_middleware.PluginHub.get_sessions", new_callable=AsyncMock) as mock_get:
|
||||
mock_get.return_value = fake_sessions
|
||||
with patch("transport.legacy.unity_connection.get_unity_connection_pool", return_value=None):
|
||||
mock_get.return_value = fake_sessions
|
||||
|
||||
instance = await middleware._maybe_autoselect_instance(mock_context)
|
||||
instance = await middleware._maybe_autoselect_instance(mock_context)
|
||||
|
||||
assert instance is None
|
||||
|
||||
|
|
@ -345,10 +347,11 @@ class TestAutoSelectInstance:
|
|||
|
||||
with patch("transport.unity_instance_middleware.PluginHub.is_configured", return_value=True):
|
||||
with patch("transport.unity_instance_middleware.PluginHub.get_sessions", new_callable=AsyncMock) as mock_get:
|
||||
mock_get.side_effect = ConnectionError("Plugin hub unavailable")
|
||||
with patch("transport.legacy.unity_connection.get_unity_connection_pool", return_value=None):
|
||||
mock_get.side_effect = ConnectionError("Plugin hub unavailable")
|
||||
|
||||
# When PluginHub fails, auto-select returns None (graceful fallback)
|
||||
instance = await middleware._maybe_autoselect_instance(mock_context)
|
||||
# When PluginHub fails, auto-select returns None (graceful fallback)
|
||||
instance = await middleware._maybe_autoselect_instance(mock_context)
|
||||
|
||||
# Should return None since both PluginHub failed
|
||||
assert instance is None
|
||||
|
|
@ -916,10 +919,11 @@ class TestTransportEdgeCases:
|
|||
|
||||
with patch("transport.unity_instance_middleware.PluginHub.is_configured", return_value=True):
|
||||
with patch("transport.unity_instance_middleware.PluginHub.get_sessions", new_callable=AsyncMock) as mock_get:
|
||||
mock_get.side_effect = RuntimeError("Unexpected error")
|
||||
with patch("transport.legacy.unity_connection.get_unity_connection_pool", return_value=None):
|
||||
mock_get.side_effect = RuntimeError("Unexpected error")
|
||||
|
||||
# Should not raise, just return None
|
||||
instance = await middleware._maybe_autoselect_instance(mock_context)
|
||||
# Should not raise, just return None
|
||||
instance = await middleware._maybe_autoselect_instance(mock_context)
|
||||
|
||||
assert instance is None
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ namespace MCPForUnityTests.Editor.Helpers
|
|||
private bool _originalHttpTransport;
|
||||
private bool _hadDevForceRefresh;
|
||||
private bool _originalDevForceRefresh;
|
||||
private bool _hadUseBetaServer;
|
||||
private bool _originalUseBetaServer;
|
||||
private IPlatformService _originalPlatformService;
|
||||
|
||||
[OneTimeSetUp]
|
||||
|
|
@ -45,6 +47,8 @@ namespace MCPForUnityTests.Editor.Helpers
|
|||
_originalHttpTransport = EditorPrefs.GetBool(EditorPrefKeys.UseHttpTransport, true);
|
||||
_hadDevForceRefresh = EditorPrefs.HasKey(EditorPrefKeys.DevModeForceServerRefresh);
|
||||
_originalDevForceRefresh = EditorPrefs.GetBool(EditorPrefKeys.DevModeForceServerRefresh, false);
|
||||
_hadUseBetaServer = EditorPrefs.HasKey(EditorPrefKeys.UseBetaServer);
|
||||
_originalUseBetaServer = EditorPrefs.GetBool(EditorPrefKeys.UseBetaServer, true);
|
||||
_originalPlatformService = MCPServiceLocator.Platform;
|
||||
}
|
||||
|
||||
|
|
@ -58,6 +62,10 @@ namespace MCPForUnityTests.Editor.Helpers
|
|||
// Ensure deterministic uvx args ordering for these tests regardless of editor settings
|
||||
// (dev-mode inserts --no-cache/--refresh, which changes the first args).
|
||||
EditorPrefs.SetBool(EditorPrefKeys.DevModeForceServerRefresh, false);
|
||||
// Tests expect beta server mode (--prerelease explicit --from mcpforunityserver>=0.0.0a0)
|
||||
EditorPrefs.SetBool(EditorPrefKeys.UseBetaServer, true);
|
||||
// Refresh the cache so it picks up the test's pref values
|
||||
EditorConfigurationCache.Instance.Refresh();
|
||||
}
|
||||
|
||||
[TearDown]
|
||||
|
|
@ -108,6 +116,15 @@ namespace MCPForUnityTests.Editor.Helpers
|
|||
{
|
||||
EditorPrefs.DeleteKey(EditorPrefKeys.DevModeForceServerRefresh);
|
||||
}
|
||||
|
||||
if (_hadUseBetaServer)
|
||||
{
|
||||
EditorPrefs.SetBool(EditorPrefKeys.UseBetaServer, _originalUseBetaServer);
|
||||
}
|
||||
else
|
||||
{
|
||||
EditorPrefs.DeleteKey(EditorPrefKeys.UseBetaServer);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,214 @@
|
|||
---
|
||||
name: unity-mcp-orchestrator
|
||||
description: Orchestrate Unity Editor via MCP (Model Context Protocol) tools and resources. Use when working with Unity projects through MCP for Unity - creating/modifying GameObjects, editing scripts, managing scenes, running tests, or any Unity Editor automation. Provides best practices, tool schemas, and workflow patterns for effective Unity-MCP integration.
|
||||
---
|
||||
|
||||
# Unity-MCP Operator Guide
|
||||
|
||||
This skill helps you effectively use the Unity Editor with MCP tools and resources.
|
||||
|
||||
## Quick Start: Resource-First Workflow
|
||||
|
||||
**Always read relevant resources before using tools.** This prevents errors and provides the necessary context.
|
||||
|
||||
```
|
||||
1. Check editor state → mcpforunity://editor/state
|
||||
2. Understand the scene → mcpforunity://scene/gameobject-api
|
||||
3. Find what you need → find_gameobjects or resources
|
||||
4. Take action → tools (manage_gameobject, create_script, script_apply_edits, apply_text_edits, validate_script, delete_script, get_sha, etc.)
|
||||
5. Verify results → read_console, capture_screenshot (in manage_scene), resources
|
||||
```
|
||||
|
||||
## Critical Best Practices
|
||||
|
||||
### 1. After Writing/Editing Scripts: Always Refresh and Check Console
|
||||
|
||||
```python
|
||||
# After create_script or script_apply_edits:
|
||||
refresh_unity(mode="force", scope="scripts", compile="request", wait_for_ready=True)
|
||||
read_console(types=["error"], count=10, include_stacktrace=True)
|
||||
```
|
||||
|
||||
**Why:** Unity must compile scripts before they're usable. Compilation errors block all tool execution.
|
||||
|
||||
### 2. Use `batch_execute` for Multiple Operations
|
||||
|
||||
```python
|
||||
# 10-100x faster than sequential calls
|
||||
batch_execute(
|
||||
commands=[
|
||||
{"tool": "manage_gameobject", "params": {"action": "create", "name": "Cube1", "primitive_type": "Cube"}},
|
||||
{"tool": "manage_gameobject", "params": {"action": "create", "name": "Cube2", "primitive_type": "Cube"}},
|
||||
{"tool": "manage_gameobject", "params": {"action": "create", "name": "Cube3", "primitive_type": "Cube"}}
|
||||
],
|
||||
parallel=True # Read-only operations can run in parallel
|
||||
)
|
||||
```
|
||||
|
||||
**Max 25 commands per batch.** Use `fail_fast=True` for dependent operations.
|
||||
|
||||
### 3. Use `screenshot` in manage_scene to Verify Visual Results
|
||||
|
||||
```python
|
||||
# Via manage_scene
|
||||
manage_scene(action="screenshot") # Returns base64 image
|
||||
|
||||
# After creating/modifying objects, verify visually:
|
||||
# 1. Create objects
|
||||
# 2. capture screenshot
|
||||
# 3. Analyze if result matches intent
|
||||
```
|
||||
|
||||
### 4. Check Console After Major Changes
|
||||
|
||||
```python
|
||||
read_console(
|
||||
action="get",
|
||||
types=["error", "warning"], # Focus on problems
|
||||
count=10,
|
||||
format="detailed"
|
||||
)
|
||||
```
|
||||
|
||||
### 5. Always Check `editor_state` Before Complex Operations
|
||||
|
||||
```python
|
||||
# Read mcpforunity://editor/state to check:
|
||||
# - is_compiling: Wait if true
|
||||
# - is_domain_reload_pending: Wait if true
|
||||
# - ready_for_tools: Only proceed if true
|
||||
# - blocking_reasons: Why tools might fail
|
||||
```
|
||||
|
||||
## Parameter Type Conventions
|
||||
|
||||
### Vectors (position, rotation, scale, color)
|
||||
```python
|
||||
# Both forms accepted:
|
||||
position=[1.0, 2.0, 3.0] # List
|
||||
position="[1.0, 2.0, 3.0]" # JSON string
|
||||
```
|
||||
|
||||
### Booleans
|
||||
```python
|
||||
# Both forms accepted:
|
||||
include_inactive=True # Boolean
|
||||
include_inactive="true" # String
|
||||
```
|
||||
|
||||
### Colors
|
||||
```python
|
||||
# Auto-detected format:
|
||||
color=[255, 0, 0, 255] # 0-255 range
|
||||
color=[1.0, 0.0, 0.0, 1.0] # 0.0-1.0 normalized (auto-converted)
|
||||
```
|
||||
|
||||
### Paths
|
||||
```python
|
||||
# Assets-relative (default):
|
||||
path="Assets/Scripts/MyScript.cs"
|
||||
|
||||
# URI forms:
|
||||
uri="mcpforunity://path/Assets/Scripts/MyScript.cs"
|
||||
uri="file:///full/path/to/file.cs"
|
||||
```
|
||||
|
||||
## Core Tool Categories
|
||||
|
||||
| Category | Key Tools | Use For |
|
||||
|----------|-----------|---------|
|
||||
| **Scene** | `manage_scene`, `find_gameobjects` | Scene operations, finding objects |
|
||||
| **Objects** | `manage_gameobject`, `manage_components` | Creating/modifying GameObjects |
|
||||
| **Scripts** | `create_script`, `script_apply_edits`, `refresh_unity` | C# code management |
|
||||
| **Assets** | `manage_asset`, `manage_prefabs` | Asset operations |
|
||||
| **Editor** | `manage_editor`, `execute_menu_item`, `read_console` | Editor control |
|
||||
| **Testing** | `run_tests`, `get_test_job` | Unity Test Framework |
|
||||
| **Batch** | `batch_execute` | Parallel/bulk operations |
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### Creating a New Script and Using It
|
||||
|
||||
```python
|
||||
# 1. Create the script
|
||||
create_script(
|
||||
path="Assets/Scripts/PlayerController.cs",
|
||||
contents="using UnityEngine;\n\npublic class PlayerController : MonoBehaviour\n{\n void Update() { }\n}"
|
||||
)
|
||||
|
||||
# 2. CRITICAL: Refresh and wait for compilation
|
||||
refresh_unity(mode="force", scope="scripts", compile="request", wait_for_ready=True)
|
||||
|
||||
# 3. Check for compilation errors
|
||||
read_console(types=["error"], count=10)
|
||||
|
||||
# 4. Only then attach to GameObject
|
||||
manage_gameobject(action="modify", target="Player", components_to_add=["PlayerController"])
|
||||
```
|
||||
|
||||
### Finding and Modifying GameObjects
|
||||
|
||||
```python
|
||||
# 1. Find by name/tag/component (returns IDs only)
|
||||
result = find_gameobjects(search_term="Enemy", search_method="by_tag", page_size=50)
|
||||
|
||||
# 2. Get full data via resource
|
||||
# mcpforunity://scene/gameobject/{instance_id}
|
||||
|
||||
# 3. Modify using the ID
|
||||
manage_gameobject(action="modify", target=instance_id, position=[10, 0, 0])
|
||||
```
|
||||
|
||||
### Running and Monitoring Tests
|
||||
|
||||
```python
|
||||
# 1. Start test run (async)
|
||||
result = run_tests(mode="EditMode", test_names=["MyTests.TestSomething"])
|
||||
job_id = result["job_id"]
|
||||
|
||||
# 2. Poll for completion
|
||||
result = get_test_job(job_id=job_id, wait_timeout=60, include_failed_tests=True)
|
||||
```
|
||||
|
||||
## Pagination Pattern
|
||||
|
||||
Large queries return paginated results. Always follow `next_cursor`:
|
||||
|
||||
```python
|
||||
cursor = 0
|
||||
all_items = []
|
||||
while True:
|
||||
result = manage_scene(action="get_hierarchy", page_size=50, cursor=cursor)
|
||||
all_items.extend(result["data"]["items"])
|
||||
if not result["data"].get("next_cursor"):
|
||||
break
|
||||
cursor = result["data"]["next_cursor"]
|
||||
```
|
||||
|
||||
## Multi-Instance Workflow
|
||||
|
||||
When multiple Unity Editors are running:
|
||||
|
||||
```python
|
||||
# 1. List instances via resource: mcpforunity://instances
|
||||
# 2. Set active instance
|
||||
set_active_instance(instance="MyProject@abc123")
|
||||
# 3. All subsequent calls route to that instance
|
||||
```
|
||||
|
||||
## Error Recovery
|
||||
|
||||
| Symptom | Cause | Solution |
|
||||
|---------|-------|----------|
|
||||
| Tools return "busy" | Compilation in progress | Wait, check `editor_state` |
|
||||
| "stale_file" error | File changed since SHA | Re-fetch SHA with `get_sha`, retry |
|
||||
| Connection lost | Domain reload | Wait ~5s, reconnect |
|
||||
| Commands fail silently | Wrong instance | Check `set_active_instance` |
|
||||
|
||||
## Reference Files
|
||||
|
||||
For detailed schemas and examples:
|
||||
|
||||
- **[tools-reference.md](references/tools-reference.md)**: Complete tool documentation with all parameters
|
||||
- **[resources-reference.md](references/resources-reference.md)**: All available resources and their data
|
||||
- **[workflows.md](references/workflows.md)**: Extended workflow examples and patterns
|
||||
|
|
@ -0,0 +1,493 @@
|
|||
# Unity-MCP Resources Reference
|
||||
|
||||
Resources provide read-only access to Unity state. Use resources to inspect before using tools to modify.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Editor State Resources](#editor-state-resources)
|
||||
- [Scene & GameObject Resources](#scene--gameobject-resources)
|
||||
- [Prefab Resources](#prefab-resources)
|
||||
- [Project Resources](#project-resources)
|
||||
- [Instance Resources](#instance-resources)
|
||||
- [Test Resources](#test-resources)
|
||||
|
||||
---
|
||||
|
||||
## URI Scheme
|
||||
|
||||
All resources use `mcpforunity://` scheme:
|
||||
|
||||
```
|
||||
mcpforunity://{category}/{resource_path}[?query_params]
|
||||
```
|
||||
|
||||
**Categories:** `editor`, `scene`, `prefab`, `project`, `menu-items`, `custom-tools`, `tests`, `instances`
|
||||
|
||||
---
|
||||
|
||||
## Editor State Resources
|
||||
|
||||
### mcpforunity://editor/state
|
||||
|
||||
**Purpose:** Editor readiness snapshot - check before tool operations.
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{
|
||||
"unity_version": "2022.3.10f1",
|
||||
"is_compiling": false,
|
||||
"is_domain_reload_pending": false,
|
||||
"play_mode": {
|
||||
"is_playing": false,
|
||||
"is_paused": false
|
||||
},
|
||||
"active_scene": {
|
||||
"path": "Assets/Scenes/Main.unity",
|
||||
"name": "Main"
|
||||
},
|
||||
"ready_for_tools": true,
|
||||
"blocking_reasons": [],
|
||||
"recommended_retry_after_ms": null,
|
||||
"staleness": {
|
||||
"age_ms": 150,
|
||||
"is_stale": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key Fields:**
|
||||
- `ready_for_tools`: Only proceed if `true`
|
||||
- `is_compiling`: Wait if `true`
|
||||
- `blocking_reasons`: Array explaining why tools might fail
|
||||
- `recommended_retry_after_ms`: Suggested wait time
|
||||
|
||||
### mcpforunity://editor/selection
|
||||
|
||||
**Purpose:** Currently selected objects.
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{
|
||||
"activeObject": "Player",
|
||||
"activeGameObject": "Player",
|
||||
"activeInstanceID": 12345,
|
||||
"count": 3,
|
||||
"gameObjects": ["Player", "Enemy", "Wall"],
|
||||
"assetGUIDs": []
|
||||
}
|
||||
```
|
||||
|
||||
### mcpforunity://editor/active-tool
|
||||
|
||||
**Purpose:** Current editor tool state.
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{
|
||||
"activeTool": "Move",
|
||||
"isCustom": false,
|
||||
"pivotMode": "Center",
|
||||
"pivotRotation": "Global"
|
||||
}
|
||||
```
|
||||
|
||||
### mcpforunity://editor/windows
|
||||
|
||||
**Purpose:** All open editor windows.
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{
|
||||
"windows": [
|
||||
{
|
||||
"title": "Scene",
|
||||
"typeName": "UnityEditor.SceneView",
|
||||
"isFocused": true,
|
||||
"position": {"x": 0, "y": 0, "width": 800, "height": 600}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### mcpforunity://editor/prefab-stage
|
||||
|
||||
**Purpose:** Current prefab editing context.
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{
|
||||
"isOpen": true,
|
||||
"assetPath": "Assets/Prefabs/Player.prefab",
|
||||
"prefabRootName": "Player",
|
||||
"isDirty": false
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Scene & GameObject Resources
|
||||
|
||||
### mcpforunity://scene/gameobject-api
|
||||
|
||||
**Purpose:** Documentation for GameObject resources (read this first).
|
||||
|
||||
### mcpforunity://scene/gameobject/{instance_id}
|
||||
|
||||
**Purpose:** Basic GameObject data (metadata, no component properties).
|
||||
|
||||
**Parameters:**
|
||||
- `instance_id` (int): GameObject instance ID from `find_gameobjects`
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{
|
||||
"instanceID": 12345,
|
||||
"name": "Player",
|
||||
"tag": "Player",
|
||||
"layer": 8,
|
||||
"layerName": "Player",
|
||||
"active": true,
|
||||
"activeInHierarchy": true,
|
||||
"isStatic": false,
|
||||
"transform": {
|
||||
"position": [0, 1, 0],
|
||||
"rotation": [0, 0, 0],
|
||||
"scale": [1, 1, 1]
|
||||
},
|
||||
"parent": {"instanceID": 0},
|
||||
"children": [{"instanceID": 67890}],
|
||||
"componentTypes": ["Transform", "Rigidbody", "PlayerController"],
|
||||
"path": "/Player"
|
||||
}
|
||||
```
|
||||
|
||||
### mcpforunity://scene/gameobject/{instance_id}/components
|
||||
|
||||
**Purpose:** All components with full property serialization (paginated).
|
||||
|
||||
**Parameters:**
|
||||
- `instance_id` (int): GameObject instance ID
|
||||
- `page_size` (int): Default 25, max 100
|
||||
- `cursor` (int): Pagination cursor
|
||||
- `include_properties` (bool): Default true, set false for just types
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{
|
||||
"gameObjectID": 12345,
|
||||
"gameObjectName": "Player",
|
||||
"components": [
|
||||
{
|
||||
"type": "Transform",
|
||||
"properties": {
|
||||
"position": {"x": 0, "y": 1, "z": 0},
|
||||
"rotation": {"x": 0, "y": 0, "z": 0, "w": 1}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Rigidbody",
|
||||
"properties": {
|
||||
"mass": 1.0,
|
||||
"useGravity": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"cursor": 0,
|
||||
"pageSize": 25,
|
||||
"nextCursor": null,
|
||||
"hasMore": false
|
||||
}
|
||||
```
|
||||
|
||||
### mcpforunity://scene/gameobject/{instance_id}/component/{component_name}
|
||||
|
||||
**Purpose:** Single component with full properties.
|
||||
|
||||
**Parameters:**
|
||||
- `instance_id` (int): GameObject instance ID
|
||||
- `component_name` (string): e.g., "Rigidbody", "Camera", "Transform"
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{
|
||||
"gameObjectID": 12345,
|
||||
"gameObjectName": "Player",
|
||||
"component": {
|
||||
"type": "Rigidbody",
|
||||
"properties": {
|
||||
"mass": 1.0,
|
||||
"drag": 0,
|
||||
"angularDrag": 0.05,
|
||||
"useGravity": true,
|
||||
"isKinematic": false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Prefab Resources
|
||||
|
||||
### mcpforunity://prefab-api
|
||||
|
||||
**Purpose:** Documentation for prefab resources.
|
||||
|
||||
### mcpforunity://prefab/{encoded_path}
|
||||
|
||||
**Purpose:** Prefab asset information.
|
||||
|
||||
**Parameters:**
|
||||
- `encoded_path` (string): URL-encoded path, e.g., `Assets%2FPrefabs%2FPlayer.prefab`
|
||||
|
||||
**Path Encoding:**
|
||||
```
|
||||
Assets/Prefabs/Player.prefab → Assets%2FPrefabs%2FPlayer.prefab
|
||||
```
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{
|
||||
"assetPath": "Assets/Prefabs/Player.prefab",
|
||||
"guid": "abc123...",
|
||||
"prefabType": "Regular",
|
||||
"rootObjectName": "Player",
|
||||
"rootComponentTypes": ["Transform", "PlayerController"],
|
||||
"childCount": 5,
|
||||
"isVariant": false,
|
||||
"parentPrefab": null
|
||||
}
|
||||
```
|
||||
|
||||
### mcpforunity://prefab/{encoded_path}/hierarchy
|
||||
|
||||
**Purpose:** Full prefab hierarchy with nested prefab info.
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{
|
||||
"prefabPath": "Assets/Prefabs/Player.prefab",
|
||||
"total": 6,
|
||||
"items": [
|
||||
{
|
||||
"name": "Player",
|
||||
"instanceId": 12345,
|
||||
"path": "/Player",
|
||||
"activeSelf": true,
|
||||
"childCount": 2,
|
||||
"componentTypes": ["Transform", "PlayerController"]
|
||||
},
|
||||
{
|
||||
"name": "Model",
|
||||
"path": "/Player/Model",
|
||||
"isNestedPrefab": true,
|
||||
"nestedPrefabPath": "Assets/Prefabs/PlayerModel.prefab"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Project Resources
|
||||
|
||||
### mcpforunity://project/info
|
||||
|
||||
**Purpose:** Static project configuration.
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{
|
||||
"projectRoot": "/Users/dev/MyProject",
|
||||
"projectName": "MyProject",
|
||||
"unityVersion": "2022.3.10f1",
|
||||
"platform": "StandaloneWindows64",
|
||||
"assetsPath": "/Users/dev/MyProject/Assets"
|
||||
}
|
||||
```
|
||||
|
||||
### mcpforunity://project/tags
|
||||
|
||||
**Purpose:** All tags defined in TagManager.
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
["Untagged", "Respawn", "Finish", "EditorOnly", "MainCamera", "Player", "GameController", "Enemy"]
|
||||
```
|
||||
|
||||
### mcpforunity://project/layers
|
||||
|
||||
**Purpose:** All layers with indices (0-31).
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{
|
||||
"0": "Default",
|
||||
"1": "TransparentFX",
|
||||
"2": "Ignore Raycast",
|
||||
"4": "Water",
|
||||
"5": "UI",
|
||||
"8": "Player",
|
||||
"9": "Enemy"
|
||||
}
|
||||
```
|
||||
|
||||
### mcpforunity://menu-items
|
||||
|
||||
**Purpose:** All available Unity menu items.
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
[
|
||||
"File/New Scene",
|
||||
"File/Open Scene",
|
||||
"File/Save",
|
||||
"Edit/Undo",
|
||||
"Edit/Redo",
|
||||
"GameObject/Create Empty",
|
||||
"GameObject/3D Object/Cube",
|
||||
"Window/General/Console"
|
||||
]
|
||||
```
|
||||
|
||||
### mcpforunity://custom-tools
|
||||
|
||||
**Purpose:** Custom tools available in the active Unity project.
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{
|
||||
"project_id": "MyProject",
|
||||
"tool_count": 3,
|
||||
"tools": [
|
||||
{
|
||||
"name": "capture_screenshot",
|
||||
"description": "Capture screenshots in Unity",
|
||||
"parameters": [
|
||||
{"name": "filename", "type": "string", "required": true},
|
||||
{"name": "width", "type": "int", "required": false},
|
||||
{"name": "height", "type": "int", "required": false}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Instance Resources
|
||||
|
||||
### mcpforunity://instances
|
||||
|
||||
**Purpose:** All running Unity Editor instances (for multi-instance workflows).
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
{
|
||||
"transport": "http",
|
||||
"instance_count": 2,
|
||||
"instances": [
|
||||
{
|
||||
"id": "MyProject@abc123",
|
||||
"name": "MyProject",
|
||||
"hash": "abc123",
|
||||
"unity_version": "2022.3.10f1",
|
||||
"connected_at": "2024-01-15T10:30:00Z"
|
||||
},
|
||||
{
|
||||
"id": "TestProject@def456",
|
||||
"name": "TestProject",
|
||||
"hash": "def456",
|
||||
"unity_version": "2022.3.10f1",
|
||||
"connected_at": "2024-01-15T11:00:00Z"
|
||||
}
|
||||
],
|
||||
"warnings": []
|
||||
}
|
||||
```
|
||||
|
||||
**Use with:** `set_active_instance(instance="MyProject@abc123")`
|
||||
|
||||
---
|
||||
|
||||
## Test Resources
|
||||
|
||||
### mcpforunity://tests
|
||||
|
||||
**Purpose:** All tests in the project.
|
||||
|
||||
**Returns:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "TestSomething",
|
||||
"full_name": "MyTests.TestSomething",
|
||||
"mode": "EditMode"
|
||||
},
|
||||
{
|
||||
"name": "TestOther",
|
||||
"full_name": "MyTests.TestOther",
|
||||
"mode": "PlayMode"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### mcpforunity://tests/{mode}
|
||||
|
||||
**Purpose:** Tests filtered by mode.
|
||||
|
||||
**Parameters:**
|
||||
- `mode` (string): "EditMode" or "PlayMode"
|
||||
|
||||
**Example:** `mcpforunity://tests/EditMode`
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Check Editor State First
|
||||
|
||||
```python
|
||||
# Before any complex operation:
|
||||
# Read mcpforunity://editor/state
|
||||
# Check ready_for_tools == true
|
||||
```
|
||||
|
||||
### 2. Use Find Then Read Pattern
|
||||
|
||||
```python
|
||||
# 1. find_gameobjects to get IDs
|
||||
result = find_gameobjects(search_term="Player")
|
||||
|
||||
# 2. Read resource for full data
|
||||
# mcpforunity://scene/gameobject/{id}
|
||||
```
|
||||
|
||||
### 3. Paginate Large Queries
|
||||
|
||||
```python
|
||||
# Start with include_properties=false for component lists
|
||||
# mcpforunity://scene/gameobject/{id}/components?include_properties=false&page_size=25
|
||||
|
||||
# Then read specific components as needed
|
||||
# mcpforunity://scene/gameobject/{id}/component/Rigidbody
|
||||
```
|
||||
|
||||
### 4. URL-Encode Prefab Paths
|
||||
|
||||
```python
|
||||
# Wrong:
|
||||
# mcpforunity://prefab/Assets/Prefabs/Player.prefab
|
||||
|
||||
# Correct:
|
||||
# mcpforunity://prefab/Assets%2FPrefabs%2FPlayer.prefab
|
||||
```
|
||||
|
||||
### 5. Multi-Instance Awareness
|
||||
|
||||
```python
|
||||
# Always check mcpforunity://instances when:
|
||||
# - First connecting
|
||||
# - Commands fail unexpectedly
|
||||
# - Working with multiple projects
|
||||
```
|
||||
|
|
@ -0,0 +1,606 @@
|
|||
# Unity-MCP Tools Reference
|
||||
|
||||
Complete reference for all MCP tools. Each tool includes parameters, types, and usage examples.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Infrastructure Tools](#infrastructure-tools)
|
||||
- [Scene Tools](#scene-tools)
|
||||
- [GameObject Tools](#gameobject-tools)
|
||||
- [Script Tools](#script-tools)
|
||||
- [Asset Tools](#asset-tools)
|
||||
- [Material & Shader Tools](#material--shader-tools)
|
||||
- [Editor Control Tools](#editor-control-tools)
|
||||
- [Testing Tools](#testing-tools)
|
||||
|
||||
---
|
||||
|
||||
## Infrastructure Tools
|
||||
|
||||
### batch_execute
|
||||
|
||||
Execute multiple MCP commands in a single batch (10-100x faster).
|
||||
|
||||
```python
|
||||
batch_execute(
|
||||
commands=[ # list[dict], required, max 25
|
||||
{"tool": "tool_name", "params": {...}},
|
||||
...
|
||||
],
|
||||
parallel=False, # bool, optional - run read-only ops in parallel
|
||||
fail_fast=False, # bool, optional - stop on first failure
|
||||
max_parallelism=None # int, optional - max parallel workers
|
||||
)
|
||||
```
|
||||
|
||||
### set_active_instance
|
||||
|
||||
Route commands to a specific Unity instance (multi-instance workflows).
|
||||
|
||||
```python
|
||||
set_active_instance(
|
||||
instance="ProjectName@abc123" # str, required - Name@hash or hash prefix
|
||||
)
|
||||
```
|
||||
|
||||
### refresh_unity
|
||||
|
||||
Refresh asset database and trigger script compilation.
|
||||
|
||||
```python
|
||||
refresh_unity(
|
||||
mode="if_dirty", # "if_dirty" | "force"
|
||||
scope="all", # "assets" | "scripts" | "all"
|
||||
compile="none", # "none" | "request"
|
||||
wait_for_ready=True # bool - wait until editor ready
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Scene Tools
|
||||
|
||||
### manage_scene
|
||||
|
||||
Scene CRUD operations and hierarchy queries.
|
||||
|
||||
```python
|
||||
# Get hierarchy (paginated)
|
||||
manage_scene(
|
||||
action="get_hierarchy",
|
||||
page_size=50, # int, default 50, max 500
|
||||
cursor=0, # int, pagination cursor
|
||||
parent=None, # str|int, optional - filter by parent
|
||||
include_transform=False # bool - include local transforms
|
||||
)
|
||||
|
||||
# Screenshot
|
||||
manage_scene(action="screenshot") # Returns base64 PNG
|
||||
|
||||
# Other actions
|
||||
manage_scene(action="get_active") # Current scene info
|
||||
manage_scene(action="get_build_settings") # Build settings
|
||||
manage_scene(action="create", name="NewScene", path="Assets/Scenes/")
|
||||
manage_scene(action="load", path="Assets/Scenes/Main.unity")
|
||||
manage_scene(action="save")
|
||||
```
|
||||
|
||||
### find_gameobjects
|
||||
|
||||
Search for GameObjects (returns instance IDs only).
|
||||
|
||||
```python
|
||||
find_gameobjects(
|
||||
search_term="Player", # str, required
|
||||
search_method="by_name", # "by_name"|"by_tag"|"by_layer"|"by_component"|"by_path"|"by_id"
|
||||
include_inactive=False, # bool|str
|
||||
page_size=50, # int, default 50, max 500
|
||||
cursor=0 # int, pagination cursor
|
||||
)
|
||||
# Returns: {"ids": [12345, 67890], "next_cursor": 50, ...}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## GameObject Tools
|
||||
|
||||
### manage_gameobject
|
||||
|
||||
Create, modify, delete, duplicate GameObjects.
|
||||
|
||||
```python
|
||||
# Create
|
||||
manage_gameobject(
|
||||
action="create",
|
||||
name="MyCube", # str, required
|
||||
primitive_type="Cube", # "Cube"|"Sphere"|"Capsule"|"Cylinder"|"Plane"|"Quad"
|
||||
position=[0, 1, 0], # list[float] or JSON string "[0,1,0]"
|
||||
rotation=[0, 45, 0], # euler angles
|
||||
scale=[1, 1, 1],
|
||||
components_to_add=["Rigidbody", "BoxCollider"],
|
||||
save_as_prefab=False,
|
||||
prefab_path="Assets/Prefabs/MyCube.prefab"
|
||||
)
|
||||
|
||||
# Modify
|
||||
manage_gameobject(
|
||||
action="modify",
|
||||
target="Player", # name, path, or instance ID
|
||||
search_method="by_name", # how to find target
|
||||
position=[10, 0, 0],
|
||||
rotation=[0, 90, 0],
|
||||
scale=[2, 2, 2],
|
||||
set_active=True,
|
||||
layer="Player",
|
||||
components_to_add=["AudioSource"],
|
||||
components_to_remove=["OldComponent"],
|
||||
component_properties={ # nested dict for property setting
|
||||
"Rigidbody": {
|
||||
"mass": 10.0,
|
||||
"useGravity": True
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
# Delete
|
||||
manage_gameobject(action="delete", target="OldObject")
|
||||
|
||||
# Duplicate
|
||||
manage_gameobject(
|
||||
action="duplicate",
|
||||
target="Player",
|
||||
new_name="Player2",
|
||||
offset=[5, 0, 0] # position offset from original
|
||||
)
|
||||
|
||||
# Move relative
|
||||
manage_gameobject(
|
||||
action="move_relative",
|
||||
target="Player",
|
||||
reference_object="Enemy", # optional reference
|
||||
direction="left", # "left"|"right"|"up"|"down"|"forward"|"back"
|
||||
distance=5.0,
|
||||
world_space=True
|
||||
)
|
||||
```
|
||||
|
||||
### manage_components
|
||||
|
||||
Add, remove, or set properties on components.
|
||||
|
||||
```python
|
||||
# Add component
|
||||
manage_components(
|
||||
action="add",
|
||||
target=12345, # instance ID (preferred) or name
|
||||
component_type="Rigidbody",
|
||||
search_method="by_id"
|
||||
)
|
||||
|
||||
# Remove component
|
||||
manage_components(
|
||||
action="remove",
|
||||
target="Player",
|
||||
component_type="OldScript"
|
||||
)
|
||||
|
||||
# Set single property
|
||||
manage_components(
|
||||
action="set_property",
|
||||
target=12345,
|
||||
component_type="Rigidbody",
|
||||
property="mass",
|
||||
value=5.0
|
||||
)
|
||||
|
||||
# Set multiple properties
|
||||
manage_components(
|
||||
action="set_property",
|
||||
target=12345,
|
||||
component_type="Transform",
|
||||
properties={
|
||||
"position": [1, 2, 3],
|
||||
"localScale": [2, 2, 2]
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Script Tools
|
||||
|
||||
### create_script
|
||||
|
||||
Create a new C# script.
|
||||
|
||||
```python
|
||||
create_script(
|
||||
path="Assets/Scripts/MyScript.cs", # str, required
|
||||
contents='''using UnityEngine;
|
||||
|
||||
public class MyScript : MonoBehaviour
|
||||
{
|
||||
void Start() { }
|
||||
void Update() { }
|
||||
}''',
|
||||
script_type="MonoBehaviour", # optional hint
|
||||
namespace="MyGame" # optional namespace
|
||||
)
|
||||
```
|
||||
|
||||
### script_apply_edits
|
||||
|
||||
Apply structured edits to C# scripts (safer than raw text edits).
|
||||
|
||||
```python
|
||||
script_apply_edits(
|
||||
name="MyScript", # script name (no .cs)
|
||||
path="Assets/Scripts", # folder path
|
||||
edits=[
|
||||
# Replace entire method
|
||||
{
|
||||
"op": "replace_method",
|
||||
"methodName": "Update",
|
||||
"replacement": "void Update() { transform.Rotate(Vector3.up); }"
|
||||
},
|
||||
# Insert new method
|
||||
{
|
||||
"op": "insert_method",
|
||||
"afterMethod": "Start",
|
||||
"code": "void OnEnable() { Debug.Log(\"Enabled\"); }"
|
||||
},
|
||||
# Delete method
|
||||
{
|
||||
"op": "delete_method",
|
||||
"methodName": "OldMethod"
|
||||
},
|
||||
# Anchor-based insert
|
||||
{
|
||||
"op": "anchor_insert",
|
||||
"anchor": "void Start()",
|
||||
"position": "before", # "before" | "after"
|
||||
"text": "// Called before Start\n"
|
||||
},
|
||||
# Regex replace
|
||||
{
|
||||
"op": "regex_replace",
|
||||
"pattern": "Debug\\.Log\\(",
|
||||
"text": "Debug.LogWarning("
|
||||
},
|
||||
# Prepend/append to file
|
||||
{"op": "prepend", "text": "// File header\n"},
|
||||
{"op": "append", "text": "\n// File footer"}
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
### apply_text_edits
|
||||
|
||||
Apply precise character-position edits (1-indexed lines/columns).
|
||||
|
||||
```python
|
||||
apply_text_edits(
|
||||
uri="mcpforunity://path/Assets/Scripts/MyScript.cs",
|
||||
edits=[
|
||||
{
|
||||
"startLine": 10,
|
||||
"startCol": 5,
|
||||
"endLine": 10,
|
||||
"endCol": 20,
|
||||
"newText": "replacement text"
|
||||
}
|
||||
],
|
||||
precondition_sha256="abc123...", # optional, prevents stale edits
|
||||
strict=True # optional, stricter validation
|
||||
)
|
||||
```
|
||||
|
||||
### validate_script
|
||||
|
||||
Check script for syntax/semantic errors.
|
||||
|
||||
```python
|
||||
validate_script(
|
||||
uri="mcpforunity://path/Assets/Scripts/MyScript.cs",
|
||||
level="standard", # "basic" | "standard"
|
||||
include_diagnostics=True # include full error details
|
||||
)
|
||||
```
|
||||
|
||||
### get_sha
|
||||
|
||||
Get file hash without content (for preconditions).
|
||||
|
||||
```python
|
||||
get_sha(uri="mcpforunity://path/Assets/Scripts/MyScript.cs")
|
||||
# Returns: {"sha256": "...", "lengthBytes": 1234, "lastModifiedUtc": "..."}
|
||||
```
|
||||
|
||||
### delete_script
|
||||
|
||||
Delete a script file.
|
||||
|
||||
```python
|
||||
delete_script(uri="mcpforunity://path/Assets/Scripts/OldScript.cs")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Asset Tools
|
||||
|
||||
### manage_asset
|
||||
|
||||
Asset operations: search, import, create, modify, delete.
|
||||
|
||||
```python
|
||||
# Search assets (paginated)
|
||||
manage_asset(
|
||||
action="search",
|
||||
path="Assets", # search scope
|
||||
search_pattern="*.prefab", # glob or "t:MonoScript" filter
|
||||
filter_type="Prefab", # optional type filter
|
||||
page_size=25, # keep small to avoid large payloads
|
||||
page_number=1, # 1-based
|
||||
generate_preview=False # avoid base64 bloat
|
||||
)
|
||||
|
||||
# Get asset info
|
||||
manage_asset(action="get_info", path="Assets/Prefabs/Player.prefab")
|
||||
|
||||
# Create asset
|
||||
manage_asset(
|
||||
action="create",
|
||||
path="Assets/Materials/NewMaterial.mat",
|
||||
asset_type="Material",
|
||||
properties={"color": [1, 0, 0, 1]}
|
||||
)
|
||||
|
||||
# Duplicate/move/rename
|
||||
manage_asset(action="duplicate", path="Assets/A.prefab", destination="Assets/B.prefab")
|
||||
manage_asset(action="move", path="Assets/A.prefab", destination="Assets/Prefabs/A.prefab")
|
||||
manage_asset(action="rename", path="Assets/A.prefab", destination="Assets/B.prefab")
|
||||
|
||||
# Create folder
|
||||
manage_asset(action="create_folder", path="Assets/NewFolder")
|
||||
|
||||
# Delete
|
||||
manage_asset(action="delete", path="Assets/OldAsset.asset")
|
||||
```
|
||||
|
||||
### manage_prefabs
|
||||
|
||||
Headless prefab operations.
|
||||
|
||||
```python
|
||||
# Get prefab info
|
||||
manage_prefabs(action="get_info", prefab_path="Assets/Prefabs/Player.prefab")
|
||||
|
||||
# Get prefab hierarchy
|
||||
manage_prefabs(action="get_hierarchy", prefab_path="Assets/Prefabs/Player.prefab")
|
||||
|
||||
# Create prefab from scene GameObject
|
||||
manage_prefabs(
|
||||
action="create_from_gameobject",
|
||||
target="Player", # GameObject in scene
|
||||
prefab_path="Assets/Prefabs/Player.prefab",
|
||||
allow_overwrite=False
|
||||
)
|
||||
|
||||
# Modify prefab contents (headless)
|
||||
manage_prefabs(
|
||||
action="modify_contents",
|
||||
prefab_path="Assets/Prefabs/Player.prefab",
|
||||
target="ChildObject", # object within prefab
|
||||
position=[0, 1, 0],
|
||||
components_to_add=["AudioSource"]
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Material & Shader Tools
|
||||
|
||||
### manage_material
|
||||
|
||||
Create and modify materials.
|
||||
|
||||
```python
|
||||
# Create material
|
||||
manage_material(
|
||||
action="create",
|
||||
material_path="Assets/Materials/Red.mat",
|
||||
shader="Standard",
|
||||
properties={"_Color": [1, 0, 0, 1]}
|
||||
)
|
||||
|
||||
# Get material info
|
||||
manage_material(action="get_material_info", material_path="Assets/Materials/Red.mat")
|
||||
|
||||
# Set shader property
|
||||
manage_material(
|
||||
action="set_material_shader_property",
|
||||
material_path="Assets/Materials/Red.mat",
|
||||
property="_Metallic",
|
||||
value=0.8
|
||||
)
|
||||
|
||||
# Set color
|
||||
manage_material(
|
||||
action="set_material_color",
|
||||
material_path="Assets/Materials/Red.mat",
|
||||
property="_BaseColor",
|
||||
color=[0, 1, 0, 1] # RGBA
|
||||
)
|
||||
|
||||
# Assign to renderer
|
||||
manage_material(
|
||||
action="assign_material_to_renderer",
|
||||
target="MyCube",
|
||||
material_path="Assets/Materials/Red.mat",
|
||||
slot=0 # material slot index
|
||||
)
|
||||
|
||||
# Set renderer color directly
|
||||
manage_material(
|
||||
action="set_renderer_color",
|
||||
target="MyCube",
|
||||
color=[1, 0, 0, 1],
|
||||
mode="instance" # "shared"|"instance"|"property_block"
|
||||
)
|
||||
```
|
||||
|
||||
### manage_texture
|
||||
|
||||
Create procedural textures.
|
||||
|
||||
```python
|
||||
manage_texture(
|
||||
action="create",
|
||||
path="Assets/Textures/Checker.png",
|
||||
width=64,
|
||||
height=64,
|
||||
fill_color=[255, 255, 255, 255] # or [1.0, 1.0, 1.0, 1.0]
|
||||
)
|
||||
|
||||
# Apply pattern
|
||||
manage_texture(
|
||||
action="apply_pattern",
|
||||
path="Assets/Textures/Checker.png",
|
||||
pattern="checkerboard", # "checkerboard"|"stripes"|"dots"|"grid"|"brick"
|
||||
palette=[[0,0,0,255], [255,255,255,255]],
|
||||
pattern_size=8
|
||||
)
|
||||
|
||||
# Apply gradient
|
||||
manage_texture(
|
||||
action="apply_gradient",
|
||||
path="Assets/Textures/Gradient.png",
|
||||
gradient_type="linear", # "linear"|"radial"
|
||||
gradient_angle=45,
|
||||
palette=[[255,0,0,255], [0,0,255,255]]
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Editor Control Tools
|
||||
|
||||
### manage_editor
|
||||
|
||||
Control Unity Editor state.
|
||||
|
||||
```python
|
||||
manage_editor(action="play") # Enter play mode
|
||||
manage_editor(action="pause") # Pause play mode
|
||||
manage_editor(action="stop") # Exit play mode
|
||||
|
||||
manage_editor(action="set_active_tool", tool_name="Move") # Move/Rotate/Scale/etc.
|
||||
|
||||
manage_editor(action="add_tag", tag_name="Enemy")
|
||||
manage_editor(action="remove_tag", tag_name="OldTag")
|
||||
|
||||
manage_editor(action="add_layer", layer_name="Projectiles")
|
||||
manage_editor(action="remove_layer", layer_name="OldLayer")
|
||||
```
|
||||
|
||||
### execute_menu_item
|
||||
|
||||
Execute any Unity menu item.
|
||||
|
||||
```python
|
||||
execute_menu_item(menu_path="File/Save Project")
|
||||
execute_menu_item(menu_path="GameObject/3D Object/Cube")
|
||||
execute_menu_item(menu_path="Window/General/Console")
|
||||
```
|
||||
|
||||
### read_console
|
||||
|
||||
Read or clear Unity console messages.
|
||||
|
||||
```python
|
||||
# Get recent messages
|
||||
read_console(
|
||||
action="get",
|
||||
types=["error", "warning", "log"], # or ["all"]
|
||||
count=10, # max messages (ignored with paging)
|
||||
filter_text="NullReference", # optional text filter
|
||||
since_timestamp="2024-01-01T00:00:00Z", # optional time filter
|
||||
page_size=50,
|
||||
cursor=0,
|
||||
format="detailed", # "plain"|"detailed"|"json"
|
||||
include_stacktrace=True
|
||||
)
|
||||
|
||||
# Clear console
|
||||
read_console(action="clear")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Tools
|
||||
|
||||
### run_tests
|
||||
|
||||
Start async test execution.
|
||||
|
||||
```python
|
||||
result = run_tests(
|
||||
mode="EditMode", # "EditMode"|"PlayMode"
|
||||
test_names=["MyTests.TestA", "MyTests.TestB"], # specific tests
|
||||
group_names=["Integration*"], # regex patterns
|
||||
category_names=["Unit"], # NUnit categories
|
||||
assembly_names=["Tests"], # assembly filter
|
||||
include_failed_tests=True, # include failure details
|
||||
include_details=False # include all test details
|
||||
)
|
||||
# Returns: {"job_id": "abc123", ...}
|
||||
```
|
||||
|
||||
### get_test_job
|
||||
|
||||
Poll test job status.
|
||||
|
||||
```python
|
||||
result = get_test_job(
|
||||
job_id="abc123",
|
||||
wait_timeout=60, # wait up to N seconds
|
||||
include_failed_tests=True,
|
||||
include_details=False
|
||||
)
|
||||
# Returns: {"status": "complete"|"running"|"failed", "results": {...}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Search Tools
|
||||
|
||||
### find_in_file
|
||||
|
||||
Search file contents with regex.
|
||||
|
||||
```python
|
||||
find_in_file(
|
||||
uri="mcpforunity://path/Assets/Scripts/MyScript.cs",
|
||||
pattern="public void \\w+", # regex pattern
|
||||
max_results=200,
|
||||
ignore_case=True
|
||||
)
|
||||
# Returns: line numbers, content excerpts, match positions
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Custom Tools
|
||||
|
||||
### execute_custom_tool
|
||||
|
||||
Execute project-specific custom tools.
|
||||
|
||||
```python
|
||||
execute_custom_tool(
|
||||
tool_name="my_custom_tool",
|
||||
parameters={"param1": "value", "param2": 42}
|
||||
)
|
||||
```
|
||||
|
||||
Discover available custom tools via `mcpforunity://custom-tools` resource.
|
||||
|
|
@ -0,0 +1,609 @@
|
|||
# Unity-MCP Workflow Patterns
|
||||
|
||||
Common workflows and patterns for effective Unity-MCP usage.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Setup & Verification](#setup--verification)
|
||||
- [Scene Creation Workflows](#scene-creation-workflows)
|
||||
- [Script Development Workflows](#script-development-workflows)
|
||||
- [Asset Management Workflows](#asset-management-workflows)
|
||||
- [Testing Workflows](#testing-workflows)
|
||||
- [Debugging Workflows](#debugging-workflows)
|
||||
- [Batch Operations](#batch-operations)
|
||||
|
||||
---
|
||||
|
||||
## Setup & Verification
|
||||
|
||||
### Initial Connection Verification
|
||||
|
||||
```python
|
||||
# 1. Check editor state
|
||||
# Read mcpforunity://editor/state
|
||||
|
||||
# 2. Verify ready_for_tools == true
|
||||
# If false, wait for recommended_retry_after_ms
|
||||
|
||||
# 3. Check active scene
|
||||
# Read mcpforunity://editor/state → active_scene
|
||||
|
||||
# 4. List available instances (multi-instance)
|
||||
# Read mcpforunity://instances
|
||||
```
|
||||
|
||||
### Before Any Operation
|
||||
|
||||
```python
|
||||
# Quick readiness check pattern:
|
||||
editor_state = read_resource("mcpforunity://editor/state")
|
||||
|
||||
if not editor_state["ready_for_tools"]:
|
||||
# Check blocking_reasons
|
||||
# Wait recommended_retry_after_ms
|
||||
pass
|
||||
|
||||
if editor_state["is_compiling"]:
|
||||
# Wait for compilation to complete
|
||||
pass
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Scene Creation Workflows
|
||||
|
||||
### Create Complete Scene from Scratch
|
||||
|
||||
```python
|
||||
# 1. Create new scene
|
||||
manage_scene(action="create", name="GameLevel", path="Assets/Scenes/")
|
||||
|
||||
# 2. Batch create environment objects
|
||||
batch_execute(commands=[
|
||||
{"tool": "manage_gameobject", "params": {
|
||||
"action": "create", "name": "Ground", "primitive_type": "Plane",
|
||||
"position": [0, 0, 0], "scale": [10, 1, 10]
|
||||
}},
|
||||
{"tool": "manage_gameobject", "params": {
|
||||
"action": "create", "name": "Light", "primitive_type": "Cube"
|
||||
}},
|
||||
{"tool": "manage_gameobject", "params": {
|
||||
"action": "create", "name": "Player", "primitive_type": "Capsule",
|
||||
"position": [0, 1, 0]
|
||||
}}
|
||||
])
|
||||
|
||||
# 3. Add light component (delete cube mesh, add light)
|
||||
manage_components(action="remove", target="Light", component_type="MeshRenderer")
|
||||
manage_components(action="remove", target="Light", component_type="MeshFilter")
|
||||
manage_components(action="remove", target="Light", component_type="BoxCollider")
|
||||
manage_components(action="add", target="Light", component_type="Light")
|
||||
manage_components(action="set_property", target="Light", component_type="Light",
|
||||
property="type", value="Directional")
|
||||
|
||||
# 4. Set up camera
|
||||
manage_gameobject(action="modify", target="Main Camera", position=[0, 5, -10],
|
||||
rotation=[30, 0, 0])
|
||||
|
||||
# 5. Verify with screenshot
|
||||
manage_scene(action="screenshot")
|
||||
|
||||
# 6. Save scene
|
||||
manage_scene(action="save")
|
||||
```
|
||||
|
||||
### Populate Scene with Grid of Objects
|
||||
|
||||
```python
|
||||
# Create 5x5 grid of cubes using batch
|
||||
commands = []
|
||||
for x in range(5):
|
||||
for z in range(5):
|
||||
commands.append({
|
||||
"tool": "manage_gameobject",
|
||||
"params": {
|
||||
"action": "create",
|
||||
"name": f"Cube_{x}_{z}",
|
||||
"primitive_type": "Cube",
|
||||
"position": [x * 2, 0, z * 2]
|
||||
}
|
||||
})
|
||||
|
||||
# Execute in batches of 25
|
||||
batch_execute(commands=commands[:25], parallel=True)
|
||||
```
|
||||
|
||||
### Clone and Arrange Objects
|
||||
|
||||
```python
|
||||
# Find template object
|
||||
result = find_gameobjects(search_term="Template", search_method="by_name")
|
||||
template_id = result["ids"][0]
|
||||
|
||||
# Duplicate in a line
|
||||
for i in range(10):
|
||||
manage_gameobject(
|
||||
action="duplicate",
|
||||
target=template_id,
|
||||
new_name=f"Instance_{i}",
|
||||
offset=[i * 2, 0, 0]
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Script Development Workflows
|
||||
|
||||
### Create New Script and Attach
|
||||
|
||||
```python
|
||||
# 1. Create script
|
||||
create_script(
|
||||
path="Assets/Scripts/EnemyAI.cs",
|
||||
contents='''using UnityEngine;
|
||||
|
||||
public class EnemyAI : MonoBehaviour
|
||||
{
|
||||
public float speed = 5f;
|
||||
public Transform target;
|
||||
|
||||
void Update()
|
||||
{
|
||||
if (target != null)
|
||||
{
|
||||
Vector3 direction = (target.position - transform.position).normalized;
|
||||
transform.position += direction * speed * Time.deltaTime;
|
||||
}
|
||||
}
|
||||
}'''
|
||||
)
|
||||
|
||||
# 2. CRITICAL: Refresh and compile
|
||||
refresh_unity(mode="force", scope="scripts", compile="request", wait_for_ready=True)
|
||||
|
||||
# 3. Check for errors
|
||||
console = read_console(types=["error"], count=10)
|
||||
if console["messages"]:
|
||||
# Handle compilation errors
|
||||
print("Compilation errors:", console["messages"])
|
||||
else:
|
||||
# 4. Attach to GameObject
|
||||
manage_gameobject(action="modify", target="Enemy", components_to_add=["EnemyAI"])
|
||||
|
||||
# 5. Set component properties
|
||||
manage_components(
|
||||
action="set_property",
|
||||
target="Enemy",
|
||||
component_type="EnemyAI",
|
||||
properties={
|
||||
"speed": 10.0
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### Edit Existing Script Safely
|
||||
|
||||
```python
|
||||
# 1. Get current SHA
|
||||
sha_info = get_sha(uri="mcpforunity://path/Assets/Scripts/PlayerController.cs")
|
||||
|
||||
# 2. Find the method to edit
|
||||
matches = find_in_file(
|
||||
uri="mcpforunity://path/Assets/Scripts/PlayerController.cs",
|
||||
pattern="void Update\\(\\)"
|
||||
)
|
||||
|
||||
# 3. Apply structured edit
|
||||
script_apply_edits(
|
||||
name="PlayerController",
|
||||
path="Assets/Scripts",
|
||||
edits=[{
|
||||
"op": "replace_method",
|
||||
"methodName": "Update",
|
||||
"replacement": '''void Update()
|
||||
{
|
||||
float h = Input.GetAxis("Horizontal");
|
||||
float v = Input.GetAxis("Vertical");
|
||||
transform.Translate(new Vector3(h, 0, v) * speed * Time.deltaTime);
|
||||
}'''
|
||||
}]
|
||||
)
|
||||
|
||||
# 4. Validate
|
||||
validate_script(
|
||||
uri="mcpforunity://path/Assets/Scripts/PlayerController.cs",
|
||||
level="standard"
|
||||
)
|
||||
|
||||
# 5. Refresh
|
||||
refresh_unity(mode="force", scope="scripts", compile="request", wait_for_ready=True)
|
||||
|
||||
# 6. Check console
|
||||
read_console(types=["error"], count=10)
|
||||
```
|
||||
|
||||
### Add Method to Existing Class
|
||||
|
||||
```python
|
||||
script_apply_edits(
|
||||
name="GameManager",
|
||||
path="Assets/Scripts",
|
||||
edits=[
|
||||
{
|
||||
"op": "insert_method",
|
||||
"afterMethod": "Start",
|
||||
"code": '''
|
||||
public void ResetGame()
|
||||
{
|
||||
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
|
||||
}'''
|
||||
},
|
||||
{
|
||||
"op": "anchor_insert",
|
||||
"anchor": "using UnityEngine;",
|
||||
"position": "after",
|
||||
"text": "\nusing UnityEngine.SceneManagement;"
|
||||
}
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Asset Management Workflows
|
||||
|
||||
### Create and Apply Material
|
||||
|
||||
```python
|
||||
# 1. Create material
|
||||
manage_material(
|
||||
action="create",
|
||||
material_path="Assets/Materials/PlayerMaterial.mat",
|
||||
shader="Standard",
|
||||
properties={
|
||||
"_Color": [0.2, 0.5, 1.0, 1.0],
|
||||
"_Metallic": 0.5,
|
||||
"_Glossiness": 0.8
|
||||
}
|
||||
)
|
||||
|
||||
# 2. Assign to renderer
|
||||
manage_material(
|
||||
action="assign_material_to_renderer",
|
||||
target="Player",
|
||||
material_path="Assets/Materials/PlayerMaterial.mat",
|
||||
slot=0
|
||||
)
|
||||
|
||||
# 3. Verify visually
|
||||
manage_scene(action="screenshot")
|
||||
```
|
||||
|
||||
### Create Procedural Texture
|
||||
|
||||
```python
|
||||
# 1. Create base texture
|
||||
manage_texture(
|
||||
action="create",
|
||||
path="Assets/Textures/Checkerboard.png",
|
||||
width=256,
|
||||
height=256,
|
||||
fill_color=[255, 255, 255, 255]
|
||||
)
|
||||
|
||||
# 2. Apply checkerboard pattern
|
||||
manage_texture(
|
||||
action="apply_pattern",
|
||||
path="Assets/Textures/Checkerboard.png",
|
||||
pattern="checkerboard",
|
||||
palette=[[0, 0, 0, 255], [255, 255, 255, 255]],
|
||||
pattern_size=32
|
||||
)
|
||||
|
||||
# 3. Create material with texture
|
||||
manage_material(
|
||||
action="create",
|
||||
material_path="Assets/Materials/CheckerMaterial.mat",
|
||||
shader="Standard"
|
||||
)
|
||||
|
||||
# 4. Assign texture to material (via manage_material set_material_shader_property)
|
||||
```
|
||||
|
||||
### Organize Assets into Folders
|
||||
|
||||
```python
|
||||
# 1. Create folder structure
|
||||
batch_execute(commands=[
|
||||
{"tool": "manage_asset", "params": {"action": "create_folder", "path": "Assets/Prefabs"}},
|
||||
{"tool": "manage_asset", "params": {"action": "create_folder", "path": "Assets/Materials"}},
|
||||
{"tool": "manage_asset", "params": {"action": "create_folder", "path": "Assets/Scripts"}},
|
||||
{"tool": "manage_asset", "params": {"action": "create_folder", "path": "Assets/Textures"}}
|
||||
])
|
||||
|
||||
# 2. Move existing assets
|
||||
manage_asset(action="move", path="Assets/MyMaterial.mat", destination="Assets/Materials/MyMaterial.mat")
|
||||
manage_asset(action="move", path="Assets/MyScript.cs", destination="Assets/Scripts/MyScript.cs")
|
||||
```
|
||||
|
||||
### Search and Process Assets
|
||||
|
||||
```python
|
||||
# Find all prefabs
|
||||
result = manage_asset(
|
||||
action="search",
|
||||
path="Assets",
|
||||
search_pattern="*.prefab",
|
||||
page_size=50,
|
||||
generate_preview=False
|
||||
)
|
||||
|
||||
# Process each prefab
|
||||
for asset in result["assets"]:
|
||||
prefab_path = asset["path"]
|
||||
# Get prefab info
|
||||
info = manage_prefabs(action="get_info", prefab_path=prefab_path)
|
||||
print(f"Prefab: {prefab_path}, Children: {info['childCount']}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Workflows
|
||||
|
||||
### Run Specific Tests
|
||||
|
||||
```python
|
||||
# 1. List available tests
|
||||
# Read mcpforunity://tests/EditMode
|
||||
|
||||
# 2. Run specific tests
|
||||
result = run_tests(
|
||||
mode="EditMode",
|
||||
test_names=["MyTests.TestPlayerMovement", "MyTests.TestEnemySpawn"],
|
||||
include_failed_tests=True
|
||||
)
|
||||
job_id = result["job_id"]
|
||||
|
||||
# 3. Wait for results
|
||||
final_result = get_test_job(
|
||||
job_id=job_id,
|
||||
wait_timeout=60,
|
||||
include_failed_tests=True
|
||||
)
|
||||
|
||||
# 4. Check results
|
||||
if final_result["status"] == "complete":
|
||||
for test in final_result.get("failed_tests", []):
|
||||
print(f"FAILED: {test['name']}: {test['message']}")
|
||||
```
|
||||
|
||||
### Run Tests by Category
|
||||
|
||||
```python
|
||||
# Run all unit tests
|
||||
result = run_tests(
|
||||
mode="EditMode",
|
||||
category_names=["Unit"],
|
||||
include_failed_tests=True
|
||||
)
|
||||
|
||||
# Poll until complete
|
||||
while True:
|
||||
status = get_test_job(job_id=result["job_id"], wait_timeout=30)
|
||||
if status["status"] in ["complete", "failed"]:
|
||||
break
|
||||
```
|
||||
|
||||
### Test-Driven Development Pattern
|
||||
|
||||
```python
|
||||
# 1. Write test first
|
||||
create_script(
|
||||
path="Assets/Tests/Editor/PlayerTests.cs",
|
||||
contents='''using NUnit.Framework;
|
||||
using UnityEngine;
|
||||
|
||||
public class PlayerTests
|
||||
{
|
||||
[Test]
|
||||
public void TestPlayerStartsAtOrigin()
|
||||
{
|
||||
var player = new GameObject("TestPlayer");
|
||||
Assert.AreEqual(Vector3.zero, player.transform.position);
|
||||
Object.DestroyImmediate(player);
|
||||
}
|
||||
}'''
|
||||
)
|
||||
|
||||
# 2. Refresh
|
||||
refresh_unity(mode="force", scope="scripts", compile="request", wait_for_ready=True)
|
||||
|
||||
# 3. Run test (expect pass for this simple test)
|
||||
result = run_tests(mode="EditMode", test_names=["PlayerTests.TestPlayerStartsAtOrigin"])
|
||||
get_test_job(job_id=result["job_id"], wait_timeout=30)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Debugging Workflows
|
||||
|
||||
### Diagnose Compilation Errors
|
||||
|
||||
```python
|
||||
# 1. Check console for errors
|
||||
errors = read_console(
|
||||
types=["error"],
|
||||
count=20,
|
||||
include_stacktrace=True,
|
||||
format="detailed"
|
||||
)
|
||||
|
||||
# 2. For each error, find the file and line
|
||||
for error in errors["messages"]:
|
||||
# Parse error message for file:line info
|
||||
# Use find_in_file to locate the problematic code
|
||||
pass
|
||||
|
||||
# 3. After fixing, refresh and check again
|
||||
refresh_unity(mode="force", scope="scripts", compile="request", wait_for_ready=True)
|
||||
read_console(types=["error"], count=10)
|
||||
```
|
||||
|
||||
### Investigate Missing References
|
||||
|
||||
```python
|
||||
# 1. Find the GameObject
|
||||
result = find_gameobjects(search_term="Player", search_method="by_name")
|
||||
|
||||
# 2. Get all components
|
||||
# Read mcpforunity://scene/gameobject/{id}/components
|
||||
|
||||
# 3. Check for null references in serialized fields
|
||||
# Look for fields with null/missing values
|
||||
|
||||
# 4. Find the referenced object
|
||||
result = find_gameobjects(search_term="Target", search_method="by_name")
|
||||
|
||||
# 5. Set the reference
|
||||
manage_components(
|
||||
action="set_property",
|
||||
target="Player",
|
||||
component_type="PlayerController",
|
||||
property="target",
|
||||
value={"instanceID": result["ids"][0]} # Reference by ID
|
||||
)
|
||||
```
|
||||
|
||||
### Check Scene State
|
||||
|
||||
```python
|
||||
# 1. Get hierarchy
|
||||
hierarchy = manage_scene(action="get_hierarchy", page_size=100, include_transform=True)
|
||||
|
||||
# 2. Find objects at unexpected positions
|
||||
for item in hierarchy["data"]["items"]:
|
||||
if item.get("transform", {}).get("position", [0,0,0])[1] < -100:
|
||||
print(f"Object {item['name']} fell through floor!")
|
||||
|
||||
# 3. Visual verification
|
||||
manage_scene(action="screenshot")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Batch Operations
|
||||
|
||||
### Mass Property Update
|
||||
|
||||
```python
|
||||
# Find all enemies
|
||||
enemies = find_gameobjects(search_term="Enemy", search_method="by_tag")
|
||||
|
||||
# Update health on all enemies
|
||||
commands = []
|
||||
for enemy_id in enemies["ids"]:
|
||||
commands.append({
|
||||
"tool": "manage_components",
|
||||
"params": {
|
||||
"action": "set_property",
|
||||
"target": enemy_id,
|
||||
"component_type": "EnemyHealth",
|
||||
"property": "maxHealth",
|
||||
"value": 100
|
||||
}
|
||||
})
|
||||
|
||||
# Execute in batches
|
||||
for i in range(0, len(commands), 25):
|
||||
batch_execute(commands=commands[i:i+25], parallel=True)
|
||||
```
|
||||
|
||||
### Mass Object Creation with Variations
|
||||
|
||||
```python
|
||||
import random
|
||||
|
||||
commands = []
|
||||
for i in range(20):
|
||||
commands.append({
|
||||
"tool": "manage_gameobject",
|
||||
"params": {
|
||||
"action": "create",
|
||||
"name": f"Tree_{i}",
|
||||
"primitive_type": "Capsule",
|
||||
"position": [random.uniform(-50, 50), 0, random.uniform(-50, 50)],
|
||||
"scale": [1, random.uniform(2, 5), 1]
|
||||
}
|
||||
})
|
||||
|
||||
batch_execute(commands=commands, parallel=True)
|
||||
```
|
||||
|
||||
### Cleanup Pattern
|
||||
|
||||
```python
|
||||
# Find all temporary objects
|
||||
temps = find_gameobjects(search_term="Temp_", search_method="by_name")
|
||||
|
||||
# Delete in batch
|
||||
commands = [
|
||||
{"tool": "manage_gameobject", "params": {"action": "delete", "target": id}}
|
||||
for id in temps["ids"]
|
||||
]
|
||||
|
||||
batch_execute(commands=commands, fail_fast=False)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Recovery Patterns
|
||||
|
||||
### Stale File Recovery
|
||||
|
||||
```python
|
||||
try:
|
||||
apply_text_edits(uri=script_uri, edits=[...], precondition_sha256=old_sha)
|
||||
except Exception as e:
|
||||
if "stale_file" in str(e):
|
||||
# Re-fetch SHA
|
||||
new_sha = get_sha(uri=script_uri)
|
||||
# Retry with new SHA
|
||||
apply_text_edits(uri=script_uri, edits=[...], precondition_sha256=new_sha["sha256"])
|
||||
```
|
||||
|
||||
### Domain Reload Recovery
|
||||
|
||||
```python
|
||||
# After domain reload, connection may be lost
|
||||
# Wait and retry pattern:
|
||||
import time
|
||||
|
||||
max_retries = 5
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
editor_state = read_resource("mcpforunity://editor/state")
|
||||
if editor_state["ready_for_tools"]:
|
||||
break
|
||||
except:
|
||||
time.sleep(2 ** attempt) # Exponential backoff
|
||||
```
|
||||
|
||||
### Compilation Block Recovery
|
||||
|
||||
```python
|
||||
# If tools fail due to compilation:
|
||||
# 1. Check console for errors
|
||||
errors = read_console(types=["error"], count=20)
|
||||
|
||||
# 2. Fix the script errors
|
||||
# ... edit scripts ...
|
||||
|
||||
# 3. Force refresh
|
||||
refresh_unity(mode="force", scope="scripts", compile="request", wait_for_ready=True)
|
||||
|
||||
# 4. Verify clean console
|
||||
errors = read_console(types=["error"], count=5)
|
||||
if not errors["messages"]:
|
||||
# Safe to proceed with tools
|
||||
pass
|
||||
```
|
||||
Loading…
Reference in New Issue