# PLAN.md — Nextcloud MCP: File Management Evolutive ## 1. Analisi del Codice Attuale ### Struttura del progetto ``` nextcloud-mcp/ ├── src/ │ ├── index.ts (~600 righe) — Server MCP principale, monolitico │ └── caldav.ts (~450 righe) — Helper CalDAV (XML builder + parser) ├── build/ — Output TypeScript compilato ├── ncmcp.mjs — Wrapper CLI (auto-load .env, sintassi `ncmcp [key=value]`) ├── package.json — deps: @modelcontextprotocol/sdk, axios, date-fns, ical.js, zod ├── tsconfig.json — target ES2022, module Node16, strict └── test-connection.js ``` ### Pattern architetturale esistente - **Classe singola** `NextcloudMCPServer` in `index.ts` con tutti i tool come metodi privati - **Modulo separato** `caldav.ts` per la logica dominio CalDAV (builder XML, parser iCal, tipi) - **Registrazione tool**: `getTools()` restituisce array di `Tool`, `setupHandlers()` fa switch sul nome - **HTTP**: `axios` con basic auth, headers XML di default - **Risposta tool**: `{ content: [{ type: "text", text: JSON.stringify(data, null, 2) }] }` - **Errore**: catch → `{ content: [{ type: "text", text: "Error: ..." }], isError: true }` - **XML parsing**: regex-based (no libreria XML), funziona ma fragile ### Criticità attuali 1. `index.ts` sta diventando troppo grande — aggiungere 20+ tool file peggiorerà la situazione 2. Lo switch in `setupHandlers()` è ripetitivo — ogni nuovo tool aggiunge un case 3. Il parser XML basato su regex in `caldav.ts` funziona per CalDAV ma il PROPFIND di WebDAV ha namespace più complessi 4. L'axios instance ha `Content-Type: application/xml` di default — i tool file che inviano binario (PUT upload) o JSON (Note/Email) lo sovrascrivono caso per caso 5. Nessuna separazione tra definizione tool, logica API e parsing XML --- ## 2. API Nextcloud — Riepilogo Endpoint WebDAV Tutti gli endpoint sono sotto `/remote.php/dav/` con autenticazione Basic Auth. ### Operazioni base (Files) | Operazione | Metodo | Endpoint | Note | |---|---|---|---| | Lista file/cartelle | `PROPFIND` | `/files/{user}/{path}` | `Depth: 1` per listing, `Depth: 0` per singolo | | Info singolo file | `PROPFIND` | `/files/{user}/{path}` | `Depth: 0` | | Download file | `GET` | `/files/{user}/{path}` | Ritorna raw bytes | | Download cartella (ZIP) | `GET` | `/files/{user}/{folder}` | `Accept: application/zip` | | Download selettivo | `GET` | `/files/{user}/{folder}?accept=zip&files=["a.txt","b.png"]` | oppure header `X-NC-Files` | | Upload file | `PUT` | `/files/{user}/{path}` | Body = raw content, sovrascrive se esiste | | Crea cartella | `MKCOL` | `/files/{user}/{path}` | | | Elimina | `DELETE` | `/files/{user}/{path}` | Cartelle eliminate ricorsivamente, finisce nel trashbin | | Sposta/Rinomina | `MOVE` | `/files/{user}/{source}` | Header `Destination: {full_url}` + `Overwrite: T/F` | | Copia | `COPY` | `/files/{user}/{source}` | Header `Destination: {full_url}` + `Overwrite: T/F` | | Imposta preferito | `PROPPATCH` | `/files/{user}/{path}` | `oc:favorite` = 0 o 1 | | Lista preferiti | `REPORT` | `/files/{user}/{path}` | `oc:filter-files` con `oc:favorite=1` | ### Proprietà WebDAV supportate (namespace) | Proprietà | Namespace | Descrizione | |---|---|---| | `displayname` | `d:` | Nome file | | `getcontenttype` | `d:` | MIME type | | `getlastmodified` | `d:` | Data modifica | | `getetag` | `d:` | ETag | | `getcontentlength` | `d:` | Dimensione (solo file) | | `resourcetype` | `d:` | `d:collection` per cartelle | | `fileid` | `oc:` | ID univoco nel server | | `size` | `oc:` | Dimensione (file + cartelle) | | `permissions` | `oc:` | Stringa permessi (S,R,M,G,D,N,V,W,C,K) | | `favorite` | `oc:` | 0 o 1 | | `owner-display-name` | `oc:` | Proprietario | | `share-types` | `oc:` | Array XML tipi share | | `has-preview` | `nc:` | Preview disponibile | | `trashbin-filename` | `nc:` | Nome originale nel cestino | | `trashbin-original-location` | `nc:` | Path originale nel cestino | | `trashbin-deletion-time` | `nc:` | Timestamp eliminazione | ### Ricerca (WebDAV SEARCH, rfc5323) | Aspect | Dettaglio | |---|---| | Endpoint | `SEARCH /remote.php/dav/` | | Content-Type | `text/xml` | | Scope | `/files/{user}/{folder}`, `depth: infinity` | | Filtri supportati | `eq`, `ne`, `lt`, `gt`, `le`, `ge`, `like`, `is-collection`, `and`, `or`, `not` | | Campi cercabili | `displayname`, `getcontenttype`, `getlastmodified`, `creationdate`, `upload_time`, `last_activity`, `size`, `favorite`, `fileid` | | Ordinamento | `orderby` con `ascending/descending` | | Limit | `nresults` nel blocco `limit` | ### Cestino (Trashbin) | Operazione | Metodo | Endpoint | |---|---|---| | Lista cestino | `PROPFIND` | `/trashbin/{user}/trash` | | Ripristina | `MOVE` | `/trashbin/{user}/trash/{item}` → `/trashbin/{user}/restore` | | Elimina dal cestino | `DELETE` | `/trashbin/{user}/trash/{item}` | | Svuota cestino | `DELETE` | `/trashbin/{user}/trash` | ### Versioni file | Operazione | Metodo | Endpoint | |---|---|---| | Lista versioni | `PROPFIND` | `/versions/{user}/versions/{fileId}` | | Ripristina versione | `MOVE` | `/versions/{user}/versions/{version}` → `/versions/{user}/restore` | ### Bulk Upload | Operazione | Metodo | Endpoint | |---|---|---| | Upload multiplo | `POST` | `/remote.php/dav/bulk` | | Content-Type | `multipart/related` | | | Headers per parte | `X-File-Path`, `X-OC-Mtime`, `X-File-Md5`, `Content-Length`, `Content-Type` | | Risposta | JSON: `{"/path": {"error": false, "etag": "..."}}` | | ### Chunked Upload v2 (file grandi) | Operazione | Metodo | Endpoint | |---|---|---| | Crea sessione | `MKCOL` | `/uploads/{user}/{uuid}` + `Destination` header | | Upload chunk | `PUT` | `/uploads/{user}/{uuid}/{NNNNN}` + `Destination` + `OC-Total-Length` | | Assembla | `MOVE` | `/uploads/{user}/{uuid}/.file` + `Destination` + `OC-Total-Length` | | Annulla | `DELETE` | `/uploads/{user}/{uuid}/` | | Limite chunk | 5MB–5GB (ultimo chunk < 5MB ok) | | | Scadenza | 24h di inattività | | --- ## 3. Design dei Tool MCP ### Principi di design 1. **Ogni tool = una singola operazione WebDAV** — mappatura 1:1, prevedibile 2. **Paths relativi alla root utente** — il tool riceve `/Documents/file.pdf`, il codice risolve in `/remote.php/dav/files/{user}/Documents/file.pdf` 3. **Metadata-first** — `list_files` e `get_file_info` restituiscono solo metadati JSON 4. **Content on-demand** — `read_file` restituisce contenuto (testo o base64), con soglia dimensione 5. **Base64 per binary** — MCP non supporta binary nativamente; file binari vengono ritornati come base64 con metadata 6. **Follow existing patterns** — stesso formato risposta, stessa gestione errori del codice esistente 7. **Smart routing per dimensioni** — il limite non è su Nextcloud (che gestisce file di qualsiasi dimensione), ma sul **trasporto MCP** (stdio/JSON). La strategia è: - **Upload piccolo** (<10MB nel param): `upload_file` con PUT diretto - **Upload grande** (≥10MB o file locali): `chunked_upload_start` → `chunked_upload_chunk` (N volte) → `chunked_upload_finish` - **Read piccolo** (<10MB): `read_file` restituisce contenuto inline - **Read grande** (≥10MB): `read_file` restituisce errore con suggerimento `download_file`; `download_file` restituisce URL diretto per scaricare fuori banda ### Lista tool (21 tool totali) #### 🔷 Browsing & Discovery (5 tool) ``` list_files path: string (default: "/") properties?: string[] (default: standard set) depth?: "0"|"1"|"infinity" (default: "1") → FileMetadata[] ``` PROPFIND sulla cartella. Restituisce array di oggetti con: name, path, type (file|folder), size, mimeType, lastModified, etag, fileId, permissions, favorite. ``` get_file_info path: string (obbligatorio) → FileMetadata (singolo oggetto dettagliato) ``` PROPFIND Depth: 0. Include tutte le proprietà disponibili (owner, share-types, checksum, etc.). ``` search_files path?: string (default: "/" — scope ricerca) query?: string (pattern nome, tipo "like") mimeType?: string (esatta o con wildcard "image/%") minSize?: number (bytes) maxSize?: number (bytes) modifiedAfter?: string (ISO 8601) modifiedBefore?: string (ISO 8601) favorite?: boolean sortBy?: "name"|"size"|"lastModified"|"created" sortOrder?: "asc"|"desc" limit?: number (default: 50) → FileMetadata[] ``` SEARCH request (rfc5323). Costruisce dinamicamente il XML `` in base ai parametri forniti. ``` list_favorites path?: string (default: "/" — ricerca ricorsiva) → FileMetadata[] ``` REPORT con `oc:filter-files` + `oc:favorite=1`. ``` get_quota → { used: number, available: number, total?: number } ``` PROPFIND Depth: 0 sulla root utente, richiedendo `d:quota-used-bytes` e `d:quota-available-bytes`. #### 🔷 Read & Download (3 tool) ``` read_file path: string (obbligatorio) encoding?: "utf8"|"base64" (default: "utf8", auto-detect per binary) maxSize?: number (default: 10_485_760 = 10MB, ritorna errore se superato) → { metadata: FileMetadata, content: string, encoding: string, size: number } ``` GET request. Per file testo: ritorna UTF-8. Per file binari o se `encoding="base64"`: ritorna base64. Se il file supera `maxSize`: ritorna errore con suggerimento di usare `download_file` (URL diretto). Il limite è un vincolo del trasporto MCP (JSON su stdio), non di Nextcloud. ``` download_file path: string (obbligatorio) → { metadata: FileMetadata, downloadUrl: string } ``` Restituisce l'URL diretto per download. Utile per file grandi che non possono passare through MCP. L'agent/user scarica il file usando questo URL (curl, browser, wget, ecc.). ``` download_folder path: string (obbligatorio, deve essere cartella) format?: "zip"|"tar" (default: "zip") files?: string[] (sottoinsieme di file da includere, default: tutti) maxSize?: number (default: 50_971_520 = 50MB) → { metadata: { path, format, fileCount, totalSize }, content?: string, encoding: "base64" } oppure se troppo grande: → { metadata: { path, format, fileCount, totalSize }, downloadUrl: string } ``` GET con `Accept: application/zip` (o tar). Se specificato `files`, usa header `X-NC-Files`. Se la cartella supera `maxSize`: ritorna solo URL con parametri query per download esterno. #### 🔷 Write & Upload (3 tool + 3 chunked) ``` upload_file path: string (obbligatorio, path completo incluso nome file) content: string (obbligatorio) encoding: "utf8"|"base64" (default: "utf8") contentType?: string (MIME type, auto-detect se mancante) mtime?: number (unix timestamp, X-OC-Mtime header) → FileMetadata ``` PUT request. Per file fino a ~10MB di content nel param (limite pratico MCP). Il content viene decodificato (base64→buffer o utf8→stringa) e inviato come body. ``` create_folder path: string (obbligatorio) → FileMetadata ``` MKCOL request. ``` bulk_upload files: Array<{ path: string, content: string, encoding?: "utf8"|"base64", contentType?: string, mtime?: number }> → Array<{ path: string, error: boolean, etag?: string, errorMessage?: string }> ``` POST `/remote.php/dav/bulk` con `Content-Type: multipart/related`. Per molti file piccoli. ``` chunked_upload_start path: string (destinazione finale) totalSize: number (dimensione totale in bytes) chunkSize?: number (default: 10_485_760 = 10MB, min 5MB max 5GB) → { uploadId: string, totalChunks: number } ``` Crea directory in `/uploads/{user}/{uuid}` con MKCOL. Calcola automaticamente il numero di chunk. ``` chunked_upload_chunk uploadId: string (dallo start) chunkIndex: number (1-based) content: string (base64 encoded) → { success: boolean, uploadedSize: number } ``` PUT chunk in `/uploads/{user}/{uuid}/{chunkIndex}`. Il content arriva come base64 per compatibilità MCP. ``` chunked_upload_finish uploadId: string mtime?: number (unix timestamp opzionale) → FileMetadata ``` MOVE `.file` per assemblare. Il server unisce i chunk nel file finale e fa cleanup. > Nota: chunked upload è 3 tool ma logicamente un'unica operazione. L'LLM coordina i 3 step. #### 🔷 Move, Copy, Delete (3 tool) ``` move_file source: string (path attuale) destination: string (path destinazione) overwrite?: boolean (default: false) → FileMetadata ``` MOVE request. Serve anche per rinominare (basta cambiare il nome nel path destinazione). ``` copy_file source: string destination: string overwrite?: boolean (default: false) → FileMetadata ``` COPY request. ``` delete_file path: string (obbligatorio) → { success: boolean, path: string } ``` DELETE request. Il file finisce nel cestino (non eliminazione permanente). #### 🔷 Trashbin (4 tool) ``` trash_list → TrashedFile[] ``` PROPFIND su `/trashbin/{user}/trash`. `TrashedFile` aggiunge: originalName, originalLocation, deletionTime. ``` trash_restore trashPath: string (path nel cestino, come restituito da trash_list) → { success: boolean, restoredPath: string } ``` MOVE al folder `/trashbin/{user}/restore`. ``` trash_delete trashPath: string (eliminazione permanente) → { success: boolean } ``` DELETE sul item nel cestino. ``` trash_empty → { success: boolean } ``` DELETE su `/trashbin/{user}/trash`. #### 🔷 Favorites & Versions (3 tool) ``` set_favorite path: string favorite: boolean (true = preferito, false = rimuovi) → FileMetadata ``` PROPPATCH con `oc:favorite`. ``` get_file_versions fileId: number (oc:fileid) → FileVersion[] ``` PROPFIND su `/versions/{user}/versions/{fileId}`. ``` restore_file_version fileId: number versionName: string (timestamp della versione) → { success: boolean } ``` MOVE al folder `/versions/{user}/restore`. --- ## 4. Tipi TypeScript ```typescript // === Core === interface NextcloudConfig { url: string; username: string; password: string; } // === File Metadata === interface FileMetadata { name: string; // filename path: string; // path relativo alla root utente (es. /Documents/file.pdf) type: "file" | "folder"; size?: number; // bytes (oc:size, include cartelle) contentLength?: number; // bytes (d:getcontentlength, solo file) mimeType?: string; lastModified?: string; // ISO 8601 etag?: string; fileId?: number; // oc:fileid permissions?: string; // es. "RGDNVCK" favorite?: boolean; ownerId?: string; ownerDisplayName?: string; hasPreview?: boolean; } // === Cestino === interface TrashedFile extends FileMetadata { originalName: string; // nc:trashbin-filename originalLocation: string; // nc:trashbin-original-location deletionTime: string; // nc:trashbin-deletion-time (unix timestamp) } // === Versioni === interface FileVersion { name: string; // timestamp della versione size: number; lastModified: string; etag?: string; } // === Quota === interface QuotaInfo { used: number; // bytes available: number; // bytes (-1 = uncomputed, -2 = unknown, -3 = unlimited) } // === Upload === interface BulkUploadResult { path: string; error: boolean; etag?: string; errorMessage?: string; } interface ChunkedUploadSession { uploadId: string; destination: string; totalSize: number; chunkSize: number; totalChunks: number; createdAt: number; // unix timestamp } // === Search === interface SearchOptions { path?: string; query?: string; mimeType?: string; minSize?: number; maxSize?: number; modifiedAfter?: string; modifiedBefore?: string; favorite?: boolean; sortBy?: string; sortOrder?: "asc" | "desc"; limit?: number; } // === Tool === interface ToolResponse { content: Array<{ type: "text"; text: string }>; isError?: boolean; } ``` --- ## 5. Piano di Implementazione Step-by-Step ### Step 0 — Refactoring strutturale **Obiettivo**: Ristrutturare il codebase per renderlo modulare e mantenibile prima di aggiungere 21 nuovi tool. **Struttura target**: ``` src/ ├── index.ts — Entry point: config, MCP server setup, tool routing (thin) ├── types.ts — Tutte le interfacce e tipi condivisi ├── client.ts — Nextcloud HTTP client (axios wrapper, metodi WebDAV generici) ├── webdav.ts — WebDAV XML builders + response parsers ├── caldav.ts — CalDAV XML builders + parsers (esistente, pulizia minima) ├── utils.ts — Shared utilities (path, mime detect, formatting) ├── tools/ │ ├── index.ts — Tool registry: definizioni + routing automatico │ ├── calendar.ts — Tool calendario (estratti da index.ts) │ ├── tasks.ts — Tool task (estratti da index.ts) │ ├── notes.ts — Tool note (estratti da index.ts) │ ├── email.ts — Tool email (estratti da index.ts) │ └── files.ts — Tool file (nuovi, implementati negli step successivi) ``` **Cosa fare nel dettaglio**: 1. **`src/types.ts`** (nuovo): - Tutte le interfacce dalla sezione 4 di questo piano - Esportare `NextcloudConfig`, `FileMetadata`, `ToolResponse`, ecc. 2. **`src/utils.ts`** (nuovo): - `normalizePath(path: string): string` — slash management - `buildDavPath(username: string, relativePath: string): string` — path completo WebDAV - `buildDavUrl(baseUrl: string, username: string, relativePath: string): string` — URL per header Destination - `resolveRelativePath(href: string, davBase: string): string` — path relativo da href PROPFIND - `isTextMimeType(mimeType: string | undefined): boolean` - `detectMimeType(filename: string): string` — da estensione - `formatFileSize(bytes: number): string` — umano-readable - `generateUUID(): string` 3. **`src/client.ts`** (nuovo): - Classe `NextcloudClient` che wrappa axios con basic auth - Due axios instance: uno per XML (default), uno per binary (responseType: arraybuffer) - Metodi generici per le operazioni WebDAV di basso livello: ```typescript propfind(path: string, body: string, depth?: string): Promise get(path: string, headers?: Record, responseType?: "text"|"arraybuffer"): Promise put(path: string, data: Buffer|string, headers?: Record): Promise mkcol(path: string): Promise delete(path: string): Promise move(source: string, destination: string, overwrite?: boolean): Promise copy(source: string, destination: string, overwrite?: boolean): Promise report(path: string, body: string): Promise search(body: string): Promise proppatch(path: string, body: string): Promise postBulk(body: Buffer, contentType: string): Promise ``` - Ogni metodo gestisce errori HTTP (404, 403, 405, 412, 507) con eccezioni tipizzate - Il client conosce username e baseURL, i metodi ricevono path relativi 4. **`src/webdav.ts`** (nuovo): - Builder XML: `buildPropfindBody()`, `buildProppatchBody()`, `buildSearchRequest()`, `buildFavoriteFilterRequest()` - Parser XML: `parsePropfindResponse()`, `parsePropfindSingleResponse()`, `parseSearchResponse()`, `parseTrashbinResponse()`, `parseVersionsResponse()` - Funzioni di supporto per estrarre proprietà dai namespace `d:`, `oc:`, `nc:` 5. **`src/tools/index.ts`** (nuovo): - Interfaccia `ToolModule`: ```typescript interface ToolModule { definitions: Tool[]; // JSON schema per MCP handler(name: string, args: any, client: NextcloudClient): Promise; } ``` - Funzione `registerAllTools(modules: ToolModule[]): { tools: Tool[], handler }` che unisce tutte le definizioni e crea il routing handler - Ogni modulo (calendar, tasks, notes, email, files) esporta un `ToolModule` 6. **Estrazione tool esistenti**: - `src/tools/calendar.ts` — `list_calendars`, `get_calendar_events`, `create_calendar_event` - `src/tools/tasks.ts` — `get_tasks`, `create_task`, `update_task` - `src/tools/notes.ts` — `get_notes`, `create_note`, `get_note_content` - `src/tools/email.ts` — `get_emails` 7. **`src/index.ts`** (ridotto): - Legge config da env - Istanzia `NextcloudClient` - Registra tutti i moduli tool - Setup MCP server (handlers + transport) - ~50 righe totali **Criteri di accettazione**: - ✅ Zero regressione: tutti i tool esistenti funzionano identici a prima - ✅ `npm run build` compila senza errori - ✅ `ncmcp list_calendars` e `ncmcp get_tasks` funzionano - ✅ La struttura supporta aggiunta di nuovi tool senza toccare `index.ts` --- ### Step 1 — Modulo `src/webdav.ts` (XML helpers + parser) **Obiettivo**: Implementare builder e parser XML per tutte le operazioni file WebDAV. **Builder XML**: ```typescript // PROPFIND standard — richiedere proprietà specifiche buildPropfindBody(selectedProps?: string[]): string // Se selectedProps vuoto: usa il set standard (displayname, getcontenttype, getlastmodified, getetag, getcontentlength, resourcetype, oc:fileid, oc:size, oc:permissions, oc:favorite) // PROPFIND esteso — per get_file_info (include owner, checksum, has-preview, etc.) buildPropfindExtendedBody(): string // PROPFIND trashbin — proprietà cestino buildTrashbinPropfindBody(): string // PROPFIND versions — proprietà versioni buildVersionsPropfindBody(): string // PROPPATCH — impostare una proprietà buildProppatchBody(namespace: string, property: string, value: string): string // es: buildProppatchBody("oc", "favorite", "1") // REPORT — filtro preferiti buildFavoriteFilterBody(selectedProps?: string[]): string // SEARCH — query generica buildSearchRequest(options: SearchOptions): string // Costruisce dinamicamente combinando i filtri con ``` **Parser XML**: ```typescript // PROPFIND response → array FileMetadata (esclude la cartella root dal risultato) parsePropfindFilesResponse(xml: string, basePath: string): FileMetadata[] // PROPFIND Depth:0 response → singolo FileMetadata parsePropfindSingleFileResponse(xml: string, basePath: string): FileMetadata // PROPFIND response con quota → QuotaInfo parseQuotaResponse(xml: string): QuotaInfo // SEARCH response → array FileMetadata parseSearchResponse(xml: string, basePath: string): FileMetadata[] // PROPFIND trashbin → array TrashedFile parseTrashbinResponse(xml: string): TrashedFile[] // PROPFIND versions → array FileVersion parseVersionsResponse(xml: string): FileVersion[] ``` **Funzioni di estrazione proprietà** (usate internamente dai parser): ```typescript // Estrae una proprietà da un blocco XML extractProperty(block: string, namespace: string, property: string): string | null // Estrae proprietà booleana extractBooleanProperty(block: string, namespace: string, property: string): boolean // Estrae proprietà numerica extractNumericProperty(block: string, namespace: string, property: string): number | null ``` **Approccio al parsing**: regex-based come in `caldav.ts`. I namespace XML (`d:`, `oc:`, `nc:`) possono avere o meno prefisso, quindi i regex devono gestire entrambi i casi: - Con prefisso: `image/png` - Senza prefisso: `image/png` **Dipendenze**: nessuna nuova. **Criteri di accettazione**: - ✅ Ogni builder produce XML valido verificabile con curl - ✅ I parser gestiscono namespace con e senza prefisso - ✅ I parser non crashano su proprietà mancanti (fallback a undefined/null) - ✅ `parsePropfindFilesResponse` esclude il primo `` (la cartella root) --- ### Step 2 — Tool Browsing & Discovery **Obiettivo**: Implementare i 5 tool di base per esplorare i file. **File**: `src/tools/files.ts` (inizio) **Tool**: `list_files`, `get_file_info`, `search_files`, `list_favorites`, `get_quota` **Implementazione**: - **`list_files`**: 1. Normalizza `path` (default: `/`) 2. Se `depth === "0"`: singola risorsa 3. `client.propfind(filesPath, buildPropfindBody(properties), depth)` 4. `parsePropfindFilesResponse(xml, filesPath)` 5. Se `depth === "1"`: il primo elemento è la cartella stessa → escludere dal risultato - **`get_file_info`**: 1. `client.propfind(filesPath, buildPropfindExtendedBody(), "0")` 2. `parsePropfindSingleFileResponse(xml, filesPath)` 3. Include proprietà estese: owner, share-types, checksum, has-preview, mount-type - **`search_files`**: 1. Costruisci `SearchOptions` dai parametri del tool 2. `client.search(buildSearchRequest(options))` 3. `parseSearchResponse(xml, basePath)` 4. Logica costruzione where-clause: - `query` → `{query}%` - `mimeType` → `` con `d:getcontenttype` - `minSize` → `` con `oc:size` - `maxSize` → `` con `oc:size` - `modifiedAfter` → `` con `d:getlastmodified` - `modifiedBefore` → `` con `d:getlastmodified` - `favorite === true` → `` con `oc:favorite` = `1` - Multipli → `` wrapper - Nessun filtro → `` (solo file) - **`list_favorites`**: 1. `client.report(filesPath, buildFavoriteFilterBody())` 2. `parsePropfindFilesResponse(xml, filesPath)` - **`get_quota`**: 1. `client.propfind(filesRoot, buildQuotaBody(), "0")` 2. `parseQuotaResponse(xml)` **Test manuali via ncmcp**: ```bash ncmcp list_files path=/Documents ncmcp list_files path=/ depth=infinity ncmcp get_file_info path=/Documents/report.pdf ncmcp search_files query="foto" mimeType="image/%" path=/Photos limit=10 ncmcp search_files modifiedAfter="2026-01-01T00:00:00Z" sortBy=size sortOrder=desc limit=5 ncmcp list_favorites ncmcp get_quota ``` --- ### Step 3 — Tool Read & Download **Obiettivo**: Leggere contenuto file e gestire download (piccoli e grandi). **File**: `src/tools/files.ts` (continuazione) **Tool**: `read_file`, `download_file`, `download_folder` **Dettagli implementativi**: - **`read_file`**: 1. Opzionale: `HEAD` per pre-check `Content-Length` vs `maxSize` (evita di scaricare file enormi) 2. Se size > maxSize → errore: `"File too large (${size} bytes). Use download_file to get a direct URL."` 3. `client.get(filesPath, {}, "arraybuffer")` (sempre come buffer per gestire sia testo che binary) 4. Determina encoding: - Se `encoding` param = `"base64"` → converti buffer in base64 - Se `encoding` param = `"utf8"` o non specificato: - Se `isTextMimeType(response.contentType)` → buffer.toString("utf-8") - Se binary → buffer.toString("base64"), encoding = "base64" 5. Ritorna: `{ metadata, content, encoding, size }` 6. Se maxSize non specificato: default 10MB - **`download_file`**: 1. Non scarica nulla — costruisce l'URL diretto 2. `url = `${config.url}/remote.php/dav/files/${config.username}${path}`` 3. Opzionale: `HEAD` per ottenere metadata del file 4. Ritorna: `{ metadata: FileMetadata, downloadUrl: string }` - **`download_folder`**: 1. Valida che il path sia una cartella (PROPFIND Depth: 0, controlla resourcetype) 2. Se `files` fornito: costruisce query params `?accept=zip&files=[...]` 3. `client.get(folderPath, { Accept: `application/${format}` }, "arraybuffer")` 4. Se response size > maxSize: ritorna URL con query params 5. Altrimenti: converti buffer in base64, ritorna content inline 6. maxSize default: 50MB **Test manuali**: ```bash ncmcp read_file path=/Documents/notes.txt ncmcp read_file path=/Documents/photo.jpg encoding=base64 ncmcp read_file path=/Documents/huge.zip # dovrebbe suggerire download_file ncmcp download_file path=/Documents/video.mp4 # ritorna URL ncmcp download_folder path=/Photos format=zip ncmcp download_folder path=/Photos files='["IMG_001.jpg","IMG_002.jpg"]' format=zip ``` --- ### Step 4 — Tool Write & Upload (diretto) **Obiettivo**: Upload file singoli, crea cartelle, bulk upload. **File**: `src/tools/files.ts` (continuazione) **Tool**: `upload_file`, `create_folder`, `bulk_upload` **Dettagli implementativi**: - **`upload_file`**: 1. Se `encoding === "base64"`: `Buffer.from(content, "base64")` 2. Se `encoding === "utf8"`: usa content come stringa 3. Determina `Content-Type`: dal param `contentType` o `detectMimeType(path)` 4. `client.put(filesPath, buffer, { "Content-Type": ct, "X-OC-Mtime": mtime })` 5. Dopo upload: `PROPFIND Depth: 0` per ritornare FileMetadata del file creato/aggiornato 6. Adatto per file fino a ~10MB di content nel param (limite pratico del trasporto MCP) - **`create_folder`**: 1. `client.mkcol(filesPath)` 2. Gestire 405 (Method Not Allowed = già esiste): ritorna errore chiaro 3. Dopo creazione: `PROPFIND Depth: 0` per metadata - **`bulk_upload`**: 1. Per ogni file: decodifica content (base64→buffer), determina mime type 2. Costruisci multipart/related body manualmente: ``` --{boundary}\r\n X-File-Path: /Documents/file1.txt\r\n X-OC-Mtime: 1675789581\r\n Content-Length: 1234\r\n Content-Type: text/plain\r\n \r\n {raw file bytes}\r\n --{boundary}--\r\n ``` 3. `client.postBulk(multipartBuffer, "multipart/related; boundary={boundary}")` 4. Parse response JSON: `{"/path": {"error": false, "etag": "..."}}` 5. Mappa in `BulkUploadResult[]` **Nota su multipart/related**: axios non gestisce multipart/related. Costruire il body manualmente come Buffer concatenando le parti con i boundary. È semplice e dà controllo totale. **Test manuali**: ```bash ncmcp create_folder path=/Documents/NewFolder ncmcp upload_file path=/Documents/test.txt content="Hello World" ncmcp upload_file path=/Documents/image.jpg encoding=base64 content="$(base64 -w0 photo.jpg)" ncmcp bulk_upload files='[{"path":"/test1.txt","content":"a"},{"path":"/test2.txt","content":"b"}]' ``` --- ### Step 5 — Tool Chunked Upload (file grandi) **Obiettivo**: Supporto upload resumable per file grandi tramite chunked upload v2. **File**: `src/tools/files.ts` (continuazione) **Tool**: `chunked_upload_start`, `chunked_upload_chunk`, `chunked_upload_finish` **State management**: le sessioni di upload attive vengono tenute in una `Map` in memoria. Non persistente (va bene — scade dopo 24h dal server comunque). **Dettagli implementativi**: - **`chunked_upload_start`**: 1. Valida `totalSize` > 0 2. Calcola `totalChunks = Math.ceil(totalSize / chunkSize)` 3. Genera `uploadId = generateUUID()` 4. `client.mkcol(`/uploads/${username}/${uploadId}`, { Destination: fullDestinationUrl })` 5. Salva sessione nella mappa: `{ uploadId, destination, totalSize, chunkSize, totalChunks, createdAt }` 6. Ritorna: `{ uploadId, totalChunks, chunkSize }` - **`chunked_upload_chunk`**: 1. Cerca sessione per `uploadId` → errore se non trovata 2. Valida `chunkIndex` (1-based, ≤ totalChunks) 3. Decodifica content: `Buffer.from(content, "base64")` 4. Valida chunk size: ≥ 5MB per chunk intermedi, ultimo chunk può essere < 5MB 5. Formatta chunkIndex a 5 cifre: `String(chunkIndex).padStart(5, "0")` 6. `client.put(`/uploads/${username}/${uploadId}/${chunkIdx}`, buffer, { Destination, "OC-Total-Length": totalSize })` 7. Ritorna: `{ success: true, uploadedSize: runningTotal }` - **`chunked_upload_finish`**: 1. Cerca sessione → errore se non trovata 2. `client.move(`/uploads/${username}/${uploadId}/.file`, fullDestinationUrl, { "OC-Total-Length": totalSize, "X-OC-Mtime": mtime })` 3. Il server assembla i chunk e fa cleanup automatico 4. Rimuovi sessione dalla mappa 5. `PROPFIND Depth: 0` sulla destinazione per ritornare FileMetadata **Flusso tipico LLM**: ``` 1. chunked_upload_start(path="/Videos/large.mp4", totalSize=150000000) → { uploadId: "abc-123", totalChunks: 15, chunkSize: 10485760 } 2. chunked_upload_chunk(uploadId="abc-123", chunkIndex=1, content="") → { success: true, uploadedSize: 10485760 } 3. chunked_upload_chunk(uploadId="abc-123", chunkIndex=2, content="") → { success: true, uploadedSize: 20971520 } ... (ripeti per tutti i chunk) 15. chunked_upload_chunk(uploadId="abc-123", chunkIndex=15, content="") → { success: true, uploadedSize: 150000000 } 16. chunked_upload_finish(uploadId="abc-123") → { name: "large.mp4", path: "/Videos/large.mp4", type: "file", size: 150000000, ... } ``` **Test manuali**: ```bash ncmcp chunked_upload_start path=/Videos/test.mp4 totalSize=20971520 ncmcp chunked_upload_chunk uploadId=UUID chunkIndex=1 content="$(base64 -w0 chunk1.bin)" ncmcp chunked_upload_chunk uploadId=UUID chunkIndex=2 content="$(base64 -w0 chunk2.bin)" ncmcp chunked_upload_finish uploadId=UUID ``` --- ### Step 6 — Tool Move, Copy, Delete **Obiettivo**: Operazioni di spostamento, copia ed eliminazione. **File**: `src/tools/files.ts` (continuazione) **Tool**: `move_file`, `copy_file`, `delete_file` **Dettagli implementativi**: - **`move_file`**: 1. `client.move(sourcePath, buildDavUrl(destination), overwrite)` 2. Header `Overwrite: F` (default) o `Overwrite: T` 3. Gestire 412 (Precondition Failed) → destinazione esiste e overwrite=false 4. Gestire 404 (Not Found) → sorgente non esiste 5. Dopo: PROPFIND sulla destinazione per metadata aggiornati - **`copy_file`**: 1. Stessa logica di move ma con `client.copy()` - **`delete_file`**: 1. `client.delete(filesPath)` 2. Il file finisce nel cestino (non eliminazione permanente) 3. Gestire 404, 403 **Test manuali**: ```bash ncmcp move_file source=/Documents/old.txt destination=/Documents/new.txt ncmcp move_file source=/Documents/file.txt destination=/Archive/file.txt ncmcp copy_file source=/template.docx destination=/Documents/new-doc.docx ncmcp delete_file path=/Documents/old-file.txt ``` --- ### Step 7 — Tool Trashbin **Obiettivo**: Gestione completa del cestino. **File**: `src/tools/files.ts` (continuazione) **Tool**: `trash_list`, `trash_restore`, `trash_delete`, `trash_empty` **Dettagli implementativi**: - **`trash_list`**: 1. `client.propfind(`/trashbin/${username}/trash`, buildTrashbinPropfindBody(), "1")` 2. `parseTrashbinResponse(xml)` → `TrashedFile[]` 3. Il parser estrae: `nc:trashbin-filename`, `nc:trashbin-original-location`, `nc:trashbin-deletion-time` + proprietà standard - **`trash_restore`**: 1. `client.move(`/trashbin/${username}/trash/${item}`, `${baseUrl}/trashbin/${username}/restore`)` 2. Il server ripristina alla posizione originale automaticamente 3. Gestire 404 (già eliminato permanentemente) - **`trash_delete`**: 1. `client.delete(`/trashbin/${username}/trash/${item}`)` → eliminazione permanente - **`trash_empty`**: 1. `client.delete(`/trashbin/${username}/trash`)` → svuota tutto **Test manuali**: ```bash ncmcp trash_list ncmcp trash_restore trashPath=/Documents/old-file.txt ncmcp trash_delete trashPath=/Documents/old-file.txt ncmcp trash_empty ``` --- ### Step 8 — Tool Favorites & Versions **Obiettivo**: Preferiti e versioning file. **File**: `src/tools/files.ts` (continuazione) **Tool**: `set_favorite`, `get_file_versions`, `restore_file_version` **Dettagli implementativi**: - **`set_favorite`**: 1. `client.proppatch(filesPath, buildProppatchBody("oc", "favorite", favorite ? "1" : "0"))` 2. PROPFIND Depth: 0 per metadata aggiornato - **`get_file_versions`**: 1. `client.propfind(`/versions/${username}/versions/${fileId}`, buildVersionsPropfindBody(), "1")` 2. `parseVersionsResponse(xml)` → `FileVersion[]` - **`restore_file_version`**: 1. `client.move(`/versions/${username}/versions/${fileId}/${versionName}`, `${baseUrl}/versions/${username}/restore`)` **Test manuali**: ```bash ncmcp set_favorite path=/Documents/important.pdf favorite=true ncmcp set_favorite path=/Documents/important.pdf favorite=false ncmcp get_file_versions fileId=12345 ncmcp restore_file_version fileId=12345 versionName="1675789581" ``` --- ### Step 9 — Aggiornamento `ncmcp.mjs` **Obiettivo**: Aggiornare il wrapper CLI per supportare tutti i nuovi tool. **Modifiche**: 1. Aggiornare la lista tool nel help text (hardcoded o dinamica) 2. Supportare `path` come argomento posizionale: `ncmcp read_file /Documents/file.txt` ≡ `ncmcp read_file path=/Documents/file.txt` 3. Supportare `content` da stdin per upload: `echo "content" | ncmcp upload_file path=/test.txt` 4. Supportare `content` da file locale: `ncmcp upload_file path=/remote/test.txt @./local/file.txt` (legge il file, lo converte in base64, lo passa come content) 5. Per `bulk_upload`: accettare un JSON file: `ncmcp bulk_upload @./files.json` 6. Per `download_file`: aggiungere flag `--curl` che stampa il comando curl da eseguire --- ### Step 10 — Test & Documentazione **Obiettivo**: Verifica completa e aggiornamento README. **Cosa fare**: 1. Test manuale di ogni tool tramite `ncmcp` e `mcporter` 2. Test edge cases: - Path con spazi e caratteri speciali (urlencode) - File con MIME type insoliti - Cartelle vuote - File grandi (chunked upload end-to-end) - Cestino vuoto - Ricerca senza risultati - Spostamento con destinazione esistente (overwrite true/false) - Chunked upload con sessione scaduta (24h) 3. Aggiornare README.md con: - Lista completa dei tool (tutti i 21) - Tabella quick-reference (tool → operazione WebDAV) - Esempi d'uso per ogni categoria - Note sui limiti di dimensione MCP 4. Aggiornare `test-connection.js` per verificare anche le operazioni file --- ## 6. Strategia Upload/Download per Dimensioni Diagramma decisionale per l'LLM: ``` UPLOAD FILE ├─ Content disponibile nel param (≤~10MB) → upload_file (PUT diretto) ├─ Content troppo grande per il param → chunked_upload_start/chunk/finish └─ Molti file piccoli → bulk_upload (multipart/related) READ FILE ├─ File ≤ maxSize (default 10MB) → read_file (content inline, utf8 o base64) └─ File > maxSize → download_file (URL diretto, scaricare fuori banda) DOWNLOAD FOLDER ├─ Cartella ≤ maxSize (default 50MB) → download_folder (ZIP/TAR inline, base64) └─ Cartella > maxSize → download_folder (URL diretto con query params) ``` **Nota**: i limiti (`maxSize`) sono configurabili per-tool. Non sono limiti di Nextcloud (che supporta qualsiasi dimensione), ma limiti pratici del trasporto MCP dove tutto passa come JSON su stdio.