Background
@modelcontextprotocol/ext-apps 1.7.0 introduced the useApp() React hook and fixed a React StrictMode bug where the App instance was not closed on unmount, leaving a zombie PostMessageTransport listener receiving every host message alongside the live instance.
The fix was applied inside useApp() — it now calls app.close() in the effect cleanup.
Current state
Echo.tsx manually wires App in a useEffect:
useEffect(() => {
let isMounted = true;
activeApp.ontoolresult = ...
activeApp.onhostcontextchanged = ...
const connect = async () => { await activeApp.connect(); ... };
connect();
return () => {
isMounted = false; // only guards callbacks, doesn't close the transport
};
}, [activeApp]);
The cleanup never calls activeApp.close(), so under React StrictMode's dev-mode double-invoke the first App instance becomes a zombie listener.
Proposed change
Either:
- Keep the manual
useEffect pattern but add activeApp.close() to the cleanup (only when activeApp is the internally-created defaultApp, not the injected app prop used in tests/Storybook), or
- Refactor
Echo.tsx to use the useApp() hook from @modelcontextprotocol/ext-apps/react, which handles the connect/close lifecycle correctly and would serve as a better reference implementation for template users.
Also update CLAUDE.md / AGENTS.md and skill docs to document the correct cleanup pattern (or useApp()) as the preferred approach.
References
- ext-apps 1.7.0 release notes:
useApp effect cleanup now closes the App, so React StrictMode's dev double-invoke doesn't leave a zombie PostMessageTransport listener (modelcontextprotocol/ext-apps#631)
Background
@modelcontextprotocol/ext-apps1.7.0 introduced theuseApp()React hook and fixed a React StrictMode bug where theAppinstance was not closed on unmount, leaving a zombiePostMessageTransportlistener receiving every host message alongside the live instance.The fix was applied inside
useApp()— it now callsapp.close()in the effect cleanup.Current state
Echo.tsxmanually wiresAppin auseEffect:The cleanup never calls
activeApp.close(), so under React StrictMode's dev-mode double-invoke the firstAppinstance becomes a zombie listener.Proposed change
Either:
useEffectpattern but addactiveApp.close()to the cleanup (only whenactiveAppis the internally-createddefaultApp, not the injectedappprop used in tests/Storybook), orEcho.tsxto use theuseApp()hook from@modelcontextprotocol/ext-apps/react, which handles the connect/close lifecycle correctly and would serve as a better reference implementation for template users.Also update
CLAUDE.md/AGENTS.mdand skill docs to document the correct cleanup pattern (oruseApp()) as the preferred approach.References
useAppeffect cleanup now closes theApp, so React StrictMode's dev double-invoke doesn't leave a zombiePostMessageTransportlistener (modelcontextprotocol/ext-apps#631)