feat(webdav): add archive member tools and temp-download#722
feat(webdav): add archive member tools and temp-download#722jospoortvliet wants to merge 13 commits into
Conversation
…uidance Adds four new MCP tools for working with non-text files stored in Nextcloud, and rewrites the nc_webdav_read_file docstring so Claude never wastes tokens trying to interpret base64-encoded binary blobs. New tools: nc_webdav_list_archive_members, nc_webdav_read_archive_member, nc_webdav_download_to_temp, nc_webdav_cleanup_temp. nc_webdav_list_archive_members(path): lists files inside any ZIP-based archive (ODS, ODT, ODP, DOCX, XLSX, PPTX, EPUB, ZIP, JAR) using stdlib zipfile. nc_webdav_read_archive_member(path, member_path): downloads the archive and extracts exactly one member. XML members (content.xml in ODS etc.) are returned as UTF-8 text. The full archive never enters the context window. nc_webdav_download_to_temp(path): downloads any file to a local temp path and returns that path, for use with shell tools (ffmpeg, pdftotext, exiftool ...). Docstring makes it explicit this requires local shell/Bash access to be useful. nc_webdav_cleanup_temp(local_path): removes a temp file. Only paths tracked in _temp_registry can be removed (no arbitrary path deletion). Also rewrites the nc_webdav_read_file docstring with explicit use/avoid sections so Claude picks the right tool without trial-and-error on binary files. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
There was a problem hiding this comment.
Pull request overview
Adds new WebDAV helper tools for working with ZIP-based office files and for downloading arbitrary Nextcloud files to a local temp path, plus updates nc_webdav_read_file guidance to steer usage away from large/binary blobs.
Changes:
- Added tools to list ZIP archive members and extract a single member inline.
- Added tools to download a Nextcloud file to a local temp file and clean it up safely.
- Rewrote
nc_webdav_read_filedocstring with “use/avoid” guidance by file type.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ❌ Do NOT use this tool for: | ||
| - ZIP-based office formats (ODS, ODT, ODP, DOCX, XLSX, PPTX, EPUB …). | ||
| The raw archive bytes are meaningless in context. Use | ||
| nc_webdav_list_archive_members + nc_webdav_read_archive_member instead. | ||
| - Images (PNG, JPEG, GIF, TIFF, HEIC, RAW …). | ||
| Binary image data cannot be interpreted here. Use | ||
| nc_webdav_download_to_temp and process locally with tools such as | ||
| `convert`, `exiftool`, or `ffmpeg` — only if you have local shell access. | ||
| - Audio or video files (MP4, MKV, MP3, FLAC …). | ||
| Use nc_webdav_download_to_temp + `ffmpeg`/`ffprobe` if you have shell | ||
| access; otherwise these files cannot be processed via MCP. |
There was a problem hiding this comment.
The updated nc_webdav_read_file docstring advises never using it for DOCX/XLSX/PPTX/EPUB and images, but the implementation will attempt parse_document() for any is_parseable_document(content_type) (e.g., Unstructured supports DOCX/XLSX/PPTX/ODT/EPUB and some image types). Please adjust the guidance to be conditional (e.g., “avoid unless document processing is enabled / configured for that MIME type”), so it matches actual behavior.
- Remove _ZIP_MIME_TYPES constant (was unused dead code; BadZipFile already handles invalid formats gracefully) - Add atexit handler _cleanup_temp_files_on_exit() so temp files registered in _temp_registry are actually removed on process exit - Fix _temp_registry.discard() ordering in nc_webdav_cleanup_temp: only discard after a successful os.unlink() or FileNotFoundError, so a failed unlink (OSError) leaves the path retryable - Change idempotentHint=True → False on nc_webdav_cleanup_temp; a second call returns an error (path no longer in registry), which is not idempotent behaviour - Clarify nc_webdav_read_file docstring: office formats CAN be parsed inline when ENABLE_DOCUMENT_PROCESSING is on and a processor supports the type (e.g. Unstructured for DOCX); archive tools are the fallback when doc-processing is disabled or unsupported for the type - Add unit tests for all new logic: atexit cleanup, zipfile member listing/reading/error paths, and temp-registry enforcement Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
- nc_webdav_read_archive_member: rename unused 'content_type' return value to '_' (Ruff F841 / unused-variable) - Add _MAX_MEMBER_BYTES = 50 MB module constant; check ZipInfo.file_size via zf.getinfo() *before* zf.read() so an oversized member (including zip-bomb expansions) raises ValueError with a clear message pointing to nc_webdav_download_to_temp for local extraction - Add unit tests: ZipInfo.file_size is readable pre-extraction, _MAX_MEMBER_BYTES is positive, monkeypatched low limit triggers the expected ValueError Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @pytest.mark.unit | ||
| def test_cleanup_temp_rejects_unregistered_path(tmp_path): | ||
| """cleanup_temp refuses paths not in _temp_registry.""" | ||
| p = tmp_path / "arbitrary.bin" | ||
| p.write_bytes(b"secret") | ||
|
|
||
| path = str(p) | ||
| assert path not in webdav_module._temp_registry | ||
|
|
||
| # Simulate what cleanup_temp does for unregistered paths | ||
| if path not in webdav_module._temp_registry: | ||
| result = { | ||
| "status": "error", | ||
| "local_path": path, | ||
| "message": "Path was not created by nc_webdav_download_to_temp in this session, or has already been cleaned up.", | ||
| } | ||
| assert result["status"] == "error" | ||
| assert os.path.exists(path) # file must NOT have been removed | ||
|
|
There was a problem hiding this comment.
Several tests here don’t exercise production code and therefore won’t detect regressions. For example, this test constructs the expected error dict via an if block instead of calling nc_webdav_cleanup_temp and asserting its real output. Prefer invoking the actual tool logic (or a factored-out helper) so the test fails if the implementation changes.
| @pytest.mark.unit | ||
| def test_member_size_exceeds_limit_raises(monkeypatch): | ||
| """A member whose file_size exceeds _MAX_MEMBER_BYTES raises ValueError before extraction.""" | ||
| # Lower the limit to 10 bytes for this test | ||
| monkeypatch.setattr(webdav_module, "_MAX_MEMBER_BYTES", 10) | ||
|
|
||
| large_content = b"x" * 100 | ||
| archive = make_zip({"big.xml": large_content}) | ||
|
|
||
| with zipfile.ZipFile(io.BytesIO(archive)) as zf: | ||
| info = zf.getinfo("big.xml") | ||
| if info.file_size > webdav_module._MAX_MEMBER_BYTES: | ||
| with pytest.raises(ValueError, match="exceeds the"): | ||
| raise ValueError( | ||
| f"Member 'big.xml' uncompressed size " | ||
| f"({info.file_size:,} bytes) exceeds the " | ||
| f"{webdav_module._MAX_MEMBER_BYTES // (1024 * 1024)} MB limit." | ||
| ) | ||
|
|
There was a problem hiding this comment.
This test raises a ValueError manually after checking ZipInfo.file_size, rather than calling the real code path that enforces _MAX_MEMBER_BYTES in nc_webdav_read_archive_member. As written, it will pass even if the tool stops enforcing the limit. Consider testing the actual function/tool behavior (with a mocked client.webdav.read_file returning the archive bytes) and asserting that it raises the expected ValueError.
- Extract _list_zip_members() and _read_zip_member() as module-level pure
functions so unit tests can call real production code directly, not just
stdlib zipfile behavior in isolation.
- Fix dotfile extension detection: os.path.splitext("_rels/.rels") returns ""
because Python treats the basename as a hidden file; now the whole basename
is used as the extension when splitext yields nothing for a dotfile.
- Expand _TEXT_EXTENSIONS to include .rels, .opf, .ncx, .xhtml, .rdf, .plist
so OOXML relationship files and EPUB packaging files are returned as UTF-8
text rather than base64.
- Rewrite unit tests to import and call the helpers directly; covers member
listing structure, bad-zip errors, text/binary detection per extension,
size-limit enforcement, atexit cleanup, and registry guard semantics.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
|
Hey @jospoortvliet thanks for the contribution - this is a significant ergonomic improvement for various file types that were previously 'useless' as WebDAV tool responses. I'm curious about how this would work over streamable http - the docstrings specifically mention shell access, but this isn't a viable option when responses are provided over a network. I haven't tested this yet, but it appears to focus on stdio transport where the processed file is accessible to an LLM after a tool call completes on the users machine. How would this work for streamable http tool call responses? |
nc_webdav_download_to_temp writes to the MCP server's local filesystem. Over a remote streamable-HTTP connection the temp path is inaccessible to the client's shell tools. Add an explicit note in the docstring so callers understand this is only meaningful in stdio/localhost mode. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
|
Great question @cbcoutinho — the two pairs of tools behave very differently over streamable HTTP: nc_webdav_list_archive_members / nc_webdav_read_archive_member — work fine over any transport. The MCP server downloads the archive from Nextcloud, unpacks the requested member entirely in memory, and returns the content (UTF-8 text or base64) in the tool response. The client never touches a filesystem. nc_webdav_download_to_temp / nc_webdav_cleanup_temp — only meaningful when the server runs as a local process (stdio or localhost SSE). The temp file is written to the server's filesystem; over a remote streamable-HTTP connection the returned path is on a machine the client's shell tools cannot reach, so the path is useless. You are right that the primary motivation is the .mcpb / stdio use-case. I have just pushed a docstring update to nc_webdav_download_to_temp that explicitly calls out the streamable-HTTP limitation so callers get a clear warning up front. The archive-member tools have no such restriction and work across all transports. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| client = await get_client(ctx) | ||
| content, content_type = await client.webdav.read_file(path) | ||
|
|
||
| filename = os.path.basename(path.rstrip("/")) | ||
| _root, suffix = os.path.splitext(filename) | ||
|
|
||
| fd, local_path = tempfile.mkstemp(suffix=suffix, prefix="nc_download_") | ||
| try: | ||
| with os.fdopen(fd, "wb") as fh: | ||
| fh.write(content) |
There was a problem hiding this comment.
nc_webdav_download_to_temp currently loads the entire remote file into memory via client.webdav.read_file() before writing it to disk. This undermines the stated use-case for large binaries and can cause high RAM usage/OOM for big files. Consider streaming the WebDAV GET directly to the temp file (e.g., using an httpx stream API in the client) or enforcing a configurable maximum download size with a clear error.
| def test_cleanup_temp_rejects_unregistered_path(tmp_path): | ||
| """Paths not in _temp_registry must not be removed.""" | ||
| p = tmp_path / "arbitrary.bin" | ||
| p.write_bytes(b"secret") | ||
| path = str(p) | ||
|
|
||
| assert path not in _temp_registry | ||
| # Verify the guard condition the tool uses | ||
| assert path not in _temp_registry | ||
| # File is untouched | ||
| assert os.path.exists(path) | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| def test_cleanup_temp_discard_only_after_successful_unlink(tmp_path): | ||
| """Registry entry is removed only after os.unlink() succeeds.""" | ||
| p = tmp_path / "nc_download_test.bin" | ||
| p.write_bytes(b"payload") | ||
| path = str(p) | ||
| _temp_registry.add(path) | ||
|
|
||
| try: | ||
| os.unlink(path) | ||
| _temp_registry.discard(path) | ||
|
|
||
| assert not os.path.exists(path) | ||
| assert path not in _temp_registry | ||
| finally: | ||
| _temp_registry.discard(path) | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| def test_cleanup_temp_registry_preserved_on_oserror(tmp_path, monkeypatch): | ||
| """Registry entry is NOT discarded when os.unlink raises OSError (allows retry).""" | ||
| p = tmp_path / "nc_download_locked.bin" | ||
| p.write_bytes(b"payload") | ||
| path = str(p) | ||
| _temp_registry.add(path) | ||
|
|
||
| def _raise(*_a, **_kw): | ||
| raise OSError("permission denied") | ||
|
|
||
| monkeypatch.setattr(os, "unlink", _raise) | ||
|
|
||
| try: | ||
| try: | ||
| os.unlink(path) | ||
| _temp_registry.discard(path) | ||
| except OSError: | ||
| pass # do NOT discard | ||
|
|
||
| assert path in _temp_registry | ||
| finally: | ||
| _temp_registry.discard(path) | ||
| monkeypatch.undo() | ||
| if p.exists(): | ||
| p.unlink() | ||
|
|
||
|
|
||
| @pytest.mark.unit | ||
| def test_cleanup_temp_file_not_found_discards_registry(tmp_path): | ||
| """FileNotFoundError (file already gone) still removes the registry entry.""" | ||
| path = str(tmp_path / "nc_download_gone.bin") | ||
| _temp_registry.add(path) | ||
|
|
||
| try: | ||
| try: | ||
| os.unlink(path) | ||
| _temp_registry.discard(path) | ||
| except FileNotFoundError: | ||
| _temp_registry.discard(path) | ||
|
|
||
| assert path not in _temp_registry |
There was a problem hiding this comment.
These cleanup-temp tests don't exercise the production cleanup behavior; they only assert preconditions or manually call os.unlink() and mutate _temp_registry. As written, they would still pass even if nc_webdav_cleanup_temp’s guard/registry semantics regressed. Prefer testing the real implementation (e.g., via an extracted pure helper or by exposing the cleanup function) and asserting that an unregistered path is rejected and that registry entries are kept/discarded in the correct error/success cases.
so that MCP refers to #721 of course. Sigh, sorry, Claude loves to get involved in the convo, had to delete a further reply from it ;-) Perhaps streaming could be a follow-up? |
Co-authored-by: Copilot <[email protected]>
- Extract _cleanup_temp_path() as a module-level pure function (same pattern as _list_zip_members/_read_zip_member) so unit tests can call real production logic instead of reimplementing it. - nc_webdav_cleanup_temp tool closure becomes a one-line wrapper. - Rewrite cleanup-temp tests to call _cleanup_temp_path() directly, asserting actual return values and registry state for the success, FileNotFoundError, OSError, and unregistered-path cases. - Remove duplicate assertion in test_cleanup_temp_rejects_unregistered_path. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.
Comments suppressed due to low confidence (1)
nextcloud_mcp_server/server/webdav.py:264
- The new guidance tells callers to use
nc_webdav_read_filefor JSON/XML/YAML, but the implementation below only decodestext/*MIME types. Commonapplication/json/application/xmlresponses will still fall back to base64, so this docstring now steers callers into the exact binary-blob failure mode the PR is trying to avoid unless the decoder is expanded or the guidance is narrowed.
) -> DirectoryListing:
"""List files and directories in the specified NextCloud path.
Args:
path: Directory path to list (empty string for root directory)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| member_mime = mimetypes.guess_type(member_path)[0] or "application/octet-stream" | ||
| basename = os.path.basename(member_path) | ||
| ext = os.path.splitext(basename)[1].lower() | ||
| # Dotfiles like ".rels" have no extension per splitext; treat the whole name as the extension. | ||
| if not ext and basename.startswith("."): | ||
| ext = basename.lower() | ||
|
|
||
| is_text = ( | ||
| member_mime.startswith("text/") | ||
| or member_mime in _TEXT_MIME_TYPES | ||
| or ext in _TEXT_EXTENSIONS | ||
| ) |
| # Registry of local temp paths created by nc_webdav_download_to_temp. | ||
| # Used to prevent nc_webdav_cleanup_temp from deleting arbitrary paths. | ||
| # Plain set is safe: asyncio is single-threaded and GIL protects simple ops. | ||
| _temp_registry: set[str] = set() |
| """Unit tests for WebDAV archive-member and temp-download tools. | ||
|
|
||
| All tests call the real production functions (_list_zip_members, | ||
| _read_zip_member, _cleanup_temp_path, _cleanup_temp_files_on_exit, | ||
| _temp_registry) so that regressions in the implementation are caught rather | ||
| than just verifying stdlib zipfile behaviour. |
| The whole archive is downloaded, but only the requested member is | ||
| returned — it never appears in the context as a base64 blob. | ||
|
|
||
| Supported archive formats: ODS, ODT, ODP, ODG, DOCX, XLSX, PPTX, | ||
| ZIP, JAR, EPUB (anything that Python's zipfile module can open). | ||
|
|
| try: | ||
| info = zf.getinfo(member_path) | ||
| except KeyError as exc: | ||
| available = [i.filename for i in zf.infolist() if not i.is_dir()] | ||
| raise ValueError( | ||
| f"Member '{member_path}' not found in '{path}'. " | ||
| f"Available files: {available[:30]}" | ||
| + (" (truncated)" if len(available) > 30 else "") | ||
| ) from exc | ||
|
|
||
| if info.file_size > _MAX_MEMBER_BYTES: | ||
| raise ValueError( | ||
| f"Member '{member_path}' uncompressed size " | ||
| f"({info.file_size:,} bytes) exceeds the " | ||
| f"{_MAX_MEMBER_BYTES // (1024 * 1024)} MB limit. " | ||
| f"Use nc_webdav_download_to_temp and extract locally." | ||
| ) | ||
|
|
||
| member_bytes = zf.read(member_path) |
Extension/MIME heuristics miss members that have no extension, such as the ODF mandatory 'mimetype' entry. Add a UTF-8 decode + null-byte probe as a fallback (the classic binary-vs-text check used by file(1)): if the bytes decode as valid UTF-8 and contain no null bytes they are returned as a UTF-8 string rather than base64. Add a regression test for 'mimetype'. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
_temp_registry was a process-global set[str], allowing any session in multi-user deployments to delete another session's temp files by supplying a known path. Change _temp_registry to dict[str, str] (local_path -> username). nc_webdav_download_to_temp stores client.username alongside the path; _cleanup_temp_path gains an owner parameter and rejects callers whose username doesn't match the registered owner. The atexit handler is unaffected (it runs without a user context and iterates the dict keys). Add test_cleanup_temp_rejects_wrong_owner to cover the cross-session denial path. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Registry of local temp paths created by nc_webdav_download_to_temp. | ||
| # Maps local_path -> owning_username so nc_webdav_cleanup_temp can verify | ||
| # that the caller is the same user who created the file, preventing one | ||
| # multi-user session from deleting another session's temp files. | ||
| # Dict mutation is safe in asyncio: single-threaded, GIL protects simple ops. |
| @mcp.tool( | ||
| title="Download File to Temp", | ||
| annotations=ToolAnnotations( | ||
| readOnlyHint=True, |
| client = await get_client(ctx) | ||
| content, content_type = await client.webdav.read_file(path) | ||
|
|
| client = await get_client(ctx) | ||
| content, content_type = await client.webdav.read_file(path) | ||
| return _list_zip_members(content, path, content_type) |
| client = await get_client(ctx) | ||
| content, _ = await client.webdav.read_file(path) | ||
| return _read_zip_member(content, path, member_path) |
Add five async tests that invoke the real MCP tool closures through FastMCP's tool.run(args, context=None) API. Passing context=None causes require_scopes to treat the call as BasicAuth mode (scope check bypassed), while get_client is mocked at the module level to inject a controlled NextcloudClient. Tests cover: - nc_webdav_list_archive_members: read_file called, result routed through _list_zip_members and returned correctly - nc_webdav_read_archive_member: read_file called, member extracted as text - nc_webdav_download_to_temp: bytes written to disk, client.username stored in _temp_registry as the file owner - nc_webdav_cleanup_temp (success): client.username passed as owner, file removed, registry entry cleared - nc_webdav_cleanup_temp (wrong owner): bob cannot delete alice's file Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
_read_zip_member: reject directory member paths with a clean ValueError before calling zf.read(), which would otherwise raise an opaque stdlib error. Add a regression test using ZipFile.mkdir(). nc_webdav_download_to_temp: add _MAX_TEMP_DOWNLOAD_BYTES (500 MB) and check len(content) after read_file, raising ValueError with a clear message if exceeded. This limits unintended disk consumption in remote HTTP deployments where callers cannot use the returned path anyway. Add a wiring test that monkeypatches the limit and asserts ToolError. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 2 out of 2 changed files in this pull request and generated 8 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| client = await get_client(ctx) | ||
| content, content_type = await client.webdav.read_file(path) | ||
| return _list_zip_members(content, path, content_type) |
| client = await get_client(ctx) | ||
| content, _ = await client.webdav.read_file(path) | ||
| return _read_zip_member(content, path, member_path) |
| @mcp.tool( | ||
| title="Download File to Temp", | ||
| annotations=ToolAnnotations( | ||
| readOnlyHint=True, |
| client = await get_client(ctx) | ||
| content, content_type = await client.webdav.read_file(path) | ||
|
|
||
| if len(content) > _MAX_TEMP_DOWNLOAD_BYTES: | ||
| raise ValueError( |
| Cleanup: always call nc_webdav_cleanup_temp when finished to free disk | ||
| space. All remaining temp files are also removed automatically when the | ||
| MCP server process exits (via an atexit handler). |
| pass | ||
| raise | ||
|
|
||
| _temp_registry[local_path] = client.username |
| Returns: | ||
| Dict with path, content_type, archive_size, member_count, and a | ||
| members list. Each member has: name, size (uncompressed), | ||
| compressed_size, is_dir. |
| client = await get_client(ctx) | ||
| content, content_type = await client.webdav.read_file(path) |
…truncation download_to_temp annotation: remove incorrect readOnlyHint=True — this tool creates a file on disk and mutates _temp_registry, so it is not read-only. Replace with idempotentHint=False, which is accurate. Archive size guard: add _MAX_ARCHIVE_BYTES (100 MB) checked after read_file() in both nc_webdav_list_archive_members and nc_webdav_read_archive_member. Returns a clear error directing the caller to nc_webdav_download_to_temp for large archives, rather than silently exhausting worker RAM. Member truncation: _list_zip_members now accepts max_members (default 500, matching _MAX_ARCHIVE_MEMBERS). Archives with more entries still report the full member_count but the members list is capped; a truncated=True flag and truncated_at field are included so callers know the list is incomplete. Add unit tests for truncation (with/without limit exceeded) and ToolError wiring tests for the archive size guard on both tools. The streaming/OOM concern for very large archives (Copilot comments 3188640133, 3189892837 etc.) requires a client-layer streaming refactor that is out of scope for this PR; the size guard provides a bounded rejection instead. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
|




Summary
Three pain points addressed:
nc_webdav_read_filereturned the raw archive as a base64 blob, which is meaningless in context.New tools
nc_webdav_list_archive_members(path)Lists members of any ZIP-based archive stored in Nextcloud (ODS, ODT, ODP, DOCX, XLSX, PPTX, EPUB, ZIP, JAR). Uses stdlib
zipfile— no new dependencies. Returns member names, sizes, and whether each entry is a directory.nc_webdav_read_archive_member(path, member_path)Downloads the archive and extracts exactly one named member (e.g.
content.xmlfrom an ODS). XML and other text members are returned as UTF-8 strings. The full archive never enters the context window.nc_webdav_download_to_temp(path)Downloads any Nextcloud file to a
tempfile.mkstemp()path and returns the local path. Intended for use with local shell tools (ffmpeg,pdftotext,exiftool,unrar…). The docstring explicitly states this is only useful when shell/Bash access is available — Claude won't call it blindly in Claude Desktop.nc_webdav_cleanup_temp(local_path)Removes a temp file. Only paths registered by
nc_webdav_download_to_tempin the current server session can be removed — prevents arbitrary filesystem deletion.Improved guidance in
nc_webdav_read_fileRewrote the docstring with explicit use/avoid sections so Claude reaches for the right tool immediately:
No new dependencies
All archive handling uses Python stdlib
zipfileandtempfile.