diff --git a/PaperlessMCP.Tests/Tools/ToolNamingTests.cs b/PaperlessMCP.Tests/Tools/ToolNamingTests.cs new file mode 100644 index 0000000..d5e85e9 --- /dev/null +++ b/PaperlessMCP.Tests/Tools/ToolNamingTests.cs @@ -0,0 +1,53 @@ +using System.Reflection; +using System.Text.RegularExpressions; +using FluentAssertions; +using ModelContextProtocol.Server; +using Xunit; + +namespace PaperlessMCP.Tests.Tools; + +public class ToolNamingTests +{ + private static readonly Regex AnthropicToolNamePattern = new("^[a-zA-Z0-9_-]{1,64}$"); + + [Fact] + public void AllToolNames_ShouldMatchAnthropicApiNamingRules() + { + var toolAssembly = typeof(PaperlessMCP.Tools.HealthTools).Assembly; + + var toolNames = toolAssembly.GetTypes() + .SelectMany(t => t.GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance)) + .SelectMany(m => m.GetCustomAttributes()) + .Where(a => a.Name is not null) + .Select(a => a.Name!) + .ToList(); + + toolNames.Should().NotBeEmpty("expected to find at least one McpServerTool attribute"); + + var violations = toolNames + .Where(name => !AnthropicToolNamePattern.IsMatch(name)) + .ToList(); + + violations.Should().BeEmpty( + "tool names must match ^[a-zA-Z0-9_-]{{1,64}}$ per Anthropic API rules, " + + $"but found: {string.Join(", ", violations)}"); + } + + [Fact] + public void AllToolNames_ShouldNotContainDots() + { + var toolAssembly = typeof(PaperlessMCP.Tools.HealthTools).Assembly; + + var toolNames = toolAssembly.GetTypes() + .SelectMany(t => t.GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance)) + .SelectMany(m => m.GetCustomAttributes()) + .Where(a => a.Name is not null) + .Select(a => a.Name!) + .ToList(); + + var dotNames = toolNames.Where(name => name.Contains('.')).ToList(); + + dotNames.Should().BeEmpty( + $"dots are not allowed in tool names, but found: {string.Join(", ", dotNames)}"); + } +} diff --git a/PaperlessMCP/Tools/CorrespondentTools.cs b/PaperlessMCP/Tools/CorrespondentTools.cs index c94effb..16ffe93 100644 --- a/PaperlessMCP/Tools/CorrespondentTools.cs +++ b/PaperlessMCP/Tools/CorrespondentTools.cs @@ -14,7 +14,7 @@ namespace PaperlessMCP.Tools; [McpServerToolType] public static class CorrespondentTools { - [McpServerTool(Name = "paperless.correspondents.list")] + [McpServerTool(Name = "paperless_correspondents_list")] [Description("List all correspondents with pagination.")] public static async Task List( PaperlessClient client, @@ -38,7 +38,7 @@ public static class CorrespondentTools return JsonSerializer.Serialize(response); } - [McpServerTool(Name = "paperless.correspondents.get")] + [McpServerTool(Name = "paperless_correspondents_get")] [Description("Get a correspondent by its ID.")] public static async Task Get( PaperlessClient client, @@ -63,7 +63,7 @@ public static class CorrespondentTools return JsonSerializer.Serialize(response); } - [McpServerTool(Name = "paperless.correspondents.create")] + [McpServerTool(Name = "paperless_correspondents_create")] [Description("Create a new correspondent.")] public static async Task Create( PaperlessClient client, @@ -97,7 +97,7 @@ public static class CorrespondentTools return JsonSerializer.Serialize(response); } - [McpServerTool(Name = "paperless.correspondents.update")] + [McpServerTool(Name = "paperless_correspondents_update")] [Description("Update an existing correspondent.")] public static async Task Update( PaperlessClient client, @@ -132,7 +132,7 @@ public static class CorrespondentTools return JsonSerializer.Serialize(response); } - [McpServerTool(Name = "paperless.correspondents.delete")] + [McpServerTool(Name = "paperless_correspondents_delete")] [Description("Delete a correspondent. Requires explicit confirmation.")] public static async Task Delete( PaperlessClient client, @@ -181,7 +181,7 @@ public static class CorrespondentTools return JsonSerializer.Serialize(response); } - [McpServerTool(Name = "paperless.correspondents.bulk_delete")] + [McpServerTool(Name = "paperless_correspondents_bulk_delete")] [Description("Delete multiple correspondents. Supports dry run mode.")] public static async Task BulkDelete( PaperlessClient client, diff --git a/PaperlessMCP/Tools/CustomFieldTools.cs b/PaperlessMCP/Tools/CustomFieldTools.cs index 3f25190..bc92029 100644 --- a/PaperlessMCP/Tools/CustomFieldTools.cs +++ b/PaperlessMCP/Tools/CustomFieldTools.cs @@ -14,7 +14,7 @@ namespace PaperlessMCP.Tools; [McpServerToolType] public static class CustomFieldTools { - [McpServerTool(Name = "paperless.custom_fields.list")] + [McpServerTool(Name = "paperless_custom_fields_list")] [Description("List all custom field definitions with pagination.")] public static async Task List( PaperlessClient client, @@ -37,7 +37,7 @@ public static class CustomFieldTools return JsonSerializer.Serialize(response); } - [McpServerTool(Name = "paperless.custom_fields.get")] + [McpServerTool(Name = "paperless_custom_fields_get")] [Description("Get a custom field definition by its ID.")] public static async Task Get( PaperlessClient client, @@ -62,7 +62,7 @@ public static class CustomFieldTools return JsonSerializer.Serialize(response); } - [McpServerTool(Name = "paperless.custom_fields.create")] + [McpServerTool(Name = "paperless_custom_fields_create")] [Description("Create a new custom field definition.")] public static async Task Create( PaperlessClient client, @@ -114,7 +114,7 @@ public static class CustomFieldTools return JsonSerializer.Serialize(response); } - [McpServerTool(Name = "paperless.custom_fields.update")] + [McpServerTool(Name = "paperless_custom_fields_update")] [Description("Update an existing custom field definition.")] public static async Task Update( PaperlessClient client, @@ -165,7 +165,7 @@ public static class CustomFieldTools return JsonSerializer.Serialize(response); } - [McpServerTool(Name = "paperless.custom_fields.delete")] + [McpServerTool(Name = "paperless_custom_fields_delete")] [Description("Delete a custom field definition. Requires explicit confirmation.")] public static async Task Delete( PaperlessClient client, @@ -214,7 +214,7 @@ public static class CustomFieldTools return JsonSerializer.Serialize(response); } - [McpServerTool(Name = "paperless.custom_fields.assign")] + [McpServerTool(Name = "paperless_custom_fields_assign")] [Description("Assign a custom field value to a document.")] public static async Task Assign( PaperlessClient client, diff --git a/PaperlessMCP/Tools/DocumentTools.cs b/PaperlessMCP/Tools/DocumentTools.cs index e5040a8..a23c18d 100644 --- a/PaperlessMCP/Tools/DocumentTools.cs +++ b/PaperlessMCP/Tools/DocumentTools.cs @@ -14,7 +14,7 @@ namespace PaperlessMCP.Tools; [McpServerToolType] public static class DocumentTools { - [McpServerTool(Name = "paperless.documents.search")] + [McpServerTool(Name = "paperless_documents_search")] [Description("Search for documents with full-text search and filters. Supports pagination.")] public static async Task Search( PaperlessClient client, @@ -82,7 +82,7 @@ public static class DocumentTools return JsonSerializer.Serialize(response); } - [McpServerTool(Name = "paperless.documents.get")] + [McpServerTool(Name = "paperless_documents_get")] [Description("Get a document by its ID.")] public static async Task Get( PaperlessClient client, @@ -107,7 +107,7 @@ public static class DocumentTools return JsonSerializer.Serialize(response); } - [McpServerTool(Name = "paperless.documents.download")] + [McpServerTool(Name = "paperless_documents_download")] [Description("Get download URLs for a document's original file, preview, and thumbnail.")] public static async Task Download( PaperlessClient client, @@ -134,7 +134,7 @@ public static class DocumentTools return JsonSerializer.Serialize(response); } - [McpServerTool(Name = "paperless.documents.preview")] + [McpServerTool(Name = "paperless_documents_preview")] [Description("Get the preview URL for a document.")] public static async Task Preview( PaperlessClient client, @@ -161,7 +161,7 @@ public static class DocumentTools return JsonSerializer.Serialize(response); } - [McpServerTool(Name = "paperless.documents.thumbnail")] + [McpServerTool(Name = "paperless_documents_thumbnail")] [Description("Get the thumbnail URL for a document.")] public static async Task Thumbnail( PaperlessClient client, @@ -188,7 +188,7 @@ public static class DocumentTools return JsonSerializer.Serialize(response); } - [McpServerTool(Name = "paperless.documents.upload")] + [McpServerTool(Name = "paperless_documents_upload")] [Description("Upload a new document to Paperless-ngx. Provide file content as base64. For large files, use paperless.documents.upload_from_path instead.")] public static async Task Upload( PaperlessClient client, @@ -247,7 +247,7 @@ public static class DocumentTools return JsonSerializer.Serialize(response); } - [McpServerTool(Name = "paperless.documents.upload_from_path")] + [McpServerTool(Name = "paperless_documents_upload_from_path")] [Description("Upload a document from a local file path. More reliable than base64 upload for large files. Includes automatic retries.")] public static async Task UploadFromPath( PaperlessClient client, @@ -326,7 +326,7 @@ public static class DocumentTools return JsonSerializer.Serialize(response); } - [McpServerTool(Name = "paperless.documents.update")] + [McpServerTool(Name = "paperless_documents_update")] [Description("Update document metadata (title, correspondent, type, tags, etc.).")] public static async Task Update( PaperlessClient client, @@ -371,7 +371,7 @@ public static class DocumentTools return JsonSerializer.Serialize(response); } - [McpServerTool(Name = "paperless.documents.delete")] + [McpServerTool(Name = "paperless_documents_delete")] [Description("Delete a document. Requires explicit confirmation.")] public static async Task Delete( PaperlessClient client, @@ -427,7 +427,7 @@ public static class DocumentTools return JsonSerializer.Serialize(response); } - [McpServerTool(Name = "paperless.documents.bulk_update")] + [McpServerTool(Name = "paperless_documents_bulk_update")] [Description("Perform bulk operations on multiple documents. Supports dry run mode.")] public static async Task BulkUpdate( PaperlessClient client, @@ -513,7 +513,7 @@ public static class DocumentTools return JsonSerializer.Serialize(response); } - [McpServerTool(Name = "paperless.documents.reprocess")] + [McpServerTool(Name = "paperless_documents_reprocess")] [Description("Reprocess a document's OCR and content extraction.")] public static async Task Reprocess( PaperlessClient client, diff --git a/PaperlessMCP/Tools/DocumentTypeTools.cs b/PaperlessMCP/Tools/DocumentTypeTools.cs index 9bf14e4..3393f26 100644 --- a/PaperlessMCP/Tools/DocumentTypeTools.cs +++ b/PaperlessMCP/Tools/DocumentTypeTools.cs @@ -14,7 +14,7 @@ namespace PaperlessMCP.Tools; [McpServerToolType] public static class DocumentTypeTools { - [McpServerTool(Name = "paperless.document_types.list")] + [McpServerTool(Name = "paperless_document_types_list")] [Description("List all document types with pagination.")] public static async Task List( PaperlessClient client, @@ -38,7 +38,7 @@ public static class DocumentTypeTools return JsonSerializer.Serialize(response); } - [McpServerTool(Name = "paperless.document_types.get")] + [McpServerTool(Name = "paperless_document_types_get")] [Description("Get a document type by its ID.")] public static async Task Get( PaperlessClient client, @@ -63,7 +63,7 @@ public static class DocumentTypeTools return JsonSerializer.Serialize(response); } - [McpServerTool(Name = "paperless.document_types.create")] + [McpServerTool(Name = "paperless_document_types_create")] [Description("Create a new document type.")] public static async Task Create( PaperlessClient client, @@ -97,7 +97,7 @@ public static class DocumentTypeTools return JsonSerializer.Serialize(response); } - [McpServerTool(Name = "paperless.document_types.update")] + [McpServerTool(Name = "paperless_document_types_update")] [Description("Update an existing document type.")] public static async Task Update( PaperlessClient client, @@ -132,7 +132,7 @@ public static class DocumentTypeTools return JsonSerializer.Serialize(response); } - [McpServerTool(Name = "paperless.document_types.delete")] + [McpServerTool(Name = "paperless_document_types_delete")] [Description("Delete a document type. Requires explicit confirmation.")] public static async Task Delete( PaperlessClient client, @@ -181,7 +181,7 @@ public static class DocumentTypeTools return JsonSerializer.Serialize(response); } - [McpServerTool(Name = "paperless.document_types.bulk_delete")] + [McpServerTool(Name = "paperless_document_types_bulk_delete")] [Description("Delete multiple document types. Supports dry run mode.")] public static async Task BulkDelete( PaperlessClient client, diff --git a/PaperlessMCP/Tools/HealthTools.cs b/PaperlessMCP/Tools/HealthTools.cs index 0986e73..aada00c 100644 --- a/PaperlessMCP/Tools/HealthTools.cs +++ b/PaperlessMCP/Tools/HealthTools.cs @@ -12,7 +12,7 @@ namespace PaperlessMCP.Tools; [McpServerToolType] public static class HealthTools { - [McpServerTool(Name = "paperless.ping")] + [McpServerTool(Name = "paperless_ping")] [Description("Verify connectivity and authentication with the Paperless-ngx instance. Returns server version if available.")] public static async Task Ping(PaperlessClient client) { @@ -35,7 +35,7 @@ public static class HealthTools return JsonSerializer.Serialize(errorResponse); } - [McpServerTool(Name = "paperless.capabilities")] + [McpServerTool(Name = "paperless_capabilities")] [Description("Return supported API endpoints and detected Paperless-ngx version information.")] public static async Task GetCapabilities(PaperlessClient client) { diff --git a/PaperlessMCP/Tools/StoragePathTools.cs b/PaperlessMCP/Tools/StoragePathTools.cs index 2d4ee9f..dd0565c 100644 --- a/PaperlessMCP/Tools/StoragePathTools.cs +++ b/PaperlessMCP/Tools/StoragePathTools.cs @@ -14,7 +14,7 @@ namespace PaperlessMCP.Tools; [McpServerToolType] public static class StoragePathTools { - [McpServerTool(Name = "paperless.storage_paths.list")] + [McpServerTool(Name = "paperless_storage_paths_list")] [Description("List all storage paths with pagination.")] public static async Task List( PaperlessClient client, @@ -38,7 +38,7 @@ public static class StoragePathTools return JsonSerializer.Serialize(response); } - [McpServerTool(Name = "paperless.storage_paths.get")] + [McpServerTool(Name = "paperless_storage_paths_get")] [Description("Get a storage path by its ID.")] public static async Task Get( PaperlessClient client, @@ -63,7 +63,7 @@ public static class StoragePathTools return JsonSerializer.Serialize(response); } - [McpServerTool(Name = "paperless.storage_paths.create")] + [McpServerTool(Name = "paperless_storage_paths_create")] [Description("Create a new storage path.")] public static async Task Create( PaperlessClient client, @@ -99,7 +99,7 @@ public static class StoragePathTools return JsonSerializer.Serialize(response); } - [McpServerTool(Name = "paperless.storage_paths.update")] + [McpServerTool(Name = "paperless_storage_paths_update")] [Description("Update an existing storage path.")] public static async Task Update( PaperlessClient client, @@ -136,7 +136,7 @@ public static class StoragePathTools return JsonSerializer.Serialize(response); } - [McpServerTool(Name = "paperless.storage_paths.delete")] + [McpServerTool(Name = "paperless_storage_paths_delete")] [Description("Delete a storage path. Requires explicit confirmation.")] public static async Task Delete( PaperlessClient client, @@ -185,7 +185,7 @@ public static class StoragePathTools return JsonSerializer.Serialize(response); } - [McpServerTool(Name = "paperless.storage_paths.bulk_delete")] + [McpServerTool(Name = "paperless_storage_paths_bulk_delete")] [Description("Delete multiple storage paths. Supports dry run mode.")] public static async Task BulkDelete( PaperlessClient client, diff --git a/PaperlessMCP/Tools/TagTools.cs b/PaperlessMCP/Tools/TagTools.cs index e475ec3..72e8f69 100644 --- a/PaperlessMCP/Tools/TagTools.cs +++ b/PaperlessMCP/Tools/TagTools.cs @@ -14,7 +14,7 @@ namespace PaperlessMCP.Tools; [McpServerToolType] public static class TagTools { - [McpServerTool(Name = "paperless.tags.list")] + [McpServerTool(Name = "paperless_tags_list")] [Description("List all tags with pagination.")] public static async Task List( PaperlessClient client, @@ -38,7 +38,7 @@ public static class TagTools return JsonSerializer.Serialize(response); } - [McpServerTool(Name = "paperless.tags.get")] + [McpServerTool(Name = "paperless_tags_get")] [Description("Get a tag by its ID.")] public static async Task Get( PaperlessClient client, @@ -63,7 +63,7 @@ public static class TagTools return JsonSerializer.Serialize(response); } - [McpServerTool(Name = "paperless.tags.create")] + [McpServerTool(Name = "paperless_tags_create")] [Description("Create a new tag.")] public static async Task Create( PaperlessClient client, @@ -103,7 +103,7 @@ public static class TagTools return JsonSerializer.Serialize(response); } - [McpServerTool(Name = "paperless.tags.update")] + [McpServerTool(Name = "paperless_tags_update")] [Description("Update an existing tag.")] public static async Task Update( PaperlessClient client, @@ -142,7 +142,7 @@ public static class TagTools return JsonSerializer.Serialize(response); } - [McpServerTool(Name = "paperless.tags.delete")] + [McpServerTool(Name = "paperless_tags_delete")] [Description("Delete a tag. Requires explicit confirmation.")] public static async Task Delete( PaperlessClient client, @@ -191,7 +191,7 @@ public static class TagTools return JsonSerializer.Serialize(response); } - [McpServerTool(Name = "paperless.tags.bulk_delete")] + [McpServerTool(Name = "paperless_tags_bulk_delete")] [Description("Delete multiple tags. Supports dry run mode.")] public static async Task BulkDelete( PaperlessClient client,