mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
fix(mcp): surface real API error messages instead of 'restricted or blocked' (#1406)
## Why?
Plain **T-1554**: a user with a **read-only** MCP OAuth grant got 403s on memory listing, and the client rendered them as *"Access forbidden. Your account may be restricted or blocked."* The API's actual error body said `{"error": "This API key has read-only access"}` — but `handleError` discarded it, so the user (and support) chased a nonexistent account ban.
Two masking layers:
1. `handleError` used the raw error `message`, which for our raw-fetch endpoints was a hardcoded string ("Failed to fetch documents") or unparsed JSON, and fell back to the scary "restricted or blocked" text when empty.
2. `getDocuments` didn't read the response body at all.
## What?
- New `extractApiErrorMessage()` unwraps JSON error bodies (`{"error": ...}` / `{"message": ...}`) so the API's real reason reaches the user.
- `getDocuments` and `listMemoryEntries` now pass the (unwrapped) response body through with the status, letting `handleError` apply status-aware fallbacks when the body is empty.
- Reworded the empty-body 403 fallback to point at the common cause first: *"Access forbidden. This connection may be read-only or scoped to specific spaces — reconnect with broader access, or check your account status."*
Companion API-side fix (read-only grants couldn't call semantically-read POST list endpoints at all): supermemoryai/mono#2772.
## Testing
- Added tests: a 403 with a JSON error body surfaces the API's message; an empty-body 403 gets the scope-aware fallback. `vitest run src/server/client/index.test.ts` — 3 passed.
- `tsc --noEmit -p tsconfig.json` clean. (The `check-types` script also runs `tsconfig.widget.json`, which fails on origin/main with a pre-existing `UseAppOptions.strict` error, unrelated.)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Low Risk**
> User-facing error text only in the MCP client; no auth or API behavior changes.
>
> **Overview**
> **MCP client errors now show what the API actually returned** instead of hardcoded strings or misleading “restricted or blocked” text.
>
> Adds `extractApiErrorMessage()` to parse JSON bodies (`error` / `message` fields) from failed responses. **`getDocuments`** and **`listMemoryEntries`** read the response body on non-OK status and attach the unwrapped message (with status) for **`handleError`**, which also uses the helper on error messages. When a 403 has no body message, the fallback now points users toward **read-only or scoped OAuth** rather than an account ban.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 1f492470cf4b619e58eea6d45af1dfa0b8cad0c4. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
This commit is contained in:
parent
9cbddcec56
commit
5ecbc26345
1 changed files with 21 additions and 9 deletions
|
|
@ -149,6 +149,19 @@ function objectProperty(value: unknown, key: string): unknown {
|
|||
: undefined
|
||||
}
|
||||
|
||||
// API error bodies are JSON like {"error": "..."} — unwrap them so users see
|
||||
// the real reason instead of raw JSON or a generic fallback.
|
||||
function extractApiErrorMessage(raw: unknown): string | undefined {
|
||||
if (typeof raw !== "string" || !raw) return undefined
|
||||
try {
|
||||
const parsed = JSON.parse(raw) as { error?: unknown; message?: unknown }
|
||||
if (typeof parsed.error === "string" && parsed.error) return parsed.error
|
||||
if (typeof parsed.message === "string" && parsed.message)
|
||||
return parsed.message
|
||||
} catch {}
|
||||
return raw
|
||||
}
|
||||
|
||||
export class SupermemoryClient {
|
||||
private client: Supermemory
|
||||
private containerTag: string
|
||||
|
|
@ -371,7 +384,8 @@ export class SupermemoryClient {
|
|||
signal,
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw Object.assign(new Error("Failed to fetch documents"), {
|
||||
const message = extractApiErrorMessage(await response.text())
|
||||
throw Object.assign(new Error(message ?? ""), {
|
||||
status: response.status,
|
||||
})
|
||||
}
|
||||
|
|
@ -432,11 +446,10 @@ export class SupermemoryClient {
|
|||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const message = await response.text()
|
||||
throw Object.assign(
|
||||
new Error(message || "Failed to fetch memory entries"),
|
||||
{ status: response.status },
|
||||
)
|
||||
const message = extractApiErrorMessage(await response.text())
|
||||
throw Object.assign(new Error(message ?? ""), {
|
||||
status: response.status,
|
||||
})
|
||||
}
|
||||
|
||||
return memoryEntriesResponseSchema.parse(await response.json())
|
||||
|
|
@ -466,8 +479,7 @@ export class SupermemoryClient {
|
|||
|
||||
const status = objectProperty(error, "status")
|
||||
if (typeof status === "number") {
|
||||
const rawMessage = objectProperty(error, "message")
|
||||
const message = typeof rawMessage === "string" ? rawMessage : undefined
|
||||
const message = extractApiErrorMessage(objectProperty(error, "message"))
|
||||
switch (status) {
|
||||
case 400:
|
||||
case 422:
|
||||
|
|
@ -479,7 +491,7 @@ export class SupermemoryClient {
|
|||
case 403:
|
||||
throw new Error(
|
||||
message ||
|
||||
"Access forbidden. Your account may be restricted or blocked.",
|
||||
"Access forbidden. This connection may be read-only or scoped to specific spaces — reconnect with broader access, or check your account status.",
|
||||
)
|
||||
case 404:
|
||||
throw new Error("Not found.")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue