diff --git a/BrickController2.sln b/BrickController2.sln
index 6eb8555c..d0133806 100644
--- a/BrickController2.sln
+++ b/BrickController2.sln
@@ -19,6 +19,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{5AFB0DE9
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BrickController2.Tools", "BrickController2\BrickController2.Tools\BrickController2.Tools.csproj", "{C127AB50-E104-2345-5239-13258E2B7474}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MCPSercer", "MCPSercer", "{12E3959D-03BB-49C4-B382-2188D5CCD111}"
+ ProjectSection(SolutionItems) = preProject
+ BrickController2\MCPServer\claude_desktop_config.json = BrickController2\MCPServer\claude_desktop_config.json
+ BrickController2\MCPServer\mcp_http_bridge.py = BrickController2\MCPServer\mcp_http_bridge.py
+ EndProjectSection
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -126,6 +132,7 @@ Global
GlobalSection(NestedProjects) = preSolution
{7981CBBF-BB2F-4AF4-BEF3-3CF0C92DBB23} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{C127AB50-E104-2345-5239-13258E2B7474} = {5AFB0DE9-536E-4C5D-831A-421ADC3EF3D9}
+ {12E3959D-03BB-49C4-B382-2188D5CCD111} = {5AFB0DE9-536E-4C5D-831A-421ADC3EF3D9}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {024A01F8-4022-4F9E-A006-AC2CABB04620}
diff --git a/BrickController2/BrickController2.Android/BrickController2.Android.csproj b/BrickController2/BrickController2.Android/BrickController2.Android.csproj
index 4ae7e8b9..81d4dd55 100644
--- a/BrickController2/BrickController2.Android/BrickController2.Android.csproj
+++ b/BrickController2/BrickController2.Android/BrickController2.Android.csproj
@@ -28,7 +28,7 @@
-
+
diff --git a/BrickController2/BrickController2.Android/Extensions/InputDeviceExtensions.cs b/BrickController2/BrickController2.Android/Extensions/InputDeviceExtensions.cs
deleted file mode 100644
index 1ddd31af..00000000
--- a/BrickController2/BrickController2.Android/Extensions/InputDeviceExtensions.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using Android.Views;
-
-namespace BrickController2.Droid.Extensions;
-
-public static class InputDeviceExtensions
-{
- ///
- /// Get an unique persistant identifier string for the given inputdevice
- ///
- /// inputdevice
- /// deviceidentifier
- public static string GetUniquePersistentDeviceId(this InputDevice? inputDevice) =>
- // https://developer.android.com/develop/ui/views/touch-and-input/game-controllers/multiple-controllers
- // Note: On devices running Android 4.1(API level 16) and higher, you can obtain an input device’s descriptor using getDescriptor(), which returns a unique persistent
- // string value for the input device.Unlike a device ID, the descriptor value won't change even if the input device is disconnected, reconnected, or reconfigured.
- inputDevice?.Descriptor ?? "NoDescriptor";
-}
diff --git a/BrickController2/BrickController2.Android/PlatformServices/DI/PlatformServicesModule.cs b/BrickController2/BrickController2.Android/PlatformServices/DI/PlatformServicesModule.cs
index 1fb4fdf6..10655614 100644
--- a/BrickController2/BrickController2.Android/PlatformServices/DI/PlatformServicesModule.cs
+++ b/BrickController2/BrickController2.Android/PlatformServices/DI/PlatformServicesModule.cs
@@ -7,12 +7,14 @@
using BrickController2.Droid.PlatformServices.GameController;
using BrickController2.Droid.PlatformServices.Infrared;
using BrickController2.Droid.PlatformServices.Localization;
+using BrickController2.Droid.PlatformServices.ModelContextProtocol;
using BrickController2.Droid.PlatformServices.Permission;
using BrickController2.Droid.PlatformServices.SharedFileStorage;
using BrickController2.PlatformServices.BluetoothLE;
using BrickController2.PlatformServices.GameController;
using BrickController2.PlatformServices.Infrared;
using BrickController2.PlatformServices.Localization;
+using BrickController2.PlatformServices.ModelContextProtocol;
using BrickController2.PlatformServices.Permission;
using BrickController2.PlatformServices.SharedFileStorage;
@@ -32,6 +34,7 @@ protected override void Load(ContainerBuilder builder)
builder.RegisterType().As().InstancePerDependency();
builder.RegisterType().As().SingleInstance();
builder.RegisterType().As().SingleInstance();
+ builder.RegisterType().AsSelf().As().SingleInstance();
}
}
}
\ No newline at end of file
diff --git a/BrickController2/BrickController2.Android/PlatformServices/GameController/GameControllerService.cs b/BrickController2/BrickController2.Android/PlatformServices/GameController/GameControllerService.cs
index 7e2f2988..baf88cb4 100644
--- a/BrickController2/BrickController2.Android/PlatformServices/GameController/GameControllerService.cs
+++ b/BrickController2/BrickController2.Android/PlatformServices/GameController/GameControllerService.cs
@@ -1,19 +1,25 @@
using Android.Views;
using Android.Hardware.Input;
using Android.Content;
+using BrickController2.Droid.PlatformServices.ModelContextProtocol;
using BrickController2.PlatformServices.GameController;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Logging;
+using BrickController2.PlatformServices.ModelContextProtocol;
namespace BrickController2.Droid.PlatformServices.GameController
{
- internal class GameControllerService : GameControllerServiceBase
+ internal class GameControllerService : GameControllerServiceBase
{
private readonly InputManager _inputManager;
+ private readonly McpServerService _mcpServerService;
- public GameControllerService(Context context, ILogger logger) :base(logger)
+ public GameControllerService(Context context,
+ McpServerService mcpServerService,
+ ILogger logger) :base(logger)
{
_inputManager = (InputManager)context.GetSystemService(Context.InputService)!;
+ _mcpServerService = mcpServerService;
}
public override bool IsControllerIdSupported => true;
@@ -36,9 +42,9 @@ internal void MainActivityOnInputDeviceAdded(int deviceId)
/// deviceId of InputDevice
internal void MainActivityOnInputDeviceRemoved(int deviceId)
{
- if (TryRemove(x => x.Gamepad.Id == deviceId, out var controller))
+ if (TryRemove(x => x.ControllerDevice.Id == deviceId, out var controller))
{
- _logger.LogInformation("Gamepad has been removed DeviceId:{id}, ControllerId:{controllerId}",
+ _logger.LogInformation("ControllerDevice has been removed DeviceId:{id}, ControllerId:{controllerId}",
deviceId, controller.ControllerId);
}
}
@@ -64,14 +70,14 @@ internal void MainActivityOnInputDeviceChanged(int deviceId)
else if (controller != null)
{
// handle change - remove and then add it again
- TryRemove(x => x.Gamepad.Id == deviceId, out _);
+ TryRemove(x => x.ControllerDevice.Id == deviceId, out _);
}
AddGameControllerDevice(device);
}
}
- else if (TryRemove(x => x.Gamepad.Id == deviceId, out var controller))
+ else if (TryRemove(x => x.ControllerDevice.Id == deviceId, out var controller))
{
- _logger.LogInformation("Gamepad has been removed DeviceId:{id}, ControllerId:{controllerId}",
+ _logger.LogInformation("ControllerDevice has been removed DeviceId:{id}, ControllerId:{controllerId}",
deviceId, controller.ControllerId);
}
}
@@ -98,6 +104,9 @@ internal bool OnGameControllerAxisEvent(MotionEvent e)
protected override void InitializeCurrentControllers()
{
+ // add McpServer
+ AddMcpServer();
+
// add any connected game controller
var deviceIds = _inputManager?.GetInputDeviceIds() ?? [];
foreach (int deviceId in deviceIds)
@@ -107,6 +116,46 @@ protected override void InitializeCurrentControllers()
AddGameControllerDevice(device);
}
}
+
+ _mcpServerService.McpServerAdded += McpServerAdded;
+ _mcpServerService.McpServerRemoved += McpServerRemoved; ;
+ }
+
+ protected override void RemoveAllControllers()
+ {
+ _mcpServerService.McpServerAdded -= McpServerAdded;
+ _mcpServerService.McpServerRemoved -= McpServerRemoved; ;
+
+ base.RemoveAllControllers();
+ }
+
+ private void McpServerRemoved(object? sender, McpServer e)
+ {
+ lock (_lockObject)
+ {
+ if (TryRemove(x => x.ControllerDevice is McpServer, out var controller))
+ {
+ _logger.LogInformation("McpServer has been removed");
+ }
+ }
+ }
+
+ private void McpServerAdded(object? sender, McpServer e)
+ {
+ AddMcpServer();
+ }
+
+ private void AddMcpServer()
+ {
+ if (_mcpServerService?.Server != null)
+ {
+ lock (_lockObject)
+ {
+ var newController = new McpServerController(this, _mcpServerService.Server);
+
+ AddController(newController);
+ }
+ }
}
///
@@ -122,7 +171,7 @@ private void AddGameControllerDevice(InputDevice gamepad)
}
private bool TryGetControllerByDeviceId(int deviceId, [MaybeNullWhen(false)] out GamepadController controller)
- => TryGetController(x => x.Gamepad.Id == deviceId, out controller);
+ => TryGetController(x => x.ControllerDevice.Id == deviceId, out controller);
private static bool TryGetGamepadDevice(int deviceId, [MaybeNullWhen(false)] out InputDevice device)
{
diff --git a/BrickController2/BrickController2.Android/PlatformServices/GameController/GamepadController.cs b/BrickController2/BrickController2.Android/PlatformServices/GameController/GamepadController.cs
index 2e0a3e19..01240f4d 100644
--- a/BrickController2/BrickController2.Android/PlatformServices/GameController/GamepadController.cs
+++ b/BrickController2/BrickController2.Android/PlatformServices/GameController/GamepadController.cs
@@ -1,5 +1,4 @@
using Android.Views;
-using BrickController2.Droid.Extensions;
using BrickController2.PlatformServices.GameController;
using System;
using System.Collections.Generic;
@@ -25,11 +24,8 @@ public GamepadController(GameControllerService service, InputDevice gamePad)
{
// initialize properties
Name = GetDisplayName(gamePad);
- VendorId = gamePad.VendorId;
- ProductId = gamePad.ProductId;
ControllerNumber = gamePad.ControllerNumber;
ControllerId = GetControllerIdFromNumber(gamePad.ControllerNumber);
- UniquePersistantDeviceId = gamePad.GetUniquePersistentDeviceId();
}
internal bool OnButtonEvent(KeyEvent e, float buttonValue)
diff --git a/BrickController2/BrickController2.Android/PlatformServices/ModelContextProtocol/McpServerController.cs b/BrickController2/BrickController2.Android/PlatformServices/ModelContextProtocol/McpServerController.cs
new file mode 100644
index 00000000..e4f0e7a1
--- /dev/null
+++ b/BrickController2/BrickController2.Android/PlatformServices/ModelContextProtocol/McpServerController.cs
@@ -0,0 +1,37 @@
+using BrickController2.Droid.PlatformServices.GameController;
+using BrickController2.PlatformServices.GameController;
+using BrickController2.PlatformServices.ModelContextProtocol;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace BrickController2.Droid.PlatformServices.ModelContextProtocol
+{
+ internal class McpServerController : GamepadControllerBase
+ {
+ ///
+ /// Constructor
+ ///
+ /// reference to GameControllerService
+ /// reference to InputDevice
+ public McpServerController(GameControllerService service, McpServer mcpServer)
+ : base(service, mcpServer)
+ {
+ ControllerDevice.ChannelStatesUpdated += mcpServer_ChannelStatesUpdated;
+
+ // initialize properties
+ Name = "McpServer";
+ ControllerNumber = -1;
+ ControllerId = "McpServer";
+ }
+
+ private void mcpServer_ChannelStatesUpdated(List obj)
+ {
+ // grab all changed axis event
+ var currentEvents = obj
+ .Where(x => HasValueChanged($"{x.Channel}", (float)x.Value))
+ .ToDictionary(x => (GameControllerEventType.Axis, $"{x.Channel}"), x => (float)x.Value);
+
+ RaiseEvent(currentEvents);
+ }
+ }
+}
\ No newline at end of file
diff --git a/BrickController2/BrickController2.Android/PlatformServices/ModelContextProtocol/McpServerService.cs b/BrickController2/BrickController2.Android/PlatformServices/ModelContextProtocol/McpServerService.cs
new file mode 100644
index 00000000..51205b3d
--- /dev/null
+++ b/BrickController2/BrickController2.Android/PlatformServices/ModelContextProtocol/McpServerService.cs
@@ -0,0 +1,106 @@
+using BrickController2.PlatformServices.ModelContextProtocol;
+using BrickController2.UI.Services.Preferences;
+using System;
+using System.Threading.Tasks;
+
+namespace BrickController2.Droid.PlatformServices.ModelContextProtocol;
+
+public class McpServerService : IMcpServerService, IDisposable
+{
+ private readonly object _Lock = new object();
+ private readonly IPreferencesService _preferencesService;
+ private McpServer? _server;
+
+ public McpServerService(IPreferencesService preferencesService)
+ {
+ _preferencesService = preferencesService;
+ _server = null;
+
+ ApplyMcpServer();
+ }
+
+ public event EventHandler? McpServerAdded;
+ public event EventHandler? McpServerRemoved;
+
+ public bool IsMcpServerAvailable => true;
+ public bool IsMcpServerEnabled
+ {
+ get => _preferencesService.Get("McpServerEnabled", false);
+ set
+ {
+ if (IsMcpServerEnabled != value)
+ {
+ _preferencesService.Set("McpServerEnabled", value);
+ ApplyMcpServer();
+ }
+ }
+ }
+ public int McpServerPort
+ {
+ get => _preferencesService.Get("McpServerPort", McpServerBase.PortDefault);
+
+ set
+ {
+ if (McpServerPort != value)
+ {
+ _preferencesService.Set("McpServerPort", value);
+ ApplyMcpServer();
+ }
+ }
+ }
+
+ public string McpServerAuthToken
+ {
+ get => _preferencesService.Get("McpServerAuthToken", McpServerBase.AuthTokenDefault);
+
+ set
+ {
+ if (McpServerAuthToken != value)
+ {
+ _preferencesService.Set("McpServerAuthToken", value);
+ ApplyMcpServer();
+ }
+ }
+ }
+
+ public McpServer? Server => _server;
+
+ public void Dispose()
+ {
+ lock (_Lock)
+ {
+ _server?.Dispose();
+ _server = null;
+ }
+ }
+
+ private void ApplyMcpServer()
+ {
+ lock (_Lock)
+ {
+ if (_server != null)
+ {
+ McpServerRemoved?.Invoke(this, _server);
+
+ _server?.Dispose();
+ _server = null;
+ }
+
+ if (IsMcpServerEnabled)
+ {
+ if (_server == null)
+ {
+ _server = new McpServer(McpServerPort, McpServerAuthToken);
+ Start();
+ McpServerAdded?.Invoke(this, _server);
+ }
+ }
+ }
+ }
+
+ public void Start()
+ {
+ // start non-blocking
+ Task.Run(() => _server?.StartAsync());
+ }
+}
diff --git a/BrickController2/BrickController2.Android/Properties/AndroidManifest.xml b/BrickController2/BrickController2.Android/Properties/AndroidManifest.xml
index b42d6d32..94f05c65 100644
--- a/BrickController2/BrickController2.Android/Properties/AndroidManifest.xml
+++ b/BrickController2/BrickController2.Android/Properties/AndroidManifest.xml
@@ -10,8 +10,9 @@
-
+
+
diff --git a/BrickController2/BrickController2.WinUI/BrickController2.WinUI.csproj b/BrickController2/BrickController2.WinUI/BrickController2.WinUI.csproj
index 43c2c91f..1c1bf630 100644
--- a/BrickController2/BrickController2.WinUI/BrickController2.WinUI.csproj
+++ b/BrickController2/BrickController2.WinUI/BrickController2.WinUI.csproj
@@ -28,6 +28,10 @@
+
+
+
+
diff --git a/BrickController2/BrickController2.WinUI/Package.appxmanifest b/BrickController2/BrickController2.WinUI/Package.appxmanifest
index 735b3b00..c56248d2 100644
--- a/BrickController2/BrickController2.WinUI/Package.appxmanifest
+++ b/BrickController2/BrickController2.WinUI/Package.appxmanifest
@@ -43,6 +43,7 @@
+
diff --git a/BrickController2/BrickController2.WinUI/PlatformServices/DI/PlatformServicesModule.cs b/BrickController2/BrickController2.WinUI/PlatformServices/DI/PlatformServicesModule.cs
index 9f7c30cd..b3e9bab1 100644
--- a/BrickController2/BrickController2.WinUI/PlatformServices/DI/PlatformServicesModule.cs
+++ b/BrickController2/BrickController2.WinUI/PlatformServices/DI/PlatformServicesModule.cs
@@ -5,6 +5,7 @@
using BrickController2.PlatformServices.GameController;
using BrickController2.PlatformServices.Infrared;
using BrickController2.PlatformServices.Localization;
+using BrickController2.PlatformServices.ModelContextProtocol;
using BrickController2.PlatformServices.Permission;
using BrickController2.PlatformServices.SharedFileStorage;
using BrickController2.Windows.PlatformServices.BluetoothLE;
@@ -13,6 +14,7 @@
using BrickController2.Windows.PlatformServices.GameController;
using BrickController2.Windows.PlatformServices.Infrared;
using BrickController2.Windows.PlatformServices.Localization;
+using BrickController2.Windows.PlatformServices.ModelContextProtocol;
using BrickController2.Windows.PlatformServices.Permission;
using BrickController2.Windows.PlatformServices.SharedFileStorage;
@@ -31,5 +33,6 @@ protected override void Load(ContainerBuilder builder)
builder.RegisterType().As().InstancePerDependency();
builder.RegisterType().As().SingleInstance();
builder.RegisterType().As().SingleInstance();
+ builder.RegisterType().AsSelf().As().SingleInstance();
}
}
\ No newline at end of file
diff --git a/BrickController2/BrickController2.WinUI/PlatformServices/GameController/GameControllerService.cs b/BrickController2/BrickController2.WinUI/PlatformServices/GameController/GameControllerService.cs
index 296a37ec..3dde9fbe 100644
--- a/BrickController2/BrickController2.WinUI/PlatformServices/GameController/GameControllerService.cs
+++ b/BrickController2/BrickController2.WinUI/PlatformServices/GameController/GameControllerService.cs
@@ -5,26 +5,34 @@
using BrickController2.PlatformServices.GameController;
using BrickController2.UI.Services.MainThread;
using Microsoft.Extensions.Logging;
+using BrickController2.Windows.PlatformServices.ModelContextProtocol;
+using BrickController2.PlatformServices.ModelContextProtocol;
namespace BrickController2.Windows.PlatformServices.GameController;
-internal class GameControllerService : GameControllerServiceBase, IGameControllerService
+internal class GameControllerService : GameControllerServiceBase, IGameControllerService
{
private readonly IMainThreadService _mainThreadService;
private readonly IDispatcherProvider _dispatcherProvider;
+ private readonly McpServerService _mcpServerService;
public GameControllerService(IMainThreadService mainThreadService,
IDispatcherProvider dispatcherProvider,
+ McpServerService mcpServerService,
ILogger logger) : base(logger)
{
_mainThreadService = mainThreadService;
_dispatcherProvider = dispatcherProvider;
+ _mcpServerService = mcpServerService;
}
public override bool IsControllerIdSupported => true;
protected override void InitializeCurrentControllers()
{
+ // add McpServer
+ AddMcpServer();
+
// get all available gamepads
if (Gamepad.Gamepads.Any())
{
@@ -34,6 +42,9 @@ protected override void InitializeCurrentControllers()
// register gamepad events
Gamepad.GamepadRemoved += Gamepad_GamepadRemoved;
Gamepad.GamepadAdded += Gamepad_GamepadAdded;
+
+ _mcpServerService.McpServerAdded += McpServerAdded;
+ _mcpServerService.McpServerRemoved += McpServerRemoved; ;
}
protected override void RemoveAllControllers()
@@ -42,6 +53,9 @@ protected override void RemoveAllControllers()
Gamepad.GamepadRemoved -= Gamepad_GamepadRemoved;
Gamepad.GamepadAdded -= Gamepad_GamepadAdded;
+ _mcpServerService.McpServerAdded -= McpServerAdded;
+ _mcpServerService.McpServerRemoved -= McpServerRemoved; ;
+
// do removal
base.RemoveAllControllers();
}
@@ -53,9 +67,9 @@ private void Gamepad_GamepadRemoved(object? sender, Gamepad gamepad)
// ensure stopped in UI thread
_ = _mainThreadService.RunOnMainThread(() =>
{
- if (TryRemove(x => x.Gamepad == gamepad, out var controller))
+ if (TryRemove(x => x.ControllerDevice == gamepad, out var controller))
{
- _logger.LogInformation("Gamepad has been removed ControllerId:{controllerId}", controller.ControllerId);
+ _logger.LogInformation("ControllerDevice has been removed ControllerId:{controllerId}", controller.ControllerId);
}
});
}
@@ -67,6 +81,28 @@ private void Gamepad_GamepadAdded(object? sender, Gamepad e)
_ = _mainThreadService.RunOnMainThread(() => AddDevices([e]));
}
+ private void McpServerRemoved(object? sender, McpServer e)
+ {
+ lock (_lockObject)
+ {
+ // ensure stopped in UI thread
+ _ = _mainThreadService.RunOnMainThread(() =>
+ {
+ if (TryRemove(x => x.ControllerDevice is McpServer, out var controller))
+ {
+ _logger.LogInformation("McpServer has been removed");
+ }
+ });
+ }
+ }
+
+ private void McpServerAdded(object? sender, McpServer e)
+ {
+ // ensure created in UI thread
+ _ = _mainThreadService.RunOnMainThread(() => AddMcpServer());
+ }
+
+
private void AddDevices(IEnumerable gamepads)
{
lock (_lockObject)
@@ -89,4 +125,18 @@ private void AddDevices(IEnumerable gamepads)
}
}
}
+
+ private void AddMcpServer()
+ {
+ if (_mcpServerService?.Server != null)
+ {
+ lock (_lockObject)
+ {
+ var dispatcher = _dispatcherProvider.GetForCurrentThread();
+ var newController = new McpServerController(this, _mcpServerService.Server, dispatcher);
+
+ AddController(newController);
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/BrickController2/BrickController2.WinUI/PlatformServices/GameController/GamepadController.cs b/BrickController2/BrickController2.WinUI/PlatformServices/GameController/GamepadController.cs
index ef203590..c105a7b3 100644
--- a/BrickController2/BrickController2.WinUI/PlatformServices/GameController/GamepadController.cs
+++ b/BrickController2/BrickController2.WinUI/PlatformServices/GameController/GamepadController.cs
@@ -4,7 +4,6 @@
using Microsoft.Maui.Dispatching;
using BrickController2.PlatformServices.GameController;
using BrickController2.Windows.Extensions;
-
using static BrickController2.PlatformServices.GameController.GameControllers;
namespace BrickController2.Windows.PlatformServices.GameController;
@@ -13,7 +12,7 @@ internal class GamepadController : GamepadControllerBase
{
private static readonly TimeSpan DefaultInterval = TimeSpan.FromMilliseconds(10);
- private readonly IDispatcherTimer _timer;
+ private readonly IDispatcherTimer? _timer;
///
/// Constructor
@@ -24,14 +23,10 @@ internal class GamepadController : GamepadControllerBase
public GamepadController(GameControllerService service, Gamepad gamepad, RawGameController rawController, int controllerNumber, IDispatcherTimer timer)
: base(service, gamepad)
{
+ Name = rawController.DisplayName;
ControllerNumber = controllerNumber;
ControllerId = GetControllerIdFromNumber(controllerNumber);
- UniquePersistantDeviceId = rawController.NonRoamableId;
- Name = rawController.DisplayName;
- VendorId = rawController.HardwareVendorId;
- ProductId = rawController.HardwareProductId;
-
_timer = timer;
_timer.Interval = DefaultInterval;
@@ -43,19 +38,19 @@ public override void Start()
base.Start();
// finally start timer
- _timer.Start();
+ _timer?.Start();
}
public override void Stop()
{
- _timer.Stop();
+ _timer?.Stop();
base.Stop();
}
private void Timer_Tick(object? sender, object e)
{
- var currentReading = Gamepad.GetCurrentReading();
+ var currentReading = ControllerDevice.GetCurrentReading();
var currentEvents = currentReading
.Enumerate()
diff --git a/BrickController2/BrickController2.WinUI/PlatformServices/ModelContextProtocol/McpServerController.cs b/BrickController2/BrickController2.WinUI/PlatformServices/ModelContextProtocol/McpServerController.cs
new file mode 100644
index 00000000..1967be34
--- /dev/null
+++ b/BrickController2/BrickController2.WinUI/PlatformServices/ModelContextProtocol/McpServerController.cs
@@ -0,0 +1,40 @@
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Maui.Dispatching;
+using BrickController2.PlatformServices.GameController;
+using BrickController2.Windows.PlatformServices.GameController;
+using BrickController2.PlatformServices.ModelContextProtocol;
+
+namespace BrickController2.Windows.PlatformServices.ModelContextProtocol;
+
+internal class McpServerController : GamepadControllerBase
+{
+ private readonly IDispatcher? _dispatcher;
+
+ ///
+ /// Constructor
+ ///
+ /// reference to GameControllerService
+ /// reference to UWP's Gamepad
+ public McpServerController(GameControllerService service, McpServer mcpServer, IDispatcher? dispatcher)
+ : base(service, mcpServer)
+ {
+ _dispatcher = dispatcher;
+ ControllerDevice.ChannelStatesUpdated += mcpServer_ChannelStatesUpdated;
+
+ // initialize properties
+ Name = "McpServer";
+ ControllerNumber = -1;
+ ControllerId = "McpServer";
+ }
+
+ private void mcpServer_ChannelStatesUpdated(List obj)
+ {
+ // grab all changed axis event
+ var currentEvents = obj
+ .Where(x => HasValueChanged($"{x.Channel}", (float)x.Value))
+ .ToDictionary(x => (GameControllerEventType.Axis, $"{x.Channel}"), x => (float)x.Value);
+
+ _dispatcher?.Dispatch(() => RaiseEvent(currentEvents));
+ }
+}
diff --git a/BrickController2/BrickController2.WinUI/PlatformServices/ModelContextProtocol/McpServerService.cs b/BrickController2/BrickController2.WinUI/PlatformServices/ModelContextProtocol/McpServerService.cs
new file mode 100644
index 00000000..66ec7960
--- /dev/null
+++ b/BrickController2/BrickController2.WinUI/PlatformServices/ModelContextProtocol/McpServerService.cs
@@ -0,0 +1,109 @@
+using BrickController2.PlatformServices.ModelContextProtocol;
+using BrickController2.UI.Services.Preferences;
+using System;
+using System.Threading.Tasks;
+
+namespace BrickController2.Windows.PlatformServices.ModelContextProtocol;
+
+public class McpServerService : IMcpServerService, IDisposable
+{
+ private readonly object _Lock = new object();
+ private readonly IPreferencesService _preferencesService;
+ private McpServer? _server;
+
+ public McpServerService(IPreferencesService preferencesService)
+ {
+ _preferencesService = preferencesService;
+ _server = null;
+
+ ApplyMcpServer();
+ }
+
+ public event EventHandler? McpServerAdded;
+ public event EventHandler? McpServerRemoved;
+
+ public bool IsMcpServerAvailable => true;
+
+ public bool IsMcpServerEnabled
+ {
+ get => _preferencesService.Get("McpServerEnabled", false);
+
+ set
+ {
+ if (IsMcpServerEnabled != value)
+ {
+ _preferencesService.Set("McpServerEnabled", value);
+ ApplyMcpServer();
+ }
+ }
+ }
+
+ public int McpServerPort
+ {
+ get => _preferencesService.Get("McpServerPort", McpServerBase.PortDefault);
+
+ set
+ {
+ if (McpServerPort != value)
+ {
+ _preferencesService.Set("McpServerPort", value);
+ ApplyMcpServer();
+ }
+ }
+ }
+
+ public string McpServerAuthToken
+ {
+ get => _preferencesService.Get("McpServerAuthToken", McpServerBase.AuthTokenDefault);
+
+ set
+ {
+ if (McpServerAuthToken != value)
+ {
+ _preferencesService.Set("McpServerAuthToken", value);
+ ApplyMcpServer();
+ }
+ }
+ }
+
+ public McpServer? Server => _server;
+
+ public void Dispose()
+ {
+ lock (_Lock)
+ {
+ _server?.Dispose();
+ _server = null;
+ }
+ }
+
+ private void ApplyMcpServer()
+ {
+ lock (_Lock)
+ {
+ if (_server != null)
+ {
+ McpServerRemoved?.Invoke(this, _server);
+
+ _server?.Dispose();
+ _server = null;
+ }
+
+ if (IsMcpServerEnabled)
+ {
+ if (_server == null)
+ {
+ _server = new McpServer(McpServerPort, McpServerAuthToken);
+ Start();
+ McpServerAdded?.Invoke(this, _server);
+ }
+ }
+ }
+ }
+
+ public void Start()
+ {
+ // start non-blocking
+ Task.Run(() => _server?.StartAsync());
+ }
+}
diff --git a/BrickController2/BrickController2.iOS/BrickController2.iOS.csproj b/BrickController2/BrickController2.iOS/BrickController2.iOS.csproj
index 1c3b030e..4a002446 100644
--- a/BrickController2/BrickController2.iOS/BrickController2.iOS.csproj
+++ b/BrickController2/BrickController2.iOS/BrickController2.iOS.csproj
@@ -2,21 +2,21 @@
- net9.0-ios
- ios-arm64
+ net9.0-ios
+ ios-arm64
12.2
- Exe
- com.scn.BrickController2
+ Exe
+ com.scn.BrickController2
BrickController2.iOS
- true
+ true
-
+
-
+
@@ -29,14 +29,14 @@
-
+
-
- #990000
- 128,128
-
+
+ #990000
+ 128,128
+
\ No newline at end of file
diff --git a/BrickController2/BrickController2.iOS/Info.plist b/BrickController2/BrickController2.iOS/Info.plist
index 9e684753..a49d8018 100644
--- a/BrickController2/BrickController2.iOS/Info.plist
+++ b/BrickController2/BrickController2.iOS/Info.plist
@@ -40,7 +40,7 @@
Location access is required to use SBrick, BuWizz or Powered-Up devices.
NSCameraUsageDescription
Camera is required in order to import a creation via QR code from another application.
- CFBundleShortVersionString
+ CFBundleShortVersionString
3.4
CFBundleVersion
50
@@ -64,5 +64,16 @@
LaunchScreen
UIFileSharingEnabled
+ NSBonjourServices
+
+ _http._tcp
+
+ NSLocalNetworkUsageDescription
+ To act as a Model Context Protocol (MCP) server, local network access is required.
+ NSAppTransportSecurity
+
+ NSAllowsArbitraryLoads
+
+
diff --git a/BrickController2/BrickController2.iOS/PlatformServices/DI/PlatformServicesModule.cs b/BrickController2/BrickController2.iOS/PlatformServices/DI/PlatformServicesModule.cs
index edb0636d..e3021614 100644
--- a/BrickController2/BrickController2.iOS/PlatformServices/DI/PlatformServicesModule.cs
+++ b/BrickController2/BrickController2.iOS/PlatformServices/DI/PlatformServicesModule.cs
@@ -7,12 +7,14 @@
using BrickController2.iOS.PlatformServices.GameController;
using BrickController2.iOS.PlatformServices.Infrared;
using BrickController2.iOS.PlatformServices.Localization;
+using BrickController2.iOS.PlatformServices.ModelContextProtocol;
using BrickController2.iOS.PlatformServices.Permission;
using BrickController2.iOS.PlatformServices.SharedFileStorage;
using BrickController2.PlatformServices.BluetoothLE;
using BrickController2.PlatformServices.GameController;
using BrickController2.PlatformServices.Infrared;
using BrickController2.PlatformServices.Localization;
+using BrickController2.PlatformServices.ModelContextProtocol;
using BrickController2.PlatformServices.Permission;
using BrickController2.PlatformServices.SharedFileStorage;
@@ -32,6 +34,8 @@ protected override void Load(ContainerBuilder builder)
builder.RegisterType().As().InstancePerDependency();
builder.RegisterType().As().SingleInstance();
builder.RegisterType().As().SingleInstance();
+ builder.RegisterType().AsSelf().SingleInstance();
+ builder.RegisterType().AsSelf().As().SingleInstance();
}
}
}
\ No newline at end of file
diff --git a/BrickController2/BrickController2.iOS/PlatformServices/GameController/GameControllerService.cs b/BrickController2/BrickController2.iOS/PlatformServices/GameController/GameControllerService.cs
index e138df48..d8e7f36f 100644
--- a/BrickController2/BrickController2.iOS/PlatformServices/GameController/GameControllerService.cs
+++ b/BrickController2/BrickController2.iOS/PlatformServices/GameController/GameControllerService.cs
@@ -1,285 +1,136 @@
-using System;
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Linq;
+using BrickController2.iOS.PlatformServices.ModelContextProtocol;
using BrickController2.PlatformServices.GameController;
+using BrickController2.PlatformServices.ModelContextProtocol;
using Foundation;
using GameController;
-
-using static BrickController2.PlatformServices.GameController.GameControllers;
+using Microsoft.Extensions.Logging;
namespace BrickController2.iOS.PlatformServices.GameController
{
- public class GameControllerService : IGameControllerService
+ internal class GameControllerService : GameControllerServiceBase
{
- private enum GameControllerType
- {
- Unknown,
- Micro,
- Standard,
- Extended
- };
-
- private readonly object _lockObject = new object();
-
- private readonly IDictionary _lastControllerEventValueMap = new Dictionary();
-
- private GCController? _gameController;
- private event EventHandler? GameControllerEventInternal;
+ private readonly McpServerService _mcpServerService;
private NSObject? _didConnectNotification;
private NSObject? _didDisconnectNotification;
- public event EventHandler GameControllerEvent
+ public GameControllerService(ILogger logger,
+ McpServerService mcpServerService) : base(logger)
{
- add
- {
- lock (_lockObject)
- {
- if (GameControllerEventInternal is null)
- {
- if (GCController.Controllers.Length == 0)
- {
- FindController();
- }
- else
- {
- FoundController();
- }
- }
-
- GameControllerEventInternal += value;
- }
- }
-
- remove
- {
- lock (_lockObject)
- {
- GameControllerEventInternal -= value;
-
- if (GameControllerEventInternal is null)
- {
- GCController.StopWirelessControllerDiscovery();
- _didConnectNotification?.Dispose();
- _didDisconnectNotification?.Dispose();
- _didConnectNotification = null;
- _didDisconnectNotification = null;
- _gameController?.Dispose();
- _gameController = null;
- }
- }
- }
+ _mcpServerService = mcpServerService;
}
- public event EventHandler? GameControllersChangedEvent;
-
- public bool IsControllerIdSupported => false; // ToDo: implement ControllerManagement
+ public override bool IsControllerIdSupported => true;
- private void FindController()
+ protected override void InitializeCurrentControllers()
{
- lock (_lockObject)
- {
- _didConnectNotification = GCController.Notifications.ObserveDidConnect((sender, args) =>
- {
- FoundController();
- });
+ // add McpServer
+ AddMcpServer();
- GCController.StartWirelessControllerDiscovery(() => { });
- }
- }
-
- private void FoundController()
- {
- lock (_lockObject)
+ // get all available gamepads
+ if (GCController.Controllers.Any())
{
- _gameController = GCController.Controllers.FirstOrDefault();
-
- if (_gameController is not null)
- {
- GCController.StopWirelessControllerDiscovery();
- _didConnectNotification?.Dispose();
- _didConnectNotification = null;
-
- _didDisconnectNotification = GCController.Notifications.ObserveDidDisconnect((sender, args) =>
- {
- FindController();
- });
-
- switch (GetGameControllerType(_gameController))
- {
- case GameControllerType.Micro:
- SetupMicroGamePad(_gameController.MicroGamepad!);
- break;
-
- case GameControllerType.Standard:
-#pragma warning disable CA1422 // Validate platform compatibility
- SetupGamePad(_gameController.Gamepad!);
-#pragma warning restore CA1422 // Validate platform compatibility
- break;
-
- case GameControllerType.Extended:
- SetupExtendedGamePad(_gameController.ExtendedGamepad!);
- break;
- }
- }
+ AddDevices(GCController.Controllers);
}
- }
- private GameControllerType GetGameControllerType(GCController controller)
- {
- try
+ // register GCController events
+ _didDisconnectNotification = GCController.Notifications.ObserveDidDisconnect((sender, args) =>
{
- if (controller.MicroGamepad is not null)
+ var controller = args.Notification.Object as GCController;
+ if (controller != null)
{
- return GameControllerType.Micro;
+ ControllerRemoved(controller);
}
- }
- catch (InvalidCastException) { }
-
- try
+ });
+ _didConnectNotification = GCController.Notifications.ObserveDidConnect((sender, args) =>
{
-#pragma warning disable CA1422 // Validate platform compatibility
- if (controller.Gamepad is not null)
+ var controller = args.Notification.Object as GCController;
+ if (controller != null)
{
- return GameControllerType.Standard;
+ ControllerAdded(controller);
}
-#pragma warning restore CA1422 // Validate platform compatibility
- }
- catch (InvalidCastException) { }
-
- try
- {
- if (controller.ExtendedGamepad is not null)
- {
- return GameControllerType.Extended;
- }
- }
- catch (InvalidCastException) { }
-
- return GameControllerType.Unknown;
- }
+ });
- private void SetupMicroGamePad(GCMicroGamepad gamePad)
- {
- SetupDigitalButtonInput(gamePad.ButtonA, "Button_A");
- SetupDigitalButtonInput(gamePad.ButtonX, "Button_X");
+ GCController.StartWirelessControllerDiscovery(() => { });
- SetupDPadInput(gamePad.Dpad, "DPad");
+ _mcpServerService.McpServerAdded += McpServerAdded;
+ _mcpServerService.McpServerRemoved += McpServerRemoved; ;
}
- private void SetupGamePad(GCGamepad gamePad)
- {
-#pragma warning disable CA1422 // Validate platform compatibility
- SetupDigitalButtonInput(gamePad.ButtonA, "Button_A");
- SetupDigitalButtonInput(gamePad.ButtonB, "Button_B");
- SetupDigitalButtonInput(gamePad.ButtonX, "Button_X");
- SetupDigitalButtonInput(gamePad.ButtonY, "Button_Y");
-
- SetupDigitalButtonInput(gamePad.LeftShoulder, "LeftShoulder");
- SetupDigitalButtonInput(gamePad.RightShoulder, "RightShoulder");
-
- SetupDPadInput(gamePad.DPad, "DPad");
-#pragma warning restore CA1422 // Validate platform compatibility
- }
- private void SetupExtendedGamePad(GCExtendedGamepad gamePad)
+ protected override void RemoveAllControllers()
{
- SetupDigitalButtonInput(gamePad.ButtonA, "Button_A");
- SetupDigitalButtonInput(gamePad.ButtonB, "Button_B");
- SetupDigitalButtonInput(gamePad.ButtonX, "Button_X");
- SetupDigitalButtonInput(gamePad.ButtonY, "Button_Y");
+ GCController.StopWirelessControllerDiscovery();
+ _didConnectNotification?.Dispose();
+ _didDisconnectNotification?.Dispose();
+ _didConnectNotification = null;
+ _didDisconnectNotification = null;
- SetupDigitalButtonInput(gamePad.LeftShoulder, "LeftShoulder");
- SetupDigitalButtonInput(gamePad.RightShoulder, "RightShoulder");
+ _mcpServerService.McpServerAdded -= McpServerAdded;
+ _mcpServerService.McpServerRemoved -= McpServerRemoved; ;
- SetupAnalogButtonInput(gamePad.LeftTrigger, "LeftTrigger");
- SetupAnalogButtonInput(gamePad.RightTrigger, "RightTrigger");
-
- SetupDPadInput(gamePad.DPad, "DPad");
-
- SetupJoyInput(gamePad.LeftThumbstick, "LeftThumbStick");
- SetupJoyInput(gamePad.RightThumbstick, "RightThumbStick");
+ base.RemoveAllControllers();
}
- private void SetupDigitalButtonInput(GCControllerButtonInput button, string name)
+ private void ControllerRemoved(GCController controller)
{
- button.ValueChangedHandler = (btn, value, isPressed) =>
+ lock (_lockObject)
{
- value = isPressed ? 1.0F : 0.0F;
-
- if (!_lastControllerEventValueMap.ContainsKey(name) || !AreAlmostEqual(_lastControllerEventValueMap[name], value))
+ if (TryRemove(x => x.ControllerDevice == controller, out var controllerDevice))
{
- // ToDo: find ControllerId
- string controllerId = GetControllerIdFromIndex(0);
-
- _lastControllerEventValueMap[name] = value;
- GameControllerEventInternal?.Invoke(this, new GameControllerEventArgs(controllerId, GameControllerEventType.Button, name, value));
+ _logger.LogInformation("ControllerDevice has been removed ControllerId:{controllerId}", controllerDevice.ControllerId);
}
- };
+ }
}
- private void SetupAnalogButtonInput(GCControllerButtonInput button, string name)
+ private void ControllerAdded(GCController controller)
{
- button.ValueChangedHandler = (btn, value, isPressed) =>
- {
- value = value < 0.1 ? 0.0F : value;
+ AddDevices([controller]);
+ }
- if (!_lastControllerEventValueMap.ContainsKey(name) || !AreAlmostEqual(_lastControllerEventValueMap[name], value))
+ private void AddDevices(IEnumerable controllers)
+ {
+ lock (_lockObject)
+ {
+ foreach (var gamepad in controllers)
{
- // ToDo: find ControllerId
- string controllerId = GetControllerIdFromIndex(0);
+ // get first unused number and apply it
+ int controllerNumber = GetFirstUnusedControllerNumber();
+ var newController = new GamepadController(this, gamepad, controllerNumber);
- _lastControllerEventValueMap[name] = value;
- GameControllerEventInternal?.Invoke(this, new GameControllerEventArgs(controllerId, GameControllerEventType.Axis, name, value));
+ AddController(newController);
}
- };
- }
-
- private void SetupDPadInput(GCControllerDirectionPad dPad, string name)
- {
- SetupDigitalAxisInput(dPad.XAxis, $"{name}_X");
- SetupDigitalAxisInput(dPad.YAxis, $"{name}_Y");
+ }
}
- private void SetupDigitalAxisInput(GCControllerAxisInput axis, string name)
+ private void McpServerRemoved(object? sender, McpServer e)
{
- axis.ValueChangedHandler = (ax, value) =>
+ lock (_lockObject)
{
- if (value < -0.1F) value = -1.0F;
- else if (value > 0.1F) value = 1.0F;
- else value = 0.0F;
-
- if (!_lastControllerEventValueMap.ContainsKey(name) || !AreAlmostEqual(_lastControllerEventValueMap[name], value))
+ if (TryRemove(x => x.ControllerDevice is McpServer, out var controller))
{
- // ToDo: find ControllerId
- string controllerId = GetControllerIdFromIndex(0);
-
- GameControllerEventInternal?.Invoke(this, new GameControllerEventArgs(controllerId, GameControllerEventType.Axis, name, value));
- _lastControllerEventValueMap[name] = value;
+ _logger.LogInformation("McpServer has been removed");
}
- };
+ }
}
- private void SetupJoyInput(GCControllerDirectionPad joy, string name)
+ private void McpServerAdded(object? sender, McpServer e)
{
- SetupAnalogAxisInput(joy.XAxis, $"{name}_X");
- SetupAnalogAxisInput(joy.YAxis, $"{name}_Y");
+ AddMcpServer();
}
- private void SetupAnalogAxisInput(GCControllerAxisInput axis, string name)
+ private void AddMcpServer()
{
- axis.ValueChangedHandler = (ax, value) =>
+ if (_mcpServerService?.Server != null)
{
- value = AdjustControllerValue(value);
-
- if (!_lastControllerEventValueMap.ContainsKey(name) || !AreAlmostEqual(_lastControllerEventValueMap[name], value))
+ lock (_lockObject)
{
- // ToDo: find ControllerId
- string controllerId = GetControllerIdFromIndex(0);
+ var newController = new McpServerController(this, _mcpServerService.Server);
- GameControllerEventInternal?.Invoke(this, new GameControllerEventArgs(controllerId, GameControllerEventType.Axis, name, value));
- _lastControllerEventValueMap[name] = value;
+ AddController(newController);
}
- };
+ }
}
}
}
\ No newline at end of file
diff --git a/BrickController2/BrickController2.iOS/PlatformServices/GameController/GamepadController.cs b/BrickController2/BrickController2.iOS/PlatformServices/GameController/GamepadController.cs
new file mode 100644
index 00000000..18a0f3cb
--- /dev/null
+++ b/BrickController2/BrickController2.iOS/PlatformServices/GameController/GamepadController.cs
@@ -0,0 +1,242 @@
+using System;
+using System.Collections.Generic;
+using BrickController2.PlatformServices.GameController;
+using GameController;
+
+using static BrickController2.PlatformServices.GameController.GameControllers;
+
+using BrickController2.iOS.PlatformServices.GameController;
+
+internal class GamepadController : GamepadControllerBase, IDisposable
+{
+ private enum GameControllerType
+ {
+ Unknown,
+ Micro,
+ Standard,
+ Extended
+ };
+
+ private readonly IDictionary _lastControllerEventValueMap = new Dictionary();
+
+ ///
+ /// Constructor
+ ///
+ /// reference to GameControllerService
+ /// reference to InputDevice
+ public GamepadController(GameControllerService service, GCController controller, int controllerNumber)
+ : base(service, controller)
+ {
+ // initialize properties
+ GameControllerType gameControllerType = GetGameControllerType(controller);
+
+ Name = GetDisplayName(gameControllerType);
+ ControllerNumber = controllerNumber;
+ ControllerId = GetControllerIdFromNumber(controllerNumber);
+
+ SetupController(controller, gameControllerType);
+ }
+
+ public void Dispose()
+ {
+ ControllerDevice.Dispose();
+ }
+
+ private void SetupController(GCController gameController, GameControllerType gameControllerType)
+ {
+ switch (gameControllerType)
+ {
+ case GameControllerType.Micro:
+ SetupMicroGamePad(gameController.MicroGamepad!);
+ break;
+
+ case GameControllerType.Standard:
+#pragma warning disable CA1422 // Validate platform compatibility
+ SetupGamePad(gameController.Gamepad!);
+#pragma warning restore CA1422 // Validate platform compatibility
+ break;
+
+ case GameControllerType.Extended:
+ SetupExtendedGamePad(gameController.ExtendedGamepad!);
+ break;
+ }
+ }
+
+ private GameControllerType GetGameControllerType(GCController controller)
+ {
+ try
+ {
+ if (controller.MicroGamepad is not null)
+ {
+ return GameControllerType.Micro;
+ }
+ }
+ catch (InvalidCastException) { }
+
+ try
+ {
+#pragma warning disable CA1422 // Validate platform compatibility
+ if (controller.Gamepad is not null)
+ {
+ return GameControllerType.Standard;
+ }
+#pragma warning restore CA1422 // Validate platform compatibility
+ }
+ catch (InvalidCastException) { }
+
+ try
+ {
+ if (controller.ExtendedGamepad is not null)
+ {
+ return GameControllerType.Extended;
+ }
+ }
+ catch (InvalidCastException) { }
+
+ return GameControllerType.Unknown;
+ }
+
+ private void SetupMicroGamePad(GCMicroGamepad gamePad)
+ {
+ SetupDigitalButtonInput(gamePad.ButtonA, "Button_A");
+ SetupDigitalButtonInput(gamePad.ButtonX, "Button_X");
+
+ SetupDPadInput(gamePad.Dpad, "DPad");
+ }
+
+ private void SetupGamePad(GCGamepad gamePad)
+ {
+#pragma warning disable CA1422 // Validate platform compatibility
+ SetupDigitalButtonInput(gamePad.ButtonA, "Button_A");
+ SetupDigitalButtonInput(gamePad.ButtonB, "Button_B");
+ SetupDigitalButtonInput(gamePad.ButtonX, "Button_X");
+ SetupDigitalButtonInput(gamePad.ButtonY, "Button_Y");
+
+ SetupDigitalButtonInput(gamePad.LeftShoulder, "LeftShoulder");
+ SetupDigitalButtonInput(gamePad.RightShoulder, "RightShoulder");
+
+ SetupDPadInput(gamePad.DPad, "DPad");
+#pragma warning restore CA1422 // Validate platform compatibility
+ }
+
+ private void SetupExtendedGamePad(GCExtendedGamepad gamePad)
+ {
+ SetupDigitalButtonInput(gamePad.ButtonA, "Button_A");
+ SetupDigitalButtonInput(gamePad.ButtonB, "Button_B");
+ SetupDigitalButtonInput(gamePad.ButtonX, "Button_X");
+ SetupDigitalButtonInput(gamePad.ButtonY, "Button_Y");
+
+ SetupDigitalButtonInput(gamePad.LeftShoulder, "LeftShoulder");
+ SetupDigitalButtonInput(gamePad.RightShoulder, "RightShoulder");
+
+ SetupAnalogButtonInput(gamePad.LeftTrigger, "LeftTrigger");
+ SetupAnalogButtonInput(gamePad.RightTrigger, "RightTrigger");
+
+ SetupDPadInput(gamePad.DPad, "DPad");
+
+ SetupJoyInput(gamePad.LeftThumbstick, "LeftThumbStick");
+ SetupJoyInput(gamePad.RightThumbstick, "RightThumbStick");
+ }
+
+ private void SetupDigitalButtonInput(GCControllerButtonInput button, string name)
+ {
+ button.ValueChangedHandler = (btn, value, isPressed) =>
+ {
+ value = isPressed ? 1.0F : 0.0F;
+
+ if (!_lastControllerEventValueMap.ContainsKey(name) || !AreAlmostEqual(_lastControllerEventValueMap[name], value))
+ {
+ // ToDo: find ControllerId
+ //string controllerId = GetControllerIdFromIndex(0);
+ //GameControllerEventInternal?.Invoke(this, new GameControllerEventArgs(controllerId, GameControllerEventType.Button, name, value));
+
+ RaiseEvent(GameControllerEventType.Button, name, value);
+
+ _lastControllerEventValueMap[name] = value;
+ }
+ };
+ }
+
+ private void SetupAnalogButtonInput(GCControllerButtonInput button, string name)
+ {
+ button.ValueChangedHandler = (btn, value, isPressed) =>
+ {
+ value = value < 0.1 ? 0.0F : value;
+
+ if (!_lastControllerEventValueMap.ContainsKey(name) || !AreAlmostEqual(_lastControllerEventValueMap[name], value))
+ {
+ // ToDo: find ControllerId
+ //string controllerId = GetControllerIdFromIndex(0);
+ //GameControllerEventInternal?.Invoke(this, new GameControllerEventArgs(controllerId, GameControllerEventType.Axis, name, value));
+
+ RaiseEvent(GameControllerEventType.Axis, name, value);
+
+ _lastControllerEventValueMap[name] = value;
+ }
+ };
+ }
+
+ private void SetupDPadInput(GCControllerDirectionPad dPad, string name)
+ {
+ SetupDigitalAxisInput(dPad.XAxis, $"{name}_X");
+ SetupDigitalAxisInput(dPad.YAxis, $"{name}_Y");
+ }
+
+ private void SetupDigitalAxisInput(GCControllerAxisInput axis, string name)
+ {
+ axis.ValueChangedHandler = (ax, value) =>
+ {
+ if (value < -0.1F) value = -1.0F;
+ else if (value > 0.1F) value = 1.0F;
+ else value = 0.0F;
+
+ if (!_lastControllerEventValueMap.ContainsKey(name) || !AreAlmostEqual(_lastControllerEventValueMap[name], value))
+ {
+ // ToDo: find ControllerId
+ //string controllerId = GetControllerIdFromIndex(0);
+ //GameControllerEventInternal?.Invoke(this, new GameControllerEventArgs(controllerId, GameControllerEventType.Axis, name, value));
+
+ RaiseEvent(GameControllerEventType.Axis, name, value);
+
+ _lastControllerEventValueMap[name] = value;
+ }
+ };
+ }
+
+ private void SetupJoyInput(GCControllerDirectionPad joy, string name)
+ {
+ SetupAnalogAxisInput(joy.XAxis, $"{name}_X");
+ SetupAnalogAxisInput(joy.YAxis, $"{name}_Y");
+ }
+
+ private void SetupAnalogAxisInput(GCControllerAxisInput axis, string name)
+ {
+ axis.ValueChangedHandler = (ax, value) =>
+ {
+ value = AdjustControllerValue(value);
+
+ if (!_lastControllerEventValueMap.ContainsKey(name) || !AreAlmostEqual(_lastControllerEventValueMap[name], value))
+ {
+ // ToDo: find ControllerId
+ //string controllerId = GetControllerIdFromIndex(0);
+ //GameControllerEventInternal?.Invoke(this, new GameControllerEventArgs(controllerId, GameControllerEventType.Axis, name, value));
+
+ RaiseEvent(GameControllerEventType.Axis, name, value);
+
+ _lastControllerEventValueMap[name] = value;
+ }
+ };
+ }
+
+ private static string GetDisplayName(GameControllerType device)
+ {
+ return device switch
+ {
+ GameControllerType.Micro => "Micro Gamepad",
+ GameControllerType.Standard => "Standard Gamepad",
+ GameControllerType.Extended => "Extended Gamepad",
+ _ => "Unknown Gamepad",
+ };
+ }
+
+}
\ No newline at end of file
diff --git a/BrickController2/BrickController2.iOS/PlatformServices/ModelContextProtocol/McpServerBonjourPublisher.cs b/BrickController2/BrickController2.iOS/PlatformServices/ModelContextProtocol/McpServerBonjourPublisher.cs
new file mode 100644
index 00000000..c1134d5c
--- /dev/null
+++ b/BrickController2/BrickController2.iOS/PlatformServices/ModelContextProtocol/McpServerBonjourPublisher.cs
@@ -0,0 +1,23 @@
+using Foundation;
+
+namespace BrickController2.iOS.PlatformServices.ModelContextProtocol;
+
+public class McpServerBonjourPublisher
+{
+ private NSNetService? _netService = null;
+
+ public void Publish(int port)
+ {
+#pragma warning disable CA1422 // Validate platform compatibility
+ _netService = new NSNetService("local.", "_http._tcp", "Brickcontroller2 MCP Server", port);
+ _netService.Publish();
+#pragma warning restore CA1422 // Validate platform compatibility
+ }
+
+ public void Stop()
+ {
+#pragma warning disable CA1422 // Validate platform compatibility
+ _netService?.Stop();
+#pragma warning restore CA1422 // Validate platform compatibility
+ }
+}
diff --git a/BrickController2/BrickController2.iOS/PlatformServices/ModelContextProtocol/McpServerController.cs b/BrickController2/BrickController2.iOS/PlatformServices/ModelContextProtocol/McpServerController.cs
new file mode 100644
index 00000000..a1f5f430
--- /dev/null
+++ b/BrickController2/BrickController2.iOS/PlatformServices/ModelContextProtocol/McpServerController.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+using System.Linq;
+using BrickController2.PlatformServices.GameController;
+using BrickController2.iOS.PlatformServices.GameController;
+using BrickController2.PlatformServices.ModelContextProtocol;
+
+namespace BrickController2.iOS.PlatformServices.ModelContextProtocol;
+internal class McpServerController : GamepadControllerBase
+{
+ ///
+ /// Constructor
+ ///
+ /// reference to GameControllerService
+ /// reference to UWP's Gamepad
+ public McpServerController(GameControllerService service, McpServer mcpServer)
+ : base(service, mcpServer)
+ {
+ ControllerDevice.ChannelStatesUpdated += mcpServer_ChannelStatesUpdated;
+
+ // initialize properties
+ Name = "McpServer";
+ ControllerNumber = -1;
+ ControllerId = "McpServer";
+ }
+
+ private void mcpServer_ChannelStatesUpdated(List obj)
+ {
+ // grab all changed axis event
+ var currentEvents = obj
+ .Where(x => HasValueChanged($"{x.Channel}", (float)x.Value))
+ .ToDictionary(x => (GameControllerEventType.Axis, $"{x.Channel}"), x => (float)x.Value);
+
+ RaiseEvent(currentEvents);
+ }
+}
diff --git a/BrickController2/BrickController2.iOS/PlatformServices/ModelContextProtocol/McpServerService.cs b/BrickController2/BrickController2.iOS/PlatformServices/ModelContextProtocol/McpServerService.cs
new file mode 100644
index 00000000..4255e1ce
--- /dev/null
+++ b/BrickController2/BrickController2.iOS/PlatformServices/ModelContextProtocol/McpServerService.cs
@@ -0,0 +1,118 @@
+using BrickController2.PlatformServices.ModelContextProtocol;
+using BrickController2.UI.Services.Preferences;
+using System;
+using System.Threading.Tasks;
+
+namespace BrickController2.iOS.PlatformServices.ModelContextProtocol;
+
+public class McpServerService : IMcpServerService, IDisposable
+{
+ private readonly object _Lock = new object();
+ private readonly IPreferencesService _preferencesService;
+ private readonly McpServerBonjourPublisher _mcpServerBonjourPublisher;
+ private McpServer? _server;
+
+ public McpServerService(IPreferencesService preferencesService, McpServerBonjourPublisher mcpServerBonjourPublisher)
+ {
+ _preferencesService = preferencesService;
+ _mcpServerBonjourPublisher = mcpServerBonjourPublisher;
+ _server = null;
+
+ ApplyMcpServer();
+ }
+
+ public event EventHandler? McpServerAdded;
+ public event EventHandler? McpServerRemoved;
+
+ public bool IsMcpServerAvailable => true;
+ public McpServer? Server => _server;
+
+ public bool IsMcpServerEnabled
+ {
+ get => _preferencesService.Get("McpServerEnabled", false);
+
+ set
+ {
+ if (IsMcpServerEnabled != value)
+ {
+ _preferencesService.Set("McpServerEnabled", value);
+ ApplyMcpServer();
+ }
+ }
+ }
+
+ public int McpServerPort
+ {
+ get => _preferencesService.Get("McpServerPort", McpServerBase.PortDefault);
+
+ set
+ {
+ if (McpServerPort != value)
+ {
+ _preferencesService.Set("McpServerPort", value);
+ ApplyMcpServer();
+ }
+ }
+ }
+
+ public string McpServerAuthToken
+ {
+ get => _preferencesService.Get("McpServerAuthToken", McpServerBase.AuthTokenDefault);
+
+ set
+ {
+ if (McpServerAuthToken != value)
+ {
+ _preferencesService.Set("McpServerAuthToken", value);
+ ApplyMcpServer();
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ lock (_Lock)
+ {
+ _server?.Dispose();
+ _server = null;
+
+ _mcpServerBonjourPublisher.Stop();
+ }
+ }
+
+ private void ApplyMcpServer()
+ {
+ lock (_Lock)
+ {
+ if (_server != null)
+ {
+ McpServerRemoved?.Invoke(this, _server);
+
+ _server?.Dispose();
+ _server = null;
+
+ _mcpServerBonjourPublisher.Stop();
+ }
+
+ if (IsMcpServerEnabled)
+ {
+ if (_server == null)
+ {
+ _server = new McpServer(McpServerPort, McpServerAuthToken);
+ Start();
+ McpServerAdded?.Invoke(this, _server);
+ }
+ }
+ }
+ }
+
+ public void Start()
+ {
+ // start non-blocking
+ Task.Run(() =>
+ {
+ _server?.StartAsync();
+ _mcpServerBonjourPublisher.Publish(McpServerPort);
+ });
+ }
+}
diff --git a/BrickController2/BrickController2/App.xaml.cs b/BrickController2/BrickController2/App.xaml.cs
index a1c4456f..e14eedd7 100644
--- a/BrickController2/BrickController2/App.xaml.cs
+++ b/BrickController2/BrickController2/App.xaml.cs
@@ -1,8 +1,10 @@
+using BrickController2.UI.Converters;
using BrickController2.UI.DI;
using BrickController2.UI.Pages;
using BrickController2.UI.Services.Background;
using BrickController2.UI.Services.Localization;
using BrickController2.UI.Services.Theme;
+using BrickController2.UI.Services.Translation;
using BrickController2.UI.ViewModels;
using Microsoft.Maui;
using Microsoft.Maui.ApplicationModel;
@@ -27,7 +29,8 @@ public App(
Func navigationPageFactory,
BackgroundService backgroundService,
IThemeService themeService,
- ILocalizationService localizationService)
+ ILocalizationService localizationService,
+ ITranslationService translationService)
{
InitializeComponent();
@@ -49,7 +52,10 @@ public App(
localizationService.ApplyCurrentLanguage();
themeService.ApplyCurrentTheme();
- }
+
+ // set Converter in Application.Resources
+ Resources["NoAuthTokenToPlaceholderConverter"] = new NoAuthTokenToPlaceholderConverter(translationService);
+ }
internal void ReloadRootPage()
{
diff --git a/BrickController2/BrickController2/BrickController2.csproj b/BrickController2/BrickController2/BrickController2.csproj
index 7146e441..55073cff 100644
--- a/BrickController2/BrickController2/BrickController2.csproj
+++ b/BrickController2/BrickController2/BrickController2.csproj
@@ -53,6 +53,7 @@
+
diff --git a/BrickController2/BrickController2/PlatformServices/GameController/GameControllerServiceBase.cs b/BrickController2/BrickController2/PlatformServices/GameController/GameControllerServiceBase.cs
index a9f08f8d..a37c145a 100644
--- a/BrickController2/BrickController2/PlatformServices/GameController/GameControllerServiceBase.cs
+++ b/BrickController2/BrickController2/PlatformServices/GameController/GameControllerServiceBase.cs
@@ -10,8 +10,7 @@ namespace BrickController2.PlatformServices.GameController;
///
/// Base class for implementation of
///
-public abstract class GameControllerServiceBase : IGameControllerServiceInternal
- where TGameController : class, IGameController
+public abstract class GameControllerServiceBase : IGameControllerServiceInternal
{
protected readonly object _lockObject = new();
protected readonly ILogger _logger;
@@ -21,7 +20,7 @@ public abstract class GameControllerServiceBase : IGameControll
///
/// Collection of available gamepads having
///
- private readonly List _availableControllers = [];
+ private readonly List _availableControllers = [];
protected GameControllerServiceBase(ILogger logger)
{
@@ -97,6 +96,8 @@ protected virtual void RemoveAllControllers()
foreach (var controller in _availableControllers)
{
controller.Stop();
+
+ (controller as IDisposable)?.Dispose();
}
_availableControllers.Clear();
// notify removal
@@ -119,7 +120,7 @@ protected int GetFirstUnusedControllerNumber()
}
}
- protected void AddController(TGameController controller)
+ protected void AddController(IGameController controller)
{
lock (_lockObject)
{
@@ -130,27 +131,36 @@ protected void AddController(TGameController controller)
}
}
- protected bool TryRemove(Predicate predicate, [MaybeNullWhen(false)] out TGameController controller)
+ protected bool TryRemove(Predicate predicate, [MaybeNullWhen(false)] out TGameController controller)
+ where TGameController : class, IGameController
{
lock (_lockObject)
{
// remove and stop the controller
- if (_availableControllers.Remove(predicate, out controller))
+ if (_availableControllers.Remove(x => x is TGameController tc && predicate(tc), out var removed))
{
+ controller = (TGameController)removed; // safe due to pattern match above
controller.Stop();
+
// notify removal
OnGameControllersChangedEvent(NotifyGameControllersChangedAction.Disconnected, controller);
+
+ (controller as IDisposable)?.Dispose();
+
return true;
}
+
+ controller = null;
return false;
}
}
- protected bool TryGetController(Predicate predicate, [MaybeNullWhen(false)] out TGameController controller)
+ protected bool TryGetController(Predicate predicate, [MaybeNullWhen(false)] out TGameController controller)
+ where TGameController : class, IGameController
{
lock (_lockObject)
{
- controller = _availableControllers.FirstOrDefault(x => predicate(x));
+ controller = _availableControllers.OfType().FirstOrDefault(x => predicate(x));
return controller is not null;
}
}
diff --git a/BrickController2/BrickController2/PlatformServices/GameController/GameControllers.cs b/BrickController2/BrickController2/PlatformServices/GameController/GameControllers.cs
index 9ca61de9..a4dfefaf 100644
--- a/BrickController2/BrickController2/PlatformServices/GameController/GameControllers.cs
+++ b/BrickController2/BrickController2/PlatformServices/GameController/GameControllers.cs
@@ -14,17 +14,6 @@ public static class GameControllers
public const float AXIS_MIN_VALUE = - 1.0f;
public const float AXIS_MAX_VALUE = 1.0f;
- ///
- /// Creates an identifier string for the controller from the given index
- ///
- /// zero-based index
- /// Identifier
- public static string GetControllerIdFromIndex(int controllerIndex)
- {
- // controllerIndex == 0 -> "Controller 1"
- return $"Controller {controllerIndex + 1}";
- }
-
///
/// Creates an identifier string for the controller from the given
///
diff --git a/BrickController2/BrickController2/PlatformServices/GameController/GamepadControllerBase.cs b/BrickController2/BrickController2/PlatformServices/GameController/GamepadControllerBase.cs
index 55a82f59..a8f33c5c 100644
--- a/BrickController2/BrickController2/PlatformServices/GameController/GamepadControllerBase.cs
+++ b/BrickController2/BrickController2/PlatformServices/GameController/GamepadControllerBase.cs
@@ -4,7 +4,7 @@
namespace BrickController2.PlatformServices.GameController;
-public abstract class GamepadControllerBase : IGameController where TGamepad : class
+public abstract class GamepadControllerBase : IGameController where TControllerDevice : class
{
/// stored last value per axis to detect changes
private readonly Dictionary _lastAxisValues = [];
@@ -13,10 +13,10 @@ public abstract class GamepadControllerBase : IGameController where TG
private readonly IGameControllerServiceInternal _controllerService;
protected GamepadControllerBase(IGameControllerServiceInternal controllerService,
- TGamepad gamepad)
+ TControllerDevice controllerDevice)
{
_controllerService = controllerService;
- Gamepad = gamepad;
+ ControllerDevice = controllerDevice;
}
///
@@ -30,19 +30,15 @@ protected GamepadControllerBase(IGameControllerServiceInternal controllerService
public string ControllerId { get; protected init; } = default!;
///
- /// Unique and persistant identifier of device
+ /// DisplayName of the controller
///
- public string UniquePersistantDeviceId { get; protected init; } = default!;
-
public string Name { get; protected init; } = default!;
- public int VendorId { get; protected init; }
- public int ProductId { get; protected init; }
///
- /// Native instance of gamepad
+ /// Native instance of controllerdevice
///
- public TGamepad Gamepad { get; }
+ public TControllerDevice ControllerDevice { get; }
public virtual void Start()
{
diff --git a/BrickController2/BrickController2/PlatformServices/GameController/IGameController.cs b/BrickController2/BrickController2/PlatformServices/GameController/IGameController.cs
index f0c8e60f..61139b65 100644
--- a/BrickController2/BrickController2/PlatformServices/GameController/IGameController.cs
+++ b/BrickController2/BrickController2/PlatformServices/GameController/IGameController.cs
@@ -18,16 +18,6 @@ public interface IGameController
///
string Name { get; }
- ///
- /// Vendor ID of the game controller.
- ///
- int VendorId { get; }
-
- ///
- /// Product ID of the game controller.
- ///
- int ProductId { get; }
-
///
/// Start the controller and publishing of its events
///
diff --git a/BrickController2/BrickController2/PlatformServices/ModelContextProtocol/IMcpServerService.cs b/BrickController2/BrickController2/PlatformServices/ModelContextProtocol/IMcpServerService.cs
new file mode 100644
index 00000000..72992b47
--- /dev/null
+++ b/BrickController2/BrickController2/PlatformServices/ModelContextProtocol/IMcpServerService.cs
@@ -0,0 +1,27 @@
+namespace BrickController2.PlatformServices.ModelContextProtocol;
+
+///
+/// Interface for managing the MCP (Model Context Protocol) server service.
+///
+public interface IMcpServerService
+{
+ ///
+ /// Gets a value indicating whether the MCP server is currently available.
+ ///
+ bool IsMcpServerAvailable { get; }
+
+ ///
+ /// Gets or sets a value indicating whether the MCP server is enabled.
+ ///
+ bool IsMcpServerEnabled { get; set; }
+
+ ///
+ /// Gets or sets the port number for the MCP server.
+ ///
+ int McpServerPort { get; set; }
+
+ ///
+ /// Gets or sets the authentication token for the MCP server.
+ ///
+ string McpServerAuthToken { get; set; }
+}
diff --git a/BrickController2/BrickController2/PlatformServices/ModelContextProtocol/McpServer.cs b/BrickController2/BrickController2/PlatformServices/ModelContextProtocol/McpServer.cs
new file mode 100644
index 00000000..9c01a548
--- /dev/null
+++ b/BrickController2/BrickController2/PlatformServices/ModelContextProtocol/McpServer.cs
@@ -0,0 +1,391 @@
+using EmbedIO;
+using EmbedIO.Routing;
+using EmbedIO.WebApi;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace BrickController2.PlatformServices.ModelContextProtocol;
+public sealed class McpServer : McpServerBase, IDisposable
+{
+ private const string ToolSetChannels = "set_channels";
+ private const string ToolStop = "stop";
+
+ private readonly string _url;
+ private readonly bool _enableCors;
+ private readonly object _channelLock = new();
+ private readonly List _channelStates = new();
+ private readonly string _authToken;
+
+ private WebServer? _server;
+
+ public event Action>? ChannelStatesUpdated;
+
+ public McpServer(int port, string authToken = "")
+ : this($"http://*:{port}", true, authToken)
+ {
+ }
+
+ public McpServer(string url = "http://*:5000", bool enableCors = true, string authToken = "")
+ {
+ _url = url;
+ _enableCors = enableCors;
+ _authToken = authToken;
+ }
+
+ public void Dispose()
+ {
+ if (_server != null)
+ {
+ _server.Dispose();
+ _server = null;
+ }
+ }
+
+ public Task StartAsync(CancellationToken ct = default)
+ {
+ if (_server != null)
+ {
+ return Task.CompletedTask;
+ }
+
+ var server = new WebServer(o => o
+ .WithUrlPrefix(_url)
+ .WithMode(HttpListenerMode.EmbedIO))
+ .WithLocalSessionManager();
+
+ // CORS (if available in your EmbedIO version)
+ if (_enableCors)
+ {
+ try
+ {
+ server.WithCors(origins: "*", headers: "*", methods: "*");
+ }
+ catch
+ {
+ // Optional: add a manual CORS ActionModule fallback if needed.
+ }
+ }
+
+ server.WithWebApi("/", m => m
+ .WithController(() => new McpController(this)));
+
+ _server = server;
+
+ // Start is non-blocking
+ return _server.RunAsync();
+ }
+
+ public Task StopAsync(CancellationToken ct = default)
+ {
+ if (_server == null)
+ {
+ return Task.CompletedTask;
+ }
+
+ _server.Dispose();
+ _server = null;
+
+ return Task.CompletedTask;
+ }
+
+ // --- State & update helpers ---
+
+ private List Snapshot()
+ {
+ lock (_channelLock) { return _channelStates.ToList(); }
+ }
+
+ private List SetChannels(IDictionary map)
+ {
+ List channelStates;
+
+ lock (_channelLock)
+ {
+ _channelStates.Clear();
+ _channelStates.AddRange(map.Select(kv => new ChannelValue { Channel = kv.Key, Value = kv.Value }));
+ channelStates = _channelStates.ToList();
+ }
+
+ ChannelStatesUpdated?.Invoke(channelStates);
+
+ return channelStates;
+ }
+
+ private List StopAll()
+ {
+ List channelStates;
+
+ lock (_channelLock)
+ {
+ for (int i = 0; i < _channelStates.Count; i++)
+ {
+ _channelStates[i].Value = 0.0;
+ }
+ channelStates = _channelStates.ToList();
+ }
+
+ ChannelStatesUpdated?.Invoke(channelStates);
+
+ return channelStates;
+ }
+
+
+ // --- Auth helper ---
+ private bool IsAuthorized(WebApiController controller)
+ {
+ // no token, no check
+ if (string.IsNullOrEmpty(_authToken))
+ {
+ return true;
+ }
+
+ var authHeader = controller.Request.Headers["Authorization"];
+
+ if (string.IsNullOrEmpty(authHeader))
+ {
+ return false;
+ }
+
+ var token = authHeader.Replace("Bearer ", "", StringComparison.OrdinalIgnoreCase);
+ return string.Equals(token, _authToken, StringComparison.Ordinal);
+ }
+
+ // --- Contracts ---
+
+ public record ChannelValue
+ {
+ [JsonPropertyName("channel")]
+ public int Channel { get; set; }
+
+ [JsonPropertyName("value")]
+ public double Value { get; set; }
+ }
+
+ public record ExecuteRequest
+ {
+ public string name { get; init; } = "";
+ public Dictionary? arguments { get; init; }
+ }
+
+ public record ToolDescription
+ {
+ public string name { get; init; } = "";
+ public string? description { get; init; }
+ public JsonSchema? parameters { get; init; }
+ }
+
+ public record JsonSchema
+ {
+ public string type { get; init; } = "object";
+ public Dictionary? properties { get; init; }
+ public string[]? required { get; init; }
+ public bool additionalProperties { get; init; } = false;
+ }
+
+ // --- Web API Controller ---
+
+ private sealed class McpController : WebApiController
+ {
+ private readonly McpServer _svc;
+
+ public McpController(McpServer svc) => _svc = svc;
+
+ [Route(HttpVerbs.Get, "/health")]
+ public object Health()
+ {
+ if (!_svc.IsAuthorized(this))
+ {
+ return HttpStatusCode.Unauthorized;
+ }
+
+ return new
+ {
+ ok = true,
+ service = "mcp-brickcontroller",
+ time = DateTimeOffset.UtcNow
+ };
+ }
+
+ [Route(HttpVerbs.Get, "/mcp/channels")]
+ public object GetChannels()
+ {
+ if (!_svc.IsAuthorized(this))
+ {
+ return HttpStatusCode.Unauthorized;
+ }
+
+ return new
+ {
+ channels = _svc.Snapshot()
+ };
+ }
+
+ [Route(HttpVerbs.Get, "/mcp/capabilities")]
+ public object Capabilities()
+ {
+ if (!_svc.IsAuthorized(this))
+ {
+ return HttpStatusCode.Unauthorized;
+ }
+
+ var setChannelsTool = new ToolDescription
+ {
+ name = ToolSetChannels,
+ description = "Sets one or more channel values, each as a channel number and a value between -1.0 and 1.0.",
+ parameters = new JsonSchema
+ {
+ type = "object",
+ properties = new Dictionary
+ {
+ ["channels"] = new Dictionary
+ {
+ ["type"] = "array",
+ ["items"] = new Dictionary
+ {
+ ["type"] = "object",
+ ["properties"] = new Dictionary
+ {
+ ["channel"] = new Dictionary
+ {
+ ["type"] = "integer",
+ ["description"] = "Channel number (integer)."
+ },
+ ["value"] = new Dictionary
+ {
+ ["type"] = "number",
+ ["minimum"] = -1.0,
+ ["maximum"] = 1.0,
+ ["description"] = "Channel value between -1.0 and 1.0."
+ }
+ },
+ ["required"] = new[] { "channel", "value" }
+ },
+ ["description"] = "Array of channel/value objects."
+ }
+ },
+ required = new[] { "channels" },
+ additionalProperties = false
+ }
+ };
+
+ var stopTool = new ToolDescription
+ {
+ name = ToolStop,
+ description = "Stops all activity by setting all channels to 0.",
+ parameters = new JsonSchema
+ {
+ type = "object",
+ properties = new Dictionary(),
+ required = Array.Empty(),
+ additionalProperties = false
+ }
+ };
+
+ return new
+ {
+ capabilities = new[] { "tools" },
+ tools = new[] { setChannelsTool, stopTool },
+ server = new { name = "android-mcp-brickcontroller", version = "0.1.0" }
+ };
+ }
+
+ [Route(HttpVerbs.Post, "/mcp/tools/execute")]
+ public async Task