How to Handle Real-Time Updates with WebSockets in Palmier Pro: The Server-Sent Events Implementation
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, 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. 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:
// 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 manages the server lifecycle. It initializes the server on 127.0.0.1 using a fixed port:
// 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 opens a streaming HTTP request to the same /mcp endpoint. At line 48, it sets the required Accept header to negotiate the SSE format:
// 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:
// 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. This parser yields a sequence of AnthropicStreamEvent values that the UI can consume asynchronously.
The client exposes this as an AsyncThrowingStream:
// 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 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:
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.swiftby sendingContent-Type: text/event-streamheaders and maintaining keep-alive connections on port19789. - Client-side consumption uses
PalmierClient.swiftwithURLSession.shared.bytes(for:)and theAnthropicSSE.parsemethod to process raw byte streams into typed events. - Tool execution flows through
ToolExecutor.swift, allowing the server to push partial AI responses and tool progress updates continuously. - Lifecycle management is handled by
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) 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, 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →