Initial commit: Paperless-ngx MCP Server

A Model Context Protocol (MCP) server for Paperless-ngx document management.

Features:
- Full CRUD operations for documents, tags, correspondents, document types,
  storage paths, and custom fields
- Document upload with retry logic (base64 and file path)
- Bulk operations with dry-run support
- Search with full-text and metadata filtering
- Pagination support across all list operations
- Proper error handling with McpResponse wrapper

Built with .NET 10 and the official MCP SDK.

🤖 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 14:01:44 -05:00
commit a37630aeac
37 changed files with 6638 additions and 0 deletions
@@ -0,0 +1,102 @@
using System.Net;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NSubstitute;
using PaperlessMCP.Client;
using PaperlessMCP.Configuration;
using RichardSzalay.MockHttp;
namespace PaperlessMCP.Tests.Fixtures;
public class MockHttpClientFactory : IDisposable
{
public MockHttpMessageHandler MockHandler { get; }
public HttpClient HttpClient { get; }
public PaperlessClient Client { get; }
public PaperlessOptions Options { get; }
public MockHttpClientFactory(string baseUrl = "https://paperless.example.com")
{
Options = new PaperlessOptions
{
BaseUrl = baseUrl,
ApiToken = "test-token",
MaxPageSize = 100
};
MockHandler = new MockHttpMessageHandler();
HttpClient = MockHandler.ToHttpClient();
HttpClient.BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/");
HttpClient.DefaultRequestHeaders.Add("Accept", "application/json; version=9");
var optionsMock = Substitute.For<IOptions<PaperlessOptions>>();
optionsMock.Value.Returns(Options);
var logger = Substitute.For<ILogger<PaperlessClient>>();
Client = new PaperlessClient(HttpClient, optionsMock, logger);
}
public MockedRequest SetupGet(string url, string responseJson, HttpStatusCode statusCode = HttpStatusCode.OK)
{
return MockHandler
.When(HttpMethod.Get, $"{Options.BaseUrl.TrimEnd('/')}/{url.TrimStart('/')}")
.Respond("application/json", responseJson);
}
public MockedRequest SetupGetWithStatus(string url, HttpStatusCode statusCode)
{
return MockHandler
.When(HttpMethod.Get, $"{Options.BaseUrl.TrimEnd('/')}/{url.TrimStart('/')}")
.Respond(statusCode);
}
public MockedRequest SetupPost(string url, string responseJson, HttpStatusCode statusCode = HttpStatusCode.OK)
{
return MockHandler
.When(HttpMethod.Post, $"{Options.BaseUrl.TrimEnd('/')}/{url.TrimStart('/')}")
.Respond("application/json", responseJson);
}
public MockedRequest SetupPostWithStatus(string url, HttpStatusCode statusCode)
{
return MockHandler
.When(HttpMethod.Post, $"{Options.BaseUrl.TrimEnd('/')}/{url.TrimStart('/')}")
.Respond(statusCode);
}
public MockedRequest SetupPatch(string url, string responseJson, HttpStatusCode statusCode = HttpStatusCode.OK)
{
return MockHandler
.When(HttpMethod.Patch, $"{Options.BaseUrl.TrimEnd('/')}/{url.TrimStart('/')}")
.Respond("application/json", responseJson);
}
public MockedRequest SetupPatchWithStatus(string url, HttpStatusCode statusCode)
{
return MockHandler
.When(HttpMethod.Patch, $"{Options.BaseUrl.TrimEnd('/')}/{url.TrimStart('/')}")
.Respond(statusCode);
}
public MockedRequest SetupDelete(string url, HttpStatusCode statusCode = HttpStatusCode.NoContent)
{
return MockHandler
.When(HttpMethod.Delete, $"{Options.BaseUrl.TrimEnd('/')}/{url.TrimStart('/')}")
.Respond(statusCode);
}
public void Dispose()
{
HttpClient.Dispose();
MockHandler.Dispose();
}
}
public static class MockHttpExtensions
{
public static MockedRequest RespondWithJson(this MockedRequest request, string json, HttpStatusCode statusCode = HttpStatusCode.OK)
{
return request.Respond(statusCode, "application/json", json);
}
}