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
Original file line number Diff line number Diff line change
Expand Up @@ -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@]+$/;

Expand Down Expand Up @@ -446,13 +447,17 @@ export const AttendeeAutocomplete = forwardRef<
addPerson(person);
}}
>
{person.photoUrl ? (
{person.photoUrl && !isMcpEmbedSurface() ? (
<img
src={person.photoUrl}
alt=""
className="h-8 w-8 shrink-0 rounded-full object-cover"
/>
) : (
// 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.
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-muted text-[11px] font-medium text-muted-foreground">
{initialsFor(person) || (
<IconUserCircle className="h-4 w-4" />
Expand Down
27 changes: 27 additions & 0 deletions templates/calendar/app/lib/mcp-embed.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
20 changes: 20 additions & 0 deletions templates/calendar/app/lib/mcp-embed.ts
Original file line number Diff line number Diff line change
@@ -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";
}
8 changes: 7 additions & 1 deletion templates/calendar/app/pages/CalendarView.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -1492,14 +1493,19 @@ function AccountAvatars() {
zIndex: accounts.length - i,
}}
>
{account.photoUrl ? (
{account.photoUrl && !isMcpEmbedSurface() ? (
<img
src={account.photoUrl}
alt=""
className="h-7 w-7 rounded-full object-cover"
referrerPolicy="no-referrer"
/>
) : (
// 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`.
<div className="flex h-7 w-7 items-center justify-center rounded-full bg-primary/20 text-[11px] font-semibold text-primary">
{account.email[0]?.toUpperCase()}
</div>
Expand Down
Loading