import { Client } from "@modelcontextprotocol/sdk/client/index.js" import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" export const MCP_URL = process.env.SUPERMEMORY_MCP_URL ?? "https://mcp.supermemory.ai/mcp" export const API_KEY = process.env.SUPERMEMORY_API_KEY export const ORIGIN = new URL(MCP_URL).origin export const API_URL = process.env.SUPERMEMORY_API_URL ?? "https://api.supermemory.ai" // Tier D (real OAuth token) creds — captured once via e2e/capture-oauth-token.ts. export const OAUTH_REFRESH_TOKEN = process.env.SUPERMEMORY_MCP_REFRESH_TOKEN export const OAUTH_CLIENT_ID = process.env.SUPERMEMORY_MCP_CLIENT_ID export type AuthServerMetadata = { authorization_endpoint: string token_endpoint: string registration_endpoint: string grant_types_supported?: string[] code_challenge_methods_supported?: string[] response_types_supported?: string[] } // Walk the discovery chain a real MCP client follows: protected-resource → authorization server. export async function authServerMetadata(): Promise<{ authServer: string metadata: AuthServerMetadata }> { const prRes = await fetch( `${ORIGIN}/.well-known/oauth-protected-resource/mcp`, ) const pr = (await prRes.json()) as { authorization_servers?: string[] } const authServer = pr.authorization_servers?.[0] if (!authServer) throw new Error("no authorization_servers in metadata") const metaRes = await fetch( `${authServer}/.well-known/oauth-authorization-server`, ) return { authServer, metadata: (await metaRes.json()) as AuthServerMetadata } } export async function registerClient(registrationEndpoint: string): Promise<{ status: number body: { client_id?: string; grant_types?: string[] } }> { const res = await fetch(registrationEndpoint, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ client_name: "sm-mcp-e2e", redirect_uris: ["http://localhost:8765/callback"], grant_types: ["authorization_code", "refresh_token"], response_types: ["code"], token_endpoint_auth_method: "none", }), }) return { status: res.status, body: await res.json() } } export async function exchangeRefreshToken( tokenEndpoint: string, refreshToken: string, clientId: string, ): Promise<{ status: number body: { access_token?: string; error?: string } }> { const res = await fetch(tokenEndpoint, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: clientId, }), }) return { status: res.status, body: await res.json() } } export type CallResult = { content?: Array<{ type: string; text?: string }> structuredContent?: unknown isError?: boolean } export function textOf(res: CallResult): string { return (res.content ?? []) .filter((c) => c.type === "text" && c.text) .map((c) => c.text) .join("\n") } export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)) export type Session = { client: Client; close: () => Promise } export async function connect( opts: { apiKey?: string; token?: string; containerTag?: string } = {}, ): Promise { const headers: Record = { Authorization: `Bearer ${opts.token ?? opts.apiKey ?? API_KEY}`, } if (opts.containerTag) headers["x-sm-project"] = opts.containerTag const transport = new StreamableHTTPClientTransport(new URL(MCP_URL), { requestInit: { headers }, }) const client = new Client({ name: "sm-mcp-e2e", version: "0.0.1" }) await client.connect(transport) return { client, close: () => transport.close().catch(() => {}), } } export async function callTool( client: Client, name: string, args: Record = {}, ): Promise { return (await client.callTool({ name, arguments: args })) as CallResult } // recall is eventually-consistent (save → ingestion pipeline → memories), so poll. export async function recallUntil( client: Client, query: string, needle: string, { tries = 18, delayMs = 5000, containerTag = undefined as string | undefined, } = {}, ): Promise { for (let i = 0; i < tries; i++) { const res = await callTool(client, "recall", { query, includeProfile: false, ...(containerTag ? { containerTag } : {}), }) const txt = textOf(res) if (txt.includes(needle)) return txt await sleep(delayMs) } return null } // forget only matches extracted memory entries, not raw chunks, so a just-saved doc // returns "No matching memory found..." until extraction completes — poll for real removal. export async function forgetUntilForgotten( client: Client, content: string, { tries = 18, delayMs = 5000, containerTag = undefined as string | undefined, } = {}, ): Promise { for (let i = 0; i < tries; i++) { const res = await callTool(client, "memory", { content, action: "forget", ...(containerTag ? { containerTag } : {}), }) if (!res.isError && /forgot/i.test(textOf(res))) return textOf(res) await sleep(delayMs) } return null } // poll until a memory is NO LONGER returned (for verifying forget). export async function recallUntilAbsent( client: Client, query: string, needle: string, { tries = 12, delayMs = 5000 } = {}, ): Promise { for (let i = 0; i < tries; i++) { const res = await callTool(client, "recall", { query, includeProfile: false, }) if (!textOf(res).includes(needle)) return true await sleep(delayMs) } return false }