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.
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.
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}
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.
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.
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.