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

@ -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);
}
}