From ea40775a1427c0e67d8d71fb392c7fa88481684d Mon Sep 17 00:00:00 2001 From: DIodide Date: Sat, 22 Aug 2026 15:28:02 -0400 Subject: [PATCH] Force Google consent: the Calendar MCP server 401s only on tool calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Google's server answers initialize and tools/list anonymously, so the connection reached "ready" without OAuth ever starting and My apps never showed a consent link. When the desk has no stored tokens, setup now fires a cheap list_calendars probe — its 401 makes the transport run the authorization leg (discovery, state, PKCE) — and surfaces the consent URL from the live connection's provider, with the addMcpServer redeem path and the persisted row as fallbacks. My apps also reconciles on mount so statuses and pending consent links are always current. Claude-Session: https://claude.ai/code/session_01MKLJUWk6biNAKXupHTTWn5 --- app/src/client/pages/AppsPage.tsx | 9 +++++- app/src/server/pi.ts | 47 +++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/app/src/client/pages/AppsPage.tsx b/app/src/client/pages/AppsPage.tsx index 4d650bc..4892b7f 100644 --- a/app/src/client/pages/AppsPage.tsx +++ b/app/src/client/pages/AppsPage.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { PI_APPS, type AppKey, @@ -19,6 +19,13 @@ export function AppsPage({ const desk = useDesk(settings); const [saving, setSaving] = useState(false); + // Reconcile connections on arrival so statuses and any pending Google + // consent link are current, not left over from the last visit. + useEffect(() => { + void desk.ensureSetup().catch(() => {}); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [desk.settingsHash]); + async function apply(next: PiSettings) { savePrefs(identity.netid, { apps: next.apps, model: next.model }); setSaving(true); diff --git a/app/src/server/pi.ts b/app/src/server/pi.ts index f57331c..815c900 100644 --- a/app/src/server/pi.ts +++ b/app/src/server/pi.ts @@ -167,6 +167,15 @@ export class Pi extends Think { }); if (result.state === "authenticating") { authUrls.gcal = result.authUrl; + } else if (this.isDesk() && !(await this.gcalTokensHas())) { + // Google's MCP server answers initialize and tools/list + // anonymously, so the connection lands "ready" without ever + // triggering OAuth — the 401 only appears on a real tool call. + // Force one so the SDK starts the authorization leg, then + // surface the consent URL it produced. + const url = await this.forceGcalConsent(); + if (url) authUrls.gcal = url; + else appErrors.gcal = "couldn't start Google sign-in — try again"; } continue; } @@ -226,6 +235,44 @@ export class Pi extends Think { return { ok: true as const, count: messages.length }; } + /** + * Kick Google's MCP server with a cheap authenticated-only call so the + * transport's OAuth machinery runs (discovery, state, PKCE) and hands us + * a consent URL. Returns null if calendar access unexpectedly works + * already or no URL could be produced. + */ + private async forceGcalConsent(): Promise { + try { + await this.mcp.callTool({ + serverId: "gcal", + name: "list_calendars", + arguments: {}, + }); + return null; // already authorized somehow — nothing to do + } catch { + // Expected: 401 → the transport started the authorization flow. + } + // The live connection's provider holds the freshly built consent URL. + const manager = this.mcp as unknown as { + mcpConnections?: Record< + string, + { options?: { transport?: { authProvider?: { authUrl?: string } } } } + >; + }; + const live = + manager.mcpConnections?.gcal?.options?.transport?.authProvider?.authUrl; + if (live) return live; + // Fallback: re-registering an existing server redeems a stored auth URL. + const retry = await this.addMcpServer("Google Calendar", GCAL_MCP_URL, { + id: "gcal", + callbackHost: this.appOrigin(), + callbackPath: GCAL_CALLBACK_PATH.slice(1), + transport: { type: "streamable-http" }, + }); + if (retry.state === "authenticating") return retry.authUrl; + return this.getMcpServers().servers.gcal?.auth_url ?? null; + } + /** The per-user desk instance is the token authority for Google OAuth. */ private isDesk(): boolean { return this.name.endsWith("-desk");