Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Repository Guidelines

## Project Structure & Module Organization
This repo has two primary codebases:
- `Server/`: Python MCP server (`src/` runtime code, `tests/` Python tests, `pyproject.toml`).
- `MCPForUnity/`: Unity package (mainly `Editor/` tools/services and `Runtime/` code).

Supporting areas:
- `TestProjects/UnityMCPTests/`: Unity test project and C# test suites.
- `docs/`: user, migration, and development documentation.
- `tools/` and `scripts/`: release/dev utilities.
- `mcp_source.py`: switch Unity package source for local/upstream workflows.

## Build, Test, and Development Commands
- `cd Server && uv run src/main.py --transport stdio`: run local server over stdio.
- `cd Server && uv run src/main.py --transport http --http-url http://127.0.0.1:8080`: run local HTTP server.
- `cd Server && uv run pytest tests/ -v`: run Python server tests.
- `cd Server && uv run pytest tests/ --cov --cov-report=html`: run coverage and generate `htmlcov/`.
- `cd Server && uv run python -m cli.main editor tests --mode PlayMode`: run Unity tests via MCP bridge (Unity must be running).
- `python mcp_source.py`: point Unity project to upstream/main/beta/local package sources.

## Coding Style & Naming Conventions
- Python: 4-space indentation, snake_case modules/functions, keep async command handlers focused by domain under `Server/src/cli/commands/`.
- C#: PascalCase for types/methods, `ManageXxx` naming for tool classes, keep one tool responsibility per class.
- Prefer small, explicit code over one-off abstractions.
- Type checking uses `Server/pyrightconfig.json` (`typeCheckingMode: basic`).

## Testing Guidelines
- Add tests for all feature work in both touched layers when applicable (Python + Unity).
- Python test files follow `test_*.py` under `Server/tests/`.
- Validate behavior changes with targeted tests first, then broader suites before PR.

## Commit & Pull Request Guidelines
- Branch from `beta`; do not target `main` for feature development.
- Follow existing commit style: `feat(scope): ...`, `fix: ...`, `docs: ...`, `chore: ...`.
- Keep commits focused and reference issue/PR IDs when relevant (e.g., `(#859)`).
- PRs should include: clear summary, linked issue/discussion, test evidence, and screenshots/GIFs for Unity UI/tooling changes.

## Security & Configuration Tips
- Default local HTTP endpoint is `127.0.0.1`; avoid exposing `0.0.0.0` unless explicitly needed.
- For local server iteration in Unity, use **Server Source Override** to your `Server/` path and enable **Dev Mode**.
80 changes: 80 additions & 0 deletions MCPForUnity/Editor/Helpers/ComponentOps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -197,11 +197,21 @@ private static bool TrySetViaReflection(object component, Type type, string prop
{
error = null;

// Skip reflection for UnityEngine.Object types with JObject values
// so SerializedProperty can resolve guid/spriteName/fileID forms.
bool isJObjectValue = value != null && value.Type == JTokenType.Object;

// Try property first
PropertyInfo propInfo = type.GetProperty(propertyName, flags)
?? type.GetProperty(normalizedName, flags);
if (propInfo != null && propInfo.CanWrite)
{
if (isJObjectValue && typeof(UnityEngine.Object).IsAssignableFrom(propInfo.PropertyType))
{
// Let SerializedProperty path handle complex object references.
return false;
}

try
{
object convertedValue = PropertyConversion.ConvertToType(value, propInfo.PropertyType);
Expand All @@ -225,6 +235,12 @@ private static bool TrySetViaReflection(object component, Type type, string prop
?? type.GetField(normalizedName, flags);
if (fieldInfo != null && !fieldInfo.IsInitOnly)
{
if (isJObjectValue && typeof(UnityEngine.Object).IsAssignableFrom(fieldInfo.FieldType))
{
// Let SerializedProperty path handle complex object references.
return false;
}

try
{
object convertedValue = PropertyConversion.ConvertToType(value, fieldInfo.FieldType);
Expand All @@ -248,6 +264,12 @@ private static bool TrySetViaReflection(object component, Type type, string prop
?? FindSerializedFieldInHierarchy(type, normalizedName);
if (fieldInfo != null)
{
if (isJObjectValue && typeof(UnityEngine.Object).IsAssignableFrom(fieldInfo.FieldType))
{
// Let SerializedProperty path handle complex object references.
return false;
}

try
{
object convertedValue = PropertyConversion.ConvertToType(value, fieldInfo.FieldType);
Expand Down Expand Up @@ -599,6 +621,47 @@ private static bool SetObjectReference(SerializedProperty prop, JToken value, ou
error = $"No asset found for GUID '{guidToken}'.";
return false;
}

var spriteNameToken = jObj["spriteName"];
if (spriteNameToken != null)
{
string spriteName = spriteNameToken.ToString();
var allAssets = AssetDatabase.LoadAllAssetsAtPath(path);
foreach (var asset in allAssets)
{
if (asset is Sprite sprite && sprite.name == spriteName)
{
prop.objectReferenceValue = sprite;
return true;
}
}

error = $"Sprite '{spriteName}' not found in atlas '{path}'.";
return false;
}

var fileIdToken = jObj["fileID"];
if (fileIdToken != null)
{
long targetFileId = ParamCoercion.CoerceLong(fileIdToken, 0);
if (targetFileId != 0)
{
var allAssets = AssetDatabase.LoadAllAssetsAtPath(path);
foreach (var asset in allAssets)
{
if (asset is Sprite sprite)
{
long spriteFileId = GetSpriteFileId(sprite);
if (spriteFileId == targetFileId)
{
prop.objectReferenceValue = sprite;
return true;
}
}
}
}
}

prop.objectReferenceValue = AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(path);
return true;
Comment on lines +643 to 666
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Silent fallthrough when fileID doesn't match any sprite.

When fileID is provided but no sprite in the atlas matches, the code falls through to load the main asset at the path (line 665). This may return an unexpected asset type (e.g., the texture atlas itself) instead of failing with a clear error.

Consider returning an error when fileID is provided but not found:

Proposed fix
                     var fileIdToken = jObj["fileID"];
                     if (fileIdToken != null)
                     {
                         long targetFileId = ParamCoercion.CoerceLong(fileIdToken, 0);
                         if (targetFileId != 0)
                         {
                             var allAssets = AssetDatabase.LoadAllAssetsAtPath(path);
                             foreach (var asset in allAssets)
                             {
                                 if (asset is Sprite sprite)
                                 {
                                     long spriteFileId = GetSpriteFileId(sprite);
                                     if (spriteFileId == targetFileId)
                                     {
                                         prop.objectReferenceValue = sprite;
                                         return true;
                                     }
                                 }
                             }
+                            error = $"Sprite with fileID {targetFileId} not found in atlas '{path}'.";
+                            return false;
                         }
                     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@MCPForUnity/Editor/Helpers/ComponentOps.cs` around lines 643 - 666, The code
currently falls through and calls AssetDatabase.LoadAssetAtPath when fileID was
present but no matching sprite was found; change the behavior so that when
jObj["fileID"] exists and ParamCoercion.CoerceLong yields a non‑zero
targetFileId but no sprite in AssetDatabase.LoadAllAssetsAtPath matches
GetSpriteFileId, the method should not load the main asset — instead set
prop.objectReferenceValue = null (or leave unchanged) and return false and
optionally log a clear error including the targetFileId and path; update the
logic around fileIdToken/targetFileId and the foreach that uses GetSpriteFileId
to detect the “not found” case and early-return false rather than falling
through to AssetDatabase.LoadAssetAtPath.

}
Expand Down Expand Up @@ -767,6 +830,23 @@ private static bool SetEnum(SerializedProperty prop, JToken value, out string er
error = $"Unknown enum name '{s}'.";
return false;
}


private static long GetSpriteFileId(Sprite sprite)
{
if (sprite == null)
return 0;

try
{
var globalId = GlobalObjectId.GetGlobalObjectIdSlow(sprite);
return (long)globalId.targetObjectId;
}
catch
{
return 0;
}
}
}
}

Loading