Skip to main content
Glama
ManageShader.cs12.4 kB
using System; using System.IO; using System.Linq; using System.Text.RegularExpressions; using MCPForUnity.Editor.Helpers; using Newtonsoft.Json.Linq; using UnityEditor; using UnityEngine; namespace MCPForUnity.Editor.Tools { /// <summary> /// Handles CRUD operations for shader files within the Unity project. /// </summary> [McpForUnityTool("manage_shader", AutoRegister = false)] public static class ManageShader { /// <summary> /// Main handler for shader management actions. /// </summary> 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<bool>() ?? false; if (contentsEncoded && @params["encodedContents"] != null) { try { contents = DecodeBase64(@params["encodedContents"].ToString()); } catch (Exception e) { return new ErrorResponse($"Failed to decode shader contents: {e.Message}"); } } else { contents = @params["contents"]?.ToString(); } // Validate required parameters if (string.IsNullOrEmpty(action)) { return new ErrorResponse("Action parameter is required."); } if (string.IsNullOrEmpty(name)) { return new ErrorResponse("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 new ErrorResponse( $"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 { if (!Directory.Exists(fullPathDir)) { Directory.CreateDirectory(fullPathDir); // Refresh AssetDatabase to recognize new folders AssetDatabase.Refresh(); } } catch (Exception e) { return new ErrorResponse( $"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 new ErrorResponse( $"Unknown action: '{action}'. Valid actions are: create, read, update, delete." ); } } /// <summary> /// Decode base64 string to normal text /// </summary> private static string DecodeBase64(string encoded) { byte[] data = Convert.FromBase64String(encoded); return System.Text.Encoding.UTF8.GetString(data); } /// <summary> /// Encode text to base64 string /// </summary> 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 new ErrorResponse( $"Shader already exists at '{relativePath}'. Use 'update' action to modify." ); } // Add validation for shader name conflicts in Unity if (Shader.Find(name) != null) { return new ErrorResponse( $"A shader with name '{name}' already exists in the project. Choose a different name." ); } // Generate default content if none provided if (string.IsNullOrEmpty(contents)) { contents = GenerateDefaultShaderContent(name); } try { File.WriteAllText(fullPath, contents, new System.Text.UTF8Encoding(false)); AssetDatabase.ImportAsset(relativePath); AssetDatabase.Refresh(); // Ensure Unity recognizes the new shader return new SuccessResponse( $"Shader '{name}.shader' created successfully at '{relativePath}'.", new { path = relativePath } ); } catch (Exception e) { return new ErrorResponse($"Failed to create shader '{relativePath}': {e.Message}"); } } private static object ReadShader(string fullPath, string relativePath) { if (!File.Exists(fullPath)) { return new ErrorResponse($"Shader not found at '{relativePath}'."); } try { 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 { path = relativePath, contents = contents, // For large files, also include base64-encoded version encodedContents = isLarge ? EncodeBase64(contents) : null, contentsEncoded = isLarge, }; return new SuccessResponse( $"Shader '{Path.GetFileName(relativePath)}' read successfully.", responseData ); } catch (Exception e) { return new ErrorResponse($"Failed to read shader '{relativePath}': {e.Message}"); } } private static object UpdateShader( string fullPath, string relativePath, string name, string contents ) { if (!File.Exists(fullPath)) { return new ErrorResponse( $"Shader not found at '{relativePath}'. Use 'create' action to add a new shader." ); } if (string.IsNullOrEmpty(contents)) { return new ErrorResponse("Content is required for the 'update' action."); } try { File.WriteAllText(fullPath, contents, new System.Text.UTF8Encoding(false)); AssetDatabase.ImportAsset(relativePath); AssetDatabase.Refresh(); return new SuccessResponse( $"Shader '{Path.GetFileName(relativePath)}' updated successfully.", new { path = relativePath } ); } catch (Exception e) { return new ErrorResponse($"Failed to update shader '{relativePath}': {e.Message}"); } } private static object DeleteShader(string fullPath, string relativePath) { if (!File.Exists(fullPath)) { return new ErrorResponse($"Shader not found at '{relativePath}'."); } try { // Delete the asset through Unity's AssetDatabase first bool success = AssetDatabase.DeleteAsset(relativePath); if (!success) { return new ErrorResponse($"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 new SuccessResponse($"Shader '{Path.GetFileName(relativePath)}' deleted successfully."); } catch (Exception e) { return new ErrorResponse($"Failed to delete shader '{relativePath}': {e.Message}"); } } //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 { 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 } } }"; } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/CoplayDev/unity-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server