Rename project and namespaces
This commit is contained in:
parent
e7fa6194f6
commit
c9ed453745
4 changed files with 426 additions and 918 deletions
|
|
@ -0,0 +1,93 @@
|
|||
using ModelContextProtocol.Protocol.Messages;
|
||||
using System.ComponentModel;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace ModelContextProtocol.Utils.Json;
|
||||
|
||||
/// <summary>
|
||||
/// JSON converter for IJsonRpcMessage that handles polymorphic deserialization of different message types.
|
||||
/// </summary>
|
||||
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||
public sealed class JsonRpcMessageConverter : JsonConverter<IJsonRpcMessage>
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
public override IJsonRpcMessage? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType != JsonTokenType.StartObject)
|
||||
{
|
||||
throw new JsonException("Expected StartObject token");
|
||||
}
|
||||
|
||||
using var doc = JsonDocument.ParseValue(ref reader);
|
||||
var root = doc.RootElement;
|
||||
|
||||
// All JSON-RPC messages must have a jsonrpc property with value "2.0"
|
||||
if (!root.TryGetProperty("jsonrpc", out var versionProperty) ||
|
||||
versionProperty.GetString() != "2.0")
|
||||
{
|
||||
throw new JsonException("Invalid or missing jsonrpc version");
|
||||
}
|
||||
|
||||
// Determine the message type based on the presence of id, method, and error properties
|
||||
bool hasId = root.TryGetProperty("id", out _);
|
||||
bool hasMethod = root.TryGetProperty("method", out _);
|
||||
bool hasError = root.TryGetProperty("error", out _);
|
||||
|
||||
var rawText = root.GetRawText();
|
||||
|
||||
// Messages with an id but no method are responses
|
||||
if (hasId && !hasMethod)
|
||||
{
|
||||
// Messages with an error property are error responses
|
||||
if (hasError)
|
||||
{
|
||||
return JsonSerializer.Deserialize(rawText, options.GetTypeInfo<JsonRpcError>());
|
||||
}
|
||||
|
||||
// Messages with a result property are success responses
|
||||
if (root.TryGetProperty("result", out _))
|
||||
{
|
||||
return JsonSerializer.Deserialize(rawText, options.GetTypeInfo<JsonRpcResponse>());
|
||||
}
|
||||
|
||||
throw new JsonException("Response must have either result or error");
|
||||
}
|
||||
|
||||
// Messages with a method but no id are notifications
|
||||
if (hasMethod && !hasId)
|
||||
{
|
||||
return JsonSerializer.Deserialize(rawText, options.GetTypeInfo<JsonRpcNotification>());
|
||||
}
|
||||
|
||||
// Messages with both method and id are requests
|
||||
if (hasMethod && hasId)
|
||||
{
|
||||
return JsonSerializer.Deserialize(rawText, options.GetTypeInfo<JsonRpcRequest>());
|
||||
}
|
||||
|
||||
throw new JsonException("Invalid JSON-RPC message format");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Write(Utf8JsonWriter writer, IJsonRpcMessage value, JsonSerializerOptions options)
|
||||
{
|
||||
switch (value)
|
||||
{
|
||||
case JsonRpcRequest request:
|
||||
JsonSerializer.Serialize(writer, request, options.GetTypeInfo<JsonRpcRequest>());
|
||||
break;
|
||||
case JsonRpcNotification notification:
|
||||
JsonSerializer.Serialize(writer, notification, options.GetTypeInfo<JsonRpcNotification>());
|
||||
break;
|
||||
case JsonRpcResponse response:
|
||||
JsonSerializer.Serialize(writer, response, options.GetTypeInfo<JsonRpcResponse>());
|
||||
break;
|
||||
case JsonRpcError error:
|
||||
JsonSerializer.Serialize(writer, error, options.GetTypeInfo<JsonRpcError>());
|
||||
break;
|
||||
default:
|
||||
throw new JsonException($"Unknown JSON-RPC message type: {value.GetType()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
using ModelContextProtocol.Protocol.Messages;
|
||||
using ModelContextProtocol.Protocol.Types;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
|
||||
namespace ModelContextProtocol.Utils.Json;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for configuring System.Text.Json serialization options for MCP.
|
||||
/// </summary>
|
||||
internal static partial class JsonSerializerOptionsExtensions
|
||||
{
|
||||
public static JsonSerializerOptions DefaultOptions { get; } = CreateDefaultOptions();
|
||||
|
||||
/// <summary>
|
||||
/// Creates default options to use for MCP-related serialization.
|
||||
/// </summary>
|
||||
/// <returns>The configured options.</returns>
|
||||
[UnconditionalSuppressMessage("AotAnalysis", "IL3050", Justification = "DefaultJsonTypeInfoResolver is only used when reflection-based serialization is enabled")]
|
||||
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", Justification = "DefaultJsonTypeInfoResolver is only used when reflection-based serialization is enabled")]
|
||||
private static JsonSerializerOptions CreateDefaultOptions()
|
||||
{
|
||||
// If reflection-based serialization is enabled by default, use it, as it's the most permissive in terms of what it can serialize,
|
||||
// and we want to be flexible in terms of what can be put into the various collections in the object model.
|
||||
// Otherwise, use the source-generated options to enable trimming and Native AOT.
|
||||
|
||||
if (JsonSerializer.IsReflectionEnabledByDefault)
|
||||
{
|
||||
// Keep in sync with the JsonSourceGenerationOptions attribute on JsonContext below.
|
||||
JsonSerializerOptions options = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
|
||||
Converters = { new JsonStringEnumConverter() },
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString
|
||||
};
|
||||
|
||||
options.MakeReadOnly();
|
||||
return options;
|
||||
}
|
||||
|
||||
return JsonContext.Default.Options;
|
||||
}
|
||||
|
||||
internal static JsonTypeInfo<T> GetTypeInfo<T>(this JsonSerializerOptions options) =>
|
||||
(JsonTypeInfo<T>)options.GetTypeInfo(typeof(T));
|
||||
|
||||
// Keep in sync with CreateDefaultOptions above.
|
||||
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web,
|
||||
UseStringEnumConverter = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
NumberHandling = JsonNumberHandling.AllowReadingFromString)]
|
||||
[JsonSerializable(typeof(JsonDocument))]
|
||||
[JsonSerializable(typeof(JsonElement))]
|
||||
[JsonSerializable(typeof(JsonNode))]
|
||||
[JsonSerializable(typeof(IJsonRpcMessage))]
|
||||
[JsonSerializable(typeof(JsonRpcRequest))]
|
||||
[JsonSerializable(typeof(JsonRpcNotification))]
|
||||
[JsonSerializable(typeof(JsonRpcResponse))]
|
||||
[JsonSerializable(typeof(JsonRpcError))]
|
||||
[JsonSerializable(typeof(ServerCapabilities))]
|
||||
[JsonSerializable(typeof(Implementation))]
|
||||
[JsonSerializable(typeof(CreateMessageResult))]
|
||||
[JsonSerializable(typeof(ListRootsResult))]
|
||||
[JsonSerializable(typeof(InitializeResult))]
|
||||
[JsonSerializable(typeof(JsonSchema))]
|
||||
[JsonSerializable(typeof(CallToolResponse))]
|
||||
internal sealed partial class JsonContext : JsonSerializerContext;
|
||||
}
|
||||
108
src/ModelContextProtocol/Utils/ProcessHelper.cs
Normal file
108
src/ModelContextProtocol/Utils/ProcessHelper.cs
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace ModelContextProtocol.Utils;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class for working with processes.
|
||||
/// </summary>
|
||||
internal static class ProcessHelper
|
||||
{
|
||||
private static readonly bool _isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||||
private static readonly TimeSpan _defaultTimeout = TimeSpan.FromSeconds(30);
|
||||
|
||||
public static void KillTree(this Process process) => process.KillTree(_defaultTimeout);
|
||||
|
||||
public static void KillTree(this Process process, TimeSpan timeout)
|
||||
{
|
||||
var pid = process.Id;
|
||||
if (_isWindows)
|
||||
{
|
||||
RunProcessAndWaitForExit(
|
||||
"taskkill",
|
||||
$"/T /F /PID {pid}",
|
||||
timeout,
|
||||
out var _);
|
||||
}
|
||||
else
|
||||
{
|
||||
var children = new HashSet<int>();
|
||||
GetAllChildIdsUnix(pid, children, timeout);
|
||||
foreach (var childId in children)
|
||||
{
|
||||
KillProcessUnix(childId, timeout);
|
||||
}
|
||||
KillProcessUnix(pid, timeout);
|
||||
}
|
||||
|
||||
// wait until the process finishes exiting/getting killed.
|
||||
// We don't want to wait forever here because the task is already supposed to be dieing, we just want to give it long enough
|
||||
// to try and flush what it can and stop. If it cannot do that in a reasonable time frame then we will just ignore it.
|
||||
process.WaitForExit((int)timeout.TotalMilliseconds);
|
||||
}
|
||||
|
||||
private static void GetAllChildIdsUnix(int parentId, ISet<int> children, TimeSpan timeout)
|
||||
{
|
||||
var exitcode = RunProcessAndWaitForExit(
|
||||
"pgrep",
|
||||
$"-P {parentId}",
|
||||
timeout,
|
||||
out var stdout);
|
||||
|
||||
if (exitcode == 0 && !string.IsNullOrEmpty(stdout))
|
||||
{
|
||||
using var reader = new StringReader(stdout);
|
||||
while (true)
|
||||
{
|
||||
var text = reader.ReadLine();
|
||||
if (text == null)
|
||||
return;
|
||||
|
||||
if (int.TryParse(text, out var id))
|
||||
{
|
||||
children.Add(id);
|
||||
// Recursively get the children
|
||||
GetAllChildIdsUnix(id, children, timeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void KillProcessUnix(int processId, TimeSpan timeout)
|
||||
{
|
||||
RunProcessAndWaitForExit(
|
||||
"kill",
|
||||
$"-TERM {processId}",
|
||||
timeout,
|
||||
out var _);
|
||||
}
|
||||
|
||||
private static int RunProcessAndWaitForExit(string fileName, string arguments, TimeSpan timeout, out string? stdout)
|
||||
{
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = fileName,
|
||||
Arguments = arguments,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
};
|
||||
|
||||
stdout = null;
|
||||
|
||||
var process = Process.Start(startInfo);
|
||||
if (process == null)
|
||||
return -1;
|
||||
|
||||
if (process.WaitForExit((int)timeout.TotalMilliseconds))
|
||||
{
|
||||
stdout = process.StandardOutput.ReadToEnd();
|
||||
}
|
||||
else
|
||||
{
|
||||
process.Kill();
|
||||
}
|
||||
|
||||
return process.ExitCode;
|
||||
}
|
||||
}
|
||||
41
src/ModelContextProtocol/Utils/Throw.cs
Normal file
41
src/ModelContextProtocol/Utils/Throw.cs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace ModelContextProtocol.Utils;
|
||||
|
||||
/// <summary>Provides helper methods for throwing exceptions.</summary>
|
||||
internal static class Throw
|
||||
{
|
||||
// NOTE: Most of these should be replaced with extension statics for the relevant extension
|
||||
// type as downlevel polyfills once the C# 14 extension everything feature is available.
|
||||
|
||||
public static void IfNull([NotNull] object? arg, [CallerArgumentExpression(nameof(arg))] string? parameterName = null)
|
||||
{
|
||||
if (arg is null)
|
||||
{
|
||||
ThrowArgumentNullException(parameterName);
|
||||
}
|
||||
}
|
||||
|
||||
public static void IfNullOrWhiteSpace([NotNull] string? arg, [CallerArgumentExpression(nameof(arg))] string? parameterName = null)
|
||||
{
|
||||
if (arg is null || arg.AsSpan().IsWhiteSpace())
|
||||
{
|
||||
ThrowArgumentNullOrWhiteSpaceException(arg);
|
||||
}
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
private static void ThrowArgumentNullOrWhiteSpaceException(string? parameterName)
|
||||
{
|
||||
if (parameterName is null)
|
||||
{
|
||||
ThrowArgumentNullException(parameterName);
|
||||
}
|
||||
|
||||
throw new ArgumentException("Value cannot be empty or composed entirely of whitespace.", parameterName);
|
||||
}
|
||||
|
||||
[DoesNotReturn]
|
||||
private static void ThrowArgumentNullException(string? parameterName) => throw new ArgumentNullException(parameterName);
|
||||
}
|
||||
Loading…
Reference in a new issue