Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat(webdav): add archive member tools and temp-download#722

Open
jospoortvliet wants to merge 13 commits into
cbcoutinho:masterfrom
jospoortvliet:claude/webdav-archive-tools
Open

feat(webdav): add archive member tools and temp-download#722
jospoortvliet wants to merge 13 commits into
cbcoutinho:masterfrom
jospoortvliet:claude/webdav-archive-tools

Conversation

@jospoortvliet

Copy link
Copy Markdown
Contributor

Summary

Three pain points addressed:

  1. ZIP-based office files (ODS, DOCX, XLSX, …) were unusablenc_webdav_read_file returned the raw archive as a base64 blob, which is meaningless in context.
  2. Images, video, audio, large PDFs had no viable path — no way to get them to local tooling.
  3. Claude had no guidance on which tool to use for which file type, leading to wasted tokens on binary blobs.

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.xml from 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_temp in the current server session can be removed — prevents arbitrary filesystem deletion.

Improved guidance in nc_webdav_read_file

Rewrote the docstring with explicit use/avoid sections so Claude reaches for the right tool immediately:

  • ✅ Use for: plain text < ~1 MB, PDFs (if doc-processing enabled)
  • ❌ Avoid for: ZIP-based office formats, images, video, audio, large binaries

No new dependencies

All archive handling uses Python stdlib zipfile and tempfile.

…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]>
Copilot AI review requested due to automatic review settings April 25, 2026 17:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_file docstring with “use/avoid” guidance by file type.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread nextcloud_mcp_server/server/webdav.py Outdated
Comment thread nextcloud_mcp_server/server/webdav.py Outdated
Comment thread nextcloud_mcp_server/server/webdav.py Outdated
Comment on lines +113 to +123
❌ 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.

Copilot AI Apr 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread nextcloud_mcp_server/server/webdav.py Outdated
Comment thread nextcloud_mcp_server/server/webdav.py Outdated
Comment thread nextcloud_mcp_server/server/webdav.py Outdated
jospoortvliet and others added 2 commits April 25, 2026 19:17
- 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]>
Copilot AI review requested due to automatic review settings April 25, 2026 17:27

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread nextcloud_mcp_server/server/webdav.py Outdated
Comment on lines +155 to +173
@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

Copilot AI Apr 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +130 to +148
@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."
)

Copilot AI Apr 25, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread nextcloud_mcp_server/server/webdav.py Outdated
- 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]>
@CLAassistant

CLAassistant commented Apr 25, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@cbcoutinho

Copy link
Copy Markdown
Owner

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]>
Copilot AI review requested due to automatic review settings April 29, 2026 21:33
@jospoortvliet

Copy link
Copy Markdown
Contributor Author

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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread tests/unit/test_webdav_archive_tools.py
Comment on lines +840 to +849
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)

Copilot AI Apr 29, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread nextcloud_mcp_server/server/webdav.py Outdated
Comment on lines +223 to +295
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

Copilot AI Apr 29, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
@jospoortvliet

jospoortvliet commented May 5, 2026

Copy link
Copy Markdown
Contributor Author

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.

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?

Copilot AI review requested due to automatic review settings May 5, 2026 12:36
jospoortvliet and others added 2 commits May 5, 2026 14:41
- 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]>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_file for JSON/XML/YAML, but the implementation below only decodes text/* MIME types. Common application/json / application/xml responses 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.

Comment on lines +169 to +180
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
)
Comment thread nextcloud_mcp_server/server/webdav.py Outdated
Comment on lines +30 to +33
# 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()
Comment on lines +1 to +6
"""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.
Comment on lines +800 to +805
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).

Comment on lines +147 to +165
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)
jospoortvliet and others added 2 commits May 5, 2026 14:56
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]>
Copilot AI review requested due to automatic review settings May 5, 2026 13:00

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +30 to +34
# 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.
Comment thread nextcloud_mcp_server/server/webdav.py Outdated
@mcp.tool(
title="Download File to Temp",
annotations=ToolAnnotations(
readOnlyHint=True,
Comment on lines +908 to +910
client = await get_client(ctx)
content, content_type = await client.webdav.read_file(path)

Comment thread nextcloud_mcp_server/server/webdav.py Outdated
Comment on lines +807 to +809
client = await get_client(ctx)
content, content_type = await client.webdav.read_file(path)
return _list_zip_members(content, path, content_type)
Comment on lines +852 to +854
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]>
Copilot AI review requested due to automatic review settings May 5, 2026 15:56

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread nextcloud_mcp_server/server/webdav.py Outdated
Comment on lines +818 to +820
client = await get_client(ctx)
content, content_type = await client.webdav.read_file(path)
return _list_zip_members(content, path, content_type)
Comment on lines +863 to +865
client = await get_client(ctx)
content, _ = await client.webdav.read_file(path)
return _read_zip_member(content, path, member_path)
Comment thread nextcloud_mcp_server/server/webdav.py Outdated
@mcp.tool(
title="Download File to Temp",
annotations=ToolAnnotations(
readOnlyHint=True,
Comment on lines +919 to +923
client = await get_client(ctx)
content, content_type = await client.webdav.read_file(path)

if len(content) > _MAX_TEMP_DOWNLOAD_BYTES:
raise ValueError(
Comment on lines +904 to +906
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
Comment thread nextcloud_mcp_server/server/webdav.py Outdated
Comment on lines +810 to +813
Returns:
Dict with path, content_type, archive_size, member_count, and a
members list. Each member has: name, size (uncompressed),
compressed_size, is_dir.
Comment on lines +919 to +920
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]>
@sonarqubecloud

sonarqubecloud Bot commented May 6, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
E Security Rating on New Code (required ≥ A)
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants