forked from taylorwilsdon/google_workspace_mcp
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfastmcp_server.py
More file actions
175 lines (144 loc) · 5.46 KB
/
fastmcp_server.py
File metadata and controls
175 lines (144 loc) · 5.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
# ruff: noqa
"""
FastMCP Cloud entrypoint for the Google Workspace MCP server.
Enforces OAuth 2.1 + stateless defaults required by FastMCP-hosted deployments.
"""
import logging
import os
import sys
from dotenv import load_dotenv
# Load environment variables BEFORE any other imports that might read them
dotenv_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env")
load_dotenv(dotenv_path=dotenv_path)
from auth.oauth_config import reload_oauth_config, is_stateless_mode
from core.log_formatter import EnhancedLogFormatter, configure_file_logging
from core.utils import check_credentials_directory_permissions
from core.server import server, set_transport_mode, configure_server_for_http
from core.tool_registry import (
set_enabled_tools as set_enabled_tool_names,
wrap_server_tool_method,
filter_server_tools,
)
from auth.scopes import set_enabled_tools
def enforce_fastmcp_cloud_defaults():
"""Force FastMCP Cloud-compatible OAuth settings before initializing the server."""
enforced = []
required = {
"MCP_ENABLE_OAUTH21": "true",
"WORKSPACE_MCP_STATELESS_MODE": "true",
}
defaults = {
"MCP_SINGLE_USER_MODE": "false",
}
for key, target in required.items():
current = os.environ.get(key)
normalized = (current or "").lower()
if normalized != target:
os.environ[key] = target
enforced.append((key, current, target))
for key, target in defaults.items():
current = os.environ.get(key)
if current != target:
os.environ[key] = target
enforced.append((key, current, target))
return enforced
_fastmcp_cloud_overrides = enforce_fastmcp_cloud_defaults()
# Suppress googleapiclient discovery cache warning
logging.getLogger("googleapiclient.discovery_cache").setLevel(logging.ERROR)
# Reload OAuth configuration after env vars loaded
reload_oauth_config()
# Configure basic logging
logging.basicConfig(
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
logger = logging.getLogger(__name__)
if _fastmcp_cloud_overrides:
for key, previous, new_value in _fastmcp_cloud_overrides:
if previous is None:
logger.info("FastMCP Cloud: set %s=%s", key, new_value)
else:
logger.warning(
"FastMCP Cloud: overriding %s from %s to %s", key, previous, new_value
)
else:
logger.info("FastMCP Cloud: OAuth 2.1 stateless defaults already satisfied")
# Configure file logging based on stateless mode
configure_file_logging()
def configure_safe_logging():
"""Configure safe Unicode handling for logging."""
class SafeEnhancedFormatter(EnhancedLogFormatter):
"""Enhanced ASCII formatter with additional Windows safety."""
def format(self, record):
try:
return super().format(record)
except UnicodeEncodeError:
# Fallback to ASCII-safe formatting
service_prefix = self._get_ascii_prefix(record.name, record.levelname)
safe_msg = (
str(record.getMessage())
.encode("ascii", errors="replace")
.decode("ascii")
)
return f"{service_prefix} {safe_msg}"
# Replace all console handlers' formatters with safe enhanced ones
for handler in logging.root.handlers:
# Only apply to console/stream handlers, keep file handlers as-is
if isinstance(handler, logging.StreamHandler) and handler.stream.name in [
"<stderr>",
"<stdout>",
]:
safe_formatter = SafeEnhancedFormatter(use_colors=True)
handler.setFormatter(safe_formatter)
# Configure safe logging
configure_safe_logging()
# Check credentials directory permissions (skip in stateless mode)
if not is_stateless_mode():
try:
logger.info("Checking credentials directory permissions...")
check_credentials_directory_permissions()
logger.info("Credentials directory permissions verified")
except (PermissionError, OSError) as e:
logger.error(f"Credentials directory permission check failed: {e}")
logger.error(
" Please ensure the service has write permissions to create/access the credentials directory"
)
sys.exit(1)
else:
logger.info("🔍 Skipping credentials directory check (stateless mode)")
# Set transport mode for HTTP (FastMCP CLI defaults to streamable-http)
set_transport_mode("streamable-http")
# Import all tool modules to register their @server.tool() decorators
import gmail.gmail_tools
import gdrive.drive_tools
import gcalendar.calendar_tools
import gdocs.docs_tools
import gsheets.sheets_tools
import gchat.chat_tools
import gforms.forms_tools
import gslides.slides_tools
import gtasks.tasks_tools
import gsearch.search_tools
# Configure tool registration
wrap_server_tool_method(server)
# Enable all tools and services by default
all_services = [
"gmail",
"drive",
"calendar",
"docs",
"sheets",
"chat",
"forms",
"slides",
"tasks",
"search",
]
set_enabled_tools(all_services) # Set enabled services for scopes
set_enabled_tool_names(None) # Don't filter individual tools - enable all
# Filter tools based on configuration
filter_server_tools(server)
# Configure authentication after scopes are known
configure_server_for_http()
# Export server instance for FastMCP CLI (looks for 'mcp', 'server', or 'app')
mcp = server
app = server