Documentation

sulfur.sh is a lightweight event review layer for teams that want simple project updates, issue rollups, and optional AI assistance without replacing their existing hosting or logs.

Start here
Quickstart
Create a project, send your first event, and see how the dashboard and AI review flow fit together.
Reference
Ingest API
HTTP endpoints for sending events — headers, severity levels, and response format.
Reference
MCP Tools
The tools Claude uses to read your events — what each one returns and when to use them.

What runs in production

The live product is a Cloudflare deployment made up of one Pages site, three Workers, one D1 database, one R2 bucket, and two KV namespaces.

sulfur-dashboard Astro site on Cloudflare Pages, serving the marketing site, docs, login page, and dashboard shell at https://sulfur.sh.
sulfur-ingest Public ingest worker behind https://sulfur.sh/e/{INGEST_TOKEN}/{TOPIC}. Validates ingest tokens, enforces plan limits, creates topics, stores signatures, and writes payloads to R2.
sulfur-api Dashboard API at https://api.sulfur.sh. Handles register/login/logout, token creation and revocation, usage reads, topic summaries, and retention cron work.
sulfur-mcp MCP server at https://mcp.sulfur.sh. Exposes read-only event inspection tools for Claude, Cursor, and other MCP clients.
Shared storage sulfur-events (D1), sulfur-payloads (R2), plus KV namespaces CONFIG and RL.

Sending events

Every event is a POST to a URL that encodes your token and a topic name you choose. The body can be plain text or JSON — sulfur handles both.

POST https://sulfur.sh/e/{INGEST_TOKEN}/{TOPIC}
INGEST_TOKEN Your slf_pub_… token from the dashboard. Never use an MCP token here.
TOPIC A name for the event stream, e.g. my-worker or checkout-cron. Created automatically on first use.
Content-Type Optional. Set to application/json for structured JSON bodies. Omit to send a plain text string — both are valid.
X-Severity Optional. One of: critical · error · warn · info · debug. If omitted, severity is inferred automatically from keywords in the body — "error", "failed", "panic", "warn", etc.

The body can be a plain text string or a JSON object — sulfur handles both. For JSON, the message or error field is used as the event summary that Claude reads. For plain text, the full string is used. All other JSON fields are stored and accessible via event_detail on the paid plan.

A 202 Accepted with a JSON body:

{
  "id": "01JT4K...",           // unique event ID
  "signature": "a3f9c2d1e4b5", // deduplication hash of this payload
  "received_at": 1746835200    // Unix timestamp (ms)
}

If the same error fires repeatedly, the signature stays the same — that's how Claude can say "47 occurrences" instead of listing every event individually.

Free 200 events / day · 60 events / min burst
Paid 2,000 events / day · 300 events / min burst · 25 projects per paid unit

Over-limit requests return 429 Too Many Requests. No overages — events are dropped, not billed.

Any HTTP client works. The simplest form is a plain curl with no headers — good for shell scripts, cron jobs, and one-off diagnostics. Use JSON when you want Claude to see structured fields.

curl — plain text (simplest)

curl -X POST https://sulfur.sh/e/$TOKEN/my-app \
  -d "DB connection failed after 3 retries"

curl — JSON body (structured)

curl -X POST https://sulfur.sh/e/$TOKEN/my-app \
  -H "Content-Type: application/json" \
  -H "X-Severity: error" \
  -d '{"message":"DB connection failed","latency_ms":5200,"host":"db-1"}'

PowerShell — plain string

Invoke-RestMethod -Uri "https://sulfur.sh/e/$env:SULFUR_TOKEN/my-job" `
  -Method POST -Body "Scheduled job failed: exit code 1"

PowerShell — JSON body

$body = @{ message = "Scheduled job failed"; exit_code = 1 } | ConvertTo-Json
Invoke-RestMethod -Uri "https://sulfur.sh/e/$env:SULFUR_TOKEN/my-job" `
  -Method POST -ContentType "application/json" -Body $body

Cloudflare Worker (fetch)

async function logToSulfur(env, severity, payload) {
  await fetch(`https://sulfur.sh/e/${env.SULFUR_TOKEN}/my-worker`, {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
      'x-severity': severity,
    },
    body: JSON.stringify(payload),
  }).catch(() => {/* fire and forget */});
}

Python (Lambda / scripts)

import requests

requests.post(
    f"https://sulfur.sh/e/{os.environ['SULFUR_TOKEN']}/my-function",
    json={"message": "Task failed", "step": "validation", "exit_code": 1},
    headers={"X-Severity": "error"},
    timeout=5,
)
# or plain text — no headers needed:
requests.post(f"https://sulfur.sh/e/{os.environ['SULFUR_TOKEN']}/my-function",
    data="Task failed during validation")

GitHub Actions (bash step)

- name: Log deploy result
  if: failure()
  run: |
    curl -X POST https://sulfur.sh/e/${{ secrets.SULFUR_TOKEN }}/deploy \
      -H "Content-Type: application/json" \
      -d '{"message":"Deploy failed","ref":"${{ github.ref }}"}'

What Claude can see

When you connect an MCP token to Claude Desktop or Cursor, Claude gets access to four tools. Claude chooses which to call based on your question — you do not have to specify them.

list_topics
Returns all topic names for your account, along with the total event count and last-seen timestamp for each. Claude calls this when you ask broad questions like "what's been logging today?" or "do I have any active topics?"
topic_summary
Returns the distinct error signatures for a topic within a time window — each with occurrence count, severity, last seen, and a short summary. This is the core tool for "any errors in the last hour?" type questions. Available on both free and paid plans.
list_events
Returns individual event IDs, signature prefixes, severities, and timestamps for a topic. Use it when you need a filtered event list before drilling into a specific signature.
event_detail
Returns the canonical payload sample for a specific event signature, plus metadata such as severity, occurrence count, and first/last seen timestamps. Full payload access requires the paid plan; free accounts get the summary only.
Example questions to ask Claude
"Any failures in my-worker in the last 6 hours?"
"What's the most frequent error today?"
"Show me the full payload for signature abc123."
"Is anything degraded right now? Check sulfur."
"How many errors has checkout-cron had this week?"

MCP configuration

Create an MCP token from the dashboard (slf_mcp_…), then add it to your AI client config. The MCP server is hosted at https://mcp.sulfur.sh.

Claude Desktop — edit ~/Library/Application Support/Claude/claude_desktop_config.json:

{
  "mcpServers": {
    "sulfur": {
      "url": "https://mcp.sulfur.sh",
      "transport": "streamable-http",
      "headers": {
        "Authorization": "Bearer slf_mcp_YOUR_TOKEN_HERE"
      }
    }
  }
}

Cursor — add to .cursor/mcp.json in your project root:

{
  "mcpServers": {
    "sulfur": {
      "url": "https://mcp.sulfur.sh",
      "headers": { "Authorization": "Bearer slf_mcp_YOUR_TOKEN_HERE" }
    }
  }
}

Ingest vs MCP tokens

sulfur issues two kinds of tokens and they should never be mixed up.

slf_pub_… Ingest token. Goes in your Worker, Lambda, or script. Authorizes writing events. Treat like a write-only API key.
slf_mcp_… MCP token. Goes in Claude Desktop or Cursor config. Authorizes reading events. You can revoke it without touching your production code.