Skip to main content
Glama
bicho-fyi

Bicho

Official
by bicho-fyi

Bicho

Version Cloudflare Workers TypeScript Node.js 24 License

Bicho is a self-hosted error tracker on Cloudflare Workers. Send exception and error logs over OTLP/HTTP, inspect grouped issues through the API or MCP, and keep the captured evidence in your own D1 database and R2 bucket.

It accepts OTLP JSON and binary protobuf logs. You can use it without a dashboard or an OpenTelemetry Collector.

SDKs

For other languages, use an OpenTelemetry logs exporter with OTLP/HTTP JSON or protobuf.

Related MCP server: Telebugs MCP Server

Deploy to Cloudflare

Deploy to Cloudflare

The button copies the public repository into your account, provisions D1 and R2, and sets up Workers Builds. Enable R2 in your Cloudflare account first. When prompted for BICHO_ADMIN_TOKEN, supply a unique token generated with openssl rand -hex 32. Save it for API and MCP access.

The deploy script applies database migrations before deploying the Worker. Your instance is available at its workers.dev URL; preview URLs are disabled.

After deployment:

  1. Connect your MCP client to https://<your-worker>/mcp using the admin bearer token.

  2. Call create_project with { "name": "My app" }. It creates the database record and returns a generated project ID, OTLP endpoint and ingest headers. No local SQL or direct database access is needed.

  3. Save the returned headers and configure your application's exporter or SDK. Use secret: true only for backend projects.

For a custom domain, configure the route and disable workers_dev when you no longer need that alternate endpoint. See Cloudflare's deploy-button documentation for platform setup details.

This package contains the Worker source, not a browser SDK or a standalone Node server. Installing it from npm does not create Cloudflare resources; use the deployment flow above to host an instance.

Local setup

Use Node 24 and npm. The Worker needs the DB (D1) and PAYLOADS (R2) bindings in wrangler.jsonc. Local development uses local storage; replace the placeholder D1 ID before deploying to an existing Cloudflare database.

For local development, copy .dev.vars.example to .dev.vars and set BICHO_ADMIN_TOKEN to a long random token. Use a Cloudflare secret for hosted instances. Never put the token in D1, frontend code or wrangler.jsonc. Admin endpoints return 503 if it is missing or empty.

nvm use
npm ci
npm run check
npm test
npx wrangler d1 migrations apply DB --local
npm run dev

Register a project through MCP at http://localhost:8787/mcp, or use POST /api/projects, with your local admin token.

For offline SQL generation, run npm run project -- 1 "My app" http://localhost:8787. It writes SQL and ingest credentials under ignored _temp/ without connecting to Cloudflare. Apply _temp/project-1.sql to the local database before using the generated otlp.endpoint and otlp.headers.

project.example.json and project.example.sql show the generated formats with placeholder credentials. The JSON is not an importable configuration, and editing it does not register a project. Generate your own credentials rather than using the placeholders.

Browser projects use a public ingest key without a secret. --secret requires an additional ingest secret for backend clients. Generated OTLP headers contain X-Bicho-Key and, when required, X-Bicho-Secret. A secret never grants admin access. Do not put it in a browser bundle.

Manual hosted setup

  1. Provision a fresh D1 database and private R2 bucket. The default names in wrangler.jsonc are bicho for the Worker and database, and bicho-events for the bucket. Replace the all-zero D1 ID with your database ID and adjust names as needed, retaining the DB/PAYLOADS binding names and nodejs_compat. The migration command uses the DB binding so you can choose a different database name.

  2. Keep migrations_dir pointed at migrations. The deploy script applies migrations/0001_core.sql to the database.

  3. Configure BICHO_ADMIN_TOKEN as a Worker secret. Set CORS_ORIGINS only for trusted browser origins; dashboards should keep the admin credential on their server rather than ship it in a public bundle.

  4. Deploy through Workers Builds or your CI/CD with npm run deploy. Use the workers.dev URL or configure a custom domain and disable workers_dev. Check that the hourly Cron Trigger is attached.

  5. Register a project through MCP or the API on the hosted instance. Local project records are separate from the hosted database.

The initial migration is for a fresh database. Editing it does not upgrade a database where it has already run. Never reset populated storage to reapply it.

Register a project

On a running local or hosted Worker, call MCP create_project with { "name": "My app" }, or send the same JSON to POST /api/projects with the admin bearer token. The optional hourly_limit accepts an integer from 1 to 1000000 (default 1000). Set secret: true to require an additional backend ingest secret.

The API returns HTTP 201 with { project: { id, name, hourly_limit }, otlp: { endpoint, headers } }. The server generates a unique numeric-string ID and random ingest credentials. Save the returned headers securely: project listings do not expose credentials, and only the ingest secret's hash is stored. IDs cannot be supplied, and registration never replaces or deletes existing projects. Each call creates a new project, even with the same name; check list_projects before retrying after a lost response.

OpenTelemetry ingestion

Configure a logs exporter for your application language. With exporters that honor the standard environment variables:

export OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=https://your-bicho-worker.example/v1/logs
export OTEL_EXPORTER_OTLP_LOGS_PROTOCOL=http/protobuf
export OTEL_EXPORTER_OTLP_LOGS_HEADERS=X-Bicho-Key=YOUR_PUBLIC_KEY

For a project provisioned with --secret, include both headers:

export OTEL_EXPORTER_OTLP_LOGS_HEADERS="X-Bicho-Key=YOUR_PUBLIC_KEY,X-Bicho-Secret=YOUR_INGEST_SECRET"

Use the generated otlp object, not these placeholder keys. http/json is also supported where the exporter implements it. Use the logs-specific endpoint above so it is used as-is; a generic OTLP base endpoint normally has /v1/logs appended by the exporter. Bicho does not accept gRPC, /v1/traces or /v1/metrics.

Install an OpenTelemetry logs SDK, exporter or logging bridge for your application language. Environment variables configure the exporter; your instrumentation must capture exceptions and emit them as logs. Trace events alone are not enough.

POST /v1/logs identifies the project from X-Bicho-Key. service.name and other resource attributes are metadata, never authentication. Browser keys are public and ingest-only; backend projects may also require X-Bicho-Secret. Neither grants read, state-change, map-upload or MCP access. Cross-origin ingestion supports preflight; read/admin CORS remains separate.

The payload must follow OTLP's protobuf JSON mapping or binary protobuf schema. Exceptions use exception.type, exception.message and exception.stacktrace attributes. Bicho accepts exception records regardless of severity, plus plain ERROR/FATAL logs; it rejects other records through partial-success responses rather than acting as a general log archive.

Full success returns HTTP 200 with an empty export response ({} for JSON). Partial acceptance returns HTTP 200 with partialSuccess.rejectedLogRecords and a diagnostic message. Exporters must not retry a populated partial-success response. A whole-request failure uses the OTLP status shape and the request encoding, not the REST read API's {error} shape. Quotas/storage failures can return 429/503 before any accepted records; respect Retry-After.

For retry-safe occurrence counts, provide a stable log.record.uid attribute for each emitted record and preserve it across retries. Bicho derives a project-scoped event ID from that UID. Without one, Bicho generates an ID and a resent batch can count as new occurrences; trace/span correlation IDs are not deduplication keys. Equal error messages still group into the same issue but represent separate occurrences.

Bicho preserves sanitized resources, instrumentation scopes, timestamp precision and raw stack text. The search index uses millisecond timestamps. Recognized stack formats also produce frames for grouping; other formats remain readable as text. Source-map enrichment needs matching debug IDs, which ordinary OTLP stack attributes do not supply.

Requests are limited to 2 MiB on the wire and after decompression, 100 records per batch, 32 nesting levels and 20,000 structural values. A normalized record is limited to 1 MiB. Invalid known fields reject the batch before storage; ordinary low-severity logs use partial rejection. Derived stacks stop at 200 input lines or 100 recognized frames; the sanitized raw stack remains available.

The bundled OTLP schemas and their upstream licenses are documented in src/vendor/otlp.

Send a JSON record

Use a dedicated test project for this example and replace the endpoint and key with its generated values. The fixed occurrence UID lets you resend the request to check deduplication without adding another event.

curl --fail-with-body -sS https://your-bicho-worker.example/v1/logs \
  -H 'Content-Type: application/json' \
  -H 'X-Bicho-Key: YOUR_PUBLIC_KEY' \
  --data-binary '{"resourceLogs":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"smoke-test"}}]},"scopeLogs":[{"logRecords":[{"severityNumber":17,"body":{"stringValue":"Smoke-test failure"},"attributes":[{"key":"exception.type","value":{"stringValue":"TestError"}},{"key":"log.record.uid","value":{"stringValue":"bicho-otlp-smoke-1"}}]}]}]}]}'

Verify the stored event through authenticated issue/event reads, not just HTTP success. Applications may send directly or through an OpenTelemetry Collector; a Collector is optional and is not bundled with Bicho. See the OTLP specification and exception log conventions.

API contract and schemas

docs/openapi.json documents the HTTP routes, authentication, parameters and response shapes in OpenAPI 3.1.1 format. Import it into your API client and set your deployment hostname. The Worker does not host a documentation UI or serve this file.

MCP tool input schemas use Zod in src/mcp.ts. An authenticated MCP client receives their JSON Schema through tools/list; no separate public schema route is needed. OpenAPI describes the /mcp transport, while Zod describes the individual tool arguments. OTLP ingest requires X-Bicho-Key and, for secret-enabled projects, X-Bicho-Secret.

Contract tests compare routes and responses with OpenAPI. The handlers also validate cross-field rules, including time ranges, cursor scope and source-map segments.

To validate the complete document independently, with Python's uv available:

uvx --from openapi-spec-validator openapi-spec-validator docs/openapi.json

Development

Run npm run check and npm test before submitting changes. Vitest runs Node tests and local Workers-runtime tests with isolated D1/R2 storage. Use npm run test:node, npm run test:workers or npm run test:watch for a narrower run. Tests do not load your .dev.vars or contact Cloudflare.

src/index.ts owns Hono routes, authentication middleware, CORS, security headers, and HTTP errors. src/core.ts owns ingestion, grouping, and storage queries. src/otlp.ts owns OTLP ingestion and mapping; src/http.ts supplies shared HTTP errors and JSON-object validation. Add routes beside the existing ones, reuse the project/admin-auth middleware, and keep fallback handlers last. Keep every storage lookup project-scoped.

Global headers apply to JSON, streamed R2 payloads, preflights, and errors: restrictive CSP, frame denial, permissions restrictions, MIME sniffing protection, referrer protection, and no-store. HTTPS responses receive HSTS for the current host only, without preload or subdomain enforcement. Each response includes a server-generated X-Request-ID; internal error logs use that ID and omit credentials, queries, and event bodies.

For a separate dashboard, set CORS_ORIGINS in wrangler.jsonc, for example https://dashboard.example.com,http://localhost:5173. CORS is browser policy, not authorization. The API header policy assumes JSON-only responses; give future HTML routes their own CSP rather than weakening the global policy.

See AGENTS.md for contributor guardrails. Ingest credentials never authorize reads or mutations, events are append-only, and MCP tools use the authenticated API.

HTTP API

Send Authorization: Bearer <BICHO_ADMIN_TOKEN> on every read, issue PATCH, source-map PUT and MCP request. The Worker compares SHA-256 digests constant-time; ingest keys and secrets are not admin credentials. REST responses are JSON; errors use { "error": "..." }.

  • GET /api/:project_id/issues: { issues, has_more, next_offset, next_cursor, provenance }

  • GET /api/:project_id/issues/:id: { issue, events, has_more, next_offset, next_cursor, provenance }

  • GET /api/:project_id/events/:id: full event JSON

  • GET /api/:project_id/releases: { releases, next_offset }, derived from stored events

  • GET /api/projects: project IDs, names, and hourly quotas, without ingest credentials

  • POST /api/projects: register a project; returns its generated ID and OTLP ingest credentials

  • GET /api/:project_id/events: searchable event metadata

  • GET /api/:project_id/issues/:id/history: recorded status transitions

  • GET /api/:project_id/stats: deterministic counts, UTC buckets, breakdowns, and representative event metadata

  • GET /api/:project_id/processing: project enrichment counts and explicitly global processor health

  • PUT /api/:project_id/sourcemaps/:debug_id: register an immutable Source Map v3 JSON object

  • GET /api/:project_id/sourcemaps/:debug_id: source-map metadata, not source contents or storage keys

  • PATCH /api/:project_id/issues/:id: { "status": "resolved" } returns { issue }

PATCH accepts only status: resolved, ignored, muted, or unresolved to reopen. It changes no events or payloads. New events do not automatically reopen hidden issues. There are no DELETE endpoints.

Actual status changes record history transactionally; repeated identical PATCH requests add nothing. History actor admin means the shared administrator credential, not a named individual.

Lists accept limit (1..100, default 50) and offset (0..1000000). Issue lists default to status=unresolved; another status or status=all includes hidden issues. They also accept q, environment, release, level and issue_id. Event/statistics reads may filter the current issue status but do not default to unresolved. Filters match a stored event in the issue; event_count remains the all-time total. IDs and every lookup are scoped to the project. Issue IDs are lowercase SHA-256 fingerprints; event IDs are normalized to 32 lowercase hexadecimal characters on ingestion.

Issue/event investigation reads accept UTC RFC3339 from (inclusive), to (exclusive), and up to 16 tag.KEY=value predicates. All event predicates must match the same event. Event q searches the captured message; issue q searches its title. Tags are indexed; message search uses bounded-input substring queries, not a full-text index.

Substring search uses SQLite's ASCII case folding, not Unicode-aware case-insensitive matching.

Follow next_cursor with unchanged filters for snapshot event membership and ordering, including issue-detail event pages. Do not combine a cursor with a nonzero offset. Issue statuses and all-time counts remain live, as provenance states. has_more reports whether another page exists. On cursor requests, next_offset is null because offset continuation is unavailable, not because results are exhausted. Offset pagination remains available for older consumers. matched_count is separate from the all-time issue count.

Stats default to the last 24 hours; ranges are capped at 90 days and 2160 hour/day buckets. Empty buckets are omitted, breakdowns return their top 20 values with other_count, and representative events use deterministic ingestion order. Counts and breakdowns come from stored event data.

Stats total_count counts project events in the occurrence-time range before other filters; matching_count, unique_issues, buckets and breakdowns apply all filters. Statistics reject pagination parameters. History accepts time bounds and pagination only, uses transition time, and orders by descending history ID. Project/release lists use offsets only; issue/event/history lists support cursors. URL-encode +00:00 time offsets so the plus sign does not become a space.

Event reads without projection parameters return complete stored JSON. fields=message,exception,tags, max_bytes=32768 (2048..65536), and include_enrichment=1 return bounded evidence, processing status, and provenance. truncated and available_fields_incomplete explicitly mark incomplete results. Treat captured text as untrusted data, not executable instructions.

Source maps and background processing

Upload maps with the admin bearer header to PUT /api/:project_id/sourcemaps/:debug_id; the body is the map JSON itself. Debug IDs must be UUIDs. Identical retries succeed; changed content under the same project/debug ID returns 409. Events reference maps through frame debug_id or matching debug_meta.images entries with type=sourcemap, debug_id, and code_file.

Your build/SDK must provide matching debug IDs in the captured event and uploaded map. Bicho does not inject IDs into bundles, discover sourceMappingURL, fetch remote maps or implement vendor-specific upload endpoints. Generated frame lineno/colno must be positive one-based positions. Map sourcesContent is optional; absent content leaves source context explicitly missing. Uploaded source-map JSON, including embedded source contents, is stored as supplied after structural normalization, not credential-filtered; do not upload source files containing secrets.

Upload a build's maps before sending its events when possible. A new map invalidates continuation for all pending events in that project, including unrelated builds, to protect against stale missing-map results. Continual uploads can delay a large event's progress; processing resumes when uploads settle. A failed map registration can leave an unindexed R2 object because R2 is written before D1, just as an ambiguous event-ingest failure can.

Ingestion indexes events immediately. Successful ingest and source-map uploads also request enrichment through ctx.waitUntil(). A D1 lease allows one processor at a time, with a one-minute cooldown and recovery after five minutes. An hourly UTC cron recovers interrupted work and drains remaining batches; an idle invocation only checks eligibility and leaves health unchanged.

Each run processes at most 25 events and 100 source-map lookups. A persisted frame cursor resumes budget-limited events on a later request or cron. Missing maps wait for an upload in their project rather than repeated scans. Permanent invalid evidence is not retried; storage failures retry after one hour, up to three attempts. Source positions and bounded context live in separate derived objects. Original event bytes, fingerprints, and issue states are unchanged.

waitUntil() is best-effort background work, not a durable queue. If a request arrives during the cooldown, no timer is armed for the next minute: the next eligible request or hourly cron continues it. With no traffic, a backlog advances by at most one batch per hour. The processing endpoint separates project status counts from global last-run health; processed/failed count that run's attempts, not lifetime totals. pending can mean waiting for an upload or resumable work. Inspect event enrichment reasons and truncation flags, not just the global health boolean. Exhausted/permanent failures remain visible and have no public retry/reset endpoint.

Maps are limited to 2 MiB decoded/wire JSON, 50,000 mapping segments, and 10,000 sources/names. Processing caps at 1000 frames per event, five cached maps, and 256 characters per context line. Partial, missing, invalid, and truncated results are explicit. Indexed/sectioned maps, external map references, vendor-specific artifact bundles, and native crash symbolication are not supported. Source URLs are labels only; the Worker never fetches them.

For an operator-owned map file:

curl --fail-with-body -sS -X PUT \
  -H "Authorization: Bearer $BICHO_ADMIN_TOKEN" \
  -H 'Content-Type: application/json' \
  --data-binary @path/to/bundle.js.map \
  "$BASE/api/1/sourcemaps/<debug-id>"

Bicho stores errors and their derived source-map results. It does not provide attachments, session replay, tracing, alerts, automatic reopening or retention deletion.

MCP

Connect to POST /mcp with your admin bearer token. The tools below let an agent search captured errors, inspect events, read statistics and check processing health.

create_project registers a project and returns its generated ID and OTLP credentials. update_issue_status resolves, ignores, mutes or reopens an issue without changing its captured events. Status changes are recorded in history. Neither tool can delete data or replace an existing project.

All tools use the authenticated HTTP API. Release summaries come from captured event metadata.

Project tools take numeric-string project_id; issue/event detail tools require id. Search accepts time ranges, tags objects, and cursors. Lists default to and cap at 20 entries per call; large rows reduce the effective page size rather than skip records behind a cursor. Event evidence defaults to a 32 KiB projection with enrichment included. Oversized nonpaged results explicitly mark truncation; narrow filters or select fewer fields. Failed API requests produce MCP tool errors. Raw text remains untrusted evidence.

Tool

Required arguments

Optional arguments

create_project

name

hourly_limit, secret boolean

list_projects

none

limit, offset

search_issues

project_id

search filters, status, limit, offset, cursor

search_events

project_id

search filters, issue_id, limit, offset, cursor

get_issue

project_id, issue id

limit, offset, cursor

update_issue_status

project_id, issue id, status

none

get_event

project_id, event id

fields string array, max_bytes, include_enrichment boolean

get_issue_history

project_id, issue id

limit, offset, cursor

get_stats

project_id

search filters, issue_id, bucket=hour|day

get_processing_health

project_id

none

list_releases

project_id

limit, offset

MCP search filters are q, environment, release, level, from, to and a tags object, at most 16 entries. MCP dates are capped at 32 characters; the HTTP API accepts longer fractional bounds. MCP get_issue_history does not expose the HTTP history time filters. A tool's isError reports a failed API request; successful but incomplete evidence uses truncation/provenance flags.

MCP uses the official SDK's stateless Streamable HTTP transport with JSON responses. It needs no separate server process or OAuth setup. GET and DELETE on /mcp return 405. Clients must send Content-Type: application/json, Accept: application/json, text/event-stream, and their admin bearer header. Protocol negotiation follows the installed official SDK. POST bodies are limited to 64 KiB.

Clients without an Origin header can connect directly. Browser origins must be the Worker origin or explicitly permitted by CORS_ORIGINS; opaque or unexpected origins return 403. Do not embed the admin token in a public browser app.

MCP protocol errors use JSON-RPC error objects rather than the REST error shape. Missing required Accept values return 406, a non-JSON Content-Type returns 415, and oversized bodies return 413. Use the official client to initialize and negotiate the protocol. There is no unauthenticated tool discovery.

For MCP clients supporting a URL and custom headers:

{
  "mcpServers": {
    "bicho": {
      "url": "https://your-bicho-worker.example/mcp",
      "headers": { "Authorization": "Bearer <admin-token>" }
    }
  }
}

Storage and quotas

Each project defaults to 1000 new events per UTC hour. Set hourly_limit when creating a project through the API or MCP, or pass --hourly-limit=1000 to the local generator. D1 enforces quotas atomically, including concurrent requests. Duplicate occurrence UIDs do not consume quota; rejections before acceptance return 429 and Retry-After.

Known credential keys, Basic/Bearer strings and credential URL/query values are filtered before indexing and storage. This cannot remove arbitrary secrets or PII embedded in prose or source code. Send only data you intend administrators and connected agents to read. Stored evidence is compact and gzip-compressed in R2.

R2 is written before the D1 index. An ambiguous D1 failure can leave an unreferenced object; Bicho retains it rather than risk deleting committed evidence. Grouping uses exception type/message and the most recent recognized stack location. It does not normalize changing IDs/URLs or infer root causes.

Operations

Bot and abuse protection

Project IDs and browser ingest keys are public, not bot protection. Ingest quotas cap accepted events, not incoming requests or Worker cost. Configure Cloudflare WAF and rate limiting rules on your production custom domain, scoped to ingest, admin API and MCP paths as needed; available controls depend on your plan. Disable unused workers.dev and preview URLs to avoid alternate entry points. Avoid interactive challenges on SDK/API routes: automated clients cannot complete them. Tune limits for legitimate error bursts and shared IPs. This repo does not configure edge rules.

HTTP status

Meaning

400

Invalid ID, query, OTLP payload, map, state body or MCP protocol input

401

Missing/invalid admin token; REST errors include the bearer challenge

403

Invalid ingest credentials or rejected MCP Origin

404

Project/resource not found within the requested scope

405

Method not implemented; GET/DELETE MCP and DELETE event/issue are unsupported

406

MCP client does not accept both JSON and event-stream media types

409

An immutable source-map debug ID already has different content

413

A request or stored-output byte limit is exceeded

415

Unsupported body compression or MCP Content-Type

429

Project UTC-hour ingest quota exceeded

500

Internal read/storage or processing failure

503

Ingestion unavailable, admin secret unconfigured or indexed payload unavailable

Cloudflare account and platform limits apply in addition to Bicho's per-project quotas and payload limits.

Monitor GET /api/:project_id/processing, Worker error logs and ingestion 429/5xx rates. Logs contain request IDs and error names, not raw captured bodies. Data grows without retention deletion, including separate derived objects and unreferenced uploads retained after ambiguous failures. Configure D1/R2 backups and capacity monitoring yourself; this core provides no cleanup or restore job.

Acknowledgements

Bicho uses the OpenTelemetry Protocol (OTLP) for error-log ingestion over HTTP JSON and binary protobuf. Vendored schemas and their upstream licenses are listed in ACKNOWLEDGEMENTS.md.

License

Copyright 2026 Alex Zappa (FreshJuice). Licensed under the Apache License, Version 2.0. See LICENSE.

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    D
    maintenance
    MCP server implementation that enables LLMs to interact with Rollbar error tracking data, allowing users to list and analyze errors, view occurrences, track deployments, and access project information.
    13
    18 npm
    6
    MIT
  • F
    license
    Not graded
    quality
    C
    maintenance
    An MCP server that enables AI agents to retrieve and analyze error reports from a self-hosted Telebugs instance. It provides tools for listing projects, searching error groups, and fetching detailed reports directly from the Telebugs SQLite database.
    4
    -
  • A
    license
    Not graded
    quality
    D
    maintenance
    Hosted MCP server that exposes your self-hosted Bugsink error tracker as tools for LLMs, enabling issue management, release tracking, and stacktrace retrieval via chat prompts.
    142 npm
    MIT
  • -
    license
    Not graded
    quality
    Not graded
    maintenance
    MCP server that parses stack traces and logs to generate deduplicated issue drafts with severity, repro steps, and owner guesses.
    -