Skip to content

Commit f5b9d8c

Browse files
committed
Use correct propstat in multi-status WebDAV responses
Extract find_ok_prop() helper that iterates all propstat elements and picks the one with HTTP 200 status, instead of blindly taking the first propstat which could be a 404 for missing properties.
1 parent 3a30bed commit f5b9d8c

7 files changed

Lines changed: 31 additions & 36 deletions

File tree

src/nc_mcp_server/client.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,24 @@ def _raise_for_ocs_status(response: niquests.Response, context: str = "") -> Non
7070
OC_NS = "http://owncloud.org/ns"
7171
NC_NS = "http://nextcloud.org/ns"
7272

73+
74+
def find_ok_prop(response: ET.Element) -> ET.Element | None:
75+
"""Find the <d:prop> from the first propstat with HTTP 200 status.
76+
77+
WebDAV multi-status responses may contain multiple propstat elements
78+
with different status codes (e.g. 200 for found props, 404 for missing ones).
79+
The ordering is not guaranteed, so we iterate all of them.
80+
"""
81+
for propstat in response.findall(f"{{{DAV_NS}}}propstat"):
82+
status_el = propstat.find(f"{{{DAV_NS}}}status")
83+
if status_el is not None and "200" not in (status_el.text or ""):
84+
continue
85+
prop = propstat.find(f"{{{DAV_NS}}}prop")
86+
if prop is not None:
87+
return prop
88+
return None
89+
90+
7391
# Standard PROPFIND body for file listings
7492
PROPFIND_BODY = """<?xml version="1.0" encoding="UTF-8"?>
7593
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
@@ -430,10 +448,7 @@ def _parse_propfind(xml_text: str, user: str) -> list[dict[str, Any]]:
430448
# Strip the DAV prefix to get the relative path
431449
path = (href.split(dav_prefix, 1)[1] if dav_prefix in href else href).rstrip("/")
432450

433-
propstat = response.find(f"{{{DAV_NS}}}propstat")
434-
if propstat is None:
435-
continue
436-
prop = propstat.find(f"{{{DAV_NS}}}prop")
451+
prop = find_ok_prop(response)
437452
if prop is None:
438453
continue
439454

src/nc_mcp_server/tools/calendar.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from mcp.server.fastmcp import FastMCP
1313

1414
from ..annotations import ADDITIVE, ADDITIVE_IDEMPOTENT, DESTRUCTIVE, READONLY
15-
from ..client import DAV_NS, NextcloudError
15+
from ..client import DAV_NS, NextcloudError, find_ok_prop
1616
from ..permissions import PermissionLevel, require_permission
1717
from ..state import get_client, get_config
1818

@@ -122,10 +122,7 @@ def _parse_calendars_xml(xml_text: str, user: str) -> list[dict[str, Any]]:
122122
if cal_id in SKIP_CALENDARS:
123123
continue
124124

125-
propstat = response.find(f"{{{DAV_NS}}}propstat")
126-
if propstat is None:
127-
continue
128-
prop = propstat.find(f"{{{DAV_NS}}}prop")
125+
prop = find_ok_prop(response)
129126
if prop is None:
130127
continue
131128
rt = prop.find(f"{{{DAV_NS}}}resourcetype")

src/nc_mcp_server/tools/comments.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from mcp.server.fastmcp import FastMCP
99

1010
from ..annotations import ADDITIVE, ADDITIVE_IDEMPOTENT, DESTRUCTIVE, READONLY
11+
from ..client import find_ok_prop
1112
from ..permissions import PermissionLevel, require_permission
1213
from ..state import get_client
1314

@@ -86,10 +87,7 @@ def _parse_comments_xml(xml_text: str) -> list[dict[str, Any]]:
8687
comment_id = parts[-1] if parts else ""
8788
if not comment_id.isdigit():
8889
continue
89-
propstat = response.find(f"{{{DAV_NS}}}propstat")
90-
if propstat is None:
91-
continue
92-
prop = propstat.find(f"{{{DAV_NS}}}prop")
90+
prop = find_ok_prop(response)
9391
if prop is None:
9492
continue
9593
comments.append(_parse_comment_prop(prop, int(comment_id)))

src/nc_mcp_server/tools/system_tags.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from mcp.server.fastmcp import FastMCP
88

99
from ..annotations import ADDITIVE, ADDITIVE_IDEMPOTENT, DESTRUCTIVE, READONLY
10-
from ..client import NextcloudError
10+
from ..client import NextcloudError, find_ok_prop
1111
from ..permissions import PermissionLevel, require_permission
1212
from ..state import get_client
1313

@@ -30,13 +30,7 @@ def _parse_tags_xml(xml_text: str) -> list[dict[str, Any]]:
3030
root = ET.fromstring(xml_text) # noqa: S314
3131
tags: list[dict[str, Any]] = []
3232
for response in root.findall(f"{{{DAV_NS}}}response"):
33-
propstat = response.find(f"{{{DAV_NS}}}propstat")
34-
if propstat is None:
35-
continue
36-
status_el = propstat.find(f"{{{DAV_NS}}}status")
37-
if status_el is None or "200" not in (status_el.text or ""):
38-
continue
39-
prop = propstat.find(f"{{{DAV_NS}}}prop")
33+
prop = find_ok_prop(response)
4034
if prop is None:
4135
continue
4236
tag_id_el = prop.find(f"{{{OC_NS}}}id")

src/nc_mcp_server/tools/tasks.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from mcp.server.fastmcp import FastMCP
1313

1414
from ..annotations import ADDITIVE, ADDITIVE_IDEMPOTENT, DESTRUCTIVE, READONLY
15-
from ..client import DAV_NS, NextcloudError
15+
from ..client import DAV_NS, NextcloudError, find_ok_prop
1616
from ..permissions import PermissionLevel, require_permission
1717
from ..state import get_client, get_config
1818

@@ -103,10 +103,7 @@ def _parse_task_lists_xml(xml_text: str, user: str) -> list[dict[str, Any]]:
103103
if list_id in SKIP_COLLECTIONS:
104104
continue
105105

106-
propstat = response.find(f"{{{DAV_NS}}}propstat")
107-
if propstat is None:
108-
continue
109-
prop = propstat.find(f"{{{DAV_NS}}}prop")
106+
prop = find_ok_prop(response)
110107
if prop is None:
111108
continue
112109
rt = prop.find(f"{{{DAV_NS}}}resourcetype")

src/nc_mcp_server/tools/trashbin.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from mcp.server.fastmcp import FastMCP
1010

1111
from ..annotations import ADDITIVE_IDEMPOTENT, DESTRUCTIVE, READONLY
12-
from ..client import DAV_NS, NC_NS, OC_NS
12+
from ..client import DAV_NS, NC_NS, OC_NS, find_ok_prop
1313
from ..permissions import PermissionLevel, require_permission
1414
from ..state import get_client, get_config
1515

@@ -66,10 +66,7 @@ def _parse_trash_xml(xml_text: str, user: str) -> list[dict[str, Any]]:
6666
trash_path = href.split(trash_prefix, 1)[1].rstrip("/") if trash_prefix in href else ""
6767
if not trash_path:
6868
continue
69-
propstat = response.find(f"{{{DAV_NS}}}propstat")
70-
if propstat is None:
71-
continue
72-
prop = propstat.find(f"{{{DAV_NS}}}prop")
69+
prop = find_ok_prop(response)
7370
if prop is None:
7471
continue
7572
entries.append(_parse_trash_entry(prop, trash_path))

src/nc_mcp_server/tools/versions.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from mcp.server.fastmcp import FastMCP
1010

1111
from ..annotations import ADDITIVE_IDEMPOTENT, READONLY
12-
from ..client import DAV_NS, NC_NS
12+
from ..client import DAV_NS, NC_NS, find_ok_prop
1313
from ..permissions import PermissionLevel, require_permission
1414
from ..state import get_client, get_config
1515

@@ -38,10 +38,7 @@ def _parse_versions_xml(xml_text: str, user: str, file_id: int) -> list[dict[str
3838
version_id = href.split(prefix, 1)[1].rstrip("/") if prefix in href else ""
3939
if not version_id:
4040
continue
41-
propstat = response.find(f"{{{DAV_NS}}}propstat")
42-
if propstat is None:
43-
continue
44-
prop = propstat.find(f"{{{DAV_NS}}}prop")
41+
prop = find_ok_prop(response)
4542
if prop is None:
4643
continue
4744
entry: dict[str, Any] = {"version_id": version_id}

0 commit comments

Comments
 (0)