Background
Echo.tsx manually wires App in a useEffect but the cleanup never calls activeApp.close(), leaving the PostMessageTransport listener alive on unmount. Under React StrictMode's dev-mode double-invoke, the first App instance becomes a zombie listener receiving every host message alongside the live one.
return () => {
isMounted = false; // only guards callbacks, doesn't close the transport
};
This is the same bug that @modelcontextprotocol/ext-apps 1.7.0 fixed inside the useApp() hook (#631).
Proposed fix
Refactor Echo.tsx to use the useApp() hook from @modelcontextprotocol/ext-apps/react, which handles the connect/close lifecycle correctly and is a better reference implementation for template users. As part of this, update CLAUDE.md / AGENTS.md and skill docs to recommend useApp() as the preferred pattern over manual useEffect wiring.
Alternatively, keep the manual pattern but add defaultApp.close() to the cleanup (only for the internally-owned instance, not the injected app prop used in tests/Storybook).
Background
Echo.tsxmanually wiresAppin auseEffectbut the cleanup never callsactiveApp.close(), leaving thePostMessageTransportlistener alive on unmount. Under React StrictMode's dev-mode double-invoke, the firstAppinstance becomes a zombie listener receiving every host message alongside the live one.This is the same bug that
@modelcontextprotocol/ext-apps1.7.0 fixed inside theuseApp()hook (#631).Proposed fix
Refactor
Echo.tsxto use theuseApp()hook from@modelcontextprotocol/ext-apps/react, which handles the connect/close lifecycle correctly and is a better reference implementation for template users. As part of this, updateCLAUDE.md/AGENTS.mdand skill docs to recommenduseApp()as the preferred pattern over manualuseEffectwiring.Alternatively, keep the manual pattern but add
defaultApp.close()to the cleanup (only for the internally-owned instance, not the injectedappprop used in tests/Storybook).