Compare commits

...

9 Commits

Author SHA1 Message Date
github-actions[bot] daeb95f3c3 bump: version 0.8.1 → 0.8.2 2025-08-31 10:36:56 +00:00
Chris Coutinho 36d44d1781 Merge pull request #139 from cbcoutinho/feature/notes-no-return-content
fix(notes): Remove note contents from responses to reduce token usage
2025-08-31 12:36:30 +02:00
Chris Coutinho 949fb7124b fix(notes): Remove note contents from responses to reduce token usage 2025-08-31 11:55:15 +02:00
github-actions[bot] 6c4f071d2b bump: version 0.8.0 → 0.8.1 2025-08-30 20:38:13 +00:00
Chris Coutinho 53b11f7fbb fix(model): Serialize timestamps in RFC3339 format 2025-08-30 22:37:16 +02:00
Chris Coutinho 336bc45637 Merge pull request #138 from cbcoutinho/renovate/nextcloud-31.0.8
chore(deps): update nextcloud:31.0.8 docker digest to fcf6370
2025-08-30 20:29:17 +02:00
renovate-bot-cbcoutinho[bot] 6c587bb265 chore(deps): update nextcloud:31.0.8 docker digest to fcf6370 2025-08-30 18:19:45 +00:00
github-actions[bot] 6b1f5c12c8 bump: version 0.7.2 → 0.8.0 2025-08-30 17:28:57 +00:00
Chris Coutinho f8dc1f060b Merge pull request #137 from cbcoutinho/feature/claude-code
Feature/claude code
2025-08-30 19:28:33 +02:00
8 changed files with 147 additions and 15 deletions
+23
View File
@@ -1,3 +1,26 @@
## v0.8.2 (2025-08-31)
### Fix
- **notes**: Remove note contents from responses to reduce token usage
## v0.8.1 (2025-08-30)
### Fix
- **model**: Serialize timestamps in RFC3339 format
## v0.8.0 (2025-08-30)
### Feat
- **client**: Preserve fields when modifying contacts/calendar resources
- **server**: Add structured output to all tool/resource output
### Refactor
- Use _make_request where available
## v0.7.2 (2025-08-30)
### Fix
+1 -1
View File
@@ -21,7 +21,7 @@ services:
restart: always
app:
image: nextcloud:31.0.8@sha256:3eaddb0a9c56e6cf81ad258a5d05b78f747f6434b974f9a44e3f0dd91311b6ef
image: nextcloud:31.0.8@sha256:fcf637074755bb1d27644441b938bf39b27dd6c0a8c2326a5752e1b3d5014366
#user: www-data:www-data
restart: always
#post_start:
+21 -5
View File
@@ -1,23 +1,39 @@
"""Base Pydantic models for common response patterns."""
from datetime import datetime
from datetime import datetime, timezone
from typing import Any, Dict, Optional, Union
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, field_serializer
def _utc_now() -> datetime:
"""Generate UTC timestamp for responses."""
return datetime.now(timezone.utc)
class BaseResponse(BaseModel):
"""Base response model for all MCP tool responses."""
model_config = {"json_encoders": {datetime: lambda v: v.isoformat()}}
success: bool = Field(
default=True, description="Whether the operation was successful"
)
timestamp: datetime = Field(
default_factory=datetime.now, description="Response timestamp"
default_factory=_utc_now, description="Response timestamp"
)
@field_serializer("timestamp")
def serialize_timestamp(self, timestamp: datetime) -> str:
"""Serialize timestamp to RFC3339 format for MCP compliance."""
if timestamp.tzinfo is None:
# If somehow we get a naive datetime, assume UTC
timestamp = timestamp.replace(tzinfo=timezone.utc)
# Use isoformat() which produces RFC3339 compliant format
# For UTC times, replace '+00:00' with 'Z' as preferred by many systems
iso_string = timestamp.isoformat()
if iso_string.endswith("+00:00"):
return iso_string[:-6] + "Z"
return iso_string
class ErrorResponse(BaseResponse):
"""Response model for error cases."""
+8 -3
View File
@@ -48,13 +48,16 @@ class NotesSettings(BaseModel):
class CreateNoteResponse(IdResponse):
"""Response model for note creation."""
note: Note = Field(description="The created note")
title: str = Field(description="The created note title")
category: str = Field(description="The created note category")
class UpdateNoteResponse(BaseResponse):
"""Response model for note updates."""
note: Note = Field(description="The updated 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")
class DeleteNoteResponse(StatusResponse):
@@ -66,7 +69,9 @@ class DeleteNoteResponse(StatusResponse):
class AppendContentResponse(BaseResponse):
"""Response model for appending content to a note."""
note: Note = Field(description="The updated note after appending content")
id: int = Field(description="The updated note ID")
title: str = Field(description="The updated note title")
category: str = Field(description="The updated note category")
class SearchNotesResponse(BaseResponse):
+10 -4
View File
@@ -90,7 +90,9 @@ def configure_notes_tools(mcp: FastMCP):
category=category,
)
note = Note(**note_data)
return CreateNoteResponse(id=note.id, note=note)
return CreateNoteResponse(
id=note.id, title=note.title, category=note.category
)
except HTTPStatusError as e:
if e.response.status_code == 403:
return ErrorResponse(
@@ -128,7 +130,9 @@ def configure_notes_tools(mcp: FastMCP):
category=category,
)
note = Note(**note_data)
return UpdateNoteResponse(note=note)
return UpdateNoteResponse(
id=note.id, title=note.title, category=note.category
)
except HTTPStatusError as e:
if e.response.status_code == 404:
return ErrorResponse(error=f"Note {note_id} not found")
@@ -151,7 +155,7 @@ def configure_notes_tools(mcp: FastMCP):
async def nc_notes_append_content(
note_id: int, content: str, ctx: Context
) -> AppendContentResponse | ErrorResponse:
"""Append content to an existing note with a clear separator"""
"""Append content to an existing note with a clear separator. The tool automatically adds separators between existing and new content - do not include separators in your content."""
logger.info("Appending content to note %s", note_id)
client: NextcloudClient = ctx.request_context.lifespan_context.client
try:
@@ -159,7 +163,9 @@ def configure_notes_tools(mcp: FastMCP):
note_id=note_id, content=content
)
note = Note(**note_data)
return AppendContentResponse(note=note)
return AppendContentResponse(
id=note.id, title=note.title, category=note.category
)
except HTTPStatusError as e:
if e.response.status_code == 404:
return ErrorResponse(error=f"Note {note_id} not found")
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "nextcloud-mcp-server"
version = "0.7.2"
version = "0.8.2"
description = ""
authors = [
{name = "Chris Coutinho",email = "chris@coutinho.io"}
+82
View File
@@ -0,0 +1,82 @@
"""Unit tests for Pydantic models and serialization."""
import json
import re
from datetime import datetime, timezone
from nextcloud_mcp_server.models.base import BaseResponse, SuccessResponse
def test_timestamp_format_validation():
"""Test that timestamps in BaseResponse are RFC3339 compliant for MCP validation.
This test should initially fail, demonstrating the timestamp validation error
seen in MCP inspector. MCP expects RFC3339 format with timezone information.
"""
# Create a response object
response = SuccessResponse(message="Test message")
# Serialize to JSON (mimics what MCP inspector sees)
json_str = response.model_dump_json()
data = json.loads(json_str)
timestamp_str = data["timestamp"]
# RFC3339 regex pattern (what MCP expects)
# Format: YYYY-MM-DDTHH:MM:SS[.ffffff][Z|±HH:MM]
rfc3339_pattern = (
r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$"
)
# This assertion should FAIL with current implementation
assert re.match(rfc3339_pattern, timestamp_str), (
f"Timestamp '{timestamp_str}' is not RFC3339 compliant. "
f"MCP expects format like '2025-08-30T19:22:58.862377Z' or '2025-08-30T19:22:58.862377+00:00'"
)
def test_base_response_timestamp_is_utc():
"""Test that BaseResponse timestamps are in UTC timezone."""
response = BaseResponse()
# The timestamp should be timezone-aware and in UTC
assert response.timestamp.tzinfo is not None, (
"Timestamp should have timezone information"
)
assert response.timestamp.tzinfo == timezone.utc, (
"Timestamp should be in UTC timezone"
)
def test_serialized_timestamp_ends_with_z_or_offset():
"""Test that serialized timestamps have proper timezone suffix."""
response = BaseResponse()
json_str = response.model_dump_json()
data = json.loads(json_str)
timestamp_str = data["timestamp"]
# Should end with 'Z' (UTC) or timezone offset like '+00:00'
assert timestamp_str.endswith("Z") or re.search(
r"[+-]\d{2}:\d{2}$", timestamp_str
), (
f"Timestamp '{timestamp_str}' should end with 'Z' or timezone offset like '+00:00'"
)
def test_current_broken_format():
"""Test showing the current broken timestamp format that causes MCP validation errors."""
# This demonstrates what the current code produces
current_naive_dt = datetime.now()
current_format = current_naive_dt.isoformat()
# Show that current format lacks timezone info
assert "Z" not in current_format
assert "+" not in current_format
assert "-" not in current_format[-6:] # Check last 6 chars for timezone
print(f"Current broken format: {current_format}")
print(
"This format causes MCP validation errors because it lacks timezone information"
)
Generated
+1 -1
View File
@@ -505,7 +505,7 @@ wheels = [
[[package]]
name = "nextcloud-mcp-server"
version = "0.7.2"
version = "0.8.2"
source = { editable = "." }
dependencies = [
{ name = "httpx" },