From 7853259fca0ae2dea5feb7fb5067021ab72385f7 Mon Sep 17 00:00:00 2001 From: markd70 Date: Tue, 1 Sep 2026 19:35:43 -0400 Subject: [PATCH] Fix JSON deserialization crashes in MCP list_properties/list_units/list_leases/list_transactions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TenantCloud's internal API returns property_status as a numeric value in some tenant accounts, but TcProperty.Status was typed as a plain string without a converter, causing System.Text.Json to throw and the MCP list_properties tool (and anything that triggers EntityCache, i.e. list_leases and list_transactions) to fail with a generic 'An error occurred invoking ...' message. Similarly, TcUnit.Price is a non-nullable decimal, but some units legitimately have a null price (e.g. no price set yet), crashing list_units the same way. Fixes: - Added JsonFlexibleStringConverter: accepts string, number, or boolean JSON tokens and normalizes to a string, matching the codebase's existing pattern of tolerant converters (see JsonTcLeaseStatusConverter, JsonAutoLongConverter, etc.) - Applied it to TcProperty.Status - Added JsonNullableDecimalConverter: accepts null, number, or numeric string and returns decimal? (or null) - Changed TcUnit.Price to decimal? and applied the new converter Verified against a live multi-property TenantCloud account (14 properties, units, leases, and transactions) after rebuilding tc-mcp self-contained win-x64 — all four previously-crashing tools now return correct data. Closes #12 --- .../JsonFlexibleStringConverter.cs | 44 +++++++++++++++++++ .../JsonNullableDecimalConverter.cs | 39 ++++++++++++++++ .../HttpMessages/TcProperty.cs | 1 + .../HttpMessages/TcUnit.cs | 3 +- 4 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 src/Yllibed.TenantCloudClient/HttpMessages/JsonFlexibleStringConverter.cs create mode 100644 src/Yllibed.TenantCloudClient/HttpMessages/JsonNullableDecimalConverter.cs diff --git a/src/Yllibed.TenantCloudClient/HttpMessages/JsonFlexibleStringConverter.cs b/src/Yllibed.TenantCloudClient/HttpMessages/JsonFlexibleStringConverter.cs new file mode 100644 index 0000000..88402da --- /dev/null +++ b/src/Yllibed.TenantCloudClient/HttpMessages/JsonFlexibleStringConverter.cs @@ -0,0 +1,44 @@ +using System.Globalization; + +namespace Yllibed.TenantCloudClient.HttpMessages; + +/// +/// Reads a JSON value that TenantCloud sometimes serializes as a string and +/// sometimes as a number (observed on property_status) into a string. +/// Writes back out as a string. +/// +public class JsonFlexibleStringConverter : JsonConverter +{ + public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + switch (reader.TokenType) + { + case JsonTokenType.String: + return reader.GetString(); + case JsonTokenType.Number: + return reader.TryGetInt64(out var l) + ? l.ToString(CultureInfo.InvariantCulture) + : reader.GetDouble().ToString(CultureInfo.InvariantCulture); + case JsonTokenType.True: + return "true"; + case JsonTokenType.False: + return "false"; + case JsonTokenType.Null: + return null; + default: + throw new NotSupportedException($"Type {reader.TokenType} not supported for flexible string"); + } + } + + public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options) + { + if (value is null) + { + writer.WriteNullValue(); + } + else + { + writer.WriteStringValue(value); + } + } +} diff --git a/src/Yllibed.TenantCloudClient/HttpMessages/JsonNullableDecimalConverter.cs b/src/Yllibed.TenantCloudClient/HttpMessages/JsonNullableDecimalConverter.cs new file mode 100644 index 0000000..582419f --- /dev/null +++ b/src/Yllibed.TenantCloudClient/HttpMessages/JsonNullableDecimalConverter.cs @@ -0,0 +1,39 @@ +using System.Globalization; + +namespace Yllibed.TenantCloudClient.HttpMessages; + +/// +/// Reads a JSON decimal value that may also legitimately be null or a +/// numeric string (observed on unit price). Returns null for a +/// JSON null and parses strings tolerantly. +/// +public class JsonNullableDecimalConverter : JsonConverter +{ + public override decimal? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + switch (reader.TokenType) + { + case JsonTokenType.Null: + return null; + case JsonTokenType.Number: + return reader.GetDecimal(); + case JsonTokenType.String: + var str = reader.GetString(); + return string.IsNullOrWhiteSpace(str) ? null : decimal.Parse(str, CultureInfo.InvariantCulture); + default: + throw new NotSupportedException($"Type {reader.TokenType} not supported for nullable decimal"); + } + } + + public override void Write(Utf8JsonWriter writer, decimal? value, JsonSerializerOptions options) + { + if (value is null) + { + writer.WriteNullValue(); + } + else + { + writer.WriteNumberValue(value.Value); + } + } +} diff --git a/src/Yllibed.TenantCloudClient/HttpMessages/TcProperty.cs b/src/Yllibed.TenantCloudClient/HttpMessages/TcProperty.cs index 33e0374..08ecd2a 100644 --- a/src/Yllibed.TenantCloudClient/HttpMessages/TcProperty.cs +++ b/src/Yllibed.TenantCloudClient/HttpMessages/TcProperty.cs @@ -16,6 +16,7 @@ public class TcProperty : IHasId public string? CityAddress { get; set; } [JsonPropertyName("property_status")] + [JsonConverter(typeof(JsonFlexibleStringConverter))] public string? Status { get; set; } public string Address => string.Format(CultureInfo.InvariantCulture, "{0} {1}", Address1, CityAddress); diff --git a/src/Yllibed.TenantCloudClient/HttpMessages/TcUnit.cs b/src/Yllibed.TenantCloudClient/HttpMessages/TcUnit.cs index 569396f..25efd4e 100644 --- a/src/Yllibed.TenantCloudClient/HttpMessages/TcUnit.cs +++ b/src/Yllibed.TenantCloudClient/HttpMessages/TcUnit.cs @@ -11,7 +11,8 @@ public class TcUnit : IHasId public string? Description { get; set; } - public decimal Price { get; set; } + [JsonConverter(typeof(JsonNullableDecimalConverter))] + public decimal? Price { get; set; } [JsonPropertyName("is_rented")] public bool IsRented { get; set; }