Cognitive Memory v3.0: rich edges, hybrid embeddings, MCP server

Add first-class edge files in graph/edges/ with bidirectional frontmatter
refs, hybrid Ollama/OpenAI embedding providers with fallback chain, and
native MCP server (18 tools) for direct Claude Code integration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2026-02-19 14:11:18 -06:00
parent 2e9bcdc0dc
commit a2d18ef0c2
5 changed files with 1678 additions and 218 deletions

View File

@ -69,11 +69,34 @@ Added socket_keepalive=True and socket_timeout=300 to Redis connection configura
| `direction` | string | Yes | `outgoing` or `incoming` |
| `strength` | float 0.0-1.0 | No | Relationship strength |
| `context` | string (quoted) | No | Context description |
| `edge_id` | string (UUID) | No | Link to edge file for rich description |
### Relationship Types
`SOLVES`, `CAUSES`, `BUILDS_ON`, `ALTERNATIVE_TO`, `REQUIRES`, `FOLLOWS`, `RELATED_TO`
### Edge File Format
Edge files live in `graph/edges/` with full descriptions. Filename: `{from-slug}--{TYPE}--{to-slug}-{6char}.md`
```markdown
---
id: <uuid>
type: SOLVES
from_id: <uuid>
from_title: "Fixed Redis connection timeouts"
to_id: <uuid>
to_title: "Redis connection drops under load"
strength: 0.8
created: <iso>
updated: <iso>
---
The keepalive fix directly resolves the idle disconnection problem because...
```
Edge frontmatter fields: id, type, from_id, from_title, to_id, to_title, strength, created, updated.
---
## _index.json
@ -82,9 +105,18 @@ Computed index for fast lookups. Rebuilt by `reindex` command. **Source of truth
```json
{
"version": 1,
"version": 2,
"updated": "2025-12-13T10:30:00+00:00",
"count": 313,
"edges": {
"<edge-uuid>": {
"type": "SOLVES",
"from_id": "<uuid>",
"to_id": "<uuid>",
"strength": 0.8,
"path": "graph/edges/fixed-redis--SOLVES--redis-drops-a1b2c3.md"
}
},
"entries": {
"a1b2c3d4-e5f6-7890-abcd-ef1234567890": {
"title": "Fixed Redis connection timeouts",
@ -353,16 +385,37 @@ Standard deploy workflow for the Major Domo Discord bot.
---
## _config.json
Embedding provider configuration. **Gitignored** (may contain API key).
```json
{
"embedding_provider": "ollama",
"openai_api_key": null,
"ollama_model": "nomic-embed-text",
"openai_model": "text-embedding-3-small"
}
```
**Notes:**
- `embedding_provider`: `"ollama"` (default) or `"openai"`
- Provider changes trigger automatic re-embedding (dimension mismatch safety: ollama=768, openai=1536)
- Configure via: `claude-memory config --provider openai --openai-key "sk-..."`
---
## .gitignore
```
_state.json
_index.json
_embeddings.json
_config.json
```
Only markdown files (memories, CORE.md, REFLECTION.md, episodes) are git-tracked. Index, state, and embeddings are derived/mutable data that can be regenerated.
Only markdown files (memories, CORE.md, REFLECTION.md, episodes) are git-tracked. Index, state, embeddings, and config are derived/mutable data that can be regenerated.
---
*Schema version: 2.0.0 | Created: 2026-02-13 | Updated: 2026-02-13*
*Schema version: 3.0.0 | Created: 2026-02-13 | Updated: 2026-02-19*

View File

@ -73,7 +73,7 @@ claude-memory tags related "python"
claude-memory tags suggest <memory_id>
```
**Full command list:** `store`, `recall`, `get`, `search`, `update`, `delete`, `stats`, `recent`, `decay`, `core`, `episode`, `reindex`, `pin`, `embed`, `reflect`, `reflection`, `tags`, `procedure`, `merge`
**Full command list:** `store`, `recall`, `get`, `search`, `update`, `delete`, `stats`, `recent`, `decay`, `core`, `episode`, `reindex`, `pin`, `embed`, `reflect`, `reflection`, `tags`, `procedure`, `merge`, `edge-get`, `edge-search`, `edge-update`, `edge-delete`, `config`
### Memory Types
@ -116,7 +116,8 @@ claude-memory tags suggest <memory_id>
│ ├── errors/ # Error memories
│ ├── general/ # General memories
│ ├── procedures/ # Procedural memories (steps/pre/postconditions)
│ └── insights/ # Reflection-generated insights
│ ├── insights/ # Reflection-generated insights
│ └── edges/ # Rich edge files (first-class relationship objects)
├── episodes/ # Daily session logs (YYYY-MM-DD.md)
├── vault/ # Pinned memories (never decay)
├── _index.json # Computed index for fast lookups
@ -257,6 +258,36 @@ Auto-generated summary of memory themes, cross-project patterns, and access stat
Requires Ollama running locally with the `nomic-embed-text` model. Generate embeddings with `claude-memory embed`, then use `--semantic` flag on recall to merge keyword and embedding-based results. Semantic search provides deeper matching beyond exact title/tag keywords - useful for finding conceptually related memories even when different terminology was used.
## MCP Server
Cognitive Memory v3.0 includes a native MCP server for direct Claude Code tool integration. Instead of CLI calls via Bash, Claude Code can call memory tools directly.
**Registration:** Configured in `~/.claude.json` under `mcpServers.cognitive-memory`.
**Available tools:** `memory_store`, `memory_recall`, `memory_get`, `memory_search`, `memory_relate`, `memory_related`, `memory_edge_get`, `memory_edge_search`, `memory_reflect`, `memory_reflection`, `memory_stats`, `memory_episode`, `memory_tags_list`, `memory_tags_related`, `memory_embed`, `memory_core`, `memory_decay`, `memory_config`
## Rich Edges
Relationships between memories are now first-class objects with their own markdown files in `graph/edges/`. Each edge has:
- Full YAML frontmatter (id, type, from/to IDs and titles, strength, timestamps)
- Description body explaining the relationship in detail
- Backward-compatible: `edge_id` field added to memory relation entries for fast BFS traversal
**Edge CLI commands:** `edge-get`, `edge-search`, `edge-update`, `edge-delete`
**Edge file format:** `{from-slug}--{TYPE}--{to-slug}-{6char}.md`
## Embedding Providers
Supports multiple embedding providers with automatic fallback:
- **Ollama** (default): Local, free, uses `nomic-embed-text` (768 dimensions)
- **OpenAI** (optional): Higher quality, uses `text-embedding-3-small` (1536 dimensions)
Configure with: `claude-memory config --provider openai --openai-key "sk-..."`
View config: `claude-memory config --show`
Provider changes trigger automatic re-embedding (dimension mismatch safety).
Config stored in `_config.json` (gitignored, may contain API key).
## Episode Logging
Daily markdown files appended during sessions, providing chronological context:
@ -294,6 +325,6 @@ This skill should be used proactively when:
**Location**: `~/.claude/skills/cognitive-memory/`
**Data**: `~/.claude/memory/`
**Version**: 2.0.0
**Version**: 3.0.0
**Created**: 2026-02-13
**Migrated from**: MemoryGraph (SQLite)

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{
"name": "cognitive-memory",
"version": "2.0.0",
"description": "Markdown-based memory system with decay scoring, episodic logging, semantic search, reflection cycles, and auto-curated CORE.md",
"version": "3.0.0",
"description": "Markdown-based memory system with decay scoring, episodic logging, semantic search, reflection cycles, auto-curated CORE.md, native MCP server integration, rich edge files, and hybrid Ollama/OpenAI embeddings",
"created": "2026-02-13",
"migrated_from": "memorygraph",
"status": "active",
@ -35,6 +35,14 @@
"embed: Generate Ollama embeddings for semantic search",
"tags list: Show all tags with usage counts",
"tags related: Find co-occurring tags",
"tags suggest: Recommend tags based on co-occurrence patterns"
"tags suggest: Recommend tags based on co-occurrence patterns",
"edge-get: Get full edge details by ID",
"edge-search: Search edges by query, type, from/to IDs",
"edge-update: Update edge description or strength",
"edge-delete: Remove edge and clean memory references",
"config: Manage embedding provider (ollama/openai) with fallback",
"MCP server: Native Claude Code tool integration via JSON-RPC stdio",
"Hybrid embeddings: Ollama (local) + OpenAI (optional) with automatic fallback",
"Rich edges: First-class edge files in graph/edges/ with descriptions"
]
}

View File

@ -0,0 +1,625 @@
#!/usr/bin/env python3
"""
Cognitive Memory MCP Server
JSON-RPC 2.0 stdio MCP server that wraps CognitiveMemoryClient.
Exposes 18 memory operations as MCP tools for Claude Code.
"""
import json
import sys
from pathlib import Path
from typing import Any, Dict
# Allow imports from this directory (client.py lives here)
sys.path.insert(0, str(Path(__file__).parent))
from client import CognitiveMemoryClient, _load_memory_config, MEMORY_DIR
def create_tools() -> list:
"""Define all 18 MCP tool definitions with inputSchema."""
return [
{
"name": "memory_store",
"description": (
"Store a new memory in the cognitive memory system. "
"Creates a markdown file with YAML frontmatter and returns the new memory UUID. "
"Valid types: solution, fix, decision, configuration, problem, workflow, "
"code_pattern, error, general, procedure, insight."
),
"inputSchema": {
"type": "object",
"properties": {
"type": {
"type": "string",
"description": "Memory type (solution, fix, decision, configuration, problem, workflow, code_pattern, error, general, procedure, insight)",
},
"title": {
"type": "string",
"description": "Short descriptive title for the memory",
},
"content": {
"type": "string",
"description": "Full content/body of the memory in markdown",
},
"tags": {
"type": "array",
"items": {"type": "string"},
"description": "List of lowercase tags for categorisation (e.g. ['python', 'fix', 'discord'])",
},
"importance": {
"type": "number",
"description": "Importance score from 0.0 to 1.0 (default 0.5)",
},
},
"required": ["type", "title", "content"],
},
},
{
"name": "memory_recall",
"description": (
"Search memories by a natural language query, ranked by relevance and decay score. "
"Set semantic=true to merge keyword results with vector similarity when embeddings exist."
),
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Natural language search query",
},
"semantic": {
"type": "boolean",
"description": "Merge with semantic/vector similarity search (requires embeddings, default false)",
},
"limit": {
"type": "integer",
"description": "Maximum number of results to return (default 10)",
},
},
"required": ["query"],
},
},
{
"name": "memory_get",
"description": (
"Retrieve a single memory by its UUID, including full content, frontmatter metadata, "
"relations, and current decay score."
),
"inputSchema": {
"type": "object",
"properties": {
"memory_id": {
"type": "string",
"description": "UUID of the memory to retrieve",
}
},
"required": ["memory_id"],
},
},
{
"name": "memory_search",
"description": (
"Filter memories by type, tags, and/or minimum importance score. "
"Optionally include a text query. Returns results sorted by importance descending. "
"Use this for structured browsing; use memory_recall for ranked relevance search."
),
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Optional text query to filter results",
},
"memory_types": {
"type": "array",
"items": {"type": "string"},
"description": "Filter by memory types (e.g. ['solution', 'fix'])",
},
"tags": {
"type": "array",
"items": {"type": "string"},
"description": "Filter by tags — memory must have at least one of these",
},
"min_importance": {
"type": "number",
"description": "Minimum importance score (0.0 to 1.0)",
},
},
},
},
{
"name": "memory_relate",
"description": (
"Create a typed relationship (edge) between two memories. "
"Valid relation types: SOLVES, CAUSES, BUILDS_ON, ALTERNATIVE_TO, REQUIRES, FOLLOWS, RELATED_TO. "
"Returns the new edge UUID, or empty string if the relationship already exists."
),
"inputSchema": {
"type": "object",
"properties": {
"from_id": {
"type": "string",
"description": "UUID of the source memory",
},
"to_id": {
"type": "string",
"description": "UUID of the target memory",
},
"rel_type": {
"type": "string",
"description": "Relationship type (SOLVES, CAUSES, BUILDS_ON, ALTERNATIVE_TO, REQUIRES, FOLLOWS, RELATED_TO)",
},
"description": {
"type": "string",
"description": "Optional human-readable description of the relationship",
},
"strength": {
"type": "number",
"description": "Relationship strength from 0.0 to 1.0 (default 0.8)",
},
},
"required": ["from_id", "to_id", "rel_type"],
},
},
{
"name": "memory_related",
"description": (
"Traverse the relationship graph from a given memory, returning connected memories "
"up to max_depth hops away. Optionally filter by relationship type."
),
"inputSchema": {
"type": "object",
"properties": {
"memory_id": {
"type": "string",
"description": "UUID of the starting memory",
},
"rel_types": {
"type": "array",
"items": {"type": "string"},
"description": "Filter by relation types (e.g. ['SOLVES', 'BUILDS_ON'])",
},
"max_depth": {
"type": "integer",
"description": "Maximum traversal depth (1-5, default 1)",
},
},
"required": ["memory_id"],
},
},
{
"name": "memory_edge_get",
"description": (
"Retrieve a single relationship edge by its UUID, including metadata and description body."
),
"inputSchema": {
"type": "object",
"properties": {
"edge_id": {
"type": "string",
"description": "UUID of the edge to retrieve",
}
},
"required": ["edge_id"],
},
},
{
"name": "memory_edge_search",
"description": (
"Search relationship edges by type, connected memory IDs, or a text query "
"that matches against the from/to memory titles and relationship type."
),
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Text query to match against edge titles and type",
},
"types": {
"type": "array",
"items": {"type": "string"},
"description": "Filter by relationship types (e.g. ['SOLVES'])",
},
"from_id": {
"type": "string",
"description": "Filter to edges originating from this memory UUID",
},
"to_id": {
"type": "string",
"description": "Filter to edges pointing at this memory UUID",
},
},
},
},
{
"name": "memory_reflect",
"description": (
"Review memories created since a given date, cluster them by shared tags, "
"and return consolidation recommendations. Does NOT auto-create new memories — "
"you review the output and decide what to store. Set dry_run=true to skip "
"updating state and logging an episode entry."
),
"inputSchema": {
"type": "object",
"properties": {
"since": {
"type": "string",
"description": "ISO date (YYYY-MM-DD) to review memories from. Defaults to last reflection date or 30 days ago.",
},
"dry_run": {
"type": "boolean",
"description": "If true, return analysis without persisting state changes (default false)",
},
},
},
},
{
"name": "memory_reflection",
"description": (
"Return the current REFLECTION.md summary — the auto-curated narrative of recent "
"memory themes, clusters, and activity. Use this to quickly orient at session start."
),
"inputSchema": {"type": "object", "properties": {}},
},
{
"name": "memory_stats",
"description": (
"Return statistics about the memory system: total count, breakdown by type, "
"relation count, decay distribution, embeddings count, and per-directory file counts."
),
"inputSchema": {"type": "object", "properties": {}},
},
{
"name": "memory_episode",
"description": (
"Append a timestamped entry to today's episode log file (~/.claude/memory/episodes/YYYY-MM-DD.md). "
"Use this to record significant session events, commits, or decisions without creating full memories."
),
"inputSchema": {
"type": "object",
"properties": {
"type": {
"type": "string",
"description": "Episode entry type (e.g. fix, commit, decision, automation)",
},
"title": {
"type": "string",
"description": "Short title for the episode entry",
},
"tags": {
"type": "array",
"items": {"type": "string"},
"description": "Tags for the episode entry",
},
"summary": {
"type": "string",
"description": "Optional summary text for the entry",
},
},
"required": ["type", "title"],
},
},
{
"name": "memory_tags_list",
"description": (
"List all tags used across the memory system, sorted by usage frequency. "
"Returns tag name and count of memories using it."
),
"inputSchema": {
"type": "object",
"properties": {
"limit": {
"type": "integer",
"description": "Maximum number of tags to return (0 = unlimited, default 0)",
}
},
},
},
{
"name": "memory_tags_related",
"description": (
"Find tags that frequently co-occur with a given tag, sorted by co-occurrence count. "
"Useful for discovering related topics and navigating the tag graph."
),
"inputSchema": {
"type": "object",
"properties": {
"tag": {
"type": "string",
"description": "The tag to find co-occurring tags for",
},
"limit": {
"type": "integer",
"description": "Maximum number of related tags to return (0 = unlimited, default 0)",
},
},
"required": ["tag"],
},
},
{
"name": "memory_embed",
"description": (
"Generate or refresh vector embeddings for all memories that do not yet have them. "
"Requires either Ollama (nomic-embed-text model) or an OpenAI API key configured. "
"Embeddings enable semantic recall via memory_recall with semantic=true."
),
"inputSchema": {"type": "object", "properties": {}},
},
{
"name": "memory_core",
"description": (
"Return the current CORE.md content — the auto-curated high-priority memory digest "
"used to seed Claude sessions. Lists critical solutions, active decisions, and key fixes."
),
"inputSchema": {"type": "object", "properties": {}},
},
{
"name": "memory_decay",
"description": (
"Run a decay pass over all memories: recalculate decay scores based on age, "
"access frequency, importance, and type weight. Archives memories whose score "
"drops below the dormant threshold. Returns a summary of updated scores."
),
"inputSchema": {"type": "object", "properties": {}},
},
{
"name": "memory_config",
"description": (
"View or update the cognitive memory embedding configuration (_config.json). "
"Set action='show' to display current config (API key is masked). "
"Provide provider='openai' or provider='ollama' to switch embedding backends. "
"Provide openai_api_key to set the OpenAI API key for embeddings."
),
"inputSchema": {
"type": "object",
"properties": {
"action": {
"type": "string",
"description": "Set to 'show' to display current config without modifying it",
},
"provider": {
"type": "string",
"description": "Embedding provider: 'ollama' or 'openai'",
},
"openai_api_key": {
"type": "string",
"description": "OpenAI API key to store in config",
},
},
},
},
]
def handle_tool_call(
tool_name: str, arguments: Dict[str, Any], client: CognitiveMemoryClient
) -> Dict[str, Any]:
"""Dispatch MCP tool calls to the CognitiveMemoryClient."""
def ok(result: Any) -> Dict[str, Any]:
return {
"content": [
{"type": "text", "text": json.dumps(result, indent=2, default=str)}
]
}
try:
if tool_name == "memory_store":
memory_id = client.store(
type=arguments["type"],
title=arguments["title"],
content=arguments["content"],
tags=arguments.get("tags"),
importance=arguments.get("importance", 0.5),
)
return ok({"success": True, "memory_id": memory_id})
elif tool_name == "memory_recall":
results = client.recall(
query=arguments["query"],
semantic=arguments.get("semantic", False),
limit=arguments.get("limit", 10),
)
return ok(results)
elif tool_name == "memory_get":
result = client.get(arguments["memory_id"])
if result is None:
return ok({"error": f"Memory not found: {arguments['memory_id']}"})
return ok(result)
elif tool_name == "memory_search":
results = client.search(
query=arguments.get("query"),
memory_types=arguments.get("memory_types"),
tags=arguments.get("tags"),
min_importance=arguments.get("min_importance"),
)
return ok(results)
elif tool_name == "memory_relate":
edge_id = client.relate(
from_id=arguments["from_id"],
to_id=arguments["to_id"],
rel_type=arguments["rel_type"],
description=arguments.get("description"),
strength=arguments.get("strength", 0.8),
)
if edge_id:
return ok({"success": True, "edge_id": edge_id})
return ok({"success": False, "message": "Relationship already exists"})
elif tool_name == "memory_related":
results = client.related(
memory_id=arguments["memory_id"],
rel_types=arguments.get("rel_types"),
max_depth=arguments.get("max_depth", 1),
)
return ok(results)
elif tool_name == "memory_edge_get":
result = client.edge_get(arguments["edge_id"])
if result is None:
return ok({"error": f"Edge not found: {arguments['edge_id']}"})
return ok(result)
elif tool_name == "memory_edge_search":
results = client.edge_search(
query=arguments.get("query"),
types=arguments.get("types"),
from_id=arguments.get("from_id"),
to_id=arguments.get("to_id"),
)
return ok(results)
elif tool_name == "memory_reflect":
result = client.reflect(
since=arguments.get("since"),
dry_run=arguments.get("dry_run", False),
)
return ok(result)
elif tool_name == "memory_reflection":
text = client.reflection_summary()
return ok({"content": text})
elif tool_name == "memory_stats":
result = client.stats()
return ok(result)
elif tool_name == "memory_episode":
client.episode(
type=arguments["type"],
title=arguments["title"],
tags=arguments.get("tags"),
summary=arguments.get("summary"),
)
return ok({"success": True})
elif tool_name == "memory_tags_list":
results = client.tags_list(limit=arguments.get("limit", 0))
return ok(results)
elif tool_name == "memory_tags_related":
results = client.tags_related(
tag=arguments["tag"],
limit=arguments.get("limit", 0),
)
return ok(results)
elif tool_name == "memory_embed":
result = client.embed()
return ok(result)
elif tool_name == "memory_core":
text = client.core()
return ok({"content": text})
elif tool_name == "memory_decay":
result = client.decay()
return ok(result)
elif tool_name == "memory_config":
config_path = MEMORY_DIR / "_config.json"
config = _load_memory_config(config_path)
changed = False
provider = arguments.get("provider")
openai_api_key = arguments.get("openai_api_key")
if provider:
config["embedding_provider"] = provider
changed = True
if openai_api_key:
config["openai_api_key"] = openai_api_key
changed = True
if changed:
config_path.write_text(json.dumps(config, indent=2))
return ok({"success": True, "updated": True})
else:
# Show config with masked API key
display = dict(config)
key = display.get("openai_api_key")
if key and isinstance(key, str) and len(key) > 8:
display["openai_api_key"] = key[:4] + "..." + key[-4:]
return ok(display)
else:
return {
"content": [{"type": "text", "text": f"Unknown tool: {tool_name}"}],
"isError": True,
}
except Exception as e:
return {
"content": [{"type": "text", "text": f"Error: {str(e)}"}],
"isError": True,
}
def main():
"""MCP stdio server main loop (JSON-RPC 2.0)."""
client = CognitiveMemoryClient()
tools = create_tools()
for line in sys.stdin:
line = line.strip()
if not line:
continue
try:
message = json.loads(line)
if message.get("method") == "initialize":
response = {
"jsonrpc": "2.0",
"id": message.get("id"),
"result": {
"protocolVersion": "2024-11-05",
"capabilities": {"tools": {}},
"serverInfo": {
"name": "cognitive-memory-mcp-server",
"version": "3.0.0",
},
},
}
print(json.dumps(response), flush=True)
elif message.get("method") == "tools/list":
response = {
"jsonrpc": "2.0",
"id": message.get("id"),
"result": {"tools": tools},
}
print(json.dumps(response), flush=True)
elif message.get("method") == "tools/call":
params = message.get("params", {})
tool_name = params.get("name")
arguments = params.get("arguments", {})
result = handle_tool_call(tool_name, arguments, client)
response = {"jsonrpc": "2.0", "id": message.get("id"), "result": result}
print(json.dumps(response), flush=True)
elif message.get("method") == "notifications/initialized":
# Acknowledge but no response required for notifications
pass
except Exception as e:
error_response = {
"jsonrpc": "2.0",
"id": message.get("id") if "message" in locals() else None,
"error": {"code": -32603, "message": str(e)},
}
print(json.dumps(error_response), flush=True)
if __name__ == "__main__":
main()