{"record":{"id":"f73bf15546201b10","repo":"toeverything/AFFiNE","slug":"mcp-credential-not-found","errorCode":null,"errorMessage":"MCP credential not found","messagePattern":"MCP credential not found","errorType":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"packages/backend/server/src/plugins/copilot/mcp/credential.ts","lineNumber":105,"sourceCode":"  }\n\n  @Transactional()\n  async rotate(\n    id: string,\n    userId: string,\n    workspaceId: string,\n    expirationDays: number\n  ) {\n    const current = await this.models.mcpCredential.get(id);\n    if (\n      !current ||\n      current.userId !== userId ||\n      current.workspaceId !== workspaceId ||\n      current.revokedAt ||\n      current.replacedById ||\n      current.expiresAt <= new Date()\n    ) {\n      throw new BadRequestException('MCP credential not found');\n    }\n\n    const maximumGraceEnd = new Date(Date.now() + ROTATION_GRACE_MS);\n    const graceEnd =\n      current.expiresAt < maximumGraceEnd ? current.expiresAt : maximumGraceEnd;\n    const issued = await this.issue({\n      userId,\n      workspaceId,\n      name: current.name,\n      accessMode: current.accessMode,\n      expirationDays,\n      familyId: current.familyId,\n      generation: current.generation + 1,\n      graceEndsAt: graceEnd,\n    });\n    const replaced = await this.models.mcpCredential.replace(\n      current.id,\n      userId,","sourceCodeStart":87,"sourceCodeEnd":123,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/b4c8548c09da21b2898443559a5b846f0ccf5dd8/packages/backend/server/src/plugins/copilot/mcp/credential.ts#L87-L123","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Re-fetch the credential list for the workspace and rotate the current (unreplaced, unrevoked, unexpired) entry","If the credential expired, issue a new credential instead of rotating","Confirm the credential belongs to the same userId and workspaceId used in the rotate call","Guard against double-rotation: disable the rotate button once a rotation is in flight"],"exampleFix":"// before\nawait credentials.rotate(id, userId, workspaceId); // id already replaced/expired\n\n// after\nconst current = (await listCredentials(workspaceId)).find(\n  c => c.familyId === familyId && !c.replacedById && !c.revokedAt && c.expiresAt > new Date()\n);\nif (!current) {\n  const issued = await credentials.create({ userId, workspaceId, name, accessMode, expirationDays });\n} else {\n  await credentials.rotate(current.id, userId, workspaceId);\n}","handlingStrategy":"validation","validationCode":"const c = await models.mcpCredential.get(id);\nconst rotatable =\n  !!c &&\n  c.userId === userId &&\n  c.workspaceId === workspaceId &&\n  !c.revokedAt &&\n  !c.replacedById &&\n  c.expiresAt > new Date();\nif (!rotatable) throw new Error('Credential not rotatable — refetch list');","typeGuard":"const isRotatableCredential = (\n  c: McpCredential | null | undefined,\n  userId: string,\n  workspaceId: string\n): c is McpCredential =>\n  !!c &&\n  c.userId === userId &&\n  c.workspaceId === workspaceId &&\n  !c.revokedAt &&\n  !c.replacedById &&\n  c.expiresAt.getTime() > Date.now();","tryCatchPattern":"try {\n  await credentials.rotate(id, userId, workspaceId);\n} catch (e) {\n  if (e instanceof BadRequestException && e.message === 'MCP credential not found') {\n    await refreshCredentialList(); // pick the current active credential or create a new one\n  } else throw e;\n}","preventionTips":["Always rotate the newest active credential in the family, never a cached id","Disable rotate actions on revoked/replaced/expired rows in the UI","Issue a fresh credential instead of rotating an expired one"],"tags":["mcp","credential","rotation","bad-request"],"backgroundTag":"credential-not-found","analyzedSha":"b4c8548c09da21b2898443559a5b846f0ccf5dd8","analyzedAt":"2026-08-18T21:16:52.546Z","contentChangedAt":"2026-08-18T21:16:52.546Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}