Files
bea 84c5bdd90e refactor: modularize codebase — Step 0 structural refactoring
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.
2026-05-11 13:43:36 +02:00

38 KiB
Raw Permalink Blame History

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 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 5MB5GB (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-firstlist_files e get_file_info restituiscono solo metadati JSON
  4. Content on-demandread_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_startchunked_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 <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:

  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:
      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
  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:
      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
  6. Estrazione tool esistenti:

    • src/tools/calendar.tslist_calendars, get_calendar_events, create_calendar_event
    • src/tools/tasks.tsget_tasks, create_task, update_task
    • src/tools/notes.tsget_notes, create_note, get_note_content
    • src/tools/email.tsget_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:

// 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)
  • parsePropfindFilesResponse esclude 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:

    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<d:like><d:prop><d:displayname/></d:prop><d:literal>{query}%</d:literal></d:like>
      • mimeType<d:eq> con d:getcontenttype
      • minSize<d:gt> con oc:size
      • maxSize<d:lt> con oc:size
      • modifiedAfter<d:gt> con d:getlastmodified
      • modifiedBefore<d:lt> con d:getlastmodified
      • favorite === true<d:eq> con oc:favorite = 1
      • Multipli → <d:and> wrapper
      • Nessun filtro → <d:not><d:is-collection/></d:not> (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:

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:

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:

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:

    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="<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:

    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:

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:

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:

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.txtncmcp 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.