Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ec8eab99f3 | |||
| da104c59ac | |||
| b3e55d444b |
@@ -1,6 +1,6 @@
|
|||||||
[tool.commitizen]
|
[tool.commitizen]
|
||||||
name = "cz_conventional_commits"
|
name = "cz_conventional_commits"
|
||||||
version = "0.57.40"
|
version = "0.57.41"
|
||||||
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,12 @@ 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.41 (2026-02-08)
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
- expand recurring events in date-range queries
|
||||||
|
|
||||||
## nextcloud-mcp-server-0.57.40 (2026-02-07)
|
## nextcloud-mcp-server-0.57.40 (2026-02-07)
|
||||||
|
|
||||||
### Fix
|
### Fix
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ 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.40
|
version: 0.57.41
|
||||||
appVersion: "0.63.3"
|
appVersion: "0.63.3"
|
||||||
keywords:
|
keywords:
|
||||||
- nextcloud
|
- nextcloud
|
||||||
|
|||||||
@@ -651,7 +651,7 @@ class CalendarClient:
|
|||||||
# Add categories
|
# Add categories
|
||||||
categories = event_data.get("categories", "")
|
categories = event_data.get("categories", "")
|
||||||
if categories:
|
if categories:
|
||||||
event.add("categories", categories.split(","))
|
event.add("categories", [c.strip() for c in categories.split(",")])
|
||||||
|
|
||||||
# Add priority and status
|
# Add priority and status
|
||||||
priority = event_data.get("priority", 5)
|
priority = event_data.get("priority", 5)
|
||||||
@@ -812,6 +812,50 @@ class CalendarClient:
|
|||||||
if "url" in event_data:
|
if "url" in event_data:
|
||||||
component["URL"] = event_data["url"]
|
component["URL"] = event_data["url"]
|
||||||
|
|
||||||
|
# Handle categories
|
||||||
|
if "categories" in event_data:
|
||||||
|
categories_str = event_data["categories"]
|
||||||
|
if categories_str:
|
||||||
|
component["CATEGORIES"] = [
|
||||||
|
c.strip() for c in categories_str.split(",")
|
||||||
|
]
|
||||||
|
elif "CATEGORIES" in component:
|
||||||
|
del component["CATEGORIES"]
|
||||||
|
|
||||||
|
# Handle recurrence rule
|
||||||
|
if "recurrence_rule" in event_data:
|
||||||
|
rrule_str = event_data["recurrence_rule"]
|
||||||
|
if rrule_str:
|
||||||
|
component["RRULE"] = vRecur.from_ical(rrule_str)
|
||||||
|
elif "RRULE" in component:
|
||||||
|
del component["RRULE"]
|
||||||
|
|
||||||
|
# Handle attendees
|
||||||
|
if "attendees" in event_data:
|
||||||
|
attendees_str = event_data["attendees"]
|
||||||
|
# Remove all existing attendees first
|
||||||
|
while "ATTENDEE" in component:
|
||||||
|
del component["ATTENDEE"]
|
||||||
|
if attendees_str:
|
||||||
|
for email in attendees_str.split(","):
|
||||||
|
if email.strip():
|
||||||
|
component.add("attendee", f"mailto:{email.strip()}")
|
||||||
|
|
||||||
|
# Handle reminder (VALARM)
|
||||||
|
if "reminder_minutes" in event_data:
|
||||||
|
component.subcomponents = [
|
||||||
|
sub
|
||||||
|
for sub in component.subcomponents
|
||||||
|
if sub.name != "VALARM"
|
||||||
|
]
|
||||||
|
minutes = event_data["reminder_minutes"]
|
||||||
|
if minutes > 0:
|
||||||
|
alarm = Alarm()
|
||||||
|
alarm.add("action", "DISPLAY")
|
||||||
|
alarm.add("description", "Event reminder")
|
||||||
|
alarm.add("trigger", dt.timedelta(minutes=-minutes))
|
||||||
|
component.add_component(alarm)
|
||||||
|
|
||||||
# Handle dates
|
# Handle dates
|
||||||
if "start_datetime" in event_data:
|
if "start_datetime" in event_data:
|
||||||
start_str = event_data["start_datetime"]
|
start_str = event_data["start_datetime"]
|
||||||
@@ -1045,7 +1089,9 @@ class CalendarClient:
|
|||||||
if "categories" in todo_data:
|
if "categories" in todo_data:
|
||||||
categories_str = todo_data["categories"]
|
categories_str = todo_data["categories"]
|
||||||
if categories_str:
|
if categories_str:
|
||||||
component["CATEGORIES"] = categories_str.split(",")
|
component["CATEGORIES"] = [
|
||||||
|
c.strip() for c in categories_str.split(",")
|
||||||
|
]
|
||||||
logger.debug(f"Set CATEGORIES to {categories_str}")
|
logger.debug(f"Set CATEGORIES to {categories_str}")
|
||||||
|
|
||||||
# Update timestamps
|
# Update timestamps
|
||||||
|
|||||||
@@ -273,6 +273,86 @@ async def test_update_event(nc_client: NextcloudClient, temporary_event: dict):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
async def test_update_event_extended_fields(
|
||||||
|
nc_client: NextcloudClient, temporary_calendar: str
|
||||||
|
):
|
||||||
|
"""Test updating categories, recurrence_rule, attendees, and reminder_minutes."""
|
||||||
|
calendar_name = temporary_calendar
|
||||||
|
|
||||||
|
tomorrow = datetime.now() + timedelta(days=1)
|
||||||
|
event_data = {
|
||||||
|
"title": "Extended Fields Update Test",
|
||||||
|
"start_datetime": tomorrow.strftime("%Y-%m-%dT10:00:00"),
|
||||||
|
"end_datetime": tomorrow.strftime("%Y-%m-%dT11:00:00"),
|
||||||
|
"description": "Base event for extended-field update test",
|
||||||
|
}
|
||||||
|
|
||||||
|
event_uid = None
|
||||||
|
try:
|
||||||
|
result = await nc_client.calendar.create_event(calendar_name, event_data)
|
||||||
|
event_uid = result["uid"]
|
||||||
|
logger.info(f"Created base event for extended fields test: {event_uid}")
|
||||||
|
|
||||||
|
# --- Phase 1: Set all four extended fields ---
|
||||||
|
updated_data = {
|
||||||
|
"categories": "work,meeting",
|
||||||
|
"recurrence_rule": "FREQ=WEEKLY;COUNT=4",
|
||||||
|
"attendees": "alice@example.com,bob@example.com",
|
||||||
|
"reminder_minutes": 15,
|
||||||
|
}
|
||||||
|
await nc_client.calendar.update_event(calendar_name, event_uid, updated_data)
|
||||||
|
|
||||||
|
retrieved, _ = await nc_client.calendar.get_event(calendar_name, event_uid)
|
||||||
|
|
||||||
|
# Verify categories
|
||||||
|
assert "work" in retrieved.get("categories", "")
|
||||||
|
assert "meeting" in retrieved.get("categories", "")
|
||||||
|
|
||||||
|
# Verify recurrence rule
|
||||||
|
assert retrieved.get("recurring") is True
|
||||||
|
assert "WEEKLY" in retrieved.get("recurrence_rule", "")
|
||||||
|
|
||||||
|
# Verify attendees
|
||||||
|
attendees = retrieved.get("attendees", "")
|
||||||
|
assert "alice@example.com" in attendees
|
||||||
|
assert "bob@example.com" in attendees
|
||||||
|
|
||||||
|
logger.info("Phase 1 passed: all extended fields set correctly")
|
||||||
|
|
||||||
|
# --- Phase 2: Clear all four extended fields ---
|
||||||
|
cleared_data = {
|
||||||
|
"categories": "",
|
||||||
|
"recurrence_rule": "",
|
||||||
|
"attendees": "",
|
||||||
|
"reminder_minutes": 0,
|
||||||
|
}
|
||||||
|
await nc_client.calendar.update_event(calendar_name, event_uid, cleared_data)
|
||||||
|
|
||||||
|
cleared, _ = await nc_client.calendar.get_event(calendar_name, event_uid)
|
||||||
|
|
||||||
|
# Verify categories cleared
|
||||||
|
assert not cleared.get("categories")
|
||||||
|
|
||||||
|
# Verify recurrence cleared
|
||||||
|
assert cleared.get("recurring") is not True
|
||||||
|
assert not cleared.get("recurrence_rule")
|
||||||
|
|
||||||
|
# Verify attendees cleared
|
||||||
|
assert not cleared.get("attendees")
|
||||||
|
|
||||||
|
logger.info("Phase 2 passed: all extended fields cleared correctly")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Extended fields update test failed: {e}")
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
if event_uid:
|
||||||
|
try:
|
||||||
|
await nc_client.calendar.delete_event(calendar_name, event_uid)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
async def test_create_event_with_attendees(
|
async def test_create_event_with_attendees(
|
||||||
nc_client: NextcloudClient, temporary_calendar: str
|
nc_client: NextcloudClient, temporary_calendar: str
|
||||||
):
|
):
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
"""Integration tests for Calendar VEVENT update MCP tools - extended fields."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from mcp import ClientSession
|
||||||
|
|
||||||
|
from nextcloud_mcp_server.client import NextcloudClient
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
pytestmark = pytest.mark.integration
|
||||||
|
|
||||||
|
|
||||||
|
async def test_mcp_update_event_extended_fields(
|
||||||
|
nc_mcp_client: ClientSession, nc_client: NextcloudClient, temporary_calendar: str
|
||||||
|
):
|
||||||
|
"""Test updating categories, recurrence_rule, attendees, and reminder_minutes via MCP."""
|
||||||
|
|
||||||
|
calendar_name = temporary_calendar
|
||||||
|
event_uid = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 1. Create a base event via MCP
|
||||||
|
tomorrow = datetime.now() + timedelta(days=1)
|
||||||
|
create_result = await nc_mcp_client.call_tool(
|
||||||
|
"nc_calendar_create_event",
|
||||||
|
{
|
||||||
|
"calendar_name": calendar_name,
|
||||||
|
"title": "Extended Fields MCP Test",
|
||||||
|
"start_datetime": tomorrow.strftime("%Y-%m-%dT14:00:00"),
|
||||||
|
"end_datetime": tomorrow.strftime("%Y-%m-%dT15:00:00"),
|
||||||
|
"description": "Base event for MCP extended-field update test",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert create_result.isError is False, (
|
||||||
|
f"MCP event creation failed: {create_result.content}"
|
||||||
|
)
|
||||||
|
|
||||||
|
result_data = json.loads(create_result.content[0].text)
|
||||||
|
event_uid = result_data["uid"]
|
||||||
|
logger.info(f"Created base event via MCP: {event_uid}")
|
||||||
|
|
||||||
|
# 2. Update with all four extended fields via MCP
|
||||||
|
update_result = await nc_mcp_client.call_tool(
|
||||||
|
"nc_calendar_update_event",
|
||||||
|
{
|
||||||
|
"calendar_name": calendar_name,
|
||||||
|
"event_uid": event_uid,
|
||||||
|
"categories": "work,meeting",
|
||||||
|
"recurrence_rule": "FREQ=WEEKLY;COUNT=4",
|
||||||
|
"attendees": "alice@example.com,bob@example.com",
|
||||||
|
"reminder_minutes": 15,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert update_result.isError is False, (
|
||||||
|
f"MCP event update failed: {update_result.content}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. Verify via direct client
|
||||||
|
event, _ = await nc_client.calendar.get_event(calendar_name, event_uid)
|
||||||
|
|
||||||
|
# Categories
|
||||||
|
assert "work" in event.get("categories", ""), (
|
||||||
|
f"Expected 'work' in categories, got: {event.get('categories')}"
|
||||||
|
)
|
||||||
|
assert "meeting" in event.get("categories", ""), (
|
||||||
|
f"Expected 'meeting' in categories, got: {event.get('categories')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Recurrence
|
||||||
|
assert event.get("recurring") is True, "Expected event to be recurring"
|
||||||
|
assert "WEEKLY" in event.get("recurrence_rule", ""), (
|
||||||
|
f"Expected WEEKLY in rrule, got: {event.get('recurrence_rule')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Attendees
|
||||||
|
attendees = event.get("attendees", "")
|
||||||
|
assert "alice@example.com" in attendees, (
|
||||||
|
f"Expected alice in attendees, got: {attendees}"
|
||||||
|
)
|
||||||
|
assert "bob@example.com" in attendees, (
|
||||||
|
f"Expected bob in attendees, got: {attendees}"
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("MCP extended fields update verified successfully")
|
||||||
|
|
||||||
|
# 4. Clear all four fields via MCP
|
||||||
|
clear_result = await nc_mcp_client.call_tool(
|
||||||
|
"nc_calendar_update_event",
|
||||||
|
{
|
||||||
|
"calendar_name": calendar_name,
|
||||||
|
"event_uid": event_uid,
|
||||||
|
"categories": "",
|
||||||
|
"recurrence_rule": "",
|
||||||
|
"attendees": "",
|
||||||
|
"reminder_minutes": 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert clear_result.isError is False, (
|
||||||
|
f"MCP event clear failed: {clear_result.content}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 5. Verify fields cleared
|
||||||
|
cleared, _ = await nc_client.calendar.get_event(calendar_name, event_uid)
|
||||||
|
assert not cleared.get("categories"), (
|
||||||
|
f"Expected categories cleared, got: {cleared.get('categories')}"
|
||||||
|
)
|
||||||
|
assert cleared.get("recurring") is not True, (
|
||||||
|
f"Expected recurring cleared, got: {cleared.get('recurring')}"
|
||||||
|
)
|
||||||
|
assert not cleared.get("attendees"), (
|
||||||
|
f"Expected attendees cleared, got: {cleared.get('attendees')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("MCP extended fields clear verified successfully")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
if event_uid:
|
||||||
|
try:
|
||||||
|
await nc_client.calendar.delete_event(calendar_name, event_uid)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
Reference in New Issue
Block a user