Android ChatGPT blocks Apps SDK widget app destructive tool before MCP, while web/iOS show confirmation modal and work
Hi Mark, here are the details.
ChatGPT Android app version
Google Pixel 7, 1.2026.125
Google Pixel 6a, 1.2026.125
Android versions/devices tested
Google Pixel 7, Android 17 build CP31.260423.012.A1
Google Pixel 6a, Android 16 build BP41.250822.010
Whether the native confirmation modal appears at all on Android
For my Isocarto app, no: the native confirmation modal does not appear on Android for the affected tools.
As a comparison, I tested the Dropbox ChatGPT app on the same Android device. Dropbox does show the native confirmation modal correctly. One difference is that Dropbox appears to be a shipped app without my widget flow, while Isocarto is an Apps SDK app with a widget.
Behavior after approval / failure
Since the confirmation modal does not appear for Isocarto on Android, there is no approval step.
Instead, the assistant returns an error before the MCP server receives the tool call. For example, I sometimes get:
“I couldn’t execute debug_write_confirmation_probe: the call was blocked by OpenAI safety controls.”
For delete_zone specifically, the Android failure produces no MCP log at all, which suggests the call is blocked client-side / OpenAI-side before reaching my MCP endpoint.
Full delete_zone tool descriptor + annotations
Here is the tool code:
import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
import { z } from "zod";
import { fetchClientApi } from "../../helpers/apiClient.js";
import { requireAuth } from "../../middleware/auth.js";
export function registerDeleteZoneTool(server) {
registerAppTool(
server,
"delete_zone",
{
title: "Supprimer une zone",
description: "Supprime une zone de chalandise d'une carte Isocarto.",
inputSchema: {
mapId: z.string(),
zoneId: z.string(),
},
annotations: { readOnlyHint: false, openWorldHint: false, destructiveHint: true },
_meta: {},
},
requireAuth(async (args) => {
try {
const { mapId, zoneId } = args;
if (!mapId || !zoneId) throw new Error("mapId et zoneId sont requis.");
await fetchClientApi(`/zones/delete-zone/${zoneId}`, null, {
bearerToken: args._mcpAccessToken,
method: "DELETE",
body: { mapId },
});
const remainingZonesResult = await fetchClientApi("/zones/get-zones", null, {
bearerToken: args._mcpAccessToken,
params: { mapId },
}).catch(() => ({ zones: [] }));
const remainingZones = remainingZonesResult?.zones || remainingZonesResult || [];
return {
content: [{ type: "text", text: "🗑️ Zone supprimée avec succès." }],
structuredContent: {
type: "zone_deleted",
mapId,
zoneId,
remainingZones,
remainingCount: remainingZones.length,
},
};
} catch (err) {
return {
isError: true,
content: [{ type: "text", text: `❌ Erreur : ${err.message}` }],
structuredContent: { error: err.message },
};
}
}),
);
}
Invocation mode
The delete_zone tool is assistant-invoked from the conversation. It is not called directly from the widget via window.openai.callTool in this repro.
The app does have a widget for map visualization, but the failing delete_zone call is triggered by the assistant/tool orchestration, not by a direct widget button calling window.openai.callTool.
Successful web/desktop MCP log
On web/desktop, the native confirmation modal appears and the tool reaches the MCP server successfully:
1|isocarto-mcp | 2026-05-15 16:57:18: 🧭 MCP tool called {
1|isocarto-mcp | 2026-05-15 16:57:18: toolName: 'delete_zone',
1|isocarto-mcp | 2026-05-15 16:57:18: userId: '252e8aad-64a1-486d-89a4-6ec06f6b8ab0',
1|isocarto-mcp | 2026-05-15 16:57:18: organizationId: '80075eaa-1314-4097-bf55-04e31ebbfb47',
1|isocarto-mcp | 2026-05-15 16:57:18: clientId: 'nrPRsqWupqcUMVEwpuwcapaRuYPllDym',
1|isocarto-mcp | 2026-05-15 16:57:18: sessionId: null,
1|isocarto-mcp | 2026-05-15 16:57:18: tokenMode: 'jwt',
1|isocarto-mcp | 2026-05-15 16:57:18: args: {
1|isocarto-mcp | 2026-05-15 16:57:18: mapId: '7d4b0b82-3941-4621-96f6-03dafeba59f0',
1|isocarto-mcp | 2026-05-15 16:57:18: zoneId: 'bfd86904-cfaf-4552-9476-3f1a8e3e34f7',
1|isocarto-mcp | 2026-05-15 16:57:18: hasGeometry: false
1|isocarto-mcp | 2026-05-15 16:57:18: }
1|isocarto-mcp | 2026-05-15 16:57:18: }
186|isocarto | 2026-05-15 16:57:18: [USER] Suppression d'une zone
1|isocarto-mcp | 2026-05-15 16:57:18: ✅ MCP tool result {
1|isocarto-mcp | 2026-05-15 16:57:18: toolName: 'delete_zone',
1|isocarto-mcp | 2026-05-15 16:57:18: durationMs: 163,
1|isocarto-mcp | 2026-05-15 16:57:18: success: true,
1|isocarto-mcp | 2026-05-15 16:57:18: isError: false,
1|isocarto-mcp | 2026-05-15 16:57:18: type: 'zone_deleted',
1|isocarto-mcp | 2026-05-15 16:57:18: error: null,
1|isocarto-mcp | 2026-05-15 16:57:18: mapId: '7d4b0b82-3941-4621-96f6-03dafeba59f0',
1|isocarto-mcp | 2026-05-15 16:57:18: zoneId: 'bfd86904-cfaf-4552-9476-3f1a8e3e34f7'
1|isocarto-mcp | 2026-05-15 16:57:18: }
Failed Android attempt
On Android, there is no MCP log for the failed delete_zone attempt. The tool is not called at all server-side.
Here is a screen (of another tool but the error is the same)
Minimal destructive test tool
I also tested a minimal harmless destructive test tool with similar annotations. It only logs/persists a debug label and does not perform a real deletion. That test also fails on Android in the same way: sometimes blocked by OpenAI safety controls before reaching the MCP server.
Summary
The issue seems specific to ChatGPT Android handling of Apps SDK confirmation/safety flow for my widget app. Web and iOS work. Android can still run some non-confirmation tools, but tools requiring the native confirmation modal, especially destructive tools like delete_zone, fail before reaching MCP.
Discussion in the ATmosphere