84c5bdd90e
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.
1036 lines
38 KiB
Markdown
1036 lines
38 KiB
Markdown
# 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 | 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 `<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
|
||
|
||
```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<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`:
|
||
```typescript
|
||
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.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 <d:where> combinando i filtri con <d:and>
|
||
```
|
||
|
||
**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 <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**:
|
||
```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<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**:
|
||
```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.
|