Create/update operations were sending empty request bodies to Paperless,
producing two visible failure modes:
1. POST creates (correspondents, tags, document types, storage paths,
custom fields) returned `400 {"name":["This field is required."]}`
even when the caller passed a valid name.
2. PATCH updates (documents, correspondents, tags, etc.) returned 200
and bumped the row's `modified` timestamp, but no fields actually
changed — server-side this looked like a successful no-op PATCH.
3. Bulk edit operations (`api/documents/bulk_edit/`,
`api/bulk_edit_objects/`) failed because the inner `parameters` field
went out as `{}`, so add_tag/remove_tag/etc. arrived without a tag id.
Reproducer: `correspondents.create(name="ACME Corp")` against a real
Paperless instance returns 400 with the body above.
Root causes are two related serialization patterns where the
compile-time type of the value to serialize is `object`:
- `PostWithResultAsync` / `PatchWithResultAsync` accept the request as
`object` and pass it to `PostAsJsonAsync<TValue>` /
`JsonContent.Create<T>(value, ...)`. The generic `T` is inferred as
`object`, and through the configured DI HttpClient pipeline
(DelegatingHandler + Polly retry policy) the body that reaches the
wire is empty, even though the same JsonContent's
`ReadAsStringAsync()` returns the expected JSON.
- `BulkEditDocumentsAsync` / `BulkEditObjectsAsync` wrap the call args
in an anonymous type whose `parameters` field has compile-time type
`object?`. System.Text.Json serializes that property against
`object`, producing `"parameters":{}` regardless of the runtime value.
Fix: serialize against the runtime type explicitly and materialize the
JSON into a `StringContent` before the request leaves this client.
For bulk edits, build the body via `JsonObject` and serialize the
`parameters` payload against its runtime type.
Adds wire-format pinning tests for create / update / bulk_edit /
bulk_edit_objects that capture the outbound request body and assert the
expected payload shape. These do not reproduce the original bug under
MockHttp (the test helper bypasses the DI handler chain), but they pin
the new serialization for future refactors.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reflection-based tests verify all McpServerTool names match
^[a-zA-Z0-9_-]{1,64}$ and contain no dots, preventing regressions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add ApiResult<T> type to carry success/failure with error details
- Add UpdateDocumentWithResultAsync and CreateTagWithResultAsync methods
- Update DocumentTools.Update and TagTools.Create to return actual
HTTP status codes and response bodies in error responses
- Add comprehensive tests for error handling (18 new tests)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The /api/ endpoint returns HTML (Swagger UI), not JSON.
Changed ping to use /api/status/ which returns JSON with version info.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
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>