Implemented a first version of an MCP server as a Chat Bot

This commit is contained in:
Anon 2026-03-28 02:08:47 +01:00
parent 5270aed315
commit 79b091cab6
15 changed files with 2350 additions and 2 deletions

View file

@ -7,27 +7,81 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MinecraftClient", "Minecraf
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleInteractive", "ConsoleInteractive\ConsoleInteractive\ConsoleInteractive\ConsoleInteractive.csproj", "{93DA4D71-EFAD-4493-BE21-A105AF663660}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "DebugTools", "DebugTools", "{02313C6C-37F1-D66D-F235-6A4537C03113}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MccMcpStdioHarness", "DebugTools\MccMcpStdioHarness\MccMcpStdioHarness.csproj", "{F032D2BB-A0A9-4726-A58F-C02F7EA606D6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MccMcpWebPlayground", "DebugTools\MccMcpWebPlayground\MccMcpWebPlayground.csproj", "{5F620CF6-BC7D-449A-B779-2D51985059C6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{1E2FACE4-F5CA-4323-9641-740C6A551770}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1E2FACE4-F5CA-4323-9641-740C6A551770}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1E2FACE4-F5CA-4323-9641-740C6A551770}.Debug|x64.ActiveCfg = Debug|Any CPU
{1E2FACE4-F5CA-4323-9641-740C6A551770}.Debug|x64.Build.0 = Debug|Any CPU
{1E2FACE4-F5CA-4323-9641-740C6A551770}.Debug|x86.ActiveCfg = Debug|Any CPU
{1E2FACE4-F5CA-4323-9641-740C6A551770}.Debug|x86.Build.0 = Debug|Any CPU
{1E2FACE4-F5CA-4323-9641-740C6A551770}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1E2FACE4-F5CA-4323-9641-740C6A551770}.Release|Any CPU.Build.0 = Release|Any CPU
{1E2FACE4-F5CA-4323-9641-740C6A551770}.Release|x64.ActiveCfg = Release|Any CPU
{1E2FACE4-F5CA-4323-9641-740C6A551770}.Release|x64.Build.0 = Release|Any CPU
{1E2FACE4-F5CA-4323-9641-740C6A551770}.Release|x86.ActiveCfg = Release|Any CPU
{1E2FACE4-F5CA-4323-9641-740C6A551770}.Release|x86.Build.0 = Release|Any CPU
{93DA4D71-EFAD-4493-BE21-A105AF663660}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{93DA4D71-EFAD-4493-BE21-A105AF663660}.Debug|Any CPU.Build.0 = Debug|Any CPU
{93DA4D71-EFAD-4493-BE21-A105AF663660}.Debug|x64.ActiveCfg = Debug|Any CPU
{93DA4D71-EFAD-4493-BE21-A105AF663660}.Debug|x64.Build.0 = Debug|Any CPU
{93DA4D71-EFAD-4493-BE21-A105AF663660}.Debug|x86.ActiveCfg = Debug|Any CPU
{93DA4D71-EFAD-4493-BE21-A105AF663660}.Debug|x86.Build.0 = Debug|Any CPU
{93DA4D71-EFAD-4493-BE21-A105AF663660}.Release|Any CPU.ActiveCfg = Release|Any CPU
{93DA4D71-EFAD-4493-BE21-A105AF663660}.Release|Any CPU.Build.0 = Release|Any CPU
{93DA4D71-EFAD-4493-BE21-A105AF663660}.Release|x64.ActiveCfg = Release|Any CPU
{93DA4D71-EFAD-4493-BE21-A105AF663660}.Release|x64.Build.0 = Release|Any CPU
{93DA4D71-EFAD-4493-BE21-A105AF663660}.Release|x86.ActiveCfg = Release|Any CPU
{93DA4D71-EFAD-4493-BE21-A105AF663660}.Release|x86.Build.0 = Release|Any CPU
{F032D2BB-A0A9-4726-A58F-C02F7EA606D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F032D2BB-A0A9-4726-A58F-C02F7EA606D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F032D2BB-A0A9-4726-A58F-C02F7EA606D6}.Debug|x64.ActiveCfg = Debug|Any CPU
{F032D2BB-A0A9-4726-A58F-C02F7EA606D6}.Debug|x64.Build.0 = Debug|Any CPU
{F032D2BB-A0A9-4726-A58F-C02F7EA606D6}.Debug|x86.ActiveCfg = Debug|Any CPU
{F032D2BB-A0A9-4726-A58F-C02F7EA606D6}.Debug|x86.Build.0 = Debug|Any CPU
{F032D2BB-A0A9-4726-A58F-C02F7EA606D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F032D2BB-A0A9-4726-A58F-C02F7EA606D6}.Release|Any CPU.Build.0 = Release|Any CPU
{F032D2BB-A0A9-4726-A58F-C02F7EA606D6}.Release|x64.ActiveCfg = Release|Any CPU
{F032D2BB-A0A9-4726-A58F-C02F7EA606D6}.Release|x64.Build.0 = Release|Any CPU
{F032D2BB-A0A9-4726-A58F-C02F7EA606D6}.Release|x86.ActiveCfg = Release|Any CPU
{F032D2BB-A0A9-4726-A58F-C02F7EA606D6}.Release|x86.Build.0 = Release|Any CPU
{5F620CF6-BC7D-449A-B779-2D51985059C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5F620CF6-BC7D-449A-B779-2D51985059C6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5F620CF6-BC7D-449A-B779-2D51985059C6}.Debug|x64.ActiveCfg = Debug|Any CPU
{5F620CF6-BC7D-449A-B779-2D51985059C6}.Debug|x64.Build.0 = Debug|Any CPU
{5F620CF6-BC7D-449A-B779-2D51985059C6}.Debug|x86.ActiveCfg = Debug|Any CPU
{5F620CF6-BC7D-449A-B779-2D51985059C6}.Debug|x86.Build.0 = Debug|Any CPU
{5F620CF6-BC7D-449A-B779-2D51985059C6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5F620CF6-BC7D-449A-B779-2D51985059C6}.Release|Any CPU.Build.0 = Release|Any CPU
{5F620CF6-BC7D-449A-B779-2D51985059C6}.Release|x64.ActiveCfg = Release|Any CPU
{5F620CF6-BC7D-449A-B779-2D51985059C6}.Release|x64.Build.0 = Release|Any CPU
{5F620CF6-BC7D-449A-B779-2D51985059C6}.Release|x86.ActiveCfg = Release|Any CPU
{5F620CF6-BC7D-449A-B779-2D51985059C6}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{F032D2BB-A0A9-4726-A58F-C02F7EA606D6} = {02313C6C-37F1-D66D-F235-6A4537C03113}
{5F620CF6-BC7D-449A-B779-2D51985059C6} = {02313C6C-37F1-D66D-F235-6A4537C03113}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
RESX_ShowErrorsInErrorList = False
SolutionGuid = {6DED60F4-9CF4-4DB3-8966-582B2EBE8487}
RESX_ShowErrorsInErrorList = False
RESX_SortFileContentOnSave = False
EndGlobalSection
EndGlobal

View file

@ -0,0 +1,147 @@
using System;
using MinecraftClient.Mcp;
using MinecraftClient.Scripting;
using Tomlet.Attributes;
namespace MinecraftClient.ChatBots
{
public class McpServer : ChatBot
{
public static Configs Config = new();
[TomlDoNotInlineObject]
public class Configs
{
[NonSerialized]
private const string BotName = "McpServer";
[TomlInlineComment("$ChatBot.McpServer.Enabled$")]
public bool Enabled = false;
[TomlPrecedingComment("$ChatBot.McpServer.Transport$")]
public MccMcpTransportConfig Transport = new();
[TomlPrecedingComment("$ChatBot.McpServer.Capabilities$")]
public MccMcpCapabilityToggles Capabilities = new();
public void OnSettingUpdate()
{
Transport ??= new MccMcpTransportConfig();
Capabilities ??= new MccMcpCapabilityToggles();
if (Transport.Port is < 1 or > 65535)
Transport.Port = 33333;
if (string.IsNullOrWhiteSpace(Transport.BindHost))
Transport.BindHost = "127.0.0.1";
if (string.IsNullOrWhiteSpace(Transport.Route))
Transport.Route = "/mcp";
if (!Transport.Route.StartsWith('/'))
Transport.Route = "/" + Transport.Route;
if (string.IsNullOrWhiteSpace(Transport.AuthTokenEnvVar))
Transport.AuthTokenEnvVar = "MCC_MCP_AUTH_TOKEN";
}
}
private MccEmbeddedMcpHost? host;
public override void Initialize()
{
Config.OnSettingUpdate();
}
public override void AfterGameJoined()
{
if (!Config.Enabled)
return;
MccMcpChatHistoryStore.Clear();
MccMcpConfig mcpConfig = new()
{
Enabled = Config.Enabled,
Transport = Config.Transport,
Capabilities = Config.Capabilities
};
host ??= new MccEmbeddedMcpHost(mcpConfig, new MccMcpCapabilities(() => Config.Capabilities));
if (host.IsRunning)
return;
LogToConsole(Translations.bot_mcpserver_starting);
if (!host.Start(out string? error))
{
if (error == "missing_auth_token")
LogToConsole(string.Format(Translations.bot_mcpserver_missing_auth_token, Config.Transport.AuthTokenEnvVar));
LogToConsole(string.Format(Translations.bot_mcpserver_start_failed, error ?? "unknown"));
return;
}
LogToConsole(string.Format(Translations.bot_mcpserver_started, host.Endpoint));
}
public override bool OnDisconnect(DisconnectReason reason, string message)
{
StopHost();
MccMcpChatHistoryStore.Clear();
return false;
}
public override void OnUnload()
{
StopHost();
MccMcpChatHistoryStore.Clear();
}
public override void GetText(string text, string? json)
{
string clean = GetVerbatim(text);
if (string.IsNullOrWhiteSpace(clean))
return;
string kind = "system";
string? sender = null;
string? message = null;
string parsedMessage = string.Empty;
string parsedSender = string.Empty;
if (IsPrivateMessage(clean, ref parsedMessage, ref parsedSender))
{
kind = "private";
sender = parsedSender;
message = parsedMessage;
}
else if (IsChatMessage(clean, ref parsedMessage, ref parsedSender))
{
kind = "chat";
sender = parsedSender;
message = parsedMessage;
}
MccMcpChatHistoryStore.Add(new MccMcpChatHistoryEntry
{
TimestampUtc = DateTimeOffset.UtcNow,
Kind = kind,
Text = clean,
Sender = sender,
Message = message,
Json = json
});
}
private void StopHost()
{
if (host is null || !host.IsRunning)
return;
if (host.Stop(out string? error))
LogToConsole(Translations.bot_mcpserver_stopped);
else
LogToConsole(string.Format(Translations.bot_mcpserver_stop_failed, error ?? "unknown"));
}
}
}

View file

@ -510,6 +510,7 @@ namespace MinecraftClient
if (Config.ChatBot.TelegramBridge.Enabled) { BotLoad(new TelegramBridge()); }
if (Config.ChatBot.ItemsCollector.Enabled) { BotLoad(new ItemsCollector()); }
if (Config.ChatBot.DiscordRpc.Enabled) { BotLoad(new DiscordRpc()); }
if (Config.ChatBot.McpServer.Enabled) { BotLoad(new McpServer()); }
if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("MCC_FILE_INPUT")))
BotLoad(new FileInputBot());
}

View file

@ -0,0 +1,34 @@
namespace MinecraftClient.Mcp;
public interface IMccMcpCapabilities
{
MccMcpResult GetSessionStatus();
MccMcpResult GetServerInfo();
MccMcpResult GetPlayerState();
MccMcpResult GetPlayersList();
MccMcpResult GetChatHistory(int maxCount, bool includeJson);
MccMcpResult GetInternalCommands();
MccMcpResult SendChat(string text);
MccMcpResult QuitClient();
MccMcpResult RunInternalCommand(string command);
MccMcpResult UseItemOnHand();
MccMcpResult ChangeHotbarSlot(int slot);
MccMcpResult UseItemOnBlock(double x, double y, double z);
MccMcpResult DigBlock(double x, double y, double z, double durationSeconds);
MccMcpResult PlaceBlock(int x, int y, int z, string face, string hand, bool lookAtBlock);
MccMcpResult InteractEntity(int entityId, string interaction, string hand);
MccMcpResult ScanNearbyBlocks(int radius, int maxCount, string? materialFilter);
MccMcpResult FindBlocks(string? query, int radius, int maxCount, bool exactMatch);
MccMcpResult IsPlayerNearby(string? playerName, double radius, bool includeSelf);
MccMcpResult LocatePlayer(string playerName, bool includeSelf);
MccMcpResult MoveTo(double x, double y, double z, bool allowUnsafe, bool allowDirectTeleport, int maxOffset, int minOffset, int timeoutMs);
MccMcpResult MoveToPlayer(string playerName, bool allowUnsafe, bool allowDirectTeleport, int maxOffset, int minOffset, int timeoutMs);
MccMcpResult LookAt(double x, double y, double z);
MccMcpResult GetInventorySnapshot(int inventoryId);
MccMcpResult InventoryWindowAction(int inventoryId, int slotId, string actionType);
MccMcpResult DropInventoryItem(string itemType, int count, int inventoryId, bool preferStack);
MccMcpResult QueryEntities(int maxCount);
MccMcpResult ListEntities(int maxCount, string? typeFilter, double radius);
MccMcpResult GetEntityInfo(int entityId, bool includeMetadata, bool includeEquipment, bool includeEffects);
MccMcpResult GetWorldBlockAt(int x, int y, int z);
}

View file

@ -0,0 +1,133 @@
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using ModelContextProtocol.Server;
namespace MinecraftClient.Mcp;
public sealed class MccEmbeddedMcpHost
{
private readonly MccMcpConfig config;
private readonly IMccMcpCapabilities capabilities;
private readonly object stateLock = new();
private WebApplication? app;
public MccEmbeddedMcpHost(MccMcpConfig config, IMccMcpCapabilities capabilities)
{
this.config = config;
this.capabilities = capabilities;
}
public bool IsRunning
{
get
{
lock (stateLock)
{
return app is not null;
}
}
}
public string Endpoint => $"http://{config.Transport.BindHost}:{config.Transport.Port}{NormalizeRoute(config.Transport.Route)}";
public bool Start(out string? error)
{
lock (stateLock)
{
error = null;
if (app is not null)
return true;
string route = NormalizeRoute(config.Transport.Route);
string bindHost = string.IsNullOrWhiteSpace(config.Transport.BindHost) ? "127.0.0.1" : config.Transport.BindHost.Trim();
if (config.Transport.Port is < 1 or > 65535)
{
error = "invalid_port";
return false;
}
string? requiredToken = null;
if (config.Transport.RequireAuthToken)
{
requiredToken = Environment.GetEnvironmentVariable(config.Transport.AuthTokenEnvVar);
if (string.IsNullOrWhiteSpace(requiredToken))
{
error = "missing_auth_token";
return false;
}
}
WebApplicationBuilder builder = WebApplication.CreateBuilder();
builder.Logging.ClearProviders();
builder.Logging.AddFilter(_ => false);
builder.Services.AddSingleton(capabilities);
builder.Services.AddSingleton(config);
builder.Services.AddMcpServer()
.WithHttpTransport()
.WithTools<MccMcpToolSet>();
builder.WebHost.UseUrls($"http://{bindHost}:{config.Transport.Port}");
WebApplication builtApp = builder.Build();
if (config.Transport.RequireAuthToken)
{
builtApp.Use(async (context, next) =>
{
if (context.Request.Path.StartsWithSegments(route, StringComparison.OrdinalIgnoreCase))
{
string auth = context.Request.Headers.Authorization.ToString();
if (!auth.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)
|| !string.Equals(auth[7..], requiredToken, StringComparison.Ordinal))
{
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
await context.Response.WriteAsync("Unauthorized");
return;
}
}
await next();
});
}
builtApp.MapMcp(route);
builtApp.StartAsync().GetAwaiter().GetResult();
app = builtApp;
return true;
}
}
public bool Stop(out string? error)
{
lock (stateLock)
{
error = null;
if (app is null)
return true;
try
{
app.StopAsync().GetAwaiter().GetResult();
app.DisposeAsync().AsTask().GetAwaiter().GetResult();
app = null;
return true;
}
catch
{
error = "stop_failed";
return false;
}
}
}
private static string NormalizeRoute(string route)
{
string normalized = string.IsNullOrWhiteSpace(route) ? "/mcp" : route.Trim();
if (!normalized.StartsWith('/'))
normalized = '/' + normalized;
return normalized;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace MinecraftClient.Mcp;
public sealed class MccMcpChatHistoryEntry
{
public required DateTimeOffset TimestampUtc { get; init; }
public required string Kind { get; init; }
public required string Text { get; init; }
public string? Sender { get; init; }
public string? Message { get; init; }
public string? Json { get; init; }
}
public static class MccMcpChatHistoryStore
{
private static readonly object historyLock = new();
private static readonly List<MccMcpChatHistoryEntry> history = new();
private const int MaxEntries = 500;
public static void Add(MccMcpChatHistoryEntry entry)
{
lock (historyLock)
{
history.Add(entry);
if (history.Count > MaxEntries)
history.RemoveRange(0, history.Count - MaxEntries);
}
}
public static MccMcpChatHistoryEntry[] GetLatest(int maxCount)
{
int count = Math.Clamp(maxCount, 1, MaxEntries);
lock (historyLock)
{
return history.TakeLast(count).ToArray();
}
}
public static void Clear()
{
lock (historyLock)
{
history.Clear();
}
}
}

View file

@ -0,0 +1,46 @@
using Tomlet.Attributes;
namespace MinecraftClient.Mcp;
public sealed class MccMcpConfig
{
public bool Enabled { get; set; }
public MccMcpTransportConfig Transport { get; set; } = new();
public MccMcpCapabilityToggles Capabilities { get; set; } = new();
}
public sealed class MccMcpTransportConfig
{
[TomlInlineComment("$ChatBot.McpServer.Transport.BindHost$")]
public string BindHost { get; set; } = "127.0.0.1";
[TomlInlineComment("$ChatBot.McpServer.Transport.Port$")]
public int Port { get; set; } = 33333;
[TomlInlineComment("$ChatBot.McpServer.Transport.Route$")]
public string Route { get; set; } = "/mcp";
[TomlInlineComment("$ChatBot.McpServer.Transport.RequireAuthToken$")]
public bool RequireAuthToken { get; set; }
[TomlInlineComment("$ChatBot.McpServer.Transport.AuthTokenEnvVar$")]
public string AuthTokenEnvVar { get; set; } = "MCC_MCP_AUTH_TOKEN";
}
public sealed class MccMcpCapabilityToggles
{
[TomlInlineComment("$ChatBot.McpServer.Capabilities.SessionStatus$")]
public bool SessionStatus { get; set; } = true;
[TomlInlineComment("$ChatBot.McpServer.Capabilities.ChatAndCommands$")]
public bool ChatAndCommands { get; set; } = true;
[TomlInlineComment("$ChatBot.McpServer.Capabilities.Movement$")]
public bool Movement { get; set; } = true;
[TomlInlineComment("$ChatBot.McpServer.Capabilities.Inventory$")]
public bool Inventory { get; set; } = true;
[TomlInlineComment("$ChatBot.McpServer.Capabilities.EntityWorld$")]
public bool EntityWorld { get; set; } = true;
}

View file

@ -0,0 +1,33 @@
using System;
namespace MinecraftClient.Mcp;
public sealed class MccMcpResult
{
public bool Success { get; init; }
public string? ErrorCode { get; init; }
public string? Message { get; init; }
public object? Data { get; init; }
public static MccMcpResult Ok(object? data = null, string? message = null)
{
return new MccMcpResult
{
Success = true,
Data = data,
Message = message
};
}
public static MccMcpResult Fail(string errorCode, string? message = null, object? data = null)
{
ArgumentException.ThrowIfNullOrEmpty(errorCode);
return new MccMcpResult
{
Success = false,
ErrorCode = errorCode,
Message = message,
Data = data
};
}
}

View file

@ -0,0 +1,193 @@
using System.ComponentModel;
using ModelContextProtocol.Server;
namespace MinecraftClient.Mcp;
[McpServerToolType]
public sealed class MccMcpToolSet
{
private readonly IMccMcpCapabilities capabilities;
public MccMcpToolSet(IMccMcpCapabilities capabilities)
{
this.capabilities = capabilities;
}
[McpServerTool(Name = "mcc_session_status"), Description("Get current MCC session and feature status.")]
public object SessionStatus()
{
return capabilities.GetSessionStatus();
}
[McpServerTool(Name = "mcc_server_info"), Description("Get active MCC server connection info and current TPS.")]
public object ServerInfo()
{
return capabilities.GetServerInfo();
}
[McpServerTool(Name = "mcc_player_state"), Description("Get current controlled player state.")]
public object PlayerState()
{
return capabilities.GetPlayerState();
}
[McpServerTool(Name = "mcc_players_list"), Description("List currently known online players.")]
public object PlayersList()
{
return capabilities.GetPlayersList();
}
[McpServerTool(Name = "mcc_chat_history"), Description("Get recent chat/system lines seen by MCC.")]
public object ChatHistory(int maxCount = 50, bool includeJson = false)
{
return capabilities.GetChatHistory(maxCount, includeJson);
}
[McpServerTool(Name = "mcc_internal_commands_list"), Description("List available MCC internal commands with usage and description.")]
public object InternalCommandsList()
{
return capabilities.GetInternalCommands();
}
[McpServerTool(Name = "mcc_send_chat"), Description("Send chat text or slash-command to the connected Minecraft server.")]
public object SendChat([Description("Text to send to server chat.")] string text)
{
return capabilities.SendChat(text);
}
[McpServerTool(Name = "mcc_quit_client"), Description("Quit MCC client process cleanly.")]
public object QuitClient()
{
return capabilities.QuitClient();
}
[McpServerTool(Name = "mcc_run_internal_command"), Description("Run an internal MCC command.")]
public object RunInternalCommand([Description("MCC command line without leading slash.")] string command)
{
return capabilities.RunInternalCommand(command);
}
[McpServerTool(Name = "mcc_change_hotbar_slot"), Description("Change active hotbar slot (1-9).")]
public object ChangeHotbarSlot(int slot)
{
return capabilities.ChangeHotbarSlot(slot);
}
[McpServerTool(Name = "mcc_use_item_on_hand"), Description("Use the currently held item.")]
public object UseItemOnHand()
{
return capabilities.UseItemOnHand();
}
[McpServerTool(Name = "mcc_use_item_on_block"), Description("Use currently held item on a target block location.")]
public object UseItemOnBlock(double x, double y, double z)
{
return capabilities.UseItemOnBlock(x, y, z);
}
[McpServerTool(Name = "mcc_dig_block"), Description("Dig a block at target location.")]
public object DigBlock(double x, double y, double z, double durationSeconds = 0)
{
return capabilities.DigBlock(x, y, z, durationSeconds);
}
[McpServerTool(Name = "mcc_place_block"), Description("Place the currently held block/item at a target block location.")]
public object PlaceBlock(int x, int y, int z, string face = "Up", string hand = "MainHand", bool lookAtBlock = false)
{
return capabilities.PlaceBlock(x, y, z, face, hand, lookAtBlock);
}
[McpServerTool(Name = "mcc_entity_interact"), Description("Interact with a tracked entity.")]
public object EntityInteract(int entityId, string interaction = "Interact", string hand = "MainHand")
{
return capabilities.InteractEntity(entityId, interaction, hand);
}
[McpServerTool(Name = "mcc_block_scan"), Description("Scan nearby blocks around player location.")]
public object BlockScan(int radius = 3, int maxCount = 200, string? materialFilter = null)
{
return capabilities.ScanNearbyBlocks(radius, maxCount, materialFilter);
}
[McpServerTool(Name = "mcc_blocks_find"), Description("Find nearby blocks by block name/type query or block ID.")]
public object BlocksFind(string? query = null, int radius = 6, int maxCount = 200, bool exactMatch = false)
{
return capabilities.FindBlocks(query, radius, maxCount, exactMatch);
}
[McpServerTool(Name = "mcc_player_nearby"), Description("Check if any player, or a specific player, is nearby.")]
public object PlayerNearby(string? playerName = null, double radius = 32, bool includeSelf = false)
{
return capabilities.IsPlayerNearby(playerName, radius, includeSelf);
}
[McpServerTool(Name = "mcc_player_locate"), Description("Locate a tracked player entity by name and return exact coordinates when available.")]
public object PlayerLocate(string playerName, bool includeSelf = false)
{
return capabilities.LocatePlayer(playerName, includeSelf);
}
[McpServerTool(Name = "mcc_move_to"), Description("Request movement/pathing to a world coordinate and verify arrival.")]
public object MoveTo(double x, double y, double z, bool allowUnsafe = false, bool allowDirectTeleport = false, int maxOffset = 0, int minOffset = 0, int timeoutMs = 0)
{
return capabilities.MoveTo(x, y, z, allowUnsafe, allowDirectTeleport, maxOffset, minOffset, timeoutMs);
}
[McpServerTool(Name = "mcc_move_to_player"), Description("Locate a tracked player entity, request movement/pathing, and verify arrival.")]
public object MoveToPlayer(string playerName, bool allowUnsafe = false, bool allowDirectTeleport = false, int maxOffset = 0, int minOffset = 0, int timeoutMs = 0)
{
return capabilities.MoveToPlayer(playerName, allowUnsafe, allowDirectTeleport, maxOffset, minOffset, timeoutMs);
}
[McpServerTool(Name = "mcc_look_at"), Description("Rotate player view toward world coordinates.")]
public object LookAt(double x, double y, double z)
{
return capabilities.LookAt(x, y, z);
}
[McpServerTool(Name = "mcc_inventory_snapshot"), Description("Get a snapshot of one inventory.")]
public object InventorySnapshot([Description("Inventory ID. 0 is the player inventory.")] int inventoryId = 0)
{
return capabilities.GetInventorySnapshot(inventoryId);
}
[McpServerTool(Name = "mcc_inventory_window_action"), Description("Perform a window action on an inventory slot.")]
public object InventoryWindowAction(int inventoryId, int slotId, [Description("WindowActionType enum name, e.g. LeftClick or ShiftClick.")] string actionType)
{
return capabilities.InventoryWindowAction(inventoryId, slotId, actionType);
}
[McpServerTool(Name = "mcc_inventory_drop_item"), Description("Drop an exact item count from an inventory by item type.")]
public object InventoryDropItem(
[Description("Item type enum name (e.g. Diamond).")] string itemType,
[Description("Exact number of items to drop.")] int count,
[Description("Inventory ID. 0 is the player inventory.")] int inventoryId = 0,
[Description("Prefer dropping from larger stacks first when true.")] bool preferStack = false)
{
return capabilities.DropInventoryItem(itemType, count, inventoryId, preferStack);
}
[McpServerTool(Name = "mcc_entities_query"), Description("Query tracked entities.")]
public object EntitiesQuery([Description("Maximum entities to return.")] int maxCount = 50)
{
return capabilities.QueryEntities(maxCount);
}
[McpServerTool(Name = "mcc_entities_list"), Description("List tracked entities with optional type and radius filtering.")]
public object EntitiesList(int maxCount = 100, string? typeFilter = null, double radius = 0)
{
return capabilities.ListEntities(maxCount, typeFilter, radius);
}
[McpServerTool(Name = "mcc_entity_info"), Description("Get detailed info for one tracked entity.")]
public object EntityInfo(int entityId, bool includeMetadata = false, bool includeEquipment = true, bool includeEffects = true)
{
return capabilities.GetEntityInfo(entityId, includeMetadata, includeEquipment, includeEffects);
}
[McpServerTool(Name = "mcc_world_block_at"), Description("Get block information at world coordinates.")]
public object WorldBlockAt(int x, int y, int z)
{
return capabilities.GetWorldBlockAt(x, y, z);
}
}

View file

@ -39,6 +39,8 @@
<PackageReference Include="FuzzySharp" Version="2.0.2" />
<PackageReference Include="Magick.NET-Q16-AnyCPU" Version="14.11.0" />
<PackageReference Include="MessagePack" Version="3.1.4" />
<PackageReference Include="ModelContextProtocol" Version="1.1.0" />
<PackageReference Include="ModelContextProtocol.AspNetCore" Version="1.1.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.3.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
<PackageReference Include="Microsoft.Windows.Compatibility" Version="10.0.5" />

View file

@ -936,4 +936,46 @@ Note: This does NOT require a Bot Token, only an Application ID. Discord must be
<data name="Main.General.AuthlibUser" xml:space="preserve">
<value>Yggdrasil authlib multi-user selection.</value>
</data>
<data name="ChatBot.McpServer" xml:space="preserve">
<value>Host an embedded MCP server while connected to Minecraft. Disabled by default.</value>
</data>
<data name="ChatBot.McpServer.Transport" xml:space="preserve">
<value>Embedded MCP HTTP transport settings.</value>
</data>
<data name="ChatBot.McpServer.Capabilities" xml:space="preserve">
<value>Enable or disable MCP tool categories.</value>
</data>
<data name="ChatBot.McpServer.Enabled" xml:space="preserve">
<value>Enable the built-in embedded MCP server bot. Server starts only after game join and stops on disconnect.</value>
</data>
<data name="ChatBot.McpServer.Transport.BindHost" xml:space="preserve">
<value>IP/host to bind the embedded MCP HTTP listener to. Default is loopback only.</value>
</data>
<data name="ChatBot.McpServer.Transport.Port" xml:space="preserve">
<value>TCP port for the embedded MCP HTTP listener.</value>
</data>
<data name="ChatBot.McpServer.Transport.Route" xml:space="preserve">
<value>Route prefix where MCP endpoints are exposed.</value>
</data>
<data name="ChatBot.McpServer.Transport.RequireAuthToken" xml:space="preserve">
<value>Require Bearer token authentication for MCP endpoint requests.</value>
</data>
<data name="ChatBot.McpServer.Transport.AuthTokenEnvVar" xml:space="preserve">
<value>Environment variable name containing the MCP auth token when auth is required.</value>
</data>
<data name="ChatBot.McpServer.Capabilities.SessionStatus" xml:space="preserve">
<value>Allow session and status inspection tools.</value>
</data>
<data name="ChatBot.McpServer.Capabilities.ChatAndCommands" xml:space="preserve">
<value>Allow chat and internal command tools.</value>
</data>
<data name="ChatBot.McpServer.Capabilities.Movement" xml:space="preserve">
<value>Allow movement and view-control tools.</value>
</data>
<data name="ChatBot.McpServer.Capabilities.Inventory" xml:space="preserve">
<value>Allow inventory read and action tools.</value>
</data>
<data name="ChatBot.McpServer.Capabilities.EntityWorld" xml:space="preserve">
<value>Allow entity and world inspection tools.</value>
</data>
</root>

View file

@ -6799,5 +6799,59 @@ namespace MinecraftClient {
return ResourceManager.GetString("tui.crafting.grid", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Starting embedded MCP server....
/// </summary>
internal static string bot_mcpserver_starting {
get {
return ResourceManager.GetString("bot.mcpserver.starting", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Embedded MCP server started on {0}.
/// </summary>
internal static string bot_mcpserver_started {
get {
return ResourceManager.GetString("bot.mcpserver.started", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Failed to start embedded MCP server: {0}.
/// </summary>
internal static string bot_mcpserver_start_failed {
get {
return ResourceManager.GetString("bot.mcpserver.start_failed", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Embedded MCP auth token is required but environment variable {0} is empty..
/// </summary>
internal static string bot_mcpserver_missing_auth_token {
get {
return ResourceManager.GetString("bot.mcpserver.missing_auth_token", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Embedded MCP server stopped..
/// </summary>
internal static string bot_mcpserver_stopped {
get {
return ResourceManager.GetString("bot.mcpserver.stopped", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Failed to stop embedded MCP server cleanly: {0}.
/// </summary>
internal static string bot_mcpserver_stop_failed {
get {
return ResourceManager.GetString("bot.mcpserver.stop_failed", resourceCulture);
}
}
}
}

View file

@ -2392,4 +2392,25 @@ see item details.</value>
<data name="tui.crafting.grid" xml:space="preserve">
<value>Crafting</value>
</data>
</root>
<data name="botname.McpServer" xml:space="preserve">
<value>McpServer</value>
</data>
<data name="bot.mcpserver.starting" xml:space="preserve">
<value>Starting embedded MCP server...</value>
</data>
<data name="bot.mcpserver.started" xml:space="preserve">
<value>Embedded MCP server started on {0}</value>
</data>
<data name="bot.mcpserver.start_failed" xml:space="preserve">
<value>Failed to start embedded MCP server: {0}</value>
</data>
<data name="bot.mcpserver.missing_auth_token" xml:space="preserve">
<value>Embedded MCP auth token is required but environment variable {0} is empty.</value>
</data>
<data name="bot.mcpserver.stopped" xml:space="preserve">
<value>Embedded MCP server stopped.</value>
</data>
<data name="bot.mcpserver.stop_failed" xml:space="preserve">
<value>Failed to stop embedded MCP server cleanly: {0}</value>
</data>
</root>

View file

@ -1755,6 +1755,17 @@ namespace MinecraftClient
get { return ChatBots.DiscordRpc.Config; }
set { ChatBots.DiscordRpc.Config = value; ChatBots.DiscordRpc.Config.OnSettingUpdate(); }
}
[TomlPrecedingComment("$ChatBot.McpServer$")]
public ChatBots.McpServer.Configs McpServer
{
get { return ChatBots.McpServer.Config; }
set
{
ChatBots.McpServer.Config = value ?? new ChatBots.McpServer.Configs();
ChatBots.McpServer.Config.OnSettingUpdate();
}
}
}
}