feat: add k8s deployment pipeline and code quality improvements

- Add deploy step to Woodpecker CI release pipeline
- Create ParsingHelpers utility to deduplicate ParseIntArray/ParseDate
- Add ConfigureAwait(false) to all async calls (library best practice)
- Fix resource disposal in UploadDocumentInternalAsync
- Configure HttpClient default 30s timeout
- Remove unused GetAllPagesAsync method

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Barry Walker
2026-01-13 18:21:00 -05:00
parent dba6c453c4
commit 6cac693e9c
12 changed files with 176 additions and 200 deletions
+19
View File
@@ -118,3 +118,22 @@ steps:
https://api.github.com/repos/barryw/PaperlessMCP/releases \ https://api.github.com/repos/barryw/PaperlessMCP/releases \
-d "{\"tag_name\":\"$$TAG\",\"name\":\"Release $$VERSION\",\"body\":\"Release $$VERSION\",\"draft\":false,\"prerelease\":false}" -d "{\"tag_name\":\"$$TAG\",\"name\":\"Release $$VERSION\",\"body\":\"Release $$VERSION\",\"draft\":false,\"prerelease\":false}"
depends_on: [git-tag] depends_on: [git-tag]
# Deploy to Kubernetes (uses in-cluster service account)
- name: deploy
image: bitnami/kubectl:latest
commands:
- |
VERSION=$$(cat .version)
echo "Deploying version $$VERSION to Kubernetes"
# Update deployment image to specific version tag
kubectl set image deployment/paperless-mcp \
paperless-mcp=ghcr.io/barryw/paperlessmcp:v$$VERSION \
-n default
# Wait for rollout to complete
kubectl rollout status deployment/paperless-mcp -n default --timeout=120s
echo "Deployment complete!"
depends_on: [docker, release]
+61 -57
View File
@@ -48,12 +48,12 @@ public class PaperlessClient
{ {
try try
{ {
var response = await _httpClient.GetAsync("api/status/", cancellationToken); var response = await _httpClient.GetAsync("api/status/", cancellationToken).ConfigureAwait(false);
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
{ {
// Extract version from the status response // Extract version from the status response
var content = await response.Content.ReadAsStringAsync(cancellationToken); var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
var json = JsonSerializer.Deserialize<JsonElement>(content); var json = JsonSerializer.Deserialize<JsonElement>(content);
var version = json.TryGetProperty("pngx_version", out var versionProp) var version = json.TryGetProperty("pngx_version", out var versionProp)
? versionProp.GetString() ? versionProp.GetString()
@@ -78,11 +78,11 @@ public class PaperlessClient
{ {
try try
{ {
var response = await _httpClient.GetAsync("api/status/", cancellationToken); var response = await _httpClient.GetAsync("api/status/", cancellationToken).ConfigureAwait(false);
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
{ {
var json = await response.Content.ReadFromJsonAsync<JsonDocument>(cancellationToken); var json = await response.Content.ReadFromJsonAsync<JsonDocument>(cancellationToken).ConfigureAwait(false);
return (true, json, null); return (true, json, null);
} }
@@ -163,7 +163,7 @@ public class PaperlessClient
queryParams["ordering"] = ordering; queryParams["ordering"] = ordering;
var url = $"api/documents/?{queryParams}"; var url = $"api/documents/?{queryParams}";
return await GetAsync<PaginatedResult<DocumentSearchResult>>(url, cancellationToken) return await GetAsync<PaginatedResult<DocumentSearchResult>>(url, cancellationToken).ConfigureAwait(false)
?? new PaginatedResult<DocumentSearchResult>(); ?? new PaginatedResult<DocumentSearchResult>();
} }
@@ -172,7 +172,7 @@ public class PaperlessClient
/// </summary> /// </summary>
public async Task<Document?> GetDocumentAsync(int id, CancellationToken cancellationToken = default) public async Task<Document?> GetDocumentAsync(int id, CancellationToken cancellationToken = default)
{ {
return await GetAsync<Document>($"api/documents/{id}/", cancellationToken); return await GetAsync<Document>($"api/documents/{id}/", cancellationToken).ConfigureAwait(false);
} }
/// <summary> /// <summary>
@@ -180,7 +180,7 @@ public class PaperlessClient
/// </summary> /// </summary>
public async Task<Document?> UpdateDocumentAsync(int id, DocumentUpdateRequest request, CancellationToken cancellationToken = default) public async Task<Document?> UpdateDocumentAsync(int id, DocumentUpdateRequest request, CancellationToken cancellationToken = default)
{ {
return await PatchAsync<Document>($"api/documents/{id}/", request, cancellationToken); return await PatchAsync<Document>($"api/documents/{id}/", request, cancellationToken).ConfigureAwait(false);
} }
/// <summary> /// <summary>
@@ -188,7 +188,7 @@ public class PaperlessClient
/// </summary> /// </summary>
public async Task<bool> DeleteDocumentAsync(int id, CancellationToken cancellationToken = default) public async Task<bool> DeleteDocumentAsync(int id, CancellationToken cancellationToken = default)
{ {
return await DeleteAsync($"api/documents/{id}/", cancellationToken); return await DeleteAsync($"api/documents/{id}/", cancellationToken).ConfigureAwait(false);
} }
/// <summary> /// <summary>
@@ -204,7 +204,7 @@ public class PaperlessClient
() => new ByteArrayContent(fileContent), () => new ByteArrayContent(fileContent),
fileName, fileName,
metadata, metadata,
cancellationToken); cancellationToken).ConfigureAwait(false);
} }
/// <summary> /// <summary>
@@ -247,7 +247,7 @@ public class PaperlessClient
fileName, fileName,
metadata, metadata,
cancellationToken, cancellationToken,
disposeContent: false); // StreamContent owns the stream disposeContent: false).ConfigureAwait(false); // StreamContent owns the stream
if (taskId != null) if (taskId != null)
{ {
@@ -262,18 +262,18 @@ public class PaperlessClient
{ {
var delay = TimeSpan.FromSeconds(Math.Pow(2, attempt)); // Exponential backoff var delay = TimeSpan.FromSeconds(Math.Pow(2, attempt)); // Exponential backoff
_logger.LogInformation("Retrying in {Delay}...", delay); _logger.LogInformation("Retrying in {Delay}...", delay);
await Task.Delay(delay, cancellationToken); await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
} }
} }
catch (IOException ex) when (attempt < maxRetries) catch (IOException ex) when (attempt < maxRetries)
{ {
_logger.LogWarning(ex, "IO error on attempt {Attempt}/{MaxRetries}, retrying...", attempt, maxRetries); _logger.LogWarning(ex, "IO error on attempt {Attempt}/{MaxRetries}, retrying...", attempt, maxRetries);
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)), cancellationToken); await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)), cancellationToken).ConfigureAwait(false);
} }
catch (HttpRequestException ex) when (attempt < maxRetries) catch (HttpRequestException ex) when (attempt < maxRetries)
{ {
_logger.LogWarning(ex, "HTTP error on attempt {Attempt}/{MaxRetries}, retrying...", attempt, maxRetries); _logger.LogWarning(ex, "HTTP error on attempt {Attempt}/{MaxRetries}, retrying...", attempt, maxRetries);
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)), cancellationToken); await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)), cancellationToken).ConfigureAwait(false);
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -294,10 +294,12 @@ public class PaperlessClient
{ {
using var formContent = new MultipartFormDataContent(); using var formContent = new MultipartFormDataContent();
var fileContent = contentFactory(); var fileContent = contentFactory();
var addedToForm = false;
try try
{ {
formContent.Add(fileContent, "document", fileName); formContent.Add(fileContent, "document", fileName);
addedToForm = true;
if (metadata != null) if (metadata != null)
{ {
@@ -327,21 +329,23 @@ public class PaperlessClient
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(TimeSpan.FromMinutes(5)); // 5 minute timeout for uploads cts.CancelAfter(TimeSpan.FromMinutes(5)); // 5 minute timeout for uploads
var response = await _httpClient.PostAsync("api/documents/post_document/", formContent, cts.Token); var response = await _httpClient.PostAsync("api/documents/post_document/", formContent, cts.Token).ConfigureAwait(false);
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
{ {
var result = await response.Content.ReadAsStringAsync(cts.Token); var result = await response.Content.ReadAsStringAsync(cts.Token).ConfigureAwait(false);
return result.Trim('"'); // Returns task UUID return result.Trim('"'); // Returns task UUID
} }
var error = await response.Content.ReadAsStringAsync(cts.Token); var error = await response.Content.ReadAsStringAsync(cts.Token).ConfigureAwait(false);
_logger.LogError("Failed to upload document: {StatusCode} - {Error}", response.StatusCode, error); _logger.LogError("Failed to upload document: {StatusCode} - {Error}", response.StatusCode, error);
return null; return null;
} }
finally finally
{ {
if (disposeContent && fileContent is IDisposable disposable) // Only dispose manually if we didn't add it to formContent
// (formContent owns and will dispose content added to it)
if (!addedToForm && disposeContent && fileContent is IDisposable disposable)
{ {
disposable.Dispose(); disposable.Dispose();
} }
@@ -383,7 +387,7 @@ public class PaperlessClient
try try
{ {
var response = await _httpClient.PostAsJsonAsync("api/documents/bulk_edit/", request, JsonOptions, cancellationToken); var response = await _httpClient.PostAsJsonAsync("api/documents/bulk_edit/", request, JsonOptions, cancellationToken).ConfigureAwait(false);
return response.IsSuccessStatusCode; return response.IsSuccessStatusCode;
} }
catch (Exception ex) catch (Exception ex)
@@ -398,7 +402,7 @@ public class PaperlessClient
/// </summary> /// </summary>
public async Task<int?> GetNextAsnAsync(CancellationToken cancellationToken = default) public async Task<int?> GetNextAsnAsync(CancellationToken cancellationToken = default)
{ {
return await GetAsync<int?>("api/documents/next_asn/", cancellationToken); return await GetAsync<int?>("api/documents/next_asn/", cancellationToken).ConfigureAwait(false);
} }
#endregion #endregion
@@ -413,28 +417,28 @@ public class PaperlessClient
if (!string.IsNullOrEmpty(ordering)) if (!string.IsNullOrEmpty(ordering))
queryParams["ordering"] = ordering; queryParams["ordering"] = ordering;
return await GetAsync<PaginatedResult<Tag>>($"api/tags/?{queryParams}", cancellationToken) return await GetAsync<PaginatedResult<Tag>>($"api/tags/?{queryParams}", cancellationToken).ConfigureAwait(false)
?? new PaginatedResult<Tag>(); ?? new PaginatedResult<Tag>();
} }
public async Task<Tag?> GetTagAsync(int id, CancellationToken cancellationToken = default) public async Task<Tag?> GetTagAsync(int id, CancellationToken cancellationToken = default)
{ {
return await GetAsync<Tag>($"api/tags/{id}/", cancellationToken); return await GetAsync<Tag>($"api/tags/{id}/", cancellationToken).ConfigureAwait(false);
} }
public async Task<Tag?> CreateTagAsync(TagCreateRequest request, CancellationToken cancellationToken = default) public async Task<Tag?> CreateTagAsync(TagCreateRequest request, CancellationToken cancellationToken = default)
{ {
return await PostAsync<Tag>("api/tags/", request, cancellationToken); return await PostAsync<Tag>("api/tags/", request, cancellationToken).ConfigureAwait(false);
} }
public async Task<Tag?> UpdateTagAsync(int id, TagUpdateRequest request, CancellationToken cancellationToken = default) public async Task<Tag?> UpdateTagAsync(int id, TagUpdateRequest request, CancellationToken cancellationToken = default)
{ {
return await PatchAsync<Tag>($"api/tags/{id}/", request, cancellationToken); return await PatchAsync<Tag>($"api/tags/{id}/", request, cancellationToken).ConfigureAwait(false);
} }
public async Task<bool> DeleteTagAsync(int id, CancellationToken cancellationToken = default) public async Task<bool> DeleteTagAsync(int id, CancellationToken cancellationToken = default)
{ {
return await DeleteAsync($"api/tags/{id}/", cancellationToken); return await DeleteAsync($"api/tags/{id}/", cancellationToken).ConfigureAwait(false);
} }
#endregion #endregion
@@ -449,28 +453,28 @@ public class PaperlessClient
if (!string.IsNullOrEmpty(ordering)) if (!string.IsNullOrEmpty(ordering))
queryParams["ordering"] = ordering; queryParams["ordering"] = ordering;
return await GetAsync<PaginatedResult<Correspondent>>($"api/correspondents/?{queryParams}", cancellationToken) return await GetAsync<PaginatedResult<Correspondent>>($"api/correspondents/?{queryParams}", cancellationToken).ConfigureAwait(false)
?? new PaginatedResult<Correspondent>(); ?? new PaginatedResult<Correspondent>();
} }
public async Task<Correspondent?> GetCorrespondentAsync(int id, CancellationToken cancellationToken = default) public async Task<Correspondent?> GetCorrespondentAsync(int id, CancellationToken cancellationToken = default)
{ {
return await GetAsync<Correspondent>($"api/correspondents/{id}/", cancellationToken); return await GetAsync<Correspondent>($"api/correspondents/{id}/", cancellationToken).ConfigureAwait(false);
} }
public async Task<Correspondent?> CreateCorrespondentAsync(CorrespondentCreateRequest request, CancellationToken cancellationToken = default) public async Task<Correspondent?> CreateCorrespondentAsync(CorrespondentCreateRequest request, CancellationToken cancellationToken = default)
{ {
return await PostAsync<Correspondent>("api/correspondents/", request, cancellationToken); return await PostAsync<Correspondent>("api/correspondents/", request, cancellationToken).ConfigureAwait(false);
} }
public async Task<Correspondent?> UpdateCorrespondentAsync(int id, CorrespondentUpdateRequest request, CancellationToken cancellationToken = default) public async Task<Correspondent?> UpdateCorrespondentAsync(int id, CorrespondentUpdateRequest request, CancellationToken cancellationToken = default)
{ {
return await PatchAsync<Correspondent>($"api/correspondents/{id}/", request, cancellationToken); return await PatchAsync<Correspondent>($"api/correspondents/{id}/", request, cancellationToken).ConfigureAwait(false);
} }
public async Task<bool> DeleteCorrespondentAsync(int id, CancellationToken cancellationToken = default) public async Task<bool> DeleteCorrespondentAsync(int id, CancellationToken cancellationToken = default)
{ {
return await DeleteAsync($"api/correspondents/{id}/", cancellationToken); return await DeleteAsync($"api/correspondents/{id}/", cancellationToken).ConfigureAwait(false);
} }
#endregion #endregion
@@ -485,28 +489,28 @@ public class PaperlessClient
if (!string.IsNullOrEmpty(ordering)) if (!string.IsNullOrEmpty(ordering))
queryParams["ordering"] = ordering; queryParams["ordering"] = ordering;
return await GetAsync<PaginatedResult<DocumentType>>($"api/document_types/?{queryParams}", cancellationToken) return await GetAsync<PaginatedResult<DocumentType>>($"api/document_types/?{queryParams}", cancellationToken).ConfigureAwait(false)
?? new PaginatedResult<DocumentType>(); ?? new PaginatedResult<DocumentType>();
} }
public async Task<DocumentType?> GetDocumentTypeAsync(int id, CancellationToken cancellationToken = default) public async Task<DocumentType?> GetDocumentTypeAsync(int id, CancellationToken cancellationToken = default)
{ {
return await GetAsync<DocumentType>($"api/document_types/{id}/", cancellationToken); return await GetAsync<DocumentType>($"api/document_types/{id}/", cancellationToken).ConfigureAwait(false);
} }
public async Task<DocumentType?> CreateDocumentTypeAsync(DocumentTypeCreateRequest request, CancellationToken cancellationToken = default) public async Task<DocumentType?> CreateDocumentTypeAsync(DocumentTypeCreateRequest request, CancellationToken cancellationToken = default)
{ {
return await PostAsync<DocumentType>("api/document_types/", request, cancellationToken); return await PostAsync<DocumentType>("api/document_types/", request, cancellationToken).ConfigureAwait(false);
} }
public async Task<DocumentType?> UpdateDocumentTypeAsync(int id, DocumentTypeUpdateRequest request, CancellationToken cancellationToken = default) public async Task<DocumentType?> UpdateDocumentTypeAsync(int id, DocumentTypeUpdateRequest request, CancellationToken cancellationToken = default)
{ {
return await PatchAsync<DocumentType>($"api/document_types/{id}/", request, cancellationToken); return await PatchAsync<DocumentType>($"api/document_types/{id}/", request, cancellationToken).ConfigureAwait(false);
} }
public async Task<bool> DeleteDocumentTypeAsync(int id, CancellationToken cancellationToken = default) public async Task<bool> DeleteDocumentTypeAsync(int id, CancellationToken cancellationToken = default)
{ {
return await DeleteAsync($"api/document_types/{id}/", cancellationToken); return await DeleteAsync($"api/document_types/{id}/", cancellationToken).ConfigureAwait(false);
} }
#endregion #endregion
@@ -521,28 +525,28 @@ public class PaperlessClient
if (!string.IsNullOrEmpty(ordering)) if (!string.IsNullOrEmpty(ordering))
queryParams["ordering"] = ordering; queryParams["ordering"] = ordering;
return await GetAsync<PaginatedResult<StoragePath>>($"api/storage_paths/?{queryParams}", cancellationToken) return await GetAsync<PaginatedResult<StoragePath>>($"api/storage_paths/?{queryParams}", cancellationToken).ConfigureAwait(false)
?? new PaginatedResult<StoragePath>(); ?? new PaginatedResult<StoragePath>();
} }
public async Task<StoragePath?> GetStoragePathAsync(int id, CancellationToken cancellationToken = default) public async Task<StoragePath?> GetStoragePathAsync(int id, CancellationToken cancellationToken = default)
{ {
return await GetAsync<StoragePath>($"api/storage_paths/{id}/", cancellationToken); return await GetAsync<StoragePath>($"api/storage_paths/{id}/", cancellationToken).ConfigureAwait(false);
} }
public async Task<StoragePath?> CreateStoragePathAsync(StoragePathCreateRequest request, CancellationToken cancellationToken = default) public async Task<StoragePath?> CreateStoragePathAsync(StoragePathCreateRequest request, CancellationToken cancellationToken = default)
{ {
return await PostAsync<StoragePath>("api/storage_paths/", request, cancellationToken); return await PostAsync<StoragePath>("api/storage_paths/", request, cancellationToken).ConfigureAwait(false);
} }
public async Task<StoragePath?> UpdateStoragePathAsync(int id, StoragePathUpdateRequest request, CancellationToken cancellationToken = default) public async Task<StoragePath?> UpdateStoragePathAsync(int id, StoragePathUpdateRequest request, CancellationToken cancellationToken = default)
{ {
return await PatchAsync<StoragePath>($"api/storage_paths/{id}/", request, cancellationToken); return await PatchAsync<StoragePath>($"api/storage_paths/{id}/", request, cancellationToken).ConfigureAwait(false);
} }
public async Task<bool> DeleteStoragePathAsync(int id, CancellationToken cancellationToken = default) public async Task<bool> DeleteStoragePathAsync(int id, CancellationToken cancellationToken = default)
{ {
return await DeleteAsync($"api/storage_paths/{id}/", cancellationToken); return await DeleteAsync($"api/storage_paths/{id}/", cancellationToken).ConfigureAwait(false);
} }
#endregion #endregion
@@ -555,28 +559,28 @@ public class PaperlessClient
queryParams["page"] = page.ToString(); queryParams["page"] = page.ToString();
queryParams["page_size"] = (pageSize ?? _options.MaxPageSize).ToString(); queryParams["page_size"] = (pageSize ?? _options.MaxPageSize).ToString();
return await GetAsync<PaginatedResult<CustomField>>($"api/custom_fields/?{queryParams}", cancellationToken) return await GetAsync<PaginatedResult<CustomField>>($"api/custom_fields/?{queryParams}", cancellationToken).ConfigureAwait(false)
?? new PaginatedResult<CustomField>(); ?? new PaginatedResult<CustomField>();
} }
public async Task<CustomField?> GetCustomFieldAsync(int id, CancellationToken cancellationToken = default) public async Task<CustomField?> GetCustomFieldAsync(int id, CancellationToken cancellationToken = default)
{ {
return await GetAsync<CustomField>($"api/custom_fields/{id}/", cancellationToken); return await GetAsync<CustomField>($"api/custom_fields/{id}/", cancellationToken).ConfigureAwait(false);
} }
public async Task<CustomField?> CreateCustomFieldAsync(CustomFieldCreateRequest request, CancellationToken cancellationToken = default) public async Task<CustomField?> CreateCustomFieldAsync(CustomFieldCreateRequest request, CancellationToken cancellationToken = default)
{ {
return await PostAsync<CustomField>("api/custom_fields/", request, cancellationToken); return await PostAsync<CustomField>("api/custom_fields/", request, cancellationToken).ConfigureAwait(false);
} }
public async Task<CustomField?> UpdateCustomFieldAsync(int id, CustomFieldUpdateRequest request, CancellationToken cancellationToken = default) public async Task<CustomField?> UpdateCustomFieldAsync(int id, CustomFieldUpdateRequest request, CancellationToken cancellationToken = default)
{ {
return await PatchAsync<CustomField>($"api/custom_fields/{id}/", request, cancellationToken); return await PatchAsync<CustomField>($"api/custom_fields/{id}/", request, cancellationToken).ConfigureAwait(false);
} }
public async Task<bool> DeleteCustomFieldAsync(int id, CancellationToken cancellationToken = default) public async Task<bool> DeleteCustomFieldAsync(int id, CancellationToken cancellationToken = default)
{ {
return await DeleteAsync($"api/custom_fields/{id}/", cancellationToken); return await DeleteAsync($"api/custom_fields/{id}/", cancellationToken).ConfigureAwait(false);
} }
#endregion #endregion
@@ -603,7 +607,7 @@ public class PaperlessClient
try try
{ {
var response = await _httpClient.PostAsJsonAsync("api/bulk_edit_objects/", request, JsonOptions, cancellationToken); var response = await _httpClient.PostAsJsonAsync("api/bulk_edit_objects/", request, JsonOptions, cancellationToken).ConfigureAwait(false);
return response.IsSuccessStatusCode; return response.IsSuccessStatusCode;
} }
catch (Exception ex) catch (Exception ex)
@@ -621,14 +625,14 @@ public class PaperlessClient
{ {
try try
{ {
var response = await _httpClient.GetAsync(url, cancellationToken); var response = await _httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
{ {
return await response.Content.ReadFromJsonAsync<T>(JsonOptions, cancellationToken); return await response.Content.ReadFromJsonAsync<T>(JsonOptions, cancellationToken).ConfigureAwait(false);
} }
await LogErrorResponse(response, "GET", url); await LogErrorResponse(response, "GET", url).ConfigureAwait(false);
return default; return default;
} }
catch (Exception ex) catch (Exception ex)
@@ -642,14 +646,14 @@ public class PaperlessClient
{ {
try try
{ {
var response = await _httpClient.PostAsJsonAsync(url, request, JsonOptions, cancellationToken); var response = await _httpClient.PostAsJsonAsync(url, request, JsonOptions, cancellationToken).ConfigureAwait(false);
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
{ {
return await response.Content.ReadFromJsonAsync<T>(JsonOptions, cancellationToken); return await response.Content.ReadFromJsonAsync<T>(JsonOptions, cancellationToken).ConfigureAwait(false);
} }
await LogErrorResponse(response, "POST", url); await LogErrorResponse(response, "POST", url).ConfigureAwait(false);
return default; return default;
} }
catch (Exception ex) catch (Exception ex)
@@ -664,14 +668,14 @@ public class PaperlessClient
try try
{ {
var content = JsonContent.Create(request, options: JsonOptions); var content = JsonContent.Create(request, options: JsonOptions);
var response = await _httpClient.PatchAsync(url, content, cancellationToken); var response = await _httpClient.PatchAsync(url, content, cancellationToken).ConfigureAwait(false);
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
{ {
return await response.Content.ReadFromJsonAsync<T>(JsonOptions, cancellationToken); return await response.Content.ReadFromJsonAsync<T>(JsonOptions, cancellationToken).ConfigureAwait(false);
} }
await LogErrorResponse(response, "PATCH", url); await LogErrorResponse(response, "PATCH", url).ConfigureAwait(false);
return default; return default;
} }
catch (Exception ex) catch (Exception ex)
@@ -685,14 +689,14 @@ public class PaperlessClient
{ {
try try
{ {
var response = await _httpClient.DeleteAsync(url, cancellationToken); var response = await _httpClient.DeleteAsync(url, cancellationToken).ConfigureAwait(false);
if (response.IsSuccessStatusCode || response.StatusCode == HttpStatusCode.NoContent) if (response.IsSuccessStatusCode || response.StatusCode == HttpStatusCode.NoContent)
{ {
return true; return true;
} }
await LogErrorResponse(response, "DELETE", url); await LogErrorResponse(response, "DELETE", url).ConfigureAwait(false);
return false; return false;
} }
catch (Exception ex) catch (Exception ex)
@@ -704,7 +708,7 @@ public class PaperlessClient
private async Task LogErrorResponse(HttpResponseMessage response, string method, string url) private async Task LogErrorResponse(HttpResponseMessage response, string method, string url)
{ {
var body = await response.Content.ReadAsStringAsync(); var body = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
_logger.LogError("{Method} {Url} failed with {StatusCode}: {Body}", _logger.LogError("{Method} {Url} failed with {StatusCode}: {Body}",
method, url, (int)response.StatusCode, body); method, url, (int)response.StatusCode, body);
} }
@@ -19,33 +19,6 @@ public record PaginatedResult<T>
[JsonPropertyName("results")] [JsonPropertyName("results")]
public List<T> Results { get; init; } = []; public List<T> Results { get; init; } = [];
/// <summary>
/// Gets all items from all pages using the provided fetch function.
/// </summary>
public static async Task<List<T>> GetAllPagesAsync(
Func<int, Task<PaginatedResult<T>>> fetchPage,
int maxPages = 100,
CancellationToken cancellationToken = default)
{
var allResults = new List<T>();
var page = 1;
while (page <= maxPages)
{
cancellationToken.ThrowIfCancellationRequested();
var result = await fetchPage(page);
allResults.AddRange(result.Results);
if (string.IsNullOrEmpty(result.Next))
break;
page++;
}
return allResults;
}
} }
/// <summary> /// <summary>
+1
View File
@@ -87,6 +87,7 @@ void ConfigureServices(IServiceCollection services, IConfiguration configuration
var options = sp.GetRequiredService<IOptions<PaperlessOptions>>().Value; var options = sp.GetRequiredService<IOptions<PaperlessOptions>>().Value;
client.BaseAddress = new Uri(options.BaseUrl.TrimEnd('/') + "/"); client.BaseAddress = new Uri(options.BaseUrl.TrimEnd('/') + "/");
client.DefaultRequestHeaders.Add("Accept", "application/json; version=9"); client.DefaultRequestHeaders.Add("Accept", "application/json; version=9");
client.Timeout = TimeSpan.FromSeconds(30);
}) })
.AddHttpMessageHandler<PaperlessAuthHandler>() .AddHttpMessageHandler<PaperlessAuthHandler>()
.AddPolicyHandler(retryPolicy); .AddPolicyHandler(retryPolicy);
+8 -18
View File
@@ -4,6 +4,7 @@ using ModelContextProtocol.Server;
using PaperlessMCP.Client; using PaperlessMCP.Client;
using PaperlessMCP.Models.Common; using PaperlessMCP.Models.Common;
using PaperlessMCP.Models.Correspondents; using PaperlessMCP.Models.Correspondents;
using static PaperlessMCP.Utils.ParsingHelpers;
namespace PaperlessMCP.Tools; namespace PaperlessMCP.Tools;
@@ -21,7 +22,7 @@ public static class CorrespondentTools
[Description("Page size (default: 25, max: 100)")] int pageSize = 25, [Description("Page size (default: 25, max: 100)")] int pageSize = 25,
[Description("Ordering field (e.g., 'name', '-document_count', 'last_correspondence')")] string? ordering = null) [Description("Ordering field (e.g., 'name', '-document_count', 'last_correspondence')")] string? ordering = null)
{ {
var result = await client.GetCorrespondentsAsync(page, Math.Min(pageSize, 100), ordering); var result = await client.GetCorrespondentsAsync(page, Math.Min(pageSize, 100), ordering).ConfigureAwait(false);
var response = McpResponse<object>.Success( var response = McpResponse<object>.Success(
result.Results, result.Results,
@@ -43,7 +44,7 @@ public static class CorrespondentTools
PaperlessClient client, PaperlessClient client,
[Description("Correspondent ID")] int id) [Description("Correspondent ID")] int id)
{ {
var correspondent = await client.GetCorrespondentAsync(id); var correspondent = await client.GetCorrespondentAsync(id).ConfigureAwait(false);
if (correspondent == null) if (correspondent == null)
{ {
@@ -77,7 +78,7 @@ public static class CorrespondentTools
MatchingAlgorithm = matchingAlgorithm MatchingAlgorithm = matchingAlgorithm
}; };
var correspondent = await client.CreateCorrespondentAsync(request); var correspondent = await client.CreateCorrespondentAsync(request).ConfigureAwait(false);
if (correspondent == null) if (correspondent == null)
{ {
@@ -112,7 +113,7 @@ public static class CorrespondentTools
MatchingAlgorithm = matchingAlgorithm MatchingAlgorithm = matchingAlgorithm
}; };
var correspondent = await client.UpdateCorrespondentAsync(id, request); var correspondent = await client.UpdateCorrespondentAsync(id, request).ConfigureAwait(false);
if (correspondent == null) if (correspondent == null)
{ {
@@ -140,7 +141,7 @@ public static class CorrespondentTools
{ {
if (!confirm) if (!confirm)
{ {
var correspondent = await client.GetCorrespondentAsync(id); var correspondent = await client.GetCorrespondentAsync(id).ConfigureAwait(false);
if (correspondent == null) if (correspondent == null)
{ {
@@ -161,7 +162,7 @@ public static class CorrespondentTools
return JsonSerializer.Serialize(dryRunResponse); return JsonSerializer.Serialize(dryRunResponse);
} }
var success = await client.DeleteCorrespondentAsync(id); var success = await client.DeleteCorrespondentAsync(id).ConfigureAwait(false);
if (!success) if (!success)
{ {
@@ -219,7 +220,7 @@ public static class CorrespondentTools
return JsonSerializer.Serialize(dryRunResponse); return JsonSerializer.Serialize(dryRunResponse);
} }
var success = await client.BulkEditObjectsAsync(ids, "correspondents", "delete"); var success = await client.BulkEditObjectsAsync(ids, "correspondents", "delete").ConfigureAwait(false);
if (!success) if (!success)
{ {
@@ -244,15 +245,4 @@ public static class CorrespondentTools
return JsonSerializer.Serialize(response); return JsonSerializer.Serialize(response);
} }
private static int[]? ParseIntArray(string? input)
{
if (string.IsNullOrWhiteSpace(input))
return null;
return input.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(s => int.TryParse(s, out var n) ? n : (int?)null)
.Where(n => n.HasValue)
.Select(n => n!.Value)
.ToArray();
}
} }
+9 -9
View File
@@ -21,7 +21,7 @@ public static class CustomFieldTools
[Description("Page number (default: 1)")] int page = 1, [Description("Page number (default: 1)")] int page = 1,
[Description("Page size (default: 25, max: 100)")] int pageSize = 25) [Description("Page size (default: 25, max: 100)")] int pageSize = 25)
{ {
var result = await client.GetCustomFieldsAsync(page, Math.Min(pageSize, 100)); var result = await client.GetCustomFieldsAsync(page, Math.Min(pageSize, 100)).ConfigureAwait(false);
var response = McpResponse<object>.Success( var response = McpResponse<object>.Success(
result.Results, result.Results,
@@ -43,7 +43,7 @@ public static class CustomFieldTools
PaperlessClient client, PaperlessClient client,
[Description("Custom field ID")] int id) [Description("Custom field ID")] int id)
{ {
var customField = await client.GetCustomFieldAsync(id); var customField = await client.GetCustomFieldAsync(id).ConfigureAwait(false);
if (customField == null) if (customField == null)
{ {
@@ -95,7 +95,7 @@ public static class CustomFieldTools
ExtraData = extraData ExtraData = extraData
}; };
var customField = await client.CreateCustomFieldAsync(request); var customField = await client.CreateCustomFieldAsync(request).ConfigureAwait(false);
if (customField == null) if (customField == null)
{ {
@@ -146,7 +146,7 @@ public static class CustomFieldTools
ExtraData = extraData ExtraData = extraData
}; };
var customField = await client.UpdateCustomFieldAsync(id, request); var customField = await client.UpdateCustomFieldAsync(id, request).ConfigureAwait(false);
if (customField == null) if (customField == null)
{ {
@@ -174,7 +174,7 @@ public static class CustomFieldTools
{ {
if (!confirm) if (!confirm)
{ {
var customField = await client.GetCustomFieldAsync(id); var customField = await client.GetCustomFieldAsync(id).ConfigureAwait(false);
if (customField == null) if (customField == null)
{ {
@@ -195,7 +195,7 @@ public static class CustomFieldTools
return JsonSerializer.Serialize(dryRunResponse); return JsonSerializer.Serialize(dryRunResponse);
} }
var success = await client.DeleteCustomFieldAsync(id); var success = await client.DeleteCustomFieldAsync(id).ConfigureAwait(false);
if (!success) if (!success)
{ {
@@ -223,7 +223,7 @@ public static class CustomFieldTools
[Description("Value to assign (string, number, boolean, or date depending on field type)")] string value) [Description("Value to assign (string, number, boolean, or date depending on field type)")] string value)
{ {
// Get current document to update its custom fields // Get current document to update its custom fields
var document = await client.GetDocumentAsync(documentId); var document = await client.GetDocumentAsync(documentId).ConfigureAwait(false);
if (document == null) if (document == null)
{ {
@@ -236,7 +236,7 @@ public static class CustomFieldTools
} }
// Get field definition to understand the data type // Get field definition to understand the data type
var field = await client.GetCustomFieldAsync(fieldId); var field = await client.GetCustomFieldAsync(fieldId).ConfigureAwait(false);
if (field == null) if (field == null)
{ {
@@ -277,7 +277,7 @@ public static class CustomFieldTools
CustomFields = customFields CustomFields = customFields
}; };
var updatedDocument = await client.UpdateDocumentAsync(documentId, updateRequest); var updatedDocument = await client.UpdateDocumentAsync(documentId, updateRequest).ConfigureAwait(false);
if (updatedDocument == null) if (updatedDocument == null)
{ {
+14 -32
View File
@@ -4,6 +4,7 @@ using ModelContextProtocol.Server;
using PaperlessMCP.Client; using PaperlessMCP.Client;
using PaperlessMCP.Models.Common; using PaperlessMCP.Models.Common;
using PaperlessMCP.Models.Documents; using PaperlessMCP.Models.Documents;
using static PaperlessMCP.Utils.ParsingHelpers;
namespace PaperlessMCP.Tools; namespace PaperlessMCP.Tools;
@@ -57,7 +58,7 @@ public static class DocumentTools
page: page, page: page,
pageSize: Math.Min(pageSize, 100), pageSize: Math.Min(pageSize, 100),
ordering: ordering ordering: ordering
); ).ConfigureAwait(false);
// Map to lightweight summaries to reduce response size // Map to lightweight summaries to reduce response size
var summaries = result.Results var summaries = result.Results
@@ -87,7 +88,7 @@ public static class DocumentTools
PaperlessClient client, PaperlessClient client,
[Description("Document ID")] int id) [Description("Document ID")] int id)
{ {
var document = await client.GetDocumentAsync(id); var document = await client.GetDocumentAsync(id).ConfigureAwait(false);
if (document == null) if (document == null)
{ {
@@ -112,7 +113,7 @@ public static class DocumentTools
PaperlessClient client, PaperlessClient client,
[Description("Document ID")] int id) [Description("Document ID")] int id)
{ {
var document = await client.GetDocumentAsync(id); var document = await client.GetDocumentAsync(id).ConfigureAwait(false);
if (document == null) if (document == null)
{ {
@@ -139,7 +140,7 @@ public static class DocumentTools
PaperlessClient client, PaperlessClient client,
[Description("Document ID")] int id) [Description("Document ID")] int id)
{ {
var document = await client.GetDocumentAsync(id); var document = await client.GetDocumentAsync(id).ConfigureAwait(false);
if (document == null) if (document == null)
{ {
@@ -166,7 +167,7 @@ public static class DocumentTools
PaperlessClient client, PaperlessClient client,
[Description("Document ID")] int id) [Description("Document ID")] int id)
{ {
var document = await client.GetDocumentAsync(id); var document = await client.GetDocumentAsync(id).ConfigureAwait(false);
if (document == null) if (document == null)
{ {
@@ -227,7 +228,7 @@ public static class DocumentTools
Created = ParseDate(created) Created = ParseDate(created)
}; };
var taskId = await client.UploadDocumentAsync(fileBytes, fileName, metadata); var taskId = await client.UploadDocumentAsync(fileBytes, fileName, metadata).ConfigureAwait(false);
if (taskId == null) if (taskId == null)
{ {
@@ -299,7 +300,7 @@ public static class DocumentTools
Created = ParseDate(created) Created = ParseDate(created)
}; };
var (taskId, error) = await client.UploadDocumentFromPathAsync(filePath, metadata); var (taskId, error) = await client.UploadDocumentFromPathAsync(filePath, metadata).ConfigureAwait(false);
if (taskId == null) if (taskId == null)
{ {
@@ -349,7 +350,7 @@ public static class DocumentTools
Created = ParseDate(created) Created = ParseDate(created)
}; };
var document = await client.UpdateDocumentAsync(id, request); var document = await client.UpdateDocumentAsync(id, request).ConfigureAwait(false);
if (document == null) if (document == null)
{ {
@@ -378,7 +379,7 @@ public static class DocumentTools
if (!confirm) if (!confirm)
{ {
// Get document info for dry run // Get document info for dry run
var document = await client.GetDocumentAsync(id); var document = await client.GetDocumentAsync(id).ConfigureAwait(false);
if (document == null) if (document == null)
{ {
@@ -405,7 +406,7 @@ public static class DocumentTools
return JsonSerializer.Serialize(dryRunResponse); return JsonSerializer.Serialize(dryRunResponse);
} }
var success = await client.DeleteDocumentAsync(id); var success = await client.DeleteDocumentAsync(id).ConfigureAwait(false);
if (!success) if (!success)
{ {
@@ -485,7 +486,7 @@ public static class DocumentTools
_ => null _ => null
}; };
var success = await client.BulkEditDocumentsAsync(ids, operation, parameters); var success = await client.BulkEditDocumentsAsync(ids, operation, parameters).ConfigureAwait(false);
if (!success) if (!success)
{ {
@@ -519,7 +520,7 @@ public static class DocumentTools
{ {
if (!confirm) if (!confirm)
{ {
var document = await client.GetDocumentAsync(id); var document = await client.GetDocumentAsync(id).ConfigureAwait(false);
if (document == null) if (document == null)
{ {
@@ -540,7 +541,7 @@ public static class DocumentTools
return JsonSerializer.Serialize(dryRunResponse); return JsonSerializer.Serialize(dryRunResponse);
} }
var success = await client.BulkEditDocumentsAsync(new[] { id }, "reprocess"); var success = await client.BulkEditDocumentsAsync(new[] { id }, "reprocess").ConfigureAwait(false);
if (!success) if (!success)
{ {
@@ -559,23 +560,4 @@ public static class DocumentTools
return JsonSerializer.Serialize(response); return JsonSerializer.Serialize(response);
} }
private static int[]? ParseIntArray(string? input)
{
if (string.IsNullOrWhiteSpace(input))
return null;
return input.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(s => int.TryParse(s, out var n) ? n : (int?)null)
.Where(n => n.HasValue)
.Select(n => n!.Value)
.ToArray();
}
private static DateTime? ParseDate(string? input)
{
if (string.IsNullOrWhiteSpace(input))
return null;
return DateTime.TryParse(input, out var date) ? date : null;
}
} }
+8 -18
View File
@@ -4,6 +4,7 @@ using ModelContextProtocol.Server;
using PaperlessMCP.Client; using PaperlessMCP.Client;
using PaperlessMCP.Models.Common; using PaperlessMCP.Models.Common;
using PaperlessMCP.Models.DocumentTypes; using PaperlessMCP.Models.DocumentTypes;
using static PaperlessMCP.Utils.ParsingHelpers;
namespace PaperlessMCP.Tools; namespace PaperlessMCP.Tools;
@@ -21,7 +22,7 @@ public static class DocumentTypeTools
[Description("Page size (default: 25, max: 100)")] int pageSize = 25, [Description("Page size (default: 25, max: 100)")] int pageSize = 25,
[Description("Ordering field (e.g., 'name', '-document_count')")] string? ordering = null) [Description("Ordering field (e.g., 'name', '-document_count')")] string? ordering = null)
{ {
var result = await client.GetDocumentTypesAsync(page, Math.Min(pageSize, 100), ordering); var result = await client.GetDocumentTypesAsync(page, Math.Min(pageSize, 100), ordering).ConfigureAwait(false);
var response = McpResponse<object>.Success( var response = McpResponse<object>.Success(
result.Results, result.Results,
@@ -43,7 +44,7 @@ public static class DocumentTypeTools
PaperlessClient client, PaperlessClient client,
[Description("Document type ID")] int id) [Description("Document type ID")] int id)
{ {
var documentType = await client.GetDocumentTypeAsync(id); var documentType = await client.GetDocumentTypeAsync(id).ConfigureAwait(false);
if (documentType == null) if (documentType == null)
{ {
@@ -77,7 +78,7 @@ public static class DocumentTypeTools
MatchingAlgorithm = matchingAlgorithm MatchingAlgorithm = matchingAlgorithm
}; };
var documentType = await client.CreateDocumentTypeAsync(request); var documentType = await client.CreateDocumentTypeAsync(request).ConfigureAwait(false);
if (documentType == null) if (documentType == null)
{ {
@@ -112,7 +113,7 @@ public static class DocumentTypeTools
MatchingAlgorithm = matchingAlgorithm MatchingAlgorithm = matchingAlgorithm
}; };
var documentType = await client.UpdateDocumentTypeAsync(id, request); var documentType = await client.UpdateDocumentTypeAsync(id, request).ConfigureAwait(false);
if (documentType == null) if (documentType == null)
{ {
@@ -140,7 +141,7 @@ public static class DocumentTypeTools
{ {
if (!confirm) if (!confirm)
{ {
var documentType = await client.GetDocumentTypeAsync(id); var documentType = await client.GetDocumentTypeAsync(id).ConfigureAwait(false);
if (documentType == null) if (documentType == null)
{ {
@@ -161,7 +162,7 @@ public static class DocumentTypeTools
return JsonSerializer.Serialize(dryRunResponse); return JsonSerializer.Serialize(dryRunResponse);
} }
var success = await client.DeleteDocumentTypeAsync(id); var success = await client.DeleteDocumentTypeAsync(id).ConfigureAwait(false);
if (!success) if (!success)
{ {
@@ -219,7 +220,7 @@ public static class DocumentTypeTools
return JsonSerializer.Serialize(dryRunResponse); return JsonSerializer.Serialize(dryRunResponse);
} }
var success = await client.BulkEditObjectsAsync(ids, "document_types", "delete"); var success = await client.BulkEditObjectsAsync(ids, "document_types", "delete").ConfigureAwait(false);
if (!success) if (!success)
{ {
@@ -244,15 +245,4 @@ public static class DocumentTypeTools
return JsonSerializer.Serialize(response); return JsonSerializer.Serialize(response);
} }
private static int[]? ParseIntArray(string? input)
{
if (string.IsNullOrWhiteSpace(input))
return null;
return input.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(s => int.TryParse(s, out var n) ? n : (int?)null)
.Where(n => n.HasValue)
.Select(n => n!.Value)
.ToArray();
}
} }
+3 -3
View File
@@ -16,7 +16,7 @@ public static class HealthTools
[Description("Verify connectivity and authentication with the Paperless-ngx instance. Returns server version if available.")] [Description("Verify connectivity and authentication with the Paperless-ngx instance. Returns server version if available.")]
public static async Task<string> Ping(PaperlessClient client) public static async Task<string> Ping(PaperlessClient client)
{ {
var (success, version, error) = await client.PingAsync(); var (success, version, error) = await client.PingAsync().ConfigureAwait(false);
if (success) if (success)
{ {
@@ -39,8 +39,8 @@ public static class HealthTools
[Description("Return supported API endpoints and detected Paperless-ngx version information.")] [Description("Return supported API endpoints and detected Paperless-ngx version information.")]
public static async Task<string> GetCapabilities(PaperlessClient client) public static async Task<string> GetCapabilities(PaperlessClient client)
{ {
var (pingSuccess, version, _) = await client.PingAsync(); var (pingSuccess, version, _) = await client.PingAsync().ConfigureAwait(false);
var (statusSuccess, status, _) = await client.GetStatusAsync(); var (statusSuccess, status, _) = await client.GetStatusAsync().ConfigureAwait(false);
var capabilities = new var capabilities = new
{ {
+8 -18
View File
@@ -4,6 +4,7 @@ using ModelContextProtocol.Server;
using PaperlessMCP.Client; using PaperlessMCP.Client;
using PaperlessMCP.Models.Common; using PaperlessMCP.Models.Common;
using PaperlessMCP.Models.StoragePaths; using PaperlessMCP.Models.StoragePaths;
using static PaperlessMCP.Utils.ParsingHelpers;
namespace PaperlessMCP.Tools; namespace PaperlessMCP.Tools;
@@ -21,7 +22,7 @@ public static class StoragePathTools
[Description("Page size (default: 25, max: 100)")] int pageSize = 25, [Description("Page size (default: 25, max: 100)")] int pageSize = 25,
[Description("Ordering field (e.g., 'name', '-document_count')")] string? ordering = null) [Description("Ordering field (e.g., 'name', '-document_count')")] string? ordering = null)
{ {
var result = await client.GetStoragePathsAsync(page, Math.Min(pageSize, 100), ordering); var result = await client.GetStoragePathsAsync(page, Math.Min(pageSize, 100), ordering).ConfigureAwait(false);
var response = McpResponse<object>.Success( var response = McpResponse<object>.Success(
result.Results, result.Results,
@@ -43,7 +44,7 @@ public static class StoragePathTools
PaperlessClient client, PaperlessClient client,
[Description("Storage path ID")] int id) [Description("Storage path ID")] int id)
{ {
var storagePath = await client.GetStoragePathAsync(id); var storagePath = await client.GetStoragePathAsync(id).ConfigureAwait(false);
if (storagePath == null) if (storagePath == null)
{ {
@@ -79,7 +80,7 @@ public static class StoragePathTools
MatchingAlgorithm = matchingAlgorithm MatchingAlgorithm = matchingAlgorithm
}; };
var storagePath = await client.CreateStoragePathAsync(request); var storagePath = await client.CreateStoragePathAsync(request).ConfigureAwait(false);
if (storagePath == null) if (storagePath == null)
{ {
@@ -116,7 +117,7 @@ public static class StoragePathTools
MatchingAlgorithm = matchingAlgorithm MatchingAlgorithm = matchingAlgorithm
}; };
var storagePath = await client.UpdateStoragePathAsync(id, request); var storagePath = await client.UpdateStoragePathAsync(id, request).ConfigureAwait(false);
if (storagePath == null) if (storagePath == null)
{ {
@@ -144,7 +145,7 @@ public static class StoragePathTools
{ {
if (!confirm) if (!confirm)
{ {
var storagePath = await client.GetStoragePathAsync(id); var storagePath = await client.GetStoragePathAsync(id).ConfigureAwait(false);
if (storagePath == null) if (storagePath == null)
{ {
@@ -165,7 +166,7 @@ public static class StoragePathTools
return JsonSerializer.Serialize(dryRunResponse); return JsonSerializer.Serialize(dryRunResponse);
} }
var success = await client.DeleteStoragePathAsync(id); var success = await client.DeleteStoragePathAsync(id).ConfigureAwait(false);
if (!success) if (!success)
{ {
@@ -223,7 +224,7 @@ public static class StoragePathTools
return JsonSerializer.Serialize(dryRunResponse); return JsonSerializer.Serialize(dryRunResponse);
} }
var success = await client.BulkEditObjectsAsync(ids, "storage_paths", "delete"); var success = await client.BulkEditObjectsAsync(ids, "storage_paths", "delete").ConfigureAwait(false);
if (!success) if (!success)
{ {
@@ -248,15 +249,4 @@ public static class StoragePathTools
return JsonSerializer.Serialize(response); return JsonSerializer.Serialize(response);
} }
private static int[]? ParseIntArray(string? input)
{
if (string.IsNullOrWhiteSpace(input))
return null;
return input.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(s => int.TryParse(s, out var n) ? n : (int?)null)
.Where(n => n.HasValue)
.Select(n => n!.Value)
.ToArray();
}
} }
+8 -18
View File
@@ -4,6 +4,7 @@ using ModelContextProtocol.Server;
using PaperlessMCP.Client; using PaperlessMCP.Client;
using PaperlessMCP.Models.Common; using PaperlessMCP.Models.Common;
using PaperlessMCP.Models.Tags; using PaperlessMCP.Models.Tags;
using static PaperlessMCP.Utils.ParsingHelpers;
namespace PaperlessMCP.Tools; namespace PaperlessMCP.Tools;
@@ -21,7 +22,7 @@ public static class TagTools
[Description("Page size (default: 25, max: 100)")] int pageSize = 25, [Description("Page size (default: 25, max: 100)")] int pageSize = 25,
[Description("Ordering field (e.g., 'name', '-document_count')")] string? ordering = null) [Description("Ordering field (e.g., 'name', '-document_count')")] string? ordering = null)
{ {
var result = await client.GetTagsAsync(page, Math.Min(pageSize, 100), ordering); var result = await client.GetTagsAsync(page, Math.Min(pageSize, 100), ordering).ConfigureAwait(false);
var response = McpResponse<object>.Success( var response = McpResponse<object>.Success(
result.Results, result.Results,
@@ -43,7 +44,7 @@ public static class TagTools
PaperlessClient client, PaperlessClient client,
[Description("Tag ID")] int id) [Description("Tag ID")] int id)
{ {
var tag = await client.GetTagAsync(id); var tag = await client.GetTagAsync(id).ConfigureAwait(false);
if (tag == null) if (tag == null)
{ {
@@ -81,7 +82,7 @@ public static class TagTools
IsInboxTag = isInboxTag IsInboxTag = isInboxTag
}; };
var tag = await client.CreateTagAsync(request); var tag = await client.CreateTagAsync(request).ConfigureAwait(false);
if (tag == null) if (tag == null)
{ {
@@ -120,7 +121,7 @@ public static class TagTools
IsInboxTag = isInboxTag IsInboxTag = isInboxTag
}; };
var tag = await client.UpdateTagAsync(id, request); var tag = await client.UpdateTagAsync(id, request).ConfigureAwait(false);
if (tag == null) if (tag == null)
{ {
@@ -148,7 +149,7 @@ public static class TagTools
{ {
if (!confirm) if (!confirm)
{ {
var tag = await client.GetTagAsync(id); var tag = await client.GetTagAsync(id).ConfigureAwait(false);
if (tag == null) if (tag == null)
{ {
@@ -169,7 +170,7 @@ public static class TagTools
return JsonSerializer.Serialize(dryRunResponse); return JsonSerializer.Serialize(dryRunResponse);
} }
var success = await client.DeleteTagAsync(id); var success = await client.DeleteTagAsync(id).ConfigureAwait(false);
if (!success) if (!success)
{ {
@@ -227,7 +228,7 @@ public static class TagTools
return JsonSerializer.Serialize(dryRunResponse); return JsonSerializer.Serialize(dryRunResponse);
} }
var success = await client.BulkEditObjectsAsync(ids, "tags", "delete"); var success = await client.BulkEditObjectsAsync(ids, "tags", "delete").ConfigureAwait(false);
if (!success) if (!success)
{ {
@@ -252,15 +253,4 @@ public static class TagTools
return JsonSerializer.Serialize(response); return JsonSerializer.Serialize(response);
} }
private static int[]? ParseIntArray(string? input)
{
if (string.IsNullOrWhiteSpace(input))
return null;
return input.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(s => int.TryParse(s, out var n) ? n : (int?)null)
.Where(n => n.HasValue)
.Select(n => n!.Value)
.ToArray();
}
} }
+37
View File
@@ -0,0 +1,37 @@
namespace PaperlessMCP.Utils;
/// <summary>
/// Shared parsing utilities for MCP tool parameters.
/// </summary>
public static class ParsingHelpers
{
/// <summary>
/// Parses a comma-separated string of integers into an array.
/// </summary>
/// <param name="input">Comma-separated integer values (e.g., "1,2,3")</param>
/// <returns>Array of parsed integers, or null if input is empty/whitespace</returns>
public static int[]? ParseIntArray(string? input)
{
if (string.IsNullOrWhiteSpace(input))
return null;
return input.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.Select(s => int.TryParse(s, out var n) ? n : (int?)null)
.Where(n => n.HasValue)
.Select(n => n!.Value)
.ToArray();
}
/// <summary>
/// Parses a date string into a DateTime.
/// </summary>
/// <param name="input">Date string in any standard format</param>
/// <returns>Parsed DateTime, or null if input is empty/invalid</returns>
public static DateTime? ParseDate(string? input)
{
if (string.IsNullOrWhiteSpace(input))
return null;
return DateTime.TryParse(input, out var date) ? date : null;
}
}