toeverything/AFFiNE · error · BadRequestException

MCP credential not found

Error message

MCP credential not found

What it means

BadRequestException('MCP credential not found') thrown at the top of MCP credential rotation. Rotation looks up the credential by id and requires ALL of: it exists, belongs to the calling userId, belongs to the given workspaceId, is not revoked, not already replaced, and not expired. Any mismatch aborts rotation — you can only rotate a live credential you own.

Source

Thrown at packages/backend/server/src/plugins/copilot/mcp/credential.ts:105

  }

  @Transactional()
  async rotate(
    id: string,
    userId: string,
    workspaceId: string,
    expirationDays: number
  ) {
    const current = await this.models.mcpCredential.get(id);
    if (
      !current ||
      current.userId !== userId ||
      current.workspaceId !== workspaceId ||
      current.revokedAt ||
      current.replacedById ||
      current.expiresAt <= new Date()
    ) {
      throw new BadRequestException('MCP credential not found');
    }

    const maximumGraceEnd = new Date(Date.now() + ROTATION_GRACE_MS);
    const graceEnd =
      current.expiresAt < maximumGraceEnd ? current.expiresAt : maximumGraceEnd;
    const issued = await this.issue({
      userId,
      workspaceId,
      name: current.name,
      accessMode: current.accessMode,
      expirationDays,
      familyId: current.familyId,
      generation: current.generation + 1,
      graceEndsAt: graceEnd,
    });
    const replaced = await this.models.mcpCredential.replace(
      current.id,
      userId,

View on GitHub (pinned to b4c8548c09)

Solutions

  1. Re-fetch the credential list for the workspace and rotate the current (unreplaced, unrevoked, unexpired) entry
  2. If the credential expired, issue a new credential instead of rotating
  3. Confirm the credential belongs to the same userId and workspaceId used in the rotate call
  4. Guard against double-rotation: disable the rotate button once a rotation is in flight

Example fix

// before
await credentials.rotate(id, userId, workspaceId); // id already replaced/expired

// after
const current = (await listCredentials(workspaceId)).find(
  c => c.familyId === familyId && !c.replacedById && !c.revokedAt && c.expiresAt > new Date()
);
if (!current) {
  const issued = await credentials.create({ userId, workspaceId, name, accessMode, expirationDays });
} else {
  await credentials.rotate(current.id, userId, workspaceId);
}
Defensive patterns

Strategy: validation

Validate before calling

const c = await models.mcpCredential.get(id);
const rotatable =
  !!c &&
  c.userId === userId &&
  c.workspaceId === workspaceId &&
  !c.revokedAt &&
  !c.replacedById &&
  c.expiresAt > new Date();
if (!rotatable) throw new Error('Credential not rotatable — refetch list');

Type guard

const isRotatableCredential = (
  c: McpCredential | null | undefined,
  userId: string,
  workspaceId: string
): c is McpCredential =>
  !!c &&
  c.userId === userId &&
  c.workspaceId === workspaceId &&
  !c.revokedAt &&
  !c.replacedById &&
  c.expiresAt.getTime() > Date.now();

Try / catch

try {
  await credentials.rotate(id, userId, workspaceId);
} catch (e) {
  if (e instanceof BadRequestException && e.message === 'MCP credential not found') {
    await refreshCredentialList(); // pick the current active credential or create a new one
  } else throw e;
}

Prevention

When it happens

Trigger: Calling rotateMcpCredential with a credential id from another user/workspace; rotating a credential already rotated (replacedById set); rotating after it expired (expiresAt <= now); rotating a revoked credential; typo'd/stale id.

Common situations: UI lists credentials from a stale fetch and the credential was rotated in another tab; cron rotates on schedule but the credential already aged out; copying credential ids between workspaces.

Related errors


AI-assisted analysis of toeverything/AFFiNE@b4c8548c09 (2026-08-18). Data as JSON: /api/errors/f73bf15546201b10. Report an issue: GitHub.