thedotmack/claude-mem · error

API key is scoped to a different project

Error message

API key is scoped to a different project

What it means

Thrown by assertProjectAllowed() inside the /v1/mcp handler when the authenticated API key is scoped to a specific project (projectScope from the auth context) but the MCP tool call requests a different projectId. This enforces project-level authorization on the read-only MCP surface, identical to /v1/search scoping. The check only fires when the key has a non-null projectScope.

Source

Thrown at src/server/routes/v1/ServerV1PostgresRoutes.ts:1017

        });
      },
    ));

    // Remote authenticated MCP endpoint. The "secure MCP link" a user pastes
    // into Claude Code (or any MCP client) to recall their cloud memory:
    //   claude mcp add --transport http claude-mem <base>/v1/mcp \
    //     --header "Authorization: Bearer cm_..."
    // Same readAuth (memories:read) + team/project scoping + audit trail as
    // /v1/search, so it reads identical data through identical guards. Stateless
    // streamable-HTTP: one transport + server per request, bound to this key's team.
    const mcpHandler = this.asyncHandler(async (req, res) => {
      const teamId = this.requireTeamId(req, res);
      if (!teamId) return;
      const projectScope = req.authContext?.projectId ?? null;
      const repo = new PostgresObservationRepository(this.options.pool);
      const assertProjectAllowed = (projectId: string): void => {
        if (projectScope && projectScope !== projectId) {
          throw new Error('API key is scoped to a different project');
        }
      };
      const backend: RecallBackend = {
        search: async ({ projectId, query, limit }) => {
          assertProjectAllowed(projectId);
          const rows = await repo.search({ projectId, teamId, query, limit });
          // Audit the read, same as POST /v1/search — the MCP path is no exception.
          await this.auditWrite(req, 'observation.read', null, projectId, {
            mode: 'search', via: 'mcp', query, limit,
            resultCount: rows.length, observationIds: rows.map(o => o.id),
          });
          return rows.map(serializeObservation);
        },
        context: async ({ projectId, query, limit }) => {
          assertProjectAllowed(projectId);
          const rows = await repo.search({ projectId, teamId, query, limit });
          await this.auditWrite(req, 'observation.read', null, projectId, {
            mode: 'context', via: 'mcp', query, limit,

View on GitHub (pinned to d768ba3643)

Solutions

  1. Use a key scoped to the requested project, or an unscoped (team-level) key if you need cross-project access.
  2. Pass the projectId that matches the key's scope in the tool arguments.
  3. Re-issue the key with the correct project scope via `claude-mem server api-key`.
  4. Audit the key's scope and align the client's projectId with it.

Example fix

// before: key scoped to projectA, client requests projectB -> throws
client.callTool({ name: 'search', arguments: { projectId: 'projectB', query: 'x' } });
// after: request the project the key is scoped to
client.callTool({ name: 'search', arguments: { projectId: 'projectA', query: 'x' } });
// or: re-issue an unscoped key for cross-project reads
Defensive patterns

Strategy: validation

Validate before calling

// Client-side: align projectId with the key's scope before calling the tool.
function pickProjectId(keyScope: string | null, requested: string): string {
  if (keyScope && keyScope !== requested) {
    // either use the key's scope, or obtain an unscoped key
    return keyScope;
  }
  return requested;
}

Type guard

function projectAllowed(keyScope: string | null, projectId: string): boolean {
  return keyScope === null || keyScope === projectId;
}

Try / catch

try {
  await backend.search({ projectId, query, limit });
} catch (error) {
  if (/scoped to a different project/i.test((error as Error).message)) {
    // re-issue with the projectId matching the key's scope,
    // or re-authenticate with an unscoped key
    return makeMcpError('API key project scope mismatch');
  }
  throw error;
}

Prevention

When it happens

Trigger: An API key created with a project scope of projectA is used in an MCP tool call (search/context/recent) that passes projectId=projectB. The client hardcodes or caches a projectId that no longer matches the key's scope.

Common situations: A team-scoped key is reused across projects after reconfiguration. A client defaults to a project id that differs from the key's scope. A key was re-scoped but the client still sends the old projectId.

Related errors


AI-assisted analysis of thedotmack/claude-mem@d768ba3643 (2026-08-12). Data as JSON: /api/errors/1ed58b205e7c2198. Report an issue: GitHub.