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
49 changes: 47 additions & 2 deletions apps/mcp/e2e/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js"
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"
import {
ElicitRequestSchema,
type ElicitResult,
} from "@modelcontextprotocol/sdk/types.js"

export const MCP_URL =
process.env.SUPERMEMORY_MCP_URL ?? "https://mcp.supermemory.ai/mcp"
Expand Down Expand Up @@ -89,12 +93,45 @@ export function textOf(res: CallResult): string {
.join("\n")
}

export function documentIdOf(res: CallResult): string | undefined {
return textOf(res).match(/id: ([^)]+)/)?.[1]
}

export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))

export async function deleteDocument(
documentId: string,
{ tries = 60, delayMs = 5000 } = {},
): Promise<void> {
if (!API_KEY) throw new Error("SUPERMEMORY_API_KEY is required")
for (let i = 0; i < tries; i++) {
const response = await fetch(
`${API_URL}/v3/documents/${encodeURIComponent(documentId)}`,
{
method: "DELETE",
headers: { Authorization: `Bearer ${API_KEY}` },
},
)
if (response.ok || response.status === 404) return
if (response.status !== 409) {
throw new Error(
`Document cleanup failed for ${documentId}: ${response.status}`,
)
}
if (i < tries - 1) await sleep(delayMs)
}
throw new Error(`Document cleanup remained busy for ${documentId}`)
}

export type Session = { client: Client; close: () => Promise<void> }

export async function connect(
opts: { apiKey?: string; token?: string; containerTag?: string } = {},
opts: {
apiKey?: string
token?: string
containerTag?: string
onElicitation?: () => ElicitResult | Promise<ElicitResult>
} = {},
): Promise<Session> {
const headers: Record<string, string> = {
Authorization: `Bearer ${opts.token ?? opts.apiKey ?? API_KEY}`,
Expand All @@ -104,7 +141,15 @@ export async function connect(
const transport = new StreamableHTTPClientTransport(new URL(MCP_URL), {
requestInit: { headers },
})
const client = new Client({ name: "sm-mcp-e2e", version: "0.0.1" })
const client = new Client(
{ name: "sm-mcp-e2e", version: "0.0.1" },
opts.onElicitation
? { capabilities: { elicitation: { form: {} } } }
: undefined,
)
if (opts.onElicitation) {
client.setRequestHandler(ElicitRequestSchema, opts.onElicitation)
}
await client.connect(transport)
return {
client,
Expand Down
46 changes: 31 additions & 15 deletions apps/mcp/e2e/list-memories.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { randomUUID } from "node:crypto"
import { afterAll, beforeAll, describe, expect, it } from "vitest"
import {
API_URL,
API_KEY,
callTool,
connect,
deleteDocument,
type Session,
sleep,
textOf,
} from "./helpers"

// listMemories reads extracted memory entries, which appear only after the
// async ingestion pipeline finishes — poll like recallUntil does.
// listMemories can be briefly eventually-consistent after fixture creation.
async function listUntil(
s: Session,
needle: string,
Expand All @@ -28,30 +29,45 @@ async function listUntil(

describe.skipIf(!API_KEY)("MCP — listMemories", () => {
let s: Session
const created: string[] = []
const createdDocumentIds: string[] = []

beforeAll(async () => {
s = await connect()
})
afterAll(async () => {
for (const content of created) {
await callTool(s.client, "memory", {
content,
action: "forget",
}).catch(() => {})
try {
await Promise.all(
createdDocumentIds.map((documentId) => deleteDocument(documentId)),
)
} finally {
await s?.close()
}
await s?.close()
})
}, 330_000)

it("lists a saved memory without dumping document content", async () => {
const marker = `lm-${randomUUID()}`
const content = `e2e listMemories. token=${marker}. The list test fruit is rambutan.`
created.push(content)

const save = await callTool(s.client, "memory", { content, action: "save" })
expect(save.isError).toBeFalsy()
const created = await fetch(`${API_URL}/v4/memories`, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
containerTag: "sm_project_default",
memories: [{ content }],
}),
})
expect(
created.ok,
`memory fixture creation failed: ${created.status}`,
).toBe(true)
const documentId = ((await created.json()) as { documentId?: string })
.documentId
expect(documentId).toBeTruthy()
if (documentId) createdDocumentIds.push(documentId)

const listing = await listUntil(s, marker)
const listing = await listUntil(s, marker, { tries: 6, delayMs: 1000 })
expect(
listing,
`listMemories never returned marker ${marker}`,
Expand Down
Loading