Skip to content

Add double-connect guard to prevent protocol message handling errors#450

Open
ochafik wants to merge 2 commits intomainfrom
claude/investigate-issue-429-JH9Gl
Open

Add double-connect guard to prevent protocol message handling errors#450
ochafik wants to merge 2 commits intomainfrom
claude/investigate-issue-429-JH9Gl

Conversation

@ochafik
Copy link
Contributor

@ochafik ochafik commented Feb 12, 2026

Summary

This PR adds validation to prevent calling connect() multiple times on the same AppBridge or App instance without first calling close(). This could prevents a subtle bug where an overwritten onmessage handler wouldn't know about earlier requests, leading to "unknown message ID" errors.

This could be what's happening in #429 but worth adding regardless.

Key Changes

  • AppBridge.connect(): Added check to throw "AppBridge is already connected" error if connect() is called when already connected
  • App.connect(): Added check to throw "App is already connected" error if connect() is called when already connected
  • Test coverage: Added comprehensive test suite covering:
    • Double-connect attempts on both AppBridge and App
    • Regression test demonstrating the original bug scenario (message processing twice)
    • Verification that reconnection works correctly after close()

Implementation Details

The guard checks if this.transport is already set before allowing a new connection. This is a simple but effective prevention mechanism that forces users to explicitly call close() before reconnecting, ensuring clean state management and preventing the message handler chaining issue that would otherwise occur.

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
@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 12, 2026

Open in StackBlitz

@modelcontextprotocol/ext-apps

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/ext-apps@450

@modelcontextprotocol/server-basic-react

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-basic-react@450

@modelcontextprotocol/server-basic-vanillajs

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-basic-vanillajs@450

@modelcontextprotocol/server-budget-allocator

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-budget-allocator@450

@modelcontextprotocol/server-cohort-heatmap

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-cohort-heatmap@450

@modelcontextprotocol/server-customer-segmentation

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-customer-segmentation@450

@modelcontextprotocol/server-map

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-map@450

@modelcontextprotocol/server-pdf

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-pdf@450

@modelcontextprotocol/server-scenario-modeler

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-scenario-modeler@450

@modelcontextprotocol/server-shadertoy

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-shadertoy@450

@modelcontextprotocol/server-sheet-music

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-sheet-music@450

@modelcontextprotocol/server-system-monitor

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-system-monitor@450

@modelcontextprotocol/server-threejs

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-threejs@450

@modelcontextprotocol/server-transcript

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-transcript@450

@modelcontextprotocol/server-video-resource

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-video-resource@450

@modelcontextprotocol/server-wiki-explorer

npm i https://pkg.pr.new/modelcontextprotocol/ext-apps/@modelcontextprotocol/server-wiki-explorer@450

commit: 002a135

@ochafik ochafik marked this pull request as ready for review February 12, 2026 20:38
@ochafik ochafik mentioned this pull request Feb 12, 2026
Comment on lines 642 to 671
// 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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand what this test is doing... newBridge and newApp use newBridgeTransport and newAppTransport. Should they use bridgeTransport and appTransport instead?

…uard check

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants