refactor(mcp): simplify history implementation

- Reduce history.ts from 169 to 51 lines
- Remove AI tools (list_history, restore_version, get_version)
- Remove /api/update-svg endpoint
- Remove 10-second history polling
- Simplify HistoryEntry to just {xml, svg}
- Use array index instead of version numbers

Total reduction: 1936 → 923 lines (-52%)
This commit is contained in:
dayuan.jiang
2025-12-21 16:11:49 +09:00
parent c215d80688
commit b9bc2a72c6
3 changed files with 224 additions and 1203 deletions

View File

@@ -1,169 +1,51 @@
/**
* Diagram Version History for MCP Server
*
* Stores diagram versions in-memory per session.
* Enables users and AI to restore previous diagram states.
* Simple diagram history - matches Next.js app pattern
* Stores {xml, svg} entries in a circular buffer
*/
import { log } from "./logger.js"
export interface HistoryEntry {
version: number
xml: string
svg: string // SVG data for thumbnail preview
source: "ai" | "human"
tool?: string // Which tool made the change (display_diagram, edit_diagram, browser_sync)
timestamp: Date
description?: string
}
const MAX_HISTORY = 20
const historyStore = new Map<string, Array<{ xml: string; svg: string }>>()
interface SessionHistory {
entries: HistoryEntry[]
nextVersion: number
}
// In-memory history store keyed by session ID
const historyStore = new Map<string, SessionHistory>()
// Configuration
const MAX_HISTORY_ENTRIES = 50
/**
* Add a new entry to session history
* Returns the assigned version number
*/
export function addHistoryEntry(
sessionId: string,
entry: Omit<HistoryEntry, "version">,
): number {
export function addHistory(sessionId: string, xml: string, svg = ""): number {
let history = historyStore.get(sessionId)
if (!history) {
history = { entries: [], nextVersion: 1 }
history = []
historyStore.set(sessionId, history)
}
// Deduplicate: skip if XML is identical to last entry
const lastEntry = history.entries[history.entries.length - 1]
if (lastEntry && lastEntry.xml === entry.xml) {
log.debug(`Skipping duplicate history entry for session ${sessionId}`)
return lastEntry.version
// Dedupe: skip if same as last entry
const last = history[history.length - 1]
if (last?.xml === xml) {
return history.length - 1
}
const version = history.nextVersion++
const newEntry: HistoryEntry = {
...entry,
version,
history.push({ xml, svg })
// Circular buffer
if (history.length > MAX_HISTORY) {
history.shift()
}
history.entries.push(newEntry)
// Prune oldest entries if over limit (circular buffer)
if (history.entries.length > MAX_HISTORY_ENTRIES) {
const removed = history.entries.shift()
log.debug(`Pruned oldest history entry v${removed?.version}`)
}
log.info(
`Added history v${version} for session ${sessionId} (source: ${entry.source}, entries: ${history.entries.length})`,
)
return version
log.debug(`History: session=${sessionId}, entries=${history.length}`)
return history.length - 1
}
/**
* Get history entries for a session
* Returns newest first, limited to specified count
*/
export function getHistory(
sessionId: string,
limit: number = 20,
): HistoryEntry[] {
const history = historyStore.get(sessionId)
if (!history) {
return []
}
// Return newest first
return [...history.entries].reverse().slice(0, limit)
): Array<{ xml: string; svg: string }> {
return historyStore.get(sessionId) || []
}
/**
* Get a specific version from history
*/
export function getVersion(
export function getHistoryEntry(
sessionId: string,
version: number,
): HistoryEntry | undefined {
index: number,
): { xml: string; svg: string } | undefined {
const history = historyStore.get(sessionId)
if (!history) {
return undefined
}
return history.entries.find((e) => e.version === version)
return history?.[index]
}
/**
* Get the latest version number for a session
*/
export function getLatestVersion(sessionId: string): number {
const history = historyStore.get(sessionId)
if (!history || history.entries.length === 0) {
return 0
}
return history.entries[history.entries.length - 1].version
}
/**
* Clear history for a session (used on session expiry)
*/
export function clearHistory(sessionId: string): void {
if (historyStore.has(sessionId)) {
historyStore.delete(sessionId)
log.info(`Cleared history for session ${sessionId}`)
}
}
/**
* Update the SVG of the latest entry (or specific version) that has empty SVG
* Used when browser generates SVG after loading AI diagram
*/
export function updateLatestEntrySvg(
sessionId: string,
svg: string,
targetVersion?: number,
): boolean {
const history = historyStore.get(sessionId)
if (!history || history.entries.length === 0) {
return false
}
// Find entry to update - either specific version or latest without SVG
let entry: HistoryEntry | undefined
if (targetVersion !== undefined) {
entry = history.entries.find((e) => e.version === targetVersion)
} else {
// Find most recent entry without SVG
for (let i = history.entries.length - 1; i >= 0; i--) {
if (!history.entries[i].svg) {
entry = history.entries[i]
break
}
}
}
if (entry && !entry.svg) {
entry.svg = svg
log.debug(`Updated SVG for history v${entry.version}`)
return true
}
return false
}
/**
* Get count of history entries for a session
*/
export function getHistoryCount(sessionId: string): number {
const history = historyStore.get(sessionId)
return history?.entries.length || 0
historyStore.delete(sessionId)
}