Merge branch 'main' of github.com:taylorwilsdon/google_workspace_mcp into feat/555-auto-reply-headers
This commit is contained in:
@@ -26,8 +26,7 @@ def create_error_response(error_message: str, status_code: int = 400) -> HTMLRes
|
||||
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 600px; margin: 40px auto; padding: 20px; text-align: center;">
|
||||
<h2 style="color: #d32f2f;">Authentication Error</h2>
|
||||
<p>{error_message}</p>
|
||||
<p>Please ensure you grant the requested permissions. You can close this window and try again.</p>
|
||||
<script>setTimeout(function() {{ window.close(); }}, 10000);</script>
|
||||
<p>Please ensure you grant the requested permissions. You can close this tab and try again.</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
@@ -176,9 +175,17 @@ def create_success_response(verified_user_id: Optional[str] = None) -> HTMLRespo
|
||||
}}
|
||||
</style>
|
||||
<script>
|
||||
setTimeout(function() {{
|
||||
function tryClose() {{
|
||||
window.close();
|
||||
}}, 10000);
|
||||
// If window.close() was blocked by the browser, update the UI
|
||||
setTimeout(function() {{
|
||||
var btn = document.querySelector('.button');
|
||||
if (btn) btn.textContent = 'You can close this tab manually';
|
||||
var ac = document.querySelector('.auto-close');
|
||||
if (ac) ac.style.display = 'none';
|
||||
}}, 500);
|
||||
}}
|
||||
setTimeout(tryClose, 10000);
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
@@ -189,10 +196,10 @@ def create_success_response(verified_user_id: Optional[str] = None) -> HTMLRespo
|
||||
You've been authenticated as <span class="user-id">{user_display}</span>
|
||||
</div>
|
||||
<div class="message">
|
||||
Your credentials have been securely saved. You can now close this window and retry your original command.
|
||||
Your credentials have been securely saved. You can now close this tab and retry your original command.
|
||||
</div>
|
||||
<button class="button" onclick="window.close()">Close Window</button>
|
||||
<div class="auto-close">This window will close automatically in 10 seconds</div>
|
||||
<button class="button" onclick="tryClose()">Close Tab</button>
|
||||
<div class="auto-close">This tab will close automatically in 10 seconds</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>"""
|
||||
@@ -215,8 +222,7 @@ def create_server_error_response(error_detail: str) -> HTMLResponse:
|
||||
<body style="font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; max-width: 600px; margin: 40px auto; padding: 20px; text-align: center;">
|
||||
<h2 style="color: #d32f2f;">Authentication Processing Error</h2>
|
||||
<p>An unexpected error occurred while processing your authentication: {error_detail}</p>
|
||||
<p>Please try again. You can close this window.</p>
|
||||
<script>setTimeout(function() {{ window.close(); }}, 10000);</script>
|
||||
<p>Please try again. You can close this tab.</p>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
@@ -172,6 +172,21 @@ def _preserve_existing_fields(
|
||||
event_body[field_name] = new_value
|
||||
|
||||
|
||||
def _get_meeting_link(item: Dict[str, Any]) -> str:
|
||||
"""Extract video meeting link from event conference data or hangoutLink."""
|
||||
conference_data = item.get("conferenceData")
|
||||
if conference_data and "entryPoints" in conference_data:
|
||||
for entry_point in conference_data["entryPoints"]:
|
||||
if entry_point.get("entryPointType") == "video":
|
||||
uri = entry_point.get("uri", "")
|
||||
if uri:
|
||||
return uri
|
||||
hangout_link = item.get("hangoutLink", "")
|
||||
if hangout_link:
|
||||
return hangout_link
|
||||
return ""
|
||||
|
||||
|
||||
def _format_attendee_details(
|
||||
attendees: List[Dict[str, Any]], indent: str = " "
|
||||
) -> str:
|
||||
@@ -448,6 +463,8 @@ async def get_events(
|
||||
)
|
||||
attendee_details_str = _format_attendee_details(attendees, indent=" ")
|
||||
|
||||
meeting_link = _get_meeting_link(item)
|
||||
|
||||
event_details = (
|
||||
f"Event Details:\n"
|
||||
f"- Title: {summary}\n"
|
||||
@@ -456,6 +473,10 @@ async def get_events(
|
||||
f"- Description: {description}\n"
|
||||
f"- Location: {location}\n"
|
||||
f"- Color ID: {color_id}\n"
|
||||
)
|
||||
if meeting_link:
|
||||
event_details += f"- Meeting Link: {meeting_link}\n"
|
||||
event_details += (
|
||||
f"- Attendees: {attendee_emails}\n"
|
||||
f"- Attendee Details: {attendee_details_str}\n"
|
||||
)
|
||||
@@ -494,10 +515,16 @@ async def get_events(
|
||||
)
|
||||
attendee_details_str = _format_attendee_details(attendees, indent=" ")
|
||||
|
||||
meeting_link = _get_meeting_link(item)
|
||||
|
||||
event_detail_parts = (
|
||||
f'- "{summary}" (Starts: {start_time}, Ends: {end_time})\n'
|
||||
f" Description: {description}\n"
|
||||
f" Location: {location}\n"
|
||||
)
|
||||
if meeting_link:
|
||||
event_detail_parts += f" Meeting Link: {meeting_link}\n"
|
||||
event_detail_parts += (
|
||||
f" Attendees: {attendee_emails}\n"
|
||||
f" Attendee Details: {attendee_details_str}\n"
|
||||
)
|
||||
@@ -513,9 +540,12 @@ async def get_events(
|
||||
event_details_list.append(event_detail_parts)
|
||||
else:
|
||||
# Basic output format
|
||||
event_details_list.append(
|
||||
f'- "{summary}" (Starts: {start_time}, Ends: {end_time}) ID: {item_event_id} | Link: {link}'
|
||||
)
|
||||
meeting_link = _get_meeting_link(item)
|
||||
basic_line = f'- "{summary}" (Starts: {start_time}, Ends: {end_time})'
|
||||
if meeting_link:
|
||||
basic_line += f" Meeting: {meeting_link}"
|
||||
basic_line += f" ID: {item_event_id} | Link: {link}"
|
||||
event_details_list.append(basic_line)
|
||||
|
||||
if event_id:
|
||||
# Single event basic output
|
||||
|
||||
@@ -1463,6 +1463,7 @@ async def update_paragraph_style(
|
||||
indent_end: float = None,
|
||||
space_above: float = None,
|
||||
space_below: float = None,
|
||||
named_style_type: str = None,
|
||||
list_type: str = None,
|
||||
list_nesting_level: int = None,
|
||||
) -> str:
|
||||
@@ -1488,6 +1489,8 @@ async def update_paragraph_style(
|
||||
indent_end: Right/end indent in points
|
||||
space_above: Space above paragraph in points (e.g., 12 for one line)
|
||||
space_below: Space below paragraph in points
|
||||
named_style_type: Direct named style type - 'NORMAL_TEXT', 'TITLE', 'SUBTITLE',
|
||||
'HEADING_1' through 'HEADING_6'. Mutually exclusive with heading_level.
|
||||
list_type: Create a list from existing paragraphs ('UNORDERED' for bullets, 'ORDERED' for numbers)
|
||||
list_nesting_level: Nesting level for lists (0-8, where 0 is top level, default is 0)
|
||||
Use higher levels for nested/indented list items
|
||||
@@ -1546,12 +1549,30 @@ async def update_paragraph_style(
|
||||
if list_nesting_level < 0 or list_nesting_level > 8:
|
||||
return "Error: list_nesting_level must be between 0 and 8"
|
||||
|
||||
# Validate named_style_type
|
||||
if named_style_type is not None and heading_level is not None:
|
||||
return "Error: heading_level and named_style_type are mutually exclusive; provide only one"
|
||||
|
||||
if named_style_type is not None:
|
||||
valid_styles = [
|
||||
"NORMAL_TEXT", "TITLE", "SUBTITLE",
|
||||
"HEADING_1", "HEADING_2", "HEADING_3",
|
||||
"HEADING_4", "HEADING_5", "HEADING_6",
|
||||
]
|
||||
if named_style_type not in valid_styles:
|
||||
return f"Error: Invalid named_style_type '{named_style_type}'. Must be one of: {', '.join(valid_styles)}"
|
||||
|
||||
# Build paragraph style object
|
||||
paragraph_style = {}
|
||||
fields = []
|
||||
|
||||
# Handle named_style_type (direct named style)
|
||||
if named_style_type is not None:
|
||||
paragraph_style["namedStyleType"] = named_style_type
|
||||
fields.append("namedStyleType")
|
||||
|
||||
# Handle heading level (named style)
|
||||
if heading_level is not None:
|
||||
elif heading_level is not None:
|
||||
if heading_level < 0 or heading_level > 6:
|
||||
return "Error: heading_level must be between 0 (normal text) and 6"
|
||||
if heading_level == 0:
|
||||
|
||||
@@ -475,6 +475,7 @@ class BatchOperationManager:
|
||||
"indent_end",
|
||||
"space_above",
|
||||
"space_below",
|
||||
"named_style_type",
|
||||
],
|
||||
"description": "Apply paragraph-level styling (headings, alignment, spacing, indentation)",
|
||||
},
|
||||
|
||||
@@ -316,6 +316,12 @@ class ValidationManager:
|
||||
"At least one paragraph style parameter must be provided (heading_level, alignment, line_spacing, indent_first_line, indent_start, indent_end, space_above, space_below, or named_style_type)",
|
||||
)
|
||||
|
||||
if heading_level is not None and named_style_type is not None:
|
||||
return (
|
||||
False,
|
||||
"heading_level and named_style_type are mutually exclusive; provide only one",
|
||||
)
|
||||
|
||||
if named_style_type is not None:
|
||||
valid_styles = [
|
||||
"NORMAL_TEXT",
|
||||
|
||||
33
main.py
33
main.py
@@ -1,3 +1,4 @@
|
||||
import io
|
||||
import argparse
|
||||
import logging
|
||||
import os
|
||||
@@ -6,6 +7,14 @@ import sys
|
||||
from importlib import metadata, import_module
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Prevent any stray startup output on macOS (e.g. platform identifiers) from
|
||||
# corrupting the MCP JSON-RPC handshake on stdout. We capture anything written
|
||||
# to stdout during module-level initialisation and replay it to stderr so that
|
||||
# diagnostic information is not lost.
|
||||
_original_stdout = sys.stdout
|
||||
if sys.platform == "darwin":
|
||||
sys.stdout = io.StringIO()
|
||||
|
||||
# Check for CLI mode early - before loading oauth_config
|
||||
# CLI mode requires OAuth 2.0 since there's no MCP session context
|
||||
_CLI_MODE = "--cli" in sys.argv
|
||||
@@ -117,12 +126,36 @@ def narrow_permissions_to_services(
|
||||
}
|
||||
|
||||
|
||||
def _restore_stdout() -> None:
|
||||
"""Restore the real stdout and replay any captured output to stderr."""
|
||||
captured_stdout = sys.stdout
|
||||
|
||||
# Idempotent: if already restored, nothing to do.
|
||||
if captured_stdout is _original_stdout:
|
||||
return
|
||||
|
||||
captured = ""
|
||||
required_stringio_methods = ("getvalue", "write", "flush")
|
||||
try:
|
||||
if all(
|
||||
callable(getattr(captured_stdout, method_name, None))
|
||||
for method_name in required_stringio_methods
|
||||
):
|
||||
captured = captured_stdout.getvalue()
|
||||
finally:
|
||||
sys.stdout = _original_stdout
|
||||
|
||||
if captured:
|
||||
print(captured, end="", file=sys.stderr)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main entry point for the Google Workspace MCP server.
|
||||
Uses FastMCP's native streamable-http transport.
|
||||
Supports CLI mode for direct tool invocation without running the server.
|
||||
"""
|
||||
_restore_stdout()
|
||||
# Check if CLI mode is enabled - suppress startup messages
|
||||
if _CLI_MODE:
|
||||
# Suppress logging output in CLI mode for clean output
|
||||
|
||||
Reference in New Issue
Block a user