Skip to content

Commit 2869443

Browse files
authored
Route diagnostics through stderr logging
Replace diagnostic print calls with stdlib logging so MCP stdio stdout remains protocol-only. Configure CLI logging from main(), disable Google API discovery file cache, and add stdout-safety regression tests.
1 parent 3eec539 commit 2869443

2 files changed

Lines changed: 77 additions & 24 deletions

File tree

src/mcp_google_sheets/server.py

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"""
66

77
import base64
8+
import logging
89
import os
910
import sys
1011
from typing import List, Dict, Any, Optional, Union
@@ -25,6 +26,8 @@
2526
from googleapiclient.discovery import build
2627
import google.auth
2728

29+
logger = logging.getLogger(__name__)
30+
2831
# Constants
2932
SCOPES = ['https://www.googleapis.com/auth/spreadsheets', 'https://www.googleapis.com/auth/drive']
3033
CREDENTIALS_CONFIG = os.environ.get('CREDENTIALS_CONFIG')
@@ -33,6 +36,15 @@
3336
SERVICE_ACCOUNT_PATH = os.environ.get('SERVICE_ACCOUNT_PATH', 'service_account.json')
3437
DRIVE_FOLDER_ID = os.environ.get('DRIVE_FOLDER_ID', '') # Working directory in Google Drive
3538

39+
40+
def _configure_logging() -> None:
41+
"""Configure CLI logging without overriding host application logging."""
42+
level_name = os.environ.get("LOG_LEVEL") or ("DEBUG" if os.environ.get("DEBUG") else "INFO")
43+
level = getattr(logging, level_name.upper(), logging.INFO)
44+
if not logging.getLogger().handlers:
45+
logging.basicConfig(level=level, stream=sys.stderr, format="%(message)s")
46+
logger.setLevel(level)
47+
3648
# Tool filtering configuration
3749
# Parse enabled tools from environment variable or command-line argument
3850
def _parse_enabled_tools() -> Optional[set]:
@@ -86,15 +98,15 @@ async def spreadsheet_lifespan(server: FastMCP) -> AsyncIterator[SpreadsheetCont
8698
SERVICE_ACCOUNT_PATH,
8799
scopes=SCOPES
88100
)
89-
print("Using service account authentication")
90-
print(f"Working with Google Drive folder ID: {DRIVE_FOLDER_ID or 'Not specified'}")
101+
logger.info("Using service account authentication")
102+
logger.info("Working with Google Drive folder ID: %s", DRIVE_FOLDER_ID or "Not specified")
91103
except Exception as e:
92-
print(f"Error using service account authentication: {e}")
104+
logger.error("Error using service account authentication: %s", e)
93105
creds = None
94106

95107
# Fall back to OAuth flow if service account auth failed or not configured
96108
if not creds:
97-
print("Trying OAuth authentication flow")
109+
logger.info("Trying OAuth authentication flow")
98110
if os.path.exists(TOKEN_PATH):
99111
with open(TOKEN_PATH, 'r') as token:
100112
creds = Credentials.from_authorized_user_info(json.load(token), SCOPES)
@@ -103,15 +115,15 @@ async def spreadsheet_lifespan(server: FastMCP) -> AsyncIterator[SpreadsheetCont
103115
if not creds or not creds.valid:
104116
if creds and creds.expired and creds.refresh_token:
105117
try:
106-
print("Attempting to refresh expired token...")
118+
logger.info("Attempting to refresh expired token...")
107119
creds.refresh(Request())
108-
print("Token refreshed successfully")
120+
logger.info("Token refreshed successfully")
109121
# Save the refreshed token
110122
with open(TOKEN_PATH, 'w') as token:
111123
token.write(creds.to_json())
112124
except Exception as refresh_error:
113-
print(f"Token refresh failed: {refresh_error}")
114-
print("Triggering reauthentication flow...")
125+
logger.error("Token refresh failed: %s", refresh_error)
126+
logger.info("Triggering reauthentication flow...")
115127
creds = None # Clear creds to trigger OAuth flow below
116128

117129
# If refresh failed or creds don't exist, run OAuth flow
@@ -123,28 +135,28 @@ async def spreadsheet_lifespan(server: FastMCP) -> AsyncIterator[SpreadsheetCont
123135
# Save the credentials for the next run
124136
with open(TOKEN_PATH, 'w') as token:
125137
token.write(creds.to_json())
126-
print("Successfully authenticated using OAuth flow")
138+
logger.info("Successfully authenticated using OAuth flow")
127139
except Exception as e:
128-
print(f"Error with OAuth flow: {e}")
140+
logger.error("Error with OAuth flow: %s", e)
129141
creds = None
130142

131143
# Try Application Default Credentials if no creds thus far
132144
# This will automatically check GOOGLE_APPLICATION_CREDENTIALS, gcloud auth, and metadata service
133145
if not creds:
134146
try:
135-
print("Attempting to use Application Default Credentials (ADC)")
136-
print("ADC will check: GOOGLE_APPLICATION_CREDENTIALS, gcloud auth, and metadata service")
147+
logger.info("Attempting to use Application Default Credentials (ADC)")
148+
logger.info("ADC will check: GOOGLE_APPLICATION_CREDENTIALS, gcloud auth, and metadata service")
137149
creds, project = google.auth.default(
138150
scopes=SCOPES
139151
)
140-
print(f"Successfully authenticated using ADC for project: {project}")
152+
logger.info("Successfully authenticated using ADC for project: %s", project)
141153
except Exception as e:
142-
print(f"Error using Application Default Credentials: {e}")
154+
logger.error("Error using Application Default Credentials: %s", e)
143155
raise Exception("All authentication methods failed. Please configure credentials.")
144156

145157
# Build the services
146-
sheets_service = build('sheets', 'v4', credentials=creds)
147-
drive_service = build('drive', 'v3', credentials=creds)
158+
sheets_service = build('sheets', 'v4', credentials=creds, cache_discovery=False)
159+
drive_service = build('drive', 'v3', credentials=creds, cache_discovery=False)
148160

149161
try:
150162
# Provide the service in the context
@@ -907,7 +919,7 @@ def create_spreadsheet(title: str, folder_id: Optional[str] = None, ctx: Context
907919
spreadsheet_id = spreadsheet.get('id')
908920
parents = spreadsheet.get('parents')
909921
folder_info = f" in folder {target_folder_id}" if target_folder_id else " in root"
910-
print(f"Spreadsheet created with ID: {spreadsheet_id}{folder_info}")
922+
logger.info("Spreadsheet created with ID: %s%s", spreadsheet_id, folder_info)
911923

912924
return {
913925
'spreadsheetId': spreadsheet_id,
@@ -994,9 +1006,9 @@ def list_spreadsheets(folder_id: Optional[str] = None, ctx: Context = None) -> L
9941006
# If a specific folder is provided or configured, search only in that folder
9951007
if target_folder_id:
9961008
query += f" and '{target_folder_id}' in parents"
997-
print(f"Searching for spreadsheets in folder: {target_folder_id}")
1009+
logger.info("Searching for spreadsheets in folder: %s", target_folder_id)
9981010
else:
999-
print("Searching for spreadsheets in 'My Drive'")
1011+
logger.info("Searching for spreadsheets in 'My Drive'")
10001012

10011013
# List spreadsheets
10021014
results = drive_service.files().list(
@@ -1122,11 +1134,11 @@ def list_folders(parent_folder_id: Optional[str] = None, ctx: Context = None) ->
11221134
# If a specific parent folder is provided, search only within that folder
11231135
if parent_folder_id:
11241136
query += f" and '{parent_folder_id}' in parents"
1125-
print(f"Searching for folders in parent folder: {parent_folder_id}")
1137+
logger.info("Searching for folders in parent folder: %s", parent_folder_id)
11261138
else:
11271139
# Search in root of My Drive (folders that don't have any parent folders)
11281140
query += " and 'root' in parents"
1129-
print("Searching for folders in 'My Drive' root")
1141+
logger.info("Searching for folders in 'My Drive' root")
11301142

11311143
# List folders
11321144
results = drive_service.files().list(
@@ -1716,11 +1728,13 @@ def add_chart(spreadsheet_id: str,
17161728

17171729

17181730
def main():
1731+
_configure_logging()
1732+
17191733
# Log tool filtering configuration if enabled
17201734
if ENABLED_TOOLS is not None:
1721-
print(f"Tool filtering enabled. Active tools: {', '.join(sorted(ENABLED_TOOLS))}")
1735+
logger.info("Tool filtering enabled. Active tools: %s", ', '.join(sorted(ENABLED_TOOLS)))
17221736
else:
1723-
print("Tool filtering disabled. All tools are enabled.")
1737+
logger.info("Tool filtering disabled. All tools are enabled.")
17241738

17251739
# Run the server
17261740
transport = "stdio"

tests/test_server_unit.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import ast
12
import os
23
import sys
34
import unittest
@@ -148,6 +149,44 @@ def test_empty_configuration_enables_all_tools(self):
148149
self.assertIsNone(server._parse_enabled_tools())
149150

150151

152+
class StdioSafetyTests(unittest.TestCase):
153+
def test_server_module_does_not_call_print(self):
154+
source_path = os.path.abspath(server.__file__)
155+
with open(source_path, "r", encoding="utf-8") as source_file:
156+
tree = ast.parse(source_file.read(), filename=source_path)
157+
158+
print_calls = [
159+
node.lineno
160+
for node in ast.walk(tree)
161+
if isinstance(node, ast.Call)
162+
and isinstance(node.func, ast.Name)
163+
and node.func.id == "print"
164+
]
165+
166+
self.assertEqual(print_calls, [])
167+
168+
def test_main_writes_no_diagnostics_to_stdout(self):
169+
with patch.object(server.mcp, "run") as run:
170+
with patch.object(server, "_configure_logging"):
171+
with patch.object(server.logger, "info"):
172+
with patch.object(sys, "argv", ["mcp-google-sheets"]):
173+
with redirect_stdout(StringIO()) as stdout:
174+
server.main()
175+
176+
self.assertEqual(stdout.getvalue(), "")
177+
run.assert_called_once_with(transport="stdio")
178+
179+
def test_main_configures_logging(self):
180+
with patch.object(server.mcp, "run") as run:
181+
with patch.object(server, "_configure_logging") as configure_logging:
182+
with patch.object(server.logger, "info"):
183+
with patch.object(sys, "argv", ["mcp-google-sheets"]):
184+
server.main()
185+
186+
configure_logging.assert_called_once_with()
187+
run.assert_called_once_with(transport="stdio")
188+
189+
151190
class A1HelperTests(unittest.TestCase):
152191
def test_column_index_to_letter(self):
153192
self.assertEqual(server._column_index_to_letter(0), "A")
@@ -343,7 +382,7 @@ def test_add_rows_returns_error_for_missing_sheet(self):
343382
def test_create_spreadsheet_targets_requested_folder(self):
344383
drive_service = RecordingDriveService()
345384

346-
with redirect_stdout(StringIO()):
385+
with patch.object(server.logger, "info"):
347386
result = server.create_spreadsheet(
348387
"Created Sheet",
349388
folder_id="folder-id",

0 commit comments

Comments
 (0)