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
- Use a key scoped to the requested project, or an unscoped (team-level) key if you need cross-project access.
- Pass the projectId that matches the key's scope in the tool arguments.
- Re-issue the key with the correct project scope via `claude-mem server api-key`.
- 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
- Know the scope of each API key at creation time; document it alongside the key.
- For cross-project reads, issue an unscoped (team-level) key.
- Have clients prefer the key's resolved scope over a hardcoded projectId.
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
- auth_invalid
- auth_invalid
- auth_invalid
- missing_api_key
- Invalid API key: check your API key configuration in ~/.clau
AI-assisted analysis of thedotmack/claude-mem@d768ba3643 (2026-08-12).
Data as JSON: /api/errors/1ed58b205e7c2198.
Report an issue: GitHub.