Architectural Design of the Memory Bank System in OpenWork: MCP Tools and MySQL Schema Explained

The OpenWork memory bank system implements a per-user persistence layer that exposes stable MCP tools for saving and searching memories while using a flexible MySQL/Drizzle backend with FULLTEXT search capabilities.

The different-ai/openwork repository implements a sophisticated memory persistence layer that allows both AI agents and human users to store and retrieve contextual information across sessions. This article examines the architectural design of the memory bank system in OpenWork, detailing how the platform balances a stable external API contract with a pluggable internal implementation.

High-Level Architecture: Stable Contracts Over Flexible Storage

The system deliberately separates the public API surface from the storage implementation. Agents interact with memory functions through Model Context Protocol (MCP) tools, while the backend uses MySQL (PlanetScale) with Drizzle ORM for persistence.

Public Contract Layer

The memory bank exposes four primary MCP tools: postMemory, getMemorySearch, getMemory, and deleteMemoryById. These tools are discovered dynamically through search_capabilities and invoked via execute_capability. According to the source in ee/apps/den-api/src/routes/memory/core.ts, tool names are not hardcoded but generated from route definitions using buildOperationId, ensuring the contract remains stable even if underlying implementations change.

Internal Implementation Layer

Behind the MCP interface, the system persists data in MySQL using Drizzle ORM. The architecture supports future migration to vector databases or organization-wide memory banks without altering the tool contracts exposed to agents.

Database Schema and Persistence Model

The storage layer centers on two related tables defined in ee/packages/den-db/src/schema/memory.ts.

Core Tables

The memory table stores the primary record with fields for id, user_id, org_id, scope, content, source, tags, and timestamps. The memory_context table handles optional citations through fields like citation, snippet, and origin, linked via memory_id.

Search Indexing

To enable natural-language queries, the schema creates a FULLTEXT index on the memory.content column. The ensureMemoryFulltextIndex routine handles this idempotently during both bootstrap and migration operations, ensuring the lexical search capability is always available.

API Routes and MCP Tool Implementation

The route handlers in ee/apps/den-api/src/routes/memory/core.ts implement the CRUD operations while enforcing strict user scoping.

Route Definitions

The API provides four endpoints:

  • POST /v1/memory → postMemory (creates memories, forces scope='user')
  • GET /v1/memory/search → getMemorySearch (FULLTEXT search using MATCH … AGAINST in natural language mode)
  • GET /v1/memory → getMemory (lists user memories)
  • DELETE /v1/memory/:id → deleteMemoryById (hard delete with cascade to memory_context)

Relevance Scoring

The shared.ts utility file defines memoryRelevance, which constructs the SQL expression for calculating match scores. This separation of concerns allows the search algorithm to evolve without modifying route handlers.

User Isolation and Security

All routes automatically filter by user_id extracted from the authentication token. If an agent attempts to access another user's memory ID, the system returns a 404 error, preventing information leakage through ID enumeration.

Saving and Retrieving Memories: Code Examples

Creating a Memory

Agents or UI clients POST to the memory endpoint:

await fetch("/v1/memory", {
  method: "POST",
  headers: { 
    "Content-Type": "application/json", 
    Authorization: "Bearer <token>" 
  },
  body: JSON.stringify({
    content: "Deployed version 1.2 to production on 2026-07-20",
    tags: ["deployment", "prod"],
    contexts: [
      {
        snippet: "Deploy script output ...",
        conversation_id: "conv_123",
        message_id: "msg_456",
        origin: "active_conversation"
      }
    ]
  })
});

The server overrides any client-provided scope value to 'user', ensuring strict ownership boundaries.

Retrieving memories uses the FULLTEXT index via the getMemorySearch tool:

const resp = await fetch(
  "/v1/memory/search?q=deployment+prod&limit=5", 
  { headers: { Authorization: "Bearer <token>" } }
);
const { results } = await resp.json();
console.log(results.map(r => ({ 
  id: r.id, 
  content: r.content, 
  score: r.score 
})));

Results include a relevance score calculated by the memoryRelevance utility and are automatically scoped to the authenticated user.

Memory Management

Listing and deletion follow standard REST patterns:

// List all memories
const { memories } = await (
  await fetch("/v1/memory", { 
    headers: { Authorization: "Bearer <token>" } 
  })
).json();

// Delete with cascade to memory_context
await fetch(`/v1/memory/${memoryId}`, {
  method: "DELETE",
  headers: { Authorization: "Bearer <token>" }
});

Deletion cascades to associated memory_context rows, supporting right-to-be-forgotten requirements.

Client Interface and Feature Flagging

The desktop application provides a thin React interface for memory management, implemented in apps/app/src/react-app/domains/settings/pages/memory-view.tsx.

UI Components

The interface displays the user's memory list with optimistic deletion (undo toast support) and a copy-prompt button. All components use Base UI primitives from shadcn/ui for consistency.

Feature Gating

Access to the memory bank UI is controlled by the featureFlags.memory flag, allowing gradual rollouts or organization-specific enablement.

Future-Proofing and Extensibility

The architectural design of the memory bank system in OpenWork anticipates several evolution paths without breaking the MCP tool contract.

Scope Expansion

The scope column in the memory table already supports an 'org' value for future organization-wide memory banks, though the current server implementation enforces 'user' for privacy.

Search Backend Migration

Because the MCP tool names (getMemorySearch, etc.) are abstracted from the underlying query implementation, the system can swap MySQL FULLTEXT for vector embeddings or dedicated search engines without requiring agent code changes.

Summary

  • OpenWork's memory bank uses a per-user storage model with strict server-side scoping enforced in ee/apps/den-api/src/routes/memory/core.ts.
  • MCP tools provide a stable contract for agents, with operation IDs generated dynamically from route definitions rather than hardcoded mappings.
  • MySQL and Drizzle ORM handle persistence, utilizing a FULLTEXT index on memory.content for natural language search via MATCH … AGAINST queries.
  • Security relies on user ID filtering on all routes, returning 404 for unauthorized access attempts to prevent data leakage.
  • Future enhancements like vector search or organization-wide scopes can be added without changing the public API surface.

Frequently Asked Questions

How does OpenWork ensure memories remain private to individual users?

The system enforces user isolation at the API layer. In ee/apps/den-api/src/routes/memory/core.ts, every route handler automatically filters queries by the authenticated user_id extracted from the JWT token. The server explicitly overwrites any client-provided scope parameter to 'user', and cross-user access attempts return a 404 status to prevent ID enumeration attacks.

What database technology powers the memory bank search functionality?

The current implementation uses MySQL (specifically PlanetScale) with a FULLTEXT index on the content column of the memory table. The memoryRelevance utility in shared.ts constructs SQL MATCH … AGAINST expressions in natural language mode. However, the MCP tool contract is designed to allow swapping this for vector-based search in future versions without breaking existing agent integrations.

Can agents directly invoke memory operations, or do they require human approval?

Agents discover memory capabilities through search_capabilities and invoke them via execute_capability using the MCP protocol. While the system architecture supports automated agent writes, the current implementation treats these as standard API calls. The architecture documentation suggests that agents are instructed not to persist secrets, with content stored as plaintext, implying human verification is recommended for sensitive operations.

How does the memory bank handle deletion and data retention?

The deleteMemoryById endpoint performs hard deletes on the memory table with cascading deletes to associated memory_context rows. This is implemented in ee/apps/den-api/src/routes/memory/core.ts to support right-to-be-forgotten requirements. The desktop UI in memory-view.tsx provides optimistic deletion with undo functionality, though the server operation is permanent once confirmed.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →