From 649599fcfe3fdc9c44c63996a03299d352c047d3 Mon Sep 17 00:00:00 2001 From: "toma.tanigawa" Date: Fri, 18 Apr 2025 20:23:41 +0900 Subject: [PATCH 1/3] Add: Shader management tool --- UnityMcpServer/src/server.py | 3 +- UnityMcpServer/src/tools/__init__.py | 2 + UnityMcpServer/src/tools/manage_shader.py | 67 +++++++++++++++++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 UnityMcpServer/src/tools/manage_shader.py diff --git a/UnityMcpServer/src/server.py b/UnityMcpServer/src/server.py index 90d0c72..55360b5 100644 --- a/UnityMcpServer/src/server.py +++ b/UnityMcpServer/src/server.py @@ -61,7 +61,8 @@ def asset_creation_strategy() -> str: "- `manage_scene`: Manages scenes.\\n" "- `manage_gameobject`: Manages GameObjects in the scene.\\n" "- `manage_script`: Manages C# script files.\\n" - "- `manage_asset`: Manages prefabs and assets.\\n\\n" + "- `manage_asset`: Manages prefabs and assets.\\n" + "- `manage_shader`: Manages shaders.\\n\\n" "Tips:\\n" "- Create prefabs for reusable GameObjects.\\n" "- Always include a camera and main light in your scenes.\\n" diff --git a/UnityMcpServer/src/tools/__init__.py b/UnityMcpServer/src/tools/__init__.py index 8cfc38e..4d8d63c 100644 --- a/UnityMcpServer/src/tools/__init__.py +++ b/UnityMcpServer/src/tools/__init__.py @@ -3,6 +3,7 @@ from .manage_scene import register_manage_scene_tools from .manage_editor import register_manage_editor_tools from .manage_gameobject import register_manage_gameobject_tools from .manage_asset import register_manage_asset_tools +from .manage_shader import register_manage_shader_tools from .read_console import register_read_console_tools from .execute_menu_item import register_execute_menu_item_tools @@ -14,6 +15,7 @@ def register_all_tools(mcp): register_manage_editor_tools(mcp) register_manage_gameobject_tools(mcp) register_manage_asset_tools(mcp) + register_manage_shader_tools(mcp) register_read_console_tools(mcp) register_execute_menu_item_tools(mcp) print("Unity MCP Server tool registration complete.") diff --git a/UnityMcpServer/src/tools/manage_shader.py b/UnityMcpServer/src/tools/manage_shader.py new file mode 100644 index 0000000..c447a3a --- /dev/null +++ b/UnityMcpServer/src/tools/manage_shader.py @@ -0,0 +1,67 @@ +from mcp.server.fastmcp import FastMCP, Context +from typing import Dict, Any +from unity_connection import get_unity_connection +import os +import base64 + +def register_manage_shader_tools(mcp: FastMCP): + """Register all shader script management tools with the MCP server.""" + + @mcp.tool() + def manage_shader( + ctx: Context, + action: str, + name: str, + path: str, + contents: str, + ) -> Dict[str, Any]: + """Manages shader scripts in Unity (create, read, update, delete). + + Args: + action: Operation ('create', 'read', 'update', 'delete'). + name: Shader name (no .cs extension). + path: Asset path (default: "Assets/"). + contents: Shader code for 'create'/'update'. + + Returns: + Dictionary with results ('success', 'message', 'data'). + """ + try: + # Prepare parameters for Unity + params = { + "action": action, + "name": name, + "path": path, + } + + # Base64 encode the contents if they exist to avoid JSON escaping issues + if contents is not None: + if action in ['create', 'update']: + # Encode content for safer transmission + params["encodedContents"] = base64.b64encode(contents.encode('utf-8')).decode('utf-8') + params["contentsEncoded"] = True + else: + params["contents"] = contents + + # Remove None values so they don't get sent as null + params = {k: v for k, v in params.items() if v is not None} + + # Send command to Unity + response = get_unity_connection().send_command("manage_shader", params) + + # Process response from Unity + if response.get("success"): + # If the response contains base64 encoded content, decode it + if response.get("data", {}).get("contentsEncoded"): + decoded_contents = base64.b64decode(response["data"]["encodedContents"]).decode('utf-8') + response["data"]["contents"] = decoded_contents + del response["data"]["encodedContents"] + del response["data"]["contentsEncoded"] + + return {"success": True, "message": response.get("message", "Operation successful."), "data": response.get("data")} + else: + return {"success": False, "message": response.get("error", "An unknown error occurred.")} + + except Exception as e: + # Handle Python-side errors (e.g., connection issues) + return {"success": False, "message": f"Python error managing shader: {str(e)}"} \ No newline at end of file From d4bd504e3167e71f4c067e8c6fb6845674ba4b3c Mon Sep 17 00:00:00 2001 From: "toma.tanigawa" Date: Fri, 18 Apr 2025 20:24:46 +0900 Subject: [PATCH 2/3] Add: MCP Bridge part of shader management tool --- UnityMcpBridge/Editor/Tools/ManageShader.cs | 325 ++++++++++++++++++++ 1 file changed, 325 insertions(+) create mode 100644 UnityMcpBridge/Editor/Tools/ManageShader.cs diff --git a/UnityMcpBridge/Editor/Tools/ManageShader.cs b/UnityMcpBridge/Editor/Tools/ManageShader.cs new file mode 100644 index 0000000..49bbf15 --- /dev/null +++ b/UnityMcpBridge/Editor/Tools/ManageShader.cs @@ -0,0 +1,325 @@ +using System; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using Newtonsoft.Json.Linq; +using UnityEditor; +using UnityEngine; +using UnityMcpBridge.Editor.Helpers; + +namespace UnityMcpBridge.Editor.Tools +{ + /// + /// Handles CRUD operations for shader files within the Unity project. + /// + public static class ManageShader + { + /// + /// Main handler for shader management actions. + /// + public static object HandleCommand(JObject @params) + { + // Extract parameters + string action = @params["action"]?.ToString().ToLower(); + string name = @params["name"]?.ToString(); + string path = @params["path"]?.ToString(); // Relative to Assets/ + string contents = null; + + // Check if we have base64 encoded contents + bool contentsEncoded = @params["contentsEncoded"]?.ToObject() ?? false; + if (contentsEncoded && @params["encodedContents"] != null) + { + try + { + contents = DecodeBase64(@params["encodedContents"].ToString()); + } + catch (Exception e) + { + return Response.Error($"Failed to decode shader contents: {e.Message}"); + } + } + else + { + contents = @params["contents"]?.ToString(); + } + + // Validate required parameters + if (string.IsNullOrEmpty(action)) + { + return Response.Error("Action parameter is required."); + } + if (string.IsNullOrEmpty(name)) + { + return Response.Error("Name parameter is required."); + } + // Basic name validation (alphanumeric, underscores, cannot start with number) + if (!Regex.IsMatch(name, @"^[a-zA-Z_][a-zA-Z0-9_]*$")) + { + return Response.Error( + $"Invalid shader name: '{name}'. Use only letters, numbers, underscores, and don't start with a number." + ); + } + + // Ensure path is relative to Assets/, removing any leading "Assets/" + // Set default directory to "Shaders" if path is not provided + string relativeDir = path ?? "Shaders"; // Default to "Shaders" if path is null + if (!string.IsNullOrEmpty(relativeDir)) + { + relativeDir = relativeDir.Replace('\\', '/').Trim('/'); + if (relativeDir.StartsWith("Assets/", StringComparison.OrdinalIgnoreCase)) + { + relativeDir = relativeDir.Substring("Assets/".Length).TrimStart('/'); + } + } + // Handle empty string case explicitly after processing + if (string.IsNullOrEmpty(relativeDir)) + { + relativeDir = "Shaders"; // Ensure default if path was provided as "" or only "/" or "Assets/" + } + + // Construct paths + string shaderFileName = $"{name}.shader"; + string fullPathDir = Path.Combine(Application.dataPath, relativeDir); + string fullPath = Path.Combine(fullPathDir, shaderFileName); + string relativePath = Path.Combine("Assets", relativeDir, shaderFileName) + .Replace('\\', '/'); // Ensure "Assets/" prefix and forward slashes + + // Ensure the target directory exists for create/update + if (action == "create" || action == "update") + { + try + { + Directory.CreateDirectory(fullPathDir); + } + catch (Exception e) + { + return Response.Error( + $"Could not create directory '{fullPathDir}': {e.Message}" + ); + } + } + + // Route to specific action handlers + switch (action) + { + case "create": + return CreateShader(fullPath, relativePath, name, contents); + case "read": + return ReadShader(fullPath, relativePath); + case "update": + return UpdateShader(fullPath, relativePath, name, contents); + case "delete": + return DeleteShader(fullPath, relativePath); + default: + return Response.Error( + $"Unknown action: '{action}'. Valid actions are: create, read, update, delete." + ); + } + } + + /// + /// Decode base64 string to normal text + /// + private static string DecodeBase64(string encoded) + { + byte[] data = Convert.FromBase64String(encoded); + return System.Text.Encoding.UTF8.GetString(data); + } + + /// + /// Encode text to base64 string + /// + private static string EncodeBase64(string text) + { + byte[] data = System.Text.Encoding.UTF8.GetBytes(text); + return Convert.ToBase64String(data); + } + + private static object CreateShader( + string fullPath, + string relativePath, + string name, + string contents + ) + { + // Check if shader already exists + if (File.Exists(fullPath)) + { + return Response.Error( + $"Shader already exists at '{relativePath}'. Use 'update' action to modify." + ); + } + + // Generate default content if none provided + if (string.IsNullOrEmpty(contents)) + { + contents = GenerateDefaultShaderContent(name); + } + + try + { + File.WriteAllText(fullPath, contents); + AssetDatabase.ImportAsset(relativePath); + AssetDatabase.Refresh(); // Ensure Unity recognizes the new shader + return Response.Success( + $"Shader '{name}.shader' created successfully at '{relativePath}'.", + new { path = relativePath } + ); + } + catch (Exception e) + { + return Response.Error($"Failed to create shader '{relativePath}': {e.Message}"); + } + } + + private static object ReadShader(string fullPath, string relativePath) + { + if (!File.Exists(fullPath)) + { + return Response.Error($"Shader not found at '{relativePath}'."); + } + + try + { + string contents = File.ReadAllText(fullPath); + + // Return both normal and encoded contents for larger files + bool isLarge = contents.Length > 10000; // If content is large, include encoded version + var responseData = new + { + path = relativePath, + contents = contents, + // For large files, also include base64-encoded version + encodedContents = isLarge ? EncodeBase64(contents) : null, + contentsEncoded = isLarge, + }; + + return Response.Success( + $"Shader '{Path.GetFileName(relativePath)}' read successfully.", + responseData + ); + } + catch (Exception e) + { + return Response.Error($"Failed to read shader '{relativePath}': {e.Message}"); + } + } + + private static object UpdateShader( + string fullPath, + string relativePath, + string name, + string contents + ) + { + if (!File.Exists(fullPath)) + { + return Response.Error( + $"Shader not found at '{relativePath}'. Use 'create' action to add a new shader." + ); + } + if (string.IsNullOrEmpty(contents)) + { + return Response.Error("Content is required for the 'update' action."); + } + + try + { + File.WriteAllText(fullPath, contents); + AssetDatabase.ImportAsset(relativePath); + return Response.Success( + $"Shader '{Path.GetFileName(relativePath)}' updated successfully.", + new { path = relativePath } + ); + } + catch (Exception e) + { + return Response.Error($"Failed to update shader '{relativePath}': {e.Message}"); + } + } + + private static object DeleteShader(string fullPath, string relativePath) + { + if (!File.Exists(fullPath)) + { + return Response.Error($"Shader not found at '{relativePath}'."); + } + + try + { + // Delete the asset through Unity's AssetDatabase first + bool success = AssetDatabase.DeleteAsset(relativePath); + if (!success) + { + return Response.Error($"Failed to delete shader through Unity's AssetDatabase: '{relativePath}'"); + } + + // If the file still exists (rare case), try direct deletion + if (File.Exists(fullPath)) + { + File.Delete(fullPath); + } + + return Response.Success($"Shader '{Path.GetFileName(relativePath)}' deleted successfully."); + } + catch (Exception e) + { + return Response.Error($"Failed to delete shader '{relativePath}': {e.Message}"); + } + } + + private static string GenerateDefaultShaderContent(string name) + { + return @"Shader """ + name + @""" +{ + Properties + { + _MainTex (""Texture"", 2D) = ""white"" {} + } + SubShader + { + Tags { ""RenderType""=""Opaque"" } + LOD 100 + + Pass + { + CGPROGRAM + #pragma vertex vert + #pragma fragment frag + #include ""UnityCG.cginc"" + + struct appdata + { + float4 vertex : POSITION; + float2 uv : TEXCOORD0; + }; + + struct v2f + { + float2 uv : TEXCOORD0; + float4 vertex : SV_POSITION; + }; + + sampler2D _MainTex; + float4 _MainTex_ST; + + v2f vert (appdata v) + { + v2f o; + o.vertex = UnityObjectToClipPos(v.vertex); + o.uv = TRANSFORM_TEX(v.uv, _MainTex); + return o; + } + + fixed4 frag (v2f i) : SV_Target + { + fixed4 col = tex2D(_MainTex, i.uv); + return col; + } + ENDCG + } + } +}"; + } + } +} \ No newline at end of file From 55f7c55389a3fd7afe773ffa521687c0e85cc7ca Mon Sep 17 00:00:00 2001 From: Scriptwonder <1300285021@qq.com> Date: Mon, 14 Jul 2025 01:42:16 -0400 Subject: [PATCH 3/3] Update for ManageShader Bug fix and error handling check for the PR. Great work and I love what I built! --- README.md | 5 +- .../Editor/Tools/CommandRegistry.cs | 1 + UnityMcpBridge/Editor/Tools/ManageShader.cs | 107 ++++++++++-------- .../Editor/Tools/ManageShader.cs.meta | 11 ++ UnityMcpBridge/Editor/UnityMcpBridge.cs | 1 + 5 files changed, 79 insertions(+), 46 deletions(-) create mode 100644 UnityMcpBridge/Editor/Tools/ManageShader.cs.meta diff --git a/README.md b/README.md index e422540..d4035a9 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Unity MCP acts as a bridge, allowing AI assistants (like Claude, Cursor) to inte * `manage_editor`: Controls and queries the editor's state and settings. * `manage_scene`: Manages scenes (load, save, create, get hierarchy, etc.). * `manage_asset`: Performs asset operations (import, create, modify, delete, etc.). + * `manage_shader`: Performs shader CRUD operations (create, read, modify, delete). * `manage_gameobject`: Manages GameObjects: create, modify, delete, find, and component operations. * `execute_menu_item`: Executes a menu item via its path (e.g., "File/Save Project"). @@ -81,7 +82,9 @@ Unity MCP connects your tools using two components: Connect your MCP Client (Claude, Cursor, etc.) to the Python server you installed in Step 1. -**Option A: Auto-Configure (Recommended for Claude/Cursor)** +image + +**Option A: Auto-Configure (Recommended for Claude/Cursor/VSC Copilot)** 1. In Unity, go to `Window > Unity MCP`. 2. Click `Auto Configure Claude` or `Auto Configure Cursor`. diff --git a/UnityMcpBridge/Editor/Tools/CommandRegistry.cs b/UnityMcpBridge/Editor/Tools/CommandRegistry.cs index 18e2de9..ce502b4 100644 --- a/UnityMcpBridge/Editor/Tools/CommandRegistry.cs +++ b/UnityMcpBridge/Editor/Tools/CommandRegistry.cs @@ -20,6 +20,7 @@ namespace UnityMcpBridge.Editor.Tools { "HandleManageAsset", ManageAsset.HandleCommand }, { "HandleReadConsole", ReadConsole.HandleCommand }, { "HandleExecuteMenuItem", ExecuteMenuItem.HandleCommand }, + { "HandleManageShader", ManageShader.HandleCommand}, }; /// diff --git a/UnityMcpBridge/Editor/Tools/ManageShader.cs b/UnityMcpBridge/Editor/Tools/ManageShader.cs index 49bbf15..03b28f1 100644 --- a/UnityMcpBridge/Editor/Tools/ManageShader.cs +++ b/UnityMcpBridge/Editor/Tools/ManageShader.cs @@ -89,7 +89,12 @@ namespace UnityMcpBridge.Editor.Tools { try { - Directory.CreateDirectory(fullPathDir); + if (!Directory.Exists(fullPathDir)) + { + Directory.CreateDirectory(fullPathDir); + // Refresh AssetDatabase to recognize new folders + AssetDatabase.Refresh(); + } } catch (Exception e) { @@ -150,6 +155,14 @@ namespace UnityMcpBridge.Editor.Tools ); } + // Add validation for shader name conflicts in Unity + if (Shader.Find(name) != null) + { + return Response.Error( + $"A shader with name '{name}' already exists in the project. Choose a different name." + ); + } + // Generate default content if none provided if (string.IsNullOrEmpty(contents)) { @@ -184,6 +197,7 @@ namespace UnityMcpBridge.Editor.Tools string contents = File.ReadAllText(fullPath); // Return both normal and encoded contents for larger files + //TODO: Consider a threshold for large files bool isLarge = contents.Length > 10000; // If content is large, include encoded version var responseData = new { @@ -227,6 +241,7 @@ namespace UnityMcpBridge.Editor.Tools { File.WriteAllText(fullPath, contents); AssetDatabase.ImportAsset(relativePath); + AssetDatabase.Refresh(); return Response.Success( $"Shader '{Path.GetFileName(relativePath)}' updated successfully.", new { path = relativePath } @@ -268,58 +283,60 @@ namespace UnityMcpBridge.Editor.Tools } } + //This is a CGProgram template + //TODO: making a HLSL template as well? private static string GenerateDefaultShaderContent(string name) { return @"Shader """ + name + @""" -{ - Properties - { - _MainTex (""Texture"", 2D) = ""white"" {} - } - SubShader - { - Tags { ""RenderType""=""Opaque"" } - LOD 100 - - Pass { - CGPROGRAM - #pragma vertex vert - #pragma fragment frag - #include ""UnityCG.cginc"" - - struct appdata + Properties { - float4 vertex : POSITION; - float2 uv : TEXCOORD0; - }; - - struct v2f - { - float2 uv : TEXCOORD0; - float4 vertex : SV_POSITION; - }; - - sampler2D _MainTex; - float4 _MainTex_ST; - - v2f vert (appdata v) - { - v2f o; - o.vertex = UnityObjectToClipPos(v.vertex); - o.uv = TRANSFORM_TEX(v.uv, _MainTex); - return o; + _MainTex (""Texture"", 2D) = ""white"" {} } - - fixed4 frag (v2f i) : SV_Target + SubShader { - fixed4 col = tex2D(_MainTex, i.uv); - return col; + Tags { ""RenderType""=""Opaque"" } + LOD 100 + + Pass + { + CGPROGRAM + #pragma vertex vert + #pragma fragment frag + #include ""UnityCG.cginc"" + + struct appdata + { + float4 vertex : POSITION; + float2 uv : TEXCOORD0; + }; + + struct v2f + { + float2 uv : TEXCOORD0; + float4 vertex : SV_POSITION; + }; + + sampler2D _MainTex; + float4 _MainTex_ST; + + v2f vert (appdata v) + { + v2f o; + o.vertex = UnityObjectToClipPos(v.vertex); + o.uv = TRANSFORM_TEX(v.uv, _MainTex); + return o; + } + + fixed4 frag (v2f i) : SV_Target + { + fixed4 col = tex2D(_MainTex, i.uv); + return col; + } + ENDCG + } } - ENDCG - } - } -}"; + }"; } } } \ No newline at end of file diff --git a/UnityMcpBridge/Editor/Tools/ManageShader.cs.meta b/UnityMcpBridge/Editor/Tools/ManageShader.cs.meta new file mode 100644 index 0000000..89d10cd --- /dev/null +++ b/UnityMcpBridge/Editor/Tools/ManageShader.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bcf4f1f3110494344b2af9324cf5c571 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/UnityMcpBridge/Editor/UnityMcpBridge.cs b/UnityMcpBridge/Editor/UnityMcpBridge.cs index 9276c05..e6b46db 100644 --- a/UnityMcpBridge/Editor/UnityMcpBridge.cs +++ b/UnityMcpBridge/Editor/UnityMcpBridge.cs @@ -378,6 +378,7 @@ namespace UnityMcpBridge.Editor "manage_editor" => ManageEditor.HandleCommand(paramsObject), "manage_gameobject" => ManageGameObject.HandleCommand(paramsObject), "manage_asset" => ManageAsset.HandleCommand(paramsObject), + "manage_shader" => ManageShader.HandleCommand(paramsObject), "read_console" => ReadConsole.HandleCommand(paramsObject), "execute_menu_item" => ExecuteMenuItem.HandleCommand(paramsObject), _ => throw new ArgumentException(