Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e6ef90423 | |||
| c5f2c8369f | |||
| b79ac29a9d | |||
| 334d62825c | |||
| 2233cb423c | |||
| 196a6cdfb2 | |||
| 93f5e70128 | |||
| e5248e70ee | |||
| 018b946b5b |
@@ -42,7 +42,7 @@ jobs:
|
|||||||
VECTOR_SYNC_SCAN_INTERVAL: "5"
|
VECTOR_SYNC_SCAN_INTERVAL: "5"
|
||||||
|
|
||||||
- name: Install the latest version of uv
|
- name: Install the latest version of uv
|
||||||
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
|
uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0
|
||||||
|
|
||||||
- name: Wait for Nextcloud to be ready
|
- name: Wait for Nextcloud to be ready
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
|
uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0
|
||||||
- name: Install Python 3.11
|
- name: Install Python 3.11
|
||||||
run: uv python install 3.11
|
run: uv python install 3.11
|
||||||
- name: Build
|
- name: Build
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
- name: Install the latest version of uv
|
- name: Install the latest version of uv
|
||||||
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
|
uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0
|
||||||
- name: Check format
|
- name: Check format
|
||||||
run: |
|
run: |
|
||||||
uv run --frozen ruff format --diff
|
uv run --frozen ruff format --diff
|
||||||
@@ -73,7 +73,7 @@ jobs:
|
|||||||
up-flags: "--build"
|
up-flags: "--build"
|
||||||
|
|
||||||
- name: Install the latest version of uv
|
- name: Install the latest version of uv
|
||||||
uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0
|
uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0
|
||||||
|
|
||||||
- name: Install Playwright dependencies
|
- name: Install Playwright dependencies
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
@@ -5,6 +5,12 @@ All notable changes to the Nextcloud MCP Server will be documented in this file.
|
|||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [PEP 440](https://peps.python.org/pep-0440/).
|
and this project adheres to [PEP 440](https://peps.python.org/pep-0440/).
|
||||||
|
|
||||||
|
## v0.63.2 (2026-02-07)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- use CalDAV time-range filter for calendar date range queries
|
||||||
|
|
||||||
## v0.63.1 (2026-02-03)
|
## v0.63.1 (2026-02-03)
|
||||||
|
|
||||||
### Fix
|
### Fix
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM docker.io/library/python:3.12-slim-trixie@sha256:43e4d702bbfe3bd6d5b743dc571b67c19121302eb172951a9b7b0149783a1c21
|
FROM docker.io/library/python:3.12-slim-trixie@sha256:9e01bf1ae5db7649a236da7be1e94ffbbbdd7a93f867dd0d8d5720d9e1f89fab
|
||||||
|
|
||||||
COPY --from=ghcr.io/astral-sh/uv:0.10.0@sha256:78a7ff97cd27b7124a5f3c2aefe146170793c56a1e03321dd31a289f6d82a04f /uv /uvx /bin/
|
COPY --from=ghcr.io/astral-sh/uv:0.10.0@sha256:78a7ff97cd27b7124a5f3c2aefe146170793c56a1e03321dd31a289f6d82a04f /uv /uvx /bin/
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@
|
|||||||
# - Per-session app password authentication
|
# - Per-session app password authentication
|
||||||
# - Multi-user support via Smithery session config
|
# - Multi-user support via Smithery session config
|
||||||
|
|
||||||
FROM docker.io/library/python:3.12-slim-trixie@sha256:43e4d702bbfe3bd6d5b743dc571b67c19121302eb172951a9b7b0149783a1c21
|
FROM docker.io/library/python:3.12-slim-trixie@sha256:9e01bf1ae5db7649a236da7be1e94ffbbbdd7a93f867dd0d8d5720d9e1f89fab
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.commitizen]
|
[tool.commitizen]
|
||||||
name = "cz_conventional_commits"
|
name = "cz_conventional_commits"
|
||||||
version = "0.57.37"
|
version = "0.57.39"
|
||||||
tag_format = "nextcloud-mcp-server-$version"
|
tag_format = "nextcloud-mcp-server-$version"
|
||||||
version_scheme = "semver"
|
version_scheme = "semver"
|
||||||
update_changelog_on_bump = true
|
update_changelog_on_bump = true
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Configurable resource limits
|
- Configurable resource limits
|
||||||
- Grafana dashboard annotations
|
- Grafana dashboard annotations
|
||||||
|
|
||||||
|
## nextcloud-mcp-server-0.57.39 (2026-02-07)
|
||||||
|
|
||||||
|
## nextcloud-mcp-server-0.57.38 (2026-02-07)
|
||||||
|
|
||||||
## nextcloud-mcp-server-0.57.37 (2026-02-06)
|
## nextcloud-mcp-server-0.57.37 (2026-02-06)
|
||||||
|
|
||||||
## nextcloud-mcp-server-0.57.36 (2026-02-06)
|
## nextcloud-mcp-server-0.57.36 (2026-02-06)
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ apiVersion: v2
|
|||||||
name: nextcloud-mcp-server
|
name: nextcloud-mcp-server
|
||||||
description: A Helm chart for Nextcloud MCP Server - enables AI assistants to interact with Nextcloud
|
description: A Helm chart for Nextcloud MCP Server - enables AI assistants to interact with Nextcloud
|
||||||
type: application
|
type: application
|
||||||
version: 0.57.37
|
version: 0.57.39
|
||||||
appVersion: "0.63.1"
|
appVersion: "0.63.2"
|
||||||
keywords:
|
keywords:
|
||||||
- nextcloud
|
- nextcloud
|
||||||
- mcp
|
- mcp
|
||||||
|
|||||||
@@ -255,8 +255,14 @@ class CalendarClient:
|
|||||||
"""List events in a calendar within date range."""
|
"""List events in a calendar within date range."""
|
||||||
calendar = self._get_calendar(calendar_name)
|
calendar = self._get_calendar(calendar_name)
|
||||||
|
|
||||||
# Get all events using caldav library (now with proper filter)
|
if start_datetime or end_datetime:
|
||||||
events = await calendar.events()
|
# Build CalDAV REPORT with time-range filter for server-side filtering
|
||||||
|
events = await self._search_events_by_date(
|
||||||
|
calendar, start_datetime, end_datetime
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# No date filter — fetch all events
|
||||||
|
events = await calendar.events()
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for event in events:
|
for event in events:
|
||||||
@@ -274,6 +280,52 @@ class CalendarClient:
|
|||||||
logger.debug(f"Found {len(result)} events")
|
logger.debug(f"Found {len(result)} events")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
async def _search_events_by_date(
|
||||||
|
self,
|
||||||
|
calendar: AsyncCalendar,
|
||||||
|
start_datetime: Optional[dt.datetime] = None,
|
||||||
|
end_datetime: Optional[dt.datetime] = None,
|
||||||
|
) -> list:
|
||||||
|
"""Execute a CalDAV REPORT with time-range filter."""
|
||||||
|
from caldav.async_collection import AsyncEvent
|
||||||
|
from caldav.elements import cdav, dav
|
||||||
|
from lxml import etree # type: ignore[import-untyped]
|
||||||
|
|
||||||
|
# Ensure naive datetimes are treated as UTC
|
||||||
|
if start_datetime and start_datetime.tzinfo is None:
|
||||||
|
start_datetime = start_datetime.replace(tzinfo=dt.UTC)
|
||||||
|
if end_datetime and end_datetime.tzinfo is None:
|
||||||
|
end_datetime = end_datetime.replace(tzinfo=dt.UTC)
|
||||||
|
|
||||||
|
# Build comp-filter with time-range (mirrors sync Calendar.build_search_xml_query)
|
||||||
|
inner_comp_filter = cdav.CompFilter(name="VEVENT")
|
||||||
|
inner_comp_filter += cdav.TimeRange(start_datetime, end_datetime)
|
||||||
|
outer_comp_filter = cdav.CompFilter(name="VCALENDAR") + inner_comp_filter
|
||||||
|
filter_element = cdav.Filter() + outer_comp_filter
|
||||||
|
|
||||||
|
query = (
|
||||||
|
cdav.CalendarQuery() + [dav.Prop() + cdav.CalendarData()] + filter_element
|
||||||
|
)
|
||||||
|
|
||||||
|
body = etree.tostring(
|
||||||
|
query.xmlelement(), encoding="utf-8", xml_declaration=True
|
||||||
|
)
|
||||||
|
assert calendar.client is not None
|
||||||
|
response = await calendar.client.report(str(calendar.url), body, depth=1)
|
||||||
|
|
||||||
|
# Parse response (same pattern as AsyncCalendar.search)
|
||||||
|
objects = []
|
||||||
|
response_data = response.expand_simple_props([cdav.CalendarData()])
|
||||||
|
for href, props in response_data.items():
|
||||||
|
if href == str(calendar.url):
|
||||||
|
continue
|
||||||
|
cal_data = props.get(cdav.CalendarData.tag)
|
||||||
|
if cal_data:
|
||||||
|
obj = AsyncEvent(client=calendar.client, data=cal_data, parent=calendar)
|
||||||
|
objects.append(obj)
|
||||||
|
|
||||||
|
return objects
|
||||||
|
|
||||||
async def create_event(
|
async def create_event(
|
||||||
self, calendar_name: str, event_data: Dict[str, Any]
|
self, calendar_name: str, event_data: Dict[str, Any]
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "nextcloud-mcp-server"
|
name = "nextcloud-mcp-server"
|
||||||
version = "0.63.1"
|
version = "0.63.2"
|
||||||
description = "Model Context Protocol (MCP) server for Nextcloud integration - enables AI assistants to interact with Nextcloud data"
|
description = "Model Context Protocol (MCP) server for Nextcloud integration - enables AI assistants to interact with Nextcloud data"
|
||||||
authors = [
|
authors = [
|
||||||
{name = "Chris Coutinho", email = "chris@coutinho.io"}
|
{name = "Chris Coutinho", email = "chris@coutinho.io"}
|
||||||
|
|||||||
@@ -380,6 +380,86 @@ async def test_event_with_url_and_categories(
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
async def test_list_events_date_range_filtering(
|
||||||
|
nc_client: NextcloudClient, temporary_calendar: str
|
||||||
|
):
|
||||||
|
"""Test that date range filtering actually excludes events outside the range.
|
||||||
|
|
||||||
|
Reproduces GH-538: get_calendar_events() accepted date range parameters
|
||||||
|
but returned events from the entire calendar history, ignoring date filters.
|
||||||
|
"""
|
||||||
|
calendar_name = temporary_calendar
|
||||||
|
past_uid = None
|
||||||
|
future_uid = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create Event A: 30 days in the past
|
||||||
|
past_date = datetime.now() - timedelta(days=30)
|
||||||
|
past_event_data = {
|
||||||
|
"title": f"Past Event {uuid.uuid4().hex[:8]}",
|
||||||
|
"start_datetime": past_date.strftime("%Y-%m-%dT10:00:00"),
|
||||||
|
"end_datetime": past_date.strftime("%Y-%m-%dT11:00:00"),
|
||||||
|
"description": "Event in the past for date range test",
|
||||||
|
}
|
||||||
|
result_past = await nc_client.calendar.create_event(
|
||||||
|
calendar_name, past_event_data
|
||||||
|
)
|
||||||
|
past_uid = result_past["uid"]
|
||||||
|
logger.info(f"Created past event: {past_uid}")
|
||||||
|
|
||||||
|
# Create Event B: 1 day in the future
|
||||||
|
future_date = datetime.now() + timedelta(days=1)
|
||||||
|
future_event_data = {
|
||||||
|
"title": f"Future Event {uuid.uuid4().hex[:8]}",
|
||||||
|
"start_datetime": future_date.strftime("%Y-%m-%dT14:00:00"),
|
||||||
|
"end_datetime": future_date.strftime("%Y-%m-%dT15:00:00"),
|
||||||
|
"description": "Event in the future for date range test",
|
||||||
|
}
|
||||||
|
result_future = await nc_client.calendar.create_event(
|
||||||
|
calendar_name, future_event_data
|
||||||
|
)
|
||||||
|
future_uid = result_future["uid"]
|
||||||
|
logger.info(f"Created future event: {future_uid}")
|
||||||
|
|
||||||
|
# Query with date range: today → 7 days ahead
|
||||||
|
now = datetime.now()
|
||||||
|
week_ahead = now + timedelta(days=7)
|
||||||
|
|
||||||
|
events = await nc_client.calendar.get_calendar_events(
|
||||||
|
calendar_name=calendar_name,
|
||||||
|
start_datetime=now,
|
||||||
|
end_datetime=week_ahead,
|
||||||
|
limit=50,
|
||||||
|
)
|
||||||
|
|
||||||
|
event_uids = [e["uid"] for e in events]
|
||||||
|
|
||||||
|
# Future event (tomorrow) SHOULD be in results
|
||||||
|
assert future_uid in event_uids, (
|
||||||
|
f"Future event {future_uid} should be in date-filtered results"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Past event (30 days ago) should NOT be in results
|
||||||
|
assert past_uid not in event_uids, (
|
||||||
|
f"Past event {past_uid} should be excluded by date range filter "
|
||||||
|
f"(GH-538: date range was being ignored)"
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Date range filtering works: {len(events)} events returned, "
|
||||||
|
f"past event correctly excluded"
|
||||||
|
)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Cleanup both events
|
||||||
|
for uid in [past_uid, future_uid]:
|
||||||
|
if uid:
|
||||||
|
try:
|
||||||
|
await nc_client.calendar.delete_event(calendar_name, uid)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(f"Cleanup failed for event {uid}: {e}")
|
||||||
|
|
||||||
|
|
||||||
async def test_calendar_operations_error_handling(
|
async def test_calendar_operations_error_handling(
|
||||||
nc_client: NextcloudClient,
|
nc_client: NextcloudClient,
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -1988,7 +1988,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nextcloud-mcp-server"
|
name = "nextcloud-mcp-server"
|
||||||
version = "0.63.1"
|
version = "0.63.2"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiosqlite" },
|
{ name = "aiosqlite" },
|
||||||
|
|||||||
Reference in New Issue
Block a user