Switch Gmail and Gcal to env-var OAuth delivery pattern
auth.deliver changes from as:file to as:env: - Gmail: GMAIL_CREDS_JSON env var (Google authorized_user JSON) - Gcal: GCAL_CREDS_JSON env var mcp_config.env removed entirely — Skald injects the env var at runtime, no path on disk needed. Server scripts updated: - Check GMAIL_CREDS_JSON / GCAL_CREDS_JSON env var first - Use Credentials.from_authorized_user_info() instead of from_authorized_user_file() - Fall back to file-based loading for standalone/legacy use - _persist_creds only writes to disk when _creds_path is set gcal/verify.py: support GCAL_CREDS_JSON env var with shared _check_api() Docs (SKALD.md): updated examples, field table, connector table.
This commit is contained in:
@@ -130,9 +130,7 @@ Configurazione tecnica per l'attivazione del connector.
|
|||||||
],
|
],
|
||||||
"setup_instructions": [
|
"setup_instructions": [
|
||||||
"Install dependencies: pip install -r requirements.txt",
|
"Install dependencies: pip install -r requirements.txt",
|
||||||
"Create secrets/google_oauth_client.json with {\"client_id\": \"...\", \"client_secret\": \"...\"}",
|
"Run: python3 gmail_oauth_setup.py (optional, for standalone use — Skald handles OAuth)"
|
||||||
"Run: python3 gmail_oauth_setup.py (opens browser for OAuth)",
|
|
||||||
"Set GMAIL_CREDS_PATH env var or place token at secrets/gmail_creds.json"
|
|
||||||
],
|
],
|
||||||
"docs": [
|
"docs": [
|
||||||
{
|
{
|
||||||
@@ -149,17 +147,14 @@ Configurazione tecnica per l'attivazione del connector.
|
|||||||
"https://www.googleapis.com/auth/gmail.labels"
|
"https://www.googleapis.com/auth/gmail.labels"
|
||||||
],
|
],
|
||||||
"deliver": {
|
"deliver": {
|
||||||
"as": "file",
|
"as": "env",
|
||||||
"format": "google_authorized_user",
|
"format": "google_authorized_user",
|
||||||
"path": "{secrets}/gmail_creds.json"
|
"env": "GMAIL_CREDS_JSON"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mcp_config": {
|
"mcp_config": {
|
||||||
"command": "python3",
|
"command": "python3",
|
||||||
"args": ["gmail_mcp_server.py"],
|
"args": ["gmail_mcp_server.py"]
|
||||||
"env": {
|
|
||||||
"GMAIL_CREDS_PATH": "{secrets}/gmail_creds.json"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"homepage": "https://mail.google.com",
|
"homepage": "https://mail.google.com",
|
||||||
"icon_small": "icon_sm.svg",
|
"icon_small": "icon_sm.svg",
|
||||||
@@ -233,7 +228,11 @@ Struttura che descrive come il connector gestisce l'autenticazione:
|
|||||||
// OAuth2 — provider SOLO slug (Skald risolve endpoint + client secrets)
|
// OAuth2 — provider SOLO slug (Skald risolve endpoint + client secrets)
|
||||||
{"type": "oauth2", "provider": "google", "scopes": ["...", "..."]}
|
{"type": "oauth2", "provider": "google", "scopes": ["...", "..."]}
|
||||||
|
|
||||||
// OAuth2 con deliver (come Skald consegna la credenziale al processo MCP)
|
// OAuth2 con deliver (Skald inietta il JSON authorized_user via env var)
|
||||||
|
{"type": "oauth2", "provider": "google", "scopes": ["..."],
|
||||||
|
"deliver": {"as": "env", "format": "google_authorized_user", "env": "GMAIL_CREDS_JSON"}}
|
||||||
|
|
||||||
|
// OAuth2 con deliver su file (legacy)
|
||||||
{"type": "oauth2", "provider": "google", "scopes": ["..."],
|
{"type": "oauth2", "provider": "google", "scopes": ["..."],
|
||||||
"deliver": {"as": "file", "format": "google_authorized_user", "path": "{secrets}/gmail_creds.json"}}
|
"deliver": {"as": "file", "format": "google_authorized_user", "path": "{secrets}/gmail_creds.json"}}
|
||||||
|
|
||||||
@@ -253,6 +252,7 @@ Dichiara **come** Skald consegna la credenziale OAuth ottenuta al processo del s
|
|||||||
| `as` | ✅ | `"file"` (su disco) o `"env"` (variabile d'ambiente) |
|
| `as` | ✅ | `"file"` (su disco) o `"env"` (variabile d'ambiente) |
|
||||||
| `format` | ✅ | Nome della serializzazione — es. `"google_authorized_user"` (JSON Google che `from_authorized_user_file` legge), `"refresh_token"`, `"access_token"` |
|
| `format` | ✅ | Nome della serializzazione — es. `"google_authorized_user"` (JSON Google che `from_authorized_user_file` legge), `"refresh_token"`, `"access_token"` |
|
||||||
| `path` | solo `as=file` | Path con placeholder `{secrets}` (Skald lo espande a dir per-utente a runtime). DEVE matchare il path in `mcp_config.env`. |
|
| `path` | solo `as=file` | Path con placeholder `{secrets}` (Skald lo espande a dir per-utente a runtime). DEVE matchare il path in `mcp_config.env`. |
|
||||||
|
| `env` | solo `as=env` | Nome della variabile d'ambiente in cui Skald inietta l'intero JSON authorized_user. **Non va dichiarata in `mcp_config.env`** — Skald la inietta a runtime. |
|
||||||
|
|
||||||
Il feed NON contiene MAI: `client_id`, `client_secret`, `endpoint` URL, `redirect_uri`. Questi sono risolti lato Skald a partire dal nome del `provider`.
|
Il feed NON contiene MAI: `client_id`, `client_secret`, `endpoint` URL, `redirect_uri`. Questi sono risolti lato Skald a partire dal nome del `provider`.
|
||||||
|
|
||||||
@@ -439,10 +439,11 @@ sudo find /var/www/connectors.skaldagent.net/ -type f -exec chmod 644 {} \;
|
|||||||
| ID | Nome | Tipo | Scope | Auth | Verify |
|
| ID | Nome | Tipo | Scope | Auth | Verify |
|
||||||
|----|------|------|-------|------|--------|
|
|----|------|------|-------|------|--------|
|
||||||
| `tavily` | Tavily | `mcp_remote` | `global` | api_key (`{SECRET:tavilyApiKey}` in URL) | `verify.py` (HTTP probe `/search`) |
|
| `tavily` | Tavily | `mcp_remote` | `global` | api_key (`{SECRET:tavilyApiKey}` in URL) | `verify.py` (HTTP probe `/search`) |
|
||||||
| `gmail` | Gmail | `mcp_local` | `user` | oauth2 (Google) + deliver: `file/google_authorized_user` | ⏳ Fase 2 — OAuth via loopback listener |
|
| `gmail` | Gmail | `mcp_local` | `user` | oauth2 (Google) + deliver: `env/google_authorized_user` (via `GMAIL_CREDS_JSON`) | ⏳ Fase 2 — OAuth via loopback listener |
|
||||||
| `gcal` | Google Calendar | `mcp_local` | `user` | oauth2 (Google) + deliver: `file/google_authorized_user` | `verify.py` (creds load + API probe) |
|
| `gcal` | Google Calendar | `mcp_local` | `user` | oauth2 (Google) + deliver: `env/google_authorized_user` (via `GCAL_CREDS_JSON`) | `verify.py` (creds load + API probe) |
|
||||||
| `email` | Email (IMAP/SMTP) | `mcp_local` | `user` | password (env) | `verify.py` (IMAP+SMTP probe) |
|
| `email` | Email (IMAP/SMTP) | `mcp_local` | `user` | password (env) | `verify.py` (IMAP+SMTP probe) |
|
||||||
| `ssh` | SSH Remote Access | `mcp_local` | `user` | none (auth runtime per-alias) | — (nessun setup credential) |
|
| `ssh` | SSH Remote Access | `mcp_local` | `user` | none (auth runtime per-alias) | — (nessun setup credential) |
|
||||||
**Stato del verify-before-save in skald**: `email`, `tavily`, e `gcal` hanno `verify` completo
|
**Stato del verify-before-save in skald**: `email`, `tavily`, e `gcal` hanno `verify` completo
|
||||||
(script + JSON output); `gmail` aspetta la Fase 2 (OAuth via loopback listener).
|
(script + JSON output); `gmail` aspetta la Fase 2 (OAuth via loopback listener).
|
||||||
|
(script + JSON output); `gmail` aspetta la Fase 2 (OAuth via loopback listener).
|
||||||
Un connector senza `verify` viene attivato senza test — vedi § Senza verify.
|
Un connector senza `verify` viene attivato senza test — vedi § Senza verify.
|
||||||
|
|||||||
@@ -31,8 +31,8 @@
|
|||||||
"files": [
|
"files": [
|
||||||
{
|
{
|
||||||
"path": "gmail_mcp_server.py",
|
"path": "gmail_mcp_server.py",
|
||||||
"sha256": "a50d4da9621f7a4b092f4e3c5ae85dec5f466783372f78ca7e792046dc01c673",
|
"sha256": "53e6cdbc4ff40a095d29f7538ec23ac8fc10fb83cb28496bb28a83e7c49deaae",
|
||||||
"size": 46772
|
"size": 47833
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "gmail_oauth_setup.py",
|
"path": "gmail_oauth_setup.py",
|
||||||
@@ -85,8 +85,8 @@
|
|||||||
"files": [
|
"files": [
|
||||||
{
|
{
|
||||||
"path": "gcal_mcp_server.py",
|
"path": "gcal_mcp_server.py",
|
||||||
"sha256": "0d1450ae552aba8035e608fa7785487459b9ecf0b05064c15fb5511d93ac92c1",
|
"sha256": "87b44a407feac90ed073997bd0a558c7002eec0c408d50c45da7d99011dd6107",
|
||||||
"size": 37210
|
"size": 38203
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "gcal_oauth_setup.py",
|
"path": "gcal_oauth_setup.py",
|
||||||
@@ -95,8 +95,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "verify.py",
|
"path": "verify.py",
|
||||||
"sha256": "3bbc8c32bd320ed772972798371a39b03c3e52ce1dfd66a1493cda9e78e428e5",
|
"sha256": "d994ae435d8a5a3d9cdc85c8f73805a877c1a8818637b2912f6c31c693fdded4",
|
||||||
"size": 2328
|
"size": 3429
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "icon_sm.svg",
|
"path": "icon_sm.svg",
|
||||||
|
|||||||
@@ -15,9 +15,7 @@
|
|||||||
],
|
],
|
||||||
"setup_instructions": [
|
"setup_instructions": [
|
||||||
"Install dependencies: pip install -r requirements.txt",
|
"Install dependencies: pip install -r requirements.txt",
|
||||||
"Create secrets/google_oauth_client.json with {\"client_id\": \"...\", \"client_secret\": \"...\"}",
|
"Run: python3 gcal_oauth_setup.py (optional, for standalone use — Skald handles OAuth)"
|
||||||
"Run: python3 gcal_oauth_setup.py (opens browser for OAuth)",
|
|
||||||
"Set GOOGLE_CREDS_PATH env var or place token at secrets/google_creds.json"
|
|
||||||
],
|
],
|
||||||
"docs": [
|
"docs": [
|
||||||
{
|
{
|
||||||
@@ -33,17 +31,14 @@
|
|||||||
"https://www.googleapis.com/auth/calendar"
|
"https://www.googleapis.com/auth/calendar"
|
||||||
],
|
],
|
||||||
"deliver": {
|
"deliver": {
|
||||||
"as": "file",
|
"as": "env",
|
||||||
"format": "google_authorized_user",
|
"format": "google_authorized_user",
|
||||||
"path": "{secrets}/google_creds.json"
|
"env": "GCAL_CREDS_JSON"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mcp_config": {
|
"mcp_config": {
|
||||||
"command": "python3",
|
"command": "python3",
|
||||||
"args": ["gcal_mcp_server.py"],
|
"args": ["gcal_mcp_server.py"]
|
||||||
"env": {
|
|
||||||
"GOOGLE_CREDS_PATH": "{secrets}/google_creds.json"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"verify": {
|
"verify": {
|
||||||
"command": "python3 verify.py",
|
"command": "python3 verify.py",
|
||||||
|
|||||||
@@ -11,14 +11,13 @@ Capabilities (callable as `mcp__gcal__<tool>`):
|
|||||||
delete_event — permanently delete an event
|
delete_event — permanently delete an event
|
||||||
respond_to_event — set RSVP / attendance response
|
respond_to_event — set RSVP / attendance response
|
||||||
|
|
||||||
Credentials are read from ./secrets/google_creds.json by default.
|
Skald mode: Skald injects credentials via GCAL_CREDS_JSON env var (authorized_user JSON).
|
||||||
Override with GOOGLE_CREDS_PATH env var.
|
Standalone mode: reads from GOOGLE_CREDS_PATH or ./secrets/google_creds.json.
|
||||||
|
Run scripts/gcal_oauth_setup.py to (re-)authenticate (standalone).
|
||||||
|
|
||||||
Required OAuth scopes:
|
Required OAuth scopes:
|
||||||
https://www.googleapis.com/auth/calendar
|
https://www.googleapis.com/auth/calendar
|
||||||
(or https://www.googleapis.com/auth/calendar.events for events-only)
|
(or https://www.googleapis.com/auth/calendar.events for events-only)
|
||||||
|
|
||||||
Run scripts/gcal_oauth_setup.py to (re-)authenticate.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -138,7 +137,14 @@ def _persist_creds() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _build_service() -> Any:
|
def _build_service() -> Any:
|
||||||
"""Build and return a Google Calendar service object, or None on failure."""
|
"""Build and return a Google Calendar service object, or None on failure.
|
||||||
|
|
||||||
|
Credentials are loaded in priority order:
|
||||||
|
1. GCAL_CREDS_JSON env var — full authorized_user JSON injected by Skald
|
||||||
|
(Credentials.from_authorized_user_info)
|
||||||
|
2. GOOGLE_CREDS_PATH env var → file on disk (standalone/legacy)
|
||||||
|
3. Default path ./secrets/google_creds.json (standalone use)
|
||||||
|
"""
|
||||||
global _init_error, _creds, _creds_path
|
global _init_error, _creds, _creds_path
|
||||||
try:
|
try:
|
||||||
from google.auth.transport.requests import Request
|
from google.auth.transport.requests import Request
|
||||||
@@ -149,25 +155,40 @@ def _build_service() -> Any:
|
|||||||
log(_init_error)
|
log(_init_error)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
_creds_path = os.environ.get(
|
_SCOPES = [
|
||||||
"GOOGLE_CREDS_PATH",
|
"https://www.googleapis.com/auth/calendar",
|
||||||
os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "secrets", "google_creds.json"),
|
]
|
||||||
)
|
|
||||||
|
|
||||||
if not os.path.exists(_creds_path):
|
raw = os.environ.get("GCAL_CREDS_JSON")
|
||||||
_init_error = (
|
if raw:
|
||||||
f"Credentials file not found at {_creds_path}. "
|
try:
|
||||||
"Run scripts/gcal_oauth_setup.py to authenticate, or set GOOGLE_CREDS_PATH."
|
creds = Credentials.from_authorized_user_info(json.loads(raw), _SCOPES)
|
||||||
|
_creds_path = None # no file, persisted only in env
|
||||||
|
log("Credentials loaded from GCAL_CREDS_JSON env var.")
|
||||||
|
except Exception as e:
|
||||||
|
_init_error = f"Failed to load credentials from GCAL_CREDS_JSON: {e}"
|
||||||
|
log(_init_error)
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
_creds_path = os.environ.get(
|
||||||
|
"GOOGLE_CREDS_PATH",
|
||||||
|
os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "secrets", "google_creds.json"),
|
||||||
)
|
)
|
||||||
log(_init_error)
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
if not os.path.exists(_creds_path):
|
||||||
creds = Credentials.from_authorized_user_file(_creds_path)
|
_init_error = (
|
||||||
except Exception as e:
|
f"Credentials not found. Set GCAL_CREDS_JSON env var (Skald mode) or "
|
||||||
_init_error = f"Failed to load credentials from {_creds_path}: {e}"
|
f"place credentials file at {_creds_path} (standalone mode)."
|
||||||
log(_init_error)
|
)
|
||||||
return None
|
log(_init_error)
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
creds = Credentials.from_authorized_user_file(_creds_path)
|
||||||
|
except Exception as e:
|
||||||
|
_init_error = f"Failed to load credentials from {_creds_path}: {e}"
|
||||||
|
log(_init_error)
|
||||||
|
return None
|
||||||
|
|
||||||
# Publish creds globally so _persist_creds / _call can see them.
|
# Publish creds globally so _persist_creds / _call can see them.
|
||||||
_creds = creds
|
_creds = creds
|
||||||
@@ -176,7 +197,8 @@ def _build_service() -> Any:
|
|||||||
if creds.expired and creds.refresh_token:
|
if creds.expired and creds.refresh_token:
|
||||||
try:
|
try:
|
||||||
creds.refresh(Request())
|
creds.refresh(Request())
|
||||||
_persist_creds()
|
if _creds_path:
|
||||||
|
_persist_creds()
|
||||||
log("Token refreshed and saved.")
|
log("Token refreshed and saved.")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log(f"Token refresh failed: {e}")
|
log(f"Token refresh failed: {e}")
|
||||||
@@ -188,7 +210,7 @@ def _build_service() -> Any:
|
|||||||
log(_init_error)
|
log(_init_error)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
log(f"Calendar service built successfully (creds: {_creds_path})")
|
log(f"Calendar service built successfully (creds: env var or {_creds_path})")
|
||||||
return service
|
return service
|
||||||
|
|
||||||
|
|
||||||
@@ -319,8 +341,8 @@ def _gcal_status(args: dict | None = None) -> str:
|
|||||||
if svc is None:
|
if svc is None:
|
||||||
return _status_report("❌", "NOT_CONFIGURED", "action needed",
|
return _status_report("❌", "NOT_CONFIGURED", "action needed",
|
||||||
f"The Google Calendar service could not be built: {_init_error or 'unknown error'}.",
|
f"The Google Calendar service could not be built: {_init_error or 'unknown error'}.",
|
||||||
["Run scripts/gcal_oauth_setup.py to authenticate and create secrets/google_creds.json.",
|
["Run scripts/gcal_oauth_setup.py to authenticate (standalone).",
|
||||||
"Or set the GOOGLE_CREDS_PATH env var to point at an existing credentials file."])
|
"For Skald mode, ensure GCAL_CREDS_JSON env var is set."])
|
||||||
|
|
||||||
# Step 2: live probe — refresh-on-auth-error is handled inside _call.
|
# Step 2: live probe — refresh-on-auth-error is handled inside _call.
|
||||||
try:
|
try:
|
||||||
|
|||||||
+57
-26
@@ -1,9 +1,12 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Verify Google Calendar OAuth credentials are valid.
|
"""Verify Google Calendar OAuth credentials are valid.
|
||||||
|
|
||||||
Reads GOOGLE_CREDS_PATH env var (or falls back to secrets/google_creds.json),
|
Priority:
|
||||||
loads the authorized_user credentials, and does one cheap API probe
|
1. GCAL_CREDS_JSON env var — authorized_user JSON injected by Skald.
|
||||||
(calendarList.list(maxResults=1)).
|
2. GOOGLE_CREDS_PATH env var → file on disk (standalone).
|
||||||
|
3. Default path ./secrets/google_creds.json (standalone).
|
||||||
|
|
||||||
|
Loads credentials and does one cheap API probe (calendarList.list(maxResults=1)).
|
||||||
|
|
||||||
Output: single JSON object on stdout:
|
Output: single JSON object on stdout:
|
||||||
{"ok": true, "message": "..."}
|
{"ok": true, "message": "..."}
|
||||||
@@ -17,40 +20,20 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def _check_api(creds) -> None:
|
||||||
creds_path = os.environ.get(
|
"""Perform a live Calendar API probe with the given credentials."""
|
||||||
"GOOGLE_CREDS_PATH",
|
|
||||||
os.path.join(os.path.dirname(__file__), "secrets", "google_creds.json"),
|
|
||||||
)
|
|
||||||
|
|
||||||
if not os.path.exists(creds_path):
|
|
||||||
_fail(f"Credentials file not found at {creds_path}. Run gcal_oauth_setup.py first.")
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from google.auth.transport.requests import Request
|
from google.auth.transport.requests import Request
|
||||||
from google.oauth2.credentials import Credentials
|
|
||||||
from googleapiclient.discovery import build
|
from googleapiclient.discovery import build
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
_fail(f"Missing dependencies: {e}. Install google-api-python-client and google-auth.")
|
_fail(f"Missing dependencies: {e}. Install google-api-python-client and google-auth.")
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
|
||||||
creds = Credentials.from_authorized_user_file(
|
|
||||||
creds_path,
|
|
||||||
["https://www.googleapis.com/auth/calendar"],
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
_fail(f"Failed to load credentials: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
if creds.expired and creds.refresh_token:
|
if creds.expired and creds.refresh_token:
|
||||||
try:
|
try:
|
||||||
creds.refresh(Request())
|
creds.refresh(Request())
|
||||||
with open(creds_path, "w") as f:
|
|
||||||
f.write(creds.to_json())
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
_fail(f"Token refresh failed: {e}. Re-run gcal_oauth_setup.py.")
|
_fail(f"Token refresh failed: {e}")
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -64,12 +47,60 @@ def main() -> None:
|
|||||||
_fail(f"API probe failed: {e}")
|
_fail(f"API probe failed: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
# Priority 1: env var with full JSON (Skald mode)
|
||||||
|
raw = os.environ.get("GCAL_CREDS_JSON")
|
||||||
|
if raw:
|
||||||
|
try:
|
||||||
|
from google.oauth2.credentials import Credentials
|
||||||
|
creds = Credentials.from_authorized_user_info(
|
||||||
|
json.loads(raw),
|
||||||
|
["https://www.googleapis.com/auth/calendar"],
|
||||||
|
)
|
||||||
|
_check_api(creds)
|
||||||
|
return
|
||||||
|
except ImportError as e:
|
||||||
|
_fail(f"Missing dependencies: {e}. Install google-api-python-client and google-auth.")
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
_fail(f"Failed to load credentials from GCAL_CREDS_JSON: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Priority 2/3: file-based (standalone use)
|
||||||
|
creds_path = os.environ.get(
|
||||||
|
"GOOGLE_CREDS_PATH",
|
||||||
|
os.path.join(os.path.dirname(__file__), "secrets", "google_creds.json"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if not os.path.exists(creds_path):
|
||||||
|
_fail(f"Credentials not found. Set GCAL_CREDS_JSON env var (Skald mode) or "
|
||||||
|
f"place credentials file at {creds_path} (standalone mode).")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
from google.oauth2.credentials import Credentials
|
||||||
|
creds = Credentials.from_authorized_user_file(
|
||||||
|
creds_path,
|
||||||
|
["https://www.googleapis.com/auth/calendar"],
|
||||||
|
)
|
||||||
|
except ImportError as e:
|
||||||
|
_fail(f"Missing dependencies: {e}. Install google-api-python-client and google-auth.")
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
_fail(f"Failed to load credentials: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
_check_api(creds)
|
||||||
|
|
||||||
|
|
||||||
def _ok(message: str) -> None:
|
def _ok(message: str) -> None:
|
||||||
json.dump({"ok": True, "message": message}, sys.stdout)
|
json.dump({"ok": True, "message": message}, sys.stdout)
|
||||||
|
sys.stdout.write("\n")
|
||||||
|
|
||||||
|
|
||||||
def _fail(message: str) -> None:
|
def _fail(message: str) -> None:
|
||||||
json.dump({"ok": False, "message": message}, sys.stdout)
|
json.dump({"ok": False, "message": message}, sys.stdout)
|
||||||
|
sys.stdout.write("\n")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -16,9 +16,7 @@
|
|||||||
],
|
],
|
||||||
"setup_instructions": [
|
"setup_instructions": [
|
||||||
"Install dependencies: pip install -r requirements.txt",
|
"Install dependencies: pip install -r requirements.txt",
|
||||||
"Create secrets/google_oauth_client.json with {\"client_id\": \"...\", \"client_secret\": \"...\"}",
|
"Run: python3 gmail_oauth_setup.py (optional, for standalone use — Skald handles OAuth)"
|
||||||
"Run: python3 gmail_oauth_setup.py (opens browser for OAuth)",
|
|
||||||
"Set GMAIL_CREDS_PATH env var or place token at secrets/gmail_creds.json"
|
|
||||||
],
|
],
|
||||||
"docs": [
|
"docs": [
|
||||||
{
|
{
|
||||||
@@ -31,10 +29,7 @@
|
|||||||
"command": "python3",
|
"command": "python3",
|
||||||
"args": [
|
"args": [
|
||||||
"gmail_mcp_server.py"
|
"gmail_mcp_server.py"
|
||||||
],
|
]
|
||||||
"env": {
|
|
||||||
"GMAIL_CREDS_PATH": "{secrets}/gmail_creds.json"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"homepage": "https://mail.google.com",
|
"homepage": "https://mail.google.com",
|
||||||
"icon_small": "icon_sm.svg",
|
"icon_small": "icon_sm.svg",
|
||||||
@@ -54,9 +49,9 @@
|
|||||||
"https://www.googleapis.com/auth/gmail.labels"
|
"https://www.googleapis.com/auth/gmail.labels"
|
||||||
],
|
],
|
||||||
"deliver": {
|
"deliver": {
|
||||||
"as": "file",
|
"as": "env",
|
||||||
"format": "google_authorized_user",
|
"format": "google_authorized_user",
|
||||||
"path": "{secrets}/gmail_creds.json"
|
"env": "GMAIL_CREDS_JSON"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,10 +15,9 @@ Capabilities (callable as `mcp__gmail__<tool>`):
|
|||||||
|
|
||||||
Provides read, modify, and send access to Gmail via the Gmail API v1.
|
Provides read, modify, and send access to Gmail via the Gmail API v1.
|
||||||
|
|
||||||
Credentials are read from ./secrets/gmail_creds.json by default.
|
Skald mode: Skald injects credentials via GMAIL_CREDS_JSON env var (authorized_user JSON).
|
||||||
Override with GMAIL_CREDS_PATH env var.
|
Standalone mode: reads from GMAIL_CREDS_PATH or ./secrets/gmail_creds.json.
|
||||||
|
Run scripts/gmail_oauth_setup.py first to generate the OAuth token (standalone).
|
||||||
Run scripts/gmail_oauth_setup.py first to generate the OAuth token.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -156,7 +155,14 @@ def _persist_creds() -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _build_service() -> Any:
|
def _build_service() -> Any:
|
||||||
"""Build and return a Gmail service object, or None on failure."""
|
"""Build and return a Gmail service object, or None on failure.
|
||||||
|
|
||||||
|
Credentials are loaded in priority order:
|
||||||
|
1. GMAIL_CREDS_JSON env var — full authorized_user JSON injected by Skald
|
||||||
|
(Credentials.from_authorized_user_info)
|
||||||
|
2. GMAIL_CREDS_PATH env var → file on disk (standalone/legacy)
|
||||||
|
3. Default path ./secrets/gmail_creds.json (standalone use)
|
||||||
|
"""
|
||||||
global _init_error, _creds, _creds_path
|
global _init_error, _creds, _creds_path
|
||||||
try:
|
try:
|
||||||
from google.auth.transport.requests import Request
|
from google.auth.transport.requests import Request
|
||||||
@@ -167,25 +173,41 @@ def _build_service() -> Any:
|
|||||||
log(_init_error)
|
log(_init_error)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
_creds_path = os.environ.get(
|
_SCOPES = [
|
||||||
"GMAIL_CREDS_PATH",
|
"https://www.googleapis.com/auth/gmail.modify",
|
||||||
os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "secrets", "gmail_creds.json"),
|
"https://www.googleapis.com/auth/gmail.labels",
|
||||||
)
|
]
|
||||||
|
|
||||||
if not os.path.exists(_creds_path):
|
raw = os.environ.get("GMAIL_CREDS_JSON")
|
||||||
_init_error = (
|
if raw:
|
||||||
f"Credentials file not found at {_creds_path}. "
|
try:
|
||||||
"Run scripts/gmail_oauth_setup.py first, or set GMAIL_CREDS_PATH."
|
creds = Credentials.from_authorized_user_info(json.loads(raw), _SCOPES)
|
||||||
|
_creds_path = None # no file, persisted only in env
|
||||||
|
log("Credentials loaded from GMAIL_CREDS_JSON env var.")
|
||||||
|
except Exception as e:
|
||||||
|
_init_error = f"Failed to load credentials from GMAIL_CREDS_JSON: {e}"
|
||||||
|
log(_init_error)
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
_creds_path = os.environ.get(
|
||||||
|
"GMAIL_CREDS_PATH",
|
||||||
|
os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "secrets", "gmail_creds.json"),
|
||||||
)
|
)
|
||||||
log(_init_error)
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
if not os.path.exists(_creds_path):
|
||||||
creds = Credentials.from_authorized_user_file(_creds_path)
|
_init_error = (
|
||||||
except Exception as e:
|
f"Credentials not found. Set GMAIL_CREDS_JSON env var (Skald mode) or "
|
||||||
_init_error = f"Failed to load credentials from {_creds_path}: {e}"
|
f"place credentials file at {_creds_path} (standalone mode)."
|
||||||
log(_init_error)
|
)
|
||||||
return None
|
log(_init_error)
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
creds = Credentials.from_authorized_user_file(_creds_path)
|
||||||
|
except Exception as e:
|
||||||
|
_init_error = f"Failed to load credentials from {_creds_path}: {e}"
|
||||||
|
log(_init_error)
|
||||||
|
return None
|
||||||
|
|
||||||
# Publish creds globally so _persist_creds / _call can see them.
|
# Publish creds globally so _persist_creds / _call can see them.
|
||||||
_creds = creds
|
_creds = creds
|
||||||
@@ -195,10 +217,11 @@ def _build_service() -> Any:
|
|||||||
if not creds.valid:
|
if not creds.valid:
|
||||||
if creds.expired and creds.refresh_token:
|
if creds.expired and creds.refresh_token:
|
||||||
creds.refresh(Request())
|
creds.refresh(Request())
|
||||||
_persist_creds()
|
if _creds_path:
|
||||||
|
_persist_creds()
|
||||||
log("Token refreshed and saved.")
|
log("Token refreshed and saved.")
|
||||||
else:
|
else:
|
||||||
_init_error = "Credentials invalid and cannot be refreshed. Re-run scripts/gmail_oauth_setup.py."
|
_init_error = "Credentials invalid and cannot be refreshed. Re-run OAuth setup."
|
||||||
log(_init_error)
|
log(_init_error)
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -213,7 +236,7 @@ def _build_service() -> Any:
|
|||||||
log(_init_error)
|
log(_init_error)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
log(f"Gmail service built successfully (creds: {_creds_path})")
|
log(f"Gmail service built successfully (creds: env var or {_creds_path})")
|
||||||
return service
|
return service
|
||||||
|
|
||||||
|
|
||||||
@@ -450,8 +473,8 @@ def _gmail_status(args: dict | None = None) -> str:
|
|||||||
if svc is None:
|
if svc is None:
|
||||||
return _status_report("❌", "NOT_CONFIGURED", "action needed",
|
return _status_report("❌", "NOT_CONFIGURED", "action needed",
|
||||||
f"The Gmail service could not be built: {_init_error or 'unknown error'}.",
|
f"The Gmail service could not be built: {_init_error or 'unknown error'}.",
|
||||||
["Run scripts/gmail_oauth_setup.py to authenticate and create secrets/gmail_creds.json.",
|
["Run scripts/gmail_oauth_setup.py to authenticate (standalone).",
|
||||||
"Or set the GMAIL_CREDS_PATH env var to point at an existing credentials file."])
|
"For Skald mode, ensure GMAIL_CREDS_JSON env var is set."])
|
||||||
|
|
||||||
# Step 2: live probe — refresh-on-auth-error is handled inside _call.
|
# Step 2: live probe — refresh-on-auth-error is handled inside _call.
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user