diff --git a/templates/calendar/app/components/calendar/AttendeeAutocomplete.tsx b/templates/calendar/app/components/calendar/AttendeeAutocomplete.tsx index 649701120..d4b38dd31 100644 --- a/templates/calendar/app/components/calendar/AttendeeAutocomplete.tsx +++ b/templates/calendar/app/components/calendar/AttendeeAutocomplete.tsx @@ -23,6 +23,7 @@ import { } from "@/components/ui/popover"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; +import { isMcpEmbedSurface } from "@/lib/mcp-embed"; const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; @@ -446,13 +447,17 @@ export const AttendeeAutocomplete = forwardRef< addPerson(person); }} > - {person.photoUrl ? ( + {person.photoUrl && !isMcpEmbedSurface() ? ( ) : ( + // MCP host iframes (ChatGPT / Claude) block cross-origin + // googleusercontent.com contact avatars at the COEP layer + // and produce console errors. Fall back to initials when + // rendered in an embedded surface. {initialsFor(person) || ( diff --git a/templates/calendar/app/lib/mcp-embed.spec.ts b/templates/calendar/app/lib/mcp-embed.spec.ts new file mode 100644 index 000000000..b861aba5d --- /dev/null +++ b/templates/calendar/app/lib/mcp-embed.spec.ts @@ -0,0 +1,27 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { isMcpEmbedSurface } from "./mcp-embed"; + +describe("isMcpEmbedSurface (calendar)", () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("returns false outside the browser", () => { + expect(isMcpEmbedSurface()).toBe(false); + }); + + it("detects ?embedded=1 query params (MCP iframe surface)", () => { + vi.stubGlobal("window", { location: { search: "?embedded=1" } }); + expect(isMcpEmbedSurface()).toBe(true); + }); + + it("accepts the truthy 'true' legacy value", () => { + vi.stubGlobal("window", { location: { search: "?embedded=true" } }); + expect(isMcpEmbedSurface()).toBe(true); + }); + + it("ignores ordinary in-app routes without the embed flag", () => { + vi.stubGlobal("window", { location: { search: "?date=2026-05-23" } }); + expect(isMcpEmbedSurface()).toBe(false); + }); +}); diff --git a/templates/calendar/app/lib/mcp-embed.ts b/templates/calendar/app/lib/mcp-embed.ts new file mode 100644 index 000000000..2f317e7d2 --- /dev/null +++ b/templates/calendar/app/lib/mcp-embed.ts @@ -0,0 +1,20 @@ +/** + * MCP embed surface detection for Calendar. + * + * When the Calendar UI is rendered inside an MCP host's iframe (ChatGPT / + * Claude.ai render `*.agent-native.com` through their own sandboxed wrapper + * with strict COEP/CORP headers), cross-origin third-party images + * (`googleusercontent.com` avatars, etc.) get blocked at the browser level + * and produce noisy console errors. Templates that ship images that work + * fine in their own UI need to gate them on this flag and fall back to + * a same-origin placeholder when embedded. + * + * Mirrors `templates/mail/app/lib/mcp-embed.ts` (added in PR #883). A future + * refactor could lift this to `@agent-native/core/client` so every template + * uses one helper — for now keep it template-local to match the Mail PR. + */ +export function isMcpEmbedSurface(): boolean { + if (typeof window === "undefined") return false; + const value = new URLSearchParams(window.location.search).get("embedded"); + return value === "1" || value === "true"; +} diff --git a/templates/calendar/app/pages/CalendarView.tsx b/templates/calendar/app/pages/CalendarView.tsx index 0a3c1db4c..f9e7e050d 100644 --- a/templates/calendar/app/pages/CalendarView.tsx +++ b/templates/calendar/app/pages/CalendarView.tsx @@ -1,6 +1,7 @@ import { useState, useMemo, useEffect, useCallback, useRef } from "react"; import { Link } from "react-router"; import { cn } from "@/lib/utils"; +import { isMcpEmbedSurface } from "@/lib/mcp-embed"; import { format, startOfMonth, @@ -1492,7 +1493,7 @@ function AccountAvatars() { zIndex: accounts.length - i, }} > - {account.photoUrl ? ( + {account.photoUrl && !isMcpEmbedSurface() ? ( ) : ( + // MCP host iframes (ChatGPT / Claude) ship strict COEP/CORP + // headers that block cross-origin googleusercontent.com + // avatars and produce noisy console errors. Fall back to a + // same-origin initial chip when embedded. See + // `templates/calendar/app/lib/mcp-embed.ts`.
{account.email[0]?.toUpperCase()}