Skip to content

Commit 825b5e8

Browse files
RafaelPoclaude
andcommitted
Skip sheets tools in stdio mode, add sheets to manifest
Sheets tools require Google OAuth (HTTP mode only), so remove them from the tool registry in stdio mode. Also strip the service account JWT path from sheets_client since it's no longer needed. Add sheets tools to manifest.json and exclude output_spreadsheet_title from stdio schema. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d46c7a3 commit 825b5e8

6 files changed

Lines changed: 53 additions & 88 deletions

File tree

everyrow-mcp/manifest.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,26 @@
6868
{
6969
"name": "everyrow_cancel",
7070
"description": "Cancel a running everyrow task. Use when the user wants to stop a task that is currently processing."
71+
},
72+
{
73+
"name": "sheets_list",
74+
"description": "List the user's Google Sheets, optionally filtered by name."
75+
},
76+
{
77+
"name": "sheets_read",
78+
"description": "Read data from a Google Sheet and return it as JSON records."
79+
},
80+
{
81+
"name": "sheets_write",
82+
"description": "Write data to a Google Sheet."
83+
},
84+
{
85+
"name": "sheets_create",
86+
"description": "Create a new Google Sheet, optionally populated with data."
87+
},
88+
{
89+
"name": "sheets_info",
90+
"description": "Get metadata about a Google Sheet: title, sheet names, and dimensions."
7191
}
7292
],
7393
"user_config": {

everyrow-mcp/src/everyrow_mcp/config.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,6 @@ class Settings(BaseSettings):
6262
description="Refresh token TTL in seconds (7 days)",
6363
)
6464
everyrow_api_key: str | None = None
65-
google_sheets_credentials_json: str | None = Field(
66-
default=None,
67-
description="Path to a Google service account JSON file or inline JSON. "
68-
"Required for Google Sheets tools in stdio mode.",
69-
)
7065

7166
@property
7267
def is_http(self) -> bool:

everyrow-mcp/src/everyrow_mcp/server.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,17 @@ def main():
115115
logging.error("Get an API key at https://everyrow.io/api-key")
116116
sys.exit(1)
117117

118+
# Sheets tools require HTTP mode (OAuth provides the Google token).
119+
# Remove them from the tool manager so they don't appear in list_tools().
120+
for name in (
121+
"sheets_list",
122+
"sheets_read",
123+
"sheets_write",
124+
"sheets_create",
125+
"sheets_info",
126+
):
127+
mcp._tool_manager._tools.pop(name, None)
128+
118129
mcp.run(transport=transport.value)
119130

120131

everyrow-mcp/src/everyrow_mcp/sheets_client.py

Lines changed: 10 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
"""Async Google Sheets API client using httpx.
22
3-
Handles token resolution for both HTTP mode (Redis-stored OAuth tokens)
4-
and stdio mode (service account JWT exchange).
3+
Handles token resolution for HTTP mode (Redis-stored OAuth tokens obtained
4+
during the Supabase/Google OAuth flow). Sheets tools are not available in
5+
stdio mode.
56
"""
67

78
from __future__ import annotations
@@ -12,17 +13,13 @@
1213
from typing import Any
1314

1415
import httpx
15-
import jwt as pyjwt
1616

17-
from everyrow_mcp.config import settings
1817
from everyrow_mcp.redis_store import build_key, get_redis_client
1918

2019
logger = logging.getLogger(__name__)
2120

2221
SHEETS_API_BASE = "https://sheets.googleapis.com/v4/spreadsheets"
2322
DRIVE_API_BASE = "https://www.googleapis.com/drive/v3"
24-
GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token"
25-
SCOPES = "https://www.googleapis.com/auth/spreadsheets https://www.googleapis.com/auth/drive.readonly"
2623

2724
# Google token TTL and refresh buffer
2825
GOOGLE_TOKEN_TTL = 3600 # 1 hour
@@ -34,22 +31,14 @@
3431

3532

3633
async def get_google_token() -> str:
37-
"""Resolve a valid Google access token.
38-
39-
- HTTP mode: reads from Redis (stored during OAuth flow), auto-refreshes if near expiry.
40-
- stdio mode: generates from service account JSON via JWT assertion.
41-
"""
42-
if settings.is_http:
43-
return await _get_google_token_http()
44-
return await _get_google_token_stdio()
45-
46-
47-
async def _get_google_token_http() -> str:
48-
"""Get Google token from Redis (HTTP mode).
34+
"""Resolve a valid Google access token from Redis.
4935
5036
The token is stored during the OAuth callback when the user logs in
51-
via Google through Supabase.
37+
via Google through Supabase. Auto-refreshes if near expiry.
38+
39+
Only available in HTTP mode — sheets tools are removed in stdio mode.
5240
"""
41+
5342
redis = get_redis_client()
5443

5544
# Try to get the stored token
@@ -81,6 +70,8 @@ async def _get_google_token_http() -> str:
8170

8271
async def _refresh_google_token_http(refresh_token: str) -> str:
8372
"""Refresh a Google access token using the Supabase-stored refresh token."""
73+
from everyrow_mcp.config import settings # noqa: PLC0415
74+
8475
async with httpx.AsyncClient(timeout=10.0) as client:
8576
# Refresh through Supabase which proxies to Google
8677
resp = await client.post(
@@ -104,69 +95,6 @@ async def _refresh_google_token_http(refresh_token: str) -> str:
10495
return provider_token
10596

10697

107-
async def _get_google_token_stdio() -> str:
108-
"""Get Google token via service account JWT exchange (stdio mode)."""
109-
creds_json = settings.google_sheets_credentials_json
110-
if not creds_json:
111-
raise RuntimeError(
112-
"GOOGLE_SHEETS_CREDENTIALS_JSON not set. "
113-
"Set it to a path to a service account JSON file or inline JSON."
114-
)
115-
116-
# Load service account credentials
117-
sa_info = _load_service_account_info(creds_json)
118-
119-
# Sign JWT assertion
120-
now = int(time.time())
121-
payload = {
122-
"iss": sa_info["client_email"],
123-
"sub": sa_info["client_email"],
124-
"scope": SCOPES,
125-
"aud": GOOGLE_TOKEN_URL,
126-
"iat": now,
127-
"exp": now + GOOGLE_TOKEN_TTL,
128-
}
129-
130-
assertion = pyjwt.encode(
131-
payload,
132-
sa_info["private_key"],
133-
algorithm="RS256",
134-
)
135-
136-
# Exchange JWT for access token
137-
async with httpx.AsyncClient(timeout=10.0) as client:
138-
resp = await client.post(
139-
GOOGLE_TOKEN_URL,
140-
data={
141-
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
142-
"assertion": assertion,
143-
},
144-
)
145-
resp.raise_for_status()
146-
token_data = resp.json()
147-
148-
return token_data["access_token"]
149-
150-
151-
def _load_service_account_info(creds_json: str) -> dict[str, Any]:
152-
"""Load service account info from a file path or inline JSON string."""
153-
import os # noqa: PLC0415
154-
155-
# If it looks like a file path, read it
156-
if os.path.isfile(creds_json):
157-
with open(creds_json) as f:
158-
return json.load(f)
159-
160-
# Otherwise treat as inline JSON
161-
try:
162-
return json.loads(creds_json)
163-
except json.JSONDecodeError as e:
164-
raise ValueError(
165-
f"GOOGLE_SHEETS_CREDENTIALS_JSON is neither a valid file path "
166-
f"nor valid JSON: {e}"
167-
) from e
168-
169-
17098
async def store_google_token(
17199
user_id: str,
172100
access_token: str,

everyrow-mcp/tests/test_sheets_tools.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ async def test_get_spreadsheet_metadata(self):
299299

300300

301301
@pytest.fixture
302-
def mock_google_token():
302+
def _mock_google_token():
303303
"""Patch get_google_token to return a fake token."""
304304
with patch(
305305
"everyrow_mcp.sheets_tools.get_google_token",

everyrow-mcp/tests/test_stdio_content.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
from everyrow_mcp.models import (
4545
AgentInput,
4646
DedupeInput,
47+
HttpResultsInput,
4748
MergeInput,
4849
ProgressInput,
4950
RankInput,
@@ -573,6 +574,16 @@ async def test_results_api_error(self, tmp_path: Path):
573574
class TestToolSchemas:
574575
"""Verify tool schemas expose the expected fields."""
575576

577+
def test_http_results_schema_includes_output_spreadsheet_title(self):
578+
"""HttpResultsInput schema includes output_spreadsheet_title for Google Sheets export."""
579+
schema = HttpResultsInput.model_json_schema()
580+
assert "output_spreadsheet_title" in schema["properties"]
581+
582+
def test_stdio_results_schema_includes_output_spreadsheet_title(self):
583+
"""StdioResultsInput schema includes output_spreadsheet_title for Google Sheets export."""
584+
schema = StdioResultsInput.model_json_schema()
585+
assert "output_spreadsheet_title" in schema["properties"]
586+
576587
@pytest.mark.parametrize(
577588
"tool_name,def_name",
578589
[

0 commit comments

Comments
 (0)