fix(mcp): clarify memory graph pagination (#1032)

Co-authored-by: Vedant Mahajan <vedant.04.mahajan@gmail.com>
This commit is contained in:
Rāna(Bass Ver.) 2026-06-17 21:08:19 +08:00 committed by GitHub
parent 6a4e0b2514
commit feca81f69e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 205 additions and 36 deletions

View file

@ -16,6 +16,8 @@
<div id="stats"></div>
<button type="button" id="load-more-btn" hidden>Load more</button>
<div id="popup">
<span id="popup-type"></span>
<div id="popup-title"></div>

View file

@ -0,0 +1,48 @@
import type { DocumentWithMemories, DocumentsApiResponse } from "./client"
export const GRAPH_PAGE_LIMIT = 10
export interface MemoryGraphStructuredContent {
containerTag?: string
documents: DocumentWithMemories[]
totalCount: number
loadedCount: number
pagination: DocumentsApiResponse["pagination"]
}
export function createMemoryGraphStructuredContent(
result: DocumentsApiResponse,
containerTag?: string,
): MemoryGraphStructuredContent {
return {
containerTag,
documents: result.documents,
totalCount: result.pagination.totalItems,
loadedCount: result.documents.length,
pagination: result.pagination,
}
}
export function summarizeMemoryGraphResult(
result: DocumentsApiResponse,
containerTag?: string,
): string {
let memoryCount = 0
for (const document of result.documents) {
memoryCount += document.memoryEntries.length
}
const textParts = [
`Memory Graph: showing ${result.documents.length} of ${result.pagination.totalItems} documents, ${memoryCount} loaded memories`,
]
if (result.pagination.totalPages > result.pagination.currentPage) {
textParts.push(
`More documents available (${result.pagination.totalPages} pages total)`,
)
}
if (containerTag) {
textParts.push(`Project: ${containerTag}`)
}
return textParts.join(". ")
}

View file

@ -5,7 +5,12 @@ import {
registerAppResource,
RESOURCE_MIME_TYPE,
} from "@modelcontextprotocol/ext-apps/server"
import { SupermemoryClient } from "./client"
import { SupermemoryClient, getMemoryText } from "./client"
import {
GRAPH_PAGE_LIMIT,
createMemoryGraphStructuredContent,
summarizeMemoryGraphResult,
} from "./memory-graph-data"
import { formatMemories } from "./format"
import { initPosthog, posthog } from "./posthog"
import { z } from "zod"
@ -321,26 +326,23 @@ export class SupermemoryMCP extends McpAgent<Env, unknown, Props> {
? [effectiveContainerTag]
: undefined
const result = await client.getDocuments(containerTags, 1, 10)
const memoryCount = result.documents.reduce(
(sum, d) => sum + d.memoryEntries.length,
0,
const result = await client.getDocuments(
containerTags,
1,
GRAPH_PAGE_LIMIT,
)
const textParts = [
`Memory Graph: ${result.documents.length} documents, ${memoryCount} memories`,
]
if (effectiveContainerTag) {
textParts.push(`Project: ${effectiveContainerTag}`)
}
return {
content: [{ type: "text" as const, text: textParts.join(". ") }],
structuredContent: {
containerTag: effectiveContainerTag,
documents: result.documents,
totalCount: result.pagination.totalItems,
},
content: [
{
type: "text" as const,
text: summarizeMemoryGraphResult(result, effectiveContainerTag),
},
],
structuredContent: createMemoryGraphStructuredContent(
result,
effectiveContainerTag,
),
}
} catch (error) {
const message =
@ -369,7 +371,7 @@ export class SupermemoryMCP extends McpAgent<Env, unknown, Props> {
inputSchema: z.object({
containerTag: z.string().optional(),
page: z.number().optional().default(1),
limit: z.number().optional().default(10),
limit: z.number().optional().default(GRAPH_PAGE_LIMIT),
}),
_meta: {
ui: {
@ -694,7 +696,7 @@ export class SupermemoryMCP extends McpAgent<Env, unknown, Props> {
mcp_client_name: clientInfo?.name,
mcp_client_version: clientInfo?.version,
sessionId: this.getMcpSessionId(),
containerTag: containerTag || this.props?.containerTag,
containerTag: effectiveContainerTag,
})
.catch((error) => console.error("PostHog tracking error:", error))

View file

@ -57,6 +57,32 @@
0 2px 4px -2px rgba(0, 0, 0, 0.1);
}
#load-more-btn {
position: fixed;
top: 12px;
right: 12px;
z-index: 10;
padding: 6px 12px;
border: 1px solid var(--border);
border-radius: 10px;
background: var(--bg-secondary);
color: var(--text-muted);
font-size: 12px;
cursor: pointer;
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -2px rgba(0, 0, 0, 0.1);
}
#load-more-btn:hover:not(:disabled) {
opacity: 0.85;
}
#load-more-btn:disabled {
cursor: wait;
opacity: 0.7;
}
/* Popup */
#popup {
display: none;

View file

@ -55,6 +55,13 @@ interface ToolResultData {
containerTag?: string
documents: GraphApiDocument[]
totalCount: number
loadedCount?: number
pagination?: {
currentPage: number
limit: number
totalItems: number
totalPages: number
}
}
interface MemoryNode extends NodeObject {
@ -129,6 +136,8 @@ const CLUSTER_SPREAD = 120
let isDark = true
let selectedNode: GraphNode | null = null
let hoveredNode: GraphNode | null = null
let activeToolData: ToolResultData | null = null
let isLoadingMore = false
// =============================================================================
// DOM References (elements are guaranteed to exist in mcp-app.html)
@ -155,6 +164,9 @@ const zoomInBtn = document.getElementById("zoom-in")!
const zoomOutBtn = document.getElementById("zoom-out")!
// biome-ignore lint/style/noNonNullAssertion: DOM element guaranteed to exist in HTML
const fitBtn = document.getElementById("fit-btn")!
const loadMoreBtn = document.getElementById(
"load-more-btn",
) as HTMLButtonElement
// =============================================================================
// Helpers
@ -857,6 +869,99 @@ prefersDark.addEventListener("change", (e) =>
applyTheme(e.matches ? "dark" : "light"),
)
function updateLoadMore(data: ToolResultData) {
const pagination = data.pagination
const loadedCount = data.loadedCount ?? data.documents.length
const hasMore = pagination
? pagination.currentPage < pagination.totalPages
: loadedCount < data.totalCount
loadMoreBtn.hidden = !hasMore
loadMoreBtn.disabled = isLoadingMore
if (hasMore) {
loadMoreBtn.textContent = isLoadingMore
? "Loading..."
: `Load more (${loadedCount}/${data.totalCount})`
}
}
function renderGraphData(data: ToolResultData) {
const { nodes, links } = transformData(data)
const memCount = nodes.filter((n) => n.nodeType === "memory").length
const docCount = nodes.filter((n) => n.nodeType === "document").length
const loadedCount = data.loadedCount ?? data.documents.length
const totalCount = data.totalCount
const countLabel =
totalCount > loadedCount
? `${docCount}/${totalCount} docs`
: `${docCount} docs`
statsEl.textContent = `${countLabel} · ${memCount} memories · ${links.length} connections`
// Update legend counts
const docCountEl = document.getElementById("legend-doc-count")
const memCountEl = document.getElementById("legend-mem-count")
if (docCountEl) docCountEl.textContent = String(docCount)
if (memCountEl) memCountEl.textContent = String(memCount)
graph.graphData({ nodes, links })
updateLoadMore(data)
// Fit to view after layout stabilizes
setTimeout(() => graph.zoomToFit(400, 40), 600)
}
async function loadNextPage() {
const data = activeToolData
const pagination = data?.pagination
if (!data || !pagination || pagination.currentPage >= pagination.totalPages) {
return
}
isLoadingMore = true
updateLoadMore(data)
try {
const result = await app.callServerTool({
name: "fetch-graph-data",
arguments: {
containerTag: data.containerTag,
page: pagination.currentPage + 1,
limit: pagination.limit,
},
})
if (result.isError) {
statsEl.textContent = "Error loading more graph data"
return
}
const nextData = result.structuredContent as unknown as {
documents?: GraphApiDocument[]
pagination?: ToolResultData["pagination"]
}
if (!nextData?.documents || !nextData.pagination) {
statsEl.textContent = "No additional graph data available"
return
}
activeToolData = {
...data,
documents: [...data.documents, ...nextData.documents],
loadedCount: data.documents.length + nextData.documents.length,
pagination: nextData.pagination,
totalCount: nextData.pagination.totalItems,
}
renderGraphData(activeToolData)
} catch (error) {
statsEl.textContent = "Error loading more graph data"
console.error(error)
} finally {
isLoadingMore = false
if (activeToolData) updateLoadMore(activeToolData)
}
}
loadMoreBtn.addEventListener("click", () => {
void loadNextPage()
})
// =============================================================================
// MCP App SDK
// =============================================================================
@ -881,22 +986,8 @@ app.ontoolresult = (result: CallToolResult) => {
return
}
const { nodes, links } = transformData(data)
const memCount = nodes.filter((n) => n.nodeType === "memory").length
const docCount = nodes.filter((n) => n.nodeType === "document").length
statsEl.textContent = `${docCount} docs \u00b7 ${memCount} memories \u00b7 ${links.length} connections`
// Update legend counts
const docCountEl = document.getElementById("legend-doc-count")
const memCountEl = document.getElementById("legend-mem-count")
if (docCountEl) docCountEl.textContent = String(docCount)
if (memCountEl) memCountEl.textContent = String(memCount)
graph.graphData({ nodes, links })
// Fit to view after layout stabilizes
setTimeout(() => graph.zoomToFit(400, 40), 600)
activeToolData = data
renderGraphData(data)
}
app.ontoolcancelled = () => {