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.
| Package | Transport | Best for |
|---|---|---|
lcyt-mcp-stdio | stdio | Claude Desktop, subprocess MCP clients |
lcyt-mcp-sse | HTTP + SSE | Remote/web MCP clients, shared sessions |
stdio vs SSE — Detailed Comparison
| Feature | stdio (lcyt-mcp-stdio) | SSE (lcyt-mcp-sse) |
|---|---|---|
| Transport | stdin/stdout pipes | HTTP + Server-Sent Events |
| Port | None — runs as subprocess | PORT env var (default 3001) |
| Sessions | One session per process | Multiple concurrent sessions shared across clients |
| Client support | Claude Desktop, any MCP stdio client | Any HTTP-capable MCP client |
| Tools available | start, send_caption, send_batch, sync_clock, get_status, stop | All stdio tools + privacy, privacy_deletion |
| MCP Resources | session://<id> resource exposed | Resources not available |
| Auth | None — process-level isolation | Optional API key enforcement |
| GDPR tools | Not available | privacy, privacy_deletion |
| Log routing | Requires LCYT_LOG_STDERR=1 | Requires LCYT_LOG_STDERR=1 |
| Typical use | Single user, local AI assistant | Shared 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 streamPOST 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.1and expose it via a secure reverse proxy only when necessary. - Set a stable
JWT_SECRETin production if you persist session tokens or expect tokens to survive server restarts. - Ensure DB volume ownership when using
DB_PATHwith Docker; chown the volume to the runtime UID (e.g.,1000:1000) to avoid read-only SQLite errors. - SSE DB-backed persistence: when
DB_PATHis configured the SSE server can persist session metadata and will rehydrate sessions on startup (this will start sender instances for persisted sessions). Seesse.mdfor details and operational notes.
Reference
- Tools Reference — all tools with per-tool transport availability (stdio / SSE)
- Stdio Transport — configuration and integration guide
- SSE Transport — configuration and integration guide
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
| Tool | Description |
|---|---|
start | Create a new caption session |
send_caption | Send a single caption |
send_batch | Send multiple captions at once |
sync_clock | Synchronise clock with YouTube |
get_status | Query session state |
stop | End 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:
| URI | Returns |
|---|---|
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
| Variable | Effect |
|---|---|
LCYT_LOG_STDERR=1 | Route 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
argsis absolute and the file exists - Verify Node.js is available in the
command’sPATH - Check Claude Desktop logs for subprocess startup errors
Garbled output / JSON parse errors
- Make sure
LCYT_LOG_STDERR=1is 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
startagain to create a new session
Deployment notes
-
LCYT_LOG_STDERR=1is required: always setLCYT_LOG_STDERR=1when running the stdio server under an MCP client so that logs go tostderrand do not corrupt the protocol onstdout. -
Optional DB usage: the stdio server can be started with
DB_PATHto enable usage logging. If you configure a SQLiteDB_PATHbacked by a Docker volume, ensure the runtime user can write the file (see README forchownexample). -
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 asessionIdfor the connectionPOST /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
| Header | Type | Required | Description |
|---|---|---|---|
X-Api-Key | string | No | API 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
| Parameter | Type | Required | Description |
|---|---|---|---|
sessionId | string | Yes | SSE 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
| Tool | Description |
|---|---|
start | Create a new caption session |
send_caption | Send a single caption |
send_batch | Send multiple captions at once |
sync_clock | Synchronise clock with YouTube |
get_status | Query session state |
stop | End a session |
privacy | Return privacy notice |
privacy_deletion | Request 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
| Variable | Default | Description |
|---|---|---|
PORT | 3001 | HTTP server port |
DB_PATH | none | Path to SQLite database. Enables usage logging and API key validation when set. |
MCP_REQUIRE_API_KEY | unset | Set to 1 to require a valid API key on all SSE connections |
LCYT_LOG_STDERR | unset | Set 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=1is set but noX-Api-Keyheader was supplied- Add the header to your request:
X-Api-Key: your-key
Messages not received on SSE stream
- Ensure the
sessionIdinPOST /messages?sessionId=...matches the value from theendpointSSE 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
starttool 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 stableJWT_SECRETin your environment (don’t rely on autogenerated secrets) so issued tokens remain valid across restarts. -
DB volume ownership: if you enable
DB_PATHand use a Docker volume for persistence, ensure the container runtime user can write the SQLite file. If you seeSqliteError: 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
/liveor call thestarttool) 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_PATHto 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
YoutubeLiveCaptionSenderinstances are started for each rehydrated session so theirsession_ids remain usable without manualstartcalls. -
Lifecycle: calling the
stoptool will end the session and remove the persisted row. Theprivacy_deletiontool 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
readonlyerrors.
-
Security: persist only on trusted hosts; stream keys are persisted in the
sessionstable 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
| Name | Type | Required | Description |
|---|---|---|---|
stream_key | string | Yes | YouTube Live stream key |
Returns
{
"session_id": "a1b2c3d4e5f6g7h8"
}
| Field | Type | Description |
|---|---|---|
session_id | string | 16-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
| Name | Type | Required | Description |
|---|---|---|---|
session_id | string | Yes | Session ID from start |
text | string | Yes | Caption text to deliver |
timestamp | string | No | ISO timestamp (YYYY-MM-DDTHH:MM:SS.mmm). Defaults to current time. |
Returns
{
"ok": true,
"sequence": 7
}
| Field | Type | Description |
|---|---|---|
ok | boolean | true on success |
sequence | number | Sequence 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
| Name | Type | Required | Description |
|---|---|---|---|
session_id | string | Yes | Session ID from start |
captions | array | Yes | Array of caption objects |
captions[].text | string | Yes | Caption text |
captions[].timestamp | string | No | ISO timestamp for this caption |
Returns
{
"ok": true,
"sequence": 9,
"count": 3
}
| Field | Type | Description |
|---|---|---|
ok | boolean | true on success |
sequence | number | Sequence number of the last caption in the batch |
count | number | Number 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
| Name | Type | Required | Description |
|---|---|---|---|
session_id | string | Yes | Session ID from start |
Returns
{
"syncOffset": 150
}
| Field | Type | Description |
|---|---|---|
syncOffset | number | Clock 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
| Name | Type | Required | Description |
|---|---|---|---|
session_id | string | Yes | Session ID from start |
Returns
{
---
id: mcp/tools/get-status
---
"sequence": 9,
"syncOffset": 150
}
| Field | Type | Description |
|---|---|---|
syncOffset | number | Current clock sync offset |
stop — Stop Caption Session
End a caption session and release its resources.
Available in: stdio, SSE
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
session_id | string | Yes | Session ID to stop |
Returns
{
"ok": true
}
| Field | Type | Description |
|---|---|---|
ok | boolean | true 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
| Name | Type | Required | Description |
|---|---|---|---|
api_key | string | Yes | API key to anonymise and delete |
Returns
{
"ok": true,
"message": "Your data has been anonymised and deleted."
}
| Field | Type | Description |
|---|---|---|
ok | boolean | true on success |
message | string | Confirmation 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"
}
| Field | Type | Description |
|---|---|---|
sequence | number | Current sequence counter |
syncOffset | number | Current clock sync offset in milliseconds |
startedAt | string | ISO timestamp when the session was created |