feat: Step 6 MCP server bridge for AI agents
- Add ModelContextProtocol + ModelContextProtocol.AspNetCore 1.4.0 to Engine.AI. - Expose AI commands as MCP tools: spawn_model, set_transform, delete_entity, list_entities. - Add AiCommandQueue to marshal commands from the MCP server thread to the main thread. - Start in-process HTTP MCP server in Program.cs (Debug/Release only; excluded in ReleaseAOT). - Add --mcp-port CLI argument to configure the MCP server port. - Fix Flecs.NET package conditions to include ReleaseAOT config.
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
using System.ComponentModel;
|
||||
using System.Numerics;
|
||||
using Engine.AI.Commands;
|
||||
using ModelContextProtocol.Server;
|
||||
|
||||
namespace Engine.AI.Mcp;
|
||||
|
||||
/// <summary>
|
||||
/// MCP tools that expose engine commands to AI agents.
|
||||
/// </summary>
|
||||
[McpServerToolType]
|
||||
public sealed class EngineMcpTools
|
||||
{
|
||||
private readonly AiCommandQueue _queue;
|
||||
|
||||
public EngineMcpTools(AiCommandQueue queue)
|
||||
{
|
||||
_queue = queue;
|
||||
}
|
||||
|
||||
[McpServerTool, Description("Spawn a 3D model entity in the engine world.")]
|
||||
public Task<string> SpawnModel(
|
||||
string name,
|
||||
string modelPath,
|
||||
[Description("Optional position as [x, y, z] (default: 0,0,0)")] IReadOnlyList<double>? position = null,
|
||||
[Description("Optional rotation as [x, y, z, w] quaternion (default: identity)")] IReadOnlyList<double>? rotation = null,
|
||||
[Description("Optional scale as [x, y, z] (default: 1,1,1)")] IReadOnlyList<double>? scale = null)
|
||||
{
|
||||
var cmd = new SpawnModelCommand
|
||||
{
|
||||
Name = name,
|
||||
ModelPath = modelPath,
|
||||
Position = ToVector3(position, Vector3.Zero),
|
||||
Rotation = ToQuaternion(rotation, Quaternion.Identity),
|
||||
Scale = ToVector3(scale, Vector3.One)
|
||||
};
|
||||
|
||||
return EnqueueAndReturnMessage(cmd);
|
||||
}
|
||||
|
||||
[McpServerTool, Description("Update the transform of an existing entity by name.")]
|
||||
public Task<string> SetTransform(
|
||||
string name,
|
||||
[Description("Optional position as [x, y, z]")] IReadOnlyList<double>? position = null,
|
||||
[Description("Optional rotation as [x, y, z, w] quaternion")] IReadOnlyList<double>? rotation = null,
|
||||
[Description("Optional scale as [x, y, z]")] IReadOnlyList<double>? scale = null)
|
||||
{
|
||||
var cmd = new SetTransformCommand
|
||||
{
|
||||
Name = name,
|
||||
Position = ToVector3(position),
|
||||
Rotation = ToQuaternion(rotation),
|
||||
Scale = ToVector3(scale)
|
||||
};
|
||||
|
||||
return EnqueueAndReturnMessage(cmd);
|
||||
}
|
||||
|
||||
[McpServerTool, Description("Delete an entity by name.")]
|
||||
public Task<string> DeleteEntity(string name)
|
||||
{
|
||||
var cmd = new DeleteEntityCommand { Name = name };
|
||||
return EnqueueAndReturnMessage(cmd);
|
||||
}
|
||||
|
||||
[McpServerTool, Description("List all named entities in the ECS world.")]
|
||||
public Task<string> ListEntities()
|
||||
{
|
||||
var cmd = new ListEntitiesCommand();
|
||||
return EnqueueAndReturnMessage(cmd);
|
||||
}
|
||||
|
||||
private async Task<string> EnqueueAndReturnMessage(AiCommand command)
|
||||
{
|
||||
var result = await _queue.EnqueueAsync(command).ConfigureAwait(false);
|
||||
return result.Success
|
||||
? result.Message
|
||||
: throw new InvalidOperationException(result.Message);
|
||||
}
|
||||
|
||||
private static Vector3 ToVector3(IReadOnlyList<double>? values, Vector3 defaultValue)
|
||||
{
|
||||
if (values == null || values.Count == 0)
|
||||
return defaultValue;
|
||||
|
||||
if (values.Count != 3)
|
||||
throw new ArgumentException("Expected 3 values for Vector3.", nameof(values));
|
||||
|
||||
return new Vector3((float)values[0], (float)values[1], (float)values[2]);
|
||||
}
|
||||
|
||||
private static Vector3? ToVector3(IReadOnlyList<double>? values)
|
||||
{
|
||||
if (values == null || values.Count == 0)
|
||||
return null;
|
||||
|
||||
if (values.Count != 3)
|
||||
throw new ArgumentException("Expected 3 values for Vector3.", nameof(values));
|
||||
|
||||
return new Vector3((float)values[0], (float)values[1], (float)values[2]);
|
||||
}
|
||||
|
||||
private static Quaternion ToQuaternion(IReadOnlyList<double>? values, Quaternion defaultValue)
|
||||
{
|
||||
if (values == null || values.Count == 0)
|
||||
return defaultValue;
|
||||
|
||||
if (values.Count != 4)
|
||||
throw new ArgumentException("Expected 4 values for Quaternion.", nameof(values));
|
||||
|
||||
return new Quaternion((float)values[0], (float)values[1], (float)values[2], (float)values[3]);
|
||||
}
|
||||
|
||||
private static Quaternion? ToQuaternion(IReadOnlyList<double>? values)
|
||||
{
|
||||
if (values == null || values.Count == 0)
|
||||
return null;
|
||||
|
||||
if (values.Count != 4)
|
||||
throw new ArgumentException("Expected 4 values for Quaternion.", nameof(values));
|
||||
|
||||
return new Quaternion((float)values[0], (float)values[1], (float)values[2], (float)values[3]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user