From c2300fb42d7e67860f6b3ae79935dc96b648957c Mon Sep 17 00:00:00 2001 From: NanoBob Date: Fri, 3 Apr 2026 17:58:55 +0200 Subject: [PATCH 1/5] Added initial version of the project, including function, event, search and data related tools --- .dockerignore | 30 ++ .gitignore | 430 ++++++++++++++++++ MtaMcpServer.slnx | 5 + MtaWikiMcp.Common/Clients/WikiClient.cs | 362 +++++++++++++++ MtaWikiMcp.Common/Data/peds.json | 312 +++++++++++++ MtaWikiMcp.Common/Data/vehicles.json | 214 +++++++++ MtaWikiMcp.Common/MtaWikiMcp.Common.csproj | 28 ++ .../MtaWikiMcpServiceCollectionExtensions.cs | 39 ++ .../Services/GameDataProvider.cs | 42 ++ MtaWikiMcp.Common/Tools/MtaWikiTools.cs | 72 +++ MtaWikiMcp.Http/Dockerfile | 31 ++ MtaWikiMcp.Http/MtaWikiMcp.Http.csproj | 20 + MtaWikiMcp.Http/Program.cs | 19 + .../Properties/launchSettings.json | 31 ++ MtaWikiMcp.Http/appsettings.Development.json | 8 + MtaWikiMcp.Http/appsettings.json | 9 + MtaWikiMcp.Stdio/MtaWikiMcp.Stdio.csproj | 41 ++ MtaWikiMcp.Stdio/Program.cs | 28 ++ MtaWikiMcp.Stdio/appsettings.json | 9 + MtaWikiMcp.Stdio/appsettings.local.json | 8 + readme.md | 44 ++ 21 files changed, 1782 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 MtaMcpServer.slnx create mode 100644 MtaWikiMcp.Common/Clients/WikiClient.cs create mode 100644 MtaWikiMcp.Common/Data/peds.json create mode 100644 MtaWikiMcp.Common/Data/vehicles.json create mode 100644 MtaWikiMcp.Common/MtaWikiMcp.Common.csproj create mode 100644 MtaWikiMcp.Common/MtaWikiMcpServiceCollectionExtensions.cs create mode 100644 MtaWikiMcp.Common/Services/GameDataProvider.cs create mode 100644 MtaWikiMcp.Common/Tools/MtaWikiTools.cs create mode 100644 MtaWikiMcp.Http/Dockerfile create mode 100644 MtaWikiMcp.Http/MtaWikiMcp.Http.csproj create mode 100644 MtaWikiMcp.Http/Program.cs create mode 100644 MtaWikiMcp.Http/Properties/launchSettings.json create mode 100644 MtaWikiMcp.Http/appsettings.Development.json create mode 100644 MtaWikiMcp.Http/appsettings.json create mode 100644 MtaWikiMcp.Stdio/MtaWikiMcp.Stdio.csproj create mode 100644 MtaWikiMcp.Stdio/Program.cs create mode 100644 MtaWikiMcp.Stdio/appsettings.json create mode 100644 MtaWikiMcp.Stdio/appsettings.local.json create mode 100644 readme.md diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..fe1152b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,30 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md +!**/.gitignore +!.git/HEAD +!.git/config +!.git/packed-refs +!.git/refs/heads/** \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6eb0680 --- /dev/null +++ b/.gitignore @@ -0,0 +1,430 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates +*.env + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ + +[Dd]ebug/x64/ +[Dd]ebugPublic/x64/ +[Rr]elease/x64/ +[Rr]eleases/x64/ +bin/x64/ +obj/x64/ + +[Dd]ebug/x86/ +[Dd]ebugPublic/x86/ +[Rr]elease/x86/ +[Rr]eleases/x86/ +bin/x86/ +obj/x86/ + +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +[Aa][Rr][Mm]64[Ee][Cc]/ +bld/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +# Build results on 'Bin' directories +**/[Bb]in/* +# Uncomment if you have tasks that rely on *.refresh files to move binaries +# (https://github.com/github/gitignore/pull/3736) +#!**/[Bb]in/*.refresh + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* +*.trx + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Approval Tests result files +*.received.* + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.idb +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +# but not Directory.Build.rsp, as it configures directory-level build defaults +!Directory.Build.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +**/.paket/paket.exe +paket-files/ + +# FAKE - F# Make +**/.fake/ + +# CodeRush personal settings +**/.cr/personal + +# Python Tools for Visual Studio (PTVS) +**/__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +#tools/** +#!tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog +MSBuild_Logs/ + +# AWS SAM Build and Temporary Artifacts folder +.aws-sam + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +**/.mfractor/ + +# Local History for Visual Studio +**/.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +**/.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +appsettings.local.json diff --git a/MtaMcpServer.slnx b/MtaMcpServer.slnx new file mode 100644 index 0000000..5be5eb7 --- /dev/null +++ b/MtaMcpServer.slnx @@ -0,0 +1,5 @@ + + + + + diff --git a/MtaWikiMcp.Common/Clients/WikiClient.cs b/MtaWikiMcp.Common/Clients/WikiClient.cs new file mode 100644 index 0000000..88d4d44 --- /dev/null +++ b/MtaWikiMcp.Common/Clients/WikiClient.cs @@ -0,0 +1,362 @@ +using HtmlAgilityPack; +using System.Net; +using System.Text; + +namespace MtaWikiMcp.Common.Clients; + +/// +/// This class makes requests to the MTA wiki to gather information. +/// +public class WikiClient(HttpClient httpClient) +{ + private async Task FetchPageAsync(string pageName) + { + var html = await httpClient.GetStringAsync(httpClient.BaseAddress + pageName); + var doc = new HtmlDocument(); + doc.LoadHtml(html); + return doc; + } + + private static HtmlNode? GetParserOutput(HtmlDocument doc) => + doc.DocumentNode.SelectSingleNode("//div[contains(@class,'mw-parser-output')]"); + + private static IEnumerable GetNodesAfterHeading(HtmlNode parserOutput, string headingId) + { + var heading = parserOutput.SelectSingleNode($".//span[@id='{headingId}']")?.ParentNode; + if (heading is null) + yield break; + + var node = heading.NextSibling; + while (node is not null) + { + if (node.Name is "h2" or "h1") + yield break; + yield return node; + node = node.NextSibling; + } + } + + private static string DecodeText(string text) => + WebUtility.HtmlDecode(text).Trim(); + + private static string ExtractDescription(HtmlNode content) + { + var firstParagraph = content.SelectSingleNode("p"); + return firstParagraph is null ? string.Empty : DecodeText(firstParagraph.InnerText); + } + + private static string ExtractSyntax(HtmlNode content) + { + var sb = new StringBuilder(); + string? pendingLabel = null; + + foreach (var node in GetNodesAfterHeading(content, "Syntax")) + { + if (node.Name is "h3" or "h2") + break; + + if (node.Name == "pre") + { + sb.AppendLine(DecodeText(node.InnerText)); + } + else if (node.Name == "p" && !string.IsNullOrWhiteSpace(node.InnerText)) + { + sb.AppendLine(DecodeText(node.InnerText)); + } + else if (node.Name == "div") + { + var cls = node.GetAttributeValue("class", ""); + if (cls.EndsWith("Header")) + { + pendingLabel = string.Concat(node.ChildNodes + .Where(n => n.NodeType == HtmlNodeType.Text) + .Select(n => n.InnerText.Trim())).Trim(); + if (string.IsNullOrEmpty(pendingLabel)) + pendingLabel = null; + } + else if (cls.EndsWith("Content")) + { + var preNodes = node.SelectNodes(".//pre"); + if (preNodes is not null) + { + foreach (var preNode in preNodes) + { + if (pendingLabel is not null) + { + sb.AppendLine(pendingLabel); + pendingLabel = null; + } + sb.AppendLine(DecodeText(preNode.InnerText)); + } + } + } + } + } + + return sb.ToString().Trim(); + } + + private static string ExtractExample(HtmlNode content) + { + var sb = new StringBuilder(); + + foreach (var node in GetNodesAfterHeading(content, "Example")) + { + if (node.Name == "h2") + break; + + if (node.Name == "pre") + sb.AppendLine(DecodeText(node.InnerText)); + else if (node.Name == "p" && !string.IsNullOrWhiteSpace(node.InnerText)) + sb.AppendLine(DecodeText(node.InnerText)); + } + + return sb.ToString().Trim(); + } + + public async Task GetDescriptionAsync(string functionName) + { + var doc = await FetchPageAsync(functionName); + var content = GetParserOutput(doc); + if (content is null) + return "Page not found."; + var result = ExtractDescription(content); + return string.IsNullOrEmpty(result) ? "Description not found." : result; + } + + public async Task GetSyntaxAsync(string functionName) + { + var doc = await FetchPageAsync(functionName); + var content = GetParserOutput(doc); + if (content is null) + return "Page not found."; + var result = ExtractSyntax(content); + return string.IsNullOrEmpty(result) ? "Syntax not found." : result; + } + + public async Task GetFunctionInformationAsync(string functionName) + { + var doc = await FetchPageAsync(functionName); + var content = GetParserOutput(doc); + if (content is null) + return "Page not found."; + + var sb = new StringBuilder(); + + var description = ExtractDescription(content); + if (!string.IsNullOrEmpty(description)) + { + sb.AppendLine("Description:"); + sb.AppendLine(description); + sb.AppendLine(); + } + + var syntax = ExtractSyntax(content); + if (!string.IsNullOrEmpty(syntax)) + { + sb.AppendLine("Syntax:"); + sb.AppendLine(syntax); + sb.AppendLine(); + } + + var example = ExtractExample(content); + if (!string.IsNullOrEmpty(example)) + { + sb.AppendLine("Example:"); + sb.AppendLine(example); + } + + var result = sb.ToString().Trim(); + return string.IsNullOrEmpty(result) ? "No information found." : result; + } + + public Task GetRawPageAsync(string functionName) => + httpClient.GetStringAsync(httpClient.BaseAddress + functionName); + + public async Task GetFunctionListAsync(string pageName) + { + var doc = await FetchPageAsync(pageName); + var content = GetParserOutput(doc); + if (content is null) + return "Page not found."; + + var sb = new StringBuilder(); + string currentSection = string.Empty; + bool sectionPrinted = false; + + foreach (var node in content.ChildNodes) + { + if (node.Name is "h2" or "h3") + { + var span = node.SelectSingleNode(".//span[contains(@class,'mw-headline')]"); + var heading = DecodeText(span?.InnerText ?? node.InnerText); + if (heading.Equals("Contents", StringComparison.OrdinalIgnoreCase)) + continue; + + if (sectionPrinted) + sb.AppendLine(); + + currentSection = heading; + sectionPrinted = false; + continue; + } + + if (node.Name is "ul" or "p") + { + if (string.IsNullOrEmpty(currentSection)) + continue; + + var linkNodes = node.SelectNodes(".//a"); + if (linkNodes is null) + continue; + + var functions = new List(); + foreach (var link in linkNodes) + { + var href = link.GetAttributeValue("href", ""); + var text = DecodeText(link.InnerText); + if (href.StartsWith("/wiki/") + && !href.Contains(':') + && !string.IsNullOrWhiteSpace(text) + && !text.Contains(' ')) + functions.Add(text); + } + + if (functions.Count == 0) + continue; + + if (!sectionPrinted) + { + sb.AppendLine(currentSection + ":"); + sectionPrinted = true; + } + + foreach (var fn in functions) + sb.AppendLine(" " + fn); + } + } + + var result = sb.ToString().Trim(); + return string.IsNullOrEmpty(result) ? "No functions found." : result; + } + + public async Task GetEventParametersAsync(string eventName) + { + var doc = await FetchPageAsync(eventName); + var content = GetParserOutput(doc); + if (content is null) + return "Page not found."; + + var sb = new StringBuilder(); + + bool hasParameters = false; + foreach (var node in GetNodesAfterHeading(content, "Parameters")) + { + if (node.Name is "h2" or "h3") + break; + + if (node.Name == "pre") + { + if (!hasParameters) + { + sb.AppendLine("Parameters:"); + hasParameters = true; + } + sb.AppendLine(DecodeText(node.InnerText)); + } + else if (node.Name == "ul") + { + var items = node.SelectNodes("li"); + if (items is not null) + { + foreach (var li in items) + sb.AppendLine("- " + DecodeText(li.InnerText)); + } + } + } + + if (!hasParameters) + sb.AppendLine("No parameters."); + + sb.AppendLine(); + + bool hasSource = false; + foreach (var node in GetNodesAfterHeading(content, "Source")) + { + if (node.Name is "h2" or "h3") + break; + + if (node.Name == "p" && !string.IsNullOrWhiteSpace(node.InnerText)) + { + sb.AppendLine("Source:"); + sb.AppendLine(DecodeText(node.InnerText)); + hasSource = true; + break; + } + } + + if (!hasSource) + sb.AppendLine("Source: Not specified."); + + return sb.ToString().Trim(); + } + + public async Task GetExampleAsync(string functionName) + { + var doc = await FetchPageAsync(functionName); + var content = GetParserOutput(doc); + if (content is null) + return "Page not found."; + var result = ExtractExample(content); + return string.IsNullOrEmpty(result) ? "Example not found." : result; + } + + public async Task SearchWikiAsync(string query) + { + var encodedQuery = Uri.EscapeDataString(query); + var doc = await FetchPageAsync($"Special:Search?search={encodedQuery}&go=Go"); + + var resultNodes = doc.DocumentNode.SelectNodes("//ul[contains(@class,'mw-search-results')]/li"); + + if (resultNodes is null || resultNodes.Count == 0) + { + // go=Go redirected to a direct page match — extract the page title from the heading + var heading = doc.DocumentNode.SelectSingleNode("//h1[@id='firstHeading']"); + if (heading is not null) + { + var title = DecodeText(heading.InnerText); + return $"Direct match found:\nPage: {title}\nUse '{Uri.EscapeDataString(title)}' with GetFunctionInformation or GetPageSource."; + } + + return "No results found."; + } + + var sb = new StringBuilder(); + foreach (var li in resultNodes) + { + var link = li.SelectSingleNode(".//div[contains(@class,'mw-search-result-heading')]//a") + ?? li.SelectSingleNode(".//a"); + if (link is null) + continue; + + var href = link.GetAttributeValue("href", ""); + var title = DecodeText(link.InnerText); + var pageName = href.StartsWith("/wiki/") ? href["/wiki/".Length..] : href; + + sb.AppendLine($"Page: {title}"); + sb.AppendLine($"Use with other tools: {pageName}"); + + var snippet = li.SelectSingleNode(".//div[contains(@class,'searchresult')]"); + if (snippet is not null) + { + var description = DecodeText(snippet.InnerText); + if (!string.IsNullOrWhiteSpace(description)) + sb.AppendLine($"Description: {description}"); + } + + sb.AppendLine(); + } + + return sb.ToString().Trim(); + } +} diff --git a/MtaWikiMcp.Common/Data/peds.json b/MtaWikiMcp.Common/Data/peds.json new file mode 100644 index 0000000..73eb15c --- /dev/null +++ b/MtaWikiMcp.Common/Data/peds.json @@ -0,0 +1,312 @@ +{ + "cj": 0, + "truth": 1, + "maccer": 2, + "cdeput": 3, + "sfpdm1": 4, + "bb": 5, + "wfycrp": 6, + "male01": 7, + "wmycd2": 8, + "bfori": 9, + "bfost": 10, + "vbfycrp": 11, + "bfyri": 12, + "bfyst": 13, + "bmori": 14, + "bmost": 15, + "bmyap": 16, + "bmybu": 17, + "bmybe": 18, + "bmydj": 19, + "bmyri": 20, + "bmycr": 21, + "bmyst": 22, + "wmybmx": 23, + "wbdyg1": 24, + "wbdyg2": 25, + "wmybp": 26, + "wmycon": 27, + "bmydrug": 28, + "wmydrug": 29, + "hmydrug": 30, + "dwfolc": 31, + "dwmolc1": 32, + "dwmolc2": 33, + "dwmylc1": 34, + "hmogar": 35, + "wmygol1": 36, + "wmygol2": 37, + "hfori": 38, + "hfost": 39, + "hfyri": 40, + "hfyst": 41, + "suzie": 42, + "hmori": 43, + "hmost": 44, + "hmybe": 45, + "hmyri": 46, + "hmycr": 47, + "hmyst": 48, + "omokung": 49, + "wmymech": 50, + "bmymoun": 51, + "wmymoun": 52, + "ofori": 53, + "ofost": 54, + "ofyri": 55, + "ofyst": 56, + "omori": 57, + "omost": 58, + "omyri": 59, + "omyst": 60, + "wmyplt": 61, + "wmopj": 62, + "bfypro": 63, + "hfypro": 64, + "vwmyap": 65, + "bmypol1": 66, + "bmypol2": 67, + "wmoprea": 68, + "sbfyst": 69, + "wmosci": 70, + "wmysgrd": 71, + "swmyhp1": 72, + "swmyhp2": 73, + "swfopro": 75, + "wfystew": 76, + "swmotr1": 77, + "wmotr1": 78, + "bmotr1": 79, + "vbmybox": 80, + "vwmybox": 81, + "vhmyelv": 82, + "vbmyelv": 83, + "vimyelv": 84, + "vwfypro": 85, + "vhfyst": 86, + "vwfyst1": 87, + "wfori": 88, + "wfost": 89, + "wfyjg": 90, + "wfyri": 91, + "wfyro": 92, + "wfyst": 93, + "wmori": 94, + "wmost": 95, + "wmyjg": 96, + "wmylg": 97, + "wmyri": 98, + "wmyro": 99, + "wmycr": 100, + "wmyst": 101, + "ballas1": 102, + "ballas2": 103, + "ballas3": 104, + "fam1": 105, + "fam2": 106, + "fam3": 107, + "lsv1": 108, + "lsv2": 109, + "lsv3": 110, + "maffa": 111, + "maffb": 112, + "mafboss": 113, + "vla1": 114, + "vla2": 115, + "vla3": 116, + "triada": 117, + "triadb": 118, + "lvpdm1": 119, + "triboss": 120, + "dnb1": 121, + "dnb2": 122, + "dnb3": 123, + "vmaff1": 124, + "vmaff2": 125, + "vmaff3": 126, + "vmaff4": 127, + "dnmylc": 128, + "dnfolc1": 129, + "dnfolc2": 130, + "dnfylc": 131, + "dnmolc1": 132, + "dnmolc2": 133, + "sbmotr2": 134, + "swmotr2": 135, + "sbmytr3": 136, + "swmotr3": 137, + "wfybe": 138, + "bfybe": 139, + "hfybe": 140, + "sofybu": 141, + "sbmyst": 142, + "sbmycr": 143, + "bmycg": 144, + "wfycrk": 145, + "hmycm": 146, + "wmybu": 147, + "bfybu": 148, + "wfybu": 150, + "dwfylc1": 151, + "wfypro": 152, + "wmyconb": 153, + "wmybe": 154, + "wmypizz": 155, + "bmobar": 156, + "cwfyhb": 157, + "cwmofr": 158, + "cwmohb1": 159, + "cwmohb2": 160, + "cwmyfr": 161, + "cwmyhb1": 162, + "bmyboun": 163, + "wmyboun": 164, + "wmomib": 165, + "bmymib": 166, + "wmybell": 167, + "bmochil": 168, + "sofyri": 169, + "somyst": 170, + "vwmybjd": 171, + "vwfycrp": 172, + "sfr1": 173, + "sfr2": 174, + "sfr3": 175, + "bmybar": 176, + "wmybar": 177, + "wfysex": 178, + "wmyammo": 179, + "bmytatt": 180, + "vwmycr": 181, + "vbmocd": 182, + "vbmycr": 183, + "vhmycr": 184, + "sbmyri": 185, + "somyri": 186, + "somybu": 187, + "swmyst": 188, + "wmyva": 189, + "copgrl3": 190, + "gungrl3": 191, + "mecgrl3": 192, + "nurgrl3": 193, + "crogrl3": 194, + "gangrl3": 195, + "cwfofr": 196, + "cwfohb": 197, + "cwfyfr1": 198, + "cwfyfr2": 199, + "cwmyhb2": 200, + "dwfylc2": 201, + "dwmylc2": 202, + "omykara": 203, + "wmykara": 204, + "wfyburg": 205, + "vwmycd": 206, + "vhfypro": 207, + "omonood": 209, + "omoboat": 210, + "wfyclot": 211, + "vwmotr1": 212, + "vwmotr2": 213, + "vwfywai": 214, + "sbfori": 215, + "swfyri": 216, + "wmyclot": 217, + "sbfost": 218, + "sbfyri": 219, + "sbmocd": 220, + "sbmori": 221, + "sbmost": 222, + "shmycr": 223, + "sofori": 224, + "sofost": 225, + "sofyst": 226, + "somobu": 227, + "somori": 228, + "somost": 229, + "swmotr5": 230, + "swfori": 231, + "swfost": 232, + "swfyst": 233, + "swmocd": 234, + "swmori": 235, + "swmost": 236, + "shfypro": 237, + "sbfypro": 238, + "swmotr4": 239, + "swmyri": 240, + "smyst": 241, + "smyst2": 242, + "sfypro": 243, + "vbfyst2": 244, + "vbfypro": 245, + "vhfyst3": 246, + "bikera": 247, + "bikerb": 248, + "bmypimp": 249, + "swmycr": 250, + "wfylg": 251, + "wmyva2": 252, + "bmosec": 253, + "bikdrug": 254, + "wmych": 255, + "sbfystr": 256, + "swfystr": 257, + "heck1": 258, + "heck2": 259, + "bmycon": 260, + "wmycd1": 261, + "bmocd": 262, + "vwfywa2": 263, + "wmoice": 264, + "tenpen": 265, + "pulaski": 266, + "hern": 267, + "dwayne": 268, + "smoke": 269, + "sweet": 270, + "ryder": 271, + "forelli": 272, + "mediatr": 273, + "laemt1": 274, + "lvemt1": 275, + "sfemt1": 276, + "lafd1": 277, + "lvfd1": 278, + "sffd1": 279, + "lapd1": 280, + "sfpd1": 281, + "lvpd1": 282, + "csher": 283, + "lapdm1": 284, + "swat": 285, + "fbi": 286, + "army": 287, + "dsher": 288, + "somyap": 289, + "rose": 290, + "paul": 291, + "cesar": 292, + "ogloc": 293, + "wuzimu": 294, + "torino": 295, + "jizzy": 296, + "maddogg": 297, + "cat": 298, + "claude": 299, + "ryder2": 300, + "ryder3": 301, + "emmet": 302, + "andre": 303, + "kendl": 304, + "jethro": 305, + "zero": 306, + "tbone": 307, + "sindaco": 308, + "janitor": 309, + "bbthin": 310, + "smokev": 311, + "psycho": 312 +} \ No newline at end of file diff --git a/MtaWikiMcp.Common/Data/vehicles.json b/MtaWikiMcp.Common/Data/vehicles.json new file mode 100644 index 0000000..fd2472a --- /dev/null +++ b/MtaWikiMcp.Common/Data/vehicles.json @@ -0,0 +1,214 @@ +{ + "Landstalker": 400, + "Bravura": 401, + "Buffalo": 402, + "Linerunner": 403, + "Perennial": 404, + "Sentinel": 405, + "Dumper": 406, + "Fire Truck": 407, + "Trashmaster": 408, + "Stretch": 409, + "Manana": 410, + "Infernus": 411, + "Voodoo": 412, + "Pony": 413, + "Mule": 414, + "Cheetah": 415, + "Ambulance": 416, + "Leviathan": 417, + "Moonbeam": 418, + "Esperanto": 419, + "Taxi": 420, + "Washington": 421, + "Bobcat": 422, + "Mr. Whoopee": 423, + "BF Injection": 424, + "Hunter": 425, + "Premier": 426, + "Enforcer": 427, + "Securicar": 428, + "Banshee": 429, + "Predator": 430, + "Bus": 431, + "Rhino": 432, + "Barracks": 433, + "Hotknife": 434, + "Trailer 1": 435, + "Previon": 436, + "Coach": 437, + "Cabbie": 438, + "Stallion": 439, + "Rumpo": 440, + "RC Bandit": 441, + "Romero": 442, + "Packer": 443, + "Monster": 444, + "Admiral": 445, + "Squalo": 446, + "Seasparrow": 447, + "Pizzaboy": 448, + "Tram": 449, + "Trailer 2": 450, + "Turismo": 451, + "Speeder": 452, + "Reefer": 453, + "Tropic": 454, + "Flatbed": 455, + "Yankee": 456, + "Caddy": 457, + "Solair": 458, + "Berkley's RC Van": 459, + "Skimmer": 460, + "PCJ-600": 461, + "Faggio": 462, + "Freeway": 463, + "RC Baron": 464, + "RC Raider": 465, + "Glendale": 466, + "Oceanic": 467, + "Sanchez": 468, + "Sparrow": 469, + "Patriot": 470, + "Quadbike": 471, + "Coastguard": 472, + "Dinghy": 473, + "Hermes": 474, + "Sabre": 475, + "Rustler": 476, + "ZR-350": 477, + "Walton": 478, + "Regina": 479, + "Comet": 480, + "BMX": 481, + "Burrito": 482, + "Camper": 483, + "Marquis": 484, + "Baggage": 485, + "Dozer": 486, + "Maverick": 487, + "News Chopper": 488, + "Rancher": 489, + "FBI Rancher": 490, + "Virgo": 491, + "Greenwood": 492, + "Jetmax": 493, + "Hotring Racer": 494, + "Sandking": 495, + "Blista Compact": 496, + "Police Maverick": 497, + "Boxville": 498, + "Benson": 499, + "Mesa": 500, + "RC Goblin": 501, + "Hotring Racer 2": 502, + "Hotring Racer 3": 503, + "Bloodring Banger": 504, + "Rancher Lure": 505, + "Super GT": 506, + "Elegant": 507, + "Journey": 508, + "Bike": 509, + "Mountain Bike": 510, + "Beagle": 511, + "Cropduster": 512, + "Stuntplane": 513, + "Tanker": 514, + "Roadtrain": 515, + "Nebula": 516, + "Majestic": 517, + "Buccaneer": 518, + "Shamal": 519, + "Hydra": 520, + "FCR-900": 521, + "NRG-500": 522, + "HPV1000": 523, + "Cement Truck": 524, + "Towtruck": 525, + "Fortune": 526, + "Cadrona": 527, + "FBI Truck": 528, + "Willard": 529, + "Forklift": 530, + "Tractor": 531, + "Combine Harvester": 532, + "Feltzer": 533, + "Remington": 534, + "Slamvan": 535, + "Blade": 536, + "Freight": 537, + "Brown Streak": 538, + "Vortex": 539, + "Vincent": 540, + "Bullet": 541, + "Clover": 542, + "Sadler": 543, + "Fire Truck Ladder": 544, + "Hustler": 545, + "Intruder": 546, + "Primo": 547, + "Cargobob": 548, + "Tampa": 549, + "Sunrise": 550, + "Merit": 551, + "Utility Van": 552, + "Nevada": 553, + "Yosemite": 554, + "Windsor": 555, + "Monster 2": 556, + "Monster 3": 557, + "Uranus": 558, + "Jester": 559, + "Sultan": 560, + "Stratum": 561, + "Elegy": 562, + "Raindance": 563, + "RC Tiger": 564, + "Flash": 565, + "Tahoma": 566, + "Savanna": 567, + "Bandito": 568, + "Freight Train Flatbed": 569, + "Streak Train Trailer": 570, + "Kart": 571, + "Mower": 572, + "Dune": 573, + "Sweeper": 574, + "Broadway": 575, + "Tornado": 576, + "AT-400": 577, + "DFT-30": 578, + "Huntley": 579, + "Stafford": 580, + "BF-400": 581, + "Newsvan": 582, + "Tug": 583, + "Trailer (Tanker Commando)": 584, + "Emperor": 585, + "Wayfarer": 586, + "Euros": 587, + "Hotdog": 588, + "Club": 589, + "Box Freight": 590, + "Trailer 3": 591, + "Andromada": 592, + "Dodo": 593, + "RC Cam": 594, + "Launch": 595, + "Police LS": 596, + "Police SF": 597, + "Police LV": 598, + "Police Ranger": 599, + "Picador": 600, + "S.W.A.T.": 601, + "Alpha": 602, + "Phoenix": 603, + "Glendale Damaged": 604, + "Sadler Damaged": 605, + "Baggage Trailer (covered)": 606, + "Baggage Trailer (Uncovered)": 607, + "Trailer (Stairs)": 608, + "Boxville Mission": 609, + "Farm Trailer": 610, + "Street Clean Trailer": 611 +} \ No newline at end of file diff --git a/MtaWikiMcp.Common/MtaWikiMcp.Common.csproj b/MtaWikiMcp.Common/MtaWikiMcp.Common.csproj new file mode 100644 index 0000000..1cf8a61 --- /dev/null +++ b/MtaWikiMcp.Common/MtaWikiMcp.Common.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + + + + Always + + + Always + + + + diff --git a/MtaWikiMcp.Common/MtaWikiMcpServiceCollectionExtensions.cs b/MtaWikiMcp.Common/MtaWikiMcpServiceCollectionExtensions.cs new file mode 100644 index 0000000..bab033a --- /dev/null +++ b/MtaWikiMcp.Common/MtaWikiMcpServiceCollectionExtensions.cs @@ -0,0 +1,39 @@ +using Microsoft.Extensions.DependencyInjection; +using MtaWikiMcp.Common.Clients; +using MtaWikiMcp.Common.Services; +using System.Threading.RateLimiting; + +namespace MtaWikiMcp.Common; + +public static class MtaWikiMcpServiceCollectionExtensions +{ + extension(IServiceCollection services) + { + /// + /// Adds the services required for the MTA Wiki MCP. + /// + /// + public IServiceCollection AddMtaWikiMcpServices() + { + services.AddSingleton(); + + services.AddHttpClient(x => x.BaseAddress = new Uri("https://wiki.multitheftauto.com/wiki/")) + .AddStandardResilienceHandler(options => + { + options.Retry.MaxRetryAttempts = 4; + options.Retry.Delay = TimeSpan.FromSeconds(1); + + options.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(60); + options.AttemptTimeout.Timeout = TimeSpan.FromSeconds(15); + + options.RateLimiter.DefaultRateLimiterOptions = new ConcurrencyLimiterOptions + { + PermitLimit = 10, + QueueLimit = 5 + }; + }); + + return services; + } + } +} \ No newline at end of file diff --git a/MtaWikiMcp.Common/Services/GameDataProvider.cs b/MtaWikiMcp.Common/Services/GameDataProvider.cs new file mode 100644 index 0000000..925f730 --- /dev/null +++ b/MtaWikiMcp.Common/Services/GameDataProvider.cs @@ -0,0 +1,42 @@ +using System.Text; +using System.Text.Json; + +namespace MtaWikiMcp.Common.Services; + +/// +/// Provides static game data loaded from the bundled JSON data files (peds, vehicles). +/// +public class GameDataProvider +{ + private readonly IReadOnlyDictionary _peds; + private readonly IReadOnlyDictionary _vehicles; + + public GameDataProvider() + { + _peds = LoadJson("Data/peds.json"); + _vehicles = LoadJson("Data/vehicles.json"); + } + + private static IReadOnlyDictionary LoadJson(string relativePath) + { + var path = Path.Combine(AppContext.BaseDirectory, relativePath); + var json = File.ReadAllText(path); + return JsonSerializer.Deserialize>(json)!; + } + + public string GetPedModelList() + { + var sb = new StringBuilder(); + foreach (var (name, id) in _peds.OrderBy(x => x.Value)) + sb.AppendLine($"{id}: {name}"); + return sb.ToString(); + } + + public string GetVehicleModelList() + { + var sb = new StringBuilder(); + foreach (var (name, id) in _vehicles.OrderBy(x => x.Value)) + sb.AppendLine($"{id}: {name}"); + return sb.ToString(); + } +} diff --git a/MtaWikiMcp.Common/Tools/MtaWikiTools.cs b/MtaWikiMcp.Common/Tools/MtaWikiTools.cs new file mode 100644 index 0000000..c5529ff --- /dev/null +++ b/MtaWikiMcp.Common/Tools/MtaWikiTools.cs @@ -0,0 +1,72 @@ +using ModelContextProtocol.Server; +using MtaWikiMcp.Common.Clients; +using MtaWikiMcp.Common.Services; +using System.ComponentModel; + +namespace MtaWikiMcp.Common.Tools; + +public enum ScriptSide { Server, Client, Shared } + +/// +/// This class contains the tools that are exposed to the AI agent for accessing information from the MTA wiki +/// +/// +[McpServerToolType] +[Description("Tools that allow access to MTA's Lua function information from the MTA wiki")] +public class MtaWikiTools(WikiClient wikiScraper, GameDataProvider gameData) +{ + private const string MtaLogoUrl = "https://wiki.multitheftauto.com/images/thumb/5/58/Mtalogo.png/100px-Mtalogo.png"; + + [McpServerTool(Name = nameof(GetFunctionList), IconSource = MtaLogoUrl)] + [Description("Returns a list of all available MTA scripting functions for the specified side (Server, Client, or Shared), grouped by category")] + public Task GetFunctionList(ScriptSide side) => side switch + { + ScriptSide.Server => wikiScraper.GetFunctionListAsync("Server_Scripting_Functions"), + ScriptSide.Client => wikiScraper.GetFunctionListAsync("Client_Scripting_Functions"), + ScriptSide.Shared => wikiScraper.GetFunctionListAsync("Shared_Scripting_Functions"), + _ => Task.FromResult("Invalid side.") + }; + + [McpServerTool(Name = nameof(GetFunctionInformation), IconSource = MtaLogoUrl)] + [Description("Returns the description, syntax, parameters and example for a specific MTA Wiki function")] + public Task GetFunctionInformation(string functionName) => + wikiScraper.GetFunctionInformationAsync(functionName); + + + [McpServerTool(Name = nameof(GetEventList), IconSource = MtaLogoUrl)] + [Description("Returns a list of all available MTA scripting events for the specified side (Server or Client), grouped by category")] + public Task GetEventList(ScriptSide side) => side switch + { + ScriptSide.Server => wikiScraper.GetFunctionListAsync("Server_Scripting_Events"), + ScriptSide.Client => wikiScraper.GetFunctionListAsync("Client_Scripting_Events"), + ScriptSide.Shared => Task.FromResult("There are no shared scripting events."), + _ => Task.FromResult("Invalid side.") + }; + + [McpServerTool(Name = nameof(GetEventParameters), IconSource = MtaLogoUrl)] + [Description("Returns the parameters and event source for a specific MTA Wiki event")] + public Task GetEventParameters(string eventName) => + wikiScraper.GetEventParametersAsync(eventName); + + + [McpServerTool(Name = nameof(GetPedModels), IconSource = MtaLogoUrl)] + [Description("Returns a list of all valid GTA:SA ped model IDs and their names")] + public Task GetPedModels() => + Task.FromResult(gameData.GetPedModelList()); + + [McpServerTool(Name = nameof(GetVehicleModels), IconSource = MtaLogoUrl)] + [Description("Returns a list of all valid GTA:SA vehicle model IDs and their names")] + public Task GetVehicleModels() => + Task.FromResult(gameData.GetVehicleModelList()); + + + [McpServerTool(Name = nameof(SearchWiki), IconSource = MtaLogoUrl)] + [Description("Searches the MTA Wiki for pages matching the given query. Returns a list of matching page names (usable with GetFunctionInformation and GetPageSource) and their short descriptions")] + public Task SearchWiki(string query) => + wikiScraper.SearchWikiAsync(query); + + [McpServerTool(Name = nameof(GetPageSource), IconSource = MtaLogoUrl)] + [Description("Returns the raw HTML source of a MTA Wiki page, useful as a fallback for detailed parsing when other tools do not return the expected result")] + public Task GetPageSource(string functionName) => + wikiScraper.GetRawPageAsync(functionName); +} diff --git a/MtaWikiMcp.Http/Dockerfile b/MtaWikiMcp.Http/Dockerfile new file mode 100644 index 0000000..2d183a2 --- /dev/null +++ b/MtaWikiMcp.Http/Dockerfile @@ -0,0 +1,31 @@ +# See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. + +# This stage is used when running from VS in fast mode (Default for Debug configuration) +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base +USER $APP_UID +WORKDIR /app +EXPOSE 8080 +EXPOSE 8081 + + +# This stage is used to build the service project +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src +COPY ["MtaWikiMcp.Http/MtaWikiMcp.Http.csproj", "MtaWikiMcp.Http/"] +COPY ["MtaWikiMcp.Common/MtaWikiMcp.Common.csproj", "MtaWikiMcp.Common/"] +RUN dotnet restore "./MtaWikiMcp.Http/MtaWikiMcp.Http.csproj" +COPY . . +WORKDIR "/src/MtaWikiMcp.Http" +RUN dotnet build "./MtaWikiMcp.Http.csproj" -c $BUILD_CONFIGURATION -o /app/build + +# This stage is used to publish the service project to be copied to the final stage +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./MtaWikiMcp.Http.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +# This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration) +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "MtaWikiMcp.Http.dll"] \ No newline at end of file diff --git a/MtaWikiMcp.Http/MtaWikiMcp.Http.csproj b/MtaWikiMcp.Http/MtaWikiMcp.Http.csproj new file mode 100644 index 0000000..34f02d6 --- /dev/null +++ b/MtaWikiMcp.Http/MtaWikiMcp.Http.csproj @@ -0,0 +1,20 @@ + + + + net10.0 + enable + enable + 53dc2f83-e42b-49b8-9ed3-82881f8676e1 + Linux + + + + + + + + + + + + diff --git a/MtaWikiMcp.Http/Program.cs b/MtaWikiMcp.Http/Program.cs new file mode 100644 index 0000000..67ca9ff --- /dev/null +++ b/MtaWikiMcp.Http/Program.cs @@ -0,0 +1,19 @@ +using MtaWikiMcp.Common; +using MtaWikiMcp.Common.Tools; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddMtaWikiMcpServices(); + +builder.Services + .AddMcpServer() + .WithToolsFromAssembly(typeof(MtaWikiTools).Assembly) + .WithHttpTransport(x => + { + x.Stateless = true; + }); + +var app = builder.Build(); + +app.MapMcp("/"); +app.Run(); diff --git a/MtaWikiMcp.Http/Properties/launchSettings.json b/MtaWikiMcp.Http/Properties/launchSettings.json new file mode 100644 index 0000000..04330f9 --- /dev/null +++ b/MtaWikiMcp.Http/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5234" + }, + "https": { + "commandName": "Project", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7187;http://localhost:5234" + }, + "Container (Dockerfile)": { + "commandName": "Docker", + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", + "environmentVariables": { + "ASPNETCORE_HTTPS_PORTS": "8081", + "ASPNETCORE_HTTP_PORTS": "8080" + }, + "publishAllPorts": true, + "useSSL": true + } + }, + "$schema": "https://json.schemastore.org/launchsettings.json" +} \ No newline at end of file diff --git a/MtaWikiMcp.Http/appsettings.Development.json b/MtaWikiMcp.Http/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/MtaWikiMcp.Http/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/MtaWikiMcp.Http/appsettings.json b/MtaWikiMcp.Http/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/MtaWikiMcp.Http/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/MtaWikiMcp.Stdio/MtaWikiMcp.Stdio.csproj b/MtaWikiMcp.Stdio/MtaWikiMcp.Stdio.csproj new file mode 100644 index 0000000..0a0d25d --- /dev/null +++ b/MtaWikiMcp.Stdio/MtaWikiMcp.Stdio.csproj @@ -0,0 +1,41 @@ + + + + Exe + net10.0 + enable + enable + + + + + PreserveNewest + true + PreserveNewest + + + PreserveNewest + true + PreserveNewest + + + + + + + + + + + + + + + Always + + + Always + + + + diff --git a/MtaWikiMcp.Stdio/Program.cs b/MtaWikiMcp.Stdio/Program.cs new file mode 100644 index 0000000..b8532ed --- /dev/null +++ b/MtaWikiMcp.Stdio/Program.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using MtaWikiMcp.Common; +using MtaWikiMcp.Common.Tools; + +var builder = Host.CreateApplicationBuilder(args); + +builder.Configuration + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile("appsettings.local.json", optional: true, reloadOnChange: true); + +builder.Logging.AddConsole(consoleLogOptions => +{ + consoleLogOptions.LogToStandardErrorThreshold = LogLevel.Trace; +}); + +builder.Services.AddMtaWikiMcpServices(); + +builder.Services + .AddMcpServer() + .WithToolsFromAssembly(typeof(MtaWikiTools).Assembly) + .WithStdioServerTransport(); + +var app = builder.Build(); + +await app.RunAsync(); diff --git a/MtaWikiMcp.Stdio/appsettings.json b/MtaWikiMcp.Stdio/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/MtaWikiMcp.Stdio/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/MtaWikiMcp.Stdio/appsettings.local.json b/MtaWikiMcp.Stdio/appsettings.local.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/MtaWikiMcp.Stdio/appsettings.local.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..3c9120e --- /dev/null +++ b/readme.md @@ -0,0 +1,44 @@ +# Project Overview + +This repositories contains the source for MCP server(s?) intended to augment AI tooling use for creating resources / scripts for MTA San Andreas. + +## MCPs and tools +These are the tools that the MCP server exposes: + +- `GetFunctionList(side)` + lists MTA scripting functions grouped by category (`Server`, `Client`, `Shared`). +- `GetFunctionInformation(functionName)` + returns description, syntax, parameters and example for a wiki function. +- `GetEventList(side)` + lists scripting events for `Server` or `Client` sides. +- `GetEventParameters(eventName)` + returns parameters and source information for a specific event. +- `GetPedModels()` + returns bundled GTA:SA ped model IDs and names. +- `GetVehicleModels()` + returns bundled GTA:SA vehicle model IDs and names. +- `SearchWiki(query)` + searches the MTA Wiki and returns matching pages usable with the other tools. +- `GetPageSource(functionName)` + returns raw HTML source for a wiki page. + +## Usage +TODO: Once hosted somewhere, update URL +The easiest way to use the MCP is by adding the MTA-hosted version of the MCP to your mcp.json (or your IDE's equivalent) +```json +"MTA Wiki MCP": { + "url": "http://localhost:32768", + "type": "http" +} +``` + +## Development +The MCP server is a C# dotnet 10 application, developing requires dotnet 10 installed, and can be done in Visual Studio, vscore, or any IDE of your choosing. + +The codebase has several projects: +- MtaWikiMcp.Common + The common library which contains the actual tools that are exposed via the MCP server. +- MtaWikiMcp.Http + A version of the MCP server that runs via HTTP. +- MtaWikiMcp.Stdio + A version of the MCP server that can be executed locally via stdio if you don't want to use HTTP. Do note this will still require access to the wiki. From eb4e89519004c6761d59cbb4de5229519bafa806 Mon Sep 17 00:00:00 2001 From: NanoBob Date: Fri, 3 Apr 2026 20:19:55 +0200 Subject: [PATCH 2/5] Added hosted URL to readme --- readme.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 3c9120e..80ced1e 100644 --- a/readme.md +++ b/readme.md @@ -23,11 +23,10 @@ These are the tools that the MCP server exposes: returns raw HTML source for a wiki page. ## Usage -TODO: Once hosted somewhere, update URL The easiest way to use the MCP is by adding the MTA-hosted version of the MCP to your mcp.json (or your IDE's equivalent) ```json "MTA Wiki MCP": { - "url": "http://localhost:32768", + "url": "https://mcp.multitheftauto.com/wiki", "type": "http" } ``` From c53db034ba532305b00a16eb3b1ad28099bff9b2 Mon Sep 17 00:00:00 2001 From: NanoBob Date: Mon, 6 Apr 2026 02:20:50 +0200 Subject: [PATCH 3/5] Added debug companion MCP server for interaction with the MTA server itself. Including the companion resource on the MTA server side. --- .../Clients/MtaServerDebugClient.cs | 41 ++++++++++++ .../MtaDebugCompanionMcp.Common.csproj | 22 +++++++ ...CompanionMcpServiceCollectionExtensions.cs | 25 ++++++++ .../Tools/MtaDebugCompanionTools.cs | 62 +++++++++++++++++++ .../MtaDebugCompanionMcp.Http.csproj | 18 ++++++ MtaDebugCompanionMcp.Http/Program.cs | 21 +++++++ .../Properties/launchSettings.json | 23 +++++++ .../appsettings.Development.json | 8 +++ MtaDebugCompanionMcp.Http/appsettings.json | 12 ++++ MtaMcpServer.slnx | 12 +++- Resources/debugCompanion/debug.http | 24 +++++++ Resources/debugCompanion/exports/debugLog.lua | 35 +++++++++++ .../debugCompanion/exports/resources.lua | 23 +++++++ Resources/debugCompanion/exports/runcode.lua | 42 +++++++++++++ .../debugCompanion/helpers/apiHelpers.lua | 48 ++++++++++++++ Resources/debugCompanion/meta.xml | 27 ++++++++ 16 files changed, 440 insertions(+), 3 deletions(-) create mode 100644 MtaDebugCompanionMcp.Common/Clients/MtaServerDebugClient.cs create mode 100644 MtaDebugCompanionMcp.Common/MtaDebugCompanionMcp.Common.csproj create mode 100644 MtaDebugCompanionMcp.Common/MtaDebugCompanionMcpServiceCollectionExtensions.cs create mode 100644 MtaDebugCompanionMcp.Common/Tools/MtaDebugCompanionTools.cs create mode 100644 MtaDebugCompanionMcp.Http/MtaDebugCompanionMcp.Http.csproj create mode 100644 MtaDebugCompanionMcp.Http/Program.cs create mode 100644 MtaDebugCompanionMcp.Http/Properties/launchSettings.json create mode 100644 MtaDebugCompanionMcp.Http/appsettings.Development.json create mode 100644 MtaDebugCompanionMcp.Http/appsettings.json create mode 100644 Resources/debugCompanion/debug.http create mode 100644 Resources/debugCompanion/exports/debugLog.lua create mode 100644 Resources/debugCompanion/exports/resources.lua create mode 100644 Resources/debugCompanion/exports/runcode.lua create mode 100644 Resources/debugCompanion/helpers/apiHelpers.lua create mode 100644 Resources/debugCompanion/meta.xml diff --git a/MtaDebugCompanionMcp.Common/Clients/MtaServerDebugClient.cs b/MtaDebugCompanionMcp.Common/Clients/MtaServerDebugClient.cs new file mode 100644 index 0000000..b4ce49a --- /dev/null +++ b/MtaDebugCompanionMcp.Common/Clients/MtaServerDebugClient.cs @@ -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 value) => + new(JsonSerializer.Serialize(value), Encoding.UTF8, "application/json"); + + public async Task RunCode(string code) + { + var response = await httpClient.PostAsync("/debugCompanion/call/httpRun", ToJsonContent(new[] { code })); + return await response.Content.ReadAsStringAsync(); + } + + public async Task RestartResource(string name) + { + var response = await httpClient.PostAsync("/debugCompanion/call/httpRestartResource", ToJsonContent(new[] { name })); + return await response.Content.ReadAsStringAsync(); + } + + public async Task StartResource(string name) + { + var response = await httpClient.PostAsync("/debugCompanion/call/httpStartResource", ToJsonContent(new[] { name })); + return await response.Content.ReadAsStringAsync(); + } + + public async Task StopResource(string name) + { + var response = await httpClient.PostAsync("/debugCompanion/call/httpStopResource", ToJsonContent(new[] { name })); + return await response.Content.ReadAsStringAsync(); + } + + public async Task GetLogs() + { + var response = await httpClient.PostAsync("/debugCompanion/call/httpGetDebugLog", ToJsonContent(Array.Empty())); + return await response.Content.ReadAsStringAsync(); + } +} diff --git a/MtaDebugCompanionMcp.Common/MtaDebugCompanionMcp.Common.csproj b/MtaDebugCompanionMcp.Common/MtaDebugCompanionMcp.Common.csproj new file mode 100644 index 0000000..e7aaae6 --- /dev/null +++ b/MtaDebugCompanionMcp.Common/MtaDebugCompanionMcp.Common.csproj @@ -0,0 +1,22 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + + + + + diff --git a/MtaDebugCompanionMcp.Common/MtaDebugCompanionMcpServiceCollectionExtensions.cs b/MtaDebugCompanionMcp.Common/MtaDebugCompanionMcpServiceCollectionExtensions.cs new file mode 100644 index 0000000..142132d --- /dev/null +++ b/MtaDebugCompanionMcp.Common/MtaDebugCompanionMcpServiceCollectionExtensions.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.DependencyInjection; +using MtaDebugCompanionMcp.Common.Clients; + +namespace MtaDebugCompanionMcp.Common; + +public static class MtaDebugCompanionMcpServiceCollectionExtensions +{ + extension(IServiceCollection services) + { + /// + /// Adds the services required for the MTA debug companion MCP. + /// + /// + public IServiceCollection AddMtaDebugCompanionMcpServices(string? baseAddress, string? apiKey) + { + services.AddHttpClient(x => + { + x.BaseAddress = new Uri(baseAddress ?? "http://localhost:22005"); + x.DefaultRequestHeaders.Add("api-key", apiKey ?? "default"); + }); + + return services; + } + } +} \ No newline at end of file diff --git a/MtaDebugCompanionMcp.Common/Tools/MtaDebugCompanionTools.cs b/MtaDebugCompanionMcp.Common/Tools/MtaDebugCompanionTools.cs new file mode 100644 index 0000000..83bb9fb --- /dev/null +++ b/MtaDebugCompanionMcp.Common/Tools/MtaDebugCompanionTools.cs @@ -0,0 +1,62 @@ +using ModelContextProtocol.Server; +using MtaDebugCompanionMcp.Common.Clients; +using System.ComponentModel; + +namespace MtaDebugCompanionMcp.Common.Tools; + +public enum ScriptSide { Server, Client, Shared } + +/// +/// This class contains the tools that are exposed to the AI agent for accessing information from the MTA wiki +/// +/// +[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 RunCode(string code) + { + return mtaServer.RunCode(code); + } + + [McpServerTool(Name = nameof(RestartResource), IconSource = MtaLogoUrl)] + [Description("Restarts the specified resource on the MTA server.")] + public Task RestartResource(string name) + { + return mtaServer.RestartResource(name); + } + + [McpServerTool(Name = nameof(StartResource), IconSource = MtaLogoUrl)] + [Description("Starts the specified resource on the MTA server.")] + public Task StartResource(string name) + { + return mtaServer.StartResource(name); + } + + [McpServerTool(Name = nameof(StopResource), IconSource = MtaLogoUrl)] + [Description("Stops the specified resource on the MTA server.")] + public Task StopResource(string name) + { + return mtaServer.StopResource(name); + } + + [McpServerTool(Name = nameof(GetLogs), IconSource = MtaLogoUrl)] + [Description("Retrieves the latest 100 lines of debug logs.")] + public Task GetLogs() + { + return mtaServer.GetLogs(); + } + + +} diff --git a/MtaDebugCompanionMcp.Http/MtaDebugCompanionMcp.Http.csproj b/MtaDebugCompanionMcp.Http/MtaDebugCompanionMcp.Http.csproj new file mode 100644 index 0000000..b94702e --- /dev/null +++ b/MtaDebugCompanionMcp.Http/MtaDebugCompanionMcp.Http.csproj @@ -0,0 +1,18 @@ + + + + net10.0 + enable + enable + + + + + + + + + + + + diff --git a/MtaDebugCompanionMcp.Http/Program.cs b/MtaDebugCompanionMcp.Http/Program.cs new file mode 100644 index 0000000..641ea56 --- /dev/null +++ b/MtaDebugCompanionMcp.Http/Program.cs @@ -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("serverHost"), builder.Configuration.GetValue("apiKey")); + +builder.Services + .AddMcpServer() + .WithToolsFromAssembly(typeof(MtaDebugCompanionTools).Assembly) + .WithHttpTransport(x => + { + x.Stateless = true; + }); + +var app = builder.Build(); + +app.MapMcp("/"); +app.Run(); diff --git a/MtaDebugCompanionMcp.Http/Properties/launchSettings.json b/MtaDebugCompanionMcp.Http/Properties/launchSettings.json new file mode 100644 index 0000000..3ac88d0 --- /dev/null +++ b/MtaDebugCompanionMcp.Http/Properties/launchSettings.json @@ -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" + } + } + } +} diff --git a/MtaDebugCompanionMcp.Http/appsettings.Development.json b/MtaDebugCompanionMcp.Http/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/MtaDebugCompanionMcp.Http/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/MtaDebugCompanionMcp.Http/appsettings.json b/MtaDebugCompanionMcp.Http/appsettings.json new file mode 100644 index 0000000..47ae76c --- /dev/null +++ b/MtaDebugCompanionMcp.Http/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + + "serverHost": "http://localhost:22005", + "apiKey": "default" +} diff --git a/MtaMcpServer.slnx b/MtaMcpServer.slnx index 5be5eb7..0f28d2e 100644 --- a/MtaMcpServer.slnx +++ b/MtaMcpServer.slnx @@ -1,5 +1,11 @@ - - - + + + + + + + + + diff --git a/Resources/debugCompanion/debug.http b/Resources/debugCompanion/debug.http new file mode 100644 index 0000000..53f8fee --- /dev/null +++ b/Resources/debugCompanion/debug.http @@ -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" +] + diff --git a/Resources/debugCompanion/exports/debugLog.lua b/Resources/debugCompanion/exports/debugLog.lua new file mode 100644 index 0000000..1e29f59 --- /dev/null +++ b/Resources/debugCompanion/exports/debugLog.lua @@ -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 + diff --git a/Resources/debugCompanion/exports/resources.lua b/Resources/debugCompanion/exports/resources.lua new file mode 100644 index 0000000..2769109 --- /dev/null +++ b/Resources/debugCompanion/exports/resources.lua @@ -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 diff --git a/Resources/debugCompanion/exports/runcode.lua b/Resources/debugCompanion/exports/runcode.lua new file mode 100644 index 0000000..8d0082c --- /dev/null +++ b/Resources/debugCompanion/exports/runcode.lua @@ -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 \ No newline at end of file diff --git a/Resources/debugCompanion/helpers/apiHelpers.lua b/Resources/debugCompanion/helpers/apiHelpers.lua new file mode 100644 index 0000000..c5d9a13 --- /dev/null +++ b/Resources/debugCompanion/helpers/apiHelpers.lua @@ -0,0 +1,48 @@ +local apiKey + +function verifyApiKey() + if not apiKey or not requestHeaders then + return false + end + + local header = requestHeaders["api-key"] + + return header == apiKey +end + +function loadApiKey() + local setting = get("@apiKey") + + if setting then + apiKey = setting + + if apiKey == "default" then + outputServerLog("Default API key detected, you can change it with /generatekey") + addCommandHandler("generatekey", generateApiKey, false, false) + end + end +end + +function generateApiKey() + local setting = get("@apiKey") + + if setting == "default" then + local key = generateRandomString(32) + set("@apiKey", key) + apiKey = key + + outputServerLog("A new key was generated: " .. key) + end +end + +function generateRandomString(length) + local charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + local randomString = "" + for i = 1, length do + local randomIndex = math.random(1, #charset) + randomString = randomString .. charset:sub(randomIndex, randomIndex) + end + return randomString +end + +loadApiKey() diff --git a/Resources/debugCompanion/meta.xml b/Resources/debugCompanion/meta.xml new file mode 100644 index 0000000..20a6ace --- /dev/null +++ b/Resources/debugCompanion/meta.xml @@ -0,0 +1,27 @@ + +