Skip to content

Commit beb0874

Browse files
authored
Merge pull request #34 from EmoTracker-Community/claude/adoring-pasteur
Add MCP dev server extension for AI-assisted testing
2 parents a390323 + 71acdcd commit beb0874

17 files changed

Lines changed: 2511 additions & 0 deletions

‎EmoTracker.Data/ScriptManager.cs‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,28 @@ public void Reset()
483483
ClearLogOutput();
484484
}
485485

486+
[NLua.LuaHide]
487+
public object[] ExecuteLuaString(string luaCode)
488+
{
489+
if (mLua == null)
490+
throw new InvalidOperationException("No Lua environment is loaded");
491+
return mLua.DoString(luaCode);
492+
}
493+
494+
[NLua.LuaHide]
495+
public object GetLuaGlobal(string name)
496+
{
497+
if (mLua == null)
498+
throw new InvalidOperationException("No Lua environment is loaded");
499+
return mLua[name];
500+
}
501+
502+
[NLua.LuaHide]
503+
public bool IsLuaLoaded
504+
{
505+
get { return mLua != null; }
506+
}
507+
486508
[NLua.LuaHide]
487509
public object FindObjectForCode(string code)
488510
{

‎EmoTracker/EmoTracker.csproj‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,18 @@
1010
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
1111
</PropertyGroup>
1212

13+
<!-- MCP dev server: only included in Debug builds -->
14+
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
15+
<FrameworkReference Include="Microsoft.AspNetCore.App" />
16+
</ItemGroup>
17+
1318
<!-- PluginManager.cs is intentionally excluded from compilation -->
19+
<!-- MCP server extension is excluded from Release builds -->
1420
<ItemGroup>
1521
<Compile Remove="PluginManager.cs" />
22+
<Compile Remove="Extensions\McpServer\**\*" Condition="'$(Configuration)' != 'Debug'" />
23+
<AvaloniaXaml Remove="Extensions\McpServer\**\*" Condition="'$(Configuration)' != 'Debug'" />
24+
<AvaloniaResource Remove="Extensions\McpServer\**\*" Condition="'$(Configuration)' != 'Debug'" />
1625
</ItemGroup>
1726

1827
<!-- Avalonia resource items — accessible via avares://EmoTracker/... -->
@@ -50,6 +59,8 @@
5059
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
5160
<PackageReference Include="Serilog" Version="4.3.1" />
5261
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
62+
<PackageReference Include="ModelContextProtocol" Version="1.2.0" Condition="'$(Configuration)' == 'Debug'" />
63+
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="1.2.0" Condition="'$(Configuration)' == 'Debug'" />
5364
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
5465
<PackageReference Include="TwitchLib" Version="3.5.3" />
5566
<PackageReference Include="WebSocketSharp" Version="1.0.3-rc11" NoWarn="NU1701" />
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
using EmoTracker.Core;
2+
using Microsoft.AspNetCore.Builder;
3+
using Microsoft.AspNetCore.Hosting;
4+
using Microsoft.Extensions.DependencyInjection;
5+
using Microsoft.Extensions.Logging;
6+
using ModelContextProtocol;
7+
using Newtonsoft.Json.Linq;
8+
using Serilog;
9+
using System;
10+
using System.Threading;
11+
using System.Threading.Tasks;
12+
13+
namespace EmoTracker.Extensions.McpServer
14+
{
15+
public class McpServerExtension : ObservableObject, Extension
16+
{
17+
const int DefaultPort = 27125;
18+
19+
public string Name => "MCP Dev Server";
20+
public string UID => "emotracker_mcp_server";
21+
public int Priority => -500;
22+
23+
private bool mbActive;
24+
public bool Active
25+
{
26+
get => mbActive;
27+
private set => SetProperty(ref mbActive, value);
28+
}
29+
30+
private string mStatusText = "Stopped";
31+
public string StatusText
32+
{
33+
get => mStatusText;
34+
private set => SetProperty(ref mStatusText, value);
35+
}
36+
37+
private WebApplication mApp;
38+
private McpStatusIndicator mStatusBarControl;
39+
40+
public object StatusBarControl
41+
{
42+
get
43+
{
44+
if (!UserDirectory.IsDevMode)
45+
return null;
46+
47+
if (mStatusBarControl == null)
48+
mStatusBarControl = new McpStatusIndicator { DataContext = this };
49+
return mStatusBarControl;
50+
}
51+
}
52+
53+
public void Start()
54+
{
55+
if (!UserDirectory.IsDevMode)
56+
return;
57+
58+
_ = StartServerAsync();
59+
}
60+
61+
public void Stop()
62+
{
63+
if (mApp != null)
64+
{
65+
try
66+
{
67+
mApp.StopAsync().GetAwaiter().GetResult();
68+
mApp.DisposeAsync().AsTask().GetAwaiter().GetResult();
69+
}
70+
catch (Exception ex)
71+
{
72+
Log.Warning(ex, "[MCP] Error stopping server");
73+
}
74+
mApp = null;
75+
Active = false;
76+
StatusText = "Stopped";
77+
}
78+
}
79+
80+
private async Task StartServerAsync()
81+
{
82+
try
83+
{
84+
int port = DefaultPort;
85+
string portEnv = Environment.GetEnvironmentVariable("EMOTRACKER_MCP_PORT");
86+
if (!string.IsNullOrEmpty(portEnv) && int.TryParse(portEnv, out int envPort))
87+
port = envPort;
88+
89+
var builder = WebApplication.CreateBuilder();
90+
builder.WebHost.UseUrls($"http://localhost:{port}");
91+
92+
builder.Logging.ClearProviders();
93+
94+
builder.Services.AddMcpServer(options =>
95+
{
96+
options.ServerInfo = new()
97+
{
98+
Name = "EmoTracker Dev Server",
99+
Version = ApplicationVersion.Current.ToString()
100+
};
101+
})
102+
.WithTools<Tools.PackDataTools>()
103+
.WithTools<Tools.ApplicationControlTools>()
104+
.WithTools<Tools.LuaTools>()
105+
.WithTools<Tools.ScreenshotTools>()
106+
.WithTools<Tools.UiAutomationTools>()
107+
.WithTools<Tools.SettingsTools>()
108+
.WithTools<Tools.LocationTools>()
109+
.WithTools<Tools.SaveLoadTools>()
110+
.WithTools<Tools.WindowTools>()
111+
.WithTools<Tools.NoteTools>()
112+
.WithTools<Tools.ExtensionTools>()
113+
.WithTools<Tools.PackageTools>()
114+
.WithHttpTransport();
115+
116+
mApp = builder.Build();
117+
mApp.MapMcp();
118+
119+
await mApp.StartAsync();
120+
121+
Active = true;
122+
StatusText = $"Listening on port {port}";
123+
Log.Information("[MCP] Server listening on port {Port}", port);
124+
}
125+
catch (Exception ex)
126+
{
127+
Active = false;
128+
StatusText = $"Error: {ex.Message}";
129+
Log.Error(ex, "[MCP] Failed to start server");
130+
}
131+
}
132+
133+
public void OnPackageUnloaded() { }
134+
public void OnPackageLoaded() { }
135+
136+
public JToken SerializeToJson() => null;
137+
public bool DeserializeFromJson(JToken token) => true;
138+
}
139+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<UserControl x:Class="EmoTracker.Extensions.McpServer.McpStatusIndicator"
2+
xmlns="https://github.com/avaloniaui"
3+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
4+
<ToolTip.Tip>
5+
<TextBlock Text="{Binding StatusText, StringFormat='MCP: {0}'}" />
6+
</ToolTip.Tip>
7+
<Ellipse Width="10" Height="10" StrokeThickness="0">
8+
<Ellipse.Styles>
9+
<Style Selector="Ellipse">
10+
<Setter Property="Fill" Value="#888888" />
11+
</Style>
12+
</Ellipse.Styles>
13+
</Ellipse>
14+
</UserControl>
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
using Avalonia.Controls;
2+
using Avalonia.Controls.Shapes;
3+
using Avalonia.Media;
4+
using System;
5+
using System.ComponentModel;
6+
7+
namespace EmoTracker.Extensions.McpServer
8+
{
9+
public partial class McpStatusIndicator : UserControl
10+
{
11+
static readonly IBrush ActiveBrush = new SolidColorBrush(Color.Parse("#4CAF50"));
12+
static readonly IBrush InactiveBrush = new SolidColorBrush(Color.Parse("#888888"));
13+
14+
public McpStatusIndicator()
15+
{
16+
InitializeComponent();
17+
DataContextChanged += OnDataContextChanged;
18+
}
19+
20+
private McpServerExtension _extension;
21+
22+
private void OnDataContextChanged(object sender, EventArgs e)
23+
{
24+
if (_extension != null)
25+
_extension.PropertyChanged -= Extension_PropertyChanged;
26+
27+
_extension = DataContext as McpServerExtension;
28+
29+
if (_extension != null)
30+
_extension.PropertyChanged += Extension_PropertyChanged;
31+
32+
UpdateIndicator();
33+
}
34+
35+
private void Extension_PropertyChanged(object sender, PropertyChangedEventArgs e)
36+
{
37+
if (e.PropertyName == nameof(McpServerExtension.Active))
38+
UpdateIndicator();
39+
}
40+
41+
private void UpdateIndicator()
42+
{
43+
var ellipse = this.FindControl<Ellipse>("PART_Indicator");
44+
if (ellipse == null)
45+
{
46+
// Fall back: find the first Ellipse in the visual tree
47+
ellipse = this.GetVisualDescendant<Ellipse>();
48+
}
49+
if (ellipse != null && _extension != null)
50+
ellipse.Fill = _extension.Active ? ActiveBrush : InactiveBrush;
51+
}
52+
}
53+
54+
internal static class VisualExtensions
55+
{
56+
public static T GetVisualDescendant<T>(this Avalonia.Visual visual) where T : class
57+
{
58+
if (visual is Avalonia.Controls.Panel panel)
59+
{
60+
foreach (var child in panel.Children)
61+
{
62+
if (child is T match)
63+
return match;
64+
if (child is Avalonia.Visual v)
65+
{
66+
var result = GetVisualDescendant<T>(v);
67+
if (result != null) return result;
68+
}
69+
}
70+
}
71+
else if (visual is Avalonia.Controls.Decorator decorator && decorator.Child != null)
72+
{
73+
if (decorator.Child is T match)
74+
return match;
75+
if (decorator.Child is Avalonia.Visual v)
76+
return GetVisualDescendant<T>(v);
77+
}
78+
else if (visual is ContentControl cc && cc.Content is Avalonia.Visual cv)
79+
{
80+
if (cv is T match)
81+
return match;
82+
return GetVisualDescendant<T>(cv);
83+
}
84+
return null;
85+
}
86+
}
87+
}

0 commit comments

Comments
 (0)