Skip to main content
Glama
cbcoutinho

Nextcloud MCP Server

by cbcoutinho
news.py11.6 kB
"""Client for Nextcloud News app operations.""" import logging from enum import IntEnum from typing import Any from .base import BaseNextcloudClient logger = logging.getLogger(__name__) class NewsItemType(IntEnum): """Type constants for News API item queries.""" FEED = 0 # Single feed FOLDER = 1 # Folder and its feeds STARRED = 2 # All starred items ALL = 3 # All items class NewsClient(BaseNextcloudClient): """Client for Nextcloud News app operations.""" app_name = "news" API_BASE = "/apps/news/api/v1-3" # --- Folders --- async def get_folders(self) -> list[dict[str, Any]]: """Get all folders.""" response = await self._make_request("GET", f"{self.API_BASE}/folders") return response.json().get("folders", []) async def create_folder(self, name: str) -> dict[str, Any]: """Create a new folder. Args: name: Folder name Returns: Created folder data Raises: HTTPStatusError: 409 if folder name already exists, 422 if name is empty """ response = await self._make_request( "POST", f"{self.API_BASE}/folders", json={"name": name} ) folders = response.json().get("folders", []) return folders[0] if folders else {} async def rename_folder(self, folder_id: int, name: str) -> None: """Rename a folder. Args: folder_id: Folder ID name: New folder name Raises: HTTPStatusError: 404 if folder not found, 409 if name exists """ await self._make_request( "PUT", f"{self.API_BASE}/folders/{folder_id}", json={"name": name} ) async def delete_folder(self, folder_id: int) -> None: """Delete a folder and all its feeds/items. Args: folder_id: Folder ID Raises: HTTPStatusError: 404 if folder not found """ await self._make_request("DELETE", f"{self.API_BASE}/folders/{folder_id}") async def mark_folder_read(self, folder_id: int, newest_item_id: int) -> None: """Mark all items in a folder as read. Args: folder_id: Folder ID newest_item_id: ID of newest item to mark read (prevents marking items user hasn't seen yet) Raises: HTTPStatusError: 404 if folder not found """ await self._make_request( "POST", f"{self.API_BASE}/folders/{folder_id}/read", json={"newestItemId": newest_item_id}, ) # --- Feeds --- async def get_feeds(self) -> dict[str, Any]: """Get all feeds with metadata. Returns: Dict with keys: - feeds: List of feed objects - starredCount: Number of starred items - newestItemId: ID of newest item (omitted if no items) """ response = await self._make_request("GET", f"{self.API_BASE}/feeds") return response.json() async def create_feed( self, url: str, folder_id: int | None = None ) -> dict[str, Any]: """Subscribe to a new feed. Args: url: Feed URL folder_id: Optional folder ID (None for root) Returns: Created feed data Raises: HTTPStatusError: 409 if feed already exists, 422 if URL is invalid """ body: dict[str, Any] = {"url": url} if folder_id is not None: body["folderId"] = folder_id response = await self._make_request("POST", f"{self.API_BASE}/feeds", json=body) data = response.json() feeds = data.get("feeds", []) return feeds[0] if feeds else {} async def delete_feed(self, feed_id: int) -> None: """Unsubscribe from a feed (deletes all items). Args: feed_id: Feed ID Raises: HTTPStatusError: 404 if feed not found """ await self._make_request("DELETE", f"{self.API_BASE}/feeds/{feed_id}") async def move_feed(self, feed_id: int, folder_id: int | None) -> None: """Move a feed to a different folder. Args: feed_id: Feed ID folder_id: Target folder ID (None for root) Raises: HTTPStatusError: 404 if feed not found """ await self._make_request( "POST", f"{self.API_BASE}/feeds/{feed_id}/move", json={"folderId": folder_id}, ) async def rename_feed(self, feed_id: int, title: str) -> None: """Rename a feed. Args: feed_id: Feed ID title: New feed title Raises: HTTPStatusError: 404 if feed not found """ await self._make_request( "POST", f"{self.API_BASE}/feeds/{feed_id}/rename", json={"feedTitle": title}, ) async def mark_feed_read(self, feed_id: int, newest_item_id: int) -> None: """Mark all items in a feed as read. Args: feed_id: Feed ID newest_item_id: ID of newest item to mark read Raises: HTTPStatusError: 404 if feed not found """ await self._make_request( "POST", f"{self.API_BASE}/feeds/{feed_id}/read", json={"newestItemId": newest_item_id}, ) # --- Items --- async def get_items( self, batch_size: int = 50, offset: int = 0, type_: int = NewsItemType.ALL, id_: int = 0, get_read: bool = True, oldest_first: bool = False, ) -> list[dict[str, Any]]: """Get items (articles) with filtering. Args: batch_size: Number of items to return (-1 for all) offset: Item ID to start after (for pagination) type_: Item type filter (NewsItemType) id_: Feed/folder ID (ignored for STARRED/ALL types) get_read: Include read items oldest_first: Sort oldest first instead of newest Returns: List of item objects """ params: dict[str, Any] = { "batchSize": batch_size, "offset": offset, "type": type_, "id": id_, "getRead": str(get_read).lower(), "oldestFirst": str(oldest_first).lower(), } response = await self._make_request( "GET", f"{self.API_BASE}/items", params=params ) return response.json().get("items", []) async def get_item(self, item_id: int) -> dict[str, Any]: """Get a specific item by ID. Note: The News API doesn't have a direct single-item endpoint, so we fetch all items and filter. For efficiency, consider caching or using get_items with specific feed if known. Args: item_id: Item ID Returns: Item data Raises: ValueError: If item not found """ # Fetch all items and find the one we need # This is inefficient but the API doesn't provide a direct endpoint items = await self.get_items(batch_size=-1, get_read=True) for item in items: if item.get("id") == item_id: return item raise ValueError(f"Item {item_id} not found") async def get_updated_items( self, last_modified: int, type_: int = NewsItemType.ALL, id_: int = 0, ) -> list[dict[str, Any]]: """Get items modified since a timestamp (for delta sync). Args: last_modified: Unix timestamp (seconds or microseconds) type_: Item type filter id_: Feed/folder ID Returns: List of modified items (includes deleted items) """ params: dict[str, Any] = { "lastModified": last_modified, "type": type_, "id": id_, } response = await self._make_request( "GET", f"{self.API_BASE}/items/updated", params=params ) return response.json().get("items", []) async def mark_item_read(self, item_id: int) -> None: """Mark a single item as read. Args: item_id: Item ID Raises: HTTPStatusError: 404 if item not found """ await self._make_request("POST", f"{self.API_BASE}/items/{item_id}/read") async def mark_item_unread(self, item_id: int) -> None: """Mark a single item as unread. Args: item_id: Item ID Raises: HTTPStatusError: 404 if item not found """ await self._make_request("POST", f"{self.API_BASE}/items/{item_id}/unread") async def star_item(self, item_id: int) -> None: """Star (favorite) a single item. Args: item_id: Item ID Raises: HTTPStatusError: 404 if item not found """ await self._make_request("POST", f"{self.API_BASE}/items/{item_id}/star") async def unstar_item(self, item_id: int) -> None: """Unstar a single item. Args: item_id: Item ID Raises: HTTPStatusError: 404 if item not found """ await self._make_request("POST", f"{self.API_BASE}/items/{item_id}/unstar") async def mark_items_read(self, item_ids: list[int]) -> None: """Mark multiple items as read. Args: item_ids: List of item IDs """ await self._make_request( "POST", f"{self.API_BASE}/items/read/multiple", json={"itemIds": item_ids} ) async def mark_items_unread(self, item_ids: list[int]) -> None: """Mark multiple items as unread. Args: item_ids: List of item IDs """ await self._make_request( "POST", f"{self.API_BASE}/items/unread/multiple", json={"itemIds": item_ids}, ) async def star_items(self, item_ids: list[int]) -> None: """Star multiple items. Args: item_ids: List of item IDs """ await self._make_request( "POST", f"{self.API_BASE}/items/star/multiple", json={"itemIds": item_ids} ) async def unstar_items(self, item_ids: list[int]) -> None: """Unstar multiple items. Args: item_ids: List of item IDs """ await self._make_request( "POST", f"{self.API_BASE}/items/unstar/multiple", json={"itemIds": item_ids}, ) async def mark_all_read(self, newest_item_id: int) -> None: """Mark all items as read. Args: newest_item_id: ID of newest item to mark read """ await self._make_request( "POST", f"{self.API_BASE}/items/read", json={"newestItemId": newest_item_id} ) # --- Status --- async def get_status(self) -> dict[str, Any]: """Get News app status and configuration. Returns: Dict with version and warnings """ response = await self._make_request("GET", f"{self.API_BASE}/status") return response.json() async def get_version(self) -> str: """Get News app version. Returns: Version string (e.g., "25.0.0") """ response = await self._make_request("GET", f"{self.API_BASE}/version") return response.json().get("version", "")

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cbcoutinho/nextcloud-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server