# How to Handle Real-Time Updates with WebSockets in Palmier Pro: The Server-Sent Events Implementation

> Learn how Palmier Pro handles real-time updates using Server-Sent Events over HTTP. Stream AI content and tool progress efficiently from MCP to your UI.

- Repository: [Palmier/palmier-pro](https://github.com/palmier-io/palmier-pro)
- Tags: how-to-guide
- Published: 2026-06-21

---

**Palmier Pro does not use WebSockets for real-time updates; instead, it implements Server-Sent Events (SSE) over HTTP to stream AI-generated content and tool progress from the MCP server to the UI.**

While many applications use WebSockets for push-based communication, the palmier-io/palmier-pro repository takes a different approach. The codebase relies on **Server-Sent Events (SSE)**—a lightweight HTTP-based streaming protocol—to deliver real-time updates from the MCP (Message Control Protocol) server to the client. This implementation leverages standard HTTP headers and `URLSession` streaming rather than WebSocket handshakes, making it ideal for macOS applications requiring one-directional server-to-client data flows.

## Why Palmier Pro Uses Server-Sent Events Instead of WebSockets

### Simplicity Over WebSocket Handshakes

SSE works over plain HTTP, avoiding the extra handshake and framing overhead required by WebSockets. In [`Sources/PalmierPro/Agent/MCP/MCPHTTPServer.swift`](https://github.com/palmier-io/palmier-pro/blob/main/Sources/PalmierPro/Agent/MCP/MCPHTTPServer.swift), the server establishes a persistent TCP connection using standard HTTP response headers, eliminating the need for protocol upgrades.

### Native macOS Networking Compatibility

The implementation uses `NWListener` and `NWConnection` from the Network framework to handle raw TCP streams, while `URLSession` provides built-in support for incremental byte handling. This approach integrates seamlessly with Swift's async/await patterns and requires no third-party WebSocket libraries.

### Unidirectional Data Flow

Palmier Pro's real-time needs are primarily server-to-client (AI streaming responses and tool progress updates). SSE naturally fits this pattern because it is designed for one-way streaming. Client-to-server messages are handled as standard HTTP POST requests, keeping the architecture simple and stateless.

## Server-Side Implementation: The MCP HTTP Server

### Setting Up the SSE Endpoint in MCPHTTPServer.swift

The server implementation resides in [`Sources/PalmierPro/Agent/MCP/MCPHTTPServer.swift`](https://github.com/palmier-io/palmier-pro/blob/main/Sources/PalmierPro/Agent/MCP/MCPHTTPServer.swift). When a client issues a `GET` request to `/mcp`, the server responds with an `event-stream` content type and maintains a keep-alive connection.

At line 85, the server sends the HTTP headers required to establish the SSE stream:

```swift
// MCPHTTPServer.swift – line 85
sendRaw("HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nCache-Control: no-cache\r\nConnection: keep-alive\r\n\r\n: connected\n\n", on: connection, keepAlive: true)

```

This raw HTTP response sets `Content-Type: text/event-stream` and `Connection: keep-alive`, signaling to the client that this is a persistent streaming endpoint. The `: connected\n\n` payload serves as an initial comment to confirm the connection is alive.

### Lifecycle Management with MCPService

The `MCPService` class in [`Sources/PalmierPro/Agent/MCP/MCPService.swift`](https://github.com/palmier-io/palmier-pro/blob/main/Sources/PalmierPro/Agent/MCP/MCPService.swift) manages the server lifecycle. It initializes the server on `127.0.0.1` using a fixed port:

```swift
// MCPService.swift
static let port: UInt16 = 19789

func start() async throws {
    let httpServer = MCPHTTPServer(port: Self.port)
    // Register tools and resources...
    await httpServer.start()
}

```

The service binds to port `19789` and coordinates tool registration before starting the HTTP listener.

## Client-Side Implementation: Consuming the Event Stream

### Establishing the Connection in PalmierClient.swift

The Swift client in [`Sources/PalmierPro/Agent/Clients/PalmierClient.swift`](https://github.com/palmier-io/palmier-pro/blob/main/Sources/PalmierPro/Agent/Clients/PalmierClient.swift) opens a streaming HTTP request to the same `/mcp` endpoint. At line 48, it sets the required `Accept` header to negotiate the SSE format:

```swift
// PalmierClient.swift – line 48
request.setValue("text/event-stream", forHTTPHeaderField: "accept")

```

The client uses `URLSession.shared.bytes(for:)` to receive the raw byte stream asynchronously:

```swift
// PalmierClient.swift – run() implementation
var request = URLRequest(url: endpoint)
request.httpMethod = "POST"
request.setValue("Bearer \(jwt)", forHTTPHeaderField: "Authorization")
request.setValue("application/json", forHTTPHeaderField: "content-type")
request.setValue("text/event-stream", forHTTPHeaderField: "accept")

let (bytes, response) = try await URLSession.shared.bytes(for: request)
// Feed the raw byte stream into the SSE parser.
try await AnthropicSSE.parse(bytes: bytes, continuation: continuation)

```

### Parsing SSE Events with AnthropicSSE

The incoming bytes are processed by `AnthropicSSE.parse`, located in [`Sources/PalmierPro/Agent/Clients/AnthropicClient.swift`](https://github.com/palmier-io/palmier-pro/blob/main/Sources/PalmierPro/Agent/Clients/AnthropicClient.swift). This parser yields a sequence of `AnthropicStreamEvent` values that the UI can consume asynchronously.

The client exposes this as an `AsyncThrowingStream`:

```swift
// PalmierClient.swift – stream() implementation
func stream(system: String, tools: [Tool], messages: [Message]) -> AsyncThrowingStream<AnthropicStreamEvent, Error> {
    AsyncThrowingStream { continuation in
        let task = Task {
            do {
                try await run(system: system, tools: tools, messages: messages, continuation: continuation)
                continuation.finish()
            } catch {
                continuation.finish(throwing: error)
            }
        }
        continuation.onTermination = { _ in task.cancel() }
    }
}

```

## Processing Real-Time Events

### Handling Tool Execution Updates

The `ToolExecutor` layer in [`Sources/PalmierPro/Agent/Tools/ToolExecutor.swift`](https://github.com/palmier-io/palmier-pro/blob/main/Sources/PalmierPro/Agent/Tools/ToolExecutor.swift) registers server methods like `ListTools` and `CallTool`, dispatching responses through the same HTTP stream. Because the connection remains open, the server can push incremental updates—such as partial AI-generated responses—without requiring round-trips for each fragment.

The UI consumes these events in a structured switch statement:

```swift
for try await event in client.stream(system: ..., tools: ..., messages: ...) {
    switch event {
    case .content(let chunk):
        // Append partial AI response to the UI.
        viewModel.append(chunk)
    case .toolResult(let result):
        // Update tool progress UI.
        viewModel.updateTool(result)
    default:
        break
    }
}

```

## Summary

- **Palmier Pro uses SSE, not WebSockets**, implementing a unidirectional HTTP streaming protocol for real-time updates from the MCP server to the UI.
- **Server-side** streaming is established in [`MCPHTTPServer.swift`](https://github.com/palmier-io/palmier-pro/blob/main/MCPHTTPServer.swift) by sending `Content-Type: text/event-stream` headers and maintaining keep-alive connections on port `19789`.
- **Client-side** consumption uses [`PalmierClient.swift`](https://github.com/palmier-io/palmier-pro/blob/main/PalmierClient.swift) with `URLSession.shared.bytes(for:)` and the `AnthropicSSE.parse` method to process raw byte streams into typed events.
- **Tool execution** flows through [`ToolExecutor.swift`](https://github.com/palmier-io/palmier-pro/blob/main/ToolExecutor.swift), allowing the server to push partial AI responses and tool progress updates continuously.
- **Lifecycle management** is handled by [`MCPService.swift`](https://github.com/palmier-io/palmier-pro/blob/main/MCPService.swift), which coordinates server startup and tool registration.

## Frequently Asked Questions

### Why does Palmier Pro use SSE instead of WebSockets?

Palmier Pro uses SSE because it provides a simpler implementation for unidirectional server-to-client streaming without the protocol upgrade overhead of WebSockets. SSE works over standard HTTP, integrates natively with `URLSession` and macOS networking frameworks, and fits the application's requirement where the server primarily pushes AI-generated content and tool progress updates to the client while client requests are handled via separate HTTP POST calls.

### How does the client parse incoming SSE events?

The client uses `AnthropicSSE.parse` (defined in [`AnthropicClient.swift`](https://github.com/palmier-io/palmier-pro/blob/main/AnthropicClient.swift)) to transform the raw byte stream from `URLSession.shared.bytes(for:)` into structured `AnthropicStreamEvent` values. This parser processes the SSE format, extracting event data and yielding typed events asynchronously that the UI layer can consume through Swift's `AsyncThrowingStream` interface.

### What port does the MCP server use for real-time updates?

According to [`MCPService.swift`](https://github.com/palmier-io/palmier-pro/blob/main/MCPService.swift), the MCP HTTP server binds to `127.0.0.1` on port `19789` (defined as `MCPService.port = 19789`). This fixed localhost port ensures the Swift client can reliably connect to the local MCP server for streaming event data.

### Can the client send messages back through the SSE connection?

No, the SSE connection is strictly unidirectional from server to client. Client-to-server communication in Palmier Pro is handled through standard HTTP POST requests with JSON payloads sent to the MCP server endpoints. This separation of concerns keeps the streaming channel dedicated to real-time updates while request/response patterns handle command execution.