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:
parent
2e9bcdc0dc
commit
a2d18ef0c2
@ -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*
|
||||
|
||||
@ -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
@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
625
skills/cognitive-memory/mcp_server.py
Normal file
625
skills/cognitive-memory/mcp_server.py
Normal 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()
|
||||
Loading…
Reference in New Issue
Block a user