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:
@@ -118,3 +118,22 @@ steps:
|
||||
https://api.github.com/repos/barryw/PaperlessMCP/releases \
|
||||
-d "{\"tag_name\":\"$$TAG\",\"name\":\"Release $$VERSION\",\"body\":\"Release $$VERSION\",\"draft\":false,\"prerelease\":false}"
|
||||
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]
|
||||
|
||||
@@ -48,12 +48,12 @@ public class PaperlessClient
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync("api/status/", cancellationToken);
|
||||
var response = await _httpClient.GetAsync("api/status/", cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
// 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 version = json.TryGetProperty("pngx_version", out var versionProp)
|
||||
? versionProp.GetString()
|
||||
@@ -78,11 +78,11 @@ public class PaperlessClient
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync("api/status/", cancellationToken);
|
||||
var response = await _httpClient.GetAsync("api/status/", cancellationToken).ConfigureAwait(false);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ public class PaperlessClient
|
||||
queryParams["ordering"] = ordering;
|
||||
|
||||
var url = $"api/documents/?{queryParams}";
|
||||
return await GetAsync<PaginatedResult<DocumentSearchResult>>(url, cancellationToken)
|
||||
return await GetAsync<PaginatedResult<DocumentSearchResult>>(url, cancellationToken).ConfigureAwait(false)
|
||||
?? new PaginatedResult<DocumentSearchResult>();
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ public class PaperlessClient
|
||||
/// </summary>
|
||||
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>
|
||||
@@ -180,7 +180,7 @@ public class PaperlessClient
|
||||
/// </summary>
|
||||
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>
|
||||
@@ -188,7 +188,7 @@ public class PaperlessClient
|
||||
/// </summary>
|
||||
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>
|
||||
@@ -204,7 +204,7 @@ public class PaperlessClient
|
||||
() => new ByteArrayContent(fileContent),
|
||||
fileName,
|
||||
metadata,
|
||||
cancellationToken);
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -247,7 +247,7 @@ public class PaperlessClient
|
||||
fileName,
|
||||
metadata,
|
||||
cancellationToken,
|
||||
disposeContent: false); // StreamContent owns the stream
|
||||
disposeContent: false).ConfigureAwait(false); // StreamContent owns the stream
|
||||
|
||||
if (taskId != null)
|
||||
{
|
||||
@@ -262,18 +262,18 @@ public class PaperlessClient
|
||||
{
|
||||
var delay = TimeSpan.FromSeconds(Math.Pow(2, attempt)); // Exponential backoff
|
||||
_logger.LogInformation("Retrying in {Delay}...", delay);
|
||||
await Task.Delay(delay, cancellationToken);
|
||||
await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (IOException ex) when (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)
|
||||
{
|
||||
_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)
|
||||
{
|
||||
@@ -294,10 +294,12 @@ public class PaperlessClient
|
||||
{
|
||||
using var formContent = new MultipartFormDataContent();
|
||||
var fileContent = contentFactory();
|
||||
var addedToForm = false;
|
||||
|
||||
try
|
||||
{
|
||||
formContent.Add(fileContent, "document", fileName);
|
||||
addedToForm = true;
|
||||
|
||||
if (metadata != null)
|
||||
{
|
||||
@@ -327,21 +329,23 @@ public class PaperlessClient
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
|
||||
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)
|
||||
{
|
||||
var result = await response.Content.ReadAsStringAsync(cts.Token);
|
||||
var result = await response.Content.ReadAsStringAsync(cts.Token).ConfigureAwait(false);
|
||||
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);
|
||||
return null;
|
||||
}
|
||||
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();
|
||||
}
|
||||
@@ -383,7 +387,7 @@ public class PaperlessClient
|
||||
|
||||
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;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -398,7 +402,7 @@ public class PaperlessClient
|
||||
/// </summary>
|
||||
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
|
||||
@@ -413,28 +417,28 @@ public class PaperlessClient
|
||||
if (!string.IsNullOrEmpty(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>();
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
return await DeleteAsync($"api/tags/{id}/", cancellationToken);
|
||||
return await DeleteAsync($"api/tags/{id}/", cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -449,28 +453,28 @@ public class PaperlessClient
|
||||
if (!string.IsNullOrEmpty(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>();
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
return await DeleteAsync($"api/correspondents/{id}/", cancellationToken);
|
||||
return await DeleteAsync($"api/correspondents/{id}/", cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -485,28 +489,28 @@ public class PaperlessClient
|
||||
if (!string.IsNullOrEmpty(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>();
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
return await DeleteAsync($"api/document_types/{id}/", cancellationToken);
|
||||
return await DeleteAsync($"api/document_types/{id}/", cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -521,28 +525,28 @@ public class PaperlessClient
|
||||
if (!string.IsNullOrEmpty(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>();
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
return await DeleteAsync($"api/storage_paths/{id}/", cancellationToken);
|
||||
return await DeleteAsync($"api/storage_paths/{id}/", cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -555,28 +559,28 @@ public class PaperlessClient
|
||||
queryParams["page"] = page.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>();
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
return await DeleteAsync($"api/custom_fields/{id}/", cancellationToken);
|
||||
return await DeleteAsync($"api/custom_fields/{id}/", cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -603,7 +607,7 @@ public class PaperlessClient
|
||||
|
||||
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;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -621,14 +625,14 @@ public class PaperlessClient
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync(url, cancellationToken);
|
||||
var response = await _httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
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;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -642,14 +646,14 @@ public class PaperlessClient
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.PostAsJsonAsync(url, request, JsonOptions, cancellationToken);
|
||||
var response = await _httpClient.PostAsJsonAsync(url, request, JsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
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;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -664,14 +668,14 @@ public class PaperlessClient
|
||||
try
|
||||
{
|
||||
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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -685,14 +689,14 @@ public class PaperlessClient
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.DeleteAsync(url, cancellationToken);
|
||||
var response = await _httpClient.DeleteAsync(url, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (response.IsSuccessStatusCode || response.StatusCode == HttpStatusCode.NoContent)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
await LogErrorResponse(response, "DELETE", url);
|
||||
await LogErrorResponse(response, "DELETE", url).ConfigureAwait(false);
|
||||
return false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -704,7 +708,7 @@ public class PaperlessClient
|
||||
|
||||
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}",
|
||||
method, url, (int)response.StatusCode, body);
|
||||
}
|
||||
|
||||
@@ -19,33 +19,6 @@ public record PaginatedResult<T>
|
||||
|
||||
[JsonPropertyName("results")]
|
||||
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>
|
||||
|
||||
@@ -87,6 +87,7 @@ void ConfigureServices(IServiceCollection services, IConfiguration configuration
|
||||
var options = sp.GetRequiredService<IOptions<PaperlessOptions>>().Value;
|
||||
client.BaseAddress = new Uri(options.BaseUrl.TrimEnd('/') + "/");
|
||||
client.DefaultRequestHeaders.Add("Accept", "application/json; version=9");
|
||||
client.Timeout = TimeSpan.FromSeconds(30);
|
||||
})
|
||||
.AddHttpMessageHandler<PaperlessAuthHandler>()
|
||||
.AddPolicyHandler(retryPolicy);
|
||||
|
||||
@@ -4,6 +4,7 @@ using ModelContextProtocol.Server;
|
||||
using PaperlessMCP.Client;
|
||||
using PaperlessMCP.Models.Common;
|
||||
using PaperlessMCP.Models.Correspondents;
|
||||
using static PaperlessMCP.Utils.ParsingHelpers;
|
||||
|
||||
namespace PaperlessMCP.Tools;
|
||||
|
||||
@@ -21,7 +22,7 @@ public static class CorrespondentTools
|
||||
[Description("Page size (default: 25, max: 100)")] int pageSize = 25,
|
||||
[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(
|
||||
result.Results,
|
||||
@@ -43,7 +44,7 @@ public static class CorrespondentTools
|
||||
PaperlessClient client,
|
||||
[Description("Correspondent ID")] int id)
|
||||
{
|
||||
var correspondent = await client.GetCorrespondentAsync(id);
|
||||
var correspondent = await client.GetCorrespondentAsync(id).ConfigureAwait(false);
|
||||
|
||||
if (correspondent == null)
|
||||
{
|
||||
@@ -77,7 +78,7 @@ public static class CorrespondentTools
|
||||
MatchingAlgorithm = matchingAlgorithm
|
||||
};
|
||||
|
||||
var correspondent = await client.CreateCorrespondentAsync(request);
|
||||
var correspondent = await client.CreateCorrespondentAsync(request).ConfigureAwait(false);
|
||||
|
||||
if (correspondent == null)
|
||||
{
|
||||
@@ -112,7 +113,7 @@ public static class CorrespondentTools
|
||||
MatchingAlgorithm = matchingAlgorithm
|
||||
};
|
||||
|
||||
var correspondent = await client.UpdateCorrespondentAsync(id, request);
|
||||
var correspondent = await client.UpdateCorrespondentAsync(id, request).ConfigureAwait(false);
|
||||
|
||||
if (correspondent == null)
|
||||
{
|
||||
@@ -140,7 +141,7 @@ public static class CorrespondentTools
|
||||
{
|
||||
if (!confirm)
|
||||
{
|
||||
var correspondent = await client.GetCorrespondentAsync(id);
|
||||
var correspondent = await client.GetCorrespondentAsync(id).ConfigureAwait(false);
|
||||
|
||||
if (correspondent == null)
|
||||
{
|
||||
@@ -161,7 +162,7 @@ public static class CorrespondentTools
|
||||
return JsonSerializer.Serialize(dryRunResponse);
|
||||
}
|
||||
|
||||
var success = await client.DeleteCorrespondentAsync(id);
|
||||
var success = await client.DeleteCorrespondentAsync(id).ConfigureAwait(false);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
@@ -219,7 +220,7 @@ public static class CorrespondentTools
|
||||
return JsonSerializer.Serialize(dryRunResponse);
|
||||
}
|
||||
|
||||
var success = await client.BulkEditObjectsAsync(ids, "correspondents", "delete");
|
||||
var success = await client.BulkEditObjectsAsync(ids, "correspondents", "delete").ConfigureAwait(false);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
@@ -244,15 +245,4 @@ public static class CorrespondentTools
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ public static class CustomFieldTools
|
||||
[Description("Page number (default: 1)")] int page = 1,
|
||||
[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(
|
||||
result.Results,
|
||||
@@ -43,7 +43,7 @@ public static class CustomFieldTools
|
||||
PaperlessClient client,
|
||||
[Description("Custom field ID")] int id)
|
||||
{
|
||||
var customField = await client.GetCustomFieldAsync(id);
|
||||
var customField = await client.GetCustomFieldAsync(id).ConfigureAwait(false);
|
||||
|
||||
if (customField == null)
|
||||
{
|
||||
@@ -95,7 +95,7 @@ public static class CustomFieldTools
|
||||
ExtraData = extraData
|
||||
};
|
||||
|
||||
var customField = await client.CreateCustomFieldAsync(request);
|
||||
var customField = await client.CreateCustomFieldAsync(request).ConfigureAwait(false);
|
||||
|
||||
if (customField == null)
|
||||
{
|
||||
@@ -146,7 +146,7 @@ public static class CustomFieldTools
|
||||
ExtraData = extraData
|
||||
};
|
||||
|
||||
var customField = await client.UpdateCustomFieldAsync(id, request);
|
||||
var customField = await client.UpdateCustomFieldAsync(id, request).ConfigureAwait(false);
|
||||
|
||||
if (customField == null)
|
||||
{
|
||||
@@ -174,7 +174,7 @@ public static class CustomFieldTools
|
||||
{
|
||||
if (!confirm)
|
||||
{
|
||||
var customField = await client.GetCustomFieldAsync(id);
|
||||
var customField = await client.GetCustomFieldAsync(id).ConfigureAwait(false);
|
||||
|
||||
if (customField == null)
|
||||
{
|
||||
@@ -195,7 +195,7 @@ public static class CustomFieldTools
|
||||
return JsonSerializer.Serialize(dryRunResponse);
|
||||
}
|
||||
|
||||
var success = await client.DeleteCustomFieldAsync(id);
|
||||
var success = await client.DeleteCustomFieldAsync(id).ConfigureAwait(false);
|
||||
|
||||
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)
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
@@ -236,7 +236,7 @@ public static class CustomFieldTools
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
@@ -277,7 +277,7 @@ public static class CustomFieldTools
|
||||
CustomFields = customFields
|
||||
};
|
||||
|
||||
var updatedDocument = await client.UpdateDocumentAsync(documentId, updateRequest);
|
||||
var updatedDocument = await client.UpdateDocumentAsync(documentId, updateRequest).ConfigureAwait(false);
|
||||
|
||||
if (updatedDocument == null)
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ using ModelContextProtocol.Server;
|
||||
using PaperlessMCP.Client;
|
||||
using PaperlessMCP.Models.Common;
|
||||
using PaperlessMCP.Models.Documents;
|
||||
using static PaperlessMCP.Utils.ParsingHelpers;
|
||||
|
||||
namespace PaperlessMCP.Tools;
|
||||
|
||||
@@ -57,7 +58,7 @@ public static class DocumentTools
|
||||
page: page,
|
||||
pageSize: Math.Min(pageSize, 100),
|
||||
ordering: ordering
|
||||
);
|
||||
).ConfigureAwait(false);
|
||||
|
||||
// Map to lightweight summaries to reduce response size
|
||||
var summaries = result.Results
|
||||
@@ -87,7 +88,7 @@ public static class DocumentTools
|
||||
PaperlessClient client,
|
||||
[Description("Document ID")] int id)
|
||||
{
|
||||
var document = await client.GetDocumentAsync(id);
|
||||
var document = await client.GetDocumentAsync(id).ConfigureAwait(false);
|
||||
|
||||
if (document == null)
|
||||
{
|
||||
@@ -112,7 +113,7 @@ public static class DocumentTools
|
||||
PaperlessClient client,
|
||||
[Description("Document ID")] int id)
|
||||
{
|
||||
var document = await client.GetDocumentAsync(id);
|
||||
var document = await client.GetDocumentAsync(id).ConfigureAwait(false);
|
||||
|
||||
if (document == null)
|
||||
{
|
||||
@@ -139,7 +140,7 @@ public static class DocumentTools
|
||||
PaperlessClient client,
|
||||
[Description("Document ID")] int id)
|
||||
{
|
||||
var document = await client.GetDocumentAsync(id);
|
||||
var document = await client.GetDocumentAsync(id).ConfigureAwait(false);
|
||||
|
||||
if (document == null)
|
||||
{
|
||||
@@ -166,7 +167,7 @@ public static class DocumentTools
|
||||
PaperlessClient client,
|
||||
[Description("Document ID")] int id)
|
||||
{
|
||||
var document = await client.GetDocumentAsync(id);
|
||||
var document = await client.GetDocumentAsync(id).ConfigureAwait(false);
|
||||
|
||||
if (document == null)
|
||||
{
|
||||
@@ -227,7 +228,7 @@ public static class DocumentTools
|
||||
Created = ParseDate(created)
|
||||
};
|
||||
|
||||
var taskId = await client.UploadDocumentAsync(fileBytes, fileName, metadata);
|
||||
var taskId = await client.UploadDocumentAsync(fileBytes, fileName, metadata).ConfigureAwait(false);
|
||||
|
||||
if (taskId == null)
|
||||
{
|
||||
@@ -299,7 +300,7 @@ public static class DocumentTools
|
||||
Created = ParseDate(created)
|
||||
};
|
||||
|
||||
var (taskId, error) = await client.UploadDocumentFromPathAsync(filePath, metadata);
|
||||
var (taskId, error) = await client.UploadDocumentFromPathAsync(filePath, metadata).ConfigureAwait(false);
|
||||
|
||||
if (taskId == null)
|
||||
{
|
||||
@@ -349,7 +350,7 @@ public static class DocumentTools
|
||||
Created = ParseDate(created)
|
||||
};
|
||||
|
||||
var document = await client.UpdateDocumentAsync(id, request);
|
||||
var document = await client.UpdateDocumentAsync(id, request).ConfigureAwait(false);
|
||||
|
||||
if (document == null)
|
||||
{
|
||||
@@ -378,7 +379,7 @@ public static class DocumentTools
|
||||
if (!confirm)
|
||||
{
|
||||
// Get document info for dry run
|
||||
var document = await client.GetDocumentAsync(id);
|
||||
var document = await client.GetDocumentAsync(id).ConfigureAwait(false);
|
||||
|
||||
if (document == null)
|
||||
{
|
||||
@@ -405,7 +406,7 @@ public static class DocumentTools
|
||||
return JsonSerializer.Serialize(dryRunResponse);
|
||||
}
|
||||
|
||||
var success = await client.DeleteDocumentAsync(id);
|
||||
var success = await client.DeleteDocumentAsync(id).ConfigureAwait(false);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
@@ -485,7 +486,7 @@ public static class DocumentTools
|
||||
_ => null
|
||||
};
|
||||
|
||||
var success = await client.BulkEditDocumentsAsync(ids, operation, parameters);
|
||||
var success = await client.BulkEditDocumentsAsync(ids, operation, parameters).ConfigureAwait(false);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
@@ -519,7 +520,7 @@ public static class DocumentTools
|
||||
{
|
||||
if (!confirm)
|
||||
{
|
||||
var document = await client.GetDocumentAsync(id);
|
||||
var document = await client.GetDocumentAsync(id).ConfigureAwait(false);
|
||||
|
||||
if (document == null)
|
||||
{
|
||||
@@ -540,7 +541,7 @@ public static class DocumentTools
|
||||
return JsonSerializer.Serialize(dryRunResponse);
|
||||
}
|
||||
|
||||
var success = await client.BulkEditDocumentsAsync(new[] { id }, "reprocess");
|
||||
var success = await client.BulkEditDocumentsAsync(new[] { id }, "reprocess").ConfigureAwait(false);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
@@ -559,23 +560,4 @@ public static class DocumentTools
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using ModelContextProtocol.Server;
|
||||
using PaperlessMCP.Client;
|
||||
using PaperlessMCP.Models.Common;
|
||||
using PaperlessMCP.Models.DocumentTypes;
|
||||
using static PaperlessMCP.Utils.ParsingHelpers;
|
||||
|
||||
namespace PaperlessMCP.Tools;
|
||||
|
||||
@@ -21,7 +22,7 @@ public static class DocumentTypeTools
|
||||
[Description("Page size (default: 25, max: 100)")] int pageSize = 25,
|
||||
[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(
|
||||
result.Results,
|
||||
@@ -43,7 +44,7 @@ public static class DocumentTypeTools
|
||||
PaperlessClient client,
|
||||
[Description("Document type ID")] int id)
|
||||
{
|
||||
var documentType = await client.GetDocumentTypeAsync(id);
|
||||
var documentType = await client.GetDocumentTypeAsync(id).ConfigureAwait(false);
|
||||
|
||||
if (documentType == null)
|
||||
{
|
||||
@@ -77,7 +78,7 @@ public static class DocumentTypeTools
|
||||
MatchingAlgorithm = matchingAlgorithm
|
||||
};
|
||||
|
||||
var documentType = await client.CreateDocumentTypeAsync(request);
|
||||
var documentType = await client.CreateDocumentTypeAsync(request).ConfigureAwait(false);
|
||||
|
||||
if (documentType == null)
|
||||
{
|
||||
@@ -112,7 +113,7 @@ public static class DocumentTypeTools
|
||||
MatchingAlgorithm = matchingAlgorithm
|
||||
};
|
||||
|
||||
var documentType = await client.UpdateDocumentTypeAsync(id, request);
|
||||
var documentType = await client.UpdateDocumentTypeAsync(id, request).ConfigureAwait(false);
|
||||
|
||||
if (documentType == null)
|
||||
{
|
||||
@@ -140,7 +141,7 @@ public static class DocumentTypeTools
|
||||
{
|
||||
if (!confirm)
|
||||
{
|
||||
var documentType = await client.GetDocumentTypeAsync(id);
|
||||
var documentType = await client.GetDocumentTypeAsync(id).ConfigureAwait(false);
|
||||
|
||||
if (documentType == null)
|
||||
{
|
||||
@@ -161,7 +162,7 @@ public static class DocumentTypeTools
|
||||
return JsonSerializer.Serialize(dryRunResponse);
|
||||
}
|
||||
|
||||
var success = await client.DeleteDocumentTypeAsync(id);
|
||||
var success = await client.DeleteDocumentTypeAsync(id).ConfigureAwait(false);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
@@ -219,7 +220,7 @@ public static class DocumentTypeTools
|
||||
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)
|
||||
{
|
||||
@@ -244,15 +245,4 @@ public static class DocumentTypeTools
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ public static class HealthTools
|
||||
[Description("Verify connectivity and authentication with the Paperless-ngx instance. Returns server version if available.")]
|
||||
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)
|
||||
{
|
||||
@@ -39,8 +39,8 @@ public static class HealthTools
|
||||
[Description("Return supported API endpoints and detected Paperless-ngx version information.")]
|
||||
public static async Task<string> GetCapabilities(PaperlessClient client)
|
||||
{
|
||||
var (pingSuccess, version, _) = await client.PingAsync();
|
||||
var (statusSuccess, status, _) = await client.GetStatusAsync();
|
||||
var (pingSuccess, version, _) = await client.PingAsync().ConfigureAwait(false);
|
||||
var (statusSuccess, status, _) = await client.GetStatusAsync().ConfigureAwait(false);
|
||||
|
||||
var capabilities = new
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ using ModelContextProtocol.Server;
|
||||
using PaperlessMCP.Client;
|
||||
using PaperlessMCP.Models.Common;
|
||||
using PaperlessMCP.Models.StoragePaths;
|
||||
using static PaperlessMCP.Utils.ParsingHelpers;
|
||||
|
||||
namespace PaperlessMCP.Tools;
|
||||
|
||||
@@ -21,7 +22,7 @@ public static class StoragePathTools
|
||||
[Description("Page size (default: 25, max: 100)")] int pageSize = 25,
|
||||
[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(
|
||||
result.Results,
|
||||
@@ -43,7 +44,7 @@ public static class StoragePathTools
|
||||
PaperlessClient client,
|
||||
[Description("Storage path ID")] int id)
|
||||
{
|
||||
var storagePath = await client.GetStoragePathAsync(id);
|
||||
var storagePath = await client.GetStoragePathAsync(id).ConfigureAwait(false);
|
||||
|
||||
if (storagePath == null)
|
||||
{
|
||||
@@ -79,7 +80,7 @@ public static class StoragePathTools
|
||||
MatchingAlgorithm = matchingAlgorithm
|
||||
};
|
||||
|
||||
var storagePath = await client.CreateStoragePathAsync(request);
|
||||
var storagePath = await client.CreateStoragePathAsync(request).ConfigureAwait(false);
|
||||
|
||||
if (storagePath == null)
|
||||
{
|
||||
@@ -116,7 +117,7 @@ public static class StoragePathTools
|
||||
MatchingAlgorithm = matchingAlgorithm
|
||||
};
|
||||
|
||||
var storagePath = await client.UpdateStoragePathAsync(id, request);
|
||||
var storagePath = await client.UpdateStoragePathAsync(id, request).ConfigureAwait(false);
|
||||
|
||||
if (storagePath == null)
|
||||
{
|
||||
@@ -144,7 +145,7 @@ public static class StoragePathTools
|
||||
{
|
||||
if (!confirm)
|
||||
{
|
||||
var storagePath = await client.GetStoragePathAsync(id);
|
||||
var storagePath = await client.GetStoragePathAsync(id).ConfigureAwait(false);
|
||||
|
||||
if (storagePath == null)
|
||||
{
|
||||
@@ -165,7 +166,7 @@ public static class StoragePathTools
|
||||
return JsonSerializer.Serialize(dryRunResponse);
|
||||
}
|
||||
|
||||
var success = await client.DeleteStoragePathAsync(id);
|
||||
var success = await client.DeleteStoragePathAsync(id).ConfigureAwait(false);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
@@ -223,7 +224,7 @@ public static class StoragePathTools
|
||||
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)
|
||||
{
|
||||
@@ -248,15 +249,4 @@ public static class StoragePathTools
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using ModelContextProtocol.Server;
|
||||
using PaperlessMCP.Client;
|
||||
using PaperlessMCP.Models.Common;
|
||||
using PaperlessMCP.Models.Tags;
|
||||
using static PaperlessMCP.Utils.ParsingHelpers;
|
||||
|
||||
namespace PaperlessMCP.Tools;
|
||||
|
||||
@@ -21,7 +22,7 @@ public static class TagTools
|
||||
[Description("Page size (default: 25, max: 100)")] int pageSize = 25,
|
||||
[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(
|
||||
result.Results,
|
||||
@@ -43,7 +44,7 @@ public static class TagTools
|
||||
PaperlessClient client,
|
||||
[Description("Tag ID")] int id)
|
||||
{
|
||||
var tag = await client.GetTagAsync(id);
|
||||
var tag = await client.GetTagAsync(id).ConfigureAwait(false);
|
||||
|
||||
if (tag == null)
|
||||
{
|
||||
@@ -81,7 +82,7 @@ public static class TagTools
|
||||
IsInboxTag = isInboxTag
|
||||
};
|
||||
|
||||
var tag = await client.CreateTagAsync(request);
|
||||
var tag = await client.CreateTagAsync(request).ConfigureAwait(false);
|
||||
|
||||
if (tag == null)
|
||||
{
|
||||
@@ -120,7 +121,7 @@ public static class TagTools
|
||||
IsInboxTag = isInboxTag
|
||||
};
|
||||
|
||||
var tag = await client.UpdateTagAsync(id, request);
|
||||
var tag = await client.UpdateTagAsync(id, request).ConfigureAwait(false);
|
||||
|
||||
if (tag == null)
|
||||
{
|
||||
@@ -148,7 +149,7 @@ public static class TagTools
|
||||
{
|
||||
if (!confirm)
|
||||
{
|
||||
var tag = await client.GetTagAsync(id);
|
||||
var tag = await client.GetTagAsync(id).ConfigureAwait(false);
|
||||
|
||||
if (tag == null)
|
||||
{
|
||||
@@ -169,7 +170,7 @@ public static class TagTools
|
||||
return JsonSerializer.Serialize(dryRunResponse);
|
||||
}
|
||||
|
||||
var success = await client.DeleteTagAsync(id);
|
||||
var success = await client.DeleteTagAsync(id).ConfigureAwait(false);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
@@ -227,7 +228,7 @@ public static class TagTools
|
||||
return JsonSerializer.Serialize(dryRunResponse);
|
||||
}
|
||||
|
||||
var success = await client.BulkEditObjectsAsync(ids, "tags", "delete");
|
||||
var success = await client.BulkEditObjectsAsync(ids, "tags", "delete").ConfigureAwait(false);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
@@ -252,15 +253,4 @@ public static class TagTools
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user