Skip to content

Lurker MCP & HTTP API ​

Lurker exposes its data and IRC actions through an authenticated Model Context Protocol endpoint so external programs — LLM-driven agents, scripts, or anything else — can drive your bouncer without a browser open.

This document covers the operator side: how to mint a token, point an MCP-aware client at your Lurker, and what tools are available.

Quick start ​

  1. Mint a token in your settings (/settings/api-tokens). Choose read-only or read-write at creation time. The raw token is shown exactly once; copy it now. On lurker.chat there are no API tokens; sign in with OAuth instead (see below).
  2. Configure your MCP client with the token and the endpoint (https://<your-lurker>/mcp). See Claude Desktop below for a worked example.
  3. Verify with curl:
    sh
    curl -X POST https://<your-lurker>/mcp \
      -H "Authorization: Bearer <your-token>" \
      -H "Content-Type: application/json" \
      -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Scopes ​

scopewhat it grants
readAll read verbs. The token can list networks, browse buffers, fetch backlog, search history, and read your nick notes.
read-writeEverything read does, plus every write verb: sending messages, notices and CTCP actions, writing nick notes, joining and parting channels, changing your nick, away state and channel topics, connecting and disconnecting networks, and send_raw — arbitrary IRC commands (MODE, KICK, OPER, …) issued as you.

Scopes are coarse on purpose. Per-verb scopes are not implemented because the threat model assumes the operator is the only person holding tokens for their own account.

API tokens and OAuth sign-in ​

/mcp accepts two bearer credentials:

  • An API token from your settings, with the scope you chose. API tokens don't open the WebSocket the browser uses. lurker.chat doesn't offer them.
  • An OAuth access token. An MCP client that looks for an OAuth server at your Lurker's root can skip the token and sign in through your browser: it finds the discovery document, registers itself, and you approve it. The token is read-write here and works everywhere a password sign-in does. Its redirect URI has to follow the rules in OAuth for third-party clients. On lurker.chat this is the only way in: use https://app.lurker.chat/mcp as the endpoint.

While an account is paused, either credential gets the read tools only.

There is no way to drive the browser-style stateful protocol (presence, drafts, snapshot resume) from an MCP client — that surface is deliberately out of scope.

Tools (MCP verbs) ​

All twenty-three tools come back through tools/list with full JSON Schemas for their inputs. A read-only token only sees the seven read tools.

list_networks (read) ​

Networks configured for your account, with live connection state and the current nick.

list_buffers (read) ​

Channels and DMs you have history for, with the most recent message timestamp. Optionally filter by networkId. Server pseudo-buffers (:server:*) are deliberately excluded — they're a UI plumbing concept, not data agents should reason about.

recent_messages (read) ​

Window of recent messages for one buffer, oldest-first. Paginate backwards by passing the lowest id from a previous result as before. Limit defaults to 100, capped at 500.

search_messages (read) ​

Full-text search across your message history. Free-text query runs through SQLite FTS5 (multiple terms are ANDed). Optional structured filters: networkId, target, nick. Limit defaults to 50, capped at 100.

get_nick_note (read) ​

Read your free-form note about a nick on a network. Empty string when no note exists.

set_nick_note (read-write) ​

Write a free-form note. Pass an empty string to delete. Notes are capped at 4096 chars. Writes fan out to any open browser tabs so the UI reflects the change immediately.

set_relay_bot (read-write) ​

Mark or unmark a nick as a relay/bridge bot (#277). When marked, messages from that bot are re-attributed to the speaker embedded in its envelope, so [Discord] <alice> hi is shown as from alice. Pass marked: false to clear the mark. An optional pattern overrides the built-in envelope formats with a template using {source}, {nick} and {message}. A template missing {nick} or {message} does not fail the call — it is stored and then silently ignored at render time, so the verb returns marked: true for a pattern that does nothing. Returns the stored { networkId, nick, marked, pattern }, echoing the canonical stored casing, and syncs the change to the user's open tabs.

send_message (read-write) ​

Send a PRIVMSG to a channel or peer. Returns { ok: false, error: "not-connected" } when the network is offline; this comes back as a normal tool result (not a JSON-RPC error) so agents can branch on the value instead of catching.

send_action (read-write) ​

Send a CTCP ACTION (/me ...). Same shape and error semantics as send_message.

send_notice (read-write) ​

Send a NOTICE to a channel or peer. Same shape and error semantics as send_message, with NOTICE conventions — no auto-reply is expected, and bots conventionally use it for output that should not trigger further bots.

send_raw (read-write) ​

Send a raw IRC protocol line verbatim on a network — the escape hatch for any command without a dedicated verb: MODE #chan +o nick, KICK #chan bob :spam, INVITE bob #chan, OPER user pass, and so on. No parsing, no trailing CRLF. Powerful and unguarded — it runs as you. Prefer a dedicated verb wherever one exists. A PRIVMSG to a channel with end-to-end encryption enabled is rejected (e2e-channel-use-send-message) rather than sent: this path has none of send_message's encryption, and no local echo either, so a leak here would be silent at both ends.

Server replies (WHOIS, LIST, …) arrive asynchronously in the network's server buffer, whose target is the literal :server:<networkId>. That buffer is deliberately absent from list_buffers, so the result carries a serverBuffer field with the exact string to hand to recent_messages.

not-connected here means the socket is actually up, not merely that a connection object exists — a network in reconnect backoff would otherwise accept the call and drop the line. Every write verb below uses the same gate.

join_channel (read-write) ​

Join a channel; optional key for +k channels. Without one, the channel's stored key is sent if Lurker has one. The channel buffer and its member list arrive asynchronously.

part_channel (read-write) ​

Leave a channel, with an optional part reason.

set_nick (read-write) ​

Change your nick on a network. Asynchronous and may be rejected (nick in use / invalid) — watch the server buffer for the outcome.

set_away (read-write) ​

Set or clear your away status on one network (networkId), or on every network when networkId is omitted. Pass message to go away; omit it to come back. Returns { ok: true, away }.

list_members (read) ​

List the members currently in a joined channel, with their prefix modes (o/h/v/…) and away state. Sorted by nick and capped at limit (default 200, max 1000) so a large channel can't flood the caller's context; count is always the true total and truncated flags a short page. Returns not-in-channel if you aren't in it.

whois (read-write) ​

Send a WHOIS for a nick. The reply arrives asynchronously as numeric lines in the network's server buffer (:server:<networkId>, which list_buffers does not list) — read it afterward by passing the returned serverBuffer string to recent_messages.

connect_network / disconnect_network (read-write) ​

Connect (optionally force a fresh reconnect) or disconnect a configured network. Connection is asynchronous — watch the server buffer for registration. connect_network returns locked-down when the instance admin does not allow that network's host, rather than tearing the connection down and failing.

get_topic (read) / set_topic (read-write) ​

Read or change a joined channel's topic. set_topic requires an explicit topic, and an empty string clears the topic — it always writes, so use get_topic to read. It needs the usual channel privileges (+o or a -t channel); the server may reject it.

get_mode_list (read-write) ​

Fetch one of a channel's lists fresh from the server — bans (b), ban exceptions (e), invite exceptions (I), or quiets (q, on networks that have a quiet list) — and wait for the answer: { ok: true, channel, letter, entries: [{ mask, setBy, setAt }] }. A server that refuses (often 482 for e and I when you aren't an operator) comes back as { ok: false, error: "refused", numeric, text }. The reply goes only to the caller; nothing lands in the server buffer.

set_channel_modes (read-write) ​

Change a channel's modes: changes is a list of { sign: "+" | "-", letter, param? }. Each letter is checked against the modes the network advertises, and a param is required exactly when the mode takes one (a mask, a limit, a key). -k may leave it out: the stored key is used, or *. The changes go out as the fewest MODE lines the network's MODES allows. A refusal from the server (482, 467, 478) lands in the channel as an error line.

Wire format ​

Transport is MCP's Streamable HTTP profile: a single POST /mcp with a JSON-RPC 2.0 envelope. Each request reauthenticates via the Authorization header — there is no Mcp-Session-Id state on the server side.

We implement four methods:

  • initialize — capability handshake. Returns protocolVersion, capabilities: { tools: {} }, and serverInfo.
  • notifications/initialized — client ack. No response.
  • tools/list — enumerates verbs the token can invoke.
  • tools/call — invokes a verb by name with arguments.

Verb-level failures (insufficient scope, unknown network, IRC offline) are returned as a tool result with isError: true and a structured payload, not as JSON-RPC errors. JSON-RPC errors are reserved for protocol problems: malformed envelope, unknown method, missing tool name.

Examples ​

Claude Code ​

Claude Code's MCP client speaks streamable HTTP natively, so the setup is a single command — no stdio bridge needed. To sign in through your browser, add the server without a token:

sh
claude mcp add --transport http lurker https://<your-lurker>/mcp

In a new session, run /mcp, pick lurker and choose Authenticate. Your browser opens Lurker's approval page; approve it and the tools load. If sign-in fails, check that https://<your-lurker>/.well-known/oauth-authorization-server gives your public URL as its issuer. Behind a reverse proxy that doesn't pass it through, set PUBLIC_BASE_URL (see OAuth for third-party clients).

To use an API token instead, pass it as a header. Claude Code doesn't offer OAuth sign-in for a server that has an Authorization header set:

sh
claude mcp add --transport http lurker https://<your-lurker>/mcp \
  --header "Authorization: Bearer <your-token>"

claude mcp list confirms the entry. MCP servers load at session start, so restart Claude Code (start a new session) before the Lurker tools appear in tool calls. To remove it later, claude mcp remove lurker.

Claude Desktop ​

Add an entry under mcpServers in your Claude Desktop config (the exact path depends on your OS; see Claude Desktop's docs). Since the transport is HTTP, use the mcp-remote bridge to expose it as a local MCP stdio server:

json
{
  "mcpServers": {
    "lurker": {
      "command": "npx",
      "args": [
        "mcp-remote",
        "https://<your-lurker>/mcp",
        "--header",
        "Authorization: Bearer <your-token>"
      ]
    }
  }
}

After restarting Claude Desktop, the Lurker tools appear in the tool picker and can be invoked directly.

curl roundtrip ​

sh
# Initialize.
curl -X POST https://<your-lurker>/mcp \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'

# List the eight tools.
curl -X POST https://<your-lurker>/mcp \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'

# Read the last 20 messages in #lurker on network 1.
curl -X POST https://<your-lurker>/mcp \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc":"2.0","id":3,"method":"tools/call",
    "params":{"name":"recent_messages",
              "arguments":{"networkId":1,"target":"#lurker","limit":20}}
  }'

Auto-notes — reference integration ​

The repo includes a small standalone web app at integrations/autonotes/ that uses the MCP API to propose updates to your nick-notes from recent channel chatter. It's useful on its own, but more importantly it serves as a worked example of consuming Lurker's MCP API from outside the server. If you're building your own integration, lib/mcpClient.js shows the wire protocol end-to-end and lib/agent.js shows how to wrap MCP verbs as Anthropic tool definitions for an agentic loop.

Revocation ​

Revoke a token from the settings pane at any time. Soft revocation: the row stays in the listing with a revoked marker (so you can see whether a specific name was previously issued and torn down). The token immediately stops authenticating against /mcp. There is no token rotation flow — revoke the old one and mint a new one.

An MCP client that signed in with OAuth is revoked under Settings → Authorized apps. Its token stops working at once, and the client has to be approved again.

What's not here ​

Intentionally outside the scope of this surface:

  • Streaming subscriptions (subscribe_events, push notifications over MCP). Agents that want to react to live activity should poll recent_messages with a before/since cursor on a schedule.
  • Channel membership (join_channel, part_channel). The operator manages this through the browser UI; agents that join channels without the operator noticing are a footgun.
  • Per-message read state (mark_read, get_unread). Defer until a concrete agent needs it.
  • WHOIS / channel-member listings. Derive from recent_messages; IRC member lists are unstable anyway.
  • REST endpoints for non-MCP HTTP clients. MCP is the only HTTP surface here. If you need a non-MCP HTTP integration, file an issue describing the use case.

Released under the MPL-2.0 License.