using System.Net; using System.Net.Http.Json; using System.Text.Json; using Microsoft.Extensions.Logging; using ModelContextProtocol.Protocol; namespace Novelly.Mcp; public class NovelApiClient(HttpClient http, ILogger logger) { private static readonly JsonSerializerOptions Options = new(JsonSerializerDefaults.Web) { WriteIndented = true }; public Task GetAsync(string path, CancellationToken ct = default) => SendAsync(new HttpRequestMessage(HttpMethod.Get, path), ct); public Task PostAsync(string path, object body, CancellationToken ct = default) => SendAsync(new HttpRequestMessage(HttpMethod.Post, path) { Content = JsonContent.Create(body, options: Options) }, ct); public Task PatchAsync(string path, object body, CancellationToken ct = default) => SendAsync(new HttpRequestMessage(HttpMethod.Patch, path) { Content = JsonContent.Create(body, options: Options) }, ct); public Task PutAsync(string path, object body, CancellationToken ct = default) => SendAsync(new HttpRequestMessage(HttpMethod.Put, path) { Content = JsonContent.Create(body, options: Options) }, ct); public Task DeleteAsync(string path, CancellationToken ct = default) => SendAsync(new HttpRequestMessage(HttpMethod.Delete, path), ct); private async Task SendAsync(HttpRequestMessage request, CancellationToken ct) { HttpResponseMessage response; try { response = await http.SendAsync(request, ct); } catch (HttpRequestException ex) { logger.LogError(ex, "Could not reach the Novelly API at {BaseAddress}", http.BaseAddress); return Error($"Could not reach the Novelly API at {http.BaseAddress}. Is it running? ({ex.Message})"); } var body = await response.Content.ReadAsStringAsync(ct); if (response.IsSuccessStatusCode) { return Ok(string.IsNullOrWhiteSpace(body) ? "{\"ok\":true}" : Prettify(body)); } var detail = TryReadProblemDetail(body) ?? body; return Error(response.StatusCode switch { HttpStatusCode.Unauthorized => $"Not permitted: the Novelly API rejected the service api key. Set NOVELLY_API_KEY to match the API's Auth:ServiceApiKey. ({detail})", HttpStatusCode.Forbidden => $"Not permitted: {detail}", HttpStatusCode.NotFound => $"Not found: {detail}", HttpStatusCode.BadRequest => $"Rejected: {detail}", _ => $"API returned {(int)response.StatusCode}: {detail}" }); } private static CallToolResult Ok(string text) => new() { Content = [new TextContentBlock { Text = text }] }; private static CallToolResult Error(string message) => new() { Content = [new TextContentBlock { Text = message }], IsError = true }; private string Prettify(string json) { try { return JsonSerializer.Serialize(JsonSerializer.Deserialize(json), Options); } catch (JsonException ex) { logger.LogWarning(ex, "Response body was not valid JSON; returning it unformatted"); return json; } } private string? TryReadProblemDetail(string body) { try { var problem = JsonSerializer.Deserialize(body); return problem.TryGetProperty("detail", out var detail) ? detail.GetString() : null; } catch (JsonException ex) { logger.LogWarning(ex, "Error response body was not valid JSON problem details"); return null; } } }