npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

seahorse-agent-bridge

v1.2.0

Published

JavaScript SDK for integrating Seahorse MCP Agent into parent applications

Readme

Seahorse Agent Bridge SDK

JavaScript/TypeScript SDK for integrating Seahorse MCP Agent into parent web applications.

Features

  • Tool Registration: Register custom tools that the agent can call via MCP-like protocol
  • Message Injection: Send messages to the agent's input area programmatically
  • Window Management: Open/close agent in a new window
  • Connection State Management: Real-time connection monitoring (disconnected, connecting, connected, reconnecting)
  • Automatic Session Recovery: Survive parent window refresh without losing agent connection
  • Heartbeat & Reconnection: Automatic health checks and reconnection on connection loss
  • Bidirectional Communication: PostMessage + WebSocket-based secure communication
  • TypeScript Support: Full type definitions included
  • React-Friendly: Callback-based API integrates seamlessly with React hooks

Architecture Overview

The Seahorse Agent Bridge enables bidirectional communication between a parent web application and the Seahorse Agent through two distinct channels:

1. Message Injection (Parent → Agent)

Simple PostMessage-based communication for sending messages to the agent's input area.

2. Tool Calls (Agent ↔ Parent)

WebSocket + PostMessage hybrid architecture for executing parent-provided tools during agent's reasoning process.

graph TB
    subgraph "Parent Window"
        PB[SeahorseAgentBridge SDK]
        TH[Tool Handlers]
    end

    subgraph "Agent Window"
        AI[InputArea Component]
        PWC[ParentWindowMCPClient]
        PWS[ParentWindowWebSocket]
    end

    subgraph "Agent Backend"
        WS[WebSocket Server]
        PP[ParentWindowProxy]
        AG[SeahorseAgent]
        LLM[LLM]
    end

    PB -->|"1. inject_message<br/>(PostMessage)"| PWC
    PWC -->|"2. CustomEvent"| AI

    LLM -->|"3. Tool Call Decision"| AG
    AG -->|"4. call_tool()"| PP
    PP -->|"5. WebSocket Request"| WS
    WS -->|"6. tool_call message"| PWS
    PWS -->|"7. PostMessage<br/>tool_call"| PWC
    PWC -->|"8. PostMessage<br/>tool_call"| PB
    PB -->|"9. Execute"| TH
    TH -->|"10. Result"| PB
    PB -->|"11. PostMessage<br/>tool_response"| PWC
    PWC -->|"12. PostMessage<br/>tool_response"| PWS
    PWS -->|"13. WebSocket Response"| WS
    WS -->|"14. MCPToolCallResult"| PP
    PP -->|"15. Result"| AG
    AG -->|"16. Continue"| LLM

Detailed Communication Flow

Flow 1: Message Injection (Parent → Agent InputArea)

This is a simple one-way communication for injecting messages into the agent's input area.

sequenceDiagram
    participant Parent as Parent Window<br/>(SeahorseAgentBridge)
    participant Agent as Agent Window<br/>(ParentWindowMCPClient)
    participant Input as InputArea<br/>Component

    Parent->>Agent: PostMessage<br/>{type: "inject_message", payload: {message, autoSend}}
    Note over Agent: Origin validation
    Agent->>Input: CustomEvent<br/>("parent-inject-message")
    Input->>Input: Set input value
    alt autoSend = true
        Input->>Input: Auto-submit message
    end

Implementation Details:

  1. Parent Window (SeahorseAgentBridge.injectMessage()):

    injectMessage(message: string, options?: { autoSend?: boolean }) {
      this.agentWindow.postMessage({
        type: 'inject_message',
        payload: { message, autoSend: options?.autoSend }
      }, this.agentOrigin);
    }
  2. Agent Frontend (ParentWindowMCPClient.handleInjectMessage()):

    private handleInjectMessage(payload: { message: string; autoSend?: boolean }) {
      window.dispatchEvent(new CustomEvent('parent-inject-message', {
        detail: { message, autoSend }
      }));
    }
  3. InputArea Component (Event Listener):

    useEffect(() => {
      const handleParentMessage = (event: CustomEvent) => {
        const { message, autoSend } = event.detail;
        setInputValue(message);
        if (autoSend) {
          handleSendMessage();
        }
      };
      window.addEventListener('parent-inject-message', handleParentMessage);
    }, []);

Flow 2: Tool Calls (Agent ↔ Parent via WebSocket + PostMessage)

This is a complex bidirectional flow that enables the agent's LLM to call tools provided by the parent window.

sequenceDiagram
    participant Parent as Parent Window<br/>(Bridge SDK)
    participant PWC as Agent Frontend<br/>(ParentWindowMCPClient)
    participant PWS as Agent Frontend<br/>(ParentWindowWebSocket)
    participant WS as Agent Backend<br/>(WebSocket Server)
    participant Proxy as Agent Backend<br/>(ParentWindowProxy)
    participant Agent as Agent Backend<br/>(SeahorseAgent)
    participant LLM as LLM

    Note over Parent,LLM: 1. Tool Registration Phase
    Parent->>Parent: registerTool({name, description, parameters})
    Parent->>Parent: onToolCall(name, handler)
    Parent->>PWC: PostMessage<br/>{type: "tools_list", payload: {tools}}
    PWC->>Proxy: POST /api/parent-window/tools<br/>{tools}
    Proxy->>Proxy: update_tools()

    Note over Parent,LLM: 2. Session Creation & WebSocket Connection
    PWC->>WS: WebSocket Connect<br/>/ws/parent-window/{session_id}
    WS->>WS: connection_manager.connect(session_id, ws)
    WS-->>PWC: WebSocket Connected

    Note over Parent,LLM: 3. Agent Query Processing
    Agent->>Agent: User sends query
    Agent->>LLM: System Prompt + Available Tools<br/>(including parent-window tools)
    LLM->>LLM: Reasoning: Need to call<br/>parent-window_get_sales_data

    Note over Parent,LLM: 4. Tool Execution Request
    LLM-->>Agent: Tool Call Decision<br/>{name: "parent-window_get_sales_data", args: {days: 30}}
    Agent->>Proxy: call_tool(server="parent-window",<br/>tool_name="get_sales_data", args, session_id)
    Proxy->>WS: connection_manager.call_tool()<br/>with request_id

    Note over Parent,LLM: 5. WebSocket → PostMessage Bridge
    WS->>PWS: WebSocket Message<br/>{type: "tool_call", request_id, tool_name, arguments}
    PWS->>PWC: parentWindowMCPClient.callTool(tool_name, args)
    PWC->>Parent: window.opener.postMessage()<br/>{type: "tool_call", call_id, tool_name, arguments}

    Note over Parent,LLM: 6. Parent Tool Execution
    Parent->>Parent: Validate origin
    Parent->>Parent: Find tool handler
    Parent->>Parent: Execute handler(arguments)
    Parent->>Parent: Get result

    Note over Parent,LLM: 7. Response Flow Back
    Parent->>PWC: window.postMessage()<br/>{type: "tool_response", call_id, result, success}
    PWC->>PWS: Resolve Promise with result
    PWS->>WS: WebSocket Message<br/>{type: "tool_response", request_id, response}
    WS->>Proxy: Resolve Future with response
    Proxy-->>Agent: MCPToolCallResult{success, content}
    Agent->>LLM: Tool result:<br/>{totalSales: $125k, period: "30 days"}
    LLM->>LLM: Continue reasoning with result
    LLM-->>Agent: Final response to user

Implementation Details:

Backend Components

  1. WebSocket Server (app/routers/parent_window_websocket.py):

    class ParentWindowConnectionManager:
        active_connections: Dict[str, WebSocket]  # session_id -> WebSocket
        pending_requests: Dict[str, asyncio.Future]  # request_id -> Future
    
        async def call_tool(self, session_id: str, tool_name: str,
                           arguments: Dict, timeout: float = 30.0) -> Dict:
            request_id = str(uuid.uuid4())
            future = asyncio.Future()
            self.pending_requests[request_id] = future
    
            # Send via WebSocket
            await websocket.send_json({
                "type": "tool_call",
                "request_id": request_id,
                "tool_name": tool_name,
                "arguments": arguments
            })
    
            # Wait for response (blocking for agent)
            response = await asyncio.wait_for(future, timeout=timeout)
            return response
  2. ParentWindowProxy (app/mcp/parent_window_proxy.py):

    async def call_tool(self, server: str, tool_name: str,
                       arguments: Dict, session_id: str = None) -> MCPToolCallResult:
        from ..routers.parent_window_websocket import connection_manager
    
        # Actually execute tool via WebSocket (synchronous-looking async)
        response = await connection_manager.call_tool(
            session_id=session_id,
            tool_name=tool_name,
            arguments=arguments,
            timeout=30.0
        )
    
        if response.get("success"):
            return MCPToolCallResult(success=True, content=response.get("result"))
        else:
            return MCPToolCallResult(success=False, error=response.get("error"))
  3. SeahorseAgent (app/agent.py):

    • Collects parent window tools in available_tools
    • Routes parent-window_* tool calls to ParentWindowProxy
    • Passes session_id for WebSocket routing

Frontend Components

  1. ParentWindowWebSocket (frontend/src/services/ParentWindowWebSocket.ts):

    class ParentWindowWebSocketClient {
        private ws: WebSocket | null = null;
        private sessionId: string | null = null;
    
        async connect(sessionId: string): Promise<void> {
            const wsUrl = `${protocol}//${host}/ws/parent-window/${sessionId}`;
            this.ws = new WebSocket(wsUrl);
    
            this.ws.onmessage = (event) => {
                const message = JSON.parse(event.data);
                if (message.type === 'tool_call') {
                    this.handleToolCall(message);
                }
            };
        }
    
        private async handleToolCall(request: ToolCallRequest): Promise<void> {
            // Call parent via PostMessage
            const result = await parentWindowMCPClient.callTool(
                request.tool_name,
                request.arguments
            );
    
            // Send response back via WebSocket
            this.send({
                type: 'tool_response',
                request_id: request.request_id,
                response: { success: true, result }
            });
        }
    }
  2. ParentWindowMCPClient (frontend/src/services/ParentWindowMCPClient.ts):

    async callTool(toolName: string, args: any): Promise<any> {
        return new Promise((resolve, reject) => {
            const callId = `call_${Date.now()}_${Math.random()}`;
    
            // Timeout
            const timeoutId = setTimeout(() => {
                reject(new Error(`Timeout: ${toolName}`));
            }, this.timeoutMs);
    
            // Register pending call
            this.pendingCalls.set(callId, { resolve, reject, timeoutId });
    
            // PostMessage to parent
            window.opener.postMessage({
                type: 'tool_call',
                payload: { call_id: callId, tool_name: toolName, arguments: args }
            }, origin);
        });
    }
    
    private handleToolResponse(payload: {call_id, result, success}) {
        const pending = this.pendingCalls.get(call_id);
        clearTimeout(pending.timeoutId);
    
        if (success) {
            pending.resolve(result);  // Resolves Promise in callTool()
        } else {
            pending.reject(new Error(payload.error));
        }
    }
  3. App.tsx (WebSocket Lifecycle):

    useEffect(() => {
        if (currentSession?.session_id) {
            parentWindowWebSocket.connect(currentSession.session_id);
    
            return () => {
                parentWindowWebSocket.disconnect();
            };
        }
    }, [currentSession?.session_id]);

Parent Window SDK

SeahorseAgentBridge (sdk/seahorse-agent-bridge/src/index.ts):

class SeahorseAgentBridge {
    private tools: ToolDefinition[] = [];
    private toolHandlers: Map<string, ToolCallHandler> = new Map();

    registerTool(tool: ToolDefinition) {
        this.tools.push(tool);
        this.sendToolsToAgent();
    }

    onToolCall(toolName: string, handler: ToolCallHandler) {
        this.toolHandlers.set(toolName, handler);
    }

    private handleToolCall(payload: {call_id, tool_name, arguments}) {
        const handler = this.toolHandlers.get(payload.tool_name);

        try {
            const result = await handler(payload.arguments);

            // Send response back
            this.agentWindow.postMessage({
                type: 'tool_response',
                payload: {
                    call_id: payload.call_id,
                    result: result,
                    success: true
                }
            }, this.agentOrigin);
        } catch (error) {
            this.agentWindow.postMessage({
                type: 'tool_response',
                payload: {
                    call_id: payload.call_id,
                    error: error.message,
                    success: false
                }
            }, this.agentOrigin);
        }
    }
}

Key Design Decisions

  1. Why WebSocket + PostMessage Hybrid?

    • WebSocket: Backend-to-Frontend real-time communication (agent-initiated tool calls)
    • PostMessage: Cross-origin communication between windows (secure parent-child communication)
    • Combination enables bidirectional flow while maintaining security boundaries
  2. Why Session-Based WebSocket Routing?

    • Multiple agent frontends can connect to one backend
    • Each WebSocket connection is mapped to a session_id
    • Enables proper request routing in multi-user scenarios
  3. Why asyncio.Future for Synchronous-Looking Async?

    • Agent's tool execution needs to block until parent responds
    • Future allows WebSocket response to resolve the waiting call_tool()
    • Maintains clean sequential code flow in agent logic
  4. Why request_id and call_id Matching?

    • request_id: Backend WebSocket request/response matching
    • call_id: Frontend PostMessage request/response matching
    • Enables concurrent tool calls without response mix-up

Installation

npm install seahorse-agent-bridge

Quick Start

Basic Usage (Vanilla JavaScript/TypeScript)

import { SeahorseAgentBridge } from 'seahorse-agent-bridge';

// Initialize bridge with connection management
const bridge = new SeahorseAgentBridge({
  agentOrigin: 'https://seahorse-agent.agent.seahorse.dnotitia.com',
  autoRecover: true, // Enable automatic session recovery (default: true)

  // Optional: Connection state callbacks
  onConnectionChange: (state, previousState) => {
    console.log(`Connection: ${previousState} → ${state}`);
  },

  onWindowClosed: () => {
    console.log('Agent window closed by user');
  },

  onSessionRecovered: (window) => {
    console.log('Session recovered after page refresh!');
  }
});

// Register a tool
bridge.registerTools([{
  name: 'get_user_info',
  description: 'Get current user information',
  parameters: {
    type: 'object',
    properties: {},
    required: []
  }
}]);

// Set tool handler
bridge.onToolCall('get_user_info', async (args) => {
  return {
    name: 'John Doe',
    email: '[email protected]'
  };
});

// Open agent window
bridge.openAgent();

// Inject a message
bridge.injectMessage('Get my user information', { autoSend: true });

React Integration Example

import React, { useEffect, useRef, useState } from 'react';
import { SeahorseAgentBridge, ConnectionState } from 'seahorse-agent-bridge';

function MyApp() {
  const bridgeRef = useRef<SeahorseAgentBridge | null>(null);
  const [agentWindow, setAgentWindow] = useState<Window | null>(null);
  const [connectionState, setConnectionState] = useState<ConnectionState>('disconnected');

  useEffect(() => {
    // Initialize bridge with callbacks
    const bridge = new SeahorseAgentBridge({
      agentOrigin: 'https://seahorse-agent.agent.seahorse.dnotitia.com',
      autoRecover: true,

      onConnectionChange: (state, previousState) => {
        setConnectionState(state);
        console.log(`Connection: ${previousState} → ${state}`);
      },

      onWindowClosed: () => {
        setAgentWindow(null);
      },

      onSessionRecovered: (recoveredWindow) => {
        setAgentWindow(recoveredWindow);
        console.log('Session recovered!');
      }
    });

    bridgeRef.current = bridge;

    // Register tools (auto-recovery happens after this)
    bridge.registerTools([{
      name: 'get_user_data',
      description: 'Get user data from parent app',
      parameters: {
        type: 'object',
        properties: {
          userId: { type: 'string', description: 'User ID' }
        },
        required: ['userId']
      }
    }]);

    // Register handler
    bridge.onToolCall('get_user_data', async (args) => {
      const { userId } = args;
      // Fetch from your API
      const response = await fetch(`/api/users/${userId}`);
      return response.json();
    });

    // Cleanup
    return () => {
      bridge.destroy();
    };
  }, []);

  const handleOpenAgent = () => {
    if (bridgeRef.current) {
      const window = bridgeRef.current.openAgent();
      setAgentWindow(window);
    }
  };

  return (
    <div>
      <button onClick={handleOpenAgent} disabled={connectionState === 'connected'}>
        Open AI Assistant
      </button>
      <div>Status: {connectionState}</div>
    </div>
  );
}

API Reference

Constructor

new SeahorseAgentBridge(config: BridgeConfig)

Config Options:

| Option | Type | Required | Default | Description | |--------|------|----------|---------|-------------| | agentOrigin | string | ✅ | - | Agent's origin for PostMessage security validation | | agentUrl | string | ❌ | agentOrigin | Full URL to open agent window | | heartbeatInterval | number | ❌ | 5000 | Interval between heartbeat pings (ms) | | heartbeatTimeout | number | ❌ | 15000 | Time before declaring connection dead (ms) | | autoRecover | boolean | ❌ | true | Auto-recover session after parent refresh | | onConnectionChange | (state, previousState) => void | ❌ | - | Callback for connection state changes | | onWindowClosed | () => void | ❌ | - | Callback when agent window closes | | onSessionRecovered | (window) => void | ❌ | - | Callback when session is recovered |

Connection States:

  • 'disconnected': No connection to agent
  • 'connecting': Opening agent window or establishing connection
  • 'connected': Active connection with heartbeat
  • 'reconnecting': Connection lost, attempting to reconnect

Methods

registerTool(tool: ToolDefinition): void

Register a single tool.

registerTools(tools: ToolDefinition[]): void

Register multiple tools at once. If autoRecover is enabled, this will automatically attempt session recovery.

onToolCall(toolName: string, handler: ToolCallHandler): void

Set execution handler for a tool. Handler can be async.

bridge.onToolCall('get_data', async (args) => {
  const result = await fetchDataFromAPI(args);
  return result;
});

openAgent(): Window | null

Open agent in a new window. Returns window reference or null if popup blocked. Automatically starts heartbeat and connection monitoring.

closeAgent(): void

Close the agent window. Stops heartbeat and cleanup resources.

injectMessage(message: string, options?: MessageInjectionOptions): void

Inject a message into the agent's input area.

Options:

  • autoSend: If true, automatically sends the message after injection

isAgentOpen(): boolean

Check if agent window is currently open (not closed by user).

getConnectionState(): ConnectionState

Get current connection state: 'disconnected', 'connecting', 'connected', or 'reconnecting'.

reconnect(): boolean

Manually trigger reconnection attempt. Returns true if window recovered, false otherwise.

tryRecoverSession(): void

Attempt to recover existing agent window session. Called automatically by registerTools() if autoRecover is enabled.

destroy(): void

Cleanup and remove all event listeners. Note: Does NOT close the agent window to allow session recovery on parent refresh.

Example: Dashboard Integration

// Dashboard tools
const dashboardTools = [
  {
    name: 'get_dashboard_overview',
    description: 'Get dashboard overview metrics',
    parameters: {
      type: 'object',
      properties: {},
      required: []
    }
  },
  {
    name: 'get_sales_data',
    description: 'Get sales data for date range',
    parameters: {
      type: 'object',
      properties: {
        days: { type: 'number', description: 'Number of days' }
      },
      required: ['days']
    }
  }
];

// Register tools
bridge.registerTools(dashboardTools);

// Set handlers
bridge.onToolCall('get_dashboard_overview', async () => {
  return {
    totalRevenue: 125000,
    activeUsers: 1523,
    conversionRate: 3.2
  };
});

bridge.onToolCall('get_sales_data', async (args) => {
  const { days } = args;
  // Fetch real data from your backend
  return {
    period: `Last ${days} days`,
    data: [/* sales data */]
  };
});

Connection Management

Connection State Flow

stateDiagram-v2
    [*] --> disconnected: Initial State
    disconnected --> connecting: openAgent()
    connecting --> connected: Heartbeat Success
    connecting --> disconnected: Window Blocked/Closed
    connected --> reconnecting: Heartbeat Timeout
    reconnecting --> connected: Recovery Success
    reconnecting --> disconnected: Max Retries Exceeded
    connected --> disconnected: User Closes Window
    disconnected --> connecting: Parent Refresh + autoRecover

Heartbeat Mechanism

The SDK automatically monitors connection health using a heartbeat (ping/pong) system:

  1. Ping Interval: Every 5 seconds (configurable via heartbeatInterval)
  2. Pong Timeout: 15 seconds (configurable via heartbeatTimeout)
  3. Auto-Reconnection: Up to 5 attempts if connection lost
  4. Window Monitoring: Checks every second if agent window still open

How it works:

// Bridge sends ping every 5 seconds
bridge.sendPing() → Agent receives ping → Agent sends pong → Bridge receives pong

// If no pong received for 15 seconds
connection state: connected → reconnecting
attempt to recover window reference
if recovered: connected
if not recovered after 5 attempts: disconnected

Session Recovery

Problem: When parent window refreshes, it loses reference to the opened agent window, even though the window is still open.

Solution: The SDK automatically recovers the window reference using window.open('', 'seahorse-agent') trick.

Recovery Flow:

sequenceDiagram
    participant User
    participant Parent as Parent Window
    participant Bridge as SeahorseAgentBridge
    participant Agent as Agent Window

    User->>Parent: Refresh page (F5)
    Parent->>Parent: Page reloads
    Parent->>Bridge: new SeahorseAgentBridge({autoRecover: true})
    Parent->>Bridge: registerTools([...])
    Note over Bridge: Auto-recovery triggered
    Bridge->>Browser: window.open('', 'seahorse-agent')
    Browser-->>Bridge: Returns existing window reference
    Bridge->>Agent: Send tools_list via PostMessage
    Agent-->>Bridge: Receive tools_list
    Bridge->>Bridge: Start heartbeat
    Bridge->>Bridge: setConnectionState('connected')
    Bridge->>Parent: onSessionRecovered(window)
    Parent->>User: ✅ Session recovered!

Usage:

const bridge = new SeahorseAgentBridge({
  agentOrigin: '...',
  autoRecover: true, // ✅ Enables automatic recovery (default)

  onSessionRecovered: (window) => {
    console.log('Session recovered! Agent still connected.');
    setAgentWindow(window);
  }
});

// Register tools AFTER initialization
bridge.registerTools([...]); // Recovery happens here

Important Notes:

  • Recovery only works if agent window is still open
  • Recovery happens automatically after registerTools() is called
  • Window must be opened with name 'seahorse-agent' (SDK does this automatically)
  • Parent must register same tools after refresh for agent to continue working
  • Recovery fails silently if window was closed - call openAgent() to open new window

Backward Compatibility

The SDK maintains backward compatibility with event-based patterns:

// New callback pattern (recommended)
const bridge = new SeahorseAgentBridge({
  onConnectionChange: (state, previousState) => {
    console.log('Connection changed');
  }
});

// Old event pattern (still works)
window.addEventListener('agent-connection-state-changed', (event) => {
  const { previousState, currentState } = event.detail;
  console.log('Connection changed');
});

Both patterns work simultaneously - SDK dispatches CustomEvents AND calls callbacks.

Security

The bridge implements multiple security layers:

1. Origin Validation

// Agent validates all incoming PostMessage events
private isValidOrigin(origin: string): boolean {
    return this.allowedOrigins.includes(origin);
}

private handleParentMessage(event: MessageEvent): void {
    if (!this.isValidOrigin(event.origin)) {
        console.warn('Message from unauthorized origin:', event.origin);
        return;
    }
    // Process message...
}
  • allowedOrigins configured in backend config.json
  • All PostMessage events are validated before processing
  • Unauthorized origins are rejected silently

2. WebSocket Session Isolation

# Backend maintains session-based WebSocket connections
class ParentWindowConnectionManager:
    active_connections: Dict[str, WebSocket]  # session_id -> WebSocket

    async def call_tool(self, session_id: str, ...):
        websocket = self.active_connections.get(session_id)
        # Each session has isolated WebSocket channel
  • Each agent session has its own WebSocket connection
  • Tool calls are routed to correct session_id
  • Prevents cross-session data leakage

3. Request/Response Matching

// Unique IDs prevent response confusion
const callId = `call_${Date.now()}_${Math.random().toString(36)}`;
this.pendingCalls.set(callId, { resolve, reject, timeoutId });
  • Each tool call gets unique call_id and request_id
  • Responses matched to exact pending request
  • Prevents response hijacking or mix-up

4. Timeout Protection

// 30-second timeout prevents hanging
const timeoutId = setTimeout(() => {
    this.pendingCalls.delete(callId);
    reject(new Error(`Timeout: ${toolName}`));
}, this.timeoutMs);
  • Default 30-second timeout for all tool calls
  • Prevents resource exhaustion from hanging requests
  • Configurable via backend settings

5. Parent-Controlled Execution

  • Parent application maintains full control over tool handlers
  • Tools only execute if handler is registered
  • Parent validates tool arguments before execution
  • No arbitrary code execution from agent

6. No Sensitive Data in Transit

  • Tool definitions sent to agent (names, descriptions, parameters)
  • Actual tool execution logic stays in parent
  • Only structured JSON data exchanged
  • No code or functions transmitted

Browser Support

  • Chrome/Edge 90+
  • Firefox 88+
  • Safari 14+

License

MIT