Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app/src/server/gcal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ export class GoogleOAuthProvider extends DurableObjectOAuthClientProvider {
private readonly tokenStore: GcalTokenStore
) {
super(storage, clientName, fixedRedirectUrl);
// The stock provider only learns its clientId from dynamic registration,
// which Google doesn't do — and its clientId getter THROWS when unset,
// killing the auth leg inside saveCodeVerifier. Pin it up front.
this.clientId = client.client_id;
}

override get redirectUrl(): string {
Expand Down Expand Up @@ -72,6 +76,9 @@ export class GoogleOAuthProvider extends DurableObjectOAuthClientProvider {
}

override async redirectToAuthorization(authUrl: URL): Promise<void> {
// The SDK derives scopes from the server's resource metadata, which
// advertises WRITE scopes too — pin the request to our read-only trio.
authUrl.searchParams.set("scope", GCAL_SCOPES.join(" "));
// Without offline access Google issues no refresh token and the
// connection would die within the hour; prompt=consent guarantees a
// refresh token on re-grants too.
Expand Down
29 changes: 20 additions & 9 deletions app/src/server/pi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,15 +167,6 @@ export class Pi extends Think<Env, PiState> {
});
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;
}
Expand All @@ -191,6 +182,26 @@ export class Pi extends Think<Env, PiState> {
}
}

// Google's MCP server answers initialize and tools/list anonymously, so
// a tokenless connection lands "ready" without OAuth ever starting — the
// 401 only appears on a real tool call. If the desk is connected but has
// no tokens (fresh toggle OR a connection left over from an earlier
// visit), force that call and surface the consent URL it produces.
if (
enabled.has("gcal") &&
this.isDesk() &&
!authUrls.gcal &&
!appErrors.gcal &&
this.env.GOOGLE_OAUTH_CLIENT_ID &&
this.env.GOOGLE_OAUTH_CLIENT_SECRET &&
this.getMcpServers().servers.gcal &&
!(await this.gcalTokensHas())
) {
const url = await this.forceGcalConsent();
if (url) authUrls.gcal = url;
else appErrors.gcal = "couldn't start Google sign-in — try again";
}

this.setState({ settings, appErrors, authUrls });
return { ok: true as const, appErrors, authUrls };
}
Expand Down
Loading