We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/Liam-Deacon/zettelkasten-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
"""Service layer for Zettelkasten operations."""
import datetime
import logging
from pathlib import Path
from typing import Any, Dict, List, Optional, Set, Tuple, Union
from zettelkasten_mcp.config import config
from zettelkasten_mcp.models.schema import Link, LinkType, Note, NoteType, Tag
from zettelkasten_mcp.storage.note_repository import NoteRepository
logger = logging.getLogger(__name__)
class ZettelService:
"""Service for managing Zettelkasten notes."""
def __init__(self, repository: Optional[NoteRepository] = None):
"""Initialize the service."""
self.repository = repository or NoteRepository()
def initialize(self) -> None:
"""Initialize the service and dependencies."""
# Nothing to do here for synchronous implementation
# The repository is initialized in its constructor
pass
def create_note(
self,
title: str,
content: str,
note_type: NoteType = NoteType.PERMANENT,
tags: Optional[List[str]] = None,
metadata: Optional[Dict[str, Any]] = None
) -> Note:
"""Create a new note."""
if not title:
raise ValueError("Title is required")
if not content:
raise ValueError("Content is required")
# Create note object
note = Note(
title=title,
content=content,
note_type=note_type,
tags=[Tag(name=tag) for tag in (tags or [])],
metadata=metadata or {}
)
# Save to repository
return self.repository.create(note)
def get_note(self, note_id: str) -> Optional[Note]:
"""Retrieve a note by ID."""
return self.repository.get(note_id)
def get_note_by_title(self, title: str) -> Optional[Note]:
"""Retrieve a note by title."""
return self.repository.get_by_title(title)
def update_note(
self,
note_id: str,
title: Optional[str] = None,
content: Optional[str] = None,
note_type: Optional[NoteType] = None,
tags: Optional[List[str]] = None,
metadata: Optional[Dict[str, Any]] = None
) -> Note:
"""Update an existing note."""
note = self.repository.get(note_id)
if not note:
raise ValueError(f"Note with ID {note_id} not found")
# Update fields
if title is not None:
note.title = title
if content is not None:
note.content = content
if note_type is not None:
note.note_type = note_type
if tags is not None:
note.tags = [Tag(name=tag) for tag in tags]
if metadata is not None:
note.metadata = metadata
note.updated_at = datetime.datetime.now()
# Save to repository
return self.repository.update(note)
def delete_note(self, note_id: str) -> None:
"""Delete a note."""
self.repository.delete(note_id)
def get_all_notes(self) -> List[Note]:
"""Get all notes."""
return self.repository.get_all()
def search_notes(self, **kwargs: Any) -> List[Note]:
"""Search for notes based on criteria."""
return self.repository.search(**kwargs)
def get_notes_by_tag(self, tag: str) -> List[Note]:
"""Get notes by tag."""
return self.repository.find_by_tag(tag)
def add_tag_to_note(self, note_id: str, tag: str) -> Note:
"""Add a tag to a note."""
note = self.repository.get(note_id)
if not note:
raise ValueError(f"Note with ID {note_id} not found")
note.add_tag(tag)
return self.repository.update(note)
def remove_tag_from_note(self, note_id: str, tag: str) -> Note:
"""Remove a tag from a note."""
note = self.repository.get(note_id)
if not note:
raise ValueError(f"Note with ID {note_id} not found")
note.remove_tag(tag)
return self.repository.update(note)
def get_all_tags(self) -> List[Tag]:
"""Get all tags in the system."""
return self.repository.get_all_tags()
def create_link(
self,
source_id: str,
target_id: str,
link_type: LinkType = LinkType.REFERENCE,
description: Optional[str] = None,
bidirectional: bool = False,
bidirectional_type: Optional[LinkType] = None
) -> Tuple[Note, Optional[Note]]:
"""Create a link between notes with proper bidirectional semantics.
Args:
source_id: ID of the source note
target_id: ID of the target note
link_type: Type of link from source to target
description: Optional description of the link
bidirectional: Whether to create a link in both directions
bidirectional_type: Optional custom link type for the reverse direction
If not provided, an appropriate inverse relation will be used
Returns:
Tuple of (source_note, target_note or None)
"""
source_note = self.repository.get(source_id)
if not source_note:
raise ValueError(f"Source note with ID {source_id} not found")
target_note = self.repository.get(target_id)
if not target_note:
raise ValueError(f"Target note with ID {target_id} not found")
# Check if this link already exists before attempting to add it
for link in source_note.links:
if link.target_id == target_id and link.link_type == link_type:
# Link already exists, no need to add it again
if not bidirectional:
return source_note, None
break
else:
# Only add the link if it doesn't exist
source_note.add_link(target_id, link_type, description)
source_note = self.repository.update(source_note)
# If bidirectional, add link from target to source with appropriate semantics
reverse_note = None
if bidirectional:
# If no explicit bidirectional type is provided, determine appropriate inverse
if bidirectional_type is None:
# Map link types to their semantic inverses
inverse_map = {
LinkType.REFERENCE: LinkType.REFERENCE,
LinkType.EXTENDS: LinkType.EXTENDED_BY,
LinkType.EXTENDED_BY: LinkType.EXTENDS,
LinkType.REFINES: LinkType.REFINED_BY,
LinkType.REFINED_BY: LinkType.REFINES,
LinkType.CONTRADICTS: LinkType.CONTRADICTED_BY,
LinkType.CONTRADICTED_BY: LinkType.CONTRADICTS,
LinkType.QUESTIONS: LinkType.QUESTIONED_BY,
LinkType.QUESTIONED_BY: LinkType.QUESTIONS,
LinkType.SUPPORTS: LinkType.SUPPORTED_BY,
LinkType.SUPPORTED_BY: LinkType.SUPPORTS,
LinkType.RELATED: LinkType.RELATED
}
bidirectional_type = inverse_map.get(link_type, link_type)
# Check if the reverse link already exists before adding it
for link in target_note.links:
if link.target_id == source_id and link.link_type == bidirectional_type:
# Reverse link already exists, no need to add it again
return source_note, target_note
# Only add the reverse link if it doesn't exist
target_note.add_link(source_id, bidirectional_type, description)
reverse_note = self.repository.update(target_note)
return source_note, reverse_note
def remove_link(
self,
source_id: str,
target_id: str,
link_type: Optional[LinkType] = None,
bidirectional: bool = False
) -> Tuple[Note, Optional[Note]]:
"""Remove a link between notes."""
source_note = self.repository.get(source_id)
if not source_note:
raise ValueError(f"Source note with ID {source_id} not found")
# Remove link from source to target
source_note.remove_link(target_id, link_type)
source_note = self.repository.update(source_note)
# If bidirectional, remove link from target to source
reverse_note = None
if bidirectional:
target_note = self.repository.get(target_id)
if target_note:
target_note.remove_link(source_id, link_type)
reverse_note = self.repository.update(target_note)
return source_note, reverse_note
def get_linked_notes(
self, note_id: str, direction: str = "outgoing"
) -> List[Note]:
"""Get notes linked to/from a note."""
note = self.repository.get(note_id)
if not note:
raise ValueError(f"Note with ID {note_id} not found")
return self.repository.find_linked_notes(note_id, direction)
def rebuild_index(self) -> None:
"""Rebuild the database index from files."""
self.repository.rebuild_index()
def export_note(self, note_id: str, format: str = "markdown") -> str:
"""Export a note in the specified format."""
note = self.repository.get(note_id)
if not note:
raise ValueError(f"Note with ID {note_id} not found")
if format.lower() == "markdown":
return note.to_markdown()
else:
raise ValueError(f"Unsupported export format: {format}")
def find_similar_notes(self, note_id: str, threshold: float = 0.5) -> List[Tuple[Note, float]]:
"""Find notes similar to the given note based on shared tags and links."""
note = self.repository.get(note_id)
if not note:
raise ValueError(f"Note with ID {note_id} not found")
# Get all notes
all_notes = self.repository.get_all()
results = []
# Set of this note's tags and links
note_tags = {tag.name for tag in note.tags}
note_links = {link.target_id for link in note.links}
# Add notes linked to this note
incoming_notes = self.repository.find_linked_notes(note_id, "incoming")
note_incoming = {n.id for n in incoming_notes}
# For each note, calculate similarity
for other_note in all_notes:
if other_note.id == note_id:
continue
# Calculate tag overlap
other_tags = {tag.name for tag in other_note.tags}
tag_overlap = len(note_tags.intersection(other_tags))
# Calculate link overlap (outgoing)
other_links = {link.target_id for link in other_note.links}
link_overlap = len(note_links.intersection(other_links))
# Check if other note links to this note
incoming_overlap = 1 if other_note.id in note_incoming else 0
# Check if this note links to other note
outgoing_overlap = 1 if other_note.id in note_links else 0
# Calculate similarity score
# Weight: 40% tags, 20% outgoing links, 20% incoming links, 20% direct connections
total_possible = (
max(len(note_tags), len(other_tags)) * 0.4 +
max(len(note_links), len(other_links)) * 0.2 +
1 * 0.2 + # Possible incoming link
1 * 0.2 # Possible outgoing link
)
# Avoid division by zero
if total_possible == 0:
similarity = 0.0
else:
similarity = (
(tag_overlap * 0.4) +
(link_overlap * 0.2) +
(incoming_overlap * 0.2) +
(outgoing_overlap * 0.2)
) / total_possible
if similarity >= threshold:
results.append((other_note, similarity))
# Sort by similarity (descending)
results.sort(key=lambda x: x[1], reverse=True)
return results