Skip to content
Open
Show file tree
Hide file tree
Changes from 9 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
9 changes: 9 additions & 0 deletions auth/scopes.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@
CHAT_SPACES_SCOPE = "https://www.googleapis.com/auth/chat.spaces"
CHAT_SPACES_READONLY_SCOPE = "https://www.googleapis.com/auth/chat.spaces.readonly"

# Google Keep API scopes
KEEP_READONLY_SCOPE = "https://www.googleapis.com/auth/keep.readonly"
KEEP_WRITE_SCOPE = "https://www.googleapis.com/auth/keep"

# Google Sheets API scopes
SHEETS_READONLY_SCOPE = "https://www.googleapis.com/auth/spreadsheets.readonly"
SHEETS_WRITE_SCOPE = "https://www.googleapis.com/auth/spreadsheets"
Expand Down Expand Up @@ -96,6 +100,7 @@
DOCS_WRITE_SCOPE: {DOCS_READONLY_SCOPE},
SHEETS_WRITE_SCOPE: {SHEETS_READONLY_SCOPE},
SLIDES_SCOPE: {SLIDES_READONLY_SCOPE},
KEEP_WRITE_SCOPE: {KEEP_READONLY_SCOPE},
TASKS_SCOPE: {TASKS_READONLY_SCOPE},
CONTACTS_SCOPE: {CONTACTS_READONLY_SCOPE},
CHAT_WRITE_SCOPE: {CHAT_READONLY_SCOPE},
Expand Down Expand Up @@ -143,6 +148,8 @@ def has_required_scopes(available_scopes, required_scopes):

DRIVE_SCOPES = [DRIVE_SCOPE, DRIVE_READONLY_SCOPE, DRIVE_FILE_SCOPE]

KEEP_SCOPES = [KEEP_WRITE_SCOPE, KEEP_READONLY_SCOPE]

GMAIL_SCOPES = [
GMAIL_READONLY_SCOPE,
GMAIL_SEND_SCOPE,
Expand Down Expand Up @@ -194,6 +201,7 @@ def has_required_scopes(available_scopes, required_scopes):
"sheets": SHEETS_SCOPES,
"chat": CHAT_SCOPES,
"forms": FORMS_SCOPES,
"keep": KEEP_SCOPES,
"slides": SLIDES_SCOPES,
"tasks": TASKS_SCOPES,
"contacts": CONTACTS_SCOPES,
Expand All @@ -211,6 +219,7 @@ def has_required_scopes(available_scopes, required_scopes):
"chat": [CHAT_READONLY_SCOPE, CHAT_SPACES_READONLY_SCOPE],
"forms": [FORMS_BODY_READONLY_SCOPE, FORMS_RESPONSES_READONLY_SCOPE],
"slides": [SLIDES_READONLY_SCOPE],
"keep": [KEEP_READONLY_SCOPE],
"tasks": [TASKS_READONLY_SCOPE],
"contacts": [CONTACTS_READONLY_SCOPE],
"search": CUSTOM_SEARCH_SCOPES,
Expand Down
6 changes: 6 additions & 0 deletions auth/service_decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
CHAT_WRITE_SCOPE,
CHAT_SPACES_SCOPE,
CHAT_SPACES_READONLY_SCOPE,
KEEP_WRITE_SCOPE,
KEEP_READONLY_SCOPE,
FORMS_BODY_SCOPE,
FORMS_BODY_READONLY_SCOPE,
FORMS_RESPONSES_READONLY_SCOPE,
Expand Down Expand Up @@ -405,6 +407,7 @@ def _remove_user_email_arg_from_docstring(docstring: str) -> str:
"drive": {"service": "drive", "version": "v3"},
"calendar": {"service": "calendar", "version": "v3"},
"docs": {"service": "docs", "version": "v1"},
"keep": {"service": "keep", "version": "v1"},
"sheets": {"service": "sheets", "version": "v4"},
"chat": {"service": "chat", "version": "v1"},
"forms": {"service": "forms", "version": "v1"},
Expand Down Expand Up @@ -434,6 +437,9 @@ def _remove_user_email_arg_from_docstring(docstring: str) -> str:
# Calendar scopes
"calendar_read": CALENDAR_READONLY_SCOPE,
"calendar_events": CALENDAR_EVENTS_SCOPE,
# Keep scopes
"keep_read": KEEP_READONLY_SCOPE,
"keep_write": KEEP_WRITE_SCOPE,
# Sheets scopes
"sheets_read": SHEETS_READONLY_SCOPE,
"sheets_write": SHEETS_WRITE_SCOPE,
Expand Down
66 changes: 25 additions & 41 deletions core/api_enablement.py
Original file line number Diff line number Diff line change
@@ -1,48 +1,32 @@
import re
from typing import Dict, Optional, Tuple


API_ENABLEMENT_LINKS: Dict[str, str] = {
"calendar-json.googleapis.com": "https://console.cloud.google.com/flows/enableapi?apiid=calendar-json.googleapis.com",
"drive.googleapis.com": "https://console.cloud.google.com/flows/enableapi?apiid=drive.googleapis.com",
"gmail.googleapis.com": "https://console.cloud.google.com/flows/enableapi?apiid=gmail.googleapis.com",
"docs.googleapis.com": "https://console.cloud.google.com/flows/enableapi?apiid=docs.googleapis.com",
"sheets.googleapis.com": "https://console.cloud.google.com/flows/enableapi?apiid=sheets.googleapis.com",
"slides.googleapis.com": "https://console.cloud.google.com/flows/enableapi?apiid=slides.googleapis.com",
"forms.googleapis.com": "https://console.cloud.google.com/flows/enableapi?apiid=forms.googleapis.com",
"tasks.googleapis.com": "https://console.cloud.google.com/flows/enableapi?apiid=tasks.googleapis.com",
"chat.googleapis.com": "https://console.cloud.google.com/flows/enableapi?apiid=chat.googleapis.com",
"customsearch.googleapis.com": "https://console.cloud.google.com/flows/enableapi?apiid=customsearch.googleapis.com",
from typing import Dict, List, Optional, Tuple


SUPPORTED_APIS: Dict[str, Tuple[List[str], str]] = {
"calendar-json.googleapis.com": (["calendar"], "Google Calendar"),
"drive.googleapis.com": (["drive"], "Google Drive"),
"gmail.googleapis.com": (["gmail"], "Gmail"),
"keep.googleapis.com": (["keep"], "Google Keep"),
"docs.googleapis.com": (["docs"], "Google Docs"),
"sheets.googleapis.com": (["sheets"], "Google Sheets"),
"slides.googleapis.com": (["slides"], "Google Slides"),
"forms.googleapis.com": (["forms"], "Google Forms"),
"tasks.googleapis.com": (["tasks"], "Google Tasks"),
"chat.googleapis.com": (["chat"], "Google Chat"),
"customsearch.googleapis.com": (["customsearch", "search"], "Google Custom Search"),
}


SERVICE_NAME_TO_API: Dict[str, str] = {
"Google Calendar": "calendar-json.googleapis.com",
"Google Drive": "drive.googleapis.com",
"Gmail": "gmail.googleapis.com",
"Google Docs": "docs.googleapis.com",
"Google Sheets": "sheets.googleapis.com",
"Google Slides": "slides.googleapis.com",
"Google Forms": "forms.googleapis.com",
"Google Tasks": "tasks.googleapis.com",
"Google Chat": "chat.googleapis.com",
"Google Custom Search": "customsearch.googleapis.com",
}
API_ENABLEMENT_LINK_BASE = "https://console.cloud.google.com/flows/enableapi?apiid={api}"


INTERNAL_SERVICE_TO_API: Dict[str, str] = {
"calendar": "calendar-json.googleapis.com",
"drive": "drive.googleapis.com",
"gmail": "gmail.googleapis.com",
"docs": "docs.googleapis.com",
"sheets": "sheets.googleapis.com",
"slides": "slides.googleapis.com",
"forms": "forms.googleapis.com",
"tasks": "tasks.googleapis.com",
"chat": "chat.googleapis.com",
"customsearch": "customsearch.googleapis.com",
"search": "customsearch.googleapis.com",
}
SERVICE_NAME_TO_API: Dict[str, str] = { name: api for api, (_, name) in SUPPORTED_APIS.items() }


INTERNAL_SERVICE_TO_API: Dict[str, str] = {}
for api, (internal_names, _) in SUPPORTED_APIS.items():
for internal in internal_names:
INTERNAL_SERVICE_TO_API[internal] = api


def extract_api_info_from_error(
Expand Down Expand Up @@ -88,8 +72,8 @@ def get_api_enablement_message(
# Check display names (e.g., "Google Calendar")
api_service = SERVICE_NAME_TO_API.get(service_type)

if api_service and api_service in API_ENABLEMENT_LINKS:
enable_link = API_ENABLEMENT_LINKS[api_service]
if api_service and api_service in SUPPORTED_APIS:
enable_link = API_ENABLEMENT_LINK_BASE.format(api=api_service)
service_display_name = next(
(name for name, api in SERVICE_NAME_TO_API.items() if api == api_service),
api_service,
Expand Down
1 change: 1 addition & 0 deletions core/log_formatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ def _get_ascii_prefix(self, logger_name: str, level_name: str) -> str:
"gcalendar.calendar_tools": "[CALENDAR]",
"gdrive.drive_tools": "[DRIVE]",
"gmail.gmail_tools": "[GMAIL]",
"gkeep.keep_tools": "[KEEP]",
"gdocs.docs_tools": "[DOCS]",
"gsheets.sheets_tools": "[SHEETS]",
"gchat.chat_tools": "[CHAT]",
Expand Down
12 changes: 12 additions & 0 deletions core/tool_tiers.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,18 @@ docs:
- reply_to_document_comment
- resolve_document_comment

keep:
core:
- list_notes
- get_note
- read_note
extended:
- delete_note
- create_note
complete:
- download_attachment
- set_permissions

sheets:
core:
- create_spreadsheet
Expand Down
2 changes: 2 additions & 0 deletions fastmcp_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ def format(self, record):
# Import all tool modules to register their @server.tool() decorators
import gmail.gmail_tools
import gdrive.drive_tools
import gkeep.keep_tools
import gcalendar.calendar_tools
import gdocs.docs_tools
import gsheets.sheets_tools
Expand All @@ -161,6 +162,7 @@ def format(self, record):
"docs",
"sheets",
"chat",
"keep",
"forms",
"slides",
"tasks",
Expand Down
1 change: 1 addition & 0 deletions gkeep/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Google Keep MCP Integration"""
186 changes: 186 additions & 0 deletions gkeep/keep_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
"""
Google Keep Helper Functions

Shared utilities for Google Keep operations.
"""

from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional


@dataclass
class ListItem:
"""A single checklist item in a Keep note."""

text: str
checked: bool = False
children: List["ListItem"] = field(default_factory=list)

@classmethod
def from_api(cls, data: Dict[str, Any]) -> "ListItem":
children = [
cls.from_api(child) for child in data.get("childListItems", [])
]
return cls(
text=data.get("text", {}).get("text", ""),
checked=data.get("checked", False),
children=children,
)


@dataclass
class Attachment:
"""An attachment on a Keep note."""

name: str
mime_types: List[str] = field(default_factory=list)

@classmethod
def from_api(cls, data: Dict[str, Any]) -> "Attachment":
return cls(
name=data.get("name", ""),
mime_types=data.get("mimeType", []),
)


@dataclass
class Permission:
"""A permission entry on a Keep note."""

name: str
role: str
email: str

@classmethod
def from_api(cls, data: Dict[str, Any]) -> "Permission":
return cls(
name=data.get("name", ""),
role=data.get("role", ""),
email=data.get("email", ""),
)


@dataclass
class Note:
"""A Google Keep note."""

name: str
title: str
text: Optional[str] = None
list_items: Optional[List[ListItem]] = None
trashed: bool = False
trash_time: Optional[str] = None
create_time: Optional[str] = None
update_time: Optional[str] = None
attachments: List[Attachment] = field(default_factory=list)
permissions: List[Permission] = field(default_factory=list)

@classmethod
def from_api(cls, data: Dict[str, Any]) -> "Note":
body = data.get("body", {})
text_content = body.get("text", {})
list_content = body.get("list", {})

text = text_content.get("text") if text_content else None
list_items = (
[ListItem.from_api(item) for item in list_content["listItems"]]
if list_content and list_content.get("listItems")
else None
)

return cls(
name=data.get("name", ""),
title=data.get("title", ""),
text=text,
list_items=list_items,
trashed=data.get("trashed", False),
trash_time=data.get("trashTime"),
create_time=data.get("createTime"),
update_time=data.get("updateTime"),
attachments=[
Attachment.from_api(att) for att in data.get("attachments", [])
],
permissions=[
Permission.from_api(perm) for perm in data.get("permissions", [])
],
)


def format_note(note: Note) -> str:
"""Format a Note into a human-readable summary string."""
lines = []
lines.append(f"- Name: {note.name or 'N/A'}")
lines.append(f" Title: {note.title or 'Untitled'}")

if note.text:
preview = note.text[:200] + ("..." if len(note.text) > 200 else "")
lines.append(f" Body (text): {preview}")
elif note.list_items is not None:
lines.append(f" Body (list): {len(note.list_items)} item(s)")
for item in note.list_items[:10]:
marker = "[x]" if item.checked else "[ ]"
lines.append(f" {marker} {item.text}")
for child in item.children:
child_marker = "[x]" if child.checked else "[ ]"
lines.append(f" {child_marker} {child.text}")
if len(note.list_items) > 10:
lines.append(f" ... and {len(note.list_items) - 10} more item(s)")

if note.trashed:
lines.append(f" Trashed: {note.trash_time or 'Yes'}")
lines.append(f" Created: {note.create_time or 'N/A'}")
lines.append(f" Updated: {note.update_time or 'N/A'}")

if note.attachments:
lines.append(f" Attachments: {len(note.attachments)}")
for att in note.attachments:
lines.append(f" - {att.name or 'N/A'} ({', '.join(att.mime_types)})")

if note.permissions:
lines.append(f" Permissions: {len(note.permissions)}")
for perm in note.permissions:
lines.append(f" - {perm.email or 'N/A'} ({perm.role or 'N/A'})")

return "\n".join(lines)


def _format_list_item(item: ListItem, indent: str = "") -> List[str]:
marker = "x" if item.checked else " "
lines = [f"{indent}- [{marker}] {item.text}"]
for child in item.children:
lines.extend(_format_list_item(child, indent=f' {indent}'))
return lines


def format_note_content(note: Note) -> str:
"""Format only the content of a note (full text or checklist) without truncation."""
lines = []
if note.title:
lines.append(f"Title: {note.title}")

if note.text:
lines.append(note.text)
elif note.list_items is not None:
for item in note.list_items:
lines.extend(_format_list_item(item))
else:
lines.append("(empty note)")

return "\n".join(lines)


def build_note_body(
title: str,
text: Optional[str] = None,
list_items: Optional[List[Dict[str, Any]]] = None,
) -> Dict[str, Any]:
"""Build a note request body for creation."""
if text is not None and list_items is not None:
raise ValueError("Provide either 'text' or 'list_items', not both.")

body: Dict[str, Any] = {"title": title}
if list_items is not None:
body["body"] = {"list": {"listItems": list_items}}
elif text is not None:
body["body"] = {"text": {"text": text}}
return body
Comment thread
hGriff0n marked this conversation as resolved.
Loading