mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
Improved the Web Based Harness
This commit is contained in:
parent
c3c57c058a
commit
ee6eb84bd8
19 changed files with 2799 additions and 2202 deletions
|
|
@ -0,0 +1,200 @@
|
|||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Reflection;
|
||||
using DebugTools.MccMcpWebPlayground.Harness;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ModelContextProtocol.Client;
|
||||
using ModelContextProtocol.Protocol;
|
||||
|
||||
namespace DebugTools.MccMcpWebPlayground.Infrastructure.Mcp;
|
||||
|
||||
public sealed class MccMcpSessionFactory
|
||||
{
|
||||
private readonly MccWebHarnessOptions options;
|
||||
|
||||
public MccMcpSessionFactory(IOptions<MccWebHarnessOptions> options)
|
||||
{
|
||||
this.options = options.Value;
|
||||
}
|
||||
|
||||
public async Task<McpClient> CreateAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
string endpoint = options.ResolveMcpEndpoint();
|
||||
string? token = options.ResolveMcpAuthToken();
|
||||
|
||||
return await McpClient.CreateAsync(new HttpClientTransport(new HttpClientTransportOptions
|
||||
{
|
||||
Endpoint = new Uri(endpoint),
|
||||
TransportMode = HttpTransportMode.AutoDetect,
|
||||
AdditionalHeaders = string.IsNullOrWhiteSpace(token)
|
||||
? null
|
||||
: new Dictionary<string, string>
|
||||
{
|
||||
["Authorization"] = $"Bearer {token}"
|
||||
}
|
||||
}), cancellationToken: cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public static class MccMcpJson
|
||||
{
|
||||
public static MccNormalizedToolResult Normalize(CallToolResult result)
|
||||
{
|
||||
JsonElement? structuredRoot = TryReadStructuredContent(result);
|
||||
string text = ReadToolResultText(result, structuredRoot);
|
||||
try
|
||||
{
|
||||
using JsonDocument document = JsonDocument.Parse(text);
|
||||
JsonElement parsedRoot = document.RootElement.Clone();
|
||||
JsonElement root = ShouldPreferStructuredRoot(parsedRoot, structuredRoot)
|
||||
? structuredRoot!.Value
|
||||
: parsedRoot;
|
||||
JsonElement? data = root.TryGetProperty("data", out JsonElement dataElement)
|
||||
? dataElement.Clone()
|
||||
: ShouldTreatRootAsData(root) ? root.Clone() : structuredRoot;
|
||||
bool success = root.TryGetProperty("success", out JsonElement successElement)
|
||||
? successElement.ValueKind != JsonValueKind.False
|
||||
: result.IsError != true;
|
||||
string? errorCode = root.TryGetProperty("errorCode", out JsonElement errorCodeElement) && errorCodeElement.ValueKind == JsonValueKind.String
|
||||
? errorCodeElement.GetString()
|
||||
: null;
|
||||
string? message = root.TryGetProperty("message", out JsonElement messageElement) && messageElement.ValueKind == JsonValueKind.String
|
||||
? messageElement.GetString()
|
||||
: null;
|
||||
bool isError = result.IsError == true || !success || !string.IsNullOrWhiteSpace(errorCode);
|
||||
|
||||
return new MccNormalizedToolResult(text, isError, success, errorCode, message, root, data);
|
||||
}
|
||||
catch
|
||||
{
|
||||
bool isError = result.IsError == true;
|
||||
return new MccNormalizedToolResult(text, isError, !isError, null, null, structuredRoot, structuredRoot);
|
||||
}
|
||||
}
|
||||
|
||||
private static string ReadToolResultText(CallToolResult result, JsonElement? structuredRoot)
|
||||
{
|
||||
if (result.Content is null)
|
||||
return structuredRoot?.GetRawText() ?? (result.IsError == true ? "{\"success\":false}" : "{\"success\":true}");
|
||||
|
||||
StringBuilder builder = new();
|
||||
foreach (ContentBlock block in result.Content)
|
||||
{
|
||||
if (block is TextContentBlock text && !string.IsNullOrWhiteSpace(text.Text))
|
||||
{
|
||||
if (builder.Length > 0)
|
||||
builder.Append('\n');
|
||||
builder.Append(text.Text);
|
||||
}
|
||||
}
|
||||
|
||||
return builder.Length > 0
|
||||
? builder.ToString()
|
||||
: structuredRoot?.GetRawText()
|
||||
?? JsonSerializer.Serialize(new { success = result.IsError != true, isError = result.IsError });
|
||||
}
|
||||
|
||||
private static JsonElement? TryReadStructuredContent(CallToolResult result)
|
||||
{
|
||||
PropertyInfo? property = typeof(CallToolResult).GetProperty("StructuredContent", BindingFlags.Instance | BindingFlags.Public);
|
||||
if (property?.GetValue(result) is not { } value)
|
||||
return null;
|
||||
|
||||
return value switch
|
||||
{
|
||||
JsonElement json when json.ValueKind != JsonValueKind.Undefined && json.ValueKind != JsonValueKind.Null => json.Clone(),
|
||||
JsonDocument document => document.RootElement.Clone(),
|
||||
string text when !string.IsNullOrWhiteSpace(text) => TryParseJson(text),
|
||||
_ => TrySerializeToJson(value)
|
||||
};
|
||||
}
|
||||
|
||||
private static JsonElement? TrySerializeToJson(object value)
|
||||
{
|
||||
try
|
||||
{
|
||||
return JsonSerializer.SerializeToElement(value);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static JsonElement? TryParseJson(string text)
|
||||
{
|
||||
try
|
||||
{
|
||||
using JsonDocument document = JsonDocument.Parse(text);
|
||||
return document.RootElement.Clone();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ShouldPreferStructuredRoot(JsonElement parsedRoot, JsonElement? structuredRoot)
|
||||
{
|
||||
if (structuredRoot is null)
|
||||
return false;
|
||||
|
||||
if (parsedRoot.ValueKind != JsonValueKind.Object)
|
||||
return true;
|
||||
|
||||
return !parsedRoot.EnumerateObject().Any(property =>
|
||||
!property.NameEquals("success") &&
|
||||
!property.NameEquals("isError"));
|
||||
}
|
||||
|
||||
private static bool ShouldTreatRootAsData(JsonElement root)
|
||||
{
|
||||
if (root.ValueKind != JsonValueKind.Object)
|
||||
return false;
|
||||
|
||||
return root.EnumerateObject().Any(property =>
|
||||
!property.NameEquals("success") &&
|
||||
!property.NameEquals("isError") &&
|
||||
!property.NameEquals("errorCode") &&
|
||||
!property.NameEquals("message"));
|
||||
}
|
||||
}
|
||||
|
||||
public static class MccJsonArguments
|
||||
{
|
||||
public static Dictionary<string, object?> Parse(string rawJson)
|
||||
{
|
||||
try
|
||||
{
|
||||
using JsonDocument document = JsonDocument.Parse(string.IsNullOrWhiteSpace(rawJson) ? "{}" : rawJson);
|
||||
if (document.RootElement.ValueKind != JsonValueKind.Object)
|
||||
return new Dictionary<string, object?>();
|
||||
|
||||
Dictionary<string, object?> values = new(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (JsonProperty property in document.RootElement.EnumerateObject())
|
||||
values[property.Name] = Convert(property.Value);
|
||||
return values;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new Dictionary<string, object?>();
|
||||
}
|
||||
}
|
||||
|
||||
private static object? Convert(JsonElement element)
|
||||
{
|
||||
return element.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Null => null,
|
||||
JsonValueKind.True => true,
|
||||
JsonValueKind.False => false,
|
||||
JsonValueKind.Number => element.TryGetInt64(out long i64)
|
||||
? i64
|
||||
: element.TryGetDouble(out double d) ? d : element.GetRawText(),
|
||||
JsonValueKind.String => element.GetString(),
|
||||
JsonValueKind.Array => element.EnumerateArray().Select(Convert).ToArray(),
|
||||
JsonValueKind.Object => element.EnumerateObject().ToDictionary(property => property.Name, property => Convert(property.Value)),
|
||||
_ => element.GetRawText()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using DebugTools.MccMcpWebPlayground.Harness;
|
||||
|
||||
namespace DebugTools.MccMcpWebPlayground.Infrastructure.OpenRouter;
|
||||
|
||||
public sealed class OpenRouterChatClient
|
||||
{
|
||||
private readonly IHttpClientFactory httpClientFactory;
|
||||
|
||||
public OpenRouterChatClient(IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
this.httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
public async Task<MccModelTurn> CreateTurnAsync(
|
||||
List<object> messages,
|
||||
IReadOnlyList<object> tools,
|
||||
MccWebHarnessOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
string apiKey = options.ResolveApiKey() ?? throw new InvalidOperationException("OPENROUTER_API_KEY is not configured.");
|
||||
string model = options.ResolveModel() ?? throw new InvalidOperationException("Model is not configured.");
|
||||
|
||||
using HttpClient client = httpClientFactory.CreateClient("openrouter");
|
||||
client.BaseAddress = new Uri(options.ResolveOpenRouterBaseUrl().TrimEnd('/') + "/");
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
|
||||
client.DefaultRequestHeaders.TryAddWithoutValidation("HTTP-Referer", "https://localhost/mcc-mcp-web-playground");
|
||||
client.DefaultRequestHeaders.TryAddWithoutValidation("X-Title", "MCC MCP Web Playground");
|
||||
|
||||
Dictionary<string, object?> payload = new()
|
||||
{
|
||||
["model"] = model,
|
||||
["messages"] = messages,
|
||||
["tools"] = tools,
|
||||
["tool_choice"] = "auto",
|
||||
["provider"] = new Dictionary<string, object?>
|
||||
{
|
||||
["allow_fallbacks"] = options.AllowFallbacks,
|
||||
["require_parameters"] = options.RequireProviderParameters
|
||||
}
|
||||
};
|
||||
|
||||
if (ShouldSendParallelToolCallsParameter(model))
|
||||
payload["parallel_tool_calls"] = !options.DisableParallelToolCalls;
|
||||
|
||||
using HttpResponseMessage response = await client.PostAsync(
|
||||
"chat/completions",
|
||||
new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json"),
|
||||
cancellationToken);
|
||||
|
||||
string body = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
throw new InvalidOperationException($"OpenRouter returned HTTP {(int)response.StatusCode}: {body}");
|
||||
|
||||
using JsonDocument document = JsonDocument.Parse(body);
|
||||
if (!document.RootElement.TryGetProperty("choices", out JsonElement choices)
|
||||
|| choices.ValueKind != JsonValueKind.Array
|
||||
|| choices.GetArrayLength() == 0)
|
||||
{
|
||||
throw new InvalidOperationException("OpenRouter did not return any choices.");
|
||||
}
|
||||
|
||||
JsonElement message = choices[0].GetProperty("message");
|
||||
string assistantContent = message.TryGetProperty("content", out JsonElement contentElement)
|
||||
? contentElement.GetString() ?? string.Empty
|
||||
: string.Empty;
|
||||
|
||||
List<MccModelToolCall> toolCalls = [];
|
||||
if (message.TryGetProperty("tool_calls", out JsonElement toolCallsElement) && toolCallsElement.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (JsonElement toolCall in toolCallsElement.EnumerateArray())
|
||||
{
|
||||
if (!toolCall.TryGetProperty("id", out JsonElement idElement)
|
||||
|| !toolCall.TryGetProperty("function", out JsonElement functionElement)
|
||||
|| !functionElement.TryGetProperty("name", out JsonElement nameElement))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
toolCalls.Add(new MccModelToolCall(
|
||||
CallId: idElement.GetString() ?? Guid.NewGuid().ToString("n"),
|
||||
Name: nameElement.GetString() ?? string.Empty,
|
||||
ArgumentsJson: functionElement.TryGetProperty("arguments", out JsonElement argumentsElement)
|
||||
? argumentsElement.GetString() ?? "{}"
|
||||
: "{}"));
|
||||
}
|
||||
}
|
||||
|
||||
string modelId = document.RootElement.TryGetProperty("model", out JsonElement modelElement)
|
||||
? modelElement.GetString() ?? model
|
||||
: model;
|
||||
|
||||
string? routedProvider = response.Headers.TryGetValues("x-openrouter-provider", out IEnumerable<string>? providerValues)
|
||||
? providerValues.FirstOrDefault()
|
||||
: null;
|
||||
|
||||
return new MccModelTurn(modelId, routedProvider, assistantContent, toolCalls);
|
||||
}
|
||||
|
||||
private static bool ShouldSendParallelToolCallsParameter(string model)
|
||||
{
|
||||
// Some OpenRouter model families reject tool-enabled requests when the parallel_tool_calls
|
||||
// parameter is present at all, even if it is explicitly set to false. The harness still
|
||||
// executes all returned tool calls sequentially, so omitting the transport hint for those
|
||||
// families preserves the intended runtime behavior while keeping the stricter flag for
|
||||
// compatible models.
|
||||
return !model.StartsWith("minimax/", StringComparison.OrdinalIgnoreCase)
|
||||
&& !model.StartsWith("google/gemini-", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record MccModelTurn(
|
||||
string ModelId,
|
||||
string? RoutedProvider,
|
||||
string AssistantContent,
|
||||
IReadOnlyList<MccModelToolCall> ToolCalls);
|
||||
|
||||
public sealed record MccModelToolCall(string CallId, string Name, string ArgumentsJson);
|
||||
Loading…
Add table
Add a link
Reference in a new issue