Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions MtaDebugCompanionMcp.Common/Clients/MtaServerDebugClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;

namespace MtaDebugCompanionMcp.Common.Clients;

public class MtaServerDebugClient(HttpClient httpClient)
{
private static StringContent ToJsonContent<T>(T value) =>
new(JsonSerializer.Serialize(value), Encoding.UTF8, "application/json");

public async Task<string> RunCode(string code)
{
var response = await httpClient.PostAsync("/debugCompanion/call/httpRun", ToJsonContent(new[] { code }));
return await response.Content.ReadAsStringAsync();
}

public async Task<string> RestartResource(string name)
{
var response = await httpClient.PostAsync("/debugCompanion/call/httpRestartResource", ToJsonContent(new[] { name }));
return await response.Content.ReadAsStringAsync();
}

public async Task<string> StartResource(string name)
{
var response = await httpClient.PostAsync("/debugCompanion/call/httpStartResource", ToJsonContent(new[] { name }));
return await response.Content.ReadAsStringAsync();
}

public async Task<string> StopResource(string name)
{
var response = await httpClient.PostAsync("/debugCompanion/call/httpStopResource", ToJsonContent(new[] { name }));
return await response.Content.ReadAsStringAsync();
}

public async Task<string> GetLogs()
{
var response = await httpClient.PostAsync("/debugCompanion/call/httpGetDebugLog", ToJsonContent(Array.Empty<string>()));
return await response.Content.ReadAsStringAsync();
}
}
22 changes: 22 additions & 0 deletions MtaDebugCompanionMcp.Common/MtaDebugCompanionMcp.Common.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.5" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="10.4.0" />
<PackageReference Include="ModelContextProtocol" Version="1.2.0" />
</ItemGroup>

<ItemGroup>
<Folder Include="Services\" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Microsoft.Extensions.DependencyInjection;
using MtaDebugCompanionMcp.Common.Clients;

namespace MtaDebugCompanionMcp.Common;

public static class MtaDebugCompanionMcpServiceCollectionExtensions
{
extension(IServiceCollection services)
{
/// <summary>
/// Adds the services required for the MTA debug companion MCP.
/// </summary>
/// <returns></returns>
public IServiceCollection AddMtaDebugCompanionMcpServices(string? baseAddress, string? apiKey)
{
services.AddHttpClient<MtaServerDebugClient>(x =>
{
x.BaseAddress = new Uri(baseAddress ?? "http://localhost:22005");
x.DefaultRequestHeaders.Add("api-key", apiKey ?? "default");
});

return services;
}
}
}
62 changes: 62 additions & 0 deletions MtaDebugCompanionMcp.Common/Tools/MtaDebugCompanionTools.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using ModelContextProtocol.Server;
using MtaDebugCompanionMcp.Common.Clients;
using System.ComponentModel;

namespace MtaDebugCompanionMcp.Common.Tools;

public enum ScriptSide { Server, Client, Shared }

/// <summary>
/// This class contains the tools that are exposed to the AI agent for accessing information from the MTA wiki
/// </summary>
/// <param name="wikiScraper"></param>
[McpServerToolType]
[Description("Tools that allow access certain actions on the MTA server")]
public class MtaDebugCompanionTools(MtaServerDebugClient mtaServer)
{
private const string MtaLogoUrl = "https://wiki.multitheftauto.com/images/thumb/5/58/Mtalogo.png/100px-Mtalogo.png";

[McpServerTool(Name = nameof(RunCode), IconSource = MtaLogoUrl)]
[Description("""
Runs any arbitrary Lua code on the MTA server, for the purpose of debugging. Will return any result from the code that was run.
If running more than a single statement returned information can be obtained using:
(function()
-- code here
return toJSON({ --[[ any value here ]] })
end)()
""")]
public Task<string> RunCode(string code)
{
return mtaServer.RunCode(code);
}

[McpServerTool(Name = nameof(RestartResource), IconSource = MtaLogoUrl)]
[Description("Restarts the specified resource on the MTA server.")]
public Task<string> RestartResource(string name)
{
return mtaServer.RestartResource(name);
}

[McpServerTool(Name = nameof(StartResource), IconSource = MtaLogoUrl)]
[Description("Starts the specified resource on the MTA server.")]
public Task<string> StartResource(string name)
{
return mtaServer.StartResource(name);
}

[McpServerTool(Name = nameof(StopResource), IconSource = MtaLogoUrl)]
[Description("Stops the specified resource on the MTA server.")]
public Task<string> StopResource(string name)
{
return mtaServer.StopResource(name);
}

[McpServerTool(Name = nameof(GetLogs), IconSource = MtaLogoUrl)]
[Description("Retrieves the latest 100 lines of debug logs.")]
public Task<string> GetLogs()
{
return mtaServer.GetLogs();
}


}
18 changes: 18 additions & 0 deletions MtaDebugCompanionMcp.Http/MtaDebugCompanionMcp.Http.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.23.0" />
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="1.2.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\MtaDebugCompanionMcp.Common\MtaDebugCompanionMcp.Common.csproj" />
</ItemGroup>

</Project>
21 changes: 21 additions & 0 deletions MtaDebugCompanionMcp.Http/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using MtaDebugCompanionMcp.Common;
using MtaDebugCompanionMcp.Common.Tools;

var builder = WebApplication.CreateBuilder(args);

builder.Configuration.AddJsonFile("appsettings.local.json", optional: true, reloadOnChange: true);

builder.Services.AddMtaDebugCompanionMcpServices(builder.Configuration.GetValue<string>("serverHost"), builder.Configuration.GetValue<string>("apiKey"));

builder.Services
.AddMcpServer()
.WithToolsFromAssembly(typeof(MtaDebugCompanionTools).Assembly)
.WithHttpTransport(x =>
{
x.Stateless = true;
});

var app = builder.Build();

app.MapMcp("/");
app.Run();
23 changes: 23 additions & 0 deletions MtaDebugCompanionMcp.Http/Properties/launchSettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5277",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:7117;http://localhost:5277",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
8 changes: 8 additions & 0 deletions MtaDebugCompanionMcp.Http/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
12 changes: 12 additions & 0 deletions MtaDebugCompanionMcp.Http/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",

"serverHost": "http://localhost:22005",
"apiKey": "default"
}
12 changes: 9 additions & 3 deletions MtaMcpServer.slnx
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
<Solution>
<Project Path="MtaWikiMcp.Common/MtaWikiMcp.Common.csproj" Id="64ce43c6-3f33-46f4-9df3-8ee9610b996c" />
<Project Path="MtaWikiMcp.Http/MtaWikiMcp.Http.csproj" Id="6555288c-3c57-487f-9c89-8e6d8c3303ed" />
<Project Path="MtaWikiMcp.Stdio/MtaWikiMcp.Stdio.csproj" Id="f54916ed-0dd9-4177-95e1-b2fae24b0123" />
<Folder Name="/DebugCompanion/">
<Project Path="MtaDebugCompanionMcp.Http/MtaDebugCompanionMcp.Http.csproj" Id="1f324c77-76f8-4362-84ad-ab5009f94bdb" />
<Project Path="MtaDebugCompanionMcp.Common/MtaDebugCompanionMcp.Common.csproj" Id="dcf0411c-12c2-405e-a6dc-f2eb5de451a1" />
</Folder>
<Folder Name="/Wiki/">
<Project Path="MtaWikiMcp.Common/MtaWikiMcp.Common.csproj" Id="64ce43c6-3f33-46f4-9df3-8ee9610b996c" />
<Project Path="MtaWikiMcp.Http/MtaWikiMcp.Http.csproj" Id="6555288c-3c57-487f-9c89-8e6d8c3303ed" />
<Project Path="MtaWikiMcp.Stdio/MtaWikiMcp.Stdio.csproj" Id="f54916ed-0dd9-4177-95e1-b2fae24b0123" />
</Folder>
</Solution>
24 changes: 24 additions & 0 deletions Resources/debugCompanion/debug.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
POST http://localhost:22005/debugCompanion/call/httpRun
api-key: default

[
"5 + 5"
]

###
POST http://localhost:22005/debugCompanion/call/httpRun
api-key: default

[
"(function() error(\"INTENTIONAL_TEST_ERROR_FROM_RUNCODE\") end)()"
]


###
POST http://localhost:22005/debugCompanion/call/httpRestartResource
api-key: default

[
"debugCompanion"
]

35 changes: 35 additions & 0 deletions Resources/debugCompanion/exports/debugLog.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
local debugLogCache = {}
local MAX_DEBUG_LOG = 100

local function addToDebugCache(entry)
table.insert(debugLogCache, entry)
if #debugLogCache > MAX_DEBUG_LOG then
table.remove(debugLogCache, 1)
end
end

local function onDebugMessageHandler(debugMessage, debugLevel, debugFile, debugLine, debugRed, debugGreen, debugBlue)
local entry = {
message = tostring(debugMessage),
level = tonumber(debugLevel) or 0,
file = debugFile or false,
line = debugLine or false,
color = { debugRed or 255, debugGreen or 255, debugBlue or 255 },
time = os.time()
}

addToDebugCache(entry)

return false
end

addEventHandler("onDebugMessage", root, onDebugMessageHandler)

function httpGetDebugLog()
if not verifyApiKey() then
return "Unauthorised"
end

return toJSON(debugLogCache)
end

23 changes: 23 additions & 0 deletions Resources/debugCompanion/exports/resources.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
function httpStartResource(name)
if not verifyApiKey() then
return "Unauthorised"
end

return toJSON({ result = startResource(getResourceFromName(name)) })
end

function httpRestartResource(name)
if not verifyApiKey() then
return "Unauthorised"
end

return toJSON({ result = restartResource(getResourceFromName(name)) })
end

function httpStopResource(name)
if not verifyApiKey() then
return "Unauthorised"
end

return toJSON({ result = stopResource(getResourceFromName(name)) })
end
42 changes: 42 additions & 0 deletions Resources/debugCompanion/exports/runcode.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
function httpRun(code)
if not verifyApiKey() then
return "Unauthorised"
end

local notReturned
local commandFunction, errorMsg = loadstring("return " .. code)
if errorMsg then
notReturned = true
commandFunction, errorMsg = loadstring(code)
end

if errorMsg then
return "Error: " .. errorMsg
end

local results = { pcall(commandFunction) }
if not results[1] then
return "Error: " .. results[2]
end

if not notReturned then
local resultsString = ""
local first = true
for i = 2, #results do
if first then
first = false
else
resultsString = resultsString .. ", "
end
local resultType = type(results[i])
if isElement(results[i]) then
resultType = "element:" .. getElementType(results[i])
end
resultsString = resultsString .. tostring(results[i]) .. " [" .. resultType .. "]"
end

return "Command results: " .. resultsString
end

return "Command executed!"
end
Loading