Skip to content
Merged
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
140 changes: 140 additions & 0 deletions backend/routers/mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
import database.memories as memories_db
import database.conversations as conversations_db
import database.users as users_db
import database.action_items as action_items_db
import database.goals as goals_db
import database.chat as chat_db
import database.screen_activity as screen_activity_db
import database.daily_summaries as daily_summaries_db

# from database.redis_db import get_filter_category_items
# from database.vector_db import query_vectors_by_metadata
Expand All @@ -23,6 +28,7 @@
from dependencies import get_uid_from_mcp_api_key, get_current_user_id
from utils.other.endpoints import with_rate_limit
from utils.log_sanitizer import sanitize_pii
from utils.mcp_data import clean_action_item, clean_chat_message, clean_person, clean_screen_activity_row
from utils.mcp_memories import (
collect_filtered_memories,
parse_mcp_bool,
Expand Down Expand Up @@ -357,3 +363,137 @@ def get_conversation_by_id(conversation_id: str, uid: str = Depends(get_uid_from
populate_speaker_names(uid, [conversation])

return conversation


# ---------------------------------------------------------------------------
# Action items — the user's actionable task layer (to-dos with due dates)
# ---------------------------------------------------------------------------


class SimpleActionItem(BaseModel):
id: str
description: str
completed: bool = False
created_at: Optional[datetime] = None
due_at: Optional[datetime] = None
completed_at: Optional[datetime] = None
conversation_id: Optional[str] = None


@router.get("/v1/mcp/action-items", response_model=List[SimpleActionItem], tags=["mcp"])
def get_action_items(
completed: Optional[bool] = None,
due_start_date: Optional[datetime] = None,
due_end_date: Optional[datetime] = None,
limit: int = 100,
offset: int = 0,
uid: str = Depends(get_uid_from_mcp_api_key),
):
logger.info(f"get_action_items {uid} completed={completed} limit={limit} offset={offset}")
limit = max(1, min(limit, 500))
offset = max(0, offset)
items = action_items_db.get_action_items(
uid,
completed=completed,
due_start_date=due_start_date,
due_end_date=due_end_date,
limit=limit,
offset=offset,
)
return [clean_action_item(i) for i in items if not i.get("deleted", False)]


# ---------------------------------------------------------------------------
# Goals — the user's stated objectives
# ---------------------------------------------------------------------------


@router.get("/v1/mcp/goals", tags=["mcp"])
def get_goals(include_inactive: bool = False, uid: str = Depends(get_uid_from_mcp_api_key)):
logger.info(f"get_goals {uid} include_inactive={include_inactive}")
return goals_db.get_all_goals(uid, include_inactive=include_inactive)


# ---------------------------------------------------------------------------
# Chat — the user's prior conversations with Omi (intent / preferences signal)
# ---------------------------------------------------------------------------


class SimpleChatMessage(BaseModel):
id: str
text: str
sender: str
type: Optional[str] = None
created_at: Optional[datetime] = None


@router.get("/v1/mcp/chat", response_model=List[SimpleChatMessage], tags=["mcp"])
def get_chat_messages(limit: int = 50, offset: int = 0, uid: str = Depends(get_uid_from_mcp_api_key)):
logger.info(f"get_chat_messages {uid} limit={limit} offset={offset}")
limit = max(1, min(limit, 200))
offset = max(0, offset)
messages = chat_db.get_messages(uid, limit=limit, offset=offset)
return [clean_chat_message(m) for m in messages]


# ---------------------------------------------------------------------------
# People — the contacts/speakers the user interacts with
# ---------------------------------------------------------------------------


class SimplePerson(BaseModel):
id: str
name: str
created_at: Optional[datetime] = None
speech_sample_transcripts: List[str] = []


@router.get("/v1/mcp/people", response_model=List[SimplePerson], tags=["mcp"])
def get_people(uid: str = Depends(get_uid_from_mcp_api_key)):
logger.info(f"get_people {uid}")
return [clean_person(p) for p in users_db.get_people(uid)]


# ---------------------------------------------------------------------------
# Screen activity — desktop Rewind (app, window title, OCR text)
# ---------------------------------------------------------------------------


@router.get("/v1/mcp/screen-activity", tags=["mcp"])
def get_screen_activity(
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None,
app: Optional[str] = None,
summary: bool = False,
limit: int = 200,
uid: str = Depends(get_uid_from_mcp_api_key),
):
logger.info(f"get_screen_activity {uid} summary={summary} app={app} limit={limit}")
if summary:
return screen_activity_db.get_screen_activity_summary(uid, start_date=start_date, end_date=end_date)
limit = max(1, min(limit, 1000))
rows = screen_activity_db.get_screen_activity(
uid, start_date=start_date, end_date=end_date, app_filter=app, limit=limit
)
return [clean_screen_activity_row(r) for r in rows]


# ---------------------------------------------------------------------------
# Daily summaries — Omi's per-day digest of the user's life
# ---------------------------------------------------------------------------


@router.get("/v1/mcp/daily-summaries", tags=["mcp"])
def get_daily_summaries(
limit: int = 30,
offset: int = 0,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
uid: str = Depends(get_uid_from_mcp_api_key),
):
Comment on lines +486 to +493

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Every other date-filtering endpoint in this file uses Optional[datetime] so FastAPI validates the format automatically. Using Optional[str] here means a caller can pass "not-a-date" and it reaches Firestore unchanged, silently returning wrong results instead of a 422.

Suggested change
@router.get("/v1/mcp/daily-summaries", tags=["mcp"])
def get_daily_summaries(
limit: int = 30,
offset: int = 0,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
uid: str = Depends(get_uid_from_mcp_api_key),
):
@router.get("/v1/mcp/daily-summaries", tags=["mcp"])
def get_daily_summaries(
limit: int = 30,
offset: int = 0,
start_date: Optional[datetime] = None,
end_date: Optional[datetime] = None,
uid: str = Depends(get_uid_from_mcp_api_key),
):

logger.info(f"get_daily_summaries {uid} limit={limit} offset={offset}")
limit = max(1, min(limit, 100))
offset = max(0, offset)
return daily_summaries_db.get_daily_summaries(
uid, limit=limit, offset=offset, start_date=start_date, end_date=end_date
)
Loading
Loading