Files
Cortex_Engine/src/Engine.AI/Stdio/McpStdioServer.cs
T
emil28092005 fb6e26a268 feat: modular HAL, Raylib backend, PBR shading, textures, 60 unit tests
- Replace hardcoded SDL3 windowing with IWindow/IInputState/Key abstractions
- Each render backend owns its window (Raylib GLFW, SDL3 for Vulkan)
- Raylib backend: DrawModelEx, custom GLSL shader with Fresnel, ACES
  tonemapping, gamma correction, hemisphere ambient
- Fix backface culling, mesh memory (NativeMemory.Alloc), texture loading
- Camera controllers use backend-agnostic Key enum (inverted yaw/strafe)
- Demo scene: 8 cubes, 7 spheres, torus knot OBJ with checker texture
- Extract ProceduralMesh + MeshMath from Program.cs to Engine.Graphics
- Vulkan backend deferred (compiles, untested, IWindow-compatible)
- 60 unit tests: ObjLoader, camera controllers, AiCommandProcessor,
  RenderBackendFactory, Timing, ProceduralMesh, MeshMath, Transform
- AGENTS.md for opencode integration
2026-06-17 13:49:12 +03:00

244 lines
8.8 KiB
C#

using System.Text;
using System.Text.Json;
using Engine.AI.Commands;
namespace Engine.AI.Stdio;
/// <summary>
/// A minimal stdio MCP server that routes JSON-RPC requests to an <see cref="AiCommandProcessor"/>.
/// This is suitable for clients like Claude Desktop that speak MCP over stdio.
/// </summary>
public sealed class McpStdioServer
{
private readonly AiCommandProcessor _processor;
private readonly JsonSerializerOptions _jsonOptions;
private readonly Dictionary<string, ToolDefinition> _tools;
public McpStdioServer(AiCommandProcessor processor)
{
_processor = processor;
_jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
_tools = new Dictionary<string, ToolDefinition>
{
["SpawnModel"] = new(
"Spawn a 3D model entity in the engine world.",
new JsonSchemaBuilder()
.AddRequiredString("name")
.AddRequiredString("modelPath")
.AddOptionalArray("position", "number", 3)
.AddOptionalArray("rotation", "number", 4)
.AddOptionalArray("scale", "number", 3)
.Build()),
["SetTransform"] = new(
"Update the transform of an existing entity by name.",
new JsonSchemaBuilder()
.AddRequiredString("name")
.AddOptionalArray("position", "number", 3)
.AddOptionalArray("rotation", "number", 4)
.AddOptionalArray("scale", "number", 3)
.Build()),
["SetMaterial"] = new(
"Update the material of an existing entity by name.",
new JsonSchemaBuilder()
.AddRequiredString("name")
.AddOptionalArray("albedo", "number", 3)
.AddOptionalNumber("roughness")
.AddOptionalNumber("metallic")
.AddOptionalString("texturePath")
.Build()),
["DeleteEntity"] = new(
"Delete an entity by name.",
new JsonSchemaBuilder()
.AddRequiredString("name")
.Build()),
["ListEntities"] = new(
"List all named entities in the ECS world.",
new JsonSchemaBuilder().Build()),
["CaptureScreenshot"] = new(
"Capture a screenshot of the current rendered frame and save it to disk. (No graphics context in stdio mode; returns requested path.)",
new JsonSchemaBuilder()
.AddOptionalString("outputPath")
.Build()),
["GetWorldState"] = new(
"Dump the current ECS world state as JSON.",
new JsonSchemaBuilder().Build())
};
}
public void Run()
{
var input = Console.OpenStandardInput();
var output = Console.OpenStandardOutput();
using var reader = new StreamReader(input, Encoding.UTF8);
using var writer = new StreamWriter(output, Encoding.UTF8) { AutoFlush = true };
while (true)
{
var line = reader.ReadLine();
if (line == null)
break;
if (string.IsNullOrWhiteSpace(line))
continue;
var response = HandleMessage(line);
if (response != null)
{
writer.WriteLine(JsonSerializer.Serialize(response, _jsonOptions));
}
}
}
private JsonElement? HandleMessage(string line)
{
using var document = JsonDocument.Parse(line);
var root = document.RootElement;
var method = root.GetProperty("method").GetString();
var id = root.TryGetProperty("id", out var idProp) ? (JsonElement?)idProp : null;
switch (method)
{
case "initialize":
return MakeResponse(id, new
{
protocolVersion = "2024-11-05",
capabilities = new { tools = new { } },
serverInfo = new { name = "CortexEngine", version = "0.1.0" }
});
case "notifications/initialized":
return null;
case "tools/list":
return MakeResponse(id, new { tools = _tools.Select(t => new { type = "function", function = new { name = t.Key, description = t.Value.Description, parameters = t.Value.Parameters } }).ToList() });
case "tools/call":
return HandleToolCall(id, root.GetProperty("params"));
case "ping":
return MakeResponse(id, new { });
default:
return MakeError(id, -32601, $"Method not found: {method}");
}
}
private JsonElement? HandleToolCall(JsonElement? id, JsonElement paramsElement)
{
var name = paramsElement.GetProperty("name").GetString();
var arguments = paramsElement.GetProperty("arguments");
if (!_tools.TryGetValue(name ?? string.Empty, out _))
return MakeError(id, -32601, $"Tool not found: {name}");
var command = BuildCommand(name!, arguments);
var result = _processor.Process(command);
return MakeResponse(id, new { content = new[] { new { type = "text", text = result.Message } }, isError = !result.Success });
}
private string BuildCommand(string toolName, JsonElement arguments)
{
var type = toolName switch
{
"SpawnModel" => "spawn_model",
"SetTransform" => "set_transform",
"SetMaterial" => "set_material",
"DeleteEntity" => "delete_entity",
"ListEntities" => "list_entities",
"CaptureScreenshot" => "capture_screenshot",
"GetWorldState" => "get_world_state",
_ => toolName.ToLowerInvariant()
};
var dict = new Dictionary<string, object> { ["type"] = type };
foreach (var property in arguments.EnumerateObject())
dict[property.Name] = ConvertArgument(property.Value);
return JsonSerializer.Serialize(dict, _jsonOptions);
}
private static object ConvertArgument(JsonElement element)
{
return element.ValueKind switch
{
JsonValueKind.String => element.GetString()!,
JsonValueKind.Number => element.GetDouble(),
JsonValueKind.True => true,
JsonValueKind.False => false,
JsonValueKind.Array => element.EnumerateArray().Select(ConvertArgument).ToList(),
JsonValueKind.Object => element.EnumerateObject().ToDictionary(p => p.Name, p => ConvertArgument(p.Value)),
_ => element.GetRawText()
};
}
private JsonElement? MakeResponse(JsonElement? id, object result)
{
if (id == null)
return null;
var json = JsonSerializer.Serialize(new { jsonrpc = "2.0", result, id = id.Value }, _jsonOptions);
using var doc = JsonDocument.Parse(json);
return doc.RootElement.Clone();
}
private JsonElement? MakeError(JsonElement? id, int code, string message)
{
if (id == null)
return null;
var json = JsonSerializer.Serialize(new { jsonrpc = "2.0", error = new { code, message }, id = id.Value }, _jsonOptions);
using var doc = JsonDocument.Parse(json);
return doc.RootElement.Clone();
}
private sealed record ToolDefinition(string Description, JsonElement Parameters);
private sealed class JsonSchemaBuilder
{
private readonly Dictionary<string, object> _properties = new();
private readonly List<string> _required = new();
private readonly string _type = "object";
public JsonSchemaBuilder AddRequiredString(string name)
{
_properties[name] = new { type = "string" };
_required.Add(name);
return this;
}
public JsonSchemaBuilder AddOptionalString(string name)
{
_properties[name] = new { type = "string" };
return this;
}
public JsonSchemaBuilder AddOptionalNumber(string name)
{
_properties[name] = new { type = "number" };
return this;
}
public JsonSchemaBuilder AddOptionalArray(string name, string itemType, int? minItems = null)
{
_properties[name] = new { type = "array", items = new { type = itemType }, minItems };
return this;
}
public JsonElement Build()
{
var dict = new Dictionary<string, object>
{
["type"] = _type,
["properties"] = _properties,
["required"] = _required
};
var json = JsonSerializer.Serialize(dict);
using var doc = JsonDocument.Parse(json);
return doc.RootElement.Clone();
}
}
}