Skip to main content

Using Server-Sent Events (SSE)

Server-Sent Events (SSE) provide real-time, unidirectional communication from the backend to the frontend. This is useful for live updates without polling.

When to Use SSE

Use SSE when you need:
  • Real-time updates from the server (activity streams, notifications, live stats)
  • Automatic reconnection on connection loss
  • Lower network overhead compared to polling
SSE is unidirectional (server � client). For bidirectional communication, use WebSockets instead.

SSE Endpoint URL Pattern

All SSE endpoints use the /stream suffix:
// REST API endpoint (polling)
const response = await fetch('/api/users/me/mcp/client-activity?team_id=123&limit=20')
const data = await response.json()

// SSE streaming endpoint (real-time)
const url = '/api/users/me/mcp/client-activity/stream?team_id=123&limit=20'
connect(url)

URL Structure

  • REST: /api/{resource}/{action} - Returns snapshot of current data
  • SSE: /api/{resource}/{action}/stream - Pushes real-time updates
Both endpoints:
  • Accept the same query parameters
  • Return the same data structure
  • Apply the same filters and limits
The REST endpoint serves as a fallback for clients that don’t support SSE.

The useSSE Composable

The useSSE<T> composable handles all SSE logic including connection management, auto-reconnect, and cleanup.

Import

import { useSSE } from '@/composables/useSSE'
import type { McpClientActivity } from '@/services/mcpClientActivityService'

Basic Usage

1

Initialize the composable

Call useSSE<T>() with your expected data type and the SSE event name:
const {
  data: activities,
  isLoading,
  error,
  connect,
  disconnect
} = useSSE<McpClientActivity[]>('client_activity')
2

Connect to the SSE endpoint

Call connect() with the full SSE stream URL:
onMounted(() => {
  const url = McpClientActivityService.getStreamUrl(teamId, {
    limit: 20,
    active_within_minutes: 30
  })
  connect(url)
})
3

Use the reactive data

The data ref will automatically update when the server sends new events:
<template>
  <div v-if="isLoading">Loading...</div>
  <div v-else-if="error">{{ error }}</div>
  <div v-else-if="activities">
    <Item v-for="activity in activities" :key="activity.id">
      {{ activity.client_name }}
    </Item>
  </div>
</template>

Complete Example

Here’s the complete implementation from McpClientConnectionsCard.vue:
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue'
import { useEventBus } from '@/composables/useEventBus'
import { useSSE } from '@/composables/useSSE'
import { McpClientActivityService } from '@/services/mcpClientActivityService'
import type { McpClientActivity } from '@/services/mcpClientActivityService'

const eventBus = useEventBus()

const {
  data: activities,
  isLoading,
  error,
  connect,
  disconnect
} = useSSE<McpClientActivity[]>('client_activity')

function connectToTeam(teamId: string) {
  const url = McpClientActivityService.getStreamUrl(teamId, {
    limit: 20,
    active_within_minutes: 30
  })
  connect(url)
}

function handleTeamChange() {
  const teamId = eventBus.getState<string>('selected_team_id')
  if (teamId) {
    connectToTeam(teamId)
  } else {
    disconnect()
  }
}

onMounted(() => {
  const teamId = eventBus.getState<string>('selected_team_id')
  if (teamId) {
    connectToTeam(teamId)
  }
  eventBus.on('team-selected', handleTeamChange)
})

onUnmounted(() => {
  eventBus.off('team-selected', handleTeamChange)
})
</script>

<template>
  <div v-if="isLoading && !activities?.length">
    Loading...
  </div>

  <div v-else-if="error">
    <p>{{ error }}</p>
    <button @click="handleTeamChange">Retry</button>
  </div>

  <div v-else-if="!activities?.length">
    No active connections
  </div>

  <div v-else>
    <Item v-for="activity in activities" :key="activity.id">
      {{ activity.client_name }}
    </Item>
  </div>
</template>

API Reference

useSSE<T>()

function useSSE<T>(
  eventName: string,
  options?: UseSSEOptions
): UseSSEReturn<T>
Parameters:
  • eventName: The SSE event name to listen for (must match backend event name)
  • options: Optional configuration
Options:
interface UseSSEOptions {
  reconnectDelay?: number      // Default: 5000ms
  withCredentials?: boolean    // Default: true
}
Returns:
interface UseSSEReturn<T> {
  data: Ref<T | null>           // Reactive data from SSE stream
  isConnected: Ref<boolean>     // Connection status
  isLoading: Ref<boolean>       // Initial loading state
  error: Ref<string | null>     // Error message if any
  connect: (url: string) => void    // Connect to SSE endpoint
  disconnect: () => void            // Close connection
}

Auto-Reconnection

The composable automatically handles reconnection when the connection is lost:
  • Reconnects after 5 seconds by default (configurable via reconnectDelay)
  • Maintains the last URL for reconnection
  • Clears any pending reconnection attempts on manual disconnect()
The composable automatically disconnects when the component unmounts via onUnmounted().

Backend SSE Event Format

Your backend SSE endpoint must send events in this format:
// Backend sends:
reply.sse.send({
  event: 'client_activity',  // Must match eventName in useSSE()
  data: JSON.stringify({
    activities: [...]  // Your actual data
  })
})
The composable will:
  1. Parse the JSON data
  2. Extract the first key’s value (e.g., activities array)
  3. Update the data ref
Make sure the event name in your backend matches the eventName parameter in useSSE(). Mismatched event names will cause the composable to never receive data.