koala73/worldmonitor · error · ProMcpIssueFailed
invalid-user-id
invalid-user-id
Error message
Invalid userId for Pro MCP token issue
What it means
Convex answered HTTP 400 (INVALID_USER_ID) for the internal token issue action: the userId in the request body was empty, missing, or not a usable user reference. issueProMcpTokenForUser maps this to ProMcpIssueFailed kind='invalid-user-id'. It is a caller bug, deterministic for the same input — retrying with the same userId will fail identically.
Source
Thrown at server/_shared/pro-mcp-token.ts:224
'network',
`Convex issue request failed: ${err instanceof Error ? err.message : String(err)}`,
);
}
if (resp.ok) {
const data = (await resp.json().catch(() => null)) as ProMcpIssueResult | null;
if (!data || typeof data.tokenId !== 'string' || !data.tokenId) {
throw new ProMcpIssueFailed('network', 'Convex issue response missing tokenId', resp.status);
}
return { tokenId: data.tokenId };
}
// Map Convex error responses (see convex/http.ts /api/internal-issue-pro-mcp-token).
if (resp.status === 403) {
throw new ProMcpIssueFailed('pro-required', 'Pro entitlement required to issue MCP token', 403);
}
if (resp.status === 400) {
throw new ProMcpIssueFailed('invalid-user-id', 'Invalid userId for Pro MCP token issue', 400);
}
// 401 (shared-secret mismatch) and 5xx and any other status → network/transient.
throw new ProMcpIssueFailed(
'network',
`Convex issue returned HTTP ${resp.status}`,
resp.status,
);
}
/**
* Validate a Pro MCP token by tokenId — discriminated-union variant.
*
* Returns `{ok:'valid', userId}` if the row exists and is not revoked.
* Returns `{ok:'revoked'}` if Convex authoritatively returned null
* (row missing, revoked, or malformed-id). Returns `{ok:'transient'}` on
* Convex 5xx / network error / timeout / non-JSON — caller can decide
* whether to fail-closed (per-request validate) or preserve the refresh
* token (refresh-grant path) instead of consuming it.View on GitHub (pinned to eeab0a219f)
Solutions
- Log and inspect the exact userId being sent in the request body at the call site
- Guard the call: only invoke issueProMcpTokenForUser after confirming userId is a non-empty string
- Check how the caller derives userId (Clerk subject vs Convex user table ID) and align it with what convex/http.ts validates
Example fix
// before — session hiccup silently produces an empty userId
const userId = session?.user?.id ?? '';
await issueProMcpTokenForUser(userId);
// after — fail fast before hitting Convex
const userId = session?.user?.id;
if (!userId) return new Response('unauthenticated', { status: 401 });
await issueProMcpTokenForUser(userId); Defensive patterns
Strategy: validation
Validate before calling
function isValidIssueUserId(userId: unknown): userId is string {
return typeof userId === 'string' && userId.trim().length > 0;
}
if (!isValidIssueUserId(userId)) return new Response('missing user', { status: 401 }); Type guard
function isInvalidUserId(e: unknown): e is ProMcpIssueFailed {
return e instanceof ProMcpIssueFailed && e.kind === 'invalid-user-id';
} Try / catch
try {
await issueProMcpTokenForUser(userId);
} catch (err) {
if (isInvalidUserId(err)) return new Response('invalid user id', { status: 400 }); // fix input, do not retry
throw err;
} Prevention
- Never default userId to '' — treat an unresolved session subject as unauthenticated
- Type the userId parameter as string (not string | undefined) so the compiler enforces the guard
- Log the userId shape (not contents) when the OAuth flow starts to catch mapping regressions early
When it happens
Trigger: issueProMcpTokenForUser('') or issueProMcpTokenForUser(undefined as any) — e.g. the Clerk session resolved without a subject; the caller passed an internal/clerk-prefixed ID where Convex expects its own user table reference; JSON.stringify body dropped userId because the variable was never assigned.
Common situations: OAuth handler reads userId from a session that expired mid-flow, yielding an empty string; a refactor changed the userId source and TypeScript did not catch the undefined because of an any cast; the Convex user row was deleted between grant and issue.
Related errors
- ${label} HTTP 400
- get_intel_timeline requires at least one of domain ("conflic
- INCOMPATIBLE_DELIVERY
- COUNTRIES_LIMIT_EXCEEDED
- TICKERS_LIMIT_EXCEEDED
AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21).
Data as JSON: /api/errors/35bff87d8ac94fb3.
Report an issue: GitHub.