feat: add listMemories tool to the Supermemory MCP server

This commit is contained in:
Matt Van Horn 2026-06-03 23:04:23 -07:00
parent 14b33401e1
commit bc7796df3f
No known key found for this signature in database
2 changed files with 306 additions and 1 deletions

View file

@ -2,6 +2,8 @@ import Supermemory from "supermemory"
const MAX_CHARS = 200000 // ~50k tokens (character-based limit)
const DEFAULT_PROJECT_ID = "sm_project_default"
const DEFAULT_LIST_LIMIT = 50
const MAX_LIST_LIMIT = 200
export type Memory =
| {
@ -25,6 +27,48 @@ export interface SearchResult {
timing: number
}
type ListMemoryBase = {
id: string
title?: string
content?: string
summary?: string
createdAt?: string
updatedAt?: string
metadata?: unknown
status?: string
containerTags?: string[]
customId?: string | null
type?: string
connectionId?: string | null
}
export type ListedMemory =
| (ListMemoryBase & { memory: string })
| (ListMemoryBase & { chunk: string })
export type ListMemoriesSort =
| "createdAt"
| "updatedAt"
| "-createdAt"
| "-updatedAt"
| "createdAt:asc"
| "createdAt:desc"
| "updatedAt:asc"
| "updatedAt:desc"
export interface ListMemoriesOptions {
containerTag?: string
limit?: number
cursor?: string
sort?: ListMemoriesSort
filter?: string
}
export interface ListMemoriesResult {
memories: ListedMemory[]
nextCursor: string | null
}
export interface Profile {
static: string[]
dynamic: string[]
@ -82,7 +126,7 @@ export interface DocumentsApiResponse {
}
}
export function getMemoryText(m: Memory): string {
export function getMemoryText(m: Memory | ListedMemory): string {
return "memory" in m ? m.memory : m.chunk
}
@ -101,6 +145,121 @@ interface SDKResult {
context?: string
}
interface SDKListMemory {
id: string
memory?: string
chunk?: string
content?: string | null
summary?: string | null
title?: string | null
createdAt?: string
updatedAt?: string
metadata?: unknown
status?: string
containerTags?: string[]
customId?: string | null
type?: string
connectionId?: string | null
}
interface SDKListResponse {
memories?: SDKListMemory[]
results?: SDKListMemory[]
pagination?: {
currentPage: number
totalPages: number
}
nextCursor?: string | null
}
function clampListLimit(limit = DEFAULT_LIST_LIMIT): number {
return Math.min(Math.max(Math.trunc(limit), 1), MAX_LIST_LIMIT)
}
function parseListCursor(cursor?: string): number {
if (!cursor) return 1
const page = Number(cursor)
if (!Number.isInteger(page) || page < 1) {
throw new Error(
"Invalid cursor. Use the nextCursor value from listMemories.",
)
}
return page
}
function parseListSort(sort: ListMemoriesSort = "-createdAt"): {
sort: "createdAt" | "updatedAt"
order: "asc" | "desc"
} {
if (sort.startsWith("-")) {
return {
sort: sort.slice(1) as "createdAt" | "updatedAt",
order: "desc",
}
}
if (sort.includes(":")) {
const [field, order] = sort.split(":") as [
"createdAt" | "updatedAt",
"asc" | "desc",
]
return { sort: field, order }
}
return {
sort: sort as "createdAt" | "updatedAt",
order: "desc",
}
}
function normalizeListedMemory(memory: SDKListMemory): ListedMemory {
const content =
typeof memory.content === "string"
? limitByChars(memory.content)
: undefined
const summary =
typeof memory.summary === "string"
? limitByChars(memory.summary)
: undefined
const text = limitByChars(
memory.content ||
memory.memory ||
memory.chunk ||
memory.summary ||
memory.title ||
"",
)
const base: ListMemoryBase = {
id: memory.id,
title: memory.title || undefined,
content,
summary,
createdAt: memory.createdAt,
updatedAt: memory.updatedAt,
metadata: memory.metadata,
status: memory.status,
containerTags: memory.containerTags,
customId: memory.customId,
type: memory.type,
connectionId: memory.connectionId,
}
if (memory.chunk && !memory.memory) {
return { ...base, chunk: text }
}
return { ...base, memory: text }
}
function matchesFilter(memory: ListedMemory, filter?: string): boolean {
const normalizedFilter = filter?.trim().toLowerCase()
if (!normalizedFilter) return true
return [getMemoryText(memory), memory.content, memory.summary, memory.title]
.filter((value): value is string => typeof value === "string")
.some((value) => value.toLowerCase().includes(normalizedFilter))
}
export class SupermemoryClient {
private client: Supermemory
private containerTag: string
@ -255,6 +414,42 @@ export class SupermemoryClient {
}
}
// List memories/documents with pagination using SDK
async listMemories(
options: ListMemoriesOptions = {},
): Promise<ListMemoriesResult> {
try {
const limit = clampListLimit(options.limit)
const page = parseListCursor(options.cursor)
const { sort, order } = parseListSort(options.sort)
const result = (await this.client.documents.list({
containerTags: [options.containerTag || this.containerTag],
includeContent: true,
limit,
page,
sort,
order,
})) as SDKListResponse
const rawMemories = result.memories || result.results || []
const memories = rawMemories
.map(normalizeListedMemory)
.filter((memory) => matchesFilter(memory, options.filter))
const nextCursor =
result.nextCursor ??
(result.pagination &&
result.pagination.currentPage < result.pagination.totalPages
? String(result.pagination.currentPage + 1)
: null)
return {
memories,
nextCursor,
}
} catch (error) {
this.handleError(error)
}
}
// Get user profile using SDK
async getProfile(query?: string): Promise<ProfileResponse> {
try {

View file

@ -89,6 +89,45 @@ export class SupermemoryMCP extends McpAgent<Env, unknown, Props> {
...(hasRootContainerTag ? {} : containerTagField),
})
const listMemoriesSchema = z.object({
limit: z
.number()
.int("Limit must be an integer")
.min(1, "Limit must be at least 1")
.max(200, "Limit cannot exceed 200")
.optional()
.default(50)
.describe(
"Maximum number of memories to return (default: 50, max: 200)",
),
cursor: z
.string()
.optional()
.describe("Cursor from a previous listMemories response"),
sort: z
.enum([
"createdAt",
"updatedAt",
"-createdAt",
"-updatedAt",
"createdAt:asc",
"createdAt:desc",
"updatedAt:asc",
"updatedAt:desc",
])
.optional()
.default("-createdAt")
.describe("Sort order for listed memories"),
filter: z
.string()
.max(1000, "Filter exceeds maximum length of 1,000 characters")
.optional()
.describe(
"Case-insensitive substring filter applied to memory content",
),
...(hasRootContainerTag ? {} : containerTagField),
})
const contextPromptSchema = z.object({
includeRecent: z
.boolean()
@ -101,6 +140,7 @@ export class SupermemoryMCP extends McpAgent<Env, unknown, Props> {
type ContextPromptArgs = z.infer<typeof contextPromptSchema>
type MemoryArgs = z.infer<typeof memorySchema>
type RecallArgs = z.infer<typeof recallSchema>
type ListMemoriesArgs = z.infer<typeof listMemoriesSchema>
// Register memory tool
this.server.registerTool(
@ -126,6 +166,18 @@ export class SupermemoryMCP extends McpAgent<Env, unknown, Props> {
(args: RecallArgs) => this.handleRecall(args),
)
// Register list memories tool
this.server.registerTool(
"listMemories",
{
description:
"List the user's stored memories with pagination. Use this to audit, clean, or diff memories before save/forget operations.",
inputSchema: listMemoriesSchema,
},
// @ts-expect-error - zod type inference issue with MCP SDK
(args: ListMemoriesArgs) => this.handleListMemories(args),
)
// Register profile resource
this.server.registerResource(
"User Profile",
@ -747,6 +799,64 @@ export class SupermemoryMCP extends McpAgent<Env, unknown, Props> {
}
}
private async handleListMemories(args: {
limit?: number
cursor?: string
sort?:
| "createdAt"
| "updatedAt"
| "-createdAt"
| "-updatedAt"
| "createdAt:asc"
| "createdAt:desc"
| "updatedAt:asc"
| "updatedAt:desc"
filter?: string
containerTag?: string
}) {
const {
containerTag,
cursor,
filter,
limit = 50,
sort = "-createdAt",
} = args
try {
const client = this.getClient(containerTag)
const result = await client.listMemories({
containerTag: containerTag || this.props?.containerTag,
cursor,
filter,
limit,
sort,
})
return {
content: [
{
type: "text" as const,
text: JSON.stringify(result, null, 2),
},
],
structuredContent: result,
}
} catch (error) {
const message =
error instanceof Error ? error.message : "An unexpected error occurred"
console.error("List memories operation failed:", error)
return {
content: [
{
type: "text" as const,
text: `Error: ${message}`,
},
],
isError: true,
}
}
}
private async getClientInfo(): Promise<
{ name: string; version?: string } | undefined
> {