MCPcopy Index your code
hub / github.com/benwilber/tinysse

github.com/benwilber/tinysse @v0.7.3

Chat with this repo
repository ↗ · DeepWiki ↗ · release v0.7.3 ↗ · + Follow
135 symbols 199 edges 22 files 39 documented · 29%
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

Tiny SSE

A programmable server for Server-Sent Events (SSE).

Features

  • Flexible Message Handling – Filter, modify, redirect, and replay messages dynamically.
  • Reliable Connections – Track subscribers, support reconnections, and maintain session state.
  • Secure Access Control – Enforce authentication, authorization, and event-based restrictions.
  • Customizable Behavior – Use hooks to modify messages and manage subscriptions programmatically.

Table of Contents

Installation

Archives of binary releases are available for Linux, macOS, and Windows.

Homebrew (macOS)

brew tap benwilber/tinysse
brew install benwilber/tinysse/tinysse-bin
tinysse --help

Building

The project can be built with the standard Rust/Cargo toolchain:

git clone https://github.com/benwilber/tinysse.git
cd tinysse
cargo build --release
./target/release/tinysse --help

Examples

Basic Pub/Sub server

Start the server

$ tinysse
INFO tinysse: Listening on 127.0.0.1:1983

1) Start a subscriber

curl http://127.0.0.1:1983/sse
: ok
...

2) Publish a message

curl -X POST -d data="Hello, World" http://127.0.0.1:1983/sse
{"queued":1,"subscribers":1}

3) Observe the message received by the subscriber

: ok

data: Hello, World
...

A Whirlwind Tour

Make a Lua script script.lua

Run the server with the script

$ tinysse --script script.lua
-- The `uuid` package is built-in to the Tiny SSE server
local uuid = require "uuid"

-- A message is published
function publish(pub)
  -- Set a unique ID on the publish request.
  -- This can later be referenced in the `message(pub, sub)`
  -- function to correlate the publish request with message
  -- delivery to subscribers
  pub.id = uuid()

  -- We can override the data in the SSE message
  pub.msg.data = "Hello, Universe!"

  -- If the publisher did not set a message ID, then we can set one here.
  -- This will be the `id: <id>` line in the SSE message.
  if not pub.msg.id then
    pub.msg.id = uuid()
  end

  -- We can set a custom event
  pub.msg.event = "custom-event"

  -- Comments too
  pub.msg.comment = {"This is a comment", "Another comment!"}

  -- Return the pub request to the server or it
  -- will be rejected and not delivered to any subscribers
  return pub
end

-- A new subscriber connects
function subscribe(sub)
  -- Set a unique ID on the subscriber.
  sub.id = uuid()

  -- Return the sub request to the server or it
  -- will be rejected and the client will be disconnected immediately
  return sub
end

-- A message is delivered to a subscriber
function message(pub, sub)
  print("Publish ID:", pub.id)
  print("Message ID:", pub.msg.id)
  print("Subscriber ID:", sub.id)

  -- Return the pub request to the server or
  -- the subscriber will not receive the message
  -- (but will still remain connected for subsequent messages)
  return pub
end

-- A subscriber disconnects
function unsubscribe(sub)
  print("Unsubscribed:", sub.id)
end

HTTP API

Publishing messages

The server supports publishing SSE messages via HTTP POST to the URL path configured by the --pub-path=<path> option (defaults to /sse).

It accepts data encoded as both application/x-www-form-urlencoded and application/json. The specific content type must always be indicated in the request or it will be rejected.

curl -i -X POST \
  --header "content-type: application/json" \
  --data-raw '{"data": "Hello World"}' \
  http://127.0.0.1:1983/sse

A successful publish will respond with a 202 Accepted status code and an application/json body with the current number of subscribers and the number of messages in the queue that have not been delivered to all subscribers (yet).

HTTP/1.1 202 Accepted
content-type: application/json
content-length: 31

{"queued": 1, "subscribers": 1}

The size of the internal message queue can be configured with the --capacity=<size> option. It defaults to 256.

SSE message fields

All fields are optional but at least one must be provided or the message will be rejected with a 400 Bad Request error.

{
  "id": "some-id",
  "event": "custom-event",
  "data": "Some data",
  "comment": ["First comment", "Second comment"]
}

Equivalent message as application/x-www-form-urlencoded:

id=some-message-id
&event=custom-event
&data=Some%20data
&comment=First%20comment
&comment=Second%20comment

data containing newlines is automatically split across multiple data: lines in the SSE message.

Subscribing to messages

The server supports subscribing to SSE messages via HTTP GET to the URL path configured by the --sub-path=<path> option (defaults to /sse).

curl -i http://127.0.0.1:1983/sse

HTTP/1.1 200 OK
content-type: text/event-stream
cache-control: no-cache

: ok

id: some-id
event: custom-event
data: Some data
: First comment
: Second comment

: keep-alive

Upon successful subscription, the server will immediately respond with the SSE comment ok indicating that the connection is established and waiting for new messages.

Keep-alive messages (SSE comments) are sent periodically to ensure the connection stays open and is not closed by intermediate proxies due to socket inactivity. These messages are configurable with the --keep-alive and --keep-alive-text options.

Lua API

The server can function as just a simple SSE pub/sub server without using the Lua API. However, much of the advanced functionality (authorization, message routing, etc.) requires writing Lua code to implement custom behaviors. The server is asynchronous and invokes global Lua functions defined in the script given by the --script=<path> option when various events occur. The server will provide arguments to the functions with context of the event.

The program runs in a single Lua context for the lifetime of the server so that a global state is shared across the various function calls.

startup(cli)

This is the first function called by the server immediately after it begins listening on the configured address and port (default: 127.0.0.1:1983) and before the socket accepts any client connections. It will be called only once during the server lifetime, and will provide the CLI options to the program as a Lua table cli. The server will not accept a return value from this function. However, it will abort if the function raises a Lua error.

function startup(cli)
  -- The `cli` table looks like:
  {
    keep_alive_text = "keep-alive",
    script = "script.lua",
    script_tick = 500,
    log_level = "INFO",
    pub_path = "/sse",
    sub_path = "/sse",
    keep_alive = 60000,
    timeout_retry = 0,
    timeout = 300000,
    serve_static_path = "/",
    capacity = 256,
    listen = "127.0.0.1:1983",
    unsafe_script = false
  }
end

tick(count)

A periodic event that allows the Lua script to "wake up" and perform background tasks at regular intervals (default 500ms). It provides a single argument count which is the number of times the tick function has been invoked (including the current) since the server started.

function tick(count)
  -- Do background work here
end

publish(pub)

Called when a client wants to publish a message. It provides a single argument pub which is a Lua table containing context of the publish request. The function is free to modify the request however it needs, but it must return it (modified or not) to the server or the publish request will be rejected with a 403 Forbidden error and the message will not be delivered to any subscribers.

NOTE: Changes to the inner req table will not be preserved.

function publish(pub)
  -- The `pub` table looks like
  {
    req = {
      headers = {
        ["content-type"] = "application/json",
        ["content-length"] = "24",
        accept = "*/*",
        host = "127.0.0.1:1983",
        ["user-agent"] = "curl/8.7.1"
      },
      query = "",
      path = "/sse",
      addr = {
        ip = "127.0.0.1",
        port = 59615
      },
      method = "POST"
    },
    msg = {
      data = "Hello, World"
    }
  }

  -- The function is free to modify this table however it needs, but it
  -- must return it to the server or the message will be rejected.
  return pub
end

subscribe(sub)

Called when a new subscriber connects. It provides a single argument sub which is a Lua table containing context of the subscribe request. The function is free to modify the request however it needs, but it must return it (modified or not) to the server or the connection will be rejected with a 403 Forbidden error and the client will be disconnected immediately.

NOTE: Changes to the inner req table will not be preserved.

function subscribe(sub)
  -- The `sub` table looks like:
  {
    req = {
      query = "",
      headers = {
        ["user-agent"] = "curl/8.7.1",
        accept = "*/*",
        host = "127.0.0.1:1983"
      },
      path = "/sse",
      addr = {
        ip = "127.0.0.1",
        port = 59632
      },
      method = "GET"
    }
  }

  -- The function is free to modify this table however it needs, but it
  -- must return it to the server or the subscribe request will be rejected.
  return sub
end

catchup(sub, last_event_id)

Called immediately after a client subscribes. If the client provides a Last-Event-ID, either through the Last-Event-ID: request header or as a query parameter (?last_event_id=), this function should attempt to retrieve missed messages. If both are provided, the header takes precedence. The function may return nil or an array of SSE messages to "catch up" the subscriber with any messages they may have missed due to reconnection or to provide recent message history. If last_event_id is nil or no messages are available, the function may return nil instead of an empty array.

NOTE: The message(pub, sub) function will not be called for messages delivered from the catchup(sub, last_event_id) function.

function catchup(sub, last_event_id)
  -- last_event_id might be nil if the client did not provide it
  local msgs = {}

  -- For instance, "catch-up" subscriber with the 10 most recent messages
  for i=1,10 do
    table.insert(msgs, {
      id = "some-id-" .. i,
      event = "some-event",
      data = "some data"
    })
  end

  return msgs
end

message(pub, sub)

Called before delivering a message to a subscriber. Receives pub and sub, the tables returned from the publish and subscribe functions. Modifications to these tables affect only this subscriber, not others receiving the same message. Typically used for routing and subscriber-specific adjustments.

NOTE: Changes to the inner req tables will not be preserved.

function message(pub, sub)
  -- Subscriber-specific logic such as routing, message modifications, etc.

  -- Returning `nil` (or just nothing at all) will prevent this subscriber from receiving
  -- the message.  Returning the `pub` table to the server will continue with
  -- delivery of the (possibly modified) SSE message to the subscriber.
  return pub
end

unsubscribe(sub)

Called when a subscriber disconnects. The server provides a single argument sub which is the Lua table returned from the subscribe function. It does not accept any return value.

function unsubscribe(sub)
  -- Client has unsubscribed (disconnected) from the SSE server.
end

timeout(sub, elapsed)

Called when a subscriber disconnects as result of an SSE timeout. The server provides two arguments, sub and elapsed. sub is the table returned from the subscribe function, and elapsed is the total milliseconds that the subscriber was connected. The server accepts an optional return value which is the number of milliseconds that the client should wait before reconnecting. If not given, it will default to the value given by the --timeout-retry option.

NOTE: The unsubscribe(sub) function will be called immediately after this.

function timeout(sub, elapsed)
  -- Subscriber timed-out and was disconnected.
end

For advanced usage, see the Lua API Built-ins and the Lua examples

Usage

```text $ tinysse --help Tiny SSE

A programmable server for Server-Sent Events (SSE).

Usage: tinysse [OPTIONS]

Options: -l, --listen The address and port for the HTTP server to listen

      [env: TINYSSE_LISTEN=]
      [default: 127.0.0.1:1983]

-L, --log-level The logging level for the server. Possible values: ERROR, WARN, INFO, DEBUG, TRACE

      [env: TINYSSE_LOG_LEVEL=]
      [default: INFO]

-k, --keep-alive <

Core symbols most depended-on inside this repo

Shape

Method 86
Class 28
Function 20
Enum 1

Languages

Rust100%

Modules by API surface

src/userdata/http.rs19 symbols
src/req.rs17 symbols
src/script.rs15 symbols
src/userdata/sqlite.rs13 symbols
src/userdata/log.rs10 symbols
src/userdata/template.rs7 symbols
src/web.rs6 symbols
src/userdata/url.rs6 symbols
src/userdata/base64.rs5 symbols
src/types.rs5 symbols
src/msg.rs5 symbols
src/cli.rs5 symbols

For agents

$ claude mcp add tinysse \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact

Ask about this repo answers extend the page