Back to Blog
Feature Unity Code MCP Server · March 2026

Automate Your Unity Workflow with UnityCodeMcpServer Favourite Scripts

Go from manual Inspector clicking to one-click automation: let your AI agent build C# scripts, refine them in conversation, and save them as reusable Favourite Scripts you can run anytime.

The Problem: Repetitive Manual Work

Urban skyscrapers representing the repetitive, grid-like nature of manual Unity Editor tasks

Every Unity developer knows the routine. You have a batch of assets to create or update, so you open each one in the Inspector and fill in the fields. You have an import setting that’s wrong across dozens of files, so you fix them one by one. You have objects in a scene that need data wired up correctly, so you drag and drop until it’s done. The task isn’t hard - it’s just slow, and it comes back every time something changes.

These tasks aren’t hard. They’re just slow, error-prone, and they interrupt the work that actually matters. This post walks through four examples - from a one-liner data sync to complex procedural placement - and shows how to go from hand work to a one-click automation you can reuse forever:

  1. Sync ScriptableObjects from CSV - generate and update CarSO assets from a spreadsheet with one click.
  2. Fix Sprite Pixels Per Unit - find every sprite in the project with the wrong PPU and correct it in bulk.
  3. Assign ScriptableObjects by Type - wire LocationDefinition ScriptableObjects onto LocationComponent slots, enforcing type matching automatically.
  4. Procedural Tilemap Placement - pack randomized buildings along a pavement border with gap and buffer constraints, fully undoable.

Step 1: Let the AI Agent Do It

Unity Code MCP Server is open source and available on GitHub. Your AI agent (Copilot, Claude, or any MCP-compatible client) can execute C# scripts directly in the Unity Editor through the execute_csharp_script_in_unity_editor tool. Describe what you need in plain language, and the agent writes and runs the script for you - with full access to UnityEngine, UnityEditor, and every assembly in your project.

That’s a big improvement over hand-editing in the Inspector - but there’s a gap. The next time you need the same automation, the script is buried in yesterday’s chat history. You re-describe the problem, wait for the agent to regenerate it, and verify it works again.

Step 2: Save It as a Favourite Script

Favourite Scripts, new in v0.2-preview, closes that gap. The workflow becomes:

  1. Ask your agent to write a script that solves the problem.
  2. Refine it in conversation - adjust parameters, fix edge cases, verify the output.
  3. Ask the agent to save it - the agent writes the finished script directly to .unityCodeMcpServer/favouriteScripts.json at your project root.

From that point on, the script is available in Tools > UnityCodeMcpServer > Favourite Scripts - a dedicated Editor window where you can select, edit, and Run any saved script with a single click. No agent round-trip, no chat history digging.

The Favourite Scripts window uses the same Roslyn-based ScriptExecutionService that powers the MCP tool. Same execution engine, same full API access - just faster to reach.

Practical Examples

The four examples below are ordered from a simple warm-up to complex procedural placement. Each follows the same structure: the tedious manual approach, a natural-language prompt to the agent, and the resulting script worth saving as a favourite.

1. Create/Update Car ScriptableObjects from CSV

CSV spreadsheet with car data being synchronized to ScriptableObject assets in the Unity Editor

By hand

Open Cars.csv, read the columns, create a CarSO script by hand, then create an asset for each row - naming it, filling in every field in the Inspector, saving. Add a new row to the CSV next week and repeat for the new entry. Miss a field and the bug surfaces at runtime.

With the agent

“Read Assets/Data/Cars.csv, create a CarSO ScriptableObject class based on the columns, then create or update a CarSO asset in Assets/ScriptableObjects/Cars for each row. Name each file after the normalized Name column (lowercase, spaces replaced with underscores).”

Favourite Script

using System.IO;
using UnityEditor;

string csvPath = "Assets/Data/Cars.csv";
string outputFolder = "Assets/ScriptableObjects/Cars";

if (!Directory.Exists(outputFolder))
    AssetDatabase.CreateFolder("Assets/ScriptableObjects", "Cars");

string[] lines = File.ReadAllLines(
    Path.Combine(Application.dataPath, "..", csvPath));
if (lines.Length < 2) { Debug.LogWarning("CSV is empty."); return; }

string[] headers = lines[0].Split(',');
int created = 0, updated = 0;

for (int row = 1; row < lines.Length; row++)
{
    string[] cols = lines[row].Split(',');
    if (cols.Length < headers.Length) continue;

    var data = new System.Collections.Generic.Dictionary<string, string>();
    for (int i = 0; i < headers.Length; i++)
        data[headers[i].Trim()] = cols[i].Trim();

    string name = data["Name"];
    string normalized = name.ToLower().Replace(" ", "_");
    string assetPath = $"{outputFolder}/{normalized}.asset";

    var so = AssetDatabase.LoadAssetAtPath<CarSO>(assetPath);
    bool isNew = so == null;
    if (isNew) so = ScriptableObject.CreateInstance<CarSO>();

    so.Name  = name;
    so.Make  = data["Make"];
    so.Model = data["Model"];
    so.TopSpeed = int.TryParse(data["TopSpeed"], out int s) ? s : 0;

    if (isNew) { AssetDatabase.CreateAsset(so, assetPath); created++; }
    else       { EditorUtility.SetDirty(so); updated++; }
}

AssetDatabase.SaveAssets();
Debug.Log($"Cars sync complete - created: {created}, updated: {updated}.");

“Save this as a favourite script called ‘Sync Cars from CSV’.”

Whenever the CSV changes - new models, corrected stats - one click keeps all assets in sync without touching the Inspector.

2. Fix Sprite Pixels Per Unit Across the Whole Project

Sprite import settings showing Pixels Per Unit configuration for correct tilemap alignment

By hand

Select each sprite in the Project window, open the Inspector, check “Pixels Per Unit”, type the correct value, click Apply. Miss a dozen imported sprites and your tilemap snaps come out wrong.

With the agent

“Find every sprite in the project that has Pixels Per Unit set to something other than 8 and set it to 8.”

Favourite Script

using UnityEditor;

const float TARGET_PPU = 8f;
int fixedCount = 0;
int skipped = 0;

var guids = AssetDatabase.FindAssets("t:Texture2D", new[] { "Assets/Images/Sprites" });
foreach (var guid in guids)
{
    var path = AssetDatabase.GUIDToAssetPath(guid);
    var importer = AssetImporter.GetAtPath(path) as TextureImporter;
    if (importer == null || importer.textureType != TextureImporterType.Sprite)
    {
        Debug.Log($"SKIPPED (not a sprite): {path}");
        skipped++;
        continue;
    }
    if (Mathf.Approximately(importer.spritePixelsPerUnit, TARGET_PPU))
    {
        Debug.Log($"SKIPPED (already correct PPU): {path}");
        skipped++;
        continue;
    }
    importer.spritePixelsPerUnit = TARGET_PPU;
    AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);
    Debug.Log($"FIXED: {path}");
    fixedCount++;
}
Debug.Log($"\n=== PPU fix complete ===\nUpdated: {fixedCount}\nSkipped: {skipped}");

“Save this as a favourite script called ‘Fix Sprite PPU to 8’.”

The agent updates favouriteScripts.json directly. Run it after every sprite import and your whole project stays consistent without touching a single Inspector field.

3. Randomized Data Assignment Across Scene Objects

By hand

Open the Locations prefab, find each Location GameObject in the hierarchy, drag a random LocationDefinition ScriptableObject into the LocationComponent Inspector slot, then verify the LocationComponent Type matches the LocationDefinition Type. Miss one and the bug takes hours to find.

With the agent

“Assign a unique LocationDefinition asset to each LocationComponent in my Locations prefab. Make sure the LocationComponent’s Type field matches its assigned LocationDefinition’s Type.”

Favourite Script

using System.Collections.Generic;
using System.Linq;
using UnityEditor;

string prefabPath = "Assets/Prefabs/Locations.prefab";
string definitionsFolder = "Assets/ScriptableObjects/LocationDefinitions";

var root = PrefabUtility.LoadPrefabContents(prefabPath);
try
{
    var guids = AssetDatabase.FindAssets("t:LocationDefinition", new[] { definitionsFolder });
    var allDefinitions = guids
        .Select(g => AssetDatabase.LoadAssetAtPath<LocationDefinition>(
            AssetDatabase.GUIDToAssetPath(g)))
        .Where(d => d != null)
        .ToList();

    var pool = new List<LocationDefinition>(allDefinitions);
    var rng = new System.Random();
    int assigned = 0;
    int typeMismatch = 0;

    foreach (var lc in root.GetComponentsInChildren<LocationComponent>(true))
    {
        var matchingIndices = pool
            .Select((d, i) => (d, i))
            .Where(x => x.d.Type == lc.Type)
            .Select(x => x.i)
            .ToList();

        if (matchingIndices.Count == 0)
        {
            Debug.LogWarning($"No matching LocationDefinition for '{lc.gameObject.name}' (Type={lc.Type}).");
            typeMismatch++;
            continue;
        }

        int idx = matchingIndices[rng.Next(matchingIndices.Count)];
        lc.Definition = pool[idx];
        pool.RemoveAt(idx);
        assigned++;
    }

    PrefabUtility.SaveAsPrefabAsset(root, prefabPath);
    Debug.Log($"Location assignment complete - assigned: {assigned}, type mismatches: {typeMismatch}.");
}
finally
{
    PrefabUtility.UnloadPrefabContents(root);
}

“The output looks right. Save this as a favourite script called ‘Assign Location Definitions’.”

Every time the prefab changes or new Location objects are added, one click gives you a correctly typed assignment with no mismatches.

4. Placeholder Object Placement with Tilemap Constraints

Urban skyscrapers representing the repetitive, grid-like nature of manual Unity Editor tasks

By hand

You’re placing buildings along the border of a pavement tilemap. Each building needs randomised dimensions, a buffer zone so it doesn’t clip into non-pavement terrain, a minimum gap from its neighbours, and heights that taper toward the edges to produce a believable city skyline. You place one, eyeball the spacing, nudge it, place the next. A single layout change means repositioning dozens of objects.

With the agent

“Place ProBuilder cube buildings along the pavement border. Randomise widths (5–10 units) and depths (5–10 units). Height should create a skyline - taller near the map centre (10–18 units) and shorter at the edges (3–7 units). Use a 2-tile pavement buffer around each building and a 1-unit minimum gap between buildings. Clear existing buildings first. Make it undoable.

Building prefab: ‘Assets/Prefabs/Building.prefab’
Grid GameObject: ‘Grid16x16’
Pavement Tilemap: ‘Pavement’”

It still takes a few rounds of conversation to tune - adjusting constants, checking the skyline looks right, verifying border placement. When you’re satisfied:

“Looks good. Save this as a favourite script called ‘Place Buildings on Pavement’.”

Favourite Script

using UnityEngine.Tilemaps;
using UnityEditor.SceneManagement;

// ---- CONSTRAINTS: Modify these to change placement behavior ----
const int MIN_WIDTH = 5;              // Minimum building width in world units
const int MAX_WIDTH = 10;             // Maximum building width in world units
const int MIN_HEIGHT_CENTER = 50;     // Min height at map center (skyline peak)
const int MAX_HEIGHT_CENTER = 100;     // Max height at map center
const int MIN_HEIGHT_EDGE = 8;        // Min height at map edges
const int MAX_HEIGHT_EDGE = 15;        // Max height at map edges
const int MIN_DEPTH = 5;              // Minimum building depth in world units
const int MAX_DEPTH = 10;             // Maximum building depth in world units
const int BUFFER_TILES = 2;           // Pavement tile buffer around each building
const int CANDIDATE_POOL_SIZE = 2000; // Number of random building dimension candidates
const float MIN_GAP = 1f;             // Minimum gap between buildings in world units
// ----

var gridGO = GameObject.Find("Grid16x16");
if (gridGO == null) { Debug.LogError("Grid16x16 not found!"); return; }
var pavementTM = gridGO.transform.Find("Pavement").GetComponent<Tilemap>();
if (pavementTM == null) { Debug.LogError("Pavement tilemap not found!"); return; }
var buildingsGO = GameObject.Find("Buildings");
if (buildingsGO == null) { Debug.LogError("Buildings GameObject not found!"); return; }

// Load Building prefab and cache reflection handles for ProBuilderShape
var buildingPrefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Prefabs/Building.prefab");
if (buildingPrefab == null) { Debug.LogError("Building.prefab not found!"); return; }

System.Type shapeType = null;
System.Reflection.PropertyInfo sizePropInfo = null;
System.Reflection.MethodInfo rebuildMethod = null;
foreach (var c in buildingPrefab.GetComponents<Component>())
{
    if (c != null && c.GetType().Name == "ProBuilderShape")
    {
        shapeType = c.GetType();
        sizePropInfo = shapeType.GetProperty("size",
            System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
        rebuildMethod = shapeType.GetMethod("Rebuild",
            System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance,
            null, System.Type.EmptyTypes, null);
        break;
    }
}
if (shapeType == null || sizePropInfo == null || rebuildMethod == null)
{
    Debug.LogError("Failed to resolve ProBuilderShape reflection handles!");
    return;
}

pavementTM.CompressBounds();
var cbounds = pavementTM.cellBounds;
int xMin = cbounds.xMin, xMax = cbounds.xMax;
int yMin = cbounds.yMin, yMax = cbounds.yMax;
int gridW = xMax - xMin;
int gridH = yMax - yMin;
var cellSz = pavementTM.cellSize;

// Build prefix sum for O(1) rectangle paved-count queries
var prefSum = new int[gridW + 1, gridH + 1];
for (int x = 0; x < gridW; x++)
    for (int y = 0; y < gridH; y++)
    {
        int val = pavementTM.GetTile(new Vector3Int(x + xMin, y + yMin, 0)) != null ? 1 : 0;
        prefSum[x + 1, y + 1] = val + prefSum[x, y + 1] + prefSum[x + 1, y] - prefSum[x, y];
    }

Func<int, int, int, int, int> RectPaved = (gx1, gy1, gx2, gy2) =>
{
    gx1 = Mathf.Clamp(gx1, 0, gridW);
    gy1 = Mathf.Clamp(gy1, 0, gridH);
    gx2 = Mathf.Clamp(gx2, 0, gridW);
    gy2 = Mathf.Clamp(gy2, 0, gridH);
    if (gx1 >= gx2 || gy1 >= gy2) return 0;
    return prefSum[gx2, gy2] - prefSum[gx1, gy2] - prefSum[gx2, gy1] + prefSum[gx1, gy1];
};

// World center of pavement
var centerWorld = pavementTM.CellToWorld(new Vector3Int((xMin + xMax) / 2, (yMin + yMax) / 2, 0));
float wcx = centerWorld.x, wcz = centerWorld.z;
var corner1 = pavementTM.CellToWorld(new Vector3Int(xMin, yMin, 0));
var corner2 = pavementTM.CellToWorld(new Vector3Int(xMax, yMax, 0));
float maxDist = Mathf.Max(
    Vector2.Distance(new Vector2(wcx, wcz), new Vector2(corner1.x, corner1.z)),
    Vector2.Distance(new Vector2(wcx, wcz), new Vector2(corner2.x, corner2.z)));

// Clear existing buildings
for (int i = buildingsGO.transform.childCount - 1; i >= 0; i--)
    Undo.DestroyObjectImmediate(buildingsGO.transform.GetChild(i).gameObject);

var rng = new System.Random(42);

// Generate random building candidates as tile footprints, sorted by volume desc
var candidates = new List<(int tw, int td)>();
for (int i = 0; i < CANDIDATE_POOL_SIZE; i++)
{
    int W = rng.Next(MIN_WIDTH, MAX_WIDTH + 1);
    int D = rng.Next(MIN_DEPTH, MAX_DEPTH + 1);
    int tw = (W + (int)cellSz.x - 1) / (int)cellSz.x;
    int td = (D + (int)cellSz.y - 1) / (int)cellSz.y;
    candidates.Add((tw, td));
}
candidates.Sort((a, b) => (b.tw * b.td).CompareTo(a.tw * a.td));

// Cache valid border corners per footprint size (prefix-sum O(1) checks)
var cornerCache = new Dictionary<(int, int), List<(int gx, int gy)>>();

List<(int gx, int gy)> GetCorners(int tw, int td)
{
    var key = (tw, td);
    if (cornerCache.ContainsKey(key)) return cornerCache[key];
    var list = new List<(int gx, int gy)>();
    int buf = BUFFER_TILES;
    int innerArea = (tw + 2 * buf) * (td + 2 * buf);
    int outerTotal = (tw + 2 * buf + 2) * (td + 2 * buf + 2);

    for (int gx = 0; gx <= gridW - tw; gx++)
        for (int gy = 0; gy <= gridH - td; gy++)
        {
            if (RectPaved(gx - buf, gy - buf, gx + tw + buf, gy + td + buf) < innerArea) continue;
            int outerPaved = RectPaved(gx - buf - 1, gy - buf - 1, gx + tw + buf + 1, gy + td + buf + 1);
            if (outerPaved >= outerTotal) continue;
            list.Add((gx, gy));
        }

    for (int i = list.Count - 1; i > 0; i--)
    {
        int j = rng.Next(i + 1);
        var tmp = list[i]; list[i] = list[j]; list[j] = tmp;
    }
    cornerCache[key] = list;
    return list;
}

var placed = new List<(float cx, float cz, float hw, float hd)>();
int placedCount = 0;

foreach (var (tw, td) in candidates)
{
    float W = tw * cellSz.x;
    float D = td * cellSz.y;
    float hw = W * 0.5f;
    float hd = D * 0.5f;
    var corners = GetCorners(tw, td);

    for (int i = 0; i < corners.Count; i++)
    {
        var (gx, gy) = corners[i];
        var wp = pavementTM.CellToWorld(new Vector3Int(gx + xMin, gy + yMin, 0));
        float cx = wp.x + hw;
        float cz = wp.z + hd;

        bool conflict = false;
        foreach (var pb in placed)
            if (Mathf.Abs(cx - pb.cx) < hw + pb.hw + MIN_GAP &&
                Mathf.Abs(cz - pb.cz) < hd + pb.hd + MIN_GAP) { conflict = true; break; }
        if (conflict) continue;

        // Skyline height: taller near center, shorter at edges
        float dx = cx - wcx;
        float dz = cz - wcz;
        float dist = Mathf.Sqrt(dx * dx + dz * dz);
        float t = maxDist > 0 ? Mathf.Clamp01(dist / maxDist) : 0f;
        float minH = Mathf.Lerp((float)MIN_HEIGHT_CENTER, (float)MIN_HEIGHT_EDGE, t);
        float maxH = Mathf.Lerp((float)MAX_HEIGHT_CENTER, (float)MAX_HEIGHT_EDGE, t);
        float H = minH + (float)rng.NextDouble() * (maxH - minH);

        // Instantiate Building prefab and resize via ProBuilderShape
        var go = (GameObject)PrefabUtility.InstantiatePrefab(buildingPrefab);
        go.name = $"Building_{placedCount}";
        go.transform.SetParent(buildingsGO.transform, false);
        go.transform.position = new Vector3(cx, H * 0.5f, cz);

        var shapeComp = go.GetComponent(shapeType);
        if (shapeComp != null)
        {
            sizePropInfo.SetValue(shapeComp, new Vector3(W, H, D));
            rebuildMethod.Invoke(shapeComp, null);
        }

        Undo.RegisterCreatedObjectUndo(go, "Place Building");
        placed.Add((cx, cz, hw, hd));
        placedCount++;
        break;
    }
}

EditorSceneManager.MarkSceneDirty(EditorSceneManager.GetActiveScene());
Debug.Log($"Done! Placed {placedCount} buildings from Building.prefab with skyline heights.");

The constants at the top are your control panel. Change MAX_HEIGHT_CENTER from 18 to 30, hit Run, and see a taller downtown skyline packed along the border. This is the kind of tool that takes multiple agent iterations to assemble correctly. Once it’s saved, you own it permanently.


Getting Started

  1. Install or update to Unity Code MCP Server v0.2-preview.
  2. Ask your AI agent to solve a repetitive Unity task - it will use execute_csharp_script_in_unity_editor to run the solution.
  3. Refine the script in conversation until you’re happy with the result.
  4. Tell the agent: “Save this as a favourite script called [name].” The agent writes it directly to .unityCodeMcpServer/favouriteScripts.json.
  5. Open Tools > UnityCodeMcpServer > Favourite Scripts to browse, edit, and re-run your saved scripts anytime.

The JSON file is plain text - commit it to source control and your whole team shares the same automation library.

Unity Code MCP Server is open source under the MIT license. Browse the code, open issues, or contribute at github.com/Signal-Loop/UnityCodeMCPServer.