Model Context Protocol · Remote server

Persistent memory
for your AI agents.

Memory-MCP stores, retrieves, and semantically searches your AI agents' "memories", organized into isolated multi-tenant spaces protected by API Key — so context survives across conversations, without wasting tokens.

.NET 10 ASP.NET Core PostgreSQL + EF Core OpenAI / Azure OpenAI / Gemini Clean Architecture
# call an MCP tool with your API Key
POST /mcp
X-Api-Key: mmcp_xxxxxxxxxxxxxxxxxxxxx

tool: "search_memory"
query: "user preferences"
category: "profile"
includeProfile: true

# → relevant memories, by semantic
#   similarity, literal keyword, or category

Architecture

Four layers, one dependency direction

Clean Architecture: Domain knows nothing about the rest, Application defines use cases through interfaces, Infrastructure implements them with EF Core and the embedding providers, Api exposes everything via the Model Context Protocol.

MemoryMcp.Api
ASP.NET Core host · API Key authentication · 9 tools, 4 resources, 1 prompt, 4 MCP Apps widgets (thin adapters)
ModelContextProtocol.AspNetCore · Extensions.Apps
↓ depends on
MemoryMcp.Infrastructure
EF Core · repositories · embedding provider (OpenAI / Azure OpenAI / Gemini)
Npgsql · EF Core 10 · pgvector
↓ depends on
MemoryMcp.Application
Use cases: interfaces + services (Memory, Document, Space) · DTOs
no infra dependency
↓ depends on
MemoryMcp.Domain
Pure entities: Space, ApiKey, ApiKeySpaceGrant, Document, Memory
zero external dependencies
◆

Multi-user, multi-tenant per space

Every API Key belongs to one user — a Writer or a Reader — and carries a Read/ReadWrite permission per space plus one default "active" space. The role caps the grant, so a Reader is read-only everywhere. Isolation between spaces is guaranteed at the query level; inside a space everyone reads everything, and every write records its author.

◇

Semantic, keyword, or category search

Cosine similarity ranked in the database by pgvector (halfvec(3072) column, HNSW index), paired with literal keyword matching and category filtering/listing — all combinable.

○

Pluggable embedding

IEmbeddingProvider abstracts OpenAI, Azure OpenAI, and Gemini behind the same EmbeddingClient, resolved lazily so it doesn't block tools that don't need it.

MCP Tools

9 tools — 7 from the spec, 2 backing the widgets

Every tool is a thin adapter: it resolves the access context, calls the application service, and formats the output.

search_memoryRead

Searches a space's memories by semantic similarity, literal keyword, and/or category (combinable), with an optional stable/recent profile.

add_memoryReadWrite

Saves (save, with optional category) or removes (forget) a memory based on its content.

listMemoriesRead

Paginated list of extracted memories in a space.

listDocumentsRead

Paginated list of a space's source documents.

getDocumentRead

Metadata and available content of a single document.

create_documentReadWrite

Stores content as a new document (text/Markdown/CSV/PDF in this version — PDF bytes are base64, extracted to text server-side via PdfPig) — source-of-truth storage only, no fact extraction. Backs the upload-file widget.

listSpaces—

Spaces accessible with the current API Key, with the effective access level (grant capped by the user's role) and counts.

whoAmI—

Authenticated user (id, email, display name, Writer/Reader role), the key in use, accessible spaces, active space.

setActiveSpace—

Sets which of the current API Key's accessible spaces is active (default). Backs the select-space widget.

MCP Resources & Prompt · Phase 3

4 resources, 1 prompt — same services, no new tool contract

For clients that support the Resources/Prompts primitives: thin adapters over the exact same Application services the tools call, so profile/space context stays consistent everywhere.

memory-mcp://profileResource

Recent-active-memories profile context for the active space — the same set search_memory's includeProfile attaches.

memory-mcp://spacesResource

The same compact space list as listSpaces, with the active space marked.

memory-mcp://memoriesResource

First page of the active space's memories (any status) — same shape as listMemories.

memory-mcp://graphResource

Nodes (memories, any status) and typed edges for the active space — feeds the memory-graph widget's visualization.

contextPrompt

A ready-to-attach text message: the active space's profile, plus up to 3 other spaces ranked by their most recent memory.

MCP Apps · Phase 3

4 interactive widgets, rendered inside the conversation

Via the experimental ModelContextProtocol.Extensions.Apps package: each widget is an HTML resource opened by a tool, driven from inside its iframe by ordinary tools/call and resources/read requests over a small postMessage JSON-RPC bridge — no widget-specific business logic beyond that bridge.

select-spaceWidget

Lists accessible spaces and switches the active one via setActiveSpace.

guided-saveWidget

Editable content/category/space form that calls add_memory.

upload-fileWidget

Local file picker that calls create_document, then optionally add_memory with the extracted text. Text/Markdown/CSV/PDF are fully ingested;

memory-graphWidget

Reads memory-mcp://graph and renders it with a small hand-rolled force-directed layout — no CDN dependency.

Note: full interactive rendering requires an MCP Apps-capable host (e.g. Claude Desktop/VS Code with Apps support). Any MCP client can still list/call the four opener tools and read the ui://* resources to confirm they're registered and serving HTML.

Technology stack

What's under the hood

LayerTechnology
Runtime.NET 10, pinned in global.json
MCP ServerModelContextProtocol / ModelContextProtocol.AspNetCore 2.1.0 + ModelContextProtocol.Extensions.Apps
Web hostASP.NET Core Minimal API
PersistencePostgreSQL + EF Core 10 (Npgsql.EntityFrameworkCore.PostgreSQL)
Vector searchpgvector ≥ 0.7.0 (halfvec + HNSW), via Pgvector.EntityFrameworkCore
EmbeddingOpenAI / Azure.AI.OpenAI / Gemini, same EmbeddingClient
PDF text extractionPdfPig (pure managed, no native/external dependency)
AuthenticationCustom scheme based on API Key (SHA-256 hash)
TestsxUnit · NSubstitute · AwesomeAssertions · Microsoft.AspNetCore.Mvc.Testing
ContainerMulti-stage Dockerfile + docker-compose

Roadmap

Project phases

Phase 1 — Completed

Core tools via standard MCP

  • ✓Domain entities and relational data model
  • ✓EF Core persistence + initial migration
  • ✓API Key authentication with per-space permissions
  • ✓Pluggable embedding provider (OpenAI / Azure OpenAI)
  • ✓The 7 MCP tools, exposed on /mcp
  • ✓Unit + integration + end-to-end test suite
Phase 2 — Completed

Graph memory

  • ✓MemoryEdge / RelationType (Updates / Extends / Derives), generalizing SupersededBy
  • ✓Pluggable IFactExtractor (OpenAI / Azure OpenAI / Gemini / self-hosted), with a safe fallback when unconfigured
  • ✓memory_edges table + recursive-CTE traversal, no graph DB extension required
  • ✓search_memory enriched with relatedMemories; add_memory extracts and links multiple facts per save
  • ✓Configurable embedding width, to support Gemini's native embedding dimensions
Phase 3 — Completed

Resources, prompt & MCP Apps widgets

  • ✓MCP Resources: memory-mcp://profile, memory-mcp://spaces, memory-mcp://memories, memory-mcp://graph
  • ✓MCP Prompt: context, ranking up to 3 other spaces by recent activity
  • ✓MCP Apps widgets: select-space, guided-save, upload-file, memory-graph
  • ✓Test suite extended end-to-end via the MCP client's resource/prompt/tool calls
Phase 4 — Completed

Native vector index

  • ✓pgvector with a native HNSW index, replacing in-app cosine scoring — kept in Postgres rather than a separate Qdrant service, so memories and vectors share one transactional store
  • ✓halfvec(3072) column, working around pgvector's 2000-dimension index limit on the vector type
  • ✓KNN pushed into SQL: embeddings never cross the wire, change tracking confined to topK rows instead of the whole space
  • ✓Embedding width made schema-bound and validated at startup, making mixed-width embeddings unrepresentable
Phase 5 — Completed

Multi-user spaces

  • ✓User with a Writer/Reader role; an API Key becomes a credential of a user, who may hold several
  • ✓The role caps the per-space grant, applied once where the access snapshot is built — so services, tools and widgets all read one effective level
  • ✓Deactivating a user invalidates every key they hold, in the authentication query itself
  • ✓createdBy/updatedBy on memories and documents, returned as ids and display names — including who forgot or superseded a colleague's memory
  • ✓Per-space isolation pinned end-to-end; reads inside a space left unfiltered by author

Getting started

Local setup in 6 steps

1

Configure the connection string

# src/MemoryMcp.Api/appsettings.Development.json (not versioned){ "ConnectionStrings": { "Default": "Host=localhost;Port=5432;Username=...;Password=...;Database=..." } }
2

Apply migrations

$ dotnet ef database update --project src/MemoryMcp.Infrastructure --startup-project src/MemoryMcp.Api
3

Configure the embedding provider (optional)

# src/MemoryMcp.Api/appsettings.Development.json{ "Embeddings": { "Provider": "Gemini", "ApiKey": "AIza...", "Model": "gemini-embedding-001", "Dimensions": 3072 } }# or "OpenAI" / "AzureOpenAI" — without this, add_memory/search_memory return a tool error
4

Configure fact extraction for graph memory (optional)

# src/MemoryMcp.Api/appsettings.Development.json{ "Extraction": { "Provider": "Gemini", "ApiKey": "AIza...", "Model": "gemini-2.5-flash" } }# or "OpenAI" / "AzureOpenAI" — without this, add_memory still works but saves whole content as one memory (no graph edges)
5

Create test spaces, users, and API Keys

$ dotnet run --project src/MemoryMcp.Api -- --seed# Seeded spaces 'default' and 'team'.# Writer (writer@memory-mcp.local) — spaces default, team — API key: mmcp_xxxxxxxx…# Reader (reader@memory-mcp.local) — space team (read-only by role) — API key: mmcp_yyyyyyyy…
6

Start the server

$ dotnet run --project src/MemoryMcp.Api# http://localhost:5004/mcp — requires the X-Api-Key header