mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-11-15 09:11:45 +00:00
Merge branch 'main' into fix/tools-check-types-zod4
This commit is contained in:
commit
11ba45edc9
16 changed files with 478 additions and 331 deletions
|
|
@ -66,7 +66,9 @@ Retrieve paginated documents with filtering. One `list` call serves documents, c
|
|||
"currentPage": 1,
|
||||
"limit": 10,
|
||||
"totalItems": 25,
|
||||
"totalPages": 3
|
||||
"totalPages": 3,
|
||||
"hasMore": true,
|
||||
"nextCursor": "eyJ2IjoxLCJrIjpb..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
@ -82,6 +84,7 @@ Every response contains `documents`, `chunks`, `memories`, and `pagination`. Onl
|
|||
| `type` | string | required | `documents`, `chunks`, or `memories` |
|
||||
| `limit` | number | 10 | Items per page (maximum 100) |
|
||||
| `page` | number | 1 | Page number |
|
||||
| `cursor` | string | — | `pagination.nextCursor` from the previous response. Takes precedence over `page`. Keep `filter`, `sort`, `order`, and `limit` the same as the request that returned it |
|
||||
| `sort` | string | `createdAt` | `createdAt` or `updatedAt` for documents and memories; `createdAt` or `position` for chunks |
|
||||
| `order` | string | `desc` | `desc` (newest) or `asc` (oldest) |
|
||||
| `body.filter` | object | — | Metadata filter. See [Organizing & Filtering](/concepts/filtering) |
|
||||
|
|
@ -102,20 +105,22 @@ curl -X POST "https://api.supermemory.ai/ns/user_123/list/documents?limit=10" \
|
|||
`content` is `null` until the first extraction publishes. When you replace a file, the previous text remains until the new extraction publishes. It also remains if the replacement fails. Including content can make pages large.
|
||||
|
||||
<Accordion title="Pagination example">
|
||||
Follow `pagination.nextCursor` to walk a long list. Cursor pages skip the total count and stay fast deep into the list. `cursor` needs the TypeScript SDK 5.1.1 or newer.
|
||||
|
||||
```typescript
|
||||
async function getAllDocuments(namespace: string) {
|
||||
const all = [];
|
||||
let page = 1;
|
||||
let cursor: string | undefined;
|
||||
|
||||
while (true) {
|
||||
const { documents, pagination } = await supermemory.list(namespace, "documents", {
|
||||
limit: 100,
|
||||
page,
|
||||
cursor,
|
||||
});
|
||||
|
||||
all.push(...documents);
|
||||
if (page >= pagination.totalPages) break;
|
||||
page++;
|
||||
if (!pagination.nextCursor) break;
|
||||
cursor = pagination.nextCursor;
|
||||
}
|
||||
|
||||
return all;
|
||||
|
|
|
|||
|
|
@ -31,18 +31,16 @@ describeWithAuth("MCP — graph, resources & prompts", () => {
|
|||
expect(result.data.documentCount).toBe(result.data.documents.length)
|
||||
})
|
||||
|
||||
it("fetch-graph-data returns paginated documents", async () => {
|
||||
const res = await callTool(s.client, "fetch-graph-data", {
|
||||
page: 1,
|
||||
limit: 5,
|
||||
})
|
||||
it("fetch-graph-data returns cursor-paginated documents", async () => {
|
||||
const res = await callTool(s.client, "fetch-graph-data", { limit: 5 })
|
||||
expect(res.isError).toBeFalsy()
|
||||
const sc = res.structuredContent as {
|
||||
documents?: unknown[]
|
||||
pagination?: { limit?: number }
|
||||
nextCursor?: string | null
|
||||
}
|
||||
expect(Array.isArray(sc?.documents)).toBe(true)
|
||||
expect(sc?.pagination?.limit).toBe(5)
|
||||
expect(sc?.documents?.length).toBeLessThanOrEqual(5)
|
||||
expect(sc).toHaveProperty("nextCursor")
|
||||
})
|
||||
|
||||
it("reads the profile resource", async () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "supermemory-mcp",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"type": "module",
|
||||
"portless": {
|
||||
"name": "mcp.dev.supermemory",
|
||||
|
|
@ -21,10 +21,10 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@cloudflare/workers-oauth-provider": "^0.2.2",
|
||||
"@modelcontextprotocol/client": "2.0.0",
|
||||
"@modelcontextprotocol/client": "2.3.1",
|
||||
"@modelcontextprotocol/ext-apps": "^1.7.5",
|
||||
"@modelcontextprotocol/sdk": "1.30.0",
|
||||
"@modelcontextprotocol/server": "2.0.0",
|
||||
"@modelcontextprotocol/sdk": "1.32.1",
|
||||
"@modelcontextprotocol/server": "2.3.1",
|
||||
"@phosphor-icons/react": "^2.1.10",
|
||||
"@posthog/mcp": "0.21.0",
|
||||
"@radix-ui/react-popover": "^1.1.15",
|
||||
|
|
|
|||
|
|
@ -7,10 +7,11 @@ import type {
|
|||
SearchResponseResultsItem,
|
||||
} from "supermemory"
|
||||
import {
|
||||
documentsApiResponseSchema,
|
||||
type DocumentMemoryEntry,
|
||||
type DocumentsApiResponse,
|
||||
type DocumentWithMemories,
|
||||
type GraphApiPage,
|
||||
type GraphDocumentsPage,
|
||||
graphApiPageSchema,
|
||||
type MemoriesList,
|
||||
type MemoryEntry,
|
||||
type MemoryEntryHistory,
|
||||
|
|
@ -22,6 +23,7 @@ export const DEFAULT_NAMESPACE = "sm_project_default"
|
|||
const FETCH_TIMEOUT_MS = 30_000
|
||||
const MCP_SOURCE = "supermemory-mcp"
|
||||
const NAMESPACE_PAGE_LIMIT = 100
|
||||
export const GRAPH_PAGE_LIMIT = 100
|
||||
// v4 defaults; v5 lowered the threshold to 0.3.
|
||||
const SEARCH_THRESHOLD = 0.6
|
||||
// Analytics matches on this text to tag quota errors, keep them in sync.
|
||||
|
|
@ -31,7 +33,7 @@ export const OUT_OF_CREDITS_MESSAGE =
|
|||
export type {
|
||||
DocumentMemoryEntry,
|
||||
DocumentWithMemories,
|
||||
DocumentsApiResponse,
|
||||
GraphDocumentsPage,
|
||||
NamespaceInfo,
|
||||
}
|
||||
|
||||
|
|
@ -171,6 +173,66 @@ export interface NamespaceSettings {
|
|||
updatedAt: string
|
||||
}
|
||||
|
||||
// Folds the v5 node/edge graph back into documents with nested memories, mirroring console-v2's use-graph-api.
|
||||
function toGraphDocuments(page: GraphApiPage): DocumentWithMemories[] {
|
||||
const memories = new Map<
|
||||
string,
|
||||
Extract<GraphApiPage["nodes"][number], { kind: "memory" }>
|
||||
>()
|
||||
for (const node of page.nodes) {
|
||||
if (node.kind === "memory") memories.set(node.id, node)
|
||||
}
|
||||
const sources = new Map<string, string[]>()
|
||||
const relations = new Map<
|
||||
string,
|
||||
NonNullable<DocumentMemoryEntry["memoryRelations"]>
|
||||
>()
|
||||
for (const edge of page.edges) {
|
||||
if (edge.type === "source") {
|
||||
sources.set(edge.source, [
|
||||
...(sources.get(edge.source) ?? []),
|
||||
edge.target,
|
||||
])
|
||||
continue
|
||||
}
|
||||
relations.set(edge.target, {
|
||||
...relations.get(edge.target),
|
||||
[edge.source]: edge.type,
|
||||
})
|
||||
}
|
||||
return page.nodes.flatMap((node) => {
|
||||
if (node.kind !== "document") return []
|
||||
return {
|
||||
id: node.id,
|
||||
title: node.title,
|
||||
summary: node.summary,
|
||||
type: node.type,
|
||||
createdAt: node.createdAt,
|
||||
updatedAt: node.updatedAt,
|
||||
memoryEntries: (sources.get(node.id) ?? []).flatMap((memoryId) => {
|
||||
const mem = memories.get(memoryId)
|
||||
if (!mem) return []
|
||||
return {
|
||||
id: mem.id,
|
||||
memory: mem.memory,
|
||||
spaceId: "",
|
||||
isStatic: mem.isStatic,
|
||||
isLatest: mem.isLatest,
|
||||
isForgotten: mem.isForgotten,
|
||||
forgetAfter: mem.forgetAfter,
|
||||
forgetReason: mem.forgetReason,
|
||||
version: mem.version,
|
||||
parentMemoryId: mem.parentMemoryId,
|
||||
rootMemoryId: mem.rootMemoryId,
|
||||
memoryRelations: relations.get(mem.id) ?? {},
|
||||
createdAt: mem.createdAt,
|
||||
updatedAt: mem.updatedAt,
|
||||
}
|
||||
}),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const isNotFound = (error: unknown): boolean => {
|
||||
const e = error as { statusCode?: number; status?: number }
|
||||
return e?.statusCode === 404 || e?.status === 404
|
||||
|
|
@ -315,28 +377,27 @@ export class SupermemoryClient {
|
|||
}
|
||||
}
|
||||
|
||||
// v5 has no graph route; this stays on the legacy endpoint until one ships.
|
||||
// The namespace graph feed is a hidden v5 route, so it isn't in the SDK.
|
||||
async getGraphDocuments(
|
||||
page = 1,
|
||||
limit = 200,
|
||||
cursor?: string,
|
||||
limit = GRAPH_PAGE_LIMIT,
|
||||
options?: { signal?: AbortSignal },
|
||||
): Promise<DocumentsApiResponse> {
|
||||
): Promise<GraphDocumentsPage> {
|
||||
try {
|
||||
const signal = options?.signal ?? AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
||||
const response = await fetch(`${this.apiUrl}/v3/documents/documents`, {
|
||||
const url = new URL(
|
||||
`${this.apiUrl}/ns/${encodeURIComponent(this.namespace)}/graph`,
|
||||
)
|
||||
url.searchParams.set("limit", String(limit))
|
||||
if (cursor) url.searchParams.set("cursor", cursor)
|
||||
const response = await fetch(url, {
|
||||
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: [this.namespace],
|
||||
}),
|
||||
body: "{}",
|
||||
signal,
|
||||
})
|
||||
if (!response.ok) {
|
||||
|
|
@ -345,7 +406,8 @@ export class SupermemoryClient {
|
|||
status: response.status,
|
||||
})
|
||||
}
|
||||
return documentsApiResponseSchema.parse(await response.json())
|
||||
const page = graphApiPageSchema.parse(await response.json())
|
||||
return { documents: toGraphDocuments(page), nextCursor: page.nextCursor }
|
||||
} catch (error) {
|
||||
this.handleError(error)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ app.use(
|
|||
app.get("/", (c) => {
|
||||
return c.json({
|
||||
name: "supermemory-mcp",
|
||||
version: "1.0.1",
|
||||
version: "1.0.2",
|
||||
description: "Supermemory MCP - AI memory for teams",
|
||||
docs: "https://supermemory.ai/docs/supermemory-mcp/mcp",
|
||||
})
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ export function createSupermemoryServer(
|
|||
const server = new McpServer(
|
||||
{
|
||||
name: "supermemory",
|
||||
version: "1.0.1",
|
||||
version: "1.0.2",
|
||||
},
|
||||
{ instructions: SERVER_INSTRUCTIONS },
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { z } from "zod"
|
||||
import { documentsApiResponseSchema } from "../../shared/types"
|
||||
import { graphDocumentsPageSchema } from "../../shared/types"
|
||||
import { GRAPH_PAGE_LIMIT } from "../client"
|
||||
import { appToolMeta } from "../app-metadata"
|
||||
import { optionalNamespaceSchema } from "../namespace"
|
||||
import { legacyNamespaceInput, namespaceArg } from "./compat"
|
||||
|
|
@ -14,10 +15,16 @@ export function register(deps: ToolDeps) {
|
|||
inputSchema: z.object({
|
||||
namespace: optionalNamespaceSchema,
|
||||
...legacyNamespaceInput,
|
||||
page: z.number().int().min(1).max(10_000).optional().default(1),
|
||||
limit: z.number().int().min(1).max(1_000).optional().default(200),
|
||||
cursor: z.string().min(1).max(2048).optional(),
|
||||
limit: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(GRAPH_PAGE_LIMIT)
|
||||
.optional()
|
||||
.default(GRAPH_PAGE_LIMIT),
|
||||
}),
|
||||
outputSchema: documentsApiResponseSchema,
|
||||
outputSchema: graphDocumentsPageSchema,
|
||||
annotations: READ_ONLY_TOOL_ANNOTATIONS,
|
||||
_meta: appToolMeta(["app"]),
|
||||
},
|
||||
|
|
@ -25,7 +32,7 @@ export function register(deps: ToolDeps) {
|
|||
try {
|
||||
const namespace = await deps.resolveNamespace(namespaceArg(args))
|
||||
const client = deps.getClient(namespace)
|
||||
const data = await client.getGraphDocuments(args.page, args.limit)
|
||||
const data = await client.getGraphDocuments(args.cursor, args.limit)
|
||||
|
||||
return {
|
||||
content: [
|
||||
|
|
|
|||
|
|
@ -1,11 +1,17 @@
|
|||
import { z } from "zod"
|
||||
import { graphViewSchema, type ViewMessage } from "../../shared/types"
|
||||
import {
|
||||
type DocumentWithMemories,
|
||||
graphViewSchema,
|
||||
type ViewMessage,
|
||||
} from "../../shared/types"
|
||||
import { appResultMeta, appToolMeta } from "../app-metadata"
|
||||
import { optionalNamespaceSchema } from "../namespace"
|
||||
import { legacyNamespaceInput, namespaceArg, withLegacyView } from "./compat"
|
||||
import { READ_ONLY_TOOL_ANNOTATIONS } from "./annotations"
|
||||
import { textContent, type ToolDeps } from "./types"
|
||||
|
||||
const GRAPH_DOCUMENT_LIMIT = 200
|
||||
|
||||
export function register(deps: ToolDeps) {
|
||||
const inputSchema = z.object({
|
||||
namespace: optionalNamespaceSchema,
|
||||
|
|
@ -28,8 +34,14 @@ export function register(deps: ToolDeps) {
|
|||
const viewId = crypto.randomUUID()
|
||||
const namespace = await deps.resolveNamespace(namespaceArg(args))
|
||||
const client = deps.getClient(namespace)
|
||||
const result = await client.getGraphDocuments(1, 200)
|
||||
const memoryCount = result.documents.reduce(
|
||||
const documents: DocumentWithMemories[] = []
|
||||
let cursor: string | undefined
|
||||
do {
|
||||
const page = await client.getGraphDocuments(cursor)
|
||||
documents.push(...page.documents)
|
||||
cursor = page.nextCursor ?? undefined
|
||||
} while (cursor && documents.length < GRAPH_DOCUMENT_LIMIT)
|
||||
const memoryCount = documents.reduce(
|
||||
(sum, document) => sum + document.memoryEntries.length,
|
||||
0,
|
||||
)
|
||||
|
|
@ -37,18 +49,18 @@ export function register(deps: ToolDeps) {
|
|||
view: "graph",
|
||||
viewId,
|
||||
namespace,
|
||||
documents: result.documents,
|
||||
totalCount: result.pagination.totalItems,
|
||||
documentCount: result.documents.length,
|
||||
documents,
|
||||
totalCount: documents.length,
|
||||
documentCount: documents.length,
|
||||
memoryCount,
|
||||
totalDocumentCount: result.pagination.totalItems,
|
||||
truncated: result.documents.length < result.pagination.totalItems,
|
||||
totalDocumentCount: documents.length,
|
||||
truncated: Boolean(cursor),
|
||||
rendered: true,
|
||||
})
|
||||
return {
|
||||
content: [
|
||||
textContent(
|
||||
`Rendered the interactive Memory Graph MCP App: ${result.documents.length} documents, ${memoryCount} memories. Space: ${namespace}. Do not create a duplicate graph or artifact unless the user explicitly requests one.`,
|
||||
`Rendered the interactive Memory Graph MCP App: ${documents.length} documents, ${memoryCount} memories. Space: ${namespace}. Do not create a duplicate graph or artifact unless the user explicitly requests one.`,
|
||||
),
|
||||
],
|
||||
structuredContent: sc,
|
||||
|
|
|
|||
|
|
@ -122,12 +122,53 @@ export const paginationSchema = z.object({
|
|||
totalPages: z.number().int().nonnegative(),
|
||||
})
|
||||
|
||||
export const documentsApiResponseSchema = z.object({
|
||||
export const graphDocumentsPageSchema = z.object({
|
||||
documents: z.array(documentWithMemoriesSchema),
|
||||
pagination: paginationSchema,
|
||||
nextCursor: z.string().nullable(),
|
||||
})
|
||||
|
||||
export type DocumentsApiResponse = z.infer<typeof documentsApiResponseSchema>
|
||||
export type GraphDocumentsPage = z.infer<typeof graphDocumentsPageSchema>
|
||||
|
||||
const graphApiNodeSchema = z.discriminatedUnion("kind", [
|
||||
z.looseObject({
|
||||
kind: z.literal("document"),
|
||||
id: z.string(),
|
||||
title: z.string().nullable(),
|
||||
summary: z.string().nullable(),
|
||||
type: z.string(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
}),
|
||||
z.looseObject({
|
||||
kind: z.literal("memory"),
|
||||
id: z.string(),
|
||||
memory: z.string(),
|
||||
isStatic: z.boolean(),
|
||||
isLatest: z.boolean(),
|
||||
isForgotten: z.boolean(),
|
||||
forgetAfter: z.string().nullable(),
|
||||
forgetReason: z.string().nullable(),
|
||||
version: z.number(),
|
||||
parentMemoryId: z.string().nullable(),
|
||||
rootMemoryId: z.string().nullable(),
|
||||
createdAt: z.string(),
|
||||
updatedAt: z.string(),
|
||||
}),
|
||||
])
|
||||
|
||||
export const graphApiPageSchema = z.object({
|
||||
nodes: z.array(graphApiNodeSchema),
|
||||
edges: z.array(
|
||||
z.looseObject({
|
||||
type: z.union([z.literal("source"), memoryRelationSchema]),
|
||||
source: z.string(),
|
||||
target: z.string(),
|
||||
}),
|
||||
),
|
||||
nextCursor: z.string().nullable(),
|
||||
})
|
||||
|
||||
export type GraphApiPage = z.infer<typeof graphApiPageSchema>
|
||||
|
||||
// Shared by the client and the list_memories output schema so the two can't drift.
|
||||
export const memoryEntryHistorySchema = z.object({
|
||||
|
|
|
|||
|
|
@ -16,8 +16,7 @@ body {
|
|||
margin: 0;
|
||||
background: var(--ink);
|
||||
color: var(--text);
|
||||
font-family:
|
||||
var(--font-sans), -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,6 @@
|
|||
import type { Metadata, Viewport } from "next"
|
||||
import { DM_Sans } from "next/font/google"
|
||||
import "./globals.css"
|
||||
|
||||
const font = DM_Sans({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-sans",
|
||||
weight: ["400", "500", "600"],
|
||||
})
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL("https://app.supermemory.ai"),
|
||||
title: "Supermemory has moved",
|
||||
|
|
@ -43,7 +36,7 @@ export default function RootLayout({
|
|||
children,
|
||||
}: Readonly<{ children: React.ReactNode }>) {
|
||||
return (
|
||||
<html className={font.variable} lang="en">
|
||||
<html lang="en">
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { defineCloudflareConfig } from "@opennextjs/cloudflare"
|
||||
import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache"
|
||||
import staticAssetsIncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/static-assets-incremental-cache"
|
||||
|
||||
export default defineCloudflareConfig({
|
||||
incrementalCache: r2IncrementalCache,
|
||||
incrementalCache: staticAssetsIncrementalCache,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -19,8 +19,8 @@
|
|||
"cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@opennextjs/cloudflare": "^1.20.8",
|
||||
"next": "16.3.8",
|
||||
"@opennextjs/cloudflare": "^1.20.15",
|
||||
"next": "16.4.1",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4"
|
||||
},
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
"@types/react": "^19.2.9",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"typescript": "^5.8.3",
|
||||
"wrangler": "^4.125.0"
|
||||
"wrangler": "^4.148.0"
|
||||
},
|
||||
"overrides": {
|
||||
"@types/react": "^19.2.9",
|
||||
|
|
|
|||
|
|
@ -21,11 +21,5 @@
|
|||
// The service should match the "name" of your worker
|
||||
"service": "supermemory-app"
|
||||
}
|
||||
],
|
||||
"r2_buckets": [
|
||||
{
|
||||
"binding": "NEXT_INC_CACHE_R2_BUCKET",
|
||||
"bucket_name": "supermemory-console-cache"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,10 +82,13 @@
|
|||
"ip-address": "10.7.3",
|
||||
"jsondiffpatch": "0.7.6",
|
||||
"lodash": "4.18.1",
|
||||
"@modelcontextprotocol/client": "2.3.1",
|
||||
"@modelcontextprotocol/sdk": "1.32.1",
|
||||
"@modelcontextprotocol/server": "2.3.1",
|
||||
"node-forge": "1.4.0",
|
||||
"postcss": "8.5.23",
|
||||
"protobufjs": "7.6.6",
|
||||
"sharp": "0.35.4",
|
||||
"sharp": "0.35.5",
|
||||
"socket.io-parser": "4.2.7",
|
||||
"undici": "7.29.1",
|
||||
"ws": "8.21.3"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue