using System.ComponentModel; using System.Text.Json; using ModelContextProtocol.Server; using PaperlessMCP.Client; using PaperlessMCP.Models.Common; using PaperlessMCP.Models.Tags; namespace PaperlessMCP.Tools; /// /// MCP tools for tag operations. /// [McpServerToolType] public static class TagTools { [McpServerTool(Name = "paperless.tags.list")] [Description("List all tags with pagination.")] public static async Task List( PaperlessClient client, [Description("Page number (default: 1)")] int page = 1, [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 response = McpResponse.Success( result.Results, new McpMeta { Page = page, PageSize = pageSize, Total = result.Count, Next = result.Next, PaperlessBaseUrl = client.BaseUrl } ); return JsonSerializer.Serialize(response); } [McpServerTool(Name = "paperless.tags.get")] [Description("Get a tag by its ID.")] public static async Task Get( PaperlessClient client, [Description("Tag ID")] int id) { var tag = await client.GetTagAsync(id); if (tag == null) { var errorResponse = McpErrorResponse.Create( ErrorCodes.NotFound, $"Tag with ID {id} not found", meta: new McpMeta { PaperlessBaseUrl = client.BaseUrl } ); return JsonSerializer.Serialize(errorResponse); } var response = McpResponse.Success( tag, new McpMeta { PaperlessBaseUrl = client.BaseUrl } ); return JsonSerializer.Serialize(response); } [McpServerTool(Name = "paperless.tags.create")] [Description("Create a new tag.")] public static async Task Create( PaperlessClient client, [Description("Tag name")] string name, [Description("Hex color (e.g., '#ff0000')")] string? color = null, [Description("Match pattern for auto-tagging")] string? match = null, [Description("Matching algorithm (0=None, 1=Any, 2=All, 3=Literal, 4=Regex, 5=Fuzzy, 6=Auto)")] int? matchingAlgorithm = null, [Description("Is inbox tag")] bool? isInboxTag = null) { var request = new TagCreateRequest { Name = name, Color = color, Match = match, MatchingAlgorithm = matchingAlgorithm, IsInboxTag = isInboxTag }; var tag = await client.CreateTagAsync(request); if (tag == null) { var errorResponse = McpErrorResponse.Create( ErrorCodes.UpstreamError, "Failed to create tag", meta: new McpMeta { PaperlessBaseUrl = client.BaseUrl } ); return JsonSerializer.Serialize(errorResponse); } var response = McpResponse.Success( tag, new McpMeta { PaperlessBaseUrl = client.BaseUrl } ); return JsonSerializer.Serialize(response); } [McpServerTool(Name = "paperless.tags.update")] [Description("Update an existing tag.")] public static async Task Update( PaperlessClient client, [Description("Tag ID")] int id, [Description("New name (optional)")] string? name = null, [Description("Hex color (e.g., '#ff0000', optional)")] string? color = null, [Description("Match pattern (optional)")] string? match = null, [Description("Matching algorithm (optional)")] int? matchingAlgorithm = null, [Description("Is inbox tag (optional)")] bool? isInboxTag = null) { var request = new TagUpdateRequest { Name = name, Color = color, Match = match, MatchingAlgorithm = matchingAlgorithm, IsInboxTag = isInboxTag }; var tag = await client.UpdateTagAsync(id, request); if (tag == null) { var errorResponse = McpErrorResponse.Create( ErrorCodes.NotFound, $"Tag with ID {id} not found or update failed", meta: new McpMeta { PaperlessBaseUrl = client.BaseUrl } ); return JsonSerializer.Serialize(errorResponse); } var response = McpResponse.Success( tag, new McpMeta { PaperlessBaseUrl = client.BaseUrl } ); return JsonSerializer.Serialize(response); } [McpServerTool(Name = "paperless.tags.delete")] [Description("Delete a tag. Requires explicit confirmation.")] public static async Task Delete( PaperlessClient client, [Description("Tag ID")] int id, [Description("Must be true to confirm deletion")] bool confirm = false) { if (!confirm) { var tag = await client.GetTagAsync(id); if (tag == null) { var notFoundResponse = McpErrorResponse.Create( ErrorCodes.NotFound, $"Tag with ID {id} not found", meta: new McpMeta { PaperlessBaseUrl = client.BaseUrl } ); return JsonSerializer.Serialize(notFoundResponse); } var dryRunResponse = McpErrorResponse.Create( ErrorCodes.ConfirmationRequired, "Deletion requires confirm=true. This is a dry run showing what would be deleted.", new { tag_id = id, name = tag.Name, document_count = tag.DocumentCount }, new McpMeta { PaperlessBaseUrl = client.BaseUrl } ); return JsonSerializer.Serialize(dryRunResponse); } var success = await client.DeleteTagAsync(id); if (!success) { var errorResponse = McpErrorResponse.Create( ErrorCodes.UpstreamError, $"Failed to delete tag with ID {id}", meta: new McpMeta { PaperlessBaseUrl = client.BaseUrl } ); return JsonSerializer.Serialize(errorResponse); } var response = McpResponse.Success( new { deleted = true, tag_id = id }, new McpMeta { PaperlessBaseUrl = client.BaseUrl } ); return JsonSerializer.Serialize(response); } [McpServerTool(Name = "paperless.tags.bulk_delete")] [Description("Delete multiple tags. Supports dry run mode.")] public static async Task BulkDelete( PaperlessClient client, [Description("Tag IDs (comma-separated)")] string tagIds, [Description("Dry run mode - shows what would be deleted without applying")] bool dryRun = true, [Description("Must be true to execute the deletion")] bool confirm = false) { var ids = ParseIntArray(tagIds); if (ids == null || ids.Length == 0) { var errorResponse = McpErrorResponse.Create( ErrorCodes.Validation, "No valid tag IDs provided", meta: new McpMeta { PaperlessBaseUrl = client.BaseUrl } ); return JsonSerializer.Serialize(errorResponse); } if (dryRun || !confirm) { var dryRunResult = new BulkOperationResult { AffectedIds = ids, Warnings = new List { dryRun ? "This is a dry run. Set dry_run=false and confirm=true to execute." : "Set confirm=true to execute the operation." }, Executed = false }; var dryRunResponse = McpResponse.Success( dryRunResult, new McpMeta { PaperlessBaseUrl = client.BaseUrl } ); return JsonSerializer.Serialize(dryRunResponse); } var success = await client.BulkEditObjectsAsync(ids, "tags", "delete"); if (!success) { var errorResponse = McpErrorResponse.Create( ErrorCodes.UpstreamError, "Bulk delete operation failed", meta: new McpMeta { PaperlessBaseUrl = client.BaseUrl } ); return JsonSerializer.Serialize(errorResponse); } var result = new BulkOperationResult { AffectedIds = ids, Executed = true }; var response = McpResponse.Success( result, new McpMeta { PaperlessBaseUrl = client.BaseUrl } ); 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(); } }