Fix tool handlers to return proper MCP content format - #21
Conversation
All tool handlers were returning raw API response objects directly,
causing Claude to receive results without the required MCP content
array. Claude.ai looks for the content field and shows nothing when
it is absent. Wrap all tool return values in the standard MCP format:
{ content: [{ type: "text", text: JSON.stringify(result) }] }
📝 WalkthroughWalkthroughAll tool handlers across correspondents, document types, documents, and tags modules are refactored to wrap API results in a standardized content payload structure via a new Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
src/tools/documents.ts (1)
3-5: Consider extractingtoContentinto a shared utility.The helper is duplicated across tool modules; centralizing it will prevent behavior drift and make future serialization fixes one-shot.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/tools/documents.ts` around lines 3 - 5, Extract the toContent helper into a single shared utility module (e.g., export function toContent(result: any)) and replace the local toContent implementation in src/tools/documents.ts with an import from that shared module; update any other tool modules that currently duplicate the function to import the single exported toContent so behavior stays consistent, preserve the return shape ({ content: [{ type: "text" as const, text: JSON.stringify(result) }] }) and TypeScript typing when moving, and run/build tests to ensure no import/name collisions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/tools/documentTypes.ts`:
- Around line 3-5: The toContent function can produce non-string or throw from
JSON.stringify(result); update toContent to ensure text is always a string by:
attempt JSON.stringify(result) inside a try/catch, fall back to String(result ??
"") (or "" for undefined/null) when stringify returns undefined or throws, and
guarantee the returned object shape remains { content: [{ type: "text" as const,
text: string }] }; apply the same change to every occurrence of toContent in
src/tools/documentTypes.ts, src/tools/tags.ts, src/tools/documents.ts, and
src/tools/correspondents.ts so all MCP payloads always have a string text.
---
Nitpick comments:
In `@src/tools/documents.ts`:
- Around line 3-5: Extract the toContent helper into a single shared utility
module (e.g., export function toContent(result: any)) and replace the local
toContent implementation in src/tools/documents.ts with an import from that
shared module; update any other tool modules that currently duplicate the
function to import the single exported toContent so behavior stays consistent,
preserve the return shape ({ content: [{ type: "text" as const, text:
JSON.stringify(result) }] }) and TypeScript typing when moving, and run/build
tests to ensure no import/name collisions.
ℹ️ Review info
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
src/tools/correspondents.tssrc/tools/documentTypes.tssrc/tools/documents.tssrc/tools/tags.ts
| function toContent(result: any) { | ||
| return { content: [{ type: "text" as const, text: JSON.stringify(result) }] }; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's look at the toContent function definition and its usage
cd /tmp && find . -name "documentTypes.ts" -type f 2>/dev/null | head -5Repository: nloui/paperless-mcp
Length of output: 45
🏁 Script executed:
# Let's find the file and examine it
fd documentTypes.ts --type fRepository: nloui/paperless-mcp
Length of output: 89
🏁 Script executed:
# Search for toContent definition and usages
rg -n "toContent" --type tsRepository: nloui/paperless-mcp
Length of output: 1625
🏁 Script executed:
# Get the specific file content to see the toContent function
cat -n src/tools/documentTypes.ts | head -50Repository: nloui/paperless-mcp
Length of output: 3990
🏁 Script executed:
# Find the API implementation to check method return types
fd api --type dRepository: nloui/paperless-mcp
Length of output: 71
🏁 Script executed:
# Search for API method definitions, particularly for bulk operations and delete
rg -n "bulkEditObjects|bulkEditDocuments|deleteTag|createDocumentType|getDocumentTypes" --type ts -A 5 | head -100Repository: nloui/paperless-mcp
Length of output: 3957
🏁 Script executed:
# Look for the api module/implementation
find . -name "*.ts" -type f | xargs rg -l "export.*bulkEditObjects|export.*deleteTag" | head -5Repository: nloui/paperless-mcp
Length of output: 45
🏁 Script executed:
# Get the API implementation file
cat -n src/api/PaperlessAPI.ts | head -100Repository: nloui/paperless-mcp
Length of output: 3383
🏁 Script executed:
# Look for the request method to understand what it returns
rg -n "async request|private request" src/api/PaperlessAPI.ts -A 10 | head -50Repository: nloui/paperless-mcp
Length of output: 462
🏁 Script executed:
# Check specifically for DELETE operations and what they return
rg -n "DELETE" src/api/PaperlessAPI.ts -B 2 -A 5Repository: nloui/paperless-mcp
Length of output: 257
🏁 Script executed:
# Get more context on the request method to see complete implementation
cat -n src/api/PaperlessAPI.ts | sed -n '10,40p'Repository: nloui/paperless-mcp
Length of output: 1036
🏁 Script executed:
# Check if there are any API endpoints that might return empty/void responses
rg -n "204|No Content|DELETE.*response" src/api/Repository: nloui/paperless-mcp
Length of output: 45
🏁 Script executed:
# Verify JSON.stringify behavior with undefined and edge cases using Node.js
node << 'EOF'
// Test JSON.stringify behavior
console.log("=== JSON.stringify behavior ===");
console.log("undefined:", JSON.stringify(undefined));
console.log("null:", JSON.stringify(null));
console.log("empty object:", JSON.stringify({}));
// Test what happens with circular references
try {
const circular = { a: 1 };
circular.self = circular;
JSON.stringify(circular);
} catch (e) {
console.log("Circular ref error:", e.message);
}
// Test with BigInt
try {
JSON.stringify({ value: BigInt(123) });
} catch (e) {
console.log("BigInt error:", e.message);
}
// Test edge case - what if result is a primitive?
console.log("String:", JSON.stringify("hello"));
console.log("Number:", JSON.stringify(42));
console.log("Boolean:", JSON.stringify(true));
EOFRepository: nloui/paperless-mcp
Length of output: 389
🏁 Script executed:
# Check what the text field should actually be in MCP protocol
rg -n "type.*text.*const" src/tools/ -B 2 -A 2 | head -30Repository: nloui/paperless-mcp
Length of output: 1067
Harden toContent so text is always a string.
JSON.stringify(result) can return undefined (for undefined/void values) or throw on non-serializable types (circular refs, BigInt), producing invalid MCP payloads where text must be a string. DELETE operations or 204 responses in particular may trigger this.
🔧 Suggested fix
-function toContent(result: any) {
- return { content: [{ type: "text" as const, text: JSON.stringify(result) }] };
+function toContent(result: unknown) {
+ let text: string;
+ try {
+ const json = JSON.stringify(result);
+ text = json ?? "null";
+ } catch {
+ text = String(result);
+ }
+ return { content: [{ type: "text" as const, text }] };
}This function appears in src/tools/documentTypes.ts, src/tools/tags.ts, src/tools/documents.ts, and src/tools/correspondents.ts and should be hardened consistently across all files.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/tools/documentTypes.ts` around lines 3 - 5, The toContent function can
produce non-string or throw from JSON.stringify(result); update toContent to
ensure text is always a string by: attempt JSON.stringify(result) inside a
try/catch, fall back to String(result ?? "") (or "" for undefined/null) when
stringify returns undefined or throws, and guarantee the returned object shape
remains { content: [{ type: "text" as const, text: string }] }; apply the same
change to every occurrence of toContent in src/tools/documentTypes.ts,
src/tools/tags.ts, src/tools/documents.ts, and src/tools/correspondents.ts so
all MCP payloads always have a string text.
...
Summary by CodeRabbit
Release Notes