Extract monolithic index.ts (~600 lines) into focused modules: - src/types.ts — shared TypeScript interfaces (FileMetadata, QuotaInfo, etc.) - src/utils.ts — path, mime detection, formatting helpers - src/client.ts — NextcloudClient wrapping axios with WebDAV primitives - src/webdav.ts — XML builders + parsers (scaffolding for file tools) - src/tools/index.ts — ToolModule interface + auto registry - src/tools/calendar.ts — extracted calendar tools - src/tools/tasks.ts — extracted task tools - src/tools/notes.ts — extracted note tools - src/tools/email.ts — extracted email tools - src/tools/files.ts — empty scaffolding for upcoming file management tools src/index.ts reduced to ~50 lines: config, client instantiation, module registration, MCP setup. Zero regression on existing tools. Verified: list_calendars, get_notes, create_note, get_note_content all functional.
38 KiB
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 <tool> [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
NextcloudMCPServerinindex.tscon tutti i tool come metodi privati - Modulo separato
caldav.tsper la logica dominio CalDAV (builder XML, parser iCal, tipi) - Registrazione tool:
getTools()restituisce array diTool,setupHandlers()fa switch sul nome - HTTP:
axioscon 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
index.tssta diventando troppo grande — aggiungere 20+ tool file peggiorerà la situazione- Lo switch in
setupHandlers()è ripetitivo — ogni nuovo tool aggiunge un case - Il parser XML basato su regex in
caldav.tsfunziona per CalDAV ma il PROPFIND di WebDAV ha namespace più complessi - L'axios instance ha
Content-Type: application/xmldi default — i tool file che inviano binario (PUT upload) o JSON (Note/Email) lo sovrascrivono caso per caso - 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
- Ogni tool = una singola operazione WebDAV — mappatura 1:1, prevedibile
- Paths relativi alla root utente — il tool riceve
/Documents/file.pdf, il codice risolve in/remote.php/dav/files/{user}/Documents/file.pdf - Metadata-first —
list_fileseget_file_inforestituiscono solo metadati JSON - Content on-demand —
read_filerestituisce contenuto (testo o base64), con soglia dimensione - Base64 per binary — MCP non supporta binary nativamente; file binari vengono ritornati come base64 con metadata
- Follow existing patterns — stesso formato risposta, stessa gestione errori del codice esistente
- 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_filecon PUT diretto - Upload grande (≥10MB o file locali):
chunked_upload_start→chunked_upload_chunk(N volte) →chunked_upload_finish - Read piccolo (<10MB):
read_filerestituisce contenuto inline - Read grande (≥10MB):
read_filerestituisce errore con suggerimentodownload_file;download_filerestituisce URL diretto per scaricare fuori banda
- Upload piccolo (<10MB nel param):
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 <d:where> 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
// === 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:
-
src/types.ts(nuovo):- Tutte le interfacce dalla sezione 4 di questo piano
- Esportare
NextcloudConfig,FileMetadata,ToolResponse, ecc.
-
src/utils.ts(nuovo):normalizePath(path: string): string— slash managementbuildDavPath(username: string, relativePath: string): string— path completo WebDAVbuildDavUrl(baseUrl: string, username: string, relativePath: string): string— URL per header DestinationresolveRelativePath(href: string, davBase: string): string— path relativo da href PROPFINDisTextMimeType(mimeType: string | undefined): booleandetectMimeType(filename: string): string— da estensioneformatFileSize(bytes: number): string— umano-readablegenerateUUID(): string
-
src/client.ts(nuovo):- Classe
NextcloudClientche 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:
propfind(path: string, body: string, depth?: string): Promise<string> get(path: string, headers?: Record<string, string>, responseType?: "text"|"arraybuffer"): Promise<any> put(path: string, data: Buffer|string, headers?: Record<string, string>): Promise<any> mkcol(path: string): Promise<any> delete(path: string): Promise<any> move(source: string, destination: string, overwrite?: boolean): Promise<any> copy(source: string, destination: string, overwrite?: boolean): Promise<any> report(path: string, body: string): Promise<string> search(body: string): Promise<string> proppatch(path: string, body: string): Promise<string> postBulk(body: Buffer, contentType: string): Promise<any> - Ogni metodo gestisce errori HTTP (404, 403, 405, 412, 507) con eccezioni tipizzate
- Il client conosce username e baseURL, i metodi ricevono path relativi
- Classe
-
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:
- Builder XML:
-
src/tools/index.ts(nuovo):- Interfaccia
ToolModule:interface ToolModule { definitions: Tool[]; // JSON schema per MCP handler(name: string, args: any, client: NextcloudClient): Promise<ToolResponse>; } - 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
- Interfaccia
-
Estrazione tool esistenti:
src/tools/calendar.ts—list_calendars,get_calendar_events,create_calendar_eventsrc/tools/tasks.ts—get_tasks,create_task,update_tasksrc/tools/notes.ts—get_notes,create_note,get_note_contentsrc/tools/email.ts—get_emails
-
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 buildcompila senza errori - ✅
ncmcp list_calendarsencmcp get_tasksfunzionano - ✅ 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:
// 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 <d:where> combinando i filtri con <d:and>
Parser XML:
// 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):
// Estrae una proprietà da un blocco <response> 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:
<d:getcontenttype>image/png</d:getcontenttype> - Senza prefisso:
<getcontenttype xmlns="DAV:">image/png</getcontenttype>
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)
- ✅
parsePropfindFilesResponseesclude il primo<response>(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:- Normalizza
path(default:/) - Se
depth === "0": singola risorsa client.propfind(filesPath, buildPropfindBody(properties), depth)parsePropfindFilesResponse(xml, filesPath)- Se
depth === "1": il primo elemento è la cartella stessa → escludere dal risultato
- Normalizza
-
get_file_info:client.propfind(filesPath, buildPropfindExtendedBody(), "0")parsePropfindSingleFileResponse(xml, filesPath)- Include proprietà estese: owner, share-types, checksum, has-preview, mount-type
-
search_files:- Costruisci
SearchOptionsdai parametri del tool client.search(buildSearchRequest(options))parseSearchResponse(xml, basePath)- Logica costruzione where-clause:
query→<d:like><d:prop><d:displayname/></d:prop><d:literal>{query}%</d:literal></d:like>mimeType→<d:eq>cond:getcontenttypeminSize→<d:gt>conoc:sizemaxSize→<d:lt>conoc:sizemodifiedAfter→<d:gt>cond:getlastmodifiedmodifiedBefore→<d:lt>cond:getlastmodifiedfavorite === true→<d:eq>conoc:favorite=1- Multipli →
<d:and>wrapper - Nessun filtro →
<d:not><d:is-collection/></d:not>(solo file)
- Costruisci
-
list_favorites:client.report(filesPath, buildFavoriteFilterBody())parsePropfindFilesResponse(xml, filesPath)
-
get_quota:client.propfind(filesRoot, buildQuotaBody(), "0")parseQuotaResponse(xml)
Test manuali via ncmcp:
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:- Opzionale:
HEADper pre-checkContent-LengthvsmaxSize(evita di scaricare file enormi) - Se size > maxSize → errore:
"File too large (${size} bytes). Use download_file to get a direct URL." client.get(filesPath, {}, "arraybuffer")(sempre come buffer per gestire sia testo che binary)- Determina encoding:
- Se
encodingparam ="base64"→ converti buffer in base64 - Se
encodingparam ="utf8"o non specificato:- Se
isTextMimeType(response.contentType)→ buffer.toString("utf-8") - Se binary → buffer.toString("base64"), encoding = "base64"
- Se
- Se
- Ritorna:
{ metadata, content, encoding, size } - Se maxSize non specificato: default 10MB
- Opzionale:
-
download_file:- Non scarica nulla — costruisce l'URL diretto
url =${config.url}/remote.php/dav/files/${config.username}${path}``- Opzionale:
HEADper ottenere metadata del file - Ritorna:
{ metadata: FileMetadata, downloadUrl: string }
-
download_folder:- Valida che il path sia una cartella (PROPFIND Depth: 0, controlla resourcetype)
- Se
filesfornito: costruisce query params?accept=zip&files=[...] client.get(folderPath, { Accept:application/${format}}, "arraybuffer")- Se response size > maxSize: ritorna URL con query params
- Altrimenti: converti buffer in base64, ritorna content inline
- maxSize default: 50MB
Test manuali:
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:- Se
encoding === "base64":Buffer.from(content, "base64") - Se
encoding === "utf8": usa content come stringa - Determina
Content-Type: dal paramcontentTypeodetectMimeType(path) client.put(filesPath, buffer, { "Content-Type": ct, "X-OC-Mtime": mtime })- Dopo upload:
PROPFIND Depth: 0per ritornare FileMetadata del file creato/aggiornato - Adatto per file fino a ~10MB di content nel param (limite pratico del trasporto MCP)
- Se
-
create_folder:client.mkcol(filesPath)- Gestire 405 (Method Not Allowed = già esiste): ritorna errore chiaro
- Dopo creazione:
PROPFIND Depth: 0per metadata
-
bulk_upload:- Per ogni file: decodifica content (base64→buffer), determina mime type
- 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 client.postBulk(multipartBuffer, "multipart/related; boundary={boundary}")- Parse response JSON:
{"/path": {"error": false, "etag": "..."}} - 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:
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<string, ChunkedUploadSession> in memoria. Non persistente (va bene — scade dopo 24h dal server comunque).
Dettagli implementativi:
-
chunked_upload_start:- Valida
totalSize> 0 - Calcola
totalChunks = Math.ceil(totalSize / chunkSize) - Genera
uploadId = generateUUID() client.mkcol(/uploads/${username}/${uploadId}, { Destination: fullDestinationUrl })- Salva sessione nella mappa:
{ uploadId, destination, totalSize, chunkSize, totalChunks, createdAt } - Ritorna:
{ uploadId, totalChunks, chunkSize }
- Valida
-
chunked_upload_chunk:- Cerca sessione per
uploadId→ errore se non trovata - Valida
chunkIndex(1-based, ≤ totalChunks) - Decodifica content:
Buffer.from(content, "base64") - Valida chunk size: ≥ 5MB per chunk intermedi, ultimo chunk può essere < 5MB
- Formatta chunkIndex a 5 cifre:
String(chunkIndex).padStart(5, "0") client.put(/uploads/${username}/${uploadId}/${chunkIdx}, buffer, { Destination, "OC-Total-Length": totalSize })- Ritorna:
{ success: true, uploadedSize: runningTotal }
- Cerca sessione per
-
chunked_upload_finish:- Cerca sessione → errore se non trovata
client.move(/uploads/${username}/${uploadId}/.file, fullDestinationUrl, { "OC-Total-Length": totalSize, "X-OC-Mtime": mtime })- Il server assembla i chunk e fa cleanup automatico
- Rimuovi sessione dalla mappa
PROPFIND Depth: 0sulla 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="<base64 10MB>")
→ { success: true, uploadedSize: 10485760 }
3. chunked_upload_chunk(uploadId="abc-123", chunkIndex=2, content="<base64 10MB>")
→ { success: true, uploadedSize: 20971520 }
... (ripeti per tutti i chunk)
15. chunked_upload_chunk(uploadId="abc-123", chunkIndex=15, content="<base64 ~4.3MB>")
→ { success: true, uploadedSize: 150000000 }
16. chunked_upload_finish(uploadId="abc-123")
→ { name: "large.mp4", path: "/Videos/large.mp4", type: "file", size: 150000000, ... }
Test manuali:
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:client.move(sourcePath, buildDavUrl(destination), overwrite)- Header
Overwrite: F(default) oOverwrite: T - Gestire 412 (Precondition Failed) → destinazione esiste e overwrite=false
- Gestire 404 (Not Found) → sorgente non esiste
- Dopo: PROPFIND sulla destinazione per metadata aggiornati
-
copy_file:- Stessa logica di move ma con
client.copy()
- Stessa logica di move ma con
-
delete_file:client.delete(filesPath)- Il file finisce nel cestino (non eliminazione permanente)
- Gestire 404, 403
Test manuali:
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:client.propfind(/trashbin/${username}/trash, buildTrashbinPropfindBody(), "1")parseTrashbinResponse(xml)→TrashedFile[]- Il parser estrae:
nc:trashbin-filename,nc:trashbin-original-location,nc:trashbin-deletion-time+ proprietà standard
-
trash_restore:client.move(/trashbin/${username}/trash/${item},${baseUrl}/trashbin/${username}/restore)- Il server ripristina alla posizione originale automaticamente
- Gestire 404 (già eliminato permanentemente)
-
trash_delete:client.delete(/trashbin/${username}/trash/${item})→ eliminazione permanente
-
trash_empty:client.delete(/trashbin/${username}/trash)→ svuota tutto
Test manuali:
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:client.proppatch(filesPath, buildProppatchBody("oc", "favorite", favorite ? "1" : "0"))- PROPFIND Depth: 0 per metadata aggiornato
-
get_file_versions:client.propfind(/versions/${username}/versions/${fileId}, buildVersionsPropfindBody(), "1")parseVersionsResponse(xml)→FileVersion[]
-
restore_file_version:client.move(/versions/${username}/versions/${fileId}/${versionName},${baseUrl}/versions/${username}/restore)
Test manuali:
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:
- Aggiornare la lista tool nel help text (hardcoded o dinamica)
- Supportare
pathcome argomento posizionale:ncmcp read_file /Documents/file.txt≡ncmcp read_file path=/Documents/file.txt - Supportare
contentda stdin per upload:echo "content" | ncmcp upload_file path=/test.txt - Supportare
contentda file locale:ncmcp upload_file path=/remote/test.txt @./local/file.txt(legge il file, lo converte in base64, lo passa come content) - Per
bulk_upload: accettare un JSON file:ncmcp bulk_upload @./files.json - Per
download_file: aggiungere flag--curlche stampa il comando curl da eseguire
Step 10 — Test & Documentazione
Obiettivo: Verifica completa e aggiornamento README.
Cosa fare:
- Test manuale di ogni tool tramite
ncmcpemcporter - 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)
- 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
- Aggiornare
test-connection.jsper 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.