Skip to main content
Glama
index.ts19.5 kB
import { Hono } from "hono" import { MemoryMCP } from "./mcp" import { initializeDatabase, deleteMemoryFromD1, updateMemoryInD1 } from "./utils/db" import { deleteVectorById, updateMemoryVector, searchMemories, storeMemory } from "./utils/vectorize" const app = new Hono<{ Bindings: Env }>() // Initialize database once let dbInitialized = false // Middleware for one-time database initialization app.use("*", async (c, next) => { if (!dbInitialized) { try { await initializeDatabase(c.env) dbInitialized = true } catch (e) { console.error("Failed to initialize D1 database:", e) } } await next() }) // index.html app.get("/", async (c) => await c.env.ASSETS.fetch(c.req.raw)) // Health check endpoint app.get("/health", async (c) => { const health = { status: "healthy", timestamp: new Date().toISOString(), checks: { database: false, vectorize: false, lastSync: null as string | null } } try { // Check D1 database const dbCheck = await c.env.DB.prepare("SELECT 1 as test").first() health.checks.database = dbCheck?.test === 1 // Check last sync time const lastSync = await c.env.DB.prepare( "SELECT value FROM system_metadata WHERE key = 'last_vector_backup'" ).first<{ value: string }>() health.checks.lastSync = lastSync?.value || null // Check Vectorize (try a simple search) try { await searchMemories("health check", "system:health", c.env) health.checks.vectorize = true } catch { health.checks.vectorize = false } // Determine overall health if (!health.checks.database || !health.checks.vectorize) { health.status = "unhealthy" return c.json(health, 503) } return c.json(health) } catch (error) { health.status = "error" return c.json({ ...health, error: error instanceof Error ? error.message : "Unknown error" }, 503) } }) // Get available namespaces (users and projects) app.get("/api/namespaces", async (c) => { try { // Get distinct namespaces from the database const result = await c.env.DB.prepare( ` SELECT DISTINCT namespace FROM memories ` ).all() const namespaces = { users: [] as string[], projects: [] as string[], all: false } if (result.results) { for (const row of result.results) { const namespace = (row as any).namespace if (namespace.startsWith("user:")) { namespaces.users.push(namespace.substring(5)) } else if (namespace.startsWith("project:")) { namespaces.projects.push(namespace.substring(8)) } else if (namespace === "all") { namespaces.all = true } } } else { } return c.json({ success: true, namespaces }) } catch (error) { console.error("Error getting namespaces:", error) return c.json( { success: false, error: "Failed to retrieve namespaces", details: error instanceof Error ? error.message : String(error) }, 500 ) } }) // Search across multiple namespaces app.post("/api/search", async (c) => { try { const body = await c.req.json() const { query, namespaces = [], dateFrom, dateTo } = body if (!query) { return c.json({ error: "Missing query parameter" }, 400) } const results = [] // Search each namespace for (const namespace of namespaces) { try { const memories = await searchMemories(query, namespace, c.env) // If date filtering is requested, we'll need to fetch from D1 to get dates if (dateFrom || dateTo) { const dbMemories = await c.env.DB.prepare( ` SELECT id, created_at FROM memories WHERE namespace = ? ${dateFrom ? "AND created_at >= ?" : ""} ${dateTo ? "AND created_at <= ?" : ""} ` ) .bind(namespace, ...(dateFrom ? [dateFrom] : []), ...(dateTo ? [dateTo] : [])) .all() const validIds = new Set((dbMemories.results || []).map((m: any) => m.id)) results.push({ namespace, memories: memories.filter((m) => validIds.has(m.id)) }) } else { results.push({ namespace, memories }) } } catch (error) { console.error(`Error searching namespace ${namespace}:`, error) } } return c.json({ success: true, query, results }) } catch (error) { console.error("Error in multi-namespace search:", error) return c.json({ success: false, error: "Failed to search memories" }, 500) } }) // Search across all namespaces (read-only) app.post("/api/search-all", async (c) => { try { const body = await c.req.json() const { query, dateFrom, dateTo } = body if (!query) { return c.json({ error: "Missing query parameter" }, 400) } // Get all namespaces const result = await c.env.DB.prepare( ` SELECT DISTINCT namespace FROM memories ` ).all() const results = [] // Search each namespace if (result.results) { for (const row of result.results) { const namespace = (row as any).namespace try { const memories = await searchMemories(query, namespace, c.env) // If date filtering is requested, we'll need to fetch from D1 to get dates if (dateFrom || dateTo) { const dbMemories = await c.env.DB.prepare( ` SELECT id, created_at FROM memories WHERE namespace = ? ${dateFrom ? "AND created_at >= ?" : ""} ${dateTo ? "AND created_at <= ?" : ""} ` ) .bind(namespace, ...(dateFrom ? [dateFrom] : []), ...(dateTo ? [dateTo] : [])) .all() const validIds = new Set((dbMemories.results || []).map((m: any) => m.id)) results.push({ namespace, memories: memories.filter((m) => validIds.has(m.id)) }) } else { results.push({ namespace, memories }) } } catch (error) { console.error(`Error searching namespace ${namespace}:`, error) } } } return c.json({ success: true, query, results }) } catch (error) { console.error("Error in all-namespace search:", error) return c.json({ success: false, error: "Failed to search memories" }, 500) } }) // Get all memories for a namespace with pagination app.get("/:namespaceType/:namespaceId/memories", async (c) => { const namespaceType = c.req.param("namespaceType") const namespaceId = c.req.param("namespaceId") const namespace = `${namespaceType}:${namespaceId}` // Pagination parameters const page = parseInt(c.req.query("page") || "1") const limit = parseInt(c.req.query("limit") || "20") const offset = (page - 1) * limit const sortBy = c.req.query("sortBy") || "date" try { // Get total count const countResult = await c.env.DB.prepare("SELECT COUNT(*) as total FROM memories WHERE namespace = ? AND deleted_at IS NULL") .bind(namespace) .first() const total = (countResult as any)?.total || 0 // Get paginated results const orderBy = sortBy === "date" ? "created_at DESC" : "created_at DESC" const memories = await c.env.DB.prepare( `SELECT id, content, created_at FROM memories WHERE namespace = ? AND deleted_at IS NULL ORDER BY ${orderBy} LIMIT ? OFFSET ?` ) .bind(namespace, limit, offset) .all() return c.json({ success: true, memories: memories.results, namespace, pagination: { page, limit, total, totalPages: Math.ceil(total / limit) } }) } catch (error) { console.error(`Error retrieving memories for namespace ${namespace}:`, error) return c.json({ success: false, error: "Failed to retrieve memories" }, 500) } }) // Delete a memory for a namespace app.delete("/:namespaceType/:namespaceId/memories/:memoryId", async (c) => { const namespaceType = c.req.param("namespaceType") const namespaceId = c.req.param("namespaceId") const memoryId = c.req.param("memoryId") const namespace = `${namespaceType}:${namespaceId}` try { // 1. Delete from D1 await deleteMemoryFromD1(memoryId, namespace, c.env) // 2. Delete from Vectorize index try { await deleteVectorById(memoryId, namespace, c.env) } catch (vectorError) { console.error(`Failed to delete vector ${memoryId} for namespace ${namespace} from Vectorize:`, vectorError) } return c.json({ success: true }) } catch (error) { console.error(`Error deleting memory ${memoryId} (D1 primary) for namespace ${namespace}:`, error) return c.json({ success: false, error: "Failed to delete memory" }, 500) } }) // Update a specific memory for a namespace app.put("/:namespaceType/:namespaceId/memories/:memoryId", async (c) => { const namespaceType = c.req.param("namespaceType") const namespaceId = c.req.param("namespaceId") const memoryId = c.req.param("memoryId") const namespace = `${namespaceType}:${namespaceId}` let updatedContent: string try { // Get updated content from request body const body = await c.req.json() if (!body || typeof body.content !== "string" || body.content.trim() === "") { return c.json({ success: false, error: "Invalid or missing content in request body" }, 400) } updatedContent = body.content.trim() } catch (e) { console.error("Failed to parse request body:", e) return c.json({ success: false, error: "Failed to parse request body" }, 400) } try { // 1. Update in D1 await updateMemoryInD1(memoryId, namespace, updatedContent, c.env) // 2. Update vector in Vectorize try { await updateMemoryVector(memoryId, updatedContent, namespace, c.env) } catch (vectorError) { console.error(`Failed to update vector ${memoryId} for namespace ${namespace} in Vectorize:`, vectorError) } return c.json({ success: true }) } catch (error: any) { console.error(`Error updating memory ${memoryId} for namespace ${namespace}:`, error) const errorMessage = error.message || "Failed to update memory" if (errorMessage.includes("not found")) { return c.json({ success: false, error: errorMessage }, 404) } return c.json({ success: false, error: errorMessage }, 500) } }) // Simple search API for Slack bot and other integrations app.post("/search/:namespaceType/:namespaceId", async (c) => { const namespaceType = c.req.param("namespaceType") const namespaceId = c.req.param("namespaceId") const namespace = `${namespaceType}:${namespaceId}` try { const { query } = await c.req.json() if (!query) { return c.json({ error: "Missing query parameter" }, 400) } const memories = await searchMemories(query, namespace, c.env) return c.json({ success: true, namespace, query, results: memories }) } catch (error) { console.error(`Error searching memories in namespace ${namespace}:`, error) return c.json({ success: false, error: "Failed to search memories" }, 500) } }) // Generic API endpoints for updating and deleting memories by ID app.put("/api/memories/:memoryId", async (c) => { const memoryId = c.req.param("memoryId") let updatedContent: string try { // Get updated content from request body const body = await c.req.json() if (!body || typeof body.content !== "string" || body.content.trim() === "") { return c.json({ success: false, error: "Invalid or missing content in request body" }, 400) } updatedContent = body.content.trim() } catch (e) { console.error("Failed to parse request body:", e) return c.json({ success: false, error: "Failed to parse request body" }, 400) } try { // First, find which namespace this memory belongs to const memoryResult = await c.env.DB.prepare("SELECT namespace FROM memories WHERE id = ?") .bind(memoryId) .first() if (!memoryResult) { return c.json({ success: false, error: "Memory not found" }, 404) } const namespace = (memoryResult as any).namespace // Update in D1 await updateMemoryInD1(memoryId, namespace, updatedContent, c.env) // Update vector in Vectorize try { await updateMemoryVector(memoryId, updatedContent, namespace, c.env) } catch (vectorError) { console.error(`Failed to update vector ${memoryId} in namespace ${namespace} in Vectorize:`, vectorError) } return c.json({ success: true }) } catch (error: any) { console.error(`Error updating memory ${memoryId}:`, error) const errorMessage = error.message || "Failed to update memory" if (errorMessage.includes("not found")) { return c.json({ success: false, error: errorMessage }, 404) } return c.json({ success: false, error: errorMessage }, 500) } }) app.delete("/api/memories/:memoryId", async (c) => { const memoryId = c.req.param("memoryId") try { // First, find which namespace this memory belongs to const memoryResult = await c.env.DB.prepare("SELECT namespace FROM memories WHERE id = ?") .bind(memoryId) .first() if (!memoryResult) { return c.json({ success: false, error: "Memory not found" }, 404) } const namespace = (memoryResult as any).namespace // Delete from D1 await deleteMemoryFromD1(memoryId, namespace, c.env) // Delete from Vectorize index try { await deleteVectorById(memoryId, namespace, c.env) } catch (vectorError) { console.error(`Failed to delete vector ${memoryId} from namespace ${namespace} in Vectorize:`, vectorError) } return c.json({ success: true }) } catch (error) { console.error(`Error deleting memory ${memoryId}:`, error) return c.json({ success: false, error: "Failed to delete memory" }, 500) } }) // Create streaming HTTP handlers for MCP const userMcpHandler = MemoryMCP.serve("/user/:userId/mcp", { binding: "MCP_OBJECT" }) const projectMcpHandler = MemoryMCP.serve("/project/:projectId/mcp", { binding: "MCP_OBJECT" }) const allMcpHandler = MemoryMCP.serve("/all/mcp", { binding: "MCP_OBJECT" }) // Streaming HTTP MCP endpoint for user namespace app.all("/user/:userId/mcp", async (c) => { return userMcpHandler.fetch(c.req.raw, c.env, c.executionCtx) }) // Streaming HTTP MCP endpoint for project namespace app.all("/project/:projectId/mcp", async (c) => { return projectMcpHandler.fetch(c.req.raw, c.env, c.executionCtx) }) // Streaming HTTP MCP endpoint for organization-wide namespace app.all("/all/mcp", async (c) => { return allMcpHandler.fetch(c.req.raw, c.env, c.executionCtx) }) // Serve admin page app.get("/admin", async (c) => { return c.html(await c.env.ASSETS.fetch("https://mcp-memory.loqwai.workers.dev/admin.html").then((r) => r.text())) }) // Fallback to static assets app.get("*", async (c) => { return c.env.ASSETS.fetch(c.req.raw) }) export default app export { MemoryMCP } // Scheduled cron job for incremental vector backups export const scheduled: ExportedHandlerScheduledHandler<Env> = async (event, env, ctx) => { try { // Get the last backup timestamp from KV or D1 const lastBackupResult = await env.DB.prepare( "SELECT value FROM system_metadata WHERE key = 'last_vector_backup'" ).first() const lastBackup = lastBackupResult?.value || '2000-01-01T00:00:00Z' const currentTime = new Date().toISOString() // Get memories created or updated since last backup const newMemories = await env.DB.prepare( `SELECT id, namespace, content, created_at FROM memories WHERE created_at > ? AND deleted_at IS NULL ORDER BY created_at ASC` ).bind(lastBackup).all() if (newMemories.results && newMemories.results.length > 0) { // For each memory, verify it exists in Vectorize let syncCount = 0 for (const row of newMemories.results) { const memory = row as any try { // Vectorize doesn't have a getByIds method, so we'll track sync in D1 // Check if this memory was synced to vectorize const syncRecord = await env.DB.prepare( "SELECT id FROM vector_sync WHERE memory_id = ?" ).bind(memory.id).first() if (!syncRecord) { // Re-create the vector if not synced await storeMemory(memory.content, memory.namespace, env) // Mark as synced await env.DB.prepare( "INSERT INTO vector_sync (memory_id, synced_at) VALUES (?, ?)" ).bind(memory.id, currentTime).run() syncCount++ } } catch (error) { console.error(`Error checking vector for memory ${memory.id}:`, error) } } } // Update last backup timestamp await env.DB.prepare( `INSERT INTO system_metadata (key, value) VALUES ('last_vector_backup', ?) ON CONFLICT(key) DO UPDATE SET value = ?` ).bind(currentTime, currentTime).run() } catch (error) { console.error("Cron job error:", error) } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/redaphid/mcp-memory'

If you have feedback or need assistance with the MCP directory API, please join our Discord server