Skip to content
Merged
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
55 changes: 42 additions & 13 deletions apps/mcp/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,49 @@ import Supermemory from "supermemory"
const MAX_CHARS = 200000 // ~50k tokens (character-based limit)
const DEFAULT_PROJECT_ID = "sm_project_default"

interface MemoryRichFields {
metadata?: Record<string, unknown> | null
updatedAt?: string
context?: Record<string, unknown>
documents?: Array<Record<string, unknown>>
isAggregated?: boolean
}

export type Memory =
| {
| ({
id: string
memory: string
similarity: number
title?: string
content?: string
}
| {
} & MemoryRichFields)
| ({
id: string
chunk: string
similarity: number
title?: string
content?: string
}
} & MemoryRichFields)

export interface SearchResult {
results: Memory[]
total: number
timing: number
}

export interface SearchOptions {
searchMode?: "memories" | "hybrid" | "documents"
rerank?: boolean
rewriteQuery?: boolean
include?: {
documents?: boolean
relatedMemories?: boolean
summaries?: boolean
chunks?: boolean
forgottenMemories?: boolean
}
}

export interface Profile {
static: string[]
dynamic: string[]
Expand Down Expand Up @@ -98,7 +119,11 @@ interface SDKResult {
content?: string
similarity: number
title?: string
context?: string
metadata?: Record<string, unknown> | null
updatedAt?: string
context?: Record<string, unknown>
documents?: Array<Record<string, unknown>>
isAggregated?: boolean
}

export class SupermemoryClient {
Expand Down Expand Up @@ -218,26 +243,32 @@ export class SupermemoryClient {
query: string,
limit = 10,
threshold?: number,
options?: SearchOptions,
): Promise<SearchResult> {
try {
const result = await this.client.search.memories({
q: query,
limit,
containerTag: this.containerTag,
searchMode: "hybrid",
searchMode: options?.searchMode ?? "hybrid",
threshold, // Optional threshold parameter
rerank: options?.rerank,
rewriteQuery: options?.rewriteQuery,
include: options?.include,
})

// Normalize and limit response size — preserve memory vs chunk distinction
const results: Memory[] = (result.results as SDKResult[]).map((r) => {
const text = limitByChars(
r.content || r.memory || r.chunk || r.context || "",
)
const text = limitByChars(r.content || r.memory || r.chunk || "")
const base = {
id: r.id,
similarity: r.similarity,
title: r.title,
content: r.content,
metadata: r.metadata,
updatedAt: r.updatedAt,
context: r.context,
documents: r.documents,
isAggregated: r.isAggregated,
}
if (r.chunk && !r.memory) {
return { ...base, chunk: text }
Expand Down Expand Up @@ -273,9 +304,7 @@ export class SupermemoryClient {
if (result.searchResults) {
response.searchResults = {
results: (result.searchResults.results as SDKResult[]).map((r) => {
const text = limitByChars(
r.content || r.memory || r.chunk || r.context || "",
)
const text = limitByChars(r.content || r.memory || r.chunk || "")
const base = {
id: r.id,
similarity: r.similarity,
Expand Down
157 changes: 157 additions & 0 deletions apps/mcp/src/format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
export function formatMemories(
response: { results?: Array<Record<string, unknown>>; total?: number },
opts: {
minSimilarity?: number
maxRelations?: number
maxDocuments?: number
maxChunkLength?: number
includeScores?: boolean
includeLegend?: boolean
} = {},
) {
const {
minSimilarity = 0,
maxRelations = 4,
maxDocuments = 3,
maxChunkLength = Number.POSITIVE_INFINITY,
includeScores = true,
includeLegend = true,
} = opts

const day = (s: string | null | undefined) => s?.slice(0, 10) ?? ""
const mime = (m: string | undefined) =>
!m
? ""
: m === "application/pdf"
? "pdf"
: m.includes("spreadsheet")
? "xlsx"
: m.includes("presentation")
? "pptx"
: m.includes("document")
? "doc"
: (m.split("/").pop() ?? "")

const temporal = (tc: Record<string, unknown> | undefined) => {
if (!tc) return [] as string[]
const ev = ((tc.eventDate as string[]) ?? []).map(day).filter(Boolean)
return [
tc.documentDate && `doc ${day(tc.documentDate as string)}`,
ev.length === 1 && `event ${ev[0]}`,
ev.length > 1 && `event ${ev[0]} → ${ev.at(-1)}`,
].filter(Boolean) as string[]
}

const describeMeta = (m: Record<string, unknown> | undefined | null) => {
if (!m) return ""
const tags = [
mime(m.mimeType as string | undefined),
m.source as string | undefined,
...temporal(m.temporalContext as Record<string, unknown> | undefined),
].filter(Boolean)
return [m.title && `"${m.title}"`, tags.length && `(${tags.join(", ")})`]
.filter(Boolean)
.join(" ")
}

const renderRelations = (
rels: Array<Record<string, unknown>> | undefined,
arrow: string,
root: string,
) => {
if (!rels?.length) return [] as string[]
const seen = new Set<string>()
const items = rels.filter((r) => {
const k = (r.memory as string).trim()
if (k === root.trim() || seen.has(k)) return false
seen.add(k)
return true
})
const shown = items.slice(0, maxRelations)
const lines = shown.map((r) => {
const t = temporal(
(r.metadata as Record<string, unknown> | undefined)?.temporalContext as
| Record<string, unknown>
| undefined,
)
const when = t.length ? t.join(", ") : day(r.updatedAt as string)
return ` ${arrow} ${r.relation}${when ? `, ${when}` : ""}: ${r.memory}`
})
if (items.length > shown.length)
lines.push(` ${arrow} … +${items.length - shown.length} more`)
return lines
}

const renderDocs = (ds: Array<Record<string, unknown>> | undefined) =>
(ds ?? []).slice(0, maxDocuments).map((d) => {
const title = d.title ? `"${d.title}"` : "(untitled)"
const type = d.type ? ` (${d.type})` : ""
const summary = d.summary ? ` — ${d.summary}` : ""
return ` Document: ${title}${type}${summary}`
})

const results = (response.results ?? []).filter(
(m) => ((m.similarity as number) ?? 0) >= minSimilarity,
)
if (!results.length) return "No relevant memories found."

const total = response.total ?? results.length
const header = [
`${results.length} memor${results.length === 1 ? "y" : "ies"}` +
(total !== results.length ? ` of ${total}` : "") +
", ranked by relevance.",
includeLegend &&
"Markers: 'agg' = aggregated synthesis, 'chunk' = raw excerpt; ← parent, → child, ~ related.",
]
.filter(Boolean)
.join(" ")

const arrows = [
["parents", "←"],
["children", "→"],
["related", "~"],
] as const

const blocks = results.map((m) => {
const score = (m.similarity as number)?.toFixed(2) ?? "—"
const prefix = includeScores ? `${score} ` : ""
const memory = (m.memory as string) ?? ""

if (m.isAggregated) return `${prefix}agg ${memory}`

if (m.chunk != null && m.memory == null) {
const body = (m.chunk as string).replace(/\s+$/, "")
const text =
body.length > maxChunkLength
? `${body.slice(0, maxChunkLength)} … [truncated, ${body.length - maxChunkLength} more chars]`
: body
return [
`${prefix}chunk ${describeMeta(m.metadata as Record<string, unknown> | null)}`.trimEnd(),
...renderDocs(
m.documents as Array<Record<string, unknown>> | undefined,
),
...text.split("\n").map((l: string) => ` ${l}`),
].join("\n")
}

const meta = describeMeta(m.metadata as Record<string, unknown> | null)
const ctx = (m.context ?? {}) as Record<
string,
Array<Record<string, unknown>>
>
return [
`${prefix}${memory}`,
meta
? ` Source: ${meta}`
: day(m.updatedAt as string)
? ` Source: updated ${day(m.updatedAt as string)}`
: null,
...renderDocs(m.documents as Array<Record<string, unknown>> | undefined),
...arrows.flatMap(([k, a]) => renderRelations(ctx[k], a, memory)),
]
.filter(Boolean)
.join("\n")
})

return [header, "", blocks.join("\n\n")].join("\n")
}
Loading
Loading