Troubleshooting
Error catalog for logbook, organized by symptom. Each entry gives the exact
error text (quoted from source), what it means, and the fix. Error strings are
verbatim from lib/auth.ts (resolveMcpAuth), lib/billing.ts, lib/db/ queries.ts, app/api/mcp/route.ts, and app/api/webhooks/github/route.ts.
MCP auth errors come back as an HTTP response with a JSON body
{ "error": "<message>", …extra }. Tool-level domain errors come back inside a
successful tool result as text prefixed with Error: .
Connection and auth (MCP)
401 — no bearer token
Unauthorized. Pass a logbook API token: Authorization: Bearer lgbk_… (generate one in Settings).
Means: the request had no Authorization: Bearer <token> header (or it did
not match the Bearer <token> shape).
Fix: send Authorization: Bearer lgbk_…. Generate a token in logbook
Settings. See Connecting agents.
401 — token not recognized
Unauthorized. This logbook API token is not recognized — generate a fresh one in Settings.
Means: a bearer token was sent, but its SHA-256 hash matches no stored
token. The token is wrong, was revoked, or is from another environment. Only
the hash is stored, so a mistyped or truncated token lands here.
Fix: generate a fresh token in Settings and reconnect. Copy the whole
lgbk_… string — it is shown exactly once at creation.
401 — token has no scope (should never happen)
This token has no project or organization scope.
Means: a stored token row has neither project_id nor org_id set. Every
mint sets exactly one, so this indicates a corrupted row.
Fix: revoke that token and mint a new one; report it if it recurs.
400 — org key on the bare /api/mcp
This is an org key — it has no default project. Connect to /api/mcp/<project-slug>; this org has: <slug-a>, <slug-b>.
The JSON body also carries { "projects": ["<slug-a>", "<slug-b>"] } (or
(no projects yet) in the message when the org has none).
Means: you connected an org key to the bare /api/mcp endpoint. Org
keys carry only who may act; where comes from the project slug in the URL,
so the bare endpoint cannot resolve a project.
Fix: connect to /api/mcp/<project-slug>, choosing one of the listed
slugs. See Connecting agents.
404 — no project with that slug (org key)
No project with slug "<slug>" in this token's organization.
Means: an org key was used at /api/mcp/<slug>, but no project in this
token's organization has that slug. This is returned for both "slug does not
exist anywhere" and "slug exists but belongs to another org" — the ambiguity
is deliberate, so a token cannot probe for the existence of another tenant's
project.
Fix: verify the slug (check Settings / the org's project list). If you
believe the project exists, confirm it belongs to this token's org — a slug
in a different org yields the same 404 by design.
403 — legacy project token on a mismatched slug
This token is bound to the "<its-slug>" project and cannot act on "<slug>". Use /api/mcp/<its-slug> or the bare /api/mcp endpoint.
(When the token's project has been deleted, the message reads
This token is bound to a deleted project and cannot act on "<slug>". Use /api/mcp/<its-project-slug> or the bare /api/mcp endpoint.)
Means: a legacy project token (fixed to one project) was used at
/api/mcp/<slug> where <slug> is not its own project's slug.
Fix: use the bare /api/mcp endpoint, or /api/mcp/<its-own-slug>. To act
on a different project, mint an org key and target that project's slug.
Billing cap (upgrade_required)
This is the free-tier cap, not the lapsed-payment lock — for that see
402 below. Only create_item and
create_items are capped. Reads, updates, notes, tasks, documents, and
archiving are never capped — a capped org keeps full access to data it
already has. The gate returns a structured tool-result JSON (not an HTTP
error):
{
"error": "upgrade_required",
"message": "This org is on the free plan (50 logs; 50 used). Upgrade to keep logging: <app-url>/settings",
"items_used": 50,
"max_items": 50,
"upgrade_url": "<app-url>/settings"
}
What counts toward the 50: FREE_MAX_ITEMS = 50 — the lifetime count of
items created in the org, counted across all its projects (countOrgItems).
Deleting/archiving does not reduce it; it is a lifetime creation count. For
create_items the check is whole-batch and all-or-nothing: if
items_used + batch_size > max_items, the entire batch is refused and nothing
is created.
Plan semantics (effectivePlan / limitsFor):
- free —
maxItems = 50,maxProjects = 1. - pro — unlimited items and projects (
Infinity). $20/month for the first 5 seats, $5/seat after; a seat is a Clerk org member. - founder — unlimited, and never lapses (no subscription; status always treated as active).
- A lapsed payment no longer demotes. It used to be treated as
freefor limit purposes; it now triggers the lock below instead. During the grace window a lapsedproorg keeps fullprolimits.
Billing fails open when unconfigured: with no STRIPE_SECRET_KEY,
billingEnabled() is false and every entitlement check passes — no org is ever
capped with no way to upgrade. The gate is also skipped when the project has no
org (the migration/backfill window, orgId === null).
Fix: upgrade at the upgrade_url (Settings). Note the also-present project
cap error (thrown by assertCanCreateProject, surfaced in the web UI):
The <plan> plan is limited to <n> project(s). Upgrade to add more: <url>.
402 — payment required (locked)
{
"error": "payment_required",
"message": "This organization's payment failed and the 14-day grace period has ended. Logbook is locked until the subscription is current: <app-url>/settings",
"status": "past_due",
"upgrade_url": "<app-url>/settings"
}
Means: the org's subscription lapsed and its 14-day grace window has
expired. Unlike the cap, this is an HTTP 402 and it applies to every tool
— reads included. The check sits in handle() before the MCP handler is built,
so no tool is exempt. On the web the same state redirects every data page to
/locked.
This never happens to a free or founder org, or on a deployment with no
STRIPE_SECRET_KEY.
Fix: settle the subscription in Settings or at the /locked paywall. The
invoice.paid webhook clears the grace window and restores access immediately
— there is nothing to re-provision. If a payment has succeeded and the org is
still locked, the Stripe webhook is the thing to check: confirm
STRIPE_WEBHOOK_SECRET is set and that invoice.paid is among the subscribed
events, since without it the lock is never lifted.
create_item / create_items validation
These throw before anything is created; the message comes back as tool-result
text prefixed with Error: . In create_items each is additionally prefixed
item <i>: and suffixed — nothing was created (all-or-nothing).
Log without a size
`size` is required for logs (S/M/L/XL). If this decomposes into shippable pieces, pass kind:'epic' and omit size.
Means: kind was omitted or log, but no size was given. Every
MCP-filed log must be sized so shape queries work.
Fix: pass size (S/M/L/XL), or file it as kind:'epic' with no
size.
Epic with a size
Epics carry no size — omit `size` for kind:'epic' (containers aren't sized).
Fix: remove size from the epic entry; size the child logs instead.
Epic with a parent
Epics cannot have a parent — `epic`/`epic_index` are for child logs only (no nesting in v1).
Means: an entry with kind:'epic' also passed epic or epic_index. No
nesting in v1.
Fix: drop epic/epic_index from the epic entry.
epic and epic_index both set
Set `epic` OR `epic_index`, not both.
Fix: choose one — epic (an existing epic ref) or epic_index (an epic
entry's 0-based index in this batch).
epic_index errors (create_items)
item <i>: epic_index <idx> is out of range (0..<N-1>) — nothing was created.
item <i>: epic_index cannot point at itself — nothing was created.
item <i>: epic_index <idx> points at a non-epic entry — nothing was created.
Fix: point epic_index at a valid index whose entry is kind:'epic' and is
not the child itself.
epic ref resolves to a log (create_items)
item <i>: LOG-<n> is a log, not an epic — nothing was created.
Fix: children attach only to epics. Point epic at an epic ref, or file the
parent as an epic first.
Unknown label
create_item (via validateLabels):
Unknown label(s): <list>. Available: <list>. Create labels deliberately (Settings, or the create_label tool).
create_items (batch prevalidation):
Unknown label(s): <list> — nothing was created. Use list_labels / create_label first, then retry.
Means: a label was used that is not in the managed taxonomy. Labels are
normalized (lowercased/slugified) before comparison.
Fix: call list_labels, then create_label for any genuinely new label,
then retry. Reuse existing labels over near-duplicates.
Unknown assignee email
No logbook user with email <email>. Known users: <comma-separated list>
(or Known users: (none).)
Means: assignee_emails / assignee_email contained an email not on this
project's team roster (org members + anyone attributed in its items). Emails are
lowercased and trimmed before matching.
Fix: call list_team for valid emails; the user must be an org member or
already appear in the project's items.
update_item / update_items rejections
Same Error: text convention. In update_items a failing entry does not
stop the others — it appears as { ref, ok: false, error } in results.
kind is immutable
There is no kind parameter on update_item — kind cannot be changed after
creation. Attempting to "convert" a log to an epic (or vice versa) is not
possible; file a new item of the correct kind and re-parent/close the old one.
epic attach on an epic
LOG-<n> is an epic — epics can't be attached to a parent.
Means: you passed epic on an item that is itself an epic. Epics are
containers, not children.
Fix: only attach logs to epics. To reorganize, re-parent the child
logs, not the epic.
epic ref is a log, not an epic
LOG-<n> is a log, not an epic — children only attach to epics.
Fix: point epic at an actual epic ref.
epic does not exist
That epic does not exist in this project.
Fix: verify the epic ref belongs to this project.
no such item
update_item returns No item LOG-<n>; update_items records
{ ref, ok: false, error: "no such item" }.
Fix: confirm the number and that the item is in this project.
Webhook: items not crossing off
When a PR merges, logbook finds every item linking that PR and crosses it off.
If that is not happening, check these in order (all from
app/api/webhooks/github/route.ts):
Repo not mapped to a project
{ "ignored": "repo not mapped to a project", "repo": "<owner/name>" }
Means: no project has this repo registered (projectIdForRepo returned
nothing), so the event is dropped before any signature check.
Fix: run link_repo with owner/name in the target project (or add it in
project setup). Repo matching is on repository.full_name.
Bad signature (secret mismatch)
{ "error": "Bad signature" } // HTTP 401
Means: the HMAC-SHA256 of the raw body under this project's webhook
secret did not match x-hub-signature-256. Each project has its own secret,
resolved from the repo before verification — a secret copied from another
project fails here.
Fix: set the GitHub webhook secret to this project's webhook secret (shown
in its setup prompt), content type application/json.
Project has no webhook secret
{ "error": "project has no webhook secret" } // HTTP 503
Means: the repo mapped to a project, but that project has no
webhookSecret configured.
Fix: complete the project's webhook setup so a secret exists.
Event is not a pull_request / not a merge
- Ping events:
{ "ok": true }(setup handshake — expected). - Other events:
{ "ignored": "<event>" }— onlypull_requestevents are processed. - A
pull_requestevent that is not a merge:{ "recorded": "<action>", "state": "<open|draft|closed|merged>" }— the PR registry is updated, but nothing crosses off. Onlyaction === "closed"withpull_request.merged === truecrosses off linked items.
Fix: ensure the webhook is subscribed to Pull requests events, and note that only an actual merge (not close-without-merge, not open/draft) crosses items off.
PR linked but item not found
Cross-off matches items whose links jsonb contains
"<owner>/<repo>/pull/<number>" (any stored variant — full URL, with or without
scheme). A successful merge returns
{ "matched": <n>, "crossed_off": ["LOG-…", …] }.
Means: if matched is 0, no item links that PR by URL/number.
Fix: attach the PR to the item as a pr link (via create_item/
update_item links/add_links), using a URL that contains
<owner>/<repo>/pull/<number>.
Notes on merge behavior: an already-done item is not re-crossed but still gets
a merge note. A written resolution is never overwritten — the auto-resolution
stub is only filled when the item's resolution is empty. Changed-file
enrichment (the file summary and item_files stamping) is best-effort: any
failure (no GITHUB_TOKEN, GitHub error) leaves the cross-off behaving exactly
as before enrichment existed and the webhook still returns 200.
Sign-in / account edge cases (human web app)
Humans sign in through Clerk; agents never do (agents use lgbk_… tokens).
Wrong email = a fresh, empty account
History adoption is by exact email. getCurrentUser resolves the Clerk user
to the local users mirror and, on first sign-in, lazily provisions a mirror
row from the Clerk email/name. If you sign in with a different email address
than the one your history is under, you get a fresh, empty account — nothing
is wrong with the data; it is simply keyed to the other email.
Fix: sign in with the exact email your items and attribution live under. An
item's attribution and assignee_email resolution both key on the exact,
lowercased email.
Redirects after sign-in
requireProjectContext redirects: no user → /sign-in; signed in but no active
org (e.g. you left your org) → /select-org; org with no project yet →
/onboarding. These are expected routing, not errors.
Checking server / deployment status
A healthy deployment behaves like this:
GET /(unauthenticated) redirects to/sign-in— the app is up.POST /api/mcpwith no/invalid token returns HTTP 401 JSON ({ "error": "Unauthorized. Pass a logbook API token: …" }) — the MCP endpoint is up and reachable. A 401 here is the expected signal that the server is healthy but your request is unauthenticated, not that the server is down.
If instead you get connection refused, a 5xx, or an HTML error page, the problem is deployment/infrastructure, not auth — check the host and the Neon database (schema migrations must be applied before the matching code ships, or endpoints 500).