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:
emil28092005
2026-06-16 19:59:19 +03:00
parent 2f21ffada4
commit a9c783d204
8 changed files with 314 additions and 20 deletions
+71 -14
View File
@@ -2,6 +2,9 @@ using System;
using System.IO;
using System.Numerics;
using Engine.AI;
#if !RELEASE_AOT
using Engine.AI.Mcp;
#endif
using Engine.Core;
using Engine.Core.Components;
using Engine.Graphics;
@@ -12,23 +15,24 @@ namespace CortexEngine.App;
class Program
{
static void Main(string[] args)
static async Task Main(string[] args)
{
Console.WriteLine("Cortex Engine Step 5Starting up...");
Console.WriteLine("Cortex Engine Step 6MCP integration...");
try
{
using var world = World.Create();
using var window = new Sdl3Window("Cortex Engine — Step 5", 1280, 720);
using var window = new Sdl3Window("Cortex Engine — Step 6", 1280, 720);
var timing = new Timing();
var input = new InputMapping();
using var vulkan = new VulkanContext(window, enableValidation: false);
using var swapchain = new Swapchain(vulkan);
using var renderer = new MeshRenderer(vulkan, swapchain);
var ai = new AiCommandProcessor(world, LoadModel);
var processor = new AiCommandProcessor(world, LoadModel);
var queue = new AiCommandQueue(processor);
var modelPath = FindModelPath(args);
var (modelPath, mcpPort) = ParseArgs(args);
var mesh = LoadModel(modelPath);
var model = world.Entity("Model")
@@ -45,12 +49,28 @@ class Program
0.1f,
100.0f));
// Demo: AI agent commands.
// Demo: local AI commands processed on the main thread.
Console.WriteLine("AI demo commands:");
Console.WriteLine(ai.Process("""{ "type": "list_entities" }""").Message);
Console.WriteLine(ai.Process("""{ "type": "spawn_model", "name": "SecondCube", "modelPath": "Content/cube.obj", "position": [0.8, 0, 0], "scale": [0.3, 0.3, 0.3] }""").Message);
Console.WriteLine(ai.Process("""{ "type": "list_entities" }""").Message);
Console.WriteLine(ai.Process("""{ "type": "set_transform", "name": "SecondCube", "position": [0.8, 0.5, 0], "rotation": [0, 0, 0, 1], "scale": [0.3, 0.3, 0.3] }""").Message);
Console.WriteLine(processor.Process("""{ "type": "list_entities" }""").Message);
Console.WriteLine(processor.Process("""{ "type": "spawn_model", "name": "SecondCube", "modelPath": "Content/cube.obj", "position": [0.8, 0, 0], "scale": [0.3, 0.3, 0.3] }""").Message);
Console.WriteLine(processor.Process("""{ "type": "list_entities" }""").Message);
Console.WriteLine(processor.Process("""{ "type": "set_transform", "name": "SecondCube", "position": [0.8, 0.5, 0], "rotation": [0, 0, 0, 1], "scale": [0.3, 0.3, 0.3] }""").Message);
#if !RELEASE_AOT
// Start the MCP server in the background so AI agents can connect via HTTP.
var mcpApp = McpEngineServerHost.Create(args, queue, port: mcpPort);
var mcpTask = mcpApp.RunAsync();
_ = mcpTask.ContinueWith(t =>
{
if (t.IsFaulted)
Console.WriteLine($"MCP server error: {t.Exception?.GetBaseException().Message}");
else if (t.IsCanceled)
Console.WriteLine("MCP server canceled.");
else
Console.WriteLine("MCP server stopped.");
}, TaskScheduler.Default);
Console.WriteLine($"MCP server starting on http://localhost:{mcpPort}");
#endif
var frames = 0;
var lastFpsTime = 0.0;
@@ -62,8 +82,11 @@ class Program
timing.Tick();
window.PumpEvents();
input.BeginFrame();
// Note: SDL events are already polled in PumpEvents.
// In a real engine, the window would expose an event iterator.
// Drain any commands that arrived from the MCP server.
var processed = queue.ProcessPending();
if (processed > 0)
Console.WriteLine($"Processed {processed} AI command(s)");
if (window.Width != lastWidth || window.Height != lastHeight)
{
@@ -89,6 +112,12 @@ class Program
}
Console.WriteLine("Shutting down...");
#if !RELEASE_AOT
await mcpApp.StopAsync();
await mcpTask;
#else
await Task.CompletedTask;
#endif
}
catch (Exception ex)
{
@@ -105,10 +134,38 @@ class Program
: ObjLoader.Load(path, new Vector3(0.7f, 0.6f, 0.5f));
}
private static (string modelPath, int mcpPort) ParseArgs(string[] args)
{
var modelPath = FindModelPath(args);
var mcpPort = 5000;
for (var i = 0; i < args.Length; i++)
{
if (args[i] == "--mcp-port" && i + 1 < args.Length && int.TryParse(args[i + 1], out var port))
{
mcpPort = port;
break;
}
}
return (modelPath, mcpPort);
}
private static string FindModelPath(string[] args)
{
if (args.Length > 0 && File.Exists(args[0]))
return args[0];
// Skip recognized flags so they are not treated as a model path.
for (var i = 0; i < args.Length; i++)
{
var arg = args[i];
if (arg == "--mcp-port")
{
i++; // skip the value
continue;
}
if (File.Exists(arg))
return arg;
}
var candidates = new[]
{