Hi everyone,
I’m trying to isolate what looks like a ChatGPT Android app issue with Apps SDK / MCP widget fullscreen behavior.
The issue seems to happen specifically when the widget/app is invoked in the first message of a new conversation.
I created a minimal MCP widget to reproduce it. The widget only renders one inline button. When the user taps the button, it calls:
await window.openai?.requestDisplayMode?.({ mode: "fullscreen" });
Then it replaces the body with a simple hello world screen.
The behavior on the ChatGPT Android app is:
| Scenario | Result |
|---|---|
| New conversation → first message asks to open the widget → tap fullscreen button | Fullscreen opens blank |
New conversation → send any normal message first, such as hi → then ask to open the same widget → tap fullscreen button |
Fullscreen works |
So this does not seem to be a general fullscreen issue, because fullscreen works on Android after the conversation already has at least one previous message.
I also noticed the same pattern in another Apps SDK app I’m testing on Android:
| Scenario | Result |
|---|---|
| New conversation → first message asks ChatGPT to generate an image and then open the app/widget | Fullscreen fails / opens blank |
New conversation → send hi first → then ask ChatGPT to generate the image and open the app/widget |
Fullscreen works |
This makes me suspect a cold-start / first-turn initialization issue in the ChatGPT Android host. The inline widget/app can render, but the fullscreen surface seems not to render correctly when the app/widget is first invoked on the first turn of a brand-new conversation.
I will attach an Android screen recording showing both cases:
- Failing case: new conversation, first message invokes the widget/app, fullscreen opens blank.
- Working case: new conversation, send
hifirst, then invoke the same widget/app, fullscreen works.
Here is the minimal repro code I used for the simple hello world widget:
import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
const PORT = Number(process.env.PORT ?? 3000);
function toOrigin(value) {
if (!value) return undefined;
try {
return new URL(value).origin;
} catch {
return undefined;
}
}
const ORIGIN = toOrigin(process.env.BASE);
const TEMPLATE_URI = "ui://widget/widget.html";
const widgetHtml = `
<!doctype html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
</head>
<body>
<button id="open">Abrir fullscreen</button>
<script>
document.getElementById("open").onclick = async () => {
try {
await window.openai?.requestDisplayMode?.({ mode: "fullscreen" });
} catch (_) {}
document.body.innerHTML =
'<main style="height:100vh;display:grid;place-items:center;font:32px system-ui;">hello world</main>';
};
</script>
</body>
</html>
`.trim();
function createServer() {
const server = new McpServer({
name: "hello-world-widget",
version: "1.0.0",
});
registerAppResource(
server,
"hello-world-widget",
TEMPLATE_URI,
{},
async () => ({
contents: [
{
uri: TEMPLATE_URI,
mimeType: RESOURCE_MIME_TYPE,
text: widgetHtml,
_meta: {
ui: {
...(ORIGIN ? { domain: ORIGIN } : {}),
csp: {
connectDomains: [],
resourceDomains: [],
},
},
},
},
],
}),
);
registerAppTool(
server,
"hello_world_widget",
{
title: "Hello World Widget",
description: "Abre um widget simples com um botão que mostra hello world em fullscreen.",
inputSchema: {},
outputSchema: {},
_meta: {
ui: {
resourceUri: TEMPLATE_URI,
},
"openai/outputTemplate": TEMPLATE_URI,
},
annotations: {
readOnlyHint: true,
destructiveHint: false,
openWorldHint: false,
},
},
async () => ({
structuredContent: {},
content: [
{
type: "text",
text: "Widget aberto.",
},
],
}),
);
return server;
}
const app = createMcpExpressApp({ host: "0.0.0.0" });
app.all("/mcp", async (req, res) => {
const server = createServer();
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
res.on("close", () => {
transport.close().catch(() => {});
server.close().catch(() => {});
});
try {
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
} catch (error) {
console.error("MCP error:", error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: "2.0",
error: {
code: -32603,
message: "Internal server error",
},
id: null,
});
}
}
});
app.listen(PORT, () => {
console.log(`MCP server running - PORT: ${PORT} - ORIGIN: ${ORIGIN}`);
});
To run this code:
BASE=https://<ngrok-hash>.ngrok-free.app node index.mjs
Things I already tried:
- Adding
_meta.ui.domainwith an HTTPS origin. - Testing different CSP configurations.
- Adding legacy metadata aliases such as
openai/widgetDomainandopenai/widgetCSP. - Cache-busting the template URI, for example changing
ui://widget/widget.htmlto versioned names. - Returning the same HTML from a real HTTP route and calling
window.openai.setOpenInAppUrl(...). - Persisting state with
window.openai.setWidgetState(...)before requesting fullscreen. - Rendering the fullscreen content before and after
requestDisplayMode. - Testing
requestModal()only as a diagnostic step. Modal is not a valid solution for my use case because I specifically need fullscreen.
The important finding is that the same widget/app can work on Android if the conversation already has a previous message. The failure appears when the app/widget is invoked on the first turn of a new conversation.
My questions:
- Is fullscreen expected to work on Android when an Apps SDK widget/app is invoked in the first message of a brand-new conversation?
- Is there any additional initialization step required before calling
requestDisplayMode({ mode: "fullscreen" })on Android? - Could this be a known Android host cold-start / first-turn issue?
- Is there any difference in fullscreen behavior between widgets invoked after a normal assistant turn and widgets invoked immediately in the first assistant response?
Environment:
- ChatGPT Android app version:
1.2026.181 (1) - Android device:
Galaxy S23 Ultra - SM-S918B - Android OS version:
16 - MCP package versions:
@modelcontextprotocol/sdk:^1.29.0@modelcontextprotocol/ext-apps:^1.7.4
- Node.js version:
v22.23.1 - Hosting/tunnel provider:
ngrok - App mode:
- Developer mode connector:
yes - Published app:
no
- Developer mode connector:
Any guidance would be appreciated. I’m trying to keep this repro intentionally minimal so the issue is easier to isolate.

