From c67781bac591059c3094a912fa8c79861677ce6e Mon Sep 17 00:00:00 2001 From: Barry Walker Date: Tue, 13 Jan 2026 18:59:16 -0500 Subject: [PATCH] feat: add proper error handling with full API error details MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add ApiResult type to carry success/failure with error details - Add UpdateDocumentWithResultAsync and CreateTagWithResultAsync methods - Update DocumentTools.Update and TagTools.Create to return actual HTTP status codes and response bodies in error responses - Add comprehensive tests for error handling (18 new tests) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- PaperlessMCP.Tests/Client/ApiResultTests.cs | 125 ++++++++++++++++++ .../Client/PaperlessClientTests.cs | 102 ++++++++++++++ .../Fixtures/MockHttpClientFactory.cs | 14 ++ .../Tools/DocumentToolsTests.cs | 41 ++++++ PaperlessMCP.Tests/Tools/TagToolsTests.cs | 41 ++++++ PaperlessMCP/Client/ApiResult.cs | 40 ++++++ PaperlessMCP/Client/PaperlessClient.cs | 106 ++++++++++++--- PaperlessMCP/Tools/DocumentTools.cs | 14 +- PaperlessMCP/Tools/TagTools.cs | 12 +- 9 files changed, 462 insertions(+), 33 deletions(-) create mode 100644 PaperlessMCP.Tests/Client/ApiResultTests.cs create mode 100644 PaperlessMCP/Client/ApiResult.cs diff --git a/PaperlessMCP.Tests/Client/ApiResultTests.cs b/PaperlessMCP.Tests/Client/ApiResultTests.cs new file mode 100644 index 0000000..cbc97ea --- /dev/null +++ b/PaperlessMCP.Tests/Client/ApiResultTests.cs @@ -0,0 +1,125 @@ +using System.Net; +using FluentAssertions; +using PaperlessMCP.Client; +using Xunit; + +namespace PaperlessMCP.Tests.Client; + +public class ApiResultTests +{ + #region Success Tests + + [Fact] + public void Success_WithValue_ReturnsSuccessResult() + { + // Act + var result = ApiResult.Success("test value"); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().Be("test value"); + result.Error.Should().BeNull(); + } + + [Fact] + public void Success_ImplicitBoolConversion_ReturnsTrue() + { + // Arrange + var result = ApiResult.Success(42); + + // Act & Assert + bool isSuccess = result; + isSuccess.Should().BeTrue(); + } + + #endregion + + #region Failure Tests + + [Fact] + public void Failure_WithStatusCodeAndMessage_ReturnsFailureResult() + { + // Act + var result = ApiResult.Failure(HttpStatusCode.NotFound, "Resource not found"); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Value.Should().BeNull(); + result.Error.Should().NotBeNull(); + result.Error!.StatusCode.Should().Be(HttpStatusCode.NotFound); + result.Error.Message.Should().Be("Resource not found"); + result.Error.ResponseBody.Should().BeNull(); + } + + [Fact] + public void Failure_WithResponseBody_IncludesBody() + { + // Arrange + var responseBody = """{"error": "Document not found"}"""; + + // Act + var result = ApiResult.Failure(HttpStatusCode.NotFound, "Not found", responseBody); + + // Assert + result.Error.Should().NotBeNull(); + result.Error!.ResponseBody.Should().Be(responseBody); + } + + [Fact] + public void Failure_WithApiError_ReturnsFailureResult() + { + // Arrange + var error = new ApiError(HttpStatusCode.BadRequest, "Invalid input", """{"field": "name is required"}"""); + + // Act + var result = ApiResult.Failure(error); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error.Should().Be(error); + } + + [Fact] + public void Failure_ImplicitBoolConversion_ReturnsFalse() + { + // Arrange + var result = ApiResult.Failure(HttpStatusCode.InternalServerError, "Server error"); + + // Act & Assert + bool isSuccess = result; + isSuccess.Should().BeFalse(); + } + + #endregion + + #region ApiError Tests + + [Fact] + public void ApiError_ToString_FormatsCorrectly() + { + // Arrange + var error = new ApiError(HttpStatusCode.Forbidden, "Access denied", null); + + // Act + var result = error.ToString(); + + // Assert + result.Should().Be("HTTP 403 Forbidden: Access denied"); + } + + [Fact] + public void ApiError_ToString_IncludesResponseBody() + { + // Arrange + var error = new ApiError(HttpStatusCode.BadRequest, "Validation failed", """{"name": ["This field is required."]}"""); + + // Act + var result = error.ToString(); + + // Assert + result.Should().Contain("HTTP 400 BadRequest: Validation failed"); + result.Should().Contain("""{"name": ["This field is required."]}"""); + } + + #endregion +} diff --git a/PaperlessMCP.Tests/Client/PaperlessClientTests.cs b/PaperlessMCP.Tests/Client/PaperlessClientTests.cs index 6b82617..2e52516 100644 --- a/PaperlessMCP.Tests/Client/PaperlessClientTests.cs +++ b/PaperlessMCP.Tests/Client/PaperlessClientTests.cs @@ -6,6 +6,7 @@ using Xunit; using PaperlessMCP.Models.CustomFields; using PaperlessMCP.Models.DocumentTypes; using PaperlessMCP.Models.StoragePaths; +using PaperlessMCP.Models.Documents; using PaperlessMCP.Models.Tags; using PaperlessMCP.Tests.Fixtures; @@ -167,6 +168,57 @@ public class PaperlessClientTests : IDisposable result.ThumbnailUrl.Should().Be("https://paperless.example.com/api/documents/1/thumb/"); } + [Fact] + public async Task UpdateDocumentWithResultAsync_WhenSuccessful_ReturnsSuccess() + { + // Arrange + _factory.SetupPatch("api/documents/1/", TestFixtures.Documents.CreateDocumentJson(1, "Updated Title")); + + // Act + var result = await _factory.Client.UpdateDocumentWithResultAsync(1, new DocumentUpdateRequest { Title = "Updated Title" }); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().NotBeNull(); + result.Value!.Title.Should().Be("Updated Title"); + result.Error.Should().BeNull(); + } + + [Fact] + public async Task UpdateDocumentWithResultAsync_WhenNotFound_ReturnsFailureWithDetails() + { + // Arrange + var errorBody = """{"detail": "Not found."}"""; + _factory.SetupPatchWithError("api/documents/999/", HttpStatusCode.NotFound, errorBody); + + // Act + var result = await _factory.Client.UpdateDocumentWithResultAsync(999, new DocumentUpdateRequest { Title = "New Title" }); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Value.Should().BeNull(); + result.Error.Should().NotBeNull(); + result.Error!.StatusCode.Should().Be(HttpStatusCode.NotFound); + result.Error.ResponseBody.Should().Be(errorBody); + } + + [Fact] + public async Task UpdateDocumentWithResultAsync_WhenBadRequest_ReturnsFailureWithDetails() + { + // Arrange + var errorBody = """{"title": ["This field may not be blank."]}"""; + _factory.SetupPatchWithError("api/documents/1/", HttpStatusCode.BadRequest, errorBody); + + // Act + var result = await _factory.Client.UpdateDocumentWithResultAsync(1, new DocumentUpdateRequest { Title = "" }); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error.Should().NotBeNull(); + result.Error!.StatusCode.Should().Be(HttpStatusCode.BadRequest); + result.Error.ResponseBody.Should().Contain("This field may not be blank"); + } + #endregion #region Tag Tests @@ -231,6 +283,56 @@ public class PaperlessClientTests : IDisposable result.Should().BeTrue(); } + [Fact] + public async Task CreateTagWithResultAsync_WhenSuccessful_ReturnsSuccess() + { + // Arrange + _factory.SetupPost("api/tags/", TestFixtures.Tags.CreateTagJson(5, "New Tag")); + + // Act + var result = await _factory.Client.CreateTagWithResultAsync(new TagCreateRequest { Name = "New Tag" }); + + // Assert + result.IsSuccess.Should().BeTrue(); + result.Value.Should().NotBeNull(); + result.Value!.Name.Should().Be("New Tag"); + result.Error.Should().BeNull(); + } + + [Fact] + public async Task CreateTagWithResultAsync_WhenDuplicate_ReturnsFailureWithDetails() + { + // Arrange + var errorBody = """{"name": ["tag with this name already exists."]}"""; + _factory.SetupPostWithError("api/tags/", HttpStatusCode.BadRequest, errorBody); + + // Act + var result = await _factory.Client.CreateTagWithResultAsync(new TagCreateRequest { Name = "Existing Tag" }); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error.Should().NotBeNull(); + result.Error!.StatusCode.Should().Be(HttpStatusCode.BadRequest); + result.Error.ResponseBody.Should().Contain("already exists"); + } + + [Fact] + public async Task CreateTagWithResultAsync_WhenUnauthorized_ReturnsFailureWithDetails() + { + // Arrange + var errorBody = """{"detail": "Authentication credentials were not provided."}"""; + _factory.SetupPostWithError("api/tags/", HttpStatusCode.Unauthorized, errorBody); + + // Act + var result = await _factory.Client.CreateTagWithResultAsync(new TagCreateRequest { Name = "Test" }); + + // Assert + result.IsSuccess.Should().BeFalse(); + result.Error.Should().NotBeNull(); + result.Error!.StatusCode.Should().Be(HttpStatusCode.Unauthorized); + result.Error.ResponseBody.Should().Contain("Authentication credentials"); + } + #endregion #region Correspondent Tests diff --git a/PaperlessMCP.Tests/Fixtures/MockHttpClientFactory.cs b/PaperlessMCP.Tests/Fixtures/MockHttpClientFactory.cs index 6402420..a06e9a5 100644 --- a/PaperlessMCP.Tests/Fixtures/MockHttpClientFactory.cs +++ b/PaperlessMCP.Tests/Fixtures/MockHttpClientFactory.cs @@ -79,6 +79,20 @@ public class MockHttpClientFactory : IDisposable .Respond(statusCode); } + public MockedRequest SetupPatchWithError(string url, HttpStatusCode statusCode, string responseBody) + { + return MockHandler + .When(HttpMethod.Patch, $"{Options.BaseUrl.TrimEnd('/')}/{url.TrimStart('/')}") + .Respond(statusCode, "application/json", responseBody); + } + + public MockedRequest SetupPostWithError(string url, HttpStatusCode statusCode, string responseBody) + { + return MockHandler + .When(HttpMethod.Post, $"{Options.BaseUrl.TrimEnd('/')}/{url.TrimStart('/')}") + .Respond(statusCode, "application/json", responseBody); + } + public MockedRequest SetupDelete(string url, HttpStatusCode statusCode = HttpStatusCode.NoContent) { return MockHandler diff --git a/PaperlessMCP.Tests/Tools/DocumentToolsTests.cs b/PaperlessMCP.Tests/Tools/DocumentToolsTests.cs index c017b74..4b87f01 100644 --- a/PaperlessMCP.Tests/Tools/DocumentToolsTests.cs +++ b/PaperlessMCP.Tests/Tools/DocumentToolsTests.cs @@ -433,6 +433,47 @@ public class DocumentToolsTests : IDisposable json.RootElement.GetProperty("error").GetProperty("code").GetString().Should().Be("NOT_FOUND"); } + [Fact] + public async Task Update_WhenBadRequest_ReturnsErrorWithDetails() + { + // Arrange + var errorBody = """{"title": ["This field may not be blank."]}"""; + _factory.SetupPatchWithError("api/documents/1/", HttpStatusCode.BadRequest, errorBody); + + // Act + var result = await DocumentTools.Update(_factory.Client, 1, title: ""); + + // Assert + var json = JsonDocument.Parse(result); + json.RootElement.GetProperty("ok").GetBoolean().Should().BeFalse(); + json.RootElement.GetProperty("error").GetProperty("code").GetString().Should().Be("UPSTREAM_ERROR"); + + // Verify error details include status code and response body + var details = json.RootElement.GetProperty("error").GetProperty("details"); + details.GetProperty("status_code").GetInt32().Should().Be(400); + details.GetProperty("response_body").GetString().Should().Contain("This field may not be blank"); + } + + [Fact] + public async Task Update_WhenForbidden_ReturnsErrorWithDetails() + { + // Arrange + var errorBody = """{"detail": "You do not have permission to perform this action."}"""; + _factory.SetupPatchWithError("api/documents/1/", HttpStatusCode.Forbidden, errorBody); + + // Act + var result = await DocumentTools.Update(_factory.Client, 1, title: "Updated"); + + // Assert + var json = JsonDocument.Parse(result); + json.RootElement.GetProperty("ok").GetBoolean().Should().BeFalse(); + json.RootElement.GetProperty("error").GetProperty("code").GetString().Should().Be("UPSTREAM_ERROR"); + + var details = json.RootElement.GetProperty("error").GetProperty("details"); + details.GetProperty("status_code").GetInt32().Should().Be(403); + details.GetProperty("response_body").GetString().Should().Contain("permission"); + } + #endregion #region Delete Tests diff --git a/PaperlessMCP.Tests/Tools/TagToolsTests.cs b/PaperlessMCP.Tests/Tools/TagToolsTests.cs index 91f7de0..7df9644 100644 --- a/PaperlessMCP.Tests/Tools/TagToolsTests.cs +++ b/PaperlessMCP.Tests/Tools/TagToolsTests.cs @@ -119,6 +119,47 @@ public class TagToolsTests : IDisposable json.RootElement.GetProperty("error").GetProperty("code").GetString().Should().Be("UPSTREAM_ERROR"); } + [Fact] + public async Task Create_WhenDuplicate_ReturnsErrorWithDetails() + { + // Arrange + var errorBody = """{"name": ["tag with this name already exists."]}"""; + _factory.SetupPostWithError("api/tags/", HttpStatusCode.BadRequest, errorBody); + + // Act + var result = await TagTools.Create(_factory.Client, "Existing Tag"); + + // Assert + var json = JsonDocument.Parse(result); + json.RootElement.GetProperty("ok").GetBoolean().Should().BeFalse(); + json.RootElement.GetProperty("error").GetProperty("code").GetString().Should().Be("UPSTREAM_ERROR"); + + // Verify error details include status code and response body + var details = json.RootElement.GetProperty("error").GetProperty("details"); + details.GetProperty("status_code").GetInt32().Should().Be(400); + details.GetProperty("response_body").GetString().Should().Contain("already exists"); + } + + [Fact] + public async Task Create_WhenUnauthorized_ReturnsErrorWithDetails() + { + // Arrange + var errorBody = """{"detail": "Authentication credentials were not provided."}"""; + _factory.SetupPostWithError("api/tags/", HttpStatusCode.Unauthorized, errorBody); + + // Act + var result = await TagTools.Create(_factory.Client, "New Tag"); + + // Assert + var json = JsonDocument.Parse(result); + json.RootElement.GetProperty("ok").GetBoolean().Should().BeFalse(); + json.RootElement.GetProperty("error").GetProperty("code").GetString().Should().Be("UPSTREAM_ERROR"); + + var details = json.RootElement.GetProperty("error").GetProperty("details"); + details.GetProperty("status_code").GetInt32().Should().Be(401); + details.GetProperty("response_body").GetString().Should().Contain("Authentication credentials"); + } + [Fact] public async Task Update_WhenSuccessful_ReturnsUpdatedTag() { diff --git a/PaperlessMCP/Client/ApiResult.cs b/PaperlessMCP/Client/ApiResult.cs new file mode 100644 index 0000000..5c75d43 --- /dev/null +++ b/PaperlessMCP/Client/ApiResult.cs @@ -0,0 +1,40 @@ +using System.Net; + +namespace PaperlessMCP.Client; + +/// +/// Represents the result of an API operation that can either succeed or fail with details. +/// +/// The type of the success value. +public readonly record struct ApiResult +{ + public bool IsSuccess { get; } + public T? Value { get; } + public ApiError? Error { get; } + + private ApiResult(bool isSuccess, T? value, ApiError? error) + { + IsSuccess = isSuccess; + Value = value; + Error = error; + } + + public static ApiResult Success(T value) => new(true, value, null); + + public static ApiResult Failure(HttpStatusCode statusCode, string message, string? responseBody = null) => + new(false, default, new ApiError(statusCode, message, responseBody)); + + public static ApiResult Failure(ApiError error) => new(false, default, error); + + public static implicit operator bool(ApiResult result) => result.IsSuccess; +} + +/// +/// Details about an API error. +/// +public record ApiError(HttpStatusCode StatusCode, string Message, string? ResponseBody) +{ + public override string ToString() => + $"HTTP {(int)StatusCode} {StatusCode}: {Message}" + + (ResponseBody != null ? $" - {ResponseBody}" : ""); +} diff --git a/PaperlessMCP/Client/PaperlessClient.cs b/PaperlessMCP/Client/PaperlessClient.cs index 4e60de5..3c52cbb 100644 --- a/PaperlessMCP/Client/PaperlessClient.cs +++ b/PaperlessMCP/Client/PaperlessClient.cs @@ -180,7 +180,16 @@ public class PaperlessClient /// public async Task UpdateDocumentAsync(int id, DocumentUpdateRequest request, CancellationToken cancellationToken = default) { - return await PatchAsync($"api/documents/{id}/", request, cancellationToken).ConfigureAwait(false); + var result = await UpdateDocumentWithResultAsync(id, request, cancellationToken).ConfigureAwait(false); + return result.IsSuccess ? result.Value : null; + } + + /// + /// Updates a document with full error details. + /// + public async Task> UpdateDocumentWithResultAsync(int id, DocumentUpdateRequest request, CancellationToken cancellationToken = default) + { + return await PatchWithResultAsync($"api/documents/{id}/", request, cancellationToken).ConfigureAwait(false); } /// @@ -188,7 +197,16 @@ public class PaperlessClient /// public async Task DeleteDocumentAsync(int id, CancellationToken cancellationToken = default) { - return await DeleteAsync($"api/documents/{id}/", cancellationToken).ConfigureAwait(false); + var result = await DeleteDocumentWithResultAsync(id, cancellationToken).ConfigureAwait(false); + return result.IsSuccess; + } + + /// + /// Deletes a document with full error details. + /// + public async Task> DeleteDocumentWithResultAsync(int id, CancellationToken cancellationToken = default) + { + return await DeleteWithResultAsync($"api/documents/{id}/", cancellationToken).ConfigureAwait(false); } /// @@ -428,17 +446,35 @@ public class PaperlessClient public async Task CreateTagAsync(TagCreateRequest request, CancellationToken cancellationToken = default) { - return await PostAsync("api/tags/", request, cancellationToken).ConfigureAwait(false); + var result = await CreateTagWithResultAsync(request, cancellationToken).ConfigureAwait(false); + return result.IsSuccess ? result.Value : null; + } + + public async Task> CreateTagWithResultAsync(TagCreateRequest request, CancellationToken cancellationToken = default) + { + return await PostWithResultAsync("api/tags/", request, cancellationToken).ConfigureAwait(false); } public async Task UpdateTagAsync(int id, TagUpdateRequest request, CancellationToken cancellationToken = default) { - return await PatchAsync($"api/tags/{id}/", request, cancellationToken).ConfigureAwait(false); + var result = await UpdateTagWithResultAsync(id, request, cancellationToken).ConfigureAwait(false); + return result.IsSuccess ? result.Value : null; + } + + public async Task> UpdateTagWithResultAsync(int id, TagUpdateRequest request, CancellationToken cancellationToken = default) + { + return await PatchWithResultAsync($"api/tags/{id}/", request, cancellationToken).ConfigureAwait(false); } public async Task DeleteTagAsync(int id, CancellationToken cancellationToken = default) { - return await DeleteAsync($"api/tags/{id}/", cancellationToken).ConfigureAwait(false); + var result = await DeleteTagWithResultAsync(id, cancellationToken).ConfigureAwait(false); + return result.IsSuccess; + } + + public async Task> DeleteTagWithResultAsync(int id, CancellationToken cancellationToken = default) + { + return await DeleteWithResultAsync($"api/tags/{id}/", cancellationToken).ConfigureAwait(false); } #endregion @@ -632,7 +668,7 @@ public class PaperlessClient return await response.Content.ReadFromJsonAsync(JsonOptions, cancellationToken).ConfigureAwait(false); } - await LogErrorResponse(response, "GET", url).ConfigureAwait(false); + await CreateApiError(response, "GET", url).ConfigureAwait(false); return default; } catch (Exception ex) @@ -642,7 +678,7 @@ public class PaperlessClient } } - private async Task PostAsync(string url, object request, CancellationToken cancellationToken) + private async Task> PostWithResultAsync(string url, object request, CancellationToken cancellationToken) { try { @@ -650,20 +686,23 @@ public class PaperlessClient if (response.IsSuccessStatusCode) { - return await response.Content.ReadFromJsonAsync(JsonOptions, cancellationToken).ConfigureAwait(false); + var result = await response.Content.ReadFromJsonAsync(JsonOptions, cancellationToken).ConfigureAwait(false); + return result != null + ? ApiResult.Success(result) + : ApiResult.Failure(response.StatusCode, "Empty response body"); } - await LogErrorResponse(response, "POST", url).ConfigureAwait(false); - return default; + var error = await CreateApiError(response, "POST", url).ConfigureAwait(false); + return ApiResult.Failure(error); } catch (Exception ex) { _logger.LogError(ex, "POST request failed: {Url}", url); - return default; + return ApiResult.Failure(HttpStatusCode.InternalServerError, ex.Message); } } - private async Task PatchAsync(string url, object request, CancellationToken cancellationToken) + private async Task> PatchWithResultAsync(string url, object request, CancellationToken cancellationToken) { try { @@ -672,20 +711,23 @@ public class PaperlessClient if (response.IsSuccessStatusCode) { - return await response.Content.ReadFromJsonAsync(JsonOptions, cancellationToken).ConfigureAwait(false); + var result = await response.Content.ReadFromJsonAsync(JsonOptions, cancellationToken).ConfigureAwait(false); + return result != null + ? ApiResult.Success(result) + : ApiResult.Failure(response.StatusCode, "Empty response body"); } - await LogErrorResponse(response, "PATCH", url).ConfigureAwait(false); - return default; + var error = await CreateApiError(response, "PATCH", url).ConfigureAwait(false); + return ApiResult.Failure(error); } catch (Exception ex) { _logger.LogError(ex, "PATCH request failed: {Url}", url); - return default; + return ApiResult.Failure(HttpStatusCode.InternalServerError, ex.Message); } } - private async Task DeleteAsync(string url, CancellationToken cancellationToken) + private async Task> DeleteWithResultAsync(string url, CancellationToken cancellationToken) { try { @@ -693,24 +735,44 @@ public class PaperlessClient if (response.IsSuccessStatusCode || response.StatusCode == HttpStatusCode.NoContent) { - return true; + return ApiResult.Success(true); } - await LogErrorResponse(response, "DELETE", url).ConfigureAwait(false); - return false; + var error = await CreateApiError(response, "DELETE", url).ConfigureAwait(false); + return ApiResult.Failure(error); } catch (Exception ex) { _logger.LogError(ex, "DELETE request failed: {Url}", url); - return false; + return ApiResult.Failure(HttpStatusCode.InternalServerError, ex.Message); } } - private async Task LogErrorResponse(HttpResponseMessage response, string method, string url) + private async Task CreateApiError(HttpResponseMessage response, string method, string url) { var body = await response.Content.ReadAsStringAsync().ConfigureAwait(false); _logger.LogError("{Method} {Url} failed with {StatusCode}: {Body}", method, url, (int)response.StatusCode, body); + return new ApiError(response.StatusCode, response.ReasonPhrase ?? "Unknown error", body); + } + + // Legacy methods for backward compatibility - will be removed after migration + private async Task PostAsync(string url, object request, CancellationToken cancellationToken) + { + var result = await PostWithResultAsync(url, request, cancellationToken).ConfigureAwait(false); + return result.IsSuccess ? result.Value : default; + } + + private async Task PatchAsync(string url, object request, CancellationToken cancellationToken) + { + var result = await PatchWithResultAsync(url, request, cancellationToken).ConfigureAwait(false); + return result.IsSuccess ? result.Value : default; + } + + private async Task DeleteAsync(string url, CancellationToken cancellationToken) + { + var result = await DeleteWithResultAsync(url, cancellationToken).ConfigureAwait(false); + return result.IsSuccess; } #endregion diff --git a/PaperlessMCP/Tools/DocumentTools.cs b/PaperlessMCP/Tools/DocumentTools.cs index f53f66f..e5040a8 100644 --- a/PaperlessMCP/Tools/DocumentTools.cs +++ b/PaperlessMCP/Tools/DocumentTools.cs @@ -350,20 +350,22 @@ public static class DocumentTools Created = ParseDate(created) }; - var document = await client.UpdateDocumentAsync(id, request).ConfigureAwait(false); + var result = await client.UpdateDocumentWithResultAsync(id, request).ConfigureAwait(false); - if (document == null) + if (!result.IsSuccess) { + var error = result.Error!; var errorResponse = McpErrorResponse.Create( - ErrorCodes.NotFound, - $"Document with ID {id} not found or update failed", - meta: new McpMeta { PaperlessBaseUrl = client.BaseUrl } + error.StatusCode == System.Net.HttpStatusCode.NotFound ? ErrorCodes.NotFound : ErrorCodes.UpstreamError, + $"Failed to update document {id}: {error.Message}", + new { status_code = (int)error.StatusCode, response_body = error.ResponseBody }, + new McpMeta { PaperlessBaseUrl = client.BaseUrl } ); return JsonSerializer.Serialize(errorResponse); } var response = McpResponse.Success( - document, + result.Value!, new McpMeta { PaperlessBaseUrl = client.BaseUrl } ); return JsonSerializer.Serialize(response); diff --git a/PaperlessMCP/Tools/TagTools.cs b/PaperlessMCP/Tools/TagTools.cs index e1ffa0c..e475ec3 100644 --- a/PaperlessMCP/Tools/TagTools.cs +++ b/PaperlessMCP/Tools/TagTools.cs @@ -82,20 +82,22 @@ public static class TagTools IsInboxTag = isInboxTag }; - var tag = await client.CreateTagAsync(request).ConfigureAwait(false); + var result = await client.CreateTagWithResultAsync(request).ConfigureAwait(false); - if (tag == null) + if (!result.IsSuccess) { + var error = result.Error!; var errorResponse = McpErrorResponse.Create( ErrorCodes.UpstreamError, - "Failed to create tag", - meta: new McpMeta { PaperlessBaseUrl = client.BaseUrl } + $"Failed to create tag: {error.Message}", + new { status_code = (int)error.StatusCode, response_body = error.ResponseBody }, + new McpMeta { PaperlessBaseUrl = client.BaseUrl } ); return JsonSerializer.Serialize(errorResponse); } var response = McpResponse.Success( - tag, + result.Value!, new McpMeta { PaperlessBaseUrl = client.BaseUrl } ); return JsonSerializer.Serialize(response);