mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-07-09 17:18:51 +00:00
fix(mcp): clarify memory graph pagination (#1032)
Co-authored-by: Vedant Mahajan <vedant.04.mahajan@gmail.com>
This commit is contained in:
parent
6a4e0b2514
commit
feca81f69e
5 changed files with 205 additions and 36 deletions
|
|
@ -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>
|
||||
|
|
|
|||
48
apps/mcp/src/memory-graph-data.ts
Normal file
48
apps/mcp/src/memory-graph-data.ts
Normal 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(". ")
|
||||
}
|
||||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue