mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
Implemented a first version of an MCP server as a Chat Bot
This commit is contained in:
parent
5270aed315
commit
79b091cab6
15 changed files with 2350 additions and 2 deletions
34
MinecraftClient/Mcp/IMccMcpCapabilities.cs
Normal file
34
MinecraftClient/Mcp/IMccMcpCapabilities.cs
Normal 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);
|
||||
}
|
||||
133
MinecraftClient/Mcp/MccEmbeddedMcpHost.cs
Normal file
133
MinecraftClient/Mcp/MccEmbeddedMcpHost.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
1528
MinecraftClient/Mcp/MccMcpCapabilities.cs
Normal file
1528
MinecraftClient/Mcp/MccMcpCapabilities.cs
Normal file
File diff suppressed because it is too large
Load diff
49
MinecraftClient/Mcp/MccMcpChatHistory.cs
Normal file
49
MinecraftClient/Mcp/MccMcpChatHistory.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
46
MinecraftClient/Mcp/MccMcpConfig.cs
Normal file
46
MinecraftClient/Mcp/MccMcpConfig.cs
Normal 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;
|
||||
}
|
||||
33
MinecraftClient/Mcp/MccMcpResult.cs
Normal file
33
MinecraftClient/Mcp/MccMcpResult.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
193
MinecraftClient/Mcp/MccMcpToolSet.cs
Normal file
193
MinecraftClient/Mcp/MccMcpToolSet.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue