Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 62 additions & 19 deletions src/backend/mcp_servers/calendar_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,12 @@ def _require_recent_context() -> Optional[str]:
return None

return (
"Confirm the current date and time using calendar_current_context "
"before requesting date-based calendar operations."
"⚠️ REQUIRED: Call calendar_current_context first to get the current date and time.\n\n"
"Why? Without knowing what day it is RIGHT NOW, any calendar operation using "
"relative dates (today, tomorrow, next week) will use stale information and "
"produce incorrect results.\n\n"
"Action: Call calendar_current_context() now, then retry this operation with "
"the accurate date information it provides."
)


Expand Down Expand Up @@ -630,15 +634,41 @@ async def generate_auth_url(

@mcp.tool("calendar_current_context")
async def calendar_current_context(timezone: Optional[str] = None) -> str:
"""Report the up-to-date calendar context and record that it was checked."""
"""Get the current date and time context for calendar operations.

IMPORTANT: Always call this tool FIRST before any calendar operations that involve
dates or times. This ensures you have accurate information about:
- What day it is today
- What day of the week it is
- What dates correspond to "tomorrow", "next week", etc.

Without this context, you may use outdated date information, leading to incorrect
calendar queries or event creation. Call this tool:
- At the start of any conversation involving calendar or dates
- Before searching for events with relative dates (today, tomorrow, next week)
- Before creating or updating events
- When the user asks "what's on my schedule" or similar time-based queries

The context remains valid for 5 minutes, after which you should refresh it.
"""

snapshot = create_time_snapshot(timezone)
_mark_context_checked(snapshot.now_utc)

lines = list(build_context_lines(snapshot))
lines.append("")
lines.append("✓ Context refreshed and valid for the next 5 minutes.")
lines.append("")
lines.append(
"IMPORTANT: Use the exact ISO dates shown above (YYYY-MM-DD format) when "
"constructing calendar queries. For example:"
)
lines.append(f"- To find today's events: use time_min='{snapshot.date.isoformat()}'")
lines.append(
f"- To find tomorrow's events: use time_min='{(snapshot.date + dt.timedelta(days=1)).isoformat()}'"
)
lines.append(
"Use these values when preparing time ranges, and re-run this tool if "
"your reasoning depends on the current date."
"- For date ranges, use the 'Upcoming anchors' shown above as reference points."
)

return "\n".join(lines)
Expand All @@ -655,25 +685,29 @@ async def get_events(
detailed: bool = False,
) -> str:
"""
Retrieve events across the user's Google calendars.
Retrieve events from Google Calendar.

Always call ``calendar_current_context`` first so the LLM has an accurate
notion of "today". With no ``calendar_id`` (or when using phrases such as
"my schedule") the search spans the preconfigured household calendars.
Provide a specific ID or friendly name (for example "Family Calendar" or
"Dad Work Schedule") to narrow the query to a single calendar.
⚠️ PREREQUISITE: You MUST call calendar_current_context() first, before using
this tool. This ensures you have accurate date information for keywords like
"today", "tomorrow", etc.

This tool searches across the user's calendars (or a specific calendar if specified).
With no calendar_id (or when using phrases like "my schedule") the search spans
all preconfigured household calendars. Provide a specific ID or friendly name
(e.g., "Family Calendar", "Dad Work Schedule") to narrow the query.

Args:
user_email: The user's email address (defaults to Jack's primary account).
calendar_id: Optional calendar ID or friendly name.
time_min: Start time (ISO format or keywords like "today").
time_min: Start time (ISO format YYYY-MM-DD or keywords like "today").
Use exact dates from calendar_current_context output.
time_max: End time (optional, ISO format or keywords).
max_results: Maximum number of events to return after aggregation.
query: Optional search query.
detailed: Whether to include full details in results.
query: Optional search query for event titles/descriptions.
detailed: Whether to include full event details in results.

Returns:
Formatted string with event details.
Formatted string with event details, or an error if context is stale.
"""

try:
Expand Down Expand Up @@ -927,12 +961,17 @@ async def create_event(
"""
Create a new calendar event.

💡 BEST PRACTICE: Call calendar_current_context() first to get accurate dates
when using relative terms like "today" or "tomorrow".

Args:
user_email: The user's email address
summary: Event title/summary
start_time: Start time (RFC3339 format or YYYY-MM-DD for all-day)
end_time: End time (RFC3339 format or YYYY-MM-DD for all-day)
calendar_id: Calendar ID (default: 'primary')
start_time: Start time - use ISO format (YYYY-MM-DD for all-day events,
YYYY-MM-DDTHH:MM:SS for timed events). Get exact dates from
calendar_current_context when using relative dates.
end_time: End time (same format as start_time)
calendar_id: Calendar ID or friendly name (default: 'primary')
description: Optional event description
location: Optional event location
attendees: Optional list of attendee email addresses
Expand Down Expand Up @@ -1797,12 +1836,16 @@ async def create_task(
"""
Create a new Google Task.

⚠️ PREREQUISITE: If setting a due date, call calendar_current_context() first
to ensure accurate date interpretation (e.g., what "today" or "tomorrow" means).

Args:
user_email: The user's email address.
task_list_id: Task list identifier (default: '@default').
title: Title for the new task.
notes: Optional detailed notes.
due: Optional due date/time (keywords supported).
due: Optional due date/time. Use ISO format (YYYY-MM-DD) or keywords
(today, tomorrow). Get exact dates from calendar_current_context.
parent: Optional parent task ID for subtasks.
previous: Optional sibling task ID for positioning.

Expand Down
37 changes: 31 additions & 6 deletions src/backend/services/time_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,27 +114,52 @@ def build_context_lines(
include_week: bool = True,
upcoming_anchors: Sequence[tuple[str, _dt.timedelta]] = (
("Tomorrow", _dt.timedelta(days=1)),
("Day after tomorrow", _dt.timedelta(days=2)),
("In 3 days", _dt.timedelta(days=3)),
("Next week", _dt.timedelta(weeks=1)),
("Next week (7 days)", _dt.timedelta(weeks=1)),
("In 2 weeks", _dt.timedelta(weeks=2)),
),
) -> Iterable[str]:
"""Yield human-readable context lines for ``snapshot``."""

today_local = snapshot.date

yield f"Current date: {today_local.isoformat()} ({snapshot.now_local.strftime('%A')})"
yesterday = today_local - _dt.timedelta(days=1)

yield "=" * 60
yield "CURRENT DATE AND TIME CONTEXT"
yield "=" * 60
yield ""
yield f"Today: {today_local.isoformat()} ({snapshot.now_local.strftime('%A')})"
yield f"Yesterday: {yesterday.isoformat()} ({yesterday.strftime('%A')})"
yield f"Current time: {snapshot.format_time()}"
yield f"Timezone: {snapshot.timezone_display()}"
yield f"ISO timestamp (local): {snapshot.iso_local}"
yield f"ISO timestamp (UTC): {snapshot.iso_utc}"
yield ""

if include_week:
start_of_week = today_local - _dt.timedelta(days=today_local.weekday())
end_of_week = start_of_week + _dt.timedelta(days=6)
yield f"Week range: {start_of_week.isoformat()} → {end_of_week.isoformat()}"
yield f"Current week: {start_of_week.isoformat()} → {end_of_week.isoformat()}"
yield f" (Monday to Sunday)"
yield ""

if upcoming_anchors:
yield "Upcoming anchors:"
yield "Upcoming date anchors (for calendar queries):"
for label, delta in upcoming_anchors:
anchor = today_local + delta
yield f"- {label}: {anchor.isoformat()} ({anchor.strftime('%A')})"
yield f" • {label}: {anchor.isoformat()} ({anchor.strftime('%A')})"

# Add next Saturday/Sunday dynamically
# weekday(): Monday=0, Tuesday=1, ..., Saturday=5, Sunday=6
days_until_saturday = (5 - today_local.weekday()) % 7
if days_until_saturday == 0:
# Today is Saturday, show next Saturday
days_until_saturday = 7
next_saturday = today_local + _dt.timedelta(days=days_until_saturday)
next_sunday = next_saturday + _dt.timedelta(days=1)

yield f" • Next weekend: {next_saturday.isoformat()} (Saturday)"
yield ""

yield "=" * 60
3 changes: 2 additions & 1 deletion tests/test_calendar_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,8 @@ async def test_get_events_requires_context_gate():

result = await get_events(user_email="test@example.com")

assert "Confirm the current date and time" in result
assert "Call calendar_current_context first" in result
assert "REQUIRED" in result


@pytest.mark.asyncio
Expand Down