chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Editor.Security;
|
||||
using MCPForUnity.Editor.Services.AssetGen;
|
||||
using MCPForUnity.Editor.Services.AssetGen.Providers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.AssetGen
|
||||
{
|
||||
/// <summary>
|
||||
/// 2D image generation via an aggregator (fal.ai / OpenRouter). Triggered here (never from the
|
||||
/// GUI); the C# side reads the provider key from the secure store and runs the job. Returns a
|
||||
/// job_id immediately; the client polls the `status` action.
|
||||
/// </summary>
|
||||
[McpForUnityTool("generate_image", AutoRegister = false, Group = "asset_gen", RequiresPolling = true, PollAction = "status", MaxPollSeconds = 300)]
|
||||
public static class GenerateImage
|
||||
{
|
||||
public static object HandleCommand(JObject @params)
|
||||
{
|
||||
if (@params == null) return new ErrorResponse("Parameters cannot be null.");
|
||||
var p = new ToolParams(@params);
|
||||
string action = (p.Get("action") ?? string.Empty).ToLowerInvariant();
|
||||
try
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case "generate": return Generate(p);
|
||||
case "remove_background":
|
||||
return new ErrorResponse("remove_background is not implemented in this version.");
|
||||
case "status": return Status(p);
|
||||
case "cancel": return Cancel(p);
|
||||
case "list_providers": return ListProviders();
|
||||
case "": return new ErrorResponse("'action' parameter is required.");
|
||||
default:
|
||||
return new ErrorResponse($"Unknown action: '{action}'. Supported: generate, remove_background, status, cancel, list_providers.");
|
||||
}
|
||||
}
|
||||
catch (NotSupportedException nse)
|
||||
{
|
||||
return new ErrorResponse(nse.Message);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ErrorResponse(SecretRedactor.Scrub(e.Message));
|
||||
}
|
||||
}
|
||||
|
||||
private static object Generate(ToolParams p)
|
||||
{
|
||||
string provider = (p.Get("provider", "fal") ?? "fal").ToLowerInvariant();
|
||||
AssetGenProviders.Image(provider); // throws NotSupportedException for unknown providers
|
||||
|
||||
if (!SecureKeyStore.Current.Has(provider))
|
||||
return new ErrorResponse(AssetGenProviders.MissingKeyMessage(provider));
|
||||
|
||||
var req = new ImageGenRequest
|
||||
{
|
||||
Provider = provider,
|
||||
Mode = (p.Get("mode", "text") ?? "text").ToLowerInvariant(),
|
||||
Prompt = p.Get("prompt"),
|
||||
ImagePath = p.Get("imagePath"),
|
||||
ImageUrl = p.Get("imageUrl"),
|
||||
Model = p.Get("model"),
|
||||
Transparent = p.GetBool("transparent", false),
|
||||
AsSprite = p.GetBool("asSprite", true),
|
||||
Width = p.GetInt("width", 0) ?? 0,
|
||||
Height = p.GetInt("height", 0) ?? 0,
|
||||
Name = p.Get("name"),
|
||||
OutputFolder = p.Get("outputFolder"),
|
||||
};
|
||||
if (!NormalizeOutputFolder(req.OutputFolder, out req.OutputFolder, out string outputErr))
|
||||
return new ErrorResponse(outputErr);
|
||||
|
||||
if (req.Mode == "text" && string.IsNullOrWhiteSpace(req.Prompt))
|
||||
return new ErrorResponse("'prompt' is required for text mode.");
|
||||
if (req.Mode == "image" && string.IsNullOrWhiteSpace(req.ImageUrl))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(req.ImagePath))
|
||||
return new ErrorResponse("image mode requires 'image_url' or 'image_path'.");
|
||||
if (!LocalImage.ResolveExisting(req.ImagePath, out string absImg, out string imgErr))
|
||||
return new ErrorResponse(imgErr);
|
||||
req.ImagePath = absImg;
|
||||
}
|
||||
|
||||
AssetGenJob job = AssetGenJobManager.StartImageGeneration(req);
|
||||
if (job.State == AssetGenJobState.Failed)
|
||||
return new ErrorResponse(job.Error ?? "Failed to start generation.");
|
||||
|
||||
return new PendingResponse(
|
||||
$"Image generation started with '{provider}'. Poll the status action with this job_id.",
|
||||
pollIntervalSeconds: 2.0,
|
||||
data: new { job_id = job.JobId, provider, status = "pending" });
|
||||
}
|
||||
|
||||
private static bool NormalizeOutputFolder(string outputFolder, out string normalized, out string error)
|
||||
{
|
||||
normalized = outputFolder;
|
||||
error = null;
|
||||
if (string.IsNullOrWhiteSpace(outputFolder)) return true;
|
||||
if (AssetGenPaths.TryGetAssetsFolder(outputFolder, out normalized)) return true;
|
||||
error = "'output_folder' must resolve under the project's Assets folder.";
|
||||
return false;
|
||||
}
|
||||
|
||||
private static object Status(ToolParams p)
|
||||
{
|
||||
string jobId = p.Get("job_id");
|
||||
if (string.IsNullOrEmpty(jobId)) return new ErrorResponse("'job_id' is required for status.");
|
||||
AssetGenJob job = AssetGenJobManager.GetJob(jobId);
|
||||
if (job == null) return new ErrorResponse($"No job found with ID '{jobId}'.");
|
||||
|
||||
switch (job.State)
|
||||
{
|
||||
case AssetGenJobState.Done:
|
||||
return new SuccessResponse(
|
||||
$"Image generated: {job.AssetPath}",
|
||||
new { state = "done", asset_path = job.AssetPath, asset_guid = job.AssetGuid, progress = 1f });
|
||||
case AssetGenJobState.Failed:
|
||||
return new ErrorResponse(job.Error ?? "Generation failed.", new { state = "failed" });
|
||||
case AssetGenJobState.Canceled:
|
||||
return new SuccessResponse("Generation canceled.", new { state = "canceled" });
|
||||
default:
|
||||
return new PendingResponse(
|
||||
$"Image {job.State.ToString().ToLowerInvariant()} ({job.Progress:P0}).",
|
||||
pollIntervalSeconds: 2.0,
|
||||
data: new { job_id = job.JobId, state = job.State.ToString().ToLowerInvariant(), progress = job.Progress });
|
||||
}
|
||||
}
|
||||
|
||||
private static object Cancel(ToolParams p)
|
||||
{
|
||||
string jobId = p.Get("job_id");
|
||||
if (string.IsNullOrEmpty(jobId)) return new ErrorResponse("'job_id' is required for cancel.");
|
||||
return AssetGenJobManager.Cancel(jobId)
|
||||
? new SuccessResponse($"Cancel requested for job '{jobId}'.")
|
||||
: new ErrorResponse($"No cancelable job found with ID '{jobId}'.");
|
||||
}
|
||||
|
||||
private static object ListProviders()
|
||||
{
|
||||
var list = new List<object>();
|
||||
foreach (ProviderInfo info in AssetGenProviders.List())
|
||||
{
|
||||
if (info.Kind != "image") continue;
|
||||
list.Add(new { id = info.Id, kind = info.Kind, configured = info.Configured, capabilities = info.Capabilities });
|
||||
}
|
||||
return new SuccessResponse($"{list.Count} image provider(s).", new { providers = list });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a5e03ff5e4e040959fedaac555752094
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,147 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Editor.Security;
|
||||
using MCPForUnity.Editor.Services.AssetGen;
|
||||
using MCPForUnity.Editor.Services.AssetGen.Providers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.AssetGen
|
||||
{
|
||||
/// <summary>
|
||||
/// 3D model generation (Tripo/Meshy). Generation is triggered here (never from
|
||||
/// the GUI); the C# side reads the provider key from the secure store and runs the job.
|
||||
/// Long-running: returns a job_id immediately and the client polls the `status` action.
|
||||
/// </summary>
|
||||
[McpForUnityTool("generate_model", AutoRegister = false, Group = "asset_gen", RequiresPolling = true, PollAction = "status", MaxPollSeconds = 300)]
|
||||
public static class GenerateModel
|
||||
{
|
||||
public static object HandleCommand(JObject @params)
|
||||
{
|
||||
if (@params == null) return new ErrorResponse("Parameters cannot be null.");
|
||||
var p = new ToolParams(@params);
|
||||
string action = (p.Get("action") ?? string.Empty).ToLowerInvariant();
|
||||
try
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case "generate": return Generate(p);
|
||||
case "status": return Status(p);
|
||||
case "cancel": return Cancel(p);
|
||||
case "list_providers": return ListProviders();
|
||||
case "": return new ErrorResponse("'action' parameter is required.");
|
||||
default:
|
||||
return new ErrorResponse($"Unknown action: '{action}'. Supported: generate, status, cancel, list_providers.");
|
||||
}
|
||||
}
|
||||
catch (NotSupportedException nse)
|
||||
{
|
||||
return new ErrorResponse(nse.Message);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ErrorResponse(SecretRedactor.Scrub(e.Message));
|
||||
}
|
||||
}
|
||||
|
||||
private static object Generate(ToolParams p)
|
||||
{
|
||||
string provider = (p.Get("provider", "tripo") ?? "tripo").ToLowerInvariant();
|
||||
AssetGenProviders.Model(provider); // throws NotSupportedException for unimplemented providers
|
||||
|
||||
if (!SecureKeyStore.Current.Has(provider))
|
||||
return new ErrorResponse(AssetGenProviders.MissingKeyMessage(provider));
|
||||
|
||||
var req = new ModelGenRequest
|
||||
{
|
||||
Provider = provider,
|
||||
Mode = (p.Get("mode", "text") ?? "text").ToLowerInvariant(),
|
||||
Prompt = p.Get("prompt"),
|
||||
ImagePath = p.Get("imagePath"),
|
||||
ImageUrl = p.Get("imageUrl"),
|
||||
Format = (p.Get("format", "glb") ?? "glb").ToLowerInvariant(),
|
||||
TargetSize = p.GetFloat("targetSize", 1f) ?? 1f,
|
||||
Texture = p.GetBool("texture", true),
|
||||
Tier = p.Get("tier"),
|
||||
Name = p.Get("name"),
|
||||
OutputFolder = p.Get("outputFolder"),
|
||||
};
|
||||
if (!NormalizeOutputFolder(req.OutputFolder, out req.OutputFolder, out string outputErr))
|
||||
return new ErrorResponse(outputErr);
|
||||
|
||||
if (req.Mode == "text" && string.IsNullOrWhiteSpace(req.Prompt))
|
||||
return new ErrorResponse("'prompt' is required for text mode.");
|
||||
if (req.Mode == "image" && string.IsNullOrWhiteSpace(req.ImageUrl))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(req.ImagePath))
|
||||
return new ErrorResponse("image mode requires 'image_url' or 'image_path'.");
|
||||
if (provider == "tripo")
|
||||
return new ErrorResponse("Tripo image input requires a hosted 'image_url'; local 'image_path' is not supported for Tripo (use Meshy for local-image→3D).");
|
||||
if (!LocalImage.ResolveExisting(req.ImagePath, out string absImg, out string imgErr))
|
||||
return new ErrorResponse(imgErr);
|
||||
req.ImagePath = absImg;
|
||||
}
|
||||
|
||||
AssetGenJob job = AssetGenJobManager.StartModelGeneration(req);
|
||||
if (job.State == AssetGenJobState.Failed)
|
||||
return new ErrorResponse(job.Error ?? "Failed to start generation.");
|
||||
|
||||
return new PendingResponse(
|
||||
$"3D generation started with '{provider}'. Poll the status action with this job_id.",
|
||||
pollIntervalSeconds: 3.0,
|
||||
data: new { job_id = job.JobId, provider, status = "pending" });
|
||||
}
|
||||
|
||||
private static bool NormalizeOutputFolder(string outputFolder, out string normalized, out string error)
|
||||
{
|
||||
normalized = outputFolder;
|
||||
error = null;
|
||||
if (string.IsNullOrWhiteSpace(outputFolder)) return true;
|
||||
if (AssetGenPaths.TryGetAssetsFolder(outputFolder, out normalized)) return true;
|
||||
error = "'output_folder' must resolve under the project's Assets folder.";
|
||||
return false;
|
||||
}
|
||||
|
||||
private static object Status(ToolParams p)
|
||||
{
|
||||
string jobId = p.Get("job_id");
|
||||
if (string.IsNullOrEmpty(jobId)) return new ErrorResponse("'job_id' is required for status.");
|
||||
AssetGenJob job = AssetGenJobManager.GetJob(jobId);
|
||||
if (job == null) return new ErrorResponse($"No job found with ID '{jobId}'.");
|
||||
|
||||
switch (job.State)
|
||||
{
|
||||
case AssetGenJobState.Done:
|
||||
return new SuccessResponse(
|
||||
$"Generation complete: {job.AssetPath}",
|
||||
new { state = "done", asset_path = job.AssetPath, asset_guid = job.AssetGuid, progress = 1f });
|
||||
case AssetGenJobState.Failed:
|
||||
return new ErrorResponse(job.Error ?? "Generation failed.", new { state = "failed" });
|
||||
case AssetGenJobState.Canceled:
|
||||
return new SuccessResponse("Generation canceled.", new { state = "canceled" });
|
||||
default:
|
||||
return new PendingResponse(
|
||||
$"Generation {job.State.ToString().ToLowerInvariant()} ({job.Progress:P0}).",
|
||||
pollIntervalSeconds: 3.0,
|
||||
data: new { job_id = job.JobId, state = job.State.ToString().ToLowerInvariant(), progress = job.Progress });
|
||||
}
|
||||
}
|
||||
|
||||
private static object Cancel(ToolParams p)
|
||||
{
|
||||
string jobId = p.Get("job_id");
|
||||
if (string.IsNullOrEmpty(jobId)) return new ErrorResponse("'job_id' is required for cancel.");
|
||||
return AssetGenJobManager.Cancel(jobId)
|
||||
? new SuccessResponse($"Cancel requested for job '{jobId}'.")
|
||||
: new ErrorResponse($"No cancelable job found with ID '{jobId}'.");
|
||||
}
|
||||
|
||||
private static object ListProviders()
|
||||
{
|
||||
var list = new List<object>();
|
||||
foreach (ProviderInfo info in AssetGenProviders.List())
|
||||
list.Add(new { id = info.Id, kind = info.Kind, configured = info.Configured, capabilities = info.Capabilities });
|
||||
return new SuccessResponse($"{list.Count} provider(s).", new { providers = list });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fa7787add8344fd99adfceff3b5dd57e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,171 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Editor.Security;
|
||||
using MCPForUnity.Editor.Services.AssetGen;
|
||||
using MCPForUnity.Editor.Services.AssetGen.Http;
|
||||
using MCPForUnity.Editor.Services.AssetGen.Providers;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.AssetGen
|
||||
{
|
||||
/// <summary>
|
||||
/// 3D marketplace import (Sketchfab). Search/preview are read-only calls awaited directly:
|
||||
/// the handler is async so the underlying UnityWebRequest completes on the editor loop
|
||||
/// rather than blocking the main thread (a synchronous .GetResult() here deadlocks the
|
||||
/// editor — the request can only finish on a tick the blocked main thread can't run).
|
||||
/// Import downloads the model archive and unpacks it into the project as a long-running job
|
||||
/// (returns a job_id; the client polls the `status` action). The provider key is read from
|
||||
/// the secure store on the C# side and never transits the bridge.
|
||||
/// </summary>
|
||||
[McpForUnityTool("import_model", AutoRegister = false, Group = "asset_gen", RequiresPolling = true, PollAction = "status", MaxPollSeconds = 300)]
|
||||
public static class ImportModel
|
||||
{
|
||||
private const string Provider = "sketchfab";
|
||||
|
||||
// Test seam for the search/preview calls (import routes through the job manager's
|
||||
// own transport seam). Defaults to the production UnityWebRequest transport.
|
||||
internal static IHttpTransport TransportOverrideForTests;
|
||||
|
||||
public static async Task<object> HandleCommand(JObject @params)
|
||||
{
|
||||
if (@params == null) return new ErrorResponse("Parameters cannot be null.");
|
||||
var p = new ToolParams(@params);
|
||||
string action = (p.Get("action") ?? string.Empty).ToLowerInvariant();
|
||||
try
|
||||
{
|
||||
switch (action)
|
||||
{
|
||||
case "search": return await Search(p);
|
||||
case "preview": return await Preview(p);
|
||||
case "import": return Import(p);
|
||||
case "status": return Status(p);
|
||||
case "cancel": return Cancel(p);
|
||||
case "list_providers": return ListProviders();
|
||||
case "": return new ErrorResponse("'action' parameter is required.");
|
||||
default:
|
||||
return new ErrorResponse($"Unknown action: '{action}'. Supported: search, preview, import, status, cancel, list_providers.");
|
||||
}
|
||||
}
|
||||
catch (NotSupportedException nse)
|
||||
{
|
||||
return new ErrorResponse(nse.Message);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ErrorResponse(SecretRedactor.Scrub(e.Message));
|
||||
}
|
||||
}
|
||||
|
||||
private static IHttpTransport Transport() => TransportOverrideForTests ?? new UnityWebRequestTransport();
|
||||
|
||||
private static async Task<object> Search(ToolParams p)
|
||||
{
|
||||
string query = p.Get("query");
|
||||
if (string.IsNullOrWhiteSpace(query)) return new ErrorResponse("'query' is required for search.");
|
||||
if (!SecureKeyStore.Current.TryGet(Provider, out string key) || string.IsNullOrEmpty(key))
|
||||
return KeyError();
|
||||
|
||||
IMarketplaceProviderAdapter adapter = AssetGenProviders.Marketplace(Provider);
|
||||
string results = await adapter.SearchAsync(
|
||||
query, p.Get("categories"), p.GetBool("downloadable", true), p.GetInt("count"), p.Get("cursor"),
|
||||
key, Transport(), CancellationToken.None);
|
||||
return new SuccessResponse($"Search results for '{query}'.",
|
||||
new { provider = Provider, results = ParseOrRaw(results) });
|
||||
}
|
||||
|
||||
private static async Task<object> Preview(ToolParams p)
|
||||
{
|
||||
string uid = p.Get("uid");
|
||||
if (string.IsNullOrWhiteSpace(uid)) return new ErrorResponse("'uid' is required for preview.");
|
||||
if (!SecureKeyStore.Current.TryGet(Provider, out string key) || string.IsNullOrEmpty(key))
|
||||
return KeyError();
|
||||
|
||||
IMarketplaceProviderAdapter adapter = AssetGenProviders.Marketplace(Provider);
|
||||
string preview = await adapter.PreviewAsync(uid, key, Transport(), CancellationToken.None);
|
||||
return new SuccessResponse($"Preview for '{uid}'.",
|
||||
new { provider = Provider, uid, preview = ParseOrRaw(preview) });
|
||||
}
|
||||
|
||||
private static object Import(ToolParams p)
|
||||
{
|
||||
string uid = p.Get("uid");
|
||||
if (string.IsNullOrWhiteSpace(uid)) return new ErrorResponse("'uid' is required for import.");
|
||||
AssetGenProviders.Marketplace(Provider); // throws NotSupportedException for unimplemented providers
|
||||
|
||||
float targetSize = p.GetFloat("targetSize", 1f) ?? 1f;
|
||||
string name = p.Get("name");
|
||||
string outputFolder = p.Get("outputFolder");
|
||||
if (!string.IsNullOrWhiteSpace(outputFolder)
|
||||
&& !AssetGenPaths.TryGetAssetsFolder(outputFolder, out outputFolder))
|
||||
{
|
||||
return new ErrorResponse("'output_folder' must resolve under the project's Assets folder.");
|
||||
}
|
||||
|
||||
AssetGenJob job = AssetGenJobManager.StartMarketplaceImport(uid, targetSize, name, outputFolder);
|
||||
if (job.State == AssetGenJobState.Failed)
|
||||
return new ErrorResponse(job.Error ?? "Failed to start import.");
|
||||
|
||||
return new PendingResponse(
|
||||
$"Sketchfab import started for '{uid}'. Poll the status action with this job_id.",
|
||||
pollIntervalSeconds: 3.0,
|
||||
data: new { job_id = job.JobId, provider = Provider, status = "pending" });
|
||||
}
|
||||
|
||||
private static object Status(ToolParams p)
|
||||
{
|
||||
string jobId = p.Get("job_id");
|
||||
if (string.IsNullOrEmpty(jobId)) return new ErrorResponse("'job_id' is required for status.");
|
||||
AssetGenJob job = AssetGenJobManager.GetJob(jobId);
|
||||
if (job == null) return new ErrorResponse($"No job found with ID '{jobId}'.");
|
||||
|
||||
switch (job.State)
|
||||
{
|
||||
case AssetGenJobState.Done:
|
||||
return new SuccessResponse(
|
||||
$"Import complete: {job.AssetPath}",
|
||||
new { state = "done", asset_path = job.AssetPath, asset_guid = job.AssetGuid, progress = 1f });
|
||||
case AssetGenJobState.Failed:
|
||||
return new ErrorResponse(job.Error ?? "Import failed.", new { state = "failed" });
|
||||
case AssetGenJobState.Canceled:
|
||||
return new SuccessResponse("Import canceled.", new { state = "canceled" });
|
||||
default:
|
||||
return new PendingResponse(
|
||||
$"Import {job.State.ToString().ToLowerInvariant()} ({job.Progress:P0}).",
|
||||
pollIntervalSeconds: 3.0,
|
||||
data: new { job_id = job.JobId, state = job.State.ToString().ToLowerInvariant(), progress = job.Progress });
|
||||
}
|
||||
}
|
||||
|
||||
private static object Cancel(ToolParams p)
|
||||
{
|
||||
string jobId = p.Get("job_id");
|
||||
if (string.IsNullOrEmpty(jobId)) return new ErrorResponse("'job_id' is required for cancel.");
|
||||
return AssetGenJobManager.Cancel(jobId)
|
||||
? new SuccessResponse($"Cancel requested for job '{jobId}'.")
|
||||
: new ErrorResponse($"No cancelable job found with ID '{jobId}'.");
|
||||
}
|
||||
|
||||
private static object ListProviders()
|
||||
{
|
||||
var list = new List<object>();
|
||||
foreach (ProviderInfo info in AssetGenProviders.List())
|
||||
{
|
||||
if (info.Kind != "marketplace") continue;
|
||||
list.Add(new { id = info.Id, kind = info.Kind, configured = info.Configured, capabilities = info.Capabilities });
|
||||
}
|
||||
return new SuccessResponse($"{list.Count} provider(s).", new { providers = list });
|
||||
}
|
||||
|
||||
private static object KeyError()
|
||||
=> new ErrorResponse(AssetGenProviders.MissingKeyMessage(Provider));
|
||||
|
||||
private static object ParseOrRaw(string json)
|
||||
{
|
||||
if (string.IsNullOrEmpty(json)) return null;
|
||||
try { return JToken.Parse(json); } catch { return json; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 294fabfc32d2417ba2d6274ebcf3eb4b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,108 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using MCPForUnity.Editor.Helpers;
|
||||
using MCPForUnity.Editor.Security;
|
||||
using MCPForUnity.Editor.Services.AssetGen;
|
||||
using MCPForUnity.Editor.Services.AssetGen.Import;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEditor;
|
||||
|
||||
namespace MCPForUnity.Editor.Tools.AssetGen
|
||||
{
|
||||
/// <summary>
|
||||
/// Import a local 3D model file (already on disk — e.g. exported from Blender/Maya) into the
|
||||
/// Unity project. DCC-agnostic and key-free: the file is copied under Assets/ and run through
|
||||
/// the shared ModelImportPipeline (glTFast/FBX/OBJ/zip handling, scale-normalize, material
|
||||
/// settings). Placement into the scene is the caller's job (kept single-purpose).
|
||||
/// </summary>
|
||||
[McpForUnityTool("import_model_file", AutoRegister = false, Group = "asset_gen")]
|
||||
public static class ImportModelFile
|
||||
{
|
||||
private static readonly string[] SupportedExt = { ".fbx", ".obj", ".glb", ".gltf", ".zip" };
|
||||
|
||||
public static object HandleCommand(JObject @params)
|
||||
{
|
||||
if (@params == null) return new ErrorResponse("Parameters cannot be null.");
|
||||
var p = new ToolParams(@params);
|
||||
try
|
||||
{
|
||||
string source = p.Get("sourcePath");
|
||||
if (string.IsNullOrWhiteSpace(source))
|
||||
return new ErrorResponse("'source_path' is required.");
|
||||
|
||||
string srcAbs = ResolveSource(source);
|
||||
if (!File.Exists(srcAbs))
|
||||
return new ErrorResponse($"Source file not found: {source}");
|
||||
|
||||
string ext = Path.GetExtension(srcAbs).ToLowerInvariant();
|
||||
if (Array.IndexOf(SupportedExt, ext) < 0)
|
||||
return new ErrorResponse(
|
||||
$"Unsupported model extension '{ext}'. Supported: .fbx, .obj, .glb, .gltf, .zip.");
|
||||
|
||||
string baseName = p.Get("name");
|
||||
if (string.IsNullOrWhiteSpace(baseName))
|
||||
baseName = Path.GetFileNameWithoutExtension(srcAbs);
|
||||
|
||||
string destRel = StageUnderAssets(srcAbs, baseName, ext, p.Get("outputFolder"));
|
||||
AssetDatabase.Refresh();
|
||||
|
||||
var job = new AssetGenJob
|
||||
{
|
||||
TargetSize = p.GetFloat("targetSize", 1f) ?? 1f,
|
||||
AnimationType = p.Get("animationType"),
|
||||
};
|
||||
AssetGenJob result = ModelImportPipeline.ImportInto(job, destRel);
|
||||
|
||||
if (result == null || result.State == AssetGenJobState.Failed)
|
||||
return new ErrorResponse(result?.Error ?? "Import failed.");
|
||||
|
||||
return new SuccessResponse(
|
||||
$"Imported model: {result.AssetPath}",
|
||||
new { asset_path = result.AssetPath, asset_guid = result.AssetGuid });
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return new ErrorResponse(SecretRedactor.Scrub(e.Message));
|
||||
}
|
||||
}
|
||||
|
||||
private static string ResolveSource(string source)
|
||||
{
|
||||
string s = source.Replace('\\', '/');
|
||||
if (s == "Assets" || s.StartsWith("Assets/")) return AssetGenPaths.ToAbsolute(s);
|
||||
return s; // absolute path on disk
|
||||
}
|
||||
|
||||
private static string StageUnderAssets(string srcAbs, string baseName, string ext, string outputFolder)
|
||||
{
|
||||
string root = !string.IsNullOrWhiteSpace(outputFolder)
|
||||
? outputFolder
|
||||
: AssetGenPrefs.OutputRoot + "/Imported";
|
||||
if (!AssetGenPaths.TryGetAssetsFolder(root, out root))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(outputFolder))
|
||||
throw new ArgumentException("'output_folder' must resolve under the project's Assets folder.");
|
||||
root = AssetGenPrefs.DefaultOutputRoot + "/Imported";
|
||||
}
|
||||
|
||||
string absRoot = AssetGenPaths.ToAbsolute(root);
|
||||
Directory.CreateDirectory(absRoot);
|
||||
|
||||
string safe = SanitizeName(baseName);
|
||||
string fileName = safe + ext;
|
||||
string abs = Path.Combine(absRoot, fileName);
|
||||
int n = 1;
|
||||
while (File.Exists(abs)) { fileName = safe + "_" + n++ + ext; abs = Path.Combine(absRoot, fileName); }
|
||||
|
||||
File.Copy(srcAbs, abs);
|
||||
return (root.TrimEnd('/') + "/" + fileName).Replace('\\', '/');
|
||||
}
|
||||
|
||||
private static string SanitizeName(string raw)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw)) return "model";
|
||||
foreach (char c in Path.GetInvalidFileNameChars()) raw = raw.Replace(c, '_');
|
||||
return raw.Trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e71d7281f8e84c45802ed38d1cceed81
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user