From f582a1e6fb74cb9461ae4eae23e596d8a74aabfc Mon Sep 17 00:00:00 2001 From: yuriassuncx Date: Wed, 21 Oct 2026 13:33:31 -0300 Subject: [PATCH 1/2] fix(google-analytics): call runFunnelReport on the v1alpha Data API The v1beta path does not exist for funnels, so run-funnel-report always returned 'Google Analytics API error: 404' (verified 2026-10-21 against property 355495152; the same request succeeds on v1alpha). decocms/reports already works around it the same way in api/connectors/data/ga4.ts. Co-Authored-By: Claude Opus 5.5 --- google-analytics/server/lib/ga-client.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/google-analytics/server/lib/ga-client.ts b/google-analytics/server/lib/ga-client.ts index 005335bf..e41c2517 100644 --- a/google-analytics/server/lib/ga-client.ts +++ b/google-analytics/server/lib/ga-client.ts @@ -5,6 +5,8 @@ const GA4_DATA_API = "https://analyticsdata.googleapis.com/v1beta"; const GA4_ADMIN_API = "https://analyticsadmin.googleapis.com/v1beta"; // Property annotations are v1alpha-only (not present in v1beta). const GA4_ADMIN_ALPHA_API = "https://analyticsadmin.googleapis.com/v1alpha"; +// runFunnelReport only exists on the Data API's v1alpha; the v1beta path 404s. +const GA4_DATA_ALPHA_API = "https://analyticsdata.googleapis.com/v1alpha"; // Hard limit on pagination loops to prevent runaway requests if GA4 returns a // repeated nextPageToken during a quota event or transient backend failure. @@ -107,7 +109,7 @@ export class GaClient { async runFunnelReport(property: string, body: object): Promise { const prop = normalizeProperty(property); - return this.request(`${GA4_DATA_API}/${prop}:runFunnelReport`, { + return this.request(`${GA4_DATA_ALPHA_API}/${prop}:runFunnelReport`, { method: "POST", body: JSON.stringify(body), }); From 5c3590f31e946766f2bb290850e13f3bb0e64246 Mon Sep 17 00:00:00 2001 From: yuriassuncx Date: Wed, 21 Oct 2026 13:41:15 -0300 Subject: [PATCH 2/2] fix(google-workspace): explain Google's agentic-client rejection; allow a dedicated client From 2026-11-11 Google's Workspace MCP servers (gmailmcp, drivemcp, calendarmcp, chatmcp, people) reject OAuth clients not marked as agentic. This package proxies to exactly those servers, so it is the one MCP here that breaks if the client is not marked. - mcp-proxy: a 'not marked as an AI agent' rejection now says so and that re-auth will not help, instead of the generic 403 missing-scope hint. - main: optional GOOGLE_WORKSPACE_CLIENT_ID/SECRET for a dedicated agentic client (falls back to the shared GOOGLE_CLIENT_ID). - README: the deadline, what to mark, and which MCPs are unaffected (REST). Co-Authored-By: Claude Opus 5.5 --- google-workspace/README.md | 10 ++++++ google-workspace/server/lib/mcp-proxy.test.ts | 16 +++++++++ google-workspace/server/lib/mcp-proxy.ts | 36 +++++++++++++++---- google-workspace/server/main.ts | 10 +++++- 4 files changed, 65 insertions(+), 7 deletions(-) create mode 100644 google-workspace/server/lib/mcp-proxy.test.ts diff --git a/google-workspace/README.md b/google-workspace/README.md index 5a0ef506..dd7cbf1a 100644 --- a/google-workspace/README.md +++ b/google-workspace/README.md @@ -54,6 +54,16 @@ mesh client ──► google-workspace MCP ──► calendarmcp.googleapis.com/ The OAuth flow is the standard `createGoogleOAuth` from `@decocms/mcps-shared` — PKCE with refresh-token rotation. The full union of scopes is sent at consent time, so a single approval covers every service. +### The OAuth client must be marked "agentic" + +From **2026-11-11** Google's Workspace MCP servers reject OAuth clients that are not marked as agentic (`is_agentic`, Google Cloud Console → Credentials → the OAuth client). From **2026-11-12** the setting is permanent: changing it means a new client ID, and every user reconnects. + +- The client in use is `GOOGLE_WORKSPACE_CLIENT_ID` / `GOOGLE_WORKSPACE_CLIENT_SECRET` when set, else the shared `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET`. +- Google recommends a separate client when one client serves both agent and non-agent traffic. All MCPs in this repo are agent traffic, so marking the shared client is enough unless it is also used elsewhere (e.g. a human sign-in). +- If the client is not marked, tool calls fail with an explicit "not marked as agentic" error instead of the generic 403 re-auth hint. + +The REST-based Google MCPs in this repo (`google-gmail`, `google-drive`, `google-analytics`, ...) call the REST APIs, not Google's MCP servers, so they keep working either way. + ## Tool definitions are committed snapshots Tool names, descriptions, JSON schemas and PRM scopes for each backend live in `server/tools/generated/.json`. These are committed to the repo for reproducible builds. diff --git a/google-workspace/server/lib/mcp-proxy.test.ts b/google-workspace/server/lib/mcp-proxy.test.ts new file mode 100644 index 00000000..8f5e6caa --- /dev/null +++ b/google-workspace/server/lib/mcp-proxy.test.ts @@ -0,0 +1,16 @@ +import { expect, test } from "bun:test"; +import { notAgenticError } from "./mcp-proxy.ts"; + +test("explains Google's not-agentic rejection instead of a re-auth hint", () => { + const err = notAgenticError( + "The client is not marked as an AI agent. See https://...", + ); + expect(err?.message).toContain("not marked as agentic"); + expect(err?.message).toContain("Re-authenticating will not help"); +}); + +test("leaves ordinary errors alone", () => { + expect( + notAgenticError("Request had insufficient authentication scopes."), + ).toBeNull(); +}); diff --git a/google-workspace/server/lib/mcp-proxy.ts b/google-workspace/server/lib/mcp-proxy.ts index 36f843bf..b42b7205 100644 --- a/google-workspace/server/lib/mcp-proxy.ts +++ b/google-workspace/server/lib/mcp-proxy.ts @@ -20,6 +20,21 @@ interface JsonRpcError { type JsonRpcResponse = JsonRpcSuccess | JsonRpcError; +/** + * From 2026-11-11 Google's Workspace MCP servers only accept OAuth clients + * flagged "agentic" (is_agentic) in Google Cloud Console; the rejection says + * the client "is not marked as an AI agent". Re-authenticating cannot fix + * that, so it must not be reported as a missing scope. + */ +export function notAgenticError(text: string): Error | null { + if (!/agentic|ai agent/i.test(text)) return null; + return new Error( + `Google rejected this MCP's OAuth client because it is not marked as agentic (is_agentic). ` + + `Re-authenticating will not help: a Google Cloud admin must enable the agentic setting on the ` + + `OAuth client (GOOGLE_WORKSPACE_CLIENT_ID, or GOOGLE_CLIENT_ID if unset). Upstream: ${text}`, + ); +} + export async function proxyMcpCall( backendUrl: string, toolName: string, @@ -51,23 +66,32 @@ export async function proxyMcpCall( if (res.status === 403) { const text = await res.text(); - throw new Error( - `Google ${backendUrl} returned 403 Forbidden. The token is missing the required scope. Re-authenticate to grant the new permission. Body: ${text}`, + throw ( + notAgenticError(text) ?? + new Error( + `Google ${backendUrl} returned 403 Forbidden. The token is missing the required scope. Re-authenticate to grant the new permission. Body: ${text}`, + ) ); } if (!res.ok) { const text = await res.text(); - throw new Error( - `Google MCP backend error ${res.status} on ${toolName}: ${text}`, + throw ( + notAgenticError(text) ?? + new Error( + `Google MCP backend error ${res.status} on ${toolName}: ${text}`, + ) ); } const json = (await res.json()) as JsonRpcResponse; if ("error" in json) { - throw new Error( - `Google MCP JSON-RPC error on ${toolName}: ${json.error.message} (code ${json.error.code})`, + throw ( + notAgenticError(json.error.message) ?? + new Error( + `Google MCP JSON-RPC error on ${toolName}: ${json.error.message} (code ${json.error.code})`, + ) ); } diff --git a/google-workspace/server/main.ts b/google-workspace/server/main.ts index 961d3693..586417d3 100644 --- a/google-workspace/server/main.ts +++ b/google-workspace/server/main.ts @@ -17,7 +17,15 @@ const runtime = withRuntime({ }, tools: (env: Env) => tools.map((createTool) => createTool(env)), prompts: () => prompts, - oauth: createGoogleOAuth({ scopes: GOOGLE_WORKSPACE_SCOPES }), + // Google's Workspace MCPs only accept OAuth clients marked "agentic". These + // optional vars let this MCP use its own agentic client instead of the + // GOOGLE_CLIENT_ID shared with the REST-based Google MCPs. Switching clients + // invalidates existing refresh tokens, so every user reconnects once. + oauth: createGoogleOAuth({ + scopes: GOOGLE_WORKSPACE_SCOPES, + clientId: process.env.GOOGLE_WORKSPACE_CLIENT_ID || undefined, + clientSecret: process.env.GOOGLE_WORKSPACE_CLIENT_SECRET || undefined, + }), }); serve(runtime.fetch);