# Directory Structure ``` ├── .cursor │ └── rules │ ├── http-transport.mdc │ └── typescript-mcp-migration.mdc ├── .dockerignore ├── .github │ ├── FUNDING.yml │ └── workflows │ ├── docker-build.yml │ └── docker-publish.yml ├── .gitignore ├── Dockerfile ├── package-lock.json ├── package.json ├── README.md ├── smithery.yaml ├── src │ ├── api │ │ └── PaperlessAPI.ts │ ├── index.ts │ └── tools │ ├── correspondents.ts │ ├── documents.ts │ ├── documentTypes.ts │ └── tags.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- ``` 1 | node_modules 2 | build ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Dependencies 2 | node_modules/ 3 | 4 | # Environment variables 5 | .env 6 | .env.local 7 | .env.*.local 8 | 9 | # Build output 10 | dist/ 11 | build/ 12 | coverage/ 13 | 14 | # Logs 15 | logs/ 16 | *.log 17 | npm-debug.log* 18 | yarn-debug.log* 19 | yarn-error.log* 20 | 21 | # OS 22 | .DS_Store 23 | Thumbs.db 24 | 25 | # Editor directories and files 26 | .idea/ 27 | .vscode/ 28 | *.suo 29 | *.ntvs* 30 | *.njsproj 31 | *.sln 32 | *.sw? 33 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | [](https://mseep.ai/app/nloui-paperless-mcp) 2 | 3 | # Paperless-NGX MCP Server 4 | 5 | [](https://smithery.ai/server/@nloui/paperless-mcp) 6 | 7 | An MCP (Model Context Protocol) server for interacting with a Paperless-NGX API server. This server provides tools for managing documents, tags, correspondents, and document types in your Paperless-NGX instance. 8 | 9 | ## Quick Start 10 | 11 | ### Installing via Smithery 12 | 13 | To install Paperless NGX MCP Server for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@nloui/paperless-mcp): 14 | 15 | ```bash 16 | npx -y @smithery/cli install @nloui/paperless-mcp --client claude 17 | ``` 18 | 19 | ### Manual Installation 20 | 1. Install the MCP server: 21 | ```bash 22 | npm install -g paperless-mcp 23 | ``` 24 | 25 | 2. Add it to your Claude's MCP configuration: 26 | 27 | For VSCode extension, edit `~/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings/cline_mcp_settings.json`: 28 | ```json 29 | { 30 | "mcpServers": { 31 | "paperless": { 32 | "command": "npx", 33 | "args": ["paperless-mcp", "http://your-paperless-instance:8000", "your-api-token"] 34 | } 35 | } 36 | } 37 | ``` 38 | 39 | For Claude desktop app, edit `~/Library/Application Support/Claude/claude_desktop_config.json`: 40 | ```json 41 | { 42 | "mcpServers": { 43 | "paperless": { 44 | "command": "npx", 45 | "args": ["paperless-mcp", "http://your-paperless-instance:8000", "your-api-token"] 46 | } 47 | } 48 | } 49 | ``` 50 | 51 | 3. Get your API token: 52 | 1. Log into your Paperless-NGX instance 53 | 2. Click your username in the top right 54 | 3. Select "My Profile" 55 | 4. Click the circular arrow button to generate a new token 56 | 57 | 4. Replace the placeholders in your MCP config: 58 | - `http://your-paperless-instance:8000` with your Paperless-NGX URL 59 | - `your-api-token` with the token you just generated 60 | 61 | That's it! Now you can ask Claude to help you manage your Paperless-NGX documents. 62 | 63 | ## Example Usage 64 | 65 | Here are some things you can ask Claude to do: 66 | 67 | - "Show me all documents tagged as 'Invoice'" 68 | - "Search for documents containing 'tax return'" 69 | - "Create a new tag called 'Receipts' with color #FF0000" 70 | - "Download document #123" 71 | - "List all correspondents" 72 | - "Create a new document type called 'Bank Statement'" 73 | 74 | ## Available Tools 75 | 76 | ### Document Operations 77 | 78 | #### list_documents 79 | Get a paginated list of all documents. 80 | 81 | Parameters: 82 | - page (optional): Page number 83 | - page_size (optional): Number of documents per page 84 | 85 | ```typescript 86 | list_documents({ 87 | page: 1, 88 | page_size: 25 89 | }) 90 | ``` 91 | 92 | #### get_document 93 | Get a specific document by ID. 94 | 95 | Parameters: 96 | - id: Document ID 97 | 98 | ```typescript 99 | get_document({ 100 | id: 123 101 | }) 102 | ``` 103 | 104 | #### search_documents 105 | Full-text search across documents. 106 | 107 | Parameters: 108 | - query: Search query string 109 | 110 | ```typescript 111 | search_documents({ 112 | query: "invoice 2024" 113 | }) 114 | ``` 115 | 116 | #### download_document 117 | Download a document file by ID. 118 | 119 | Parameters: 120 | - id: Document ID 121 | - original (optional): If true, downloads original file instead of archived version 122 | 123 | ```typescript 124 | download_document({ 125 | id: 123, 126 | original: false 127 | }) 128 | ``` 129 | 130 | #### bulk_edit_documents 131 | Perform bulk operations on multiple documents. 132 | 133 | Parameters: 134 | - documents: Array of document IDs 135 | - method: One of: 136 | - set_correspondent: Set correspondent for documents 137 | - set_document_type: Set document type for documents 138 | - set_storage_path: Set storage path for documents 139 | - add_tag: Add a tag to documents 140 | - remove_tag: Remove a tag from documents 141 | - modify_tags: Add and/or remove multiple tags 142 | - delete: Delete documents 143 | - reprocess: Reprocess documents 144 | - set_permissions: Set document permissions 145 | - merge: Merge multiple documents 146 | - split: Split a document into multiple documents 147 | - rotate: Rotate document pages 148 | - delete_pages: Delete specific pages from a document 149 | - Additional parameters based on method: 150 | - correspondent: ID for set_correspondent 151 | - document_type: ID for set_document_type 152 | - storage_path: ID for set_storage_path 153 | - tag: ID for add_tag/remove_tag 154 | - add_tags: Array of tag IDs for modify_tags 155 | - remove_tags: Array of tag IDs for modify_tags 156 | - permissions: Object for set_permissions with owner, permissions, merge flag 157 | - metadata_document_id: ID for merge to specify metadata source 158 | - delete_originals: Boolean for merge/split 159 | - pages: String for split "[1,2-3,4,5-7]" or delete_pages "[2,3,4]" 160 | - degrees: Number for rotate (90, 180, or 270) 161 | 162 | Examples: 163 | ```typescript 164 | // Add a tag to multiple documents 165 | bulk_edit_documents({ 166 | documents: [1, 2, 3], 167 | method: "add_tag", 168 | tag: 5 169 | }) 170 | 171 | // Set correspondent and document type 172 | bulk_edit_documents({ 173 | documents: [4, 5], 174 | method: "set_correspondent", 175 | correspondent: 2 176 | }) 177 | 178 | // Merge documents 179 | bulk_edit_documents({ 180 | documents: [6, 7, 8], 181 | method: "merge", 182 | metadata_document_id: 6, 183 | delete_originals: true 184 | }) 185 | 186 | // Split document into parts 187 | bulk_edit_documents({ 188 | documents: [9], 189 | method: "split", 190 | pages: "[1-2,3-4,5]" 191 | }) 192 | 193 | // Modify multiple tags at once 194 | bulk_edit_documents({ 195 | documents: [10, 11], 196 | method: "modify_tags", 197 | add_tags: [1, 2], 198 | remove_tags: [3, 4] 199 | }) 200 | ``` 201 | 202 | #### post_document 203 | Upload a new document to Paperless-NGX. 204 | 205 | Parameters: 206 | - file: Base64 encoded file content 207 | - filename: Name of the file 208 | - title (optional): Title for the document 209 | - created (optional): DateTime when the document was created (e.g. "2024-01-19" or "2024-01-19 06:15:00+02:00") 210 | - correspondent (optional): ID of a correspondent 211 | - document_type (optional): ID of a document type 212 | - storage_path (optional): ID of a storage path 213 | - tags (optional): Array of tag IDs 214 | - archive_serial_number (optional): Archive serial number 215 | - custom_fields (optional): Array of custom field IDs 216 | 217 | ```typescript 218 | post_document({ 219 | file: "base64_encoded_content", 220 | filename: "invoice.pdf", 221 | title: "January Invoice", 222 | created: "2024-01-19", 223 | correspondent: 1, 224 | document_type: 2, 225 | tags: [1, 3], 226 | archive_serial_number: "2024-001" 227 | }) 228 | ``` 229 | 230 | ### Tag Operations 231 | 232 | #### list_tags 233 | Get all tags. 234 | 235 | ```typescript 236 | list_tags() 237 | ``` 238 | 239 | #### create_tag 240 | Create a new tag. 241 | 242 | Parameters: 243 | - name: Tag name 244 | - color (optional): Hex color code (e.g. "#ff0000") 245 | - match (optional): Text pattern to match 246 | - matching_algorithm (optional): One of "any", "all", "exact", "regular expression", "fuzzy" 247 | 248 | ```typescript 249 | create_tag({ 250 | name: "Invoice", 251 | color: "#ff0000", 252 | match: "invoice", 253 | matching_algorithm: "fuzzy" 254 | }) 255 | ``` 256 | 257 | ### Correspondent Operations 258 | 259 | #### list_correspondents 260 | Get all correspondents. 261 | 262 | ```typescript 263 | list_correspondents() 264 | ``` 265 | 266 | #### create_correspondent 267 | Create a new correspondent. 268 | 269 | Parameters: 270 | - name: Correspondent name 271 | - match (optional): Text pattern to match 272 | - matching_algorithm (optional): One of "any", "all", "exact", "regular expression", "fuzzy" 273 | 274 | ```typescript 275 | create_correspondent({ 276 | name: "ACME Corp", 277 | match: "ACME", 278 | matching_algorithm: "fuzzy" 279 | }) 280 | ``` 281 | 282 | ### Document Type Operations 283 | 284 | #### list_document_types 285 | Get all document types. 286 | 287 | ```typescript 288 | list_document_types() 289 | ``` 290 | 291 | #### create_document_type 292 | Create a new document type. 293 | 294 | Parameters: 295 | - name: Document type name 296 | - match (optional): Text pattern to match 297 | - matching_algorithm (optional): One of "any", "all", "exact", "regular expression", "fuzzy" 298 | 299 | ```typescript 300 | create_document_type({ 301 | name: "Invoice", 302 | match: "invoice total amount due", 303 | matching_algorithm: "any" 304 | }) 305 | ``` 306 | 307 | ## Error Handling 308 | 309 | The server will show clear error messages if: 310 | - The Paperless-NGX URL or API token is incorrect 311 | - The Paperless-NGX server is unreachable 312 | - The requested operation fails 313 | - The provided parameters are invalid 314 | 315 | ## Development 316 | 317 | Want to contribute or modify the server? Here's what you need to know: 318 | 319 | 1. Clone the repository 320 | 2. Install dependencies: 321 | ```bash 322 | npm install 323 | ``` 324 | 325 | 3. Make your changes to server.js 326 | 4. Test locally: 327 | ```bash 328 | node server.js http://localhost:8000 your-test-token 329 | ``` 330 | 331 | The server is built with: 332 | - [litemcp](https://github.com/wong2/litemcp): A TypeScript framework for building MCP servers 333 | - [zod](https://github.com/colinhacks/zod): TypeScript-first schema validation 334 | 335 | ## API Documentation 336 | 337 | This MCP server implements endpoints from the Paperless-NGX REST API. For more details about the underlying API, see the [official documentation](https://docs.paperless-ngx.com/api/). 338 | 339 | ## Running the MCP Server 340 | 341 | The MCP server can be run in two modes: 342 | 343 | ### 1. stdio (default) 344 | 345 | This is the default mode. The server communicates over stdio, suitable for CLI and direct integrations. 346 | 347 | ``` 348 | npm run start -- <baseUrl> <token> 349 | ``` 350 | 351 | ### 2. HTTP (Streamable HTTP Transport) 352 | 353 | To run the server as an HTTP service, use the `--http` flag. You can also specify the port with `--port` (default: 3000). This mode requires [Express](https://expressjs.com/) to be installed (it is included as a dependency). 354 | 355 | ``` 356 | npm run start -- <baseUrl> <token> --http --port 3000 357 | ``` 358 | 359 | - The MCP API will be available at `POST /mcp` on the specified port. 360 | - Each request is handled statelessly, following the [StreamableHTTPServerTransport](https://github.com/modelcontextprotocol/typescript-sdk) pattern. 361 | - GET and DELETE requests to `/mcp` will return 405 Method Not Allowed. 362 | ``` -------------------------------------------------------------------------------- /.github/workflows/docker-build.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Docker Build (PR) 2 | on: 3 | pull_request: 4 | branches: 5 | - '**' 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - name: Set up Docker Buildx 12 | uses: docker/setup-buildx-action@v3 13 | - name: Build Docker image 14 | uses: docker/build-push-action@v6 15 | with: 16 | context: . 17 | file: ./Dockerfile 18 | push: false 19 | ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile 1 | # Builder stage 2 | FROM node:20-slim AS builder 3 | WORKDIR /app 4 | COPY package.json package-lock.json ./ 5 | RUN npm ci 6 | COPY . . 7 | RUN npm run build 8 | 9 | # Production stage 10 | FROM node:20-slim AS production 11 | 12 | WORKDIR /app 13 | COPY --from=builder /app/build . 14 | COPY --from=builder /app/node_modules ./node_modules 15 | COPY --from=builder /app/package.json ./package.json 16 | COPY --from=builder /app/package-lock.json ./package-lock.json 17 | 18 | EXPOSE 3000 19 | ENTRYPOINT [ "node", "index.js", "--http", "--port", "3000" ] 20 | ``` -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- ```yaml 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml 2 | 3 | startCommand: 4 | type: stdio 5 | configSchema: 6 | # JSON Schema defining the configuration options for the MCP. 7 | type: object 8 | required: 9 | - baseUrl 10 | - token 11 | properties: 12 | baseUrl: 13 | type: string 14 | description: The base URL of your Paperless-NGX instance. 15 | token: 16 | type: string 17 | description: The API token for accessing your Paperless-NGX instance. 18 | commandFunction: 19 | # A function that produces the CLI command to start the MCP on stdio. 20 | |- 21 | (config) => ({command: 'node', args: ['src/index.js', config.baseUrl, config.token]}) ``` -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- ```yaml 1 | # These are supported funding model platforms 2 | 3 | github: [nloui] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | ``` -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Docker Publish 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - main 7 | 8 | jobs: 9 | build-and-push: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | packages: write 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Set up Docker Buildx 17 | uses: docker/setup-buildx-action@v3 18 | - name: Log in to GitHub Container Registry 19 | uses: docker/login-action@v3 20 | with: 21 | registry: ghcr.io 22 | username: ${{ github.actor }} 23 | password: ${{ secrets.GITHUB_TOKEN }} 24 | - name: Extract Docker metadata 25 | id: meta 26 | uses: docker/metadata-action@v5 27 | with: 28 | images: ghcr.io/${{ github.actor }}/${{ github.repository }} 29 | flavor: | 30 | latest=true 31 | - name: Build and push Docker image 32 | uses: docker/build-push-action@v6 33 | with: 34 | context: . 35 | file: ./Dockerfile 36 | push: true 37 | tags: ${{ steps.meta.outputs.tags }} 38 | labels: ${{ steps.meta.outputs.labels }} ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "name": "@nloui/paperless-mcp", 3 | "version": "1.0.0", 4 | "description": "Model Context Protocol (MCP) server for interacting with Paperless-NGX document management system. Enables AI assistants to manage documents, tags, correspondents, and document types through the Paperless-NGX API.", 5 | "main": "src/index.js", 6 | "bin": { 7 | "paperless-mcp": "src/index.js" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1", 11 | "start": "ts-node src/index.ts", 12 | "build": "tsc", 13 | "inspect": "npm run build && npx -y @modelcontextprotocol/inspector node build/index.js" 14 | }, 15 | "keywords": [ 16 | "mcp", 17 | "paperless-ngx", 18 | "document-management", 19 | "ai", 20 | "claude", 21 | "model-context-protocol", 22 | "paperless" 23 | ], 24 | "author": "Nick Loui", 25 | "license": "ISC", 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/nloui/paperless-mcp.git" 29 | }, 30 | "dependencies": { 31 | "@modelcontextprotocol/sdk": "^1.11.1", 32 | "express": "^5.1.0", 33 | "typescript": "^5.8.3", 34 | "zod": "^3.24.1" 35 | }, 36 | "devDependencies": { 37 | "@types/node": "^22.15.17", 38 | "ts-node": "^10.9.2" 39 | } 40 | } 41 | ``` -------------------------------------------------------------------------------- /src/tools/correspondents.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp"; 2 | import { z } from "zod"; 3 | 4 | export function registerCorrespondentTools(server: McpServer, api) { 5 | server.tool( 6 | "list_correspondents", 7 | "Retrieve all available correspondents (people, companies, organizations that send/receive documents). Returns names and automatic matching patterns for document assignment.", 8 | { }, async (args, extra) => { 9 | if (!api) throw new Error("Please configure API connection first"); 10 | return api.getCorrespondents(); 11 | }); 12 | 13 | server.tool( 14 | "create_correspondent", 15 | "Create a new correspondent (person, company, or organization) for tracking document senders and receivers. Can include automatic matching patterns for smart assignment to incoming documents.", 16 | { 17 | name: z.string().describe("Name of the correspondent (person, company, or organization that sends/receives documents). Examples: 'Bank of America', 'John Smith', 'Electric Company'."), 18 | match: z.string().optional().describe("Text pattern to automatically assign this correspondent to matching documents. Use names, email addresses, or keywords that appear in documents from this correspondent."), 19 | matching_algorithm: z 20 | .enum(["any", "all", "exact", "regular expression", "fuzzy"]) 21 | .optional().describe("How to match text patterns: 'any'=any word matches, 'all'=all words must match, 'exact'=exact phrase match, 'regular expression'=use regex patterns, 'fuzzy'=approximate matching with typos. Default is 'any'."), 22 | }, 23 | async (args, extra) => { 24 | if (!api) throw new Error("Please configure API connection first"); 25 | return api.createCorrespondent(args); 26 | } 27 | ); 28 | 29 | server.tool( 30 | "bulk_edit_correspondents", 31 | "Perform bulk operations on multiple correspondents: set permissions to control who can assign them to documents, or permanently delete multiple correspondents. Use with caution as deletion affects all associated documents.", 32 | { 33 | correspondent_ids: z.array(z.number()).describe("Array of correspondent IDs to perform bulk operations on. Use list_correspondents to get valid correspondent IDs."), 34 | operation: z.enum(["set_permissions", "delete"]).describe("Bulk operation: 'set_permissions' to control who can assign these correspondents to documents, 'delete' to permanently remove correspondents from the system. Warning: Deleting correspondents will remove them from all associated documents."), 35 | owner: z.number().optional().describe("User ID to set as owner when operation is 'set_permissions'. The owner has full control over these correspondents."), 36 | permissions: z 37 | .object({ 38 | view: z.object({ 39 | users: z.array(z.number()).optional().describe("User IDs who can see and assign these correspondents to documents"), 40 | groups: z.array(z.number()).optional().describe("Group IDs who can see and assign these correspondents to documents"), 41 | }).describe("Users and groups with permission to view and use these correspondents"), 42 | change: z.object({ 43 | users: z.array(z.number()).optional().describe("User IDs who can modify correspondent details (name, matching rules)"), 44 | groups: z.array(z.number()).optional().describe("Group IDs who can modify correspondent details"), 45 | }).describe("Users and groups with permission to edit these correspondent settings"), 46 | }) 47 | .optional().describe("Permission settings when operation is 'set_permissions'. Defines who can view/assign and modify these correspondents."), 48 | merge: z.boolean().optional().describe("Whether to merge with existing permissions (true) or replace them entirely (false). Default is false."), 49 | }, 50 | async (args, extra) => { 51 | if (!api) throw new Error("Please configure API connection first"); 52 | return api.bulkEditObjects( 53 | args.correspondent_ids, 54 | "correspondents", 55 | args.operation, 56 | args.operation === "set_permissions" 57 | ? { 58 | owner: args.owner, 59 | permissions: args.permissions, 60 | merge: args.merge, 61 | } 62 | : {} 63 | ); 64 | } 65 | ); 66 | } 67 | ``` -------------------------------------------------------------------------------- /src/tools/documentTypes.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from "zod"; 2 | 3 | export function registerDocumentTypeTools(server, api) { 4 | server.tool( 5 | "list_document_types", 6 | "Retrieve all available document types for categorizing documents by purpose or format (Invoice, Receipt, Contract, etc.). Returns names and automatic matching rules.", 7 | { 8 | // No parameters - returns all available document types 9 | }, async (args, extra) => { 10 | if (!api) throw new Error("Please configure API connection first"); 11 | return api.getDocumentTypes(); 12 | }); 13 | 14 | server.tool( 15 | "create_document_type", 16 | "Create a new document type for categorizing documents by their purpose or format (e.g., Invoice, Receipt, Contract). Can include automatic matching rules for smart classification.", 17 | { 18 | name: z.string().describe("Name of the document type for categorizing documents by their purpose or format. Examples: 'Invoice', 'Receipt', 'Contract', 'Letter', 'Bank Statement', 'Tax Document'."), 19 | match: z.string().optional().describe("Text pattern to automatically assign this document type to matching documents. Use keywords that commonly appear in this type of document (e.g., 'invoice', 'receipt', 'contract terms')."), 20 | matching_algorithm: z 21 | .enum(["any", "all", "exact", "regular expression", "fuzzy"]) 22 | .optional().describe("How to match text patterns: 'any'=any word matches, 'all'=all words must match, 'exact'=exact phrase match, 'regular expression'=use regex patterns, 'fuzzy'=approximate matching with typos. Default is 'any'."), 23 | }, 24 | async (args, extra) => { 25 | if (!api) throw new Error("Please configure API connection first"); 26 | return api.createDocumentType(args); 27 | } 28 | ); 29 | 30 | server.tool( 31 | "bulk_edit_document_types", 32 | "Perform bulk operations on multiple document types: set permissions to control who can assign them to documents, or permanently delete multiple types. Use with caution as deletion affects all associated documents.", 33 | { 34 | document_type_ids: z.array(z.number()).describe("Array of document type IDs to perform bulk operations on. Use list_document_types to get valid document type IDs."), 35 | operation: z.enum(["set_permissions", "delete"]).describe("Bulk operation: 'set_permissions' to control who can assign these document types to documents, 'delete' to permanently remove document types from the system. Warning: Deleting document types will remove the classification from all associated documents."), 36 | owner: z.number().optional().describe("User ID to set as owner when operation is 'set_permissions'. The owner has full control over these document types."), 37 | permissions: z 38 | .object({ 39 | view: z.object({ 40 | users: z.array(z.number()).optional().describe("User IDs who can see and assign these document types to documents"), 41 | groups: z.array(z.number()).optional().describe("Group IDs who can see and assign these document types to documents"), 42 | }).describe("Users and groups with permission to view and use these document types for categorization"), 43 | change: z.object({ 44 | users: z.array(z.number()).optional().describe("User IDs who can modify document type details (name, matching rules)"), 45 | groups: z.array(z.number()).optional().describe("Group IDs who can modify document type details"), 46 | }).describe("Users and groups with permission to edit these document type settings"), 47 | }) 48 | .optional().describe("Permission settings when operation is 'set_permissions'. Defines who can view/assign and modify these document types."), 49 | merge: z.boolean().optional().describe("Whether to merge with existing permissions (true) or replace them entirely (false). Default is false."), 50 | }, 51 | async (args, extra) => { 52 | if (!api) throw new Error("Please configure API connection first"); 53 | return api.bulkEditObjects( 54 | args.document_type_ids, 55 | "document_types", 56 | args.operation, 57 | args.operation === "set_permissions" 58 | ? { 59 | owner: args.owner, 60 | permissions: args.permissions, 61 | merge: args.merge, 62 | } 63 | : {} 64 | ); 65 | } 66 | ); 67 | } 68 | ``` -------------------------------------------------------------------------------- /src/api/PaperlessAPI.ts: -------------------------------------------------------------------------------- ```typescript 1 | export class PaperlessAPI { 2 | constructor( 3 | private readonly baseUrl: string, 4 | private readonly token: string 5 | ) { 6 | this.baseUrl = baseUrl; 7 | this.token = token; 8 | } 9 | 10 | async request(path: string, options: RequestInit = {}) { 11 | const url = `${this.baseUrl}/api${path}`; 12 | const headers = { 13 | Authorization: `Token ${this.token}`, 14 | Accept: "application/json; version=5", 15 | "Content-Type": "application/json", 16 | "Accept-Language": "en-US,en;q=0.9", 17 | }; 18 | 19 | const response = await fetch(url, { 20 | ...options, 21 | headers: { 22 | ...headers, 23 | ...options.headers, 24 | }, 25 | }); 26 | 27 | if (!response.ok) { 28 | console.error({ 29 | error: "Error executing request", 30 | url, 31 | options, 32 | status: response.status, 33 | response: await response.json(), 34 | }); 35 | throw new Error(`HTTP error! status: ${response.status}`); 36 | } 37 | 38 | return response.json(); 39 | } 40 | 41 | // Document operations 42 | async bulkEditDocuments(documents, method, parameters = {}) { 43 | return this.request("/documents/bulk_edit/", { 44 | method: "POST", 45 | body: JSON.stringify({ 46 | documents, 47 | method, 48 | parameters, 49 | }), 50 | }); 51 | } 52 | 53 | async postDocument( 54 | file: File, 55 | metadata: Record<string, string | string[]> = {} 56 | ) { 57 | const formData = new FormData(); 58 | formData.append("document", file); 59 | 60 | // Add optional metadata fields 61 | if (metadata.title) formData.append("title", metadata.title); 62 | if (metadata.created) formData.append("created", metadata.created); 63 | if (metadata.correspondent) 64 | formData.append("correspondent", metadata.correspondent); 65 | if (metadata.document_type) 66 | formData.append("document_type", metadata.document_type); 67 | if (metadata.storage_path) 68 | formData.append("storage_path", metadata.storage_path); 69 | if (metadata.tags) { 70 | (metadata.tags as string[]).forEach((tag) => 71 | formData.append("tags", tag) 72 | ); 73 | } 74 | if (metadata.archive_serial_number) { 75 | formData.append("archive_serial_number", metadata.archive_serial_number); 76 | } 77 | if (metadata.custom_fields) { 78 | (metadata.custom_fields as string[]).forEach((field) => 79 | formData.append("custom_fields", field) 80 | ); 81 | } 82 | 83 | const response = await fetch( 84 | `${this.baseUrl}/api/documents/post_document/`, 85 | { 86 | method: "POST", 87 | headers: { 88 | Authorization: `Token ${this.token}`, 89 | }, 90 | body: formData, 91 | } 92 | ); 93 | 94 | if (!response.ok) { 95 | throw new Error(`HTTP error! status: ${response.status}`); 96 | } 97 | 98 | return response.json(); 99 | } 100 | 101 | async getDocuments(query = "") { 102 | return this.request(`/documents/${query}`); 103 | } 104 | 105 | async getDocument(id) { 106 | return this.request(`/documents/${id}/`); 107 | } 108 | 109 | async searchDocuments(query) { 110 | return this.request(`/documents/?query=${encodeURIComponent(query)}`); 111 | } 112 | 113 | async downloadDocument(id, asOriginal = false) { 114 | const query = asOriginal ? "?original=true" : ""; 115 | const response = await fetch( 116 | `${this.baseUrl}/api/documents/${id}/download/${query}`, 117 | { 118 | headers: { 119 | Authorization: `Token ${this.token}`, 120 | }, 121 | } 122 | ); 123 | return response; 124 | } 125 | 126 | // Tag operations 127 | async getTags() { 128 | return this.request("/tags/"); 129 | } 130 | 131 | async createTag(data) { 132 | return this.request("/tags/", { 133 | method: "POST", 134 | body: JSON.stringify(data), 135 | }); 136 | } 137 | 138 | async updateTag(id, data) { 139 | return this.request(`/tags/${id}/`, { 140 | method: "PUT", 141 | body: JSON.stringify(data), 142 | }); 143 | } 144 | 145 | async deleteTag(id) { 146 | return this.request(`/tags/${id}/`, { 147 | method: "DELETE", 148 | }); 149 | } 150 | 151 | // Correspondent operations 152 | async getCorrespondents() { 153 | return this.request("/correspondents/"); 154 | } 155 | 156 | async createCorrespondent(data) { 157 | return this.request("/correspondents/", { 158 | method: "POST", 159 | body: JSON.stringify(data), 160 | }); 161 | } 162 | 163 | // Document type operations 164 | async getDocumentTypes() { 165 | return this.request("/document_types/"); 166 | } 167 | 168 | async createDocumentType(data) { 169 | return this.request("/document_types/", { 170 | method: "POST", 171 | body: JSON.stringify(data), 172 | }); 173 | } 174 | 175 | // Bulk object operations 176 | async bulkEditObjects(objects, objectType, operation, parameters = {}) { 177 | return this.request("/bulk_edit_objects/", { 178 | method: "POST", 179 | body: JSON.stringify({ 180 | objects, 181 | object_type: objectType, 182 | operation, 183 | ...parameters, 184 | }), 185 | }); 186 | } 187 | } 188 | ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; 3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 4 | import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; 5 | import express from "express"; 6 | import { PaperlessAPI } from "./api/PaperlessAPI"; 7 | import { registerCorrespondentTools } from "./tools/correspondents"; 8 | import { registerDocumentTools } from "./tools/documents"; 9 | import { registerDocumentTypeTools } from "./tools/documentTypes"; 10 | import { registerTagTools } from "./tools/tags"; 11 | 12 | // Simple CLI argument parsing 13 | const args = process.argv.slice(2); 14 | const useHttp = args.includes("--http"); 15 | let port = 3000; 16 | const portIndex = args.indexOf("--port"); 17 | if (portIndex !== -1 && args[portIndex + 1]) { 18 | const parsed = parseInt(args[portIndex + 1], 10); 19 | if (!isNaN(parsed)) port = parsed; 20 | } 21 | 22 | async function main() { 23 | let baseUrl: string | undefined; 24 | let token: string | undefined; 25 | 26 | if (useHttp) { 27 | baseUrl = process.env.PAPERLESS_URL; 28 | token = process.env.API_KEY; 29 | if (!baseUrl || !token) { 30 | console.error( 31 | "When using --http, PAPERLESS_URL and API_KEY environment variables must be set." 32 | ); 33 | process.exit(1); 34 | } 35 | } else { 36 | baseUrl = args[0]; 37 | token = args[1]; 38 | if (!baseUrl || !token) { 39 | console.error( 40 | "Usage: paperless-mcp <baseUrl> <token> [--http] [--port <port>]" 41 | ); 42 | console.error( 43 | "Example: paperless-mcp http://localhost:8000 your-api-token --http --port 3000" 44 | ); 45 | console.error( 46 | "When using --http, PAPERLESS_URL and API_KEY environment variables must be set." 47 | ); 48 | process.exit(1); 49 | } 50 | } 51 | 52 | // Initialize API client and server once 53 | const api = new PaperlessAPI(baseUrl, token); 54 | const server = new McpServer({ name: "paperless-ngx", version: "1.0.0" }); 55 | registerDocumentTools(server, api); 56 | registerTagTools(server, api); 57 | registerCorrespondentTools(server, api); 58 | registerDocumentTypeTools(server, api); 59 | 60 | if (useHttp) { 61 | const app = express(); 62 | app.use(express.json()); 63 | 64 | // Store transports for each session 65 | const sseTransports: Record<string, SSEServerTransport> = {}; 66 | 67 | app.post("/mcp", async (req, res) => { 68 | try { 69 | const transport = new StreamableHTTPServerTransport({ 70 | sessionIdGenerator: undefined, 71 | }); 72 | res.on("close", () => { 73 | transport.close(); 74 | }); 75 | await server.connect(transport); 76 | await transport.handleRequest(req, res, req.body); 77 | } catch (error) { 78 | console.error("Error handling MCP request:", error); 79 | if (!res.headersSent) { 80 | res.status(500).json({ 81 | jsonrpc: "2.0", 82 | error: { 83 | code: -32603, 84 | message: "Internal server error", 85 | }, 86 | id: null, 87 | }); 88 | } 89 | } 90 | }); 91 | 92 | app.get("/mcp", async (req, res) => { 93 | res.writeHead(405).end( 94 | JSON.stringify({ 95 | jsonrpc: "2.0", 96 | error: { 97 | code: -32000, 98 | message: "Method not allowed.", 99 | }, 100 | id: null, 101 | }) 102 | ); 103 | }); 104 | 105 | app.delete("/mcp", async (req, res) => { 106 | res.writeHead(405).end( 107 | JSON.stringify({ 108 | jsonrpc: "2.0", 109 | error: { 110 | code: -32000, 111 | message: "Method not allowed.", 112 | }, 113 | id: null, 114 | }) 115 | ); 116 | }); 117 | 118 | app.get("/sse", async (req, res) => { 119 | console.log("SSE request received"); 120 | try { 121 | const transport = new SSEServerTransport("/messages", res); 122 | sseTransports[transport.sessionId] = transport; 123 | res.on("close", () => { 124 | delete sseTransports[transport.sessionId]; 125 | transport.close(); 126 | }); 127 | await server.connect(transport); 128 | } catch (error) { 129 | console.error("Error handling SSE request:", error); 130 | if (!res.headersSent) { 131 | res.status(500).json({ 132 | jsonrpc: "2.0", 133 | error: { 134 | code: -32603, 135 | message: "Internal server error", 136 | }, 137 | id: null, 138 | }); 139 | } 140 | } 141 | }); 142 | 143 | app.post("/messages", async (req, res) => { 144 | const sessionId = req.query.sessionId as string; 145 | const transport = sseTransports[sessionId]; 146 | if (transport) { 147 | await transport.handlePostMessage(req, res, req.body); 148 | } else { 149 | res.status(400).send("No transport found for sessionId"); 150 | } 151 | }); 152 | 153 | app.listen(port, () => { 154 | console.log( 155 | `MCP Stateless Streamable HTTP Server listening on port ${port}` 156 | ); 157 | }); 158 | } else { 159 | const transport = new StdioServerTransport(); 160 | await server.connect(transport); 161 | } 162 | } 163 | 164 | main().catch((e) => console.error(e.message)); 165 | ``` -------------------------------------------------------------------------------- /src/tools/tags.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from "zod"; 2 | 3 | export function registerTagTools(server, api) { 4 | server.tool( 5 | "list_tags", 6 | "Retrieve all available tags for labeling and organizing documents. Returns tag names, colors, and matching rules for automatic assignment.", 7 | { 8 | // No parameters - returns all available tags 9 | }, async (args, extra) => { 10 | if (!api) throw new Error("Please configure API connection first"); 11 | return api.getTags(); 12 | }); 13 | 14 | server.tool( 15 | "create_tag", 16 | "Create a new tag for labeling and organizing documents. Tags can have colors for visual identification and automatic matching rules for smart assignment.", 17 | { 18 | name: z.string().describe("Tag name for labeling and organizing documents (e.g., 'important', 'taxes', 'receipts'). Must be unique and descriptive."), 19 | color: z 20 | .string() 21 | .regex(/^#[0-9A-Fa-f]{6}$/) 22 | .optional().describe("Hex color code for visual identification (e.g., '#FF0000' for red, '#00FF00' for green). If not provided, Paperless assigns a random color."), 23 | match: z.string().optional().describe("Text pattern to automatically assign this tag to matching documents. Use keywords, phrases, or regular expressions depending on matching_algorithm."), 24 | matching_algorithm: z.number().int().min(0).max(4).optional().describe("How to match text patterns: 0=any word, 1=all words, 2=exact phrase, 3=regular expression, 4=fuzzy matching. Default is 0 (any word)."), 25 | }, 26 | async (args, extra) => { 27 | if (!api) throw new Error("Please configure API connection first"); 28 | return api.createTag(args); 29 | } 30 | ); 31 | 32 | server.tool( 33 | "update_tag", 34 | "Modify an existing tag's name, color, or automatic matching rules. Useful for refining tag organization and improving automatic document classification.", 35 | { 36 | id: z.number().describe("ID of the tag to update. Use list_tags to find existing tag IDs."), 37 | name: z.string().describe("New tag name. Must be unique among all tags."), 38 | color: z 39 | .string() 40 | .regex(/^#[0-9A-Fa-f]{6}$/) 41 | .optional().describe("New hex color code for visual identification (e.g., '#FF0000' for red). Leave empty to keep current color."), 42 | match: z.string().optional().describe("Text pattern for automatic tag assignment. Empty string removes auto-matching. Use keywords, phrases, or regex depending on matching_algorithm."), 43 | matching_algorithm: z.number().int().min(0).max(4).optional().describe("Algorithm for pattern matching: 0=any word, 1=all words, 2=exact phrase, 3=regular expression, 4=fuzzy matching."), 44 | }, 45 | async (args, extra) => { 46 | if (!api) throw new Error("Please configure API connection first"); 47 | return api.updateTag(args.id, args); 48 | } 49 | ); 50 | 51 | server.tool( 52 | "delete_tag", 53 | "Permanently delete a tag from the system. This removes the tag from all documents that currently use it. Use with caution as this action cannot be undone.", 54 | { 55 | id: z.number().describe("ID of the tag to permanently delete. This will remove the tag from all documents that currently use it. Use list_tags to find tag IDs."), 56 | }, 57 | async (args, extra) => { 58 | if (!api) throw new Error("Please configure API connection first"); 59 | return api.deleteTag(args.id); 60 | } 61 | ); 62 | 63 | server.tool( 64 | "bulk_edit_tags", 65 | "Perform bulk operations on multiple tags: set permissions to control access or permanently delete multiple tags at once. Efficient for managing large tag collections.", 66 | { 67 | tag_ids: z.array(z.number()).describe("Array of tag IDs to perform bulk operations on. Use list_tags to get valid tag IDs."), 68 | operation: z.enum(["set_permissions", "delete"]).describe("Bulk operation: 'set_permissions' to control who can use these tags, 'delete' to permanently remove all specified tags from the system."), 69 | owner: z.number().optional().describe("User ID to set as owner when operation is 'set_permissions'. Owner has full control over the tags."), 70 | permissions: z 71 | .object({ 72 | view: z.object({ 73 | users: z.array(z.number()).optional().describe("User IDs who can see and use these tags"), 74 | groups: z.array(z.number()).optional().describe("Group IDs who can see and use these tags"), 75 | }).describe("Users and groups with view/use permissions for these tags"), 76 | change: z.object({ 77 | users: z.array(z.number()).optional().describe("User IDs who can modify these tags (name, color, matching rules)"), 78 | groups: z.array(z.number()).optional().describe("Group IDs who can modify these tags"), 79 | }).describe("Users and groups with edit permissions for these tags"), 80 | }) 81 | .optional().describe("Permission settings when operation is 'set_permissions'. Defines who can view/use and modify these tags."), 82 | merge: z.boolean().optional().describe("Whether to merge with existing permissions (true) or replace them entirely (false). Default is false."), 83 | }, 84 | async (args, extra) => { 85 | if (!api) throw new Error("Please configure API connection first"); 86 | return api.bulkEditObjects( 87 | args.tag_ids, 88 | "tags", 89 | args.operation, 90 | args.operation === "set_permissions" 91 | ? { 92 | owner: args.owner, 93 | permissions: args.permissions, 94 | merge: args.merge, 95 | } 96 | : {} 97 | ); 98 | } 99 | ); 100 | } 101 | ``` -------------------------------------------------------------------------------- /src/tools/documents.ts: -------------------------------------------------------------------------------- ```typescript 1 | import { z } from "zod"; 2 | 3 | export function registerDocumentTools(server, api) { 4 | server.tool( 5 | "bulk_edit_documents", 6 | "Perform bulk operations on multiple documents simultaneously: set correspondent/type/tags, delete, reprocess, merge, split, rotate, or manage permissions. Efficient for managing large document collections.", 7 | { 8 | documents: z.array(z.number()).describe("Array of document IDs to perform bulk operations on. Get document IDs from list_documents or search_documents first."), 9 | method: z.enum([ 10 | "set_correspondent", 11 | "set_document_type", 12 | "set_storage_path", 13 | "add_tag", 14 | "remove_tag", 15 | "modify_tags", 16 | "delete", 17 | "reprocess", 18 | "set_permissions", 19 | "merge", 20 | "split", 21 | "rotate", 22 | "delete_pages", 23 | ]).describe("The bulk operation to perform: set_correspondent (assign sender/receiver), set_document_type (categorize documents), set_storage_path (organize file location), add_tag/remove_tag/modify_tags (manage labels), delete (permanently remove), reprocess (re-run OCR/indexing), set_permissions (control access), merge (combine documents), split (separate into multiple), rotate (adjust orientation), delete_pages (remove specific pages)"), 24 | correspondent: z.number().optional().describe("ID of correspondent to assign when method is 'set_correspondent'. Use list_correspondents to get valid IDs."), 25 | document_type: z.number().optional().describe("ID of document type to assign when method is 'set_document_type'. Use list_document_types to get valid IDs."), 26 | storage_path: z.number().optional().describe("ID of storage path to assign when method is 'set_storage_path'. Storage paths organize documents in folder hierarchies."), 27 | tag: z.number().optional().describe("Single tag ID to add or remove when method is 'add_tag' or 'remove_tag'. Use list_tags to get valid IDs."), 28 | add_tags: z.array(z.number()).optional().describe("Array of tag IDs to add when method is 'modify_tags'. Use list_tags to get valid IDs."), 29 | remove_tags: z.array(z.number()).optional().describe("Array of tag IDs to remove when method is 'modify_tags'. Use list_tags to get valid IDs."), 30 | permissions: z 31 | .object({ 32 | owner: z.number().nullable().optional().describe("User ID to set as document owner, or null to remove ownership"), 33 | set_permissions: z 34 | .object({ 35 | view: z.object({ 36 | users: z.array(z.number()).describe("User IDs granted view permission"), 37 | groups: z.array(z.number()).describe("Group IDs granted view permission"), 38 | }).describe("Users and groups who can view these documents"), 39 | change: z.object({ 40 | users: z.array(z.number()).describe("User IDs granted edit permission"), 41 | groups: z.array(z.number()).describe("Group IDs granted edit permission"), 42 | }).describe("Users and groups who can edit these documents"), 43 | }) 44 | .optional().describe("Specific permission settings for users and groups"), 45 | merge: z.boolean().optional().describe("Whether to merge with existing permissions (true) or replace them (false)"), 46 | }) 47 | .optional().describe("Permission settings when method is 'set_permissions'. Controls who can view and edit the documents."), 48 | metadata_document_id: z.number().optional().describe("Source document ID when merging documents. The metadata from this document will be preserved."), 49 | delete_originals: z.boolean().optional().describe("Whether to delete original documents after merge/split operations. Use with caution."), 50 | pages: z.string().optional().describe("Page specification for delete_pages method. Format: '1,3,5-7' to delete pages 1, 3, and 5 through 7."), 51 | degrees: z.number().optional().describe("Rotation angle in degrees when method is 'rotate'. Use 90, 180, or 270 for standard rotations."), 52 | }, 53 | async (args, extra) => { 54 | if (!api) throw new Error("Please configure API connection first"); 55 | const { documents, method, ...parameters } = args; 56 | return api.bulkEditDocuments(documents, method, parameters); 57 | } 58 | ); 59 | 60 | server.tool( 61 | "post_document", 62 | "Upload a new document to Paperless-NGX with metadata. Supports PDF, images (PNG/JPG/TIFF), and text files. Automatically processes for OCR and indexing.", 63 | { 64 | file: z.string().describe("Base64 encoded file content. Convert your file to base64 before uploading. Supports PDF, images (PNG, JPG, TIFF), and text files."), 65 | filename: z.string().describe("Original filename with extension (e.g., 'invoice.pdf', 'receipt.png'). This helps Paperless determine file type and initial document title."), 66 | title: z.string().optional().describe("Custom document title. If not provided, Paperless will extract title from filename or document content."), 67 | created: z.string().optional().describe("Document creation date in ISO format (YYYY-MM-DD or YYYY-MM-DDTHH:mm:ss). If not provided, uses current date."), 68 | correspondent: z.number().optional().describe("ID of the correspondent (sender/receiver) for this document. Use list_correspondents to find or create_correspondent to add new ones."), 69 | document_type: z.number().optional().describe("ID of document type for categorization (e.g., Invoice, Receipt, Letter). Use list_document_types to find or create_document_type to add new ones."), 70 | storage_path: z.number().optional().describe("ID of storage path to organize document location in folder hierarchy. Leave empty for default storage."), 71 | tags: z.array(z.number()).optional().describe("Array of tag IDs to label this document. Use list_tags to find existing tags or create_tag to add new ones."), 72 | archive_serial_number: z.string().optional().describe("Custom archive number for document organization and reference. Useful for maintaining external filing systems."), 73 | custom_fields: z.array(z.number()).optional().describe("Array of custom field IDs to associate with this document. Custom fields store additional metadata."), 74 | }, 75 | async (args, extra) => { 76 | if (!api) throw new Error("Please configure API connection first"); 77 | const binaryData = Buffer.from(args.file, "base64"); 78 | const blob = new Blob([binaryData]); 79 | const file = new File([blob], args.filename); 80 | const { file: _, filename: __, ...metadata } = args; 81 | return api.postDocument(file, metadata); 82 | } 83 | ); 84 | 85 | server.tool( 86 | "list_documents", 87 | "Retrieve paginated list of all documents in the system with basic metadata. Use for browsing document collections or getting document IDs for other operations.", 88 | { 89 | page: z.number().optional().describe("Page number for pagination (starts at 1). Use this to browse through large document collections."), 90 | page_size: z.number().optional().describe("Number of documents per page (default 25, max 100). Larger page sizes return more documents but may be slower."), 91 | }, 92 | async (args, extra) => { 93 | if (!api) throw new Error("Please configure API connection first"); 94 | const query = new URLSearchParams(); 95 | if (args.page) query.set("page", args.page); 96 | if (args.page_size) query.set("page_size", args.page_size); 97 | return api.getDocuments(query.toString() ? `?${query.toString()}` : ""); 98 | } 99 | ); 100 | 101 | server.tool( 102 | "get_document", 103 | "Get complete details for a specific document including full metadata, content preview, tags, correspondent, and document type information.", 104 | { 105 | id: z.number().describe("Unique document ID. Get this from list_documents or search_documents results. Returns full document metadata, content preview, and associated tags/correspondent/type."), 106 | }, 107 | async (args, extra) => { 108 | if (!api) throw new Error("Please configure API connection first"); 109 | return api.getDocument(args.id); 110 | } 111 | ); 112 | 113 | server.tool( 114 | "search_documents", 115 | "Search through documents using full-text search across content, titles, tags, and metadata. Supports advanced query syntax and filtering.", 116 | { 117 | query: z.string().describe("Search query to find documents. Supports full-text search through document content, titles, tags, and metadata. Use keywords, phrases in quotes, or advanced syntax like 'tag:important' or 'correspondent:john' for targeted searches."), 118 | }, 119 | async (args, extra) => { 120 | if (!api) throw new Error("Please configure API connection first"); 121 | return api.searchDocuments(args.query); 122 | } 123 | ); 124 | 125 | server.tool( 126 | "download_document", 127 | "Download a document file as base64-encoded data. Choose between original uploaded file or processed/archived version with OCR improvements.", 128 | { 129 | id: z.number().describe("Document ID to download. Get this from list_documents, search_documents, or get_document results."), 130 | original: z.boolean().optional().describe("Whether to download the original uploaded file (true) or the processed/archived version (false, default). Original files preserve exact formatting but may not include OCR improvements."), 131 | }, 132 | async (args, extra) => { 133 | if (!api) throw new Error("Please configure API connection first"); 134 | const response = await api.downloadDocument(args.id, args.original); 135 | return { 136 | blob: Buffer.from(await response.arrayBuffer()).toString("base64"), 137 | filename: 138 | response.headers 139 | .get("content-disposition") 140 | ?.split("filename=")[1] 141 | ?.replace(/"/g, "") || `document-${args.id}`, 142 | }; 143 | } 144 | ); 145 | } 146 | ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | /* Projects */ 5 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 6 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 7 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 8 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 9 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 10 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 11 | /* Language and Environment */ 12 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 13 | "lib": [ 14 | "es2016", 15 | "ES2015" 16 | ], 17 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 18 | // "libReplacement": true, /* Enable lib replacement. */ 19 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 20 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 21 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 22 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 23 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 24 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 25 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 26 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 27 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 28 | /* Modules */ 29 | "module": "CommonJS", 30 | "moduleResolution": "node", 31 | // "rootDir": "./", /* Specify the root folder within your source files. */ 32 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 33 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 34 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 35 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 36 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 37 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 38 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 39 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 40 | // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ 41 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 42 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 43 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 44 | // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ 45 | // "resolveJsonModule": true, /* Enable importing .json files. */ 46 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 47 | // "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */ 48 | /* JavaScript Support */ 49 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 50 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 51 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 52 | /* Emit */ 53 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 54 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 55 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 56 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 57 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 58 | // "noEmit": true, /* Disable emitting files from a compilation. */ 59 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 60 | "outDir": "build", 61 | // "removeComments": true, /* Disable emitting comments. */ 62 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 67 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 68 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 69 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 70 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 71 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 72 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 73 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 74 | /* Interop Constraints */ 75 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 76 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 77 | // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ 78 | // "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */ 79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 80 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 82 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 83 | /* Type Checking */ 84 | "strict": true, /* Enable all strict type-checking options. */ 85 | "noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 86 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 87 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 88 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 89 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 90 | // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ 91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 93 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 94 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 95 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 96 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 97 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 98 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 99 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 100 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 101 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 102 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 103 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 104 | /* Completeness */ 105 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 106 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 107 | } 108 | } ```