MCP Servers — Overview

lcyt provides two Model Context Protocol (MCP) servers that allow AI assistants (such as Claude) to send live captions to YouTube Live.

Both servers share a common set of caption tools; the SSE server adds two additional tools for privacy and GDPR data deletion. They differ in their transport mechanism.

PackageTransportBest for
lcyt-mcp-stdiostdioClaude Desktop, subprocess MCP clients
lcyt-mcp-sseHTTP + SSERemote/web MCP clients, shared sessions

stdio vs SSE — Detailed Comparison

Featurestdio (lcyt-mcp-stdio)SSE (lcyt-mcp-sse)
Transportstdin/stdout pipesHTTP + Server-Sent Events
PortNone — runs as subprocessPORT env var (default 3001)
SessionsOne session per processMultiple concurrent sessions shared across clients
Client supportClaude Desktop, any MCP stdio clientAny HTTP-capable MCP client
Tools availablestart, send_caption, send_batch, sync_clock, get_status, stopAll stdio tools + privacy, privacy_deletion
MCP Resourcessession://<id> resource exposedResources not available
AuthNone — process-level isolationOptional API key enforcement
GDPR toolsNot availableprivacy, privacy_deletion
Log routingRequires LCYT_LOG_STDERR=1Requires LCYT_LOG_STDERR=1
Typical useSingle user, local AI assistantShared service, multiple users

See the Tools Reference for full per-tool transport availability.


Quick Start

Stdio (Claude Desktop)

node packages/lcyt-mcp-stdio/src/server.js

Add to your Claude Desktop config:

{
  "mcpServers": {
    "lcyt": {
      "command": "node",
      "args": ["/path/to/packages/lcyt-mcp-stdio/src/server.js"],
      "env": { "LCYT_LOG_STDERR": "1" }
    }
  }
}

SSE (HTTP)

PORT=3001 node packages/lcyt-mcp-sse/src/server.js

Connect your MCP client to:

  • GET http://localhost:3001/sse — open SSE stream
  • POST http://localhost:3001/messages?sessionId=<id> — send messages

Configuration Examples

Managed SSE — mcp.lcyt.fi (easiest)

The quickest way to get started is to connect your MCP client to the hosted SSE service at https://mcp.lcyt.fi. No server setup is required — just obtain an API key and point your client at the endpoint.

Get an API key: contact the service administrator or sign up at lcyt.fi.

Claude Desktop config:

{
  "mcpServers": {
    "lcyt": {
      "type": "sse",
      "url": "https://mcp.lcyt.fi/sse",
      "headers": {
        "X-Api-Key": "YOUR_API_KEY"
      }
    }
  }
}

After restarting Claude Desktop the full tool set is available (start, send_caption, send_batch, sync_clock, get_status, stop, privacy, privacy_deletion). You can prompt Claude with:

“Start a YouTube Live caption session with stream key xxxx-xxxx-xxxx-xxxx and send ‘Hello, world!’”

Note: Stream keys are transmitted over TLS but are stored server-side while a session is active. Use a dedicated stream key and rotate it if you believe it has been exposed.


Claude Desktop — stdio

The stdio server integrates directly with Claude Desktop. Add the following block to your claude_desktop_config.json (location: ~/Library/Application Support/Claude/ on macOS, %APPDATA%\Claude\ on Windows):

{
  "mcpServers": {
    "lcyt": {
      "command": "node",
      "args": ["/absolute/path/to/packages/lcyt-mcp-stdio/src/server.js"],
      "env": {
        "LCYT_LOG_STDERR": "1"
      }
    }
  }
}

If you installed lcyt-mcp-stdio via npm globally you can also use:

{
  "mcpServers": {
    "lcyt": {
      "command": "npx",
      "args": ["lcyt-mcp-stdio"],
      "env": {
        "LCYT_LOG_STDERR": "1"
      }
    }
  }
}

After restarting Claude Desktop, the tools (start, send_caption, send_batch, sync_clock, get_status, stop) will be available. You can prompt Claude with:

“You will be sender of closed captions. Please start a YouTube live caption session. My stream key is rhh1-etst-bf7b-0wvm-5aem. Treat all my messages from now on as captions to be sent, and respond to my mesasges after tool use with: Sent (sequence): text.”

Claude Desktop — SSE (via reverse proxy or local port)

When the MCP SSE server is running locally (e.g. bound to 127.0.0.1:3001 behind nginx), you can connect to it from Claude Desktop using an HTTP-based MCP client config:

{
  "mcpServers": {
    "lcyt-sse": {
      "type": "sse",
      "url": "http://127.0.0.1:3001/"
    }
  }
}

With an API key (when MCP_REQUIRE_API_KEY=1 is set on the server):

{
  "mcpServers": {
    "lcyt-sse": {
      "type": "sse",
      "url": "http://127.0.0.1:3001/",
      "headers": {
        "X-Api-Key": "your-api-key"
      }
    }
  }
}

Docker / production (SSE server behind nginx)

The included docker-compose.yml binds both ports to loopback so they are not directly reachable from the public internet. Configure your host nginx to reverse-proxy from HTTPS to the local ports:

# API backend (port 3000)
server {
    listen 443 ssl;
    server_name api.example.com;
    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

# MCP SSE server (port 3001)
server {
    listen 443 ssl;
    server_name mcp.example.com;
    location / {
        proxy_pass http://127.0.0.1:3001;
        proxy_http_version 1.1;
        proxy_set_header Connection '';   # required for SSE
        proxy_buffering off;
        proxy_cache off;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Then configure your MCP client to use the public HTTPS endpoint:

{
  "mcpServers": {
    "lcyt-sse": {
      "type": "sse",
      "url": "https://mcp.example.com/sse",
      "headers": {
        "X-Api-Key": "your-api-key"
      }
    }
  }
}

Important: Log Routing

Both MCP servers must not write logs to stdout because the MCP protocol uses stdout for its message stream. Set the environment variable LCYT_LOG_STDERR=1 to route all lcyt logs to stderr.

LCYT_LOG_STDERR=1 node packages/lcyt-mcp-stdio/src/server.js

Deployment & security

  • Bind SSE to loopback when possible: for single-host deployments bind the SSE server to 127.0.0.1 and expose it via a secure reverse proxy only when necessary.
  • Set a stable JWT_SECRET in production if you persist session tokens or expect tokens to survive server restarts.
  • Ensure DB volume ownership when using DB_PATH with Docker; chown the volume to the runtime UID (e.g., 1000:1000) to avoid read-only SQLite errors.
  • SSE DB-backed persistence: when DB_PATH is configured the SSE server can persist session metadata and will rehydrate sessions on startup (this will start sender instances for persisted sessions). See sse.md for details and operational notes.

Reference

privacy — Privacy Notice

Return the service privacy notice as plain text.

Available in: SSE only


Parameters

None.

Returns

Plain text privacy statement.

MCP Stdio Transport

lcyt-mcp-stdio is an MCP server that communicates over standard input/output (stdin/stdout). It is the recommended integration for Claude Desktop and any MCP client that launches the server as a child process.

Package: packages/lcyt-mcp-stdio


How It Works

The MCP client launches lcyt-mcp-stdio as a subprocess. The client sends JSON-RPC messages over stdin; the server responds on stdout. This is the standard MCP stdio transport pattern.

Caption sessions are stored in memory within the subprocess. They are lost if the process exits.


Running the Server

node packages/lcyt-mcp-stdio/src/server.js

Set LCYT_LOG_STDERR=1 so that lcyt log messages go to stderr and do not corrupt the MCP protocol stream on stdout:

LCYT_LOG_STDERR=1 node packages/lcyt-mcp-stdio/src/server.js

Claude Desktop Integration

Add the following to your Claude Desktop configuration file (claude_desktop_config.json):

{
  "mcpServers": {
    "lcyt": {
      "command": "node",
      "args": ["/absolute/path/to/packages/lcyt-mcp-stdio/src/server.js"],
      "env": {
        "LCYT_LOG_STDERR": "1"
      }
    }
  }
}

After restarting Claude Desktop, the lcyt MCP server will be available. You can prompt Claude with:

“Start a YouTube Live caption session with stream key xxxx-xxxx-xxxx-xxxx and send ‘Hello, world!’”


Available Tools

ToolDescription
startCreate a new caption session
send_captionSend a single caption
send_batchSend multiple captions at once
sync_clockSynchronise clock with YouTube
get_statusQuery session state
stopEnd a session

See the Tools Reference for full parameter and return value documentation.


Resources

The stdio server exposes MCP resources that clients can read directly:

URIReturns
session://<session_id>JSON: {sequence, syncOffset, startedAt}

Architecture

MCP Client (Claude Desktop)
        │ stdin/stdout

lcyt-mcp-stdio (subprocess)


YoutubeLiveCaptionSender


YouTube Live Ingestion API
  • One MCP server process handles one client connection
  • Sessions survive reconnects within the same process lifetime
  • Process exit destroys all sessions

Environment Variables

VariableEffect
LCYT_LOG_STDERR=1Route all lcyt logs to stderr (required for MCP stdio)

No other environment variables are required. Stream keys and session parameters are supplied via tool calls at runtime.


Troubleshooting

Server not appearing in Claude Desktop

  • Ensure the path in args is absolute and the file exists
  • Verify Node.js is available in the command’s PATH
  • Check Claude Desktop logs for subprocess startup errors

Garbled output / JSON parse errors

  • Make sure LCYT_LOG_STDERR=1 is set — log output on stdout breaks the MCP stream

Session not found errors

  • Sessions are in-memory and are lost if the server process restarts
  • Call start again to create a new session

Deployment notes

  • LCYT_LOG_STDERR=1 is required: always set LCYT_LOG_STDERR=1 when running the stdio server under an MCP client so that logs go to stderr and do not corrupt the protocol on stdout.

  • Optional DB usage: the stdio server can be started with DB_PATH to enable usage logging. If you configure a SQLite DB_PATH backed by a Docker volume, ensure the runtime user can write the file (see README for chown example).

  • Session persistence: stdio-mode sessions are process-local. If you need sessions to survive restarts or to be shared between remote clients, use the SSE server instead.

MCP SSE Transport

lcyt-mcp-sse is an MCP server that communicates over HTTP with Server-Sent Events. It is suitable for web-based MCP clients, remote AI agents, and scenarios where multiple clients share caption sessions.

Package: packages/lcyt-mcp-sse


How It Works

The server listens for HTTP connections on a configurable port (default 3001).

  • GET /sse — client opens an SSE stream; the server assigns a sessionId for the connection
  • POST /messages?sessionId=<id> — client sends MCP messages to the server

Caption sessions are held in a shared in-memory pool accessible to all SSE connections. A caption session (identified by session_id returned from the start tool) survives HTTP reconnects as long as the server process is running.


Running the Server

node packages/lcyt-mcp-sse/src/server.js

With options:

PORT=3001 LCYT_LOG_STDERR=1 node packages/lcyt-mcp-sse/src/server.js

With optional database logging:

PORT=3001 DB_PATH=./lcyt.db LCYT_LOG_STDERR=1 node packages/lcyt-mcp-sse/src/server.js

HTTP Endpoints

GET /sse

Open an SSE stream. The server returns MCP protocol messages as SSE events.

Request headers

HeaderTypeRequiredDescription
X-Api-KeystringNoAPI key for usage logging (requires DB_PATH to be configured). Required when MCP_REQUIRE_API_KEY=1

Response headers

Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

The first SSE event contains the sessionId needed for POST /messages:

event: endpoint
data: /messages?sessionId=abc123

POST /messages

Send an MCP JSON-RPC message to the server.

Query parameters

ParameterTypeRequiredDescription
sessionIdstringYesSSE connection session ID (from the endpoint SSE event)

Body: MCP JSON-RPC message

Response: 202 Accepted or error

The server processes the message and sends the response on the SSE stream.


Available Tools

ToolDescription
startCreate a new caption session
send_captionSend a single caption
send_batchSend multiple captions at once
sync_clockSynchronise clock with YouTube
get_statusQuery session state
stopEnd a session
privacyReturn privacy notice
privacy_deletionRequest GDPR data erasure

See the Tools Reference for full parameter and return value documentation.


Authentication & API Keys

Authentication is optional by default. When DB_PATH is configured, sending an X-Api-Key request header to GET /sse enables usage logging and limits enforcement.

Enforce authentication:

Set MCP_REQUIRE_API_KEY=1 to reject connections that do not supply a valid API key:

MCP_REQUIRE_API_KEY=1 DB_PATH=./lcyt.db node packages/lcyt-mcp-sse/src/server.js

When MCP_REQUIRE_API_KEY=1 is set and no valid API key is provided, GET /sse returns 401 Unauthorized.


Environment Variables

VariableDefaultDescription
PORT3001HTTP server port
DB_PATHnonePath to SQLite database. Enables usage logging and API key validation when set.
MCP_REQUIRE_API_KEYunsetSet to 1 to require a valid API key on all SSE connections
LCYT_LOG_STDERRunsetSet to 1 to route lcyt logs to stderr (recommended)

Architecture

MCP Client A ──GET /sse──────────────────┐
MCP Client B ──GET /sse──────────────────┤

                              lcyt-mcp-sse (HTTP server)

                              Shared caption session pool
                              (in-memory Map<session_id, Sender>)

                              YoutubeLiveCaptionSender instances


                              YouTube Live Ingestion API

MCP Client A ──POST /messages?sessionId=a──► SSE server processes, responds via SSE
MCP Client B ──POST /messages?sessionId=b──► SSE server processes, responds via SSE
  • Multiple SSE clients can co-exist in the same server process
  • Caption sessions (session_id) are independent of SSE connections (sessionId)
  • A caption session started by Client A can be used by Client B if they share the session_id

Example: Connecting with curl

# 1. Open SSE stream (in a separate terminal)
curl -N http://localhost:3001/sse
# → event: endpoint
# → data: /messages?sessionId=abc123

# 2. Start a caption session
curl -X POST 'http://localhost:3001/messages?sessionId=abc123' \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"start","arguments":{"stream_key":"xxxx-xxxx-xxxx-xxxx"}}}'

# 3. Send a caption
curl -X POST 'http://localhost:3001/messages?sessionId=abc123' \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"send_caption","arguments":{"session_id":"<session_id>","text":"Hello!"}}}'

Troubleshooting

401 Unauthorized on GET /sse

  • MCP_REQUIRE_API_KEY=1 is set but no X-Api-Key header was supplied
  • Add the header to your request: X-Api-Key: your-key

Messages not received on SSE stream

  • Ensure the sessionId in POST /messages?sessionId=... matches the value from the endpoint SSE event
  • The SSE connection may have been closed; reconnect and use the new sessionId

Session not found errors

  • Caption sessions (session_id) are in-memory; they are lost if the server restarts
  • Call the start tool again to create a new session

Deployment & security notes

  • Bind to loopback for safety: the SSE server is network-accessible by default. For single-host deployments prefer binding to loopback (e.g. 127.0.0.1:3001) and expose it only via a secure reverse proxy (nginx) if external access is required.

  • Reverse-proxy recommended: if you must expose MCP SSE to the public internet, put an authenticated TLS-terminating reverse proxy in front of it and restrict access with firewall rules.

  • Stable JWT_SECRET: when using persistent sessions or token persistence, set a stable JWT_SECRET in your environment (don’t rely on autogenerated secrets) so issued tokens remain valid across restarts.

  • DB volume ownership: if you enable DB_PATH and use a Docker volume for persistence, ensure the container runtime user can write the SQLite file. If you see SqliteError: attempt to write a readonly database, chown the volume to the runtime UID (e.g., 1000:1000) before starting the container.

  • Reconnection behaviour: sessions stored in SQLite are rehydrated on server start without an active sender. Clients should re-register (POST /live or call the start tool) to obtain a fresh token and re-open SSE after a backend restart.

Persistence (DB-backed sessions)

When DB_PATH is set to a writable SQLite path, lcyt-mcp-sse will persist session metadata to the sessions table and will automatically rehydrate those sessions on server start. Key points:

  • How to enable: set DB_PATH to the desired SQLite file (or volume) and restart the server. Example:
DB_PATH=./lcyt.db node packages/lcyt-mcp-sse/src/server.js
  • Behaviour: persisted sessions are loaded on startup and YoutubeLiveCaptionSender instances are started for each rehydrated session so their session_ids remain usable without manual start calls.

  • Lifecycle: calling the stop tool will end the session and remove the persisted row. The privacy_deletion tool will also erase session records for the authenticated API key.

  • Operational notes:

    • Rehydration starts sender instances at boot — this uses network and CPU resources proportional to the number of persisted sessions. If you prefer a lazy approach (restore metadata but start senders only when a client attaches), request the lazy option and we can update the server to support it.
    • Ensure the SQLite file is writable by the runtime user (see README chown example) to avoid readonly errors.
  • Security: persist only on trusted hosts; stream keys are persisted in the sessions table and should be protected accordingly.

start — Start Caption Session

Create a new YoutubeLiveCaptionSender session identified by a unique session_id. The session is held in memory on the server.

Available in: stdio, SSE


Parameters

NameTypeRequiredDescription
stream_keystringYesYouTube Live stream key

Returns

{
  "session_id": "a1b2c3d4e5f6g7h8"
}
FieldTypeDescription
session_idstring16-character hex identifier for this session. Pass this to all other tools.

Example prompt: “Start a caption session with stream key xxxx-xxxx-xxxx-xxxx”

send_caption — Send a Single Caption

Send one caption to YouTube for an active session.

Available in: stdio, SSE


Parameters

NameTypeRequiredDescription
session_idstringYesSession ID from start
textstringYesCaption text to deliver
timestampstringNoISO timestamp (YYYY-MM-DDTHH:MM:SS.mmm). Defaults to current time.

Returns

{
  "ok": true,
  "sequence": 7
}
FieldTypeDescription
okbooleantrue on success
sequencenumberSequence number used for this caption

Throws if the session is not found or YouTube returns an error.

send_batch — Send Multiple Captions

Send an array of captions in a single HTTP request to YouTube.

Available in: stdio, SSE


Parameters

NameTypeRequiredDescription
session_idstringYesSession ID from start
captionsarrayYesArray of caption objects
captions[].textstringYesCaption text
captions[].timestampstringNoISO timestamp for this caption

Returns

{
  "ok": true,
  "sequence": 9,
  "count": 3
}
FieldTypeDescription
okbooleantrue on success
sequencenumberSequence number of the last caption in the batch
countnumberNumber of captions delivered

sync_clock — Synchronise Clock

Perform an NTP-style clock sync for the session. This compensates for clock drift between the MCP server and YouTube, improving timestamp accuracy.

Call this once after start and periodically during long sessions.

Available in: stdio, SSE


Parameters

NameTypeRequiredDescription
session_idstringYesSession ID from start

Returns

{
  "syncOffset": 150
}
FieldTypeDescription
syncOffsetnumberClock offset in milliseconds. Positive means YouTube’s clock is ahead.

get_status — Session Status

Retrieve the current state of a caption session.

Available in: stdio, SSE


Parameters

NameTypeRequiredDescription
session_idstringYesSession ID from start

Returns

{
---
id: mcp/tools/get-status
---
  "sequence": 9,
  "syncOffset": 150
}
FieldTypeDescription
syncOffsetnumberCurrent clock sync offset

stop — Stop Caption Session

End a caption session and release its resources.

Available in: stdio, SSE


Parameters

NameTypeRequiredDescription
session_idstringYesSession ID to stop

Returns

{
  "ok": true
}
FieldTypeDescription
okbooleantrue on success

privacy — Privacy Notice

Return the service privacy notice as plain text.

Available in: SSE only


Parameters

None.

Returns

Plain text privacy statement.

privacy_deletion — Request Data Deletion

Submit a GDPR right-to-erasure request. Requires a configured database (DB_PATH) and a valid API key.

Available in: SSE only


Parameters

NameTypeRequiredDescription
api_keystringYesAPI key to anonymise and delete

Returns

{
  "ok": true,
  "message": "Your data has been anonymised and deleted."
}
FieldTypeDescription
okbooleantrue on success
messagestringConfirmation message

Side effects:

  • Terminates any active session for the key
  • Anonymises owner name and email in the database
  • Deletes associated session stats, caption errors, and auth events

Requires: DB_PATH environment variable set on the SSE server.

Session Resources

The stdio server exposes MCP resources (in addition to tools) that clients can read directly.

Available in: stdio only


session://<session_id>

Read the current state of a session as a JSON resource.

URI pattern: session://<session_id>

Returns

{
  "sequence": 9,
  "syncOffset": 150,
  "startedAt": "2024-01-01T12:00:00.000Z"
}
FieldTypeDescription
sequencenumberCurrent sequence counter
syncOffsetnumberCurrent clock sync offset in milliseconds
startedAtstringISO timestamp when the session was created