Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 196 additions & 1 deletion apps/mcp/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
| {
Expand All @@ -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[]
Expand Down Expand Up @@ -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
}

Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
110 changes: 110 additions & 0 deletions apps/mcp/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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(
Expand All @@ -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",
Expand Down Expand Up @@ -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
> {
Expand Down