From 358cd40373f265e22abf5d04848647587c7c6f54 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Feb 2026 18:22:24 +0000 Subject: [PATCH 1/2] fix: guard against double-connect in App and AppBridge (#429) Calling connect() twice on the same transport instance caused the MCP SDK's Protocol class to chain onmessage handlers, so each incoming message was processed twice. This made incoming responses get consumed by the first processing (resolving the handler) and then trigger a spurious "Received a response for an unknown message ID" error on the second processing pass, since the handler was already removed from the response map. Add an explicit guard in both App.connect() and AppBridge.connect() that throws if the instance is already connected. This converts the silent message-corruption into a clear, actionable error and prevents downstream confusion. Adds three regression tests: - AppBridge.connect() throws when already connected - App.connect() throws when already connected - Demonstrates the double-processing root cause and verifies it is fixed Fixes #429 https://claude.ai/code/session_01XU3FW1TpjBkyRbkBUH5gqR --- src/app-bridge.test.ts | 67 ++++++++++++++++++++++++++++++++++++++++++ src/app-bridge.ts | 5 ++++ src/app.ts | 5 ++++ 3 files changed, 77 insertions(+) diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index 875c10e49..0f9022bd4 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -615,6 +615,73 @@ describe("App <-> AppBridge integration", () => { }); }); + describe("double-connect guard", () => { + it("AppBridge.connect() throws if already connected", async () => { + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + + // Attempting to connect again with a different transport should throw + const [, secondBridgeTransport] = InMemoryTransport.createLinkedPair(); + await expect(bridge.connect(secondBridgeTransport)).rejects.toThrow( + "AppBridge is already connected", + ); + }); + + it("App.connect() throws if already connected", async () => { + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + + // Attempting to connect again should throw + const [secondAppTransport] = InMemoryTransport.createLinkedPair(); + await expect(app.connect(secondAppTransport)).rejects.toThrow( + "App is already connected", + ); + }); + + it("double-connect on same transport would have caused unknown message ID errors", async () => { + // Regression test: before the double-connect guard was added, calling + // connect() twice on the same transport caused the MCP Protocol to chain + // onmessage handlers, processing each incoming message twice. This caused + // the second processing of a response to fail with "unknown message ID" + // because the first processing already consumed the response handler. + // + // This test verifies that the guard prevents this scenario entirely. + bridge.onupdatemodelcontext = async () => ({}); + + await bridge.connect(bridgeTransport); + await app.connect(appTransport); + + // After close(), reconnection should be allowed + await bridgeTransport.close(); + const [newAppTransport, newBridgeTransport] = + InMemoryTransport.createLinkedPair(); + const newBridge = new AppBridge( + createMockClient() as Client, + testHostInfo, + testHostCapabilities, + ); + const newApp = new App(testAppInfo, {}, { autoResize: false }); + + const errors: Error[] = []; + newBridge.onerror = (e) => errors.push(e); + newApp.onerror = (e) => errors.push(e); + newBridge.onupdatemodelcontext = async () => ({}); + + await newBridge.connect(newBridgeTransport); + await newApp.connect(newAppTransport); + + // This request (id=1) should be processed exactly once + await newApp.updateModelContext({ + content: [{ type: "text", text: "test" }], + }); + + expect(errors).toHaveLength(0); + + await newAppTransport.close(); + await newBridgeTransport.close(); + }); + }); + describe("ping", () => { it("App responds to ping from bridge", async () => { await bridge.connect(bridgeTransport); diff --git a/src/app-bridge.ts b/src/app-bridge.ts index 3322a0dda..6e1c02172 100644 --- a/src/app-bridge.ts +++ b/src/app-bridge.ts @@ -1409,6 +1409,11 @@ export class AppBridge extends Protocol< * ``` */ async connect(transport: Transport) { + if (this.transport) { + throw new Error( + "AppBridge is already connected. Call close() before connecting again.", + ); + } if (this._client) { // When a client was passed to the constructor, automatically forward // MCP requests/notifications between the view and the server diff --git a/src/app.ts b/src/app.ts index f813f44c8..4613e8f13 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1112,6 +1112,11 @@ export class App extends Protocol { ), options?: RequestOptions, ): Promise { + if (this.transport) { + throw new Error( + "App is already connected. Call close() before connecting again.", + ); + } await super.connect(transport); try { From 002a1354ad1dfcfa26af6cef08e5b6416d509d33 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 13 Feb 2026 19:06:28 +0000 Subject: [PATCH 2/2] test: replace redundant regression test with cleaner same-transport guard check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous third test in "double-connect guard" never actually attempted a double-connect — it created fresh instances and verified normal operation, which every other test already covers. The stray bridge.onupdatemodelcontext and bridgeTransport.close() at the top were leftover noise. Replace it with a focused test that verifies the guard fires even when the caller passes the *same* transport object (not just a different one), which is the exact scenario described in #429. https://claude.ai/code/session_01XU3FW1TpjBkyRbkBUH5gqR --- src/app-bridge.test.ts | 41 ++++------------------------------------- 1 file changed, 4 insertions(+), 37 deletions(-) diff --git a/src/app-bridge.test.ts b/src/app-bridge.test.ts index 0f9022bd4..a2ace3c20 100644 --- a/src/app-bridge.test.ts +++ b/src/app-bridge.test.ts @@ -638,47 +638,14 @@ describe("App <-> AppBridge integration", () => { ); }); - it("double-connect on same transport would have caused unknown message ID errors", async () => { - // Regression test: before the double-connect guard was added, calling - // connect() twice on the same transport caused the MCP Protocol to chain - // onmessage handlers, processing each incoming message twice. This caused - // the second processing of a response to fail with "unknown message ID" - // because the first processing already consumed the response handler. - // - // This test verifies that the guard prevents this scenario entirely. - bridge.onupdatemodelcontext = async () => ({}); - + it("AppBridge.connect() throws even when called with the same transport", async () => { await bridge.connect(bridgeTransport); await app.connect(appTransport); - // After close(), reconnection should be allowed - await bridgeTransport.close(); - const [newAppTransport, newBridgeTransport] = - InMemoryTransport.createLinkedPair(); - const newBridge = new AppBridge( - createMockClient() as Client, - testHostInfo, - testHostCapabilities, + // Should throw regardless of whether it's the same or a different transport + await expect(bridge.connect(bridgeTransport)).rejects.toThrow( + "AppBridge is already connected", ); - const newApp = new App(testAppInfo, {}, { autoResize: false }); - - const errors: Error[] = []; - newBridge.onerror = (e) => errors.push(e); - newApp.onerror = (e) => errors.push(e); - newBridge.onupdatemodelcontext = async () => ({}); - - await newBridge.connect(newBridgeTransport); - await newApp.connect(newAppTransport); - - // This request (id=1) should be processed exactly once - await newApp.updateModelContext({ - content: [{ type: "text", text: "test" }], - }); - - expect(errors).toHaveLength(0); - - await newAppTransport.close(); - await newBridgeTransport.close(); }); });