4b026e9aa0
This implements ADR-009, which documents the decision to use a generic
`semantic:read` OAuth scope instead of requiring all app-specific scopes
for semantic search functionality.
Changes:
- Created new `nextcloud_mcp_server/models/semantic.py` with semantic search models
- SemanticSearchResult (with new doc_type field for multi-app support)
- SemanticSearchResponse
- SamplingSearchResponse
- VectorSyncStatusResponse
- Created new `nextcloud_mcp_server/server/semantic.py` with semantic search tools
- nc_semantic_search (renamed from nc_notes_semantic_search)
- nc_semantic_search_answer (renamed from nc_notes_semantic_search_answer)
- nc_get_vector_sync_status (renamed from nc_notes_get_vector_sync_status)
- All tools now use @require_scopes("semantic:read") instead of "notes:read"
- Updated `nextcloud_mcp_server/server/notes.py`
- Removed semantic search tools (moved to semantic.py)
- Removed semantic search model imports
- Removed unused MCP imports (ModelHint, ModelPreferences, etc.)
- Updated `nextcloud_mcp_server/models/notes.py`
- Removed semantic search models (moved to semantic.py)
- Updated `nextcloud_mcp_server/app.py`
- Import configure_semantic_tools
- Register semantic tools when VECTOR_SYNC_ENABLED=true
- Updated `nextcloud_mcp_server/server/__init__.py`
- Export configure_semantic_tools
- Updated tests
- tests/integration/test_sampling.py: Use new tool names
- tests/unit/test_response_models.py: Import from semantic.py, add doc_type field
Architecture:
- Semantic search is now a cross-app feature, not tied to Notes
- Uses dual-phase authorization: semantic:read scope + per-document verification
- Supports future multi-app indexing (notes, calendar, deck, files, contacts)
Test results:
- All 69 unit tests passing
- All 5 smoke tests passing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
86 lines
2.9 KiB
Python
86 lines
2.9 KiB
Python
"""Pydantic models for Notes app responses."""
|
|
|
|
from datetime import datetime
|
|
from typing import List, Optional
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
from .base import BaseResponse, IdResponse, StatusResponse
|
|
|
|
|
|
class Note(BaseModel):
|
|
"""Model for a Nextcloud note."""
|
|
|
|
id: int = Field(description="Note ID")
|
|
title: str = Field(description="Note title")
|
|
content: str = Field(description="Note content in markdown")
|
|
category: str = Field(default="", description="Note category")
|
|
modified: int = Field(description="Unix timestamp of last modification")
|
|
favorite: bool = Field(
|
|
default=False, description="Whether note is marked as favorite"
|
|
)
|
|
etag: str = Field(description="ETag for versioning")
|
|
readonly: bool = Field(default=False, description="Whether note is read-only")
|
|
|
|
@property
|
|
def modified_datetime(self) -> datetime:
|
|
"""Convert Unix timestamp to datetime."""
|
|
return datetime.fromtimestamp(self.modified)
|
|
|
|
|
|
class NoteSearchResult(BaseModel):
|
|
"""Model for note search results (limited fields)."""
|
|
|
|
id: int = Field(description="Note ID")
|
|
title: str = Field(description="Note title")
|
|
category: str = Field(default="", description="Note category")
|
|
score: Optional[float] = Field(None, description="Search relevance score")
|
|
|
|
|
|
class NotesSettings(BaseModel):
|
|
"""Model for Notes app settings."""
|
|
|
|
notesPath: str = Field(description="Path to notes directory")
|
|
fileSuffix: str = Field(description="File suffix for notes")
|
|
noteMode: str = Field(description="Note mode setting")
|
|
|
|
|
|
class CreateNoteResponse(IdResponse):
|
|
"""Response model for note creation."""
|
|
|
|
title: str = Field(description="The created note title")
|
|
category: str = Field(description="The created note category")
|
|
etag: str = Field(description="Current ETag for the created note")
|
|
|
|
|
|
class UpdateNoteResponse(BaseResponse):
|
|
"""Response model for note updates."""
|
|
|
|
id: int = Field(description="The updated note ID")
|
|
title: str = Field(description="The updated note title")
|
|
category: str = Field(description="The updated note category")
|
|
etag: str = Field(description="Current ETag for the updated note")
|
|
|
|
|
|
class DeleteNoteResponse(StatusResponse):
|
|
"""Response model for note deletion."""
|
|
|
|
deleted_id: int = Field(description="ID of the deleted note")
|
|
|
|
|
|
class AppendContentResponse(BaseResponse):
|
|
"""Response model for appending content to a note."""
|
|
|
|
id: int = Field(description="The updated note ID")
|
|
title: str = Field(description="The updated note title")
|
|
category: str = Field(description="The updated note category")
|
|
etag: str = Field(description="Current ETag for the updated note")
|
|
|
|
|
|
class SearchNotesResponse(BaseResponse):
|
|
"""Response model for note search."""
|
|
|
|
results: List[NoteSearchResult] = Field(description="Search results")
|
|
query: str = Field(description="The search query used")
|
|
total_found: int = Field(description="Total number of notes found")
|