mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-03 02:21:23 +00:00
feat: add listMemories tool to the Supermemory MCP server
This commit is contained in:
parent
14b33401e1
commit
bc7796df3f
2 changed files with 306 additions and 1 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
> {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue