import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
import { McpAgent } from "agents/mcp"
import { z } from "zod"
import { storeMemoryInD1, deleteMemoryFromD1, updateMemoryInD1 } from "./utils/db"
import { searchMemories, storeMemory, deleteVectorById, updateMemoryVector } from "./utils/vectorize"
import { version } from "../package.json"
type MemoryMCPProps = {
namespace: string // e.g., "user:alice", "project:frontend", "all"
namespaceType: "user" | "project" | "all"
}
export class MemoryMCP extends McpAgent<Env, {}, MemoryMCPProps> {
server = new McpServer({
name: "MCP Memory",
version
})
// Get the namespace from props or fall back to the Durable Object's name
get namespace(): string {
return this.props?.namespace || this.name || "default"
}
async init() {
const env = this.env as Env
this.server.tool(
"addToMCPMemory",
`This tool stores important information in a persistent memory layer. Use it when:
1. User explicitly asks to remember something ("remember this...")
2. You detect significant user preferences, traits, or patterns worth preserving
3. Technical details, examples, or emotional responses emerge that would be valuable in future interactions
4. Important project information, documentation, or code patterns should be preserved
The memory will be stored in the current namespace (user, project, or organization-wide).
To automatically detect and use the project namespace for the current directory:
1. First, check if we're in a git repository: git rev-parse --is-inside-work-tree
2. If yes, get the remote URL: git config --get remote.origin.url
3. Extract project name from URL patterns:
- SSH: git@github.com:owner/project.git → project
- HTTPS: https://github.com/owner/project.git → project
- gitlab.com/user/repo.git → repo
- Custom domain: git@custom.com:team/repo.git → repo
4. Convert to project namespace: project:{extracted-name}
5. Store memories with: addToMCPMemory after switching to project namespace
Example: If in /home/user/myproject with origin github.com/alice/myproject.git
The namespace would be: project:myproject
This tool must be invoked through a function call - it is not a passive resource but an active storage mechanism.`,
{
thingToRemember: z.string().describe("The information to remember"),
namespace: z
.string()
.optional()
.describe(
"Optional namespace to store the memory in (e.g., 'user:alice', 'project:frontend'). If not provided, uses the current server namespace."
)
},
async ({ thingToRemember, namespace }: { thingToRemember: string; namespace?: string }) => {
try {
const targetNamespace = namespace || this.namespace
const memoryId = await storeMemory(thingToRemember, targetNamespace, env)
await storeMemoryInD1(thingToRemember, targetNamespace, env, memoryId)
return {
content: [{ type: "text", text: `Remembered in ${targetNamespace}: ${thingToRemember}` }]
}
} catch (error) {
console.error("Error in addToMCPMemory:", error)
return {
content: [{ type: "text", text: `Failed to remember: ${error}` }]
}
}
}
)
this.server.tool(
"searchMCPMemory",
`This tool searches the persistent memory layer for relevant information, preferences, and past context.
It uses semantic matching to find connections between your query and stored memories, even when exact keywords don't match.
Use this tool when:
1. You need historical context about the user's preferences or past interactions
2. You need to find project-specific information, documentation, or code patterns
3. The user refers to something they previously mentioned or asked you to remember
4. You need to verify if specific information exists in the current namespace
The search is performed within the current namespace (user, project, or organization-wide).
To automatically detect and use the project namespace for the current directory:
1. First, check if we're in a git repository: git rev-parse --is-inside-work-tree
2. If yes, get the remote URL: git config --get remote.origin.url
3. Extract project name from URL patterns:
- SSH: git@github.com:owner/project.git → project
- HTTPS: https://github.com/owner/project.git → project
- gitlab.com/user/repo.git → repo
- Custom domain: git@custom.com:team/repo.git → repo
4. Convert to project namespace: project:{extracted-name}
5. Search memories with: searchMCPMemory after switching to project namespace
Example: If in /home/user/myproject with origin github.com/alice/myproject.git
The namespace would be: project:myproject
This tool must be explicitly invoked through a function call - it is not a passive resource but an active search mechanism.`,
{
informationToGet: z.string().describe("The information to search for"),
namespace: z
.string()
.optional()
.describe(
"Optional namespace to search in (e.g., 'user:alice', 'project:frontend'). If not provided, uses the current server namespace."
)
},
async ({ informationToGet, namespace }: { informationToGet: string; namespace?: string }) => {
try {
const targetNamespace = namespace || this.namespace
const memories = await searchMemories(informationToGet, targetNamespace, env)
if (memories.length === 0)
return { content: [{ type: "text", text: `No relevant memories found in ${targetNamespace}.` }] }
return {
content: [{
type: "text",
text: `Found memories in ${targetNamespace}:\n` +
memories.map(m => `[${m.id}] ${m.content} (score: ${m.score.toFixed(4)}${m.created_at ? `, ${m.created_at}` : ''})`).join("\n")
}]
}
} catch (error) {
console.error("Error in searchMCPMemory:", error)
return {
content: [{ type: "text", text: `Search failed: ${error}` }]
}
}
}
)
// Add capability to search across all namespaces
this.server.tool(
"searchAllMemories",
"This tool searches across all namespaces to find relevant memories. Use when you need to find information that might be stored in any user or project namespace.",
{
query: z.string().describe("The search query to find relevant memories")
},
async ({ query }: { query: string }) => {
const result = await env.DB.prepare(`SELECT DISTINCT namespace FROM memories WHERE deleted_at IS NULL`).all()
const allResults = []
if (result.results) {
for (const row of result.results) {
const namespace = (row as any).namespace
try {
const memories = await searchMemories(query, namespace, env)
if (memories.length > 0) {
allResults.push({
namespace,
memories: memories.map(m => ({ id: m.id, content: m.content, score: m.score, created_at: m.created_at }))
})
}
} catch (error) {
console.error(`Error searching namespace ${namespace}:`, error)
}
}
}
if (allResults.length === 0)
return { content: [{ type: "text", text: "No relevant memories found across any namespace." }] }
return {
content: [{
type: "text",
text: "Found memories across all namespaces:\n" +
allResults.map(result =>
`\nIn ${result.namespace}:\n` +
result.memories.map(m => `[${m.id}] ${m.content} (score: ${m.score.toFixed(4)}${m.created_at ? `, ${m.created_at}` : ''})`).join("\n")
).join("\n")
}]
}
}
)
// Add update memory tool
this.server.tool(
"updateMemory",
"This tool updates an existing memory's content. Use when you need to correct or improve a memory without deleting and recreating it.",
{
memoryId: z.string().describe("The ID of the memory to update"),
newContent: z.string().describe("The new content for the memory"),
namespace: z
.string()
.optional()
.describe(
"Optional namespace (e.g., 'user:alice', 'project:frontend'). If not provided, uses the current server namespace."
)
},
async ({ memoryId, newContent, namespace }: { memoryId: string; newContent: string; namespace?: string }) => {
try {
const targetNamespace = namespace || this.namespace
// Update in both D1 and Vectorize
await updateMemoryInD1(memoryId, targetNamespace, newContent, env)
await updateMemoryVector(memoryId, newContent, targetNamespace, env)
return {
content: [{ type: "text", text: `Memory ${memoryId} updated in ${targetNamespace}` }]
}
} catch (error) {
console.error("Error in updateMemory:", error)
return {
content: [{ type: "text", text: `Failed to update memory: ${error}` }]
}
}
}
)
// Add delete memory tool
this.server.tool(
"deleteMemory",
"This tool deletes a specific memory by its ID from the current namespace. Use when you need to remove outdated or incorrect information.",
{
memoryId: z.string().describe("The ID of the memory to delete"),
namespace: z
.string()
.optional()
.describe(
"Optional namespace to delete from (e.g., 'user:alice', 'project:frontend'). If not provided, uses the current server namespace."
)
},
async ({ memoryId, namespace }: { memoryId: string; namespace?: string }) => {
try {
const targetNamespace = namespace || this.namespace
// Fetch the memory content before deleting for confirmation
const memory = await env.DB.prepare(
"SELECT content FROM memories WHERE id = ? AND namespace = ? AND deleted_at IS NULL"
).bind(memoryId, targetNamespace).first<{ content: string }>()
const preview = memory?.content
? memory.content.substring(0, 100) + (memory.content.length > 100 ? '...' : '')
: '(content not found)'
// Delete from both D1 and Vectorize
await deleteMemoryFromD1(memoryId, targetNamespace, env)
await deleteVectorById(memoryId, targetNamespace, env)
return {
content: [{ type: "text", text: `Deleted from ${targetNamespace}: "${preview}"` }]
}
} catch (error) {
console.error("Error in deleteMemory:", error)
return {
content: [{ type: "text", text: `Failed to delete memory: ${error}` }]
}
}
}
)
// Add list namespaces tool
this.server.tool(
"listNamespaces",
"This tool lists all available namespaces with memory counts. Use to discover what namespaces exist.",
{},
async () => {
try {
const result = await env.DB.prepare(
`SELECT namespace, COUNT(*) as count
FROM memories
WHERE deleted_at IS NULL
GROUP BY namespace
ORDER BY count DESC`
).all()
if (!result.results || result.results.length === 0) {
return { content: [{ type: "text", text: "No namespaces found." }] }
}
const namespaces = result.results.map((row: any) =>
`${row.namespace}: ${row.count} memories`
).join("\n")
return {
content: [{ type: "text", text: `Available namespaces:\n${namespaces}` }]
}
} catch (error) {
console.error("Error in listNamespaces:", error)
return {
content: [{ type: "text", text: `Failed to list namespaces: ${error}` }]
}
}
}
)
// Add delete namespace tool
this.server.tool(
"deleteNamespace",
"This tool deletes an entire namespace and all its memories. Use with caution as this action cannot be undone.",
{
namespace: z.string().describe("The namespace to delete (e.g., 'user:alice', 'project:frontend')")
},
async ({ namespace }: { namespace: string }) => {
try {
// Get all memories in the namespace first (excluding already deleted)
const memories = await env.DB.prepare(
"SELECT id FROM memories WHERE namespace = ? AND deleted_at IS NULL"
).bind(namespace).all()
// Delete all vectors for this namespace in bulk
if (memories.results && memories.results.length > 0) {
const memoryIds = memories.results.map((row: any) => row.id)
try {
await env.VECTORIZE.deleteByIds(memoryIds)
} catch (error) {
console.error(`Error bulk deleting vectors for namespace ${namespace}:`, error)
}
}
// Soft delete all memories from D1
const deletedAt = new Date().toISOString()
const deleteResult = await env.DB.prepare(
"UPDATE memories SET deleted_at = ? WHERE namespace = ? AND deleted_at IS NULL"
).bind(deletedAt, namespace).run()
const deletedCount = deleteResult.meta?.changes || 0
return {
content: [{ type: "text", text: `Namespace ${namespace} deleted with ${deletedCount} memories` }]
}
} catch (error) {
console.error("Error in deleteNamespace:", error)
return {
content: [{ type: "text", text: `Failed to delete namespace: ${error}` }]
}
}
}
)
}
}