Skip to content
This repository was archived by the owner on Sep 23, 2025. It is now read-only.

Commit fd5451b

Browse files
committed
Implement Phase 1: Journal MCP Server JSON prototype
Complete implementation of all 5 MCP tools with comprehensive testing: Core features: - journal_write: Add entries and update overviews - journal_read: Read sections with optional entry inclusion - journal_search: Dual-dimension semantic search with temporal salience - journal_toc: Navigate hierarchical journal structure - journal_list_entries: Browse entries with pagination Technical implementation: - Python project with uv package manager - JSON storage backend with atomic saves and nested sections - Sentence-transformers for semantic embeddings - Full type annotations and mypy compliance - Comprehensive test suite (6/6 tests passing) - CLI with configurable --data-file parameter This validates the journal server interface design and provides foundation for Phase 2 refinement and eventual git migration. Closes Phase 1 deliverables in tracking issue #9.
1 parent 0cd70b9 commit fd5451b

File tree

14 files changed

+2573
-10
lines changed

14 files changed

+2573
-10
lines changed

journal-mcp-server/.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.11

journal-mcp-server/README.md

Lines changed: 93 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,106 @@
22

33
A memory system that emerges from collaborative understanding, reimagining AI memory as an organic, reflective practice rather than mechanical storage.
44

5+
## Phase 1: JSON Prototype ✅
6+
7+
The JSON prototype is now complete with all core functionality:
8+
9+
- **5 MCP Tools**: `journal_read`, `journal_write`, `journal_search`, `journal_toc`, `journal_list_entries`
10+
- **Dual-dimension search**: Work context + content matching with temporal salience
11+
- **Configurable storage**: `--data-file` argument for custom JSON locations
12+
- **Full type checking**: mypy compliance with comprehensive test coverage
13+
514
## Quick Start
615

7-
*Implementation coming soon - currently in design phase*
16+
### Installation
17+
18+
```bash
19+
# Install in development mode
20+
uv pip install -e .
21+
22+
# Run with default data file (./journal.json)
23+
uv run journal-server
24+
25+
# Run with custom data file
26+
uv run journal-server --data-file ~/my-journal.json
27+
```
28+
29+
### Testing
30+
31+
```bash
32+
# Run all tests
33+
uv run pytest tests/ -v
34+
35+
# Run type checking
36+
uv run mypy src/journal_server/ --ignore-missing-imports
37+
```
38+
39+
## MCP Tools
40+
41+
### journal_write
42+
Add entries and optionally update section overviews:
43+
```json
44+
{
45+
"path": "project-alpha",
46+
"entry": "Implemented user authentication with JWT tokens",
47+
"work_context": "authentication development",
48+
"overview": "Building secure user management system"
49+
}
50+
```
51+
52+
### journal_read
53+
Read section overviews and recent entries:
54+
```json
55+
{
56+
"path": "project-alpha",
57+
"include_entries": true,
58+
"max_entries": 5
59+
}
60+
```
61+
62+
### journal_search
63+
Semantic search with dual-dimension matching:
64+
```json
65+
{
66+
"work_context": "authentication development",
67+
"content": "JWT tokens",
68+
"salience_threshold": 0.5
69+
}
70+
```
71+
72+
### journal_toc
73+
Navigate journal structure:
74+
```json
75+
{
76+
"max_depth": 3
77+
}
78+
```
79+
80+
### journal_list_entries
81+
Browse entries with pagination:
82+
```json
83+
{
84+
"path": "project-alpha",
85+
"limit": 10,
86+
"offset": 0
87+
}
88+
```
89+
90+
## Architecture
91+
92+
**Core concept**: Git-centric design where journal sections are markdown files with current understanding as file contents and incremental entries stored as git commit messages.
93+
94+
**Current implementation**: JSON prototype that validates the interface before migrating to git backend.
95+
96+
**Tree structure**: Hierarchical organization with overviews (current synthesis), entries (chronological journey), and subsections that emerge naturally.
897

998
## Documentation
1099

11100
For the full concept, design, and architecture details, see the [mdBook documentation](../src/journal-mcp-server/).
12101

13102
## Development Status
14103

15-
Track implementation progress in the [GitHub tracking issue](https://github.com/nikomat/socratic-shell/issues) - look for "Journal MCP Server Implementation".
16-
17-
## Core Concept
18-
19-
The journal server uses git as both storage engine and identifier system:
20-
- **File contents**: Current understanding (overview)
21-
- **Commit messages**: Incremental journey (journal entries)
22-
- **Git history**: Complete collaborative record
104+
Track implementation progress in [GitHub issue #9](https://github.com/socratic-shell/socratic-shell/issues/9).
23105

24-
This creates a memory system that captures both our current understanding and the collaborative journey that led there, aligning with natural patterns of exploration → synthesis → new exploration.
106+
**Phase 1 Complete**: JSON prototype with all 5 MCP tools working
107+
**Next**: Phase 2 interface refinement and real-world testing

journal-mcp-server/main.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
def main():
2+
print("Hello from journal-server!")
3+
4+
5+
if __name__ == "__main__":
6+
main()

journal-mcp-server/pyproject.toml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
[project]
2+
name = "journal-server"
3+
version = "0.1.0"
4+
description = "A memory system that emerges from collaborative understanding"
5+
readme = "README.md"
6+
requires-python = ">=3.11"
7+
dependencies = [
8+
"mcp>=1.12.1",
9+
"pydantic>=2.11.7",
10+
"sentence-transformers>=5.0.0",
11+
]
12+
13+
[project.scripts]
14+
journal-server = "journal_server.cli:main"
15+
16+
[dependency-groups]
17+
dev = [
18+
"black>=25.1.0",
19+
"isort>=6.0.1",
20+
"mypy>=1.17.0",
21+
"pytest>=8.4.1",
22+
"pytest-asyncio>=1.1.0",
23+
]
24+
25+
[tool.mypy]
26+
python_version = "3.11"
27+
warn_return_any = true
28+
warn_unused_configs = true
29+
ignore_missing_imports = true
30+
# Relax strict mode for MCP integration
31+
strict = false
32+
33+
[tool.black]
34+
line-length = 88
35+
target-version = ['py311']
36+
37+
[tool.isort]
38+
profile = "black"
39+
line_length = 88
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""Journal Server - A memory system that emerges from collaborative understanding."""
2+
3+
__version__ = "0.1.0"
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
"""CLI entry point for the journal server."""
2+
3+
import argparse
4+
import asyncio
5+
from pathlib import Path
6+
7+
from .server import JournalServer
8+
9+
10+
def main() -> None:
11+
"""Main CLI entry point."""
12+
parser = argparse.ArgumentParser(
13+
description="Journal MCP Server - A memory system that emerges from collaborative understanding"
14+
)
15+
parser.add_argument(
16+
"--data-file",
17+
type=Path,
18+
default=Path("./journal.json"),
19+
help="Path to the JSON data file (default: ./journal.json)",
20+
)
21+
22+
args = parser.parse_args()
23+
24+
# Create and run the server
25+
server = JournalServer(data_file=args.data_file)
26+
asyncio.run(server.run())
27+
28+
29+
if __name__ == "__main__":
30+
main()
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
"""Semantic search implementation for journal entries."""
2+
3+
import math
4+
from datetime import datetime, timedelta
5+
from typing import Any, List, Tuple, TYPE_CHECKING
6+
7+
from sentence_transformers import SentenceTransformer
8+
9+
from .types import Journal, JournalEntry, SearchResult
10+
11+
if TYPE_CHECKING:
12+
from .types import JournalSection
13+
14+
15+
class JournalSearcher:
16+
"""Semantic search for journal entries with dual-dimension matching."""
17+
18+
def __init__(self) -> None:
19+
# Use a lightweight model for embeddings
20+
self.model = SentenceTransformer('all-MiniLM-L6-v2')
21+
22+
def search(
23+
self,
24+
journal: Journal,
25+
work_context: str,
26+
content: str,
27+
salience_threshold: float = 0.5,
28+
max_results: int = 10,
29+
) -> List[SearchResult]:
30+
"""Search journal entries using dual-dimension matching."""
31+
32+
# Generate embeddings for search queries
33+
work_context_embedding = self.model.encode([work_context])[0]
34+
content_embedding = self.model.encode([content])[0]
35+
36+
results: List[SearchResult] = []
37+
38+
# Search through all sections and entries
39+
for section_path, section in journal.sections.items():
40+
self._search_section(
41+
section, section_path, work_context_embedding, content_embedding,
42+
salience_threshold, results
43+
)
44+
45+
# Sort by combined score (descending)
46+
results.sort(key=lambda r: r.combined_score, reverse=True)
47+
48+
return results[:max_results]
49+
50+
def _search_section(
51+
self,
52+
section: "JournalSection",
53+
section_path: str,
54+
work_context_embedding: Any,
55+
content_embedding: Any,
56+
salience_threshold: float,
57+
results: List[SearchResult],
58+
) -> None:
59+
"""Recursively search a section and its subsections."""
60+
61+
# Search entries in this section
62+
for i, entry in enumerate(section.entries):
63+
result = self._score_entry(
64+
section_path, i, entry, work_context_embedding, content_embedding
65+
)
66+
67+
if result.combined_score >= salience_threshold:
68+
results.append(result)
69+
70+
# Search subsections
71+
for subsection_name, subsection in section.subsections.items():
72+
subsection_path = f"{section_path}/{subsection_name}"
73+
self._search_section(
74+
subsection, subsection_path, work_context_embedding, content_embedding,
75+
salience_threshold, results
76+
)
77+
78+
def _score_entry(
79+
self,
80+
section_path: str,
81+
entry_index: int,
82+
entry: JournalEntry,
83+
work_context_embedding: Any,
84+
content_embedding: Any,
85+
) -> SearchResult:
86+
"""Score a single entry against the search criteria."""
87+
88+
# Generate embeddings for the entry
89+
entry_work_embedding = self.model.encode([entry.work_context])[0]
90+
entry_content_embedding = self.model.encode([entry.content])[0]
91+
92+
# Calculate cosine similarity scores
93+
work_context_score = self._cosine_similarity(work_context_embedding, entry_work_embedding)
94+
content_score = self._cosine_similarity(content_embedding, entry_content_embedding)
95+
96+
# Calculate temporal salience (recent entries score higher)
97+
temporal_score = self._calculate_temporal_score(entry.timestamp)
98+
99+
# Combine scores (equal weight for now, could be tunable)
100+
combined_score = (work_context_score + content_score) / 2 * temporal_score
101+
102+
return SearchResult(
103+
section_path=section_path,
104+
entry_index=entry_index,
105+
entry=entry,
106+
work_context_score=work_context_score,
107+
content_score=content_score,
108+
combined_score=combined_score,
109+
temporal_score=temporal_score,
110+
)
111+
112+
def _cosine_similarity(self, a: Any, b: Any) -> float:
113+
"""Calculate cosine similarity between two vectors."""
114+
import numpy as np
115+
116+
dot_product = np.dot(a, b)
117+
norm_a = np.linalg.norm(a)
118+
norm_b = np.linalg.norm(b)
119+
120+
if norm_a == 0 or norm_b == 0:
121+
return 0.0
122+
123+
return float(dot_product / (norm_a * norm_b))
124+
125+
def _calculate_temporal_score(self, timestamp: datetime) -> float:
126+
"""Calculate temporal salience score (recent entries score higher)."""
127+
now = datetime.utcnow()
128+
age = now - timestamp
129+
130+
# Exponential decay with half-life of 30 days
131+
half_life_days = 30
132+
decay_factor = math.exp(-age.days * math.log(2) / half_life_days)
133+
134+
# Ensure minimum score of 0.1 for very old entries
135+
return max(0.1, decay_factor)

0 commit comments

Comments
 (0)