Added a Skill for MCP

This commit is contained in:
Anon 2026-03-29 20:57:16 +02:00
parent cf382122e9
commit 7b415d5388
7 changed files with 367 additions and 3 deletions

View file

@ -66,9 +66,11 @@ public sealed class MccEmbeddedMcpHost
builder.Logging.AddFilter(_ => false);
builder.Services.AddSingleton(capabilities);
builder.Services.AddSingleton(config);
builder.Services.AddSingleton<MccMcpGuidanceProvider>();
builder.Services.AddMcpServer()
.WithHttpTransport()
.WithTools<MccMcpToolSet>();
.WithTools<MccMcpToolSet>()
.WithPrompts<MccMcpPromptSet>();
builder.WebHost.UseUrls($"http://{bindHost}:{config.Transport.Port}");
WebApplication builtApp = builder.Build();

View file

@ -0,0 +1,236 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json.Serialization;
namespace MinecraftClient.Mcp;
public sealed class MccMcpGuidanceProvider
{
private const string EmbeddedSkillResourceSuffix = "MccMcpOperatorSkill.md";
private const string BestPracticesHeading = "## Best Practices";
private const string ExampleScenariosHeading = "## Example Scenarios";
private readonly MccMcpConfig config;
private readonly Lazy<GuidanceDocument> guidanceDocument;
public MccMcpGuidanceProvider(MccMcpConfig config)
{
this.config = config;
guidanceDocument = new Lazy<GuidanceDocument>(LoadGuidanceDocument);
}
public string SkillName => "mcc-mcp-operator";
public string GetSystemPrompt()
{
GuidanceDocument document = guidanceDocument.Value;
MccMcpAgentCapabilityStatus capabilityStatus = BuildCapabilityStatus();
StringBuilder builder = new();
builder.AppendLine("You are an external agent controlling Minecraft Console Client (MCC) through its built-in MCP server.");
builder.AppendLine("Use the following operator guide as your system prompt. Treat the capability snapshot as authoritative and do not invent unsupported actions.");
builder.AppendLine();
builder.AppendLine(document.BodyMarkdown);
builder.AppendLine();
builder.AppendLine("Current capability snapshot");
builder.AppendLine($"- sessionStatus: {FormatCapability(capabilityStatus.SessionStatus)}");
builder.AppendLine($"- chatAndCommands: {FormatCapability(capabilityStatus.ChatAndCommands)}");
builder.AppendLine($"- movement: {FormatCapability(capabilityStatus.Movement)}");
builder.AppendLine($"- inventory: {FormatCapability(capabilityStatus.Inventory)}");
builder.AppendLine($"- entityWorld: {FormatCapability(capabilityStatus.EntityWorld)}");
return builder.ToString().Trim();
}
public MccMcpAgentGuidancePayload GetToolPayload()
{
GuidanceDocument document = guidanceDocument.Value;
return new MccMcpAgentGuidancePayload
{
SkillName = SkillName,
SkillMarkdown = document.SkillMarkdown,
SystemPrompt = GetSystemPrompt(),
BestPractices = document.BestPractices,
ExampleScenarios = document.ExampleScenarios,
CapabilityStatus = BuildCapabilityStatus()
};
}
private GuidanceDocument LoadGuidanceDocument()
{
Assembly assembly = typeof(MccMcpGuidanceProvider).Assembly;
string resourceName = assembly.GetManifestResourceNames()
.FirstOrDefault(name => name.EndsWith(EmbeddedSkillResourceSuffix, StringComparison.Ordinal))
?? throw new InvalidOperationException($"Embedded MCP skill resource '{EmbeddedSkillResourceSuffix}' was not found.");
using Stream? stream = assembly.GetManifestResourceStream(resourceName);
if (stream is null)
throw new InvalidOperationException($"Embedded MCP skill resource '{resourceName}' could not be opened.");
using StreamReader reader = new(stream, Encoding.UTF8);
string skillMarkdown = reader.ReadToEnd();
string bodyMarkdown = StripFrontmatter(skillMarkdown);
string bestPracticesSection = ExtractSection(bodyMarkdown, BestPracticesHeading);
string exampleScenariosSection = ExtractSection(bodyMarkdown, ExampleScenariosHeading);
return new GuidanceDocument(
skillMarkdown.Replace("\r\n", "\n").Trim(),
bodyMarkdown,
ExtractBulletList(bestPracticesSection),
ExtractExampleScenarios(exampleScenariosSection));
}
private MccMcpAgentCapabilityStatus BuildCapabilityStatus()
{
return new MccMcpAgentCapabilityStatus
{
SessionStatus = config.Capabilities.SessionStatus,
ChatAndCommands = config.Capabilities.ChatAndCommands,
Movement = config.Capabilities.Movement,
Inventory = config.Capabilities.Inventory,
EntityWorld = config.Capabilities.EntityWorld
};
}
private static string StripFrontmatter(string markdown)
{
string normalized = markdown.Replace("\r\n", "\n");
if (!normalized.StartsWith("---\n", StringComparison.Ordinal))
return normalized.Trim();
int endOfFrontmatter = normalized.IndexOf("\n---\n", 4, StringComparison.Ordinal);
if (endOfFrontmatter < 0)
return normalized.Trim();
return normalized[(endOfFrontmatter + 5)..].Trim();
}
private static string ExtractSection(string markdownBody, string heading)
{
int headingIndex = markdownBody.IndexOf(heading, StringComparison.Ordinal);
if (headingIndex < 0)
return string.Empty;
int sectionStart = headingIndex + heading.Length;
int nextHeading = markdownBody.IndexOf("\n## ", sectionStart, StringComparison.Ordinal);
string section = nextHeading >= 0
? markdownBody[sectionStart..nextHeading]
: markdownBody[sectionStart..];
return section.Trim();
}
private static string[] ExtractBulletList(string section)
{
return section
.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Where(line => line.StartsWith("- ", StringComparison.Ordinal))
.Select(line => line[2..].Trim())
.Where(line => line.Length > 0)
.ToArray();
}
private static MccMcpAgentScenario[] ExtractExampleScenarios(string section)
{
if (string.IsNullOrWhiteSpace(section))
return [];
List<MccMcpAgentScenario> scenarios = [];
string? currentTitle = null;
List<string> currentBodyLines = [];
foreach (string rawLine in section.Split('\n'))
{
string line = rawLine.TrimEnd();
if (line.StartsWith("### ", StringComparison.Ordinal))
{
AddScenario(scenarios, currentTitle, currentBodyLines);
currentTitle = line[4..].Trim();
currentBodyLines = [];
continue;
}
if (currentTitle is not null)
currentBodyLines.Add(line);
}
AddScenario(scenarios, currentTitle, currentBodyLines);
return scenarios.ToArray();
}
private static void AddScenario(List<MccMcpAgentScenario> scenarios, string? title, List<string> bodyLines)
{
if (string.IsNullOrWhiteSpace(title))
return;
string guidance = string.Join('\n', bodyLines)
.Trim();
scenarios.Add(new MccMcpAgentScenario
{
Title = title,
Guidance = guidance
});
}
private static string FormatCapability(bool enabled)
{
return enabled ? "enabled" : "disabled";
}
private sealed record GuidanceDocument(
string SkillMarkdown,
string BodyMarkdown,
string[] BestPractices,
MccMcpAgentScenario[] ExampleScenarios);
}
public sealed class MccMcpAgentGuidancePayload
{
[JsonPropertyName("skillName")]
public string SkillName { get; init; } = string.Empty;
[JsonPropertyName("skillMarkdown")]
public string SkillMarkdown { get; init; } = string.Empty;
[JsonPropertyName("systemPrompt")]
public string SystemPrompt { get; init; } = string.Empty;
[JsonPropertyName("bestPractices")]
public string[] BestPractices { get; init; } = [];
[JsonPropertyName("exampleScenarios")]
public MccMcpAgentScenario[] ExampleScenarios { get; init; } = [];
[JsonPropertyName("capabilityStatus")]
public MccMcpAgentCapabilityStatus CapabilityStatus { get; init; } = new();
}
public sealed class MccMcpAgentScenario
{
[JsonPropertyName("title")]
public string Title { get; init; } = string.Empty;
[JsonPropertyName("guidance")]
public string Guidance { get; init; } = string.Empty;
}
public sealed class MccMcpAgentCapabilityStatus
{
[JsonPropertyName("sessionStatus")]
public bool SessionStatus { get; init; }
[JsonPropertyName("chatAndCommands")]
public bool ChatAndCommands { get; init; }
[JsonPropertyName("movement")]
public bool Movement { get; init; }
[JsonPropertyName("inventory")]
public bool Inventory { get; init; }
[JsonPropertyName("entityWorld")]
public bool EntityWorld { get; init; }
}

View file

@ -0,0 +1,20 @@
using System.ComponentModel;
using ModelContextProtocol.Server;
namespace MinecraftClient.Mcp;
public sealed class MccMcpPromptSet
{
private readonly MccMcpGuidanceProvider guidanceProvider;
public MccMcpPromptSet(MccMcpGuidanceProvider guidanceProvider)
{
this.guidanceProvider = guidanceProvider;
}
[McpServerPrompt(Name = "mcc_operator_guide"), Description("Get the canonical MCC operator guidance prompt for external agents using this MCP server.")]
public string OperatorGuide()
{
return guidanceProvider.GetSystemPrompt();
}
}

View file

@ -7,10 +7,12 @@ namespace MinecraftClient.Mcp;
public sealed class MccMcpToolSet
{
private readonly IMccMcpCapabilities capabilities;
private readonly MccMcpGuidanceProvider guidanceProvider;
public MccMcpToolSet(IMccMcpCapabilities capabilities)
public MccMcpToolSet(IMccMcpCapabilities capabilities, MccMcpGuidanceProvider guidanceProvider)
{
this.capabilities = capabilities;
this.guidanceProvider = guidanceProvider;
}
[McpServerTool(Name = "mcc_session_status"), Description("Get current MCC session and feature status.")]
@ -49,6 +51,12 @@ public sealed class MccMcpToolSet
return capabilities.GetInternalCommands();
}
[McpServerTool(Name = "mcc_agent_guidance"), Description("Get the canonical MCC operator guidance bundle for external agents using this MCP server.")]
public object AgentGuidance()
{
return guidanceProvider.GetToolPayload();
}
[McpServerTool(Name = "mcc_materials_list"), Description("List known MCC material names with optional filtering.")]
public object MaterialsList(string? filter = null, int maxCount = 500)
{

View file

@ -20,6 +20,7 @@
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Physics\BlockShapeData.json" LogicalName="BlockShapeData.json" />
<EmbeddedResource Include="..\.skills\mcc-mcp-operator\SKILL.md" Link="Mcp\EmbeddedSkills\SKILL.md" LogicalName="MccMcpOperatorSkill.md" />
</ItemGroup>
<ItemGroup>
<Compile Remove="Protocol\Handlers\Compression\**" />