From 91d7f07c0f12f31d594fe87ff7830406ae81125d Mon Sep 17 00:00:00 2001 From: MauryaQbit Date: Sun, 20 Sep 2026 23:04:38 +0530 Subject: [PATCH] fix: fallback memory graph to POST /v3/documents/list on 404 Fixes #1672 - self-hosted supermemory-server 0.0.8 returns 404 for POST /v3/documents/documents. All graph/context callers now try the rich endpoint first, then fallback to documented POST /v3/documents/list and normalize { memories } to { documents }. - apps/memory-graph-playground/src/app/api/graph/route.ts: fallback + normalize - apps/mcp/src/server/client/index.ts: getDocuments fallback + map to DocumentsApiResponse - apps/sdk-playground/src/lib/context-api.ts + python/server.py: same fallback for container context - apps/memory-graph-playground/src/app/page.tsx: persist API key in localStorage so Bearer auth is sent on non-localhost hosts - apps/docs/integrations/memory-graph.mdx: update examples to /list with normalization --- apps/docs/integrations/memory-graph.mdx | 85 ++++++++++++++----- apps/mcp/src/server/client/index.ts | 80 +++++++++++++---- .../src/app/api/graph/route.ts | 85 ++++++++++++++----- apps/memory-graph-playground/src/app/page.tsx | 23 ++++- apps/sdk-playground/python/server.py | 34 +++++--- apps/sdk-playground/src/lib/context-api.ts | 53 +++++++++--- 6 files changed, 278 insertions(+), 82 deletions(-) diff --git a/apps/docs/integrations/memory-graph.mdx b/apps/docs/integrations/memory-graph.mdx index decd5f52..fab12e2c 100644 --- a/apps/docs/integrations/memory-graph.mdx +++ b/apps/docs/integrations/memory-graph.mdx @@ -70,21 +70,42 @@ Create an API route to fetch documents from Supermemory: import { NextResponse } from 'next/server'; export async function GET() { - const response = await fetch('https://api.supermemory.ai/v3/documents/documents', { + const body = JSON.stringify({ + page: 1, + limit: 500, + sort: 'createdAt', + order: 'desc', + }); + const headers = { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${process.env.SUPERMEMORY_API_KEY}`, + }; + + let response = await fetch('https://api.supermemory.ai/v3/documents/documents', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${process.env.SUPERMEMORY_API_KEY}`, - }, - body: JSON.stringify({ - page: 1, - limit: 500, - sort: 'createdAt', - order: 'desc', - }), + headers, + body, }); + // Fallback for servers without the rich graph endpoint (e.g. self-hosted + // supermemory-server returns 404 for /documents/documents). + // POST /v3/documents/list exists on both cloud and self-hosted. + if (response.status === 404) { + response = await fetch('https://api.supermemory.ai/v3/documents/list', { + method: 'POST', + headers, + body, + }); + } + const data = await response.json(); + // Normalize /list ({ memories }) to graph shape ({ documents }). + if (!data.documents && Array.isArray(data.memories)) { + return NextResponse.json({ + documents: data.memories.map((m) => ({ ...m, memories: [], memoryEntries: [] })), + pagination: data.pagination, + }); + } return NextResponse.json(data); } ``` @@ -94,32 +115,50 @@ export async function GET() { import type { NextApiRequest, NextApiResponse } from 'next'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const response = await fetch('https://api.supermemory.ai/v3/documents/documents', { + const body = JSON.stringify({ page: 1, limit: 500, sort: 'createdAt', order: 'desc' }); + const headers = { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${process.env.SUPERMEMORY_API_KEY}`, + }; + + let response = await fetch('https://api.supermemory.ai/v3/documents/list', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${process.env.SUPERMEMORY_API_KEY}`, - }, - body: JSON.stringify({ page: 1, limit: 500, sort: 'createdAt', order: 'desc' }), + headers, + body, }); const data = await response.json(); + if (!data.documents && Array.isArray(data.memories)) { + return res.json({ + documents: data.memories.map((m) => ({ ...m, memories: [], memoryEntries: [] })), + pagination: data.pagination, + }); + } res.json(data); } ``` ```javascript Express app.get('/api/graph', async (req, res) => { - const response = await fetch('https://api.supermemory.ai/v3/documents/documents', { + const body = JSON.stringify({ page: 1, limit: 500, sort: 'createdAt', order: 'desc' }); + const headers = { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${process.env.SUPERMEMORY_API_KEY}`, + }; + + let response = await fetch('https://api.supermemory.ai/v3/documents/list', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${process.env.SUPERMEMORY_API_KEY}`, - }, - body: JSON.stringify({ page: 1, limit: 500, sort: 'createdAt', order: 'desc' }), + headers, + body, }); const data = await response.json(); + if (!data.documents && Array.isArray(data.memories)) { + return res.json({ + documents: data.memories.map((m) => ({ ...m, memories: [], memoryEntries: [] })), + pagination: data.pagination, + }); + } res.json(data); }); ``` diff --git a/apps/mcp/src/server/client/index.ts b/apps/mcp/src/server/client/index.ts index a2ccc8e7..f683abe0 100644 --- a/apps/mcp/src/server/client/index.ts +++ b/apps/mcp/src/server/client/index.ts @@ -337,22 +337,72 @@ export class SupermemoryClient { ): Promise { try { const signal = options?.signal ?? AbortSignal.timeout(FETCH_TIMEOUT_MS) - const response = await fetch(`${this.apiUrl}/v3/documents/documents`, { - method: "POST", - headers: { - Authorization: `Bearer ${this.bearerToken}`, - "Content-Type": "application/json", - "x-sm-source": MCP_SOURCE, - }, - body: JSON.stringify({ - page, - limit, - sort: "createdAt", - order: "desc", - containerTags, - }), - signal, + const headers = { + Authorization: `Bearer ${this.bearerToken}`, + "Content-Type": "application/json", + "x-sm-source": MCP_SOURCE, + } + const body = JSON.stringify({ + page, + limit, + sort: "createdAt", + order: "desc", + containerTags, }) + + const postDocuments = async (path: string) => + fetch(`${this.apiUrl}${path}`, { + method: "POST", + headers, + body, + signal, + }) + + let response = await postDocuments("/v3/documents/documents") + + // Fallback for servers without the rich graph endpoint (see #1672): + // POST /v3/documents/list exists on both cloud and self-hosted + // (supermemory-server 0.0.8 returns 404 for /documents/documents). + if (response.status === 404) { + response = await postDocuments("/v3/documents/list") + if (!response.ok) { + const message = extractApiErrorMessage(await response.text()) + throw Object.assign(new Error(message ?? ""), { + status: response.status, + }) + } + const listData = (await response.json()) as { + memories?: Array> + pagination?: DocumentsApiResponse["pagination"] + } + const memories = Array.isArray(listData.memories) + ? listData.memories + : [] + return documentsApiResponseSchema.parse({ + documents: memories.map((mem) => ({ + id: String(mem.id ?? ""), + title: (mem.title as string | null | undefined) ?? null, + summary: (mem.summary as string | null | undefined) ?? null, + type: (mem.type as string | undefined) ?? "unknown", + createdAt: String( + (mem.createdAt as string | undefined) ?? + new Date(0).toISOString(), + ), + updatedAt: String( + (mem.updatedAt as string | undefined) ?? + new Date(0).toISOString(), + ), + memoryEntries: [], + })), + pagination: listData.pagination ?? { + currentPage: page, + limit, + totalItems: memories.length, + totalPages: 1, + }, + }) + } + if (!response.ok) { const message = extractApiErrorMessage(await response.text()) throw Object.assign(new Error(message ?? ""), { diff --git a/apps/memory-graph-playground/src/app/api/graph/route.ts b/apps/memory-graph-playground/src/app/api/graph/route.ts index 67b62d34..c3183534 100644 --- a/apps/memory-graph-playground/src/app/api/graph/route.ts +++ b/apps/memory-graph-playground/src/app/api/graph/route.ts @@ -21,27 +21,39 @@ export async function POST(request: Request) { ) } - const graphUrl = new URL( - "/v3/documents/documents", - SUPERMEMORY_API_BASE_URL, - ) + const requestBody = { + page, + limit, + sort, + order, + ...(Array.isArray(containerTags) && containerTags.length > 0 + ? { containerTags } + : {}), + } - const response = await fetch(graphUrl, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${apiKey}`, - }, - body: JSON.stringify({ - page, - limit, - sort, - order, - ...(Array.isArray(containerTags) && containerTags.length > 0 - ? { containerTags } - : {}), - }), - }) + const headers = { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + } + + // Primary: documents with memories for rich graph rendering. + // Fallback: POST /v3/documents/list (see #1672) — self-hosted + // servers (e.g. supermemory-server 0.0.8) return 404 for + // /v3/documents/documents, so normalize { memories } to { documents }. + async function postDocuments(path: string) { + const url = new URL(path, SUPERMEMORY_API_BASE_URL) + return fetch(url, { + method: "POST", + headers, + body: JSON.stringify(requestBody), + }) + } + + let response = await postDocuments("/v3/documents/documents") + + if (response.status === 404) { + response = await postDocuments("/v3/documents/list") + } if (!response.ok) { const errorData = await response.json().catch(() => ({})) @@ -52,6 +64,39 @@ export async function POST(request: Request) { } const data = await response.json() + + // Normalize POST /v3/documents/list ({ memories }) to the graph + // shape ({ documents }) so the UI works against both cloud and + // self-hosted servers. + if (Array.isArray((data as { documents?: unknown })?.documents)) { + return NextResponse.json(data) + } + + const memories = (data as { memories?: Array> }) + ?.memories + if (Array.isArray(memories)) { + return NextResponse.json({ + documents: memories.map((mem) => ({ + id: mem.id, + title: mem.title ?? null, + summary: mem.summary ?? null, + documentType: (mem.type as string | undefined) ?? "unknown", + type: (mem.type as string | undefined) ?? "unknown", + containerTags: mem.containerTags ?? [], + createdAt: mem.createdAt, + updatedAt: mem.updatedAt, + memories: [], + memoryEntries: [], + })), + pagination: (data as { pagination?: unknown }).pagination ?? { + currentPage: page, + limit, + totalItems: memories.length, + totalPages: 1, + }, + }) + } + return NextResponse.json(data) } catch (error) { console.error("Graph API error:", error) diff --git a/apps/memory-graph-playground/src/app/page.tsx b/apps/memory-graph-playground/src/app/page.tsx index 8131ef69..4bfeec76 100644 --- a/apps/memory-graph-playground/src/app/page.tsx +++ b/apps/memory-graph-playground/src/app/page.tsx @@ -113,7 +113,14 @@ function toGraphDocuments(docs: PlaygroundApiDocument[]): GraphApiDocument[] { } export default function Home() { - const [apiKey, setApiKey] = useState("") + const [apiKey, setApiKey] = useState(() => { + if (typeof window === "undefined") return "" + try { + return window.localStorage.getItem("supermemory-api-key") ?? "" + } catch { + return "" + } + }) const [containerTag, setContainerTag] = useState("") const [containerTags, setContainerTags] = useState([]) const [isLoadingContainerTags, setIsLoadingContainerTags] = useState(false) @@ -140,6 +147,20 @@ export default function Home() { documents: GraphApiDocument[] } | null>(null) + // Persist API key so all graph calls send `Authorization: Bearer ` + // (see #1672 — unauthenticated calls 401 on non-localhost hosts). + useEffect(() => { + try { + if (apiKey) { + window.localStorage.setItem("supermemory-api-key", apiKey) + } else { + window.localStorage.removeItem("supermemory-api-key") + } + } catch { + // ignore storage errors (private mode, etc.) + } + }, [apiKey]) + const selectedContainerTags = useMemo(() => { const trimmed = containerTag.trim() return trimmed ? [trimmed] : undefined diff --git a/apps/sdk-playground/python/server.py b/apps/sdk-playground/python/server.py index 842e3d7e..60ae0725 100644 --- a/apps/sdk-playground/python/server.py +++ b/apps/sdk-playground/python/server.py @@ -484,23 +484,35 @@ async def fetch_container_context( timeout=HTTP_TIMEOUT_SECONDS, follow_redirects=False, ) as http: + headers = { + "Authorization": f"Bearer {sm_key}", + "Content-Type": "application/json", + } + payload = { + "containerTags": [container_tag], + "limit": 25, + "sort": "createdAt", + "order": "desc", + } docs_response = await http.post( f"{base_url}/v3/documents/documents", - headers={ - "Authorization": f"Bearer {sm_key}", - "Content-Type": "application/json", - }, - json={ - "containerTags": [container_tag], - "limit": 25, - "sort": "createdAt", - "order": "desc", - }, + headers=headers, + json=payload, ) + # Fallback for servers without the rich graph endpoint (see #1672): + # supermemory-server 0.0.8 returns 404 for /documents/documents. + # POST /v3/documents/list exists on both cloud and self-hosted. + if docs_response.status_code == 404: + docs_response = await http.post( + f"{base_url}/v3/documents/list", + headers=headers, + json=payload, + ) docs_response.raise_for_status() docs = docs_response.json() - raw_documents = docs.get("documents", []) if isinstance(docs, dict) else [] + if isinstance(docs, dict): + raw_documents = docs.get("documents", []) or docs.get("memories", []) documents = [] for doc in raw_documents: diff --git a/apps/sdk-playground/src/lib/context-api.ts b/apps/sdk-playground/src/lib/context-api.ts index 9c201a11..7cb3a2fe 100644 --- a/apps/sdk-playground/src/lib/context-api.ts +++ b/apps/sdk-playground/src/lib/context-api.ts @@ -152,19 +152,48 @@ export async function fetchContainerContext( const client = getSupermemoryClient(apiKey) const profile = await fetchProfileContext(client, containerTag, query) - const docsResponse = await client.post<{ - documents?: unknown[] - pagination?: unknown - }>("/v3/documents/documents", { - body: { - containerTags: [containerTag], - limit: 25, - sort: "createdAt", - order: "desc", - }, - }) + async function fetchDocs() { + try { + return await client.post<{ + documents?: unknown[] + memories?: unknown[] + pagination?: unknown + }>("/v3/documents/documents", { + body: { + containerTags: [containerTag], + limit: 25, + sort: "createdAt", + order: "desc", + }, + }) + } catch (error) { + // Fallback for servers without the rich graph endpoint (see #1672): + // supermemory-server 0.0.8 returns 404 for /documents/documents. + // POST /v3/documents/list exists on both cloud and self-hosted. + const status = + typeof error === "object" && error !== null && "status" in error + ? (error as { status?: unknown }).status + : undefined + const message = error instanceof Error ? error.message : String(error) + if (status !== 404 && !message.includes("404")) throw error + return await client.post<{ + documents?: unknown[] + memories?: unknown[] + pagination?: unknown + }>("/v3/documents/list", { + body: { + containerTags: [containerTag], + limit: 25, + sort: "createdAt", + order: "desc", + }, + }) + } + } - const rawDocuments = docsResponse.documents ?? [] + const docsResponse = await fetchDocs() + + const rawDocuments = docsResponse.documents ?? docsResponse.memories ?? [] const documents = rawDocuments.map((doc) => { const record = doc as Record return {