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
This commit is contained in:
MauryaQbit 2026-09-20 23:04:38 +05:30 • committed by Dhananjay Maurya
parent 4d8a4ebfdd
commit 91d7f07c0f
6 changed files with 278 additions and 82 deletions

View file

@ -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);
});
```

View file

@ -337,22 +337,72 @@ export class SupermemoryClient {
): Promise<DocumentsApiResponse> {
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<Record<string, unknown>>
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 ?? ""), {

View file

@ -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<Record<string, unknown>> })
?.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)

View file

@ -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<ContainerTagOption[]>([])
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 <key>`
// (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

View file

@ -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:

View file

@ -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<string, unknown>
return {