Add ICS calendar support
All checks were successful
Build And Test / publish (push) Successful in 48s
All checks were successful
Build And Test / publish (push) Successful in 48s
This commit is contained in:
@@ -86,12 +86,18 @@ SMTP_FROM_NAME=Your Name
|
|||||||
CALDAV_URL=https://caldav.example.com/dav
|
CALDAV_URL=https://caldav.example.com/dav
|
||||||
CALDAV_USERNAME=you@example.com
|
CALDAV_USERNAME=you@example.com
|
||||||
CALDAV_PASSWORD=your-password
|
CALDAV_PASSWORD=your-password
|
||||||
|
ICS_CALENDARS=Team|https://example.com/team.ics,Family|https://example.com/family.ics
|
||||||
|
ICS_CALENDAR_TIMEOUT=20
|
||||||
|
ICS_CALENDARS=Team|https://example.com/team.ics,Family|https://example.com/family.ics
|
||||||
|
ICS_CALENDAR_TIMEOUT=20
|
||||||
|
|
||||||
CARDDAV_URL=https://carddav.example.com/dav
|
CARDDAV_URL=https://carddav.example.com/dav
|
||||||
CARDDAV_USERNAME=you@example.com
|
CARDDAV_USERNAME=you@example.com
|
||||||
CARDDAV_PASSWORD=your-password
|
CARDDAV_PASSWORD=your-password
|
||||||
```
|
```
|
||||||
|
|
||||||
|
ICS calendars are optional and read-only. Set `ICS_CALENDARS` to a comma-separated list of entries, each as `name|url` or just `url` if you want the name inferred.
|
||||||
|
|
||||||
### Email notifications (Poke webhook)
|
### Email notifications (Poke webhook)
|
||||||
|
|
||||||
Enable notifications to send new-email alerts to Poke. The server will use IMAP IDLE when available and fall back to polling.
|
Enable notifications to send new-email alerts to Poke. The server will use IMAP IDLE when available and fall back to polling.
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ class Settings(BaseSettings):
|
|||||||
caldav_url: Optional[str] = Field(default=None, alias="CALDAV_URL")
|
caldav_url: Optional[str] = Field(default=None, alias="CALDAV_URL")
|
||||||
caldav_username: Optional[str] = Field(default=None, alias="CALDAV_USERNAME")
|
caldav_username: Optional[str] = Field(default=None, alias="CALDAV_USERNAME")
|
||||||
caldav_password: Optional[SecretStr] = Field(default=None, alias="CALDAV_PASSWORD")
|
caldav_password: Optional[SecretStr] = Field(default=None, alias="CALDAV_PASSWORD")
|
||||||
|
ics_calendars: Optional[str] = Field(default=None, alias="ICS_CALENDARS")
|
||||||
|
ics_calendar_timeout: int = Field(default=20, alias="ICS_CALENDAR_TIMEOUT")
|
||||||
|
|
||||||
# CardDAV Configuration
|
# CardDAV Configuration
|
||||||
carddav_url: Optional[str] = Field(default=None, alias="CARDDAV_URL")
|
carddav_url: Optional[str] = Field(default=None, alias="CARDDAV_URL")
|
||||||
@@ -107,7 +109,7 @@ class Settings(BaseSettings):
|
|||||||
self.smtp_from_email,
|
self.smtp_from_email,
|
||||||
])
|
])
|
||||||
|
|
||||||
def is_calendar_configured(self) -> bool:
|
def is_caldav_configured(self) -> bool:
|
||||||
return all([
|
return all([
|
||||||
self.enable_calendar,
|
self.enable_calendar,
|
||||||
self.caldav_url,
|
self.caldav_url,
|
||||||
@@ -115,6 +117,33 @@ class Settings(BaseSettings):
|
|||||||
self.caldav_password,
|
self.caldav_password,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
def is_calendar_configured(self) -> bool:
|
||||||
|
return all([
|
||||||
|
self.enable_calendar,
|
||||||
|
(self.is_caldav_configured() or self.get_ics_calendars()),
|
||||||
|
])
|
||||||
|
|
||||||
|
def get_ics_calendars(self) -> list[tuple[Optional[str], str]]:
|
||||||
|
if not self.ics_calendars:
|
||||||
|
return []
|
||||||
|
|
||||||
|
calendars: list[tuple[Optional[str], str]] = []
|
||||||
|
for entry in self.ics_calendars.split(","):
|
||||||
|
item = entry.strip()
|
||||||
|
if not item:
|
||||||
|
continue
|
||||||
|
if "|" in item:
|
||||||
|
name, url = item.split("|", 1)
|
||||||
|
name = name.strip() or None
|
||||||
|
url = url.strip()
|
||||||
|
else:
|
||||||
|
name = None
|
||||||
|
url = item
|
||||||
|
if url:
|
||||||
|
calendars.append((name, url))
|
||||||
|
|
||||||
|
return calendars
|
||||||
|
|
||||||
def is_contacts_configured(self) -> bool:
|
def is_contacts_configured(self) -> bool:
|
||||||
return all([
|
return all([
|
||||||
self.enable_contacts,
|
self.enable_contacts,
|
||||||
|
|||||||
@@ -53,7 +53,10 @@ def setup_services():
|
|||||||
if settings.is_calendar_configured():
|
if settings.is_calendar_configured():
|
||||||
from services.calendar_service import CalendarService
|
from services.calendar_service import CalendarService
|
||||||
calendar_service = CalendarService(settings)
|
calendar_service = CalendarService(settings)
|
||||||
print(f" Calendar service: enabled (CalDAV: {settings.caldav_url})")
|
if settings.is_caldav_configured():
|
||||||
|
print(f" Calendar service: enabled (CalDAV: {settings.caldav_url})")
|
||||||
|
else:
|
||||||
|
print(" Calendar service: enabled (ICS calendars only)")
|
||||||
else:
|
else:
|
||||||
print(" Calendar service: disabled (not configured)")
|
print(" Calendar service: disabled (not configured)")
|
||||||
|
|
||||||
@@ -116,6 +119,7 @@ def get_server_info() -> dict:
|
|||||||
"calendar": {
|
"calendar": {
|
||||||
"enabled": calendar_service is not None,
|
"enabled": calendar_service is not None,
|
||||||
"caldav_url": settings.caldav_url if calendar_service else None,
|
"caldav_url": settings.caldav_url if calendar_service else None,
|
||||||
|
"ics_calendars": [c[1] for c in settings.get_ics_calendars()] if calendar_service else [],
|
||||||
},
|
},
|
||||||
"contacts": {
|
"contacts": {
|
||||||
"enabled": contacts_service is not None,
|
"enabled": contacts_service is not None,
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
import uuid
|
import uuid
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import caldav
|
import caldav
|
||||||
|
import httpx
|
||||||
from icalendar import Calendar as iCalendar, Event as iEvent, vText
|
from icalendar import Calendar as iCalendar, Event as iEvent, vText
|
||||||
from dateutil.parser import parse as parse_date
|
from dateutil.parser import parse as parse_date
|
||||||
from dateutil.rrule import rrulestr
|
from dateutil.rrule import rrulestr
|
||||||
@@ -24,8 +26,39 @@ class CalendarService:
|
|||||||
self.settings = settings
|
self.settings = settings
|
||||||
self._client: Optional[caldav.DAVClient] = None
|
self._client: Optional[caldav.DAVClient] = None
|
||||||
self._principal = None
|
self._principal = None
|
||||||
|
self._ics_calendars = self._load_ics_calendars()
|
||||||
|
|
||||||
|
def _load_ics_calendars(self) -> list[dict]:
|
||||||
|
calendars = []
|
||||||
|
for idx, (name, url) in enumerate(self.settings.get_ics_calendars()):
|
||||||
|
cal_id = f"ics:{url}"
|
||||||
|
calendars.append(
|
||||||
|
{
|
||||||
|
"id": cal_id,
|
||||||
|
"name": name or self._derive_ics_name(url, idx),
|
||||||
|
"url": url,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return calendars
|
||||||
|
|
||||||
|
def _derive_ics_name(self, url: str, fallback_index: int) -> str:
|
||||||
|
parsed = urlparse(url)
|
||||||
|
if parsed.path and parsed.path != "/":
|
||||||
|
return parsed.path.rstrip("/").split("/")[-1] or f"ICS Calendar {fallback_index + 1}"
|
||||||
|
return parsed.netloc or f"ICS Calendar {fallback_index + 1}"
|
||||||
|
|
||||||
|
def _is_ics_calendar(self, calendar_id: str) -> bool:
|
||||||
|
return calendar_id.startswith("ics:")
|
||||||
|
|
||||||
|
def _get_ics_calendar(self, calendar_id: str) -> Optional[dict]:
|
||||||
|
for cal in self._ics_calendars:
|
||||||
|
if cal["id"] == calendar_id:
|
||||||
|
return cal
|
||||||
|
return None
|
||||||
|
|
||||||
def _get_client(self) -> caldav.DAVClient:
|
def _get_client(self) -> caldav.DAVClient:
|
||||||
|
if not self.settings.is_caldav_configured():
|
||||||
|
raise ValueError("CalDAV is not configured")
|
||||||
if self._client is None:
|
if self._client is None:
|
||||||
self._client = caldav.DAVClient(
|
self._client = caldav.DAVClient(
|
||||||
url=self.settings.caldav_url,
|
url=self.settings.caldav_url,
|
||||||
@@ -40,21 +73,33 @@ class CalendarService:
|
|||||||
return self._principal
|
return self._principal
|
||||||
|
|
||||||
def list_calendars(self) -> list[Calendar]:
|
def list_calendars(self) -> list[Calendar]:
|
||||||
principal = self._get_principal()
|
|
||||||
calendars = principal.calendars()
|
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for cal in calendars:
|
if self.settings.is_caldav_configured():
|
||||||
props = cal.get_properties([caldav.dav.DisplayName()])
|
principal = self._get_principal()
|
||||||
name = props.get("{DAV:}displayname", cal.name or "Unnamed")
|
calendars = principal.calendars()
|
||||||
|
|
||||||
|
for cal in calendars:
|
||||||
|
props = cal.get_properties([caldav.dav.DisplayName()])
|
||||||
|
name = props.get("{DAV:}displayname", cal.name or "Unnamed")
|
||||||
|
|
||||||
|
result.append(
|
||||||
|
Calendar(
|
||||||
|
id=str(cal.url),
|
||||||
|
name=name,
|
||||||
|
color=None,
|
||||||
|
description=None,
|
||||||
|
is_readonly=False,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for ics_cal in self._ics_calendars:
|
||||||
result.append(
|
result.append(
|
||||||
Calendar(
|
Calendar(
|
||||||
id=str(cal.url),
|
id=ics_cal["id"],
|
||||||
name=name,
|
name=ics_cal["name"],
|
||||||
color=None,
|
color=None,
|
||||||
description=None,
|
description=None,
|
||||||
is_readonly=False,
|
is_readonly=True,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -77,6 +122,9 @@ class CalendarService:
|
|||||||
end_date: str,
|
end_date: str,
|
||||||
include_recurring: bool = True,
|
include_recurring: bool = True,
|
||||||
) -> EventList:
|
) -> EventList:
|
||||||
|
if self._is_ics_calendar(calendar_id):
|
||||||
|
return self._list_ics_events(calendar_id, start_date, end_date, include_recurring)
|
||||||
|
|
||||||
calendar = self._get_calendar_by_id(calendar_id)
|
calendar = self._get_calendar_by_id(calendar_id)
|
||||||
|
|
||||||
start = parse_date(start_date)
|
start = parse_date(start_date)
|
||||||
@@ -101,6 +149,9 @@ class CalendarService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_event(self, calendar_id: str, event_id: str) -> Optional[Event]:
|
def get_event(self, calendar_id: str, event_id: str) -> Optional[Event]:
|
||||||
|
if self._is_ics_calendar(calendar_id):
|
||||||
|
return self._get_ics_event(calendar_id, event_id)
|
||||||
|
|
||||||
calendar = self._get_calendar_by_id(calendar_id)
|
calendar = self._get_calendar_by_id(calendar_id)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -127,6 +178,9 @@ class CalendarService:
|
|||||||
reminders: Optional[list[int]] = None,
|
reminders: Optional[list[int]] = None,
|
||||||
recurrence: Optional[str] = None,
|
recurrence: Optional[str] = None,
|
||||||
) -> Event:
|
) -> Event:
|
||||||
|
if self._is_ics_calendar(calendar_id):
|
||||||
|
raise ValueError("ICS calendars are read-only")
|
||||||
|
|
||||||
calendar = self._get_calendar_by_id(calendar_id)
|
calendar = self._get_calendar_by_id(calendar_id)
|
||||||
|
|
||||||
# Create iCalendar event
|
# Create iCalendar event
|
||||||
@@ -184,6 +238,9 @@ class CalendarService:
|
|||||||
location: Optional[str] = None,
|
location: Optional[str] = None,
|
||||||
attendees: Optional[list[str]] = None,
|
attendees: Optional[list[str]] = None,
|
||||||
) -> Optional[Event]:
|
) -> Optional[Event]:
|
||||||
|
if self._is_ics_calendar(calendar_id):
|
||||||
|
raise ValueError("ICS calendars are read-only")
|
||||||
|
|
||||||
calendar = self._get_calendar_by_id(calendar_id)
|
calendar = self._get_calendar_by_id(calendar_id)
|
||||||
|
|
||||||
# Find the event
|
# Find the event
|
||||||
@@ -224,6 +281,13 @@ class CalendarService:
|
|||||||
def delete_event(
|
def delete_event(
|
||||||
self, calendar_id: str, event_id: str, notify_attendees: bool = True
|
self, calendar_id: str, event_id: str, notify_attendees: bool = True
|
||||||
) -> OperationResult:
|
) -> OperationResult:
|
||||||
|
if self._is_ics_calendar(calendar_id):
|
||||||
|
return OperationResult(
|
||||||
|
success=False,
|
||||||
|
message="ICS calendars are read-only",
|
||||||
|
id=event_id,
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
calendar = self._get_calendar_by_id(calendar_id)
|
calendar = self._get_calendar_by_id(calendar_id)
|
||||||
|
|
||||||
@@ -314,3 +378,186 @@ class CalendarService:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def _list_ics_events(
|
||||||
|
self,
|
||||||
|
calendar_id: str,
|
||||||
|
start_date: str,
|
||||||
|
end_date: str,
|
||||||
|
include_recurring: bool,
|
||||||
|
) -> EventList:
|
||||||
|
ics_calendar = self._get_ics_calendar(calendar_id)
|
||||||
|
if not ics_calendar:
|
||||||
|
raise ValueError(f"Calendar not found: {calendar_id}")
|
||||||
|
|
||||||
|
start = parse_date(start_date)
|
||||||
|
end = parse_date(end_date)
|
||||||
|
|
||||||
|
ical = self._fetch_ics_calendar(ics_calendar["url"])
|
||||||
|
events: list[Event] = []
|
||||||
|
|
||||||
|
for component in ical.walk():
|
||||||
|
if component.name != "VEVENT":
|
||||||
|
continue
|
||||||
|
|
||||||
|
parsed_events = self._parse_ics_component(
|
||||||
|
component, calendar_id, start, end, include_recurring
|
||||||
|
)
|
||||||
|
events.extend(parsed_events)
|
||||||
|
|
||||||
|
events.sort(key=lambda e: e.start)
|
||||||
|
|
||||||
|
return EventList(
|
||||||
|
events=events,
|
||||||
|
calendar_id=calendar_id,
|
||||||
|
start_date=start_date,
|
||||||
|
end_date=end_date,
|
||||||
|
total=len(events),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_ics_event(self, calendar_id: str, event_id: str) -> Optional[Event]:
|
||||||
|
ics_calendar = self._get_ics_calendar(calendar_id)
|
||||||
|
if not ics_calendar:
|
||||||
|
raise ValueError(f"Calendar not found: {calendar_id}")
|
||||||
|
|
||||||
|
ical = self._fetch_ics_calendar(ics_calendar["url"])
|
||||||
|
for component in ical.walk():
|
||||||
|
if component.name != "VEVENT":
|
||||||
|
continue
|
||||||
|
uid = str(component.get("uid", ""))
|
||||||
|
if uid == event_id:
|
||||||
|
events = self._parse_ics_component(
|
||||||
|
component,
|
||||||
|
calendar_id,
|
||||||
|
datetime.min,
|
||||||
|
datetime.max,
|
||||||
|
include_recurring=False,
|
||||||
|
)
|
||||||
|
return events[0] if events else None
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _fetch_ics_calendar(self, url: str) -> iCalendar:
|
||||||
|
response = httpx.get(url, timeout=self.settings.ics_calendar_timeout)
|
||||||
|
response.raise_for_status()
|
||||||
|
return iCalendar.from_ical(response.text)
|
||||||
|
|
||||||
|
def _parse_ics_component(
|
||||||
|
self,
|
||||||
|
component,
|
||||||
|
calendar_id: str,
|
||||||
|
range_start: datetime,
|
||||||
|
range_end: datetime,
|
||||||
|
include_recurring: bool,
|
||||||
|
) -> list[Event]:
|
||||||
|
base_event = self._build_event_from_component(component, calendar_id)
|
||||||
|
if not base_event:
|
||||||
|
return []
|
||||||
|
|
||||||
|
range_start_cmp = range_start
|
||||||
|
range_end_cmp = range_end
|
||||||
|
if base_event.start.tzinfo and range_start.tzinfo is None:
|
||||||
|
range_start_cmp = range_start.replace(tzinfo=base_event.start.tzinfo)
|
||||||
|
range_end_cmp = range_end.replace(tzinfo=base_event.start.tzinfo)
|
||||||
|
elif base_event.start.tzinfo is None and range_start.tzinfo is not None:
|
||||||
|
range_start_cmp = range_start.replace(tzinfo=None)
|
||||||
|
range_end_cmp = range_end.replace(tzinfo=None)
|
||||||
|
|
||||||
|
if not include_recurring or not base_event.recurrence_rule:
|
||||||
|
if base_event.start <= range_end_cmp and base_event.end >= range_start_cmp:
|
||||||
|
return [base_event]
|
||||||
|
return []
|
||||||
|
|
||||||
|
dtstart = base_event.start
|
||||||
|
duration = base_event.end - base_event.start
|
||||||
|
if duration.total_seconds() <= 0:
|
||||||
|
duration = timedelta(hours=1)
|
||||||
|
|
||||||
|
rrule = rrulestr(base_event.recurrence_rule, dtstart=dtstart)
|
||||||
|
occurrences = rrule.between(range_start_cmp, range_end_cmp, inc=True)
|
||||||
|
excluded = self._extract_exdates(component)
|
||||||
|
|
||||||
|
events = []
|
||||||
|
for occ_start in occurrences:
|
||||||
|
if occ_start in excluded:
|
||||||
|
continue
|
||||||
|
occ_end = occ_start + duration
|
||||||
|
occurrence = base_event.model_copy()
|
||||||
|
occurrence.start = occ_start
|
||||||
|
occurrence.end = occ_end
|
||||||
|
events.append(occurrence)
|
||||||
|
|
||||||
|
return events
|
||||||
|
|
||||||
|
def _extract_exdates(self, component) -> set[datetime]:
|
||||||
|
exdates: set[datetime] = set()
|
||||||
|
exdate_prop = component.get("exdate")
|
||||||
|
if not exdate_prop:
|
||||||
|
return exdates
|
||||||
|
|
||||||
|
exdate_list = exdate_prop if isinstance(exdate_prop, list) else [exdate_prop]
|
||||||
|
for exdate in exdate_list:
|
||||||
|
dates = getattr(exdate, "dts", [])
|
||||||
|
for dt in dates:
|
||||||
|
if isinstance(dt.dt, datetime):
|
||||||
|
exdates.add(dt.dt)
|
||||||
|
else:
|
||||||
|
exdates.add(datetime.combine(dt.dt, datetime.min.time()))
|
||||||
|
|
||||||
|
return exdates
|
||||||
|
|
||||||
|
def _build_event_from_component(self, component, calendar_id: str) -> Optional[Event]:
|
||||||
|
try:
|
||||||
|
uid = str(component.get("uid", ""))
|
||||||
|
dtstart = component.get("dtstart")
|
||||||
|
dtend = component.get("dtend")
|
||||||
|
|
||||||
|
start = dtstart.dt if dtstart else datetime.now()
|
||||||
|
end = dtend.dt if dtend else start + timedelta(hours=1)
|
||||||
|
|
||||||
|
all_day = False
|
||||||
|
if not isinstance(start, datetime):
|
||||||
|
all_day = True
|
||||||
|
start = datetime.combine(start, datetime.min.time())
|
||||||
|
if not isinstance(end, datetime):
|
||||||
|
end = datetime.combine(end, datetime.min.time())
|
||||||
|
|
||||||
|
status_str = str(component.get("status", "CONFIRMED")).upper()
|
||||||
|
status = EventStatus.CONFIRMED
|
||||||
|
if status_str == "TENTATIVE":
|
||||||
|
status = EventStatus.TENTATIVE
|
||||||
|
elif status_str == "CANCELLED":
|
||||||
|
status = EventStatus.CANCELLED
|
||||||
|
|
||||||
|
attendees = []
|
||||||
|
for attendee in component.get("attendee", []):
|
||||||
|
if isinstance(attendee, list):
|
||||||
|
for a in attendee:
|
||||||
|
email = str(a).replace("mailto:", "")
|
||||||
|
attendees.append(Attendee(email=email))
|
||||||
|
else:
|
||||||
|
email = str(attendee).replace("mailto:", "")
|
||||||
|
attendees.append(Attendee(email=email))
|
||||||
|
|
||||||
|
rrule = component.get("rrule")
|
||||||
|
recurrence_rule = None
|
||||||
|
if rrule:
|
||||||
|
recurrence_rule = rrule.to_ical().decode("utf-8")
|
||||||
|
|
||||||
|
return Event(
|
||||||
|
id=uid,
|
||||||
|
calendar_id=calendar_id,
|
||||||
|
title=str(component.get("summary", "Untitled")),
|
||||||
|
start=start,
|
||||||
|
end=end,
|
||||||
|
all_day=all_day,
|
||||||
|
description=str(component.get("description", "")) or None,
|
||||||
|
location=str(component.get("location", "")) or None,
|
||||||
|
status=status,
|
||||||
|
attendees=attendees,
|
||||||
|
recurrence_rule=recurrence_rule,
|
||||||
|
organizer=str(component.get("organizer", "")).replace("mailto:", "") or None,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error parsing ICS event: {e}")
|
||||||
|
return None
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from services.calendar_service import CalendarService
|
|||||||
def register_calendar_tools(mcp: FastMCP, service: CalendarService):
|
def register_calendar_tools(mcp: FastMCP, service: CalendarService):
|
||||||
"""Register all calendar-related MCP tools."""
|
"""Register all calendar-related MCP tools."""
|
||||||
|
|
||||||
@mcp.tool(description="List all available calendars from the CalDAV server. Returns calendar ID, name, and properties.")
|
@mcp.tool(description="List all available calendars from CalDAV and configured ICS feeds. Returns calendar ID, name, and properties.")
|
||||||
def list_calendars() -> list[dict]:
|
def list_calendars() -> list[dict]:
|
||||||
"""List all calendars."""
|
"""List all calendars."""
|
||||||
calendars = service.list_calendars()
|
calendars = service.list_calendars()
|
||||||
@@ -26,7 +26,7 @@ def register_calendar_tools(mcp: FastMCP, service: CalendarService):
|
|||||||
Args:
|
Args:
|
||||||
start_date: Start of date range (ISO format: YYYY-MM-DD)
|
start_date: Start of date range (ISO format: YYYY-MM-DD)
|
||||||
end_date: End of date range (ISO format: YYYY-MM-DD)
|
end_date: End of date range (ISO format: YYYY-MM-DD)
|
||||||
calendar_id: The calendar ID (URL) to query. If not provided, lists from all calendars.
|
calendar_id: The calendar ID (CalDAV URL or ICS ID) to query. If not provided, lists from all calendars.
|
||||||
include_recurring: Whether to expand recurring events (default: True)
|
include_recurring: Whether to expand recurring events (default: True)
|
||||||
"""
|
"""
|
||||||
if calendar_id:
|
if calendar_id:
|
||||||
@@ -67,7 +67,7 @@ def register_calendar_tools(mcp: FastMCP, service: CalendarService):
|
|||||||
Get a specific event.
|
Get a specific event.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
calendar_id: The calendar ID containing the event
|
calendar_id: The calendar ID (CalDAV URL or ICS ID) containing the event
|
||||||
event_id: The unique ID (UID) of the event
|
event_id: The unique ID (UID) of the event
|
||||||
"""
|
"""
|
||||||
result = service.get_event(calendar_id, event_id)
|
result = service.get_event(calendar_id, event_id)
|
||||||
@@ -89,7 +89,7 @@ def register_calendar_tools(mcp: FastMCP, service: CalendarService):
|
|||||||
Create a new calendar event.
|
Create a new calendar event.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
calendar_id: The calendar ID to create the event in
|
calendar_id: The calendar ID to create the event in (CalDAV only)
|
||||||
title: Event title/summary
|
title: Event title/summary
|
||||||
start: Start datetime (ISO format: YYYY-MM-DDTHH:MM:SS)
|
start: Start datetime (ISO format: YYYY-MM-DDTHH:MM:SS)
|
||||||
end: End datetime (ISO format: YYYY-MM-DDTHH:MM:SS)
|
end: End datetime (ISO format: YYYY-MM-DDTHH:MM:SS)
|
||||||
@@ -119,7 +119,7 @@ def register_calendar_tools(mcp: FastMCP, service: CalendarService):
|
|||||||
Update an existing event.
|
Update an existing event.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
calendar_id: The calendar ID containing the event
|
calendar_id: The calendar ID containing the event (CalDAV only)
|
||||||
event_id: The unique ID of the event to update
|
event_id: The unique ID of the event to update
|
||||||
title: New event title (optional)
|
title: New event title (optional)
|
||||||
start: New start datetime (optional)
|
start: New start datetime (optional)
|
||||||
@@ -143,7 +143,7 @@ def register_calendar_tools(mcp: FastMCP, service: CalendarService):
|
|||||||
Delete a calendar event.
|
Delete a calendar event.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
calendar_id: The calendar ID containing the event
|
calendar_id: The calendar ID containing the event (CalDAV only)
|
||||||
event_id: The unique ID of the event to delete
|
event_id: The unique ID of the event to delete
|
||||||
notify_attendees: Whether to notify attendees of cancellation (default: True)
|
notify_attendees: Whether to notify attendees of cancellation (default: True)
|
||||||
"""
|
"""
|
||||||
|
|||||||
Reference in New Issue
Block a user