sse-transport.md•9.71 kB
## Executive overview
Server-Sent Events (SSE) let the server stream a unidirectional, text-based event feed over plain HTTP. Compared with raw “streaming-HTTP” hacks or a full WebSocket upgrade, SSE provides automatic reconnection, built-in last-event IDs, and works through most corporate proxies—all while piggy-backing on the existing MCP REST port.➀ The upgrade path below adds a lightweight `SseTransport` alongside your existing HTTP and (paused) WebSocket modules, keeping the public MCP API unchanged for callers that still prefer request/response semantics. This document walks through the spec, code additions, and testing so an offline agent can implement the feature end-to-end.
---
## 1 Why SSE for MCPControl?
* **Standards-based.** Defined in the HTML Standard as the `text/event-stream` MIME type and the `EventSource` API, so every browser—and many Node polyfills—“just work”.➁ ➂
* **Cheaper than WebSockets.** No HTTP upgrade handshake (+1 RTT) and no full-duplex buffering; the server can flush whenever data is ready.➆ ➉
* **Auto-reconnect & backfill.** Clients resend `Last-Event-ID`; server can replay missed messages.➃
* **Fits MCP’s mostly-server-to-client traffic.** Tool-invocation responses, progress updates, and logs stream *out*; inbound commands remain normal POSTs.
---
## 2 High-level upgrade checklist
| Step | Owner | Key files |
|------|-------|-----------|
| 1. Add `SSE` to `TransportType` enum | `src/types/transport.ts` | |
| 2. Create `src/transports/sseTransport.ts` | new | |
| 3. Register route in `src/server.ts` (`/mcp/sse`) | server | |
| 4. Emit heartbeat & ID tracking | transport | |
| 5. Client polyfill docs (`eventsource` npm) | docs | |
| 6. Unit tests with Vitest | `test/sse.spec.ts` | |
| 7. Update README badges / docs | docs | |
(A diff view appears in §5.)
---
## 3 Protocol specification (SSE flavour)
### 3.1 Endpoint
```
GET /mcp/sse?auth=<jwt>&v=2025-04-30 HTTP/1.1
Accept: text/event-stream
```
Headers returned:
```
HTTP/1.1 200 OK
Content-Type: text/event-stream; charset=utf-8
Cache-Control: no-cache
Connection: keep-alive
```
> **Auth** – Re-use the existing bearer/JWT query param logic used by your REST calls; if OAuth tokens are in headers today, mirror that here.
### 3.2 Event grammar
```
:id:<ULID>
:event:<mcp-event-name> # optional; default "message"
:data:<JSON-encoded payload without newlines>
```
*Send exactly one JSON payload per event block.* An empty line terminates the event, per spec.➁ Events relevant to MCP:
| `event` | Payload schema | Direction |
|---------|----------------|-----------|
| `mcp.log` | `{ level, message, ts }` | → client |
| `mcp.response` | `{ requestId, data }` | → client |
| `mcp.heartbeat` | `{ ts }` | → client (every 25 s) |
| *(custom)* | free-form | – |
### 3.3 Keep-alive & reconnection
* Send a `comment` ping (`":\n\n"`) every 25 seconds (well under common 60 s proxy idle-timeout).➆
* Clients that disconnect will auto-reconnect after `retry:<ms>`; you may emit `retry: 3000` once at start to suggest 3 s back-off.➂
* On `Last-Event-ID`, replay from your in-memory ring buffer (configurable depth; 100 events default).
---
## 4 Mermaid diagrams
### 4.1 Connection handshake
```mermaid
sequenceDiagram
participant C as Client
participant S as MCPControl<br/>SSE Endpoint
C->>S: GET /mcp/sse (Accept: text/event-stream)
S-->>C: 200 OK + initial event stream
loop every 25 s
S-->>C: ":keep-alive\\n\\n"
end
Note over C,S: Any POST /mcp/tool remains<br/>separate HTTP request
```
### 4.2 State flow
```mermaid
stateDiagram-v2
[*] --> Disconnected
Disconnected --> Connecting: open()
Connecting --> Open: 200 OK
Open --> Reconnecting: network error
Reconnecting --> Connecting: back-off expires
Open --> Closed: evtSource.close()
Closed --> [*]
```
---
## 5 Reference TypeScript implementation
<details>
<summary><code>src/transports/sseTransport.ts</code></summary>
```ts
import { Response } from 'express';
import { ULID } from 'ulid';
import { Transport } from './baseTransport';
export class SseTransport extends Transport {
private clients = new Set<Response>();
private replayBuffer: { id: string; data: string }[] = [];
attach(app: import('express').Express) {
app.get('/mcp/sse', (req, res) => {
res.set({
'Content-Type': 'text/event-stream; charset=utf-8',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
});
res.flushHeaders(); // send headers right away
this.clients.add(res);
// replay missed events if Last-Event-ID is present
const lastId = req.header('Last-Event-ID');
if (lastId) {
this.replayBuffer
.filter(e => e.id > lastId)
.forEach(e => res.write(e.data));
}
req.on('close', () => this.clients.delete(res));
});
// ping loop
setInterval(() => this.broadcast(':\n\n'), 25_000);
}
/** Broadcasts a JSON payload under the given event name */
emitEvent(evt: string, payload: unknown) {
const id = ULID();
const data =
`id:${id}\n` +
(evt ? `event:${evt}\n` : '') +
`data:${JSON.stringify(payload)}\n\n`;
this.replayBuffer.push({ id, data });
if (this.replayBuffer.length > 100) this.replayBuffer.shift();
this.broadcast(data);
}
private broadcast(chunk: string) {
for (const res of this.clients) res.write(chunk);
}
}
```
</details>
### 5.1 Server hook (`src/server.ts` delta)
```diff
import { HttpTransport } from './transports/httpTransport';
+import { SseTransport } from './transports/sseTransport';
const app = express();
const httpTransport = new HttpTransport(app);
+const sseTransport = new SseTransport(app);
```
Everywhere you currently call `transport.emit(...)`, inject the SSE transport as well.
---
## 6 Offline client examples
### 6.1 Browser / WebView
```js
const es = new EventSource('/mcp/sse?auth=' + jwt);
es.onopen = () => console.log('open');
es.onmessage = (e) => console.log(JSON.parse(e.data));
es.addEventListener('mcp.response', ({ data }) => {
const { requestId, data: body } = JSON.parse(data);
});
```
### 6.2 Node 18+ (CommonJS) – no native `EventSource`
```bash
npm i eventsource # vendored in repo for offline use
```
```js
const EventSource = require('eventsource');
const es = new EventSource('http://localhost:3232/mcp/sse?auth=' + token);
```
(Offline agents should symlink `node_modules/eventsource` or embed a minimal polyfill; the official package is 600 lines, no deps.)➂
---
## 7 Testing recipes
| Tool | Command | Expect |
|------|---------|--------|
| `curl` | `curl -N http://localhost:3232/mcp/sse` | continuous stream with `data:` lines |
| Browser dev-tools | `new EventSource('/mcp/sse')` | `readyState === 1` |
| Vitest | mock Response objects; assert write calls | heartbeat every 25 s |
---
## 8 Security considerations
1. **CORS** – mirror REST settings; disallow “*” in prod.
2. **Bearer token leakage** – prefer `Authorization: Bearer` header; avoid query if referrers might log.
3. **Denial-of-service** – cap open connections (`MAX_SSE_CLIENTS=100` env); shed oldest if exceeded.
4. **Sensitive clipboard/mouse data** – enforce existing MCP permission model before emitting events.
---
## 9 Further reading (offline mirrors recommended)
* WHATWG HTML Standard § 9.2 Server-Sent Events➁
* MDN “Using server-sent events” guide➆
* MCP 2025-03-26 spec (core message envelope)➅
* “Understanding SSE with Node.js” tutorial➂
* `eventsource` npm README➂
* Ably blog: WebSockets vs SSE trade-offs➉
* StackOverflow answers on auto-reconnect➃ and Express setup➈
* MCPControl source tree for hook points➄
---
## 10 Changelog template
```md
### Added
* **SSE Transport** (`/mcp/sse`) providing real-time event streaming.
* Auto-reconnect, heartbeat, last-event replay.
* New `TransportType.SSE` enum.
```
---
> **Footnotes / Citations**
1. Streaming-over-HTTP workaround noted in earlier MCPControl discussions.
2. WHATWG SSE spec ([HTML Standard](https://html.spec.whatwg.org/multipage/server-sent-events.html?utm_source=chatgpt.com))
3. “Understanding SSE with Node.js” Medium guide ([Medium](https://itsfuad.medium.com/understanding-server-sent-events-sse-with-node-js-3e881c533081?utm_source=chatgpt.com))
4. StackOverflow: automatic reconnect behaviour ([Stack Overflow](https://stackoverflow.com/questions/61541750/server-sent-events-sse-automatic-reconnect?utm_source=chatgpt.com))
5. MCPControl GitHub repository root ([GitHub](https://github.com/Cheffromspace/MCPControl?utm_source=chatgpt.com))
6. MCP formal spec 2025-03-26 ([Model Context Protocol](https://modelcontextprotocol.io/specification/2025-03-26?utm_source=chatgpt.com))
7. MDN server-sent events reference ([MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events?utm_source=chatgpt.com))
8. StackOverflow Express SSE example ([Stack Overflow](https://stackoverflow.com/questions/34657222/how-to-use-server-sent-events-in-express-js?utm_source=chatgpt.com))
9. `eventsource` npm package docs ([npm](https://www.npmjs.com/package/eventsource?utm_source=chatgpt.com))
10. Ably engineering blog comparison of WebSockets vs SSE ([Ably Realtime](https://ably.com/blog/websockets-vs-sse?utm_source=chatgpt.com))
11. StackOverflow performance discussion ([Stack Overflow](https://stackoverflow.com/questions/63583989/performance-difference-between-websocket-and-server-sent-events-sse-for-chat-r?utm_source=chatgpt.com))