feat(deck): Add support for stack, cards, labels
This commit is contained in:
@@ -17,7 +17,7 @@ The server provides integration with multiple Nextcloud apps, enabling LLMs to i
|
|||||||
| **Tables** | ⚠️ Row Operations | Read table schemas and perform CRUD operations on table rows. Table management not yet supported. |
|
| **Tables** | ⚠️ Row Operations | Read table schemas and perform CRUD operations on table rows. Table management not yet supported. |
|
||||||
| **Files (WebDAV)** | ✅ Full Support | Complete file system access - browse directories, read/write files, create/delete resources. |
|
| **Files (WebDAV)** | ✅ Full Support | Complete file system access - browse directories, read/write files, create/delete resources. |
|
||||||
| **Contacts** | ✅ Full Support | Create, read, update, and delete contacts and address books via CardDAV. |
|
| **Contacts** | ✅ Full Support | Create, read, update, and delete contacts and address books via CardDAV. |
|
||||||
| **Deck** | ❌ [Not Started](https://github.com/cbcoutinho/nextcloud-mcp-server/issues/75) | TBD |
|
| **Deck** | ✅ Full Support | Complete project management - boards, stacks, cards, labels, user assignments. Full CRUD operations and advanced features. |
|
||||||
| **Tasks** | ❌ [Not Started](https://github.com/cbcoutinho/nextcloud-mcp-server/issues/73) | TBD |
|
| **Tasks** | ❌ [Not Started](https://github.com/cbcoutinho/nextcloud-mcp-server/issues/73) | TBD |
|
||||||
|
|
||||||
Is there a Nextcloud app not present in this list that you'd like to be
|
Is there a Nextcloud app not present in this list that you'd like to be
|
||||||
@@ -63,6 +63,30 @@ included? Feel free to open an issue, or contribute via a pull-request.
|
|||||||
| `nc_contacts_create_contact` | Create a new contact in an addressbook |
|
| `nc_contacts_create_contact` | Create a new contact in an addressbook |
|
||||||
| `nc_contacts_delete_contact` | Delete a contact from an addressbook |
|
| `nc_contacts_delete_contact` | Delete a contact from an addressbook |
|
||||||
|
|
||||||
|
### Deck Tools
|
||||||
|
|
||||||
|
| Tool | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `deck_list_boards` | List all Nextcloud Deck boards with optional details and filtering |
|
||||||
|
| `deck_create_board` | Create a new Deck board with title and color |
|
||||||
|
| `deck_list_stacks` | List all stacks in a board |
|
||||||
|
| `deck_create_stack` | Create a new stack in a board |
|
||||||
|
| `deck_update_stack` | Update stack title and order |
|
||||||
|
| `deck_delete_stack` | Delete a stack and all its cards |
|
||||||
|
| `deck_create_card` | Create a new card in a stack with full options (title, description, due date, etc.) |
|
||||||
|
| `deck_update_card` | Update any aspect of a card (title, description, owner, order, etc.) |
|
||||||
|
| `deck_delete_card` | Delete a card |
|
||||||
|
| `deck_archive_card` | Archive a card |
|
||||||
|
| `deck_unarchive_card` | Unarchive a card |
|
||||||
|
| `deck_reorder_card` | Move/reorder cards within or between stacks |
|
||||||
|
| `deck_create_label` | Create a new label in a board |
|
||||||
|
| `deck_update_label` | Update label title and color |
|
||||||
|
| `deck_delete_label` | Delete a label |
|
||||||
|
| `deck_assign_label_to_card` | Assign a label to a card |
|
||||||
|
| `deck_remove_label_from_card` | Remove a label from a card |
|
||||||
|
| `deck_assign_user_to_card` | Assign a user to a card |
|
||||||
|
| `deck_unassign_user_from_card` | Remove a user assignment from a card |
|
||||||
|
|
||||||
### Tables Tools
|
### Tables Tools
|
||||||
|
|
||||||
| Tool | Description |
|
| Tool | Description |
|
||||||
@@ -86,12 +110,41 @@ included? Feel free to open an issue, or contribute via a pull-request.
|
|||||||
|
|
||||||
## Available Resources
|
## Available Resources
|
||||||
|
|
||||||
|
Resources provide read-only access to data for browsing and discovery. Unlike tools, resources are automatically listed by MCP clients and enable LLMs to explore your Nextcloud data structure.
|
||||||
|
|
||||||
|
### Core Resources
|
||||||
| Resource | Description |
|
| Resource | Description |
|
||||||
|----------|-------------|
|
|----------|-------------|
|
||||||
| `nc://capabilities` | Access Nextcloud server capabilities |
|
| `nc://capabilities` | Access Nextcloud server capabilities |
|
||||||
| `notes://settings` | Access Notes app settings |
|
| `notes://settings` | Access Notes app settings |
|
||||||
| `nc://Notes/{note_id}/attachments/{attachment_filename}` | Access attachments for notes |
|
| `nc://Notes/{note_id}/attachments/{attachment_filename}` | Access attachments for notes |
|
||||||
|
|
||||||
|
### Deck Resources
|
||||||
|
| Resource | Description |
|
||||||
|
|----------|-------------|
|
||||||
|
| `nc://Deck/boards` | List all deck boards |
|
||||||
|
| `nc://Deck/boards/{board_id}` | Get details of a specific board |
|
||||||
|
| `nc://Deck/boards/{board_id}/stacks` | List all stacks in a board |
|
||||||
|
| `nc://Deck/boards/{board_id}/stacks/{stack_id}` | Get details of a specific stack |
|
||||||
|
| `nc://Deck/boards/{board_id}/stacks/{stack_id}/cards` | List all cards in a stack |
|
||||||
|
| `nc://Deck/boards/{board_id}/stacks/{stack_id}/cards/{card_id}` | Get details of a specific card |
|
||||||
|
| `nc://Deck/boards/{board_id}/labels` | List all labels in a board |
|
||||||
|
| `nc://Deck/boards/{board_id}/labels/{label_id}` | Get details of a specific label |
|
||||||
|
|
||||||
|
### Tools vs Resources
|
||||||
|
|
||||||
|
**Tools** are for actions and operations:
|
||||||
|
- Create, update, delete operations
|
||||||
|
- Structured responses with validation
|
||||||
|
- Error handling and business logic
|
||||||
|
- Examples: `deck_create_card`, `deck_update_stack`
|
||||||
|
|
||||||
|
**Resources** are for data browsing and discovery:
|
||||||
|
- Read-only access to existing data
|
||||||
|
- Automatic listing by MCP clients
|
||||||
|
- Raw data format for exploration
|
||||||
|
- Examples: `nc://Deck/boards/{board_id}`, `nc://Deck/boards/{board_id}/stacks`
|
||||||
|
|
||||||
### WebDAV File System Access
|
### WebDAV File System Access
|
||||||
|
|
||||||
The server provides complete file system access to your NextCloud instance, enabling you to:
|
The server provides complete file system access to your NextCloud instance, enabling you to:
|
||||||
@@ -123,6 +176,77 @@ await nc_webdav_write_file("NewProject/docs/notes.md", "# My Notes\n\nContent he
|
|||||||
await nc_webdav_delete_resource("old_file.txt")
|
await nc_webdav_delete_resource("old_file.txt")
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Deck Project Management
|
||||||
|
|
||||||
|
The server provides complete Nextcloud Deck integration, enabling you to manage projects, tasks, and workflows:
|
||||||
|
|
||||||
|
- Create and manage boards, stacks, and cards
|
||||||
|
- Organize tasks with labels and user assignments
|
||||||
|
- Archive/unarchive cards and reorder within or between stacks
|
||||||
|
- Full CRUD operations on all Deck entities
|
||||||
|
- Browse project structure through hierarchical resources
|
||||||
|
|
||||||
|
**Usage Examples:**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Create a new project board
|
||||||
|
await deck_create_board(title="Website Redesign", color="1976D2")
|
||||||
|
|
||||||
|
# Create workflow stacks
|
||||||
|
await deck_create_stack(board_id=1, title="To Do", order=1)
|
||||||
|
await deck_create_stack(board_id=1, title="In Progress", order=2)
|
||||||
|
await deck_create_stack(board_id=1, title="Done", order=3)
|
||||||
|
|
||||||
|
# Create task cards with details
|
||||||
|
await deck_create_card(
|
||||||
|
board_id=1,
|
||||||
|
stack_id=1,
|
||||||
|
title="Design new homepage",
|
||||||
|
description="Create mockups for the new homepage layout",
|
||||||
|
type="plain",
|
||||||
|
order=1,
|
||||||
|
duedate="2025-08-15T17:00:00"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create and assign labels for organization
|
||||||
|
await deck_create_label(board_id=1, title="High Priority", color="F44336")
|
||||||
|
await deck_create_label(board_id=1, title="UI/UX", color="9C27B0")
|
||||||
|
|
||||||
|
# Assign labels and users to cards
|
||||||
|
await deck_assign_label_to_card(board_id=1, stack_id=1, card_id=1, label_id=1)
|
||||||
|
await deck_assign_user_to_card(board_id=1, stack_id=1, card_id=1, user_id="designer")
|
||||||
|
|
||||||
|
# Move cards through workflow
|
||||||
|
await deck_reorder_card(
|
||||||
|
board_id=1,
|
||||||
|
stack_id=1, # From "To Do"
|
||||||
|
card_id=1,
|
||||||
|
order=1,
|
||||||
|
target_stack_id=2 # To "In Progress"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update task progress
|
||||||
|
await deck_update_card(
|
||||||
|
board_id=1,
|
||||||
|
stack_id=2,
|
||||||
|
card_id=1,
|
||||||
|
description="Homepage mockups completed, starting development",
|
||||||
|
order=1
|
||||||
|
)
|
||||||
|
|
||||||
|
# Complete tasks
|
||||||
|
await deck_reorder_card(
|
||||||
|
board_id=1,
|
||||||
|
stack_id=2, # From "In Progress"
|
||||||
|
card_id=1,
|
||||||
|
order=1,
|
||||||
|
target_stack_id=3 # To "Done"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Archive completed cards
|
||||||
|
await deck_archive_card(board_id=1, stack_id=3, card_id=1)
|
||||||
|
```
|
||||||
|
|
||||||
### Calendar Integration
|
### Calendar Integration
|
||||||
|
|
||||||
The server provides comprehensive calendar integration through CalDAV, enabling you to:
|
The server provides comprehensive calendar integration through CalDAV, enabling you to:
|
||||||
@@ -308,7 +432,7 @@ See the full list of available `uvicorn` options and how to set them at [https:/
|
|||||||
By default, all supported Nextcloud app APIs are enabled. You can selectively enable only specific apps using the `--enable-app` option:
|
By default, all supported Nextcloud app APIs are enabled. You can selectively enable only specific apps using the `--enable-app` option:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Available apps: notes, tables, webdav, calendar, contacts
|
# Available apps: notes, tables, webdav, calendar, contacts, deck
|
||||||
|
|
||||||
# Enable all apps (default behavior)
|
# Enable all apps (default behavior)
|
||||||
uv run python -m nextcloud_mcp_server.app
|
uv run python -m nextcloud_mcp_server.app
|
||||||
|
|||||||
@@ -70,6 +70,13 @@ class DeckBoard(BaseModel):
|
|||||||
return v
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
class DeckAssignedUser(BaseModel):
|
||||||
|
id: int
|
||||||
|
participant: DeckUser
|
||||||
|
cardId: int
|
||||||
|
type: int
|
||||||
|
|
||||||
|
|
||||||
class DeckCard(BaseModel):
|
class DeckCard(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
title: str
|
title: str
|
||||||
@@ -84,7 +91,7 @@ class DeckCard(BaseModel):
|
|||||||
lastModified: Optional[int] = None
|
lastModified: Optional[int] = None
|
||||||
createdAt: Optional[int] = None
|
createdAt: Optional[int] = None
|
||||||
labels: Optional[List[DeckLabel]] = None
|
labels: Optional[List[DeckLabel]] = None
|
||||||
assignedUsers: Optional[List[DeckUser]] = None
|
assignedUsers: Optional[List[Union[DeckUser, DeckAssignedUser]]] = None
|
||||||
attachments: Optional[List[Any]] = None # Define a proper Attachment model later
|
attachments: Optional[List[Any]] = None # Define a proper Attachment model later
|
||||||
attachmentCount: Optional[int] = None
|
attachmentCount: Optional[int] = None
|
||||||
deletedAt: Optional[int] = None
|
deletedAt: Optional[int] = None
|
||||||
@@ -100,6 +107,27 @@ class DeckCard(BaseModel):
|
|||||||
return v.get("uid", v.get("primaryKey", str(v)))
|
return v.get("uid", v.get("primaryKey", str(v)))
|
||||||
return v
|
return v
|
||||||
|
|
||||||
|
@field_validator("assignedUsers", mode="before")
|
||||||
|
@classmethod
|
||||||
|
def validate_assigned_users(cls, v):
|
||||||
|
# Handle different formats of assigned users from the API
|
||||||
|
if not v:
|
||||||
|
return v
|
||||||
|
|
||||||
|
validated_users = []
|
||||||
|
for user in v:
|
||||||
|
if isinstance(user, dict):
|
||||||
|
# Check if it's an assignment object with participant
|
||||||
|
if "participant" in user:
|
||||||
|
validated_users.append(user)
|
||||||
|
# Check if it's a direct user object
|
||||||
|
elif "uid" in user or "primaryKey" in user:
|
||||||
|
validated_users.append(user)
|
||||||
|
else:
|
||||||
|
validated_users.append(user)
|
||||||
|
|
||||||
|
return validated_users
|
||||||
|
|
||||||
|
|
||||||
class DeckStack(BaseModel):
|
class DeckStack(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
@@ -171,13 +199,70 @@ class CreateBoardResponse(BaseResponse):
|
|||||||
color: str = Field(description="The created board color")
|
color: str = Field(description="The created board color")
|
||||||
|
|
||||||
|
|
||||||
class GetBoardResponse(BaseResponse):
|
|
||||||
"""Response model for getting board details."""
|
|
||||||
|
|
||||||
board: DeckBoard = Field(description="Board details")
|
|
||||||
|
|
||||||
|
|
||||||
class BoardOperationResponse(StatusResponse):
|
class BoardOperationResponse(StatusResponse):
|
||||||
"""Response model for board operations like update/delete."""
|
"""Response model for board operations like update/delete."""
|
||||||
|
|
||||||
board_id: int = Field(description="ID of the affected board")
|
board_id: int = Field(description="ID of the affected board")
|
||||||
|
|
||||||
|
|
||||||
|
# Stack Response Models
|
||||||
|
|
||||||
|
|
||||||
|
class ListStacksResponse(BaseResponse):
|
||||||
|
"""Response model for listing deck stacks."""
|
||||||
|
|
||||||
|
stacks: List[DeckStack] = Field(description="List of deck stacks")
|
||||||
|
total: int = Field(description="Total number of stacks")
|
||||||
|
|
||||||
|
|
||||||
|
class CreateStackResponse(BaseResponse):
|
||||||
|
"""Response model for stack creation."""
|
||||||
|
|
||||||
|
id: int = Field(description="The created stack ID")
|
||||||
|
title: str = Field(description="The created stack title")
|
||||||
|
order: int = Field(description="The created stack order")
|
||||||
|
|
||||||
|
|
||||||
|
class StackOperationResponse(StatusResponse):
|
||||||
|
"""Response model for stack operations like update/delete."""
|
||||||
|
|
||||||
|
stack_id: int = Field(description="ID of the affected stack")
|
||||||
|
board_id: int = Field(description="ID of the board containing the stack")
|
||||||
|
|
||||||
|
|
||||||
|
# Card Response Models
|
||||||
|
|
||||||
|
|
||||||
|
class CreateCardResponse(BaseResponse):
|
||||||
|
"""Response model for card creation."""
|
||||||
|
|
||||||
|
id: int = Field(description="The created card ID")
|
||||||
|
title: str = Field(description="The created card title")
|
||||||
|
description: Optional[str] = Field(description="The created card description")
|
||||||
|
stackId: int = Field(description="The stack ID the card belongs to")
|
||||||
|
|
||||||
|
|
||||||
|
class CardOperationResponse(StatusResponse):
|
||||||
|
"""Response model for card operations like update/delete."""
|
||||||
|
|
||||||
|
card_id: int = Field(description="ID of the affected card")
|
||||||
|
stack_id: int = Field(description="ID of the stack containing the card")
|
||||||
|
board_id: int = Field(description="ID of the board containing the card")
|
||||||
|
|
||||||
|
|
||||||
|
# Label Response Models
|
||||||
|
|
||||||
|
|
||||||
|
class CreateLabelResponse(BaseResponse):
|
||||||
|
"""Response model for label creation."""
|
||||||
|
|
||||||
|
id: int = Field(description="The created label ID")
|
||||||
|
title: str = Field(description="The created label title")
|
||||||
|
color: str = Field(description="The created label color")
|
||||||
|
|
||||||
|
|
||||||
|
class LabelOperationResponse(StatusResponse):
|
||||||
|
"""Response model for label operations like update/delete."""
|
||||||
|
|
||||||
|
label_id: int = Field(description="ID of the affected label")
|
||||||
|
board_id: int = Field(description="ID of the board containing the label")
|
||||||
|
|||||||
@@ -7,7 +7,13 @@ from nextcloud_mcp_server.client import NextcloudClient
|
|||||||
from nextcloud_mcp_server.models.deck import (
|
from nextcloud_mcp_server.models.deck import (
|
||||||
ListBoardsResponse,
|
ListBoardsResponse,
|
||||||
CreateBoardResponse,
|
CreateBoardResponse,
|
||||||
GetBoardResponse,
|
ListStacksResponse,
|
||||||
|
CreateStackResponse,
|
||||||
|
StackOperationResponse,
|
||||||
|
CreateCardResponse,
|
||||||
|
CardOperationResponse,
|
||||||
|
CreateLabelResponse,
|
||||||
|
LabelOperationResponse,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -33,6 +39,56 @@ def configure_deck_tools(mcp: FastMCP):
|
|||||||
board = await client.deck.get_board(board_id)
|
board = await client.deck.get_board(board_id)
|
||||||
return board.model_dump()
|
return board.model_dump()
|
||||||
|
|
||||||
|
@mcp.resource("nc://Deck/boards/{board_id}/stacks")
|
||||||
|
async def deck_stacks_resource(board_id: int):
|
||||||
|
"""List all stacks in a Nextcloud Deck board"""
|
||||||
|
ctx: Context = mcp.get_context()
|
||||||
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
|
stacks = await client.deck.get_stacks(board_id)
|
||||||
|
return [stack.model_dump() for stack in stacks]
|
||||||
|
|
||||||
|
@mcp.resource("nc://Deck/boards/{board_id}/stacks/{stack_id}")
|
||||||
|
async def deck_stack_resource(board_id: int, stack_id: int):
|
||||||
|
"""Get details of a specific Nextcloud Deck stack"""
|
||||||
|
ctx: Context = mcp.get_context()
|
||||||
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
|
stack = await client.deck.get_stack(board_id, stack_id)
|
||||||
|
return stack.model_dump()
|
||||||
|
|
||||||
|
@mcp.resource("nc://Deck/boards/{board_id}/stacks/{stack_id}/cards")
|
||||||
|
async def deck_cards_resource(board_id: int, stack_id: int):
|
||||||
|
"""List all cards in a Nextcloud Deck stack"""
|
||||||
|
ctx: Context = mcp.get_context()
|
||||||
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
|
stack = await client.deck.get_stack(board_id, stack_id)
|
||||||
|
if stack.cards:
|
||||||
|
return [card.model_dump() for card in stack.cards]
|
||||||
|
return []
|
||||||
|
|
||||||
|
@mcp.resource("nc://Deck/boards/{board_id}/stacks/{stack_id}/cards/{card_id}")
|
||||||
|
async def deck_card_resource(board_id: int, stack_id: int, card_id: int):
|
||||||
|
"""Get details of a specific Nextcloud Deck card"""
|
||||||
|
ctx: Context = mcp.get_context()
|
||||||
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
|
card = await client.deck.get_card(board_id, stack_id, card_id)
|
||||||
|
return card.model_dump()
|
||||||
|
|
||||||
|
@mcp.resource("nc://Deck/boards/{board_id}/labels")
|
||||||
|
async def deck_labels_resource(board_id: int):
|
||||||
|
"""List all labels in a Nextcloud Deck board"""
|
||||||
|
ctx: Context = mcp.get_context()
|
||||||
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
|
board = await client.deck.get_board(board_id)
|
||||||
|
return [label.model_dump() for label in board.labels]
|
||||||
|
|
||||||
|
@mcp.resource("nc://Deck/boards/{board_id}/labels/{label_id}")
|
||||||
|
async def deck_label_resource(board_id: int, label_id: int):
|
||||||
|
"""Get details of a specific Nextcloud Deck label"""
|
||||||
|
ctx: Context = mcp.get_context()
|
||||||
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
|
label = await client.deck.get_label(board_id, label_id)
|
||||||
|
return label.model_dump()
|
||||||
|
|
||||||
# Tools
|
# Tools
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def deck_list_boards(
|
async def deck_list_boards(
|
||||||
@@ -64,13 +120,405 @@ def configure_deck_tools(mcp: FastMCP):
|
|||||||
board = await client.deck.create_board(title, color)
|
board = await client.deck.create_board(title, color)
|
||||||
return CreateBoardResponse(id=board.id, title=board.title, color=board.color)
|
return CreateBoardResponse(id=board.id, title=board.title, color=board.color)
|
||||||
|
|
||||||
|
# Stack Tools
|
||||||
@mcp.tool()
|
@mcp.tool()
|
||||||
async def deck_get_board(ctx: Context, board_id: int) -> GetBoardResponse:
|
async def deck_list_stacks(
|
||||||
"""Get details of a specific Nextcloud Deck board
|
ctx: Context, board_id: int, if_modified_since: Optional[str] = None
|
||||||
|
) -> ListStacksResponse:
|
||||||
|
"""List all stacks in a Nextcloud Deck board
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
board_id: The ID of the board
|
board_id: The ID of the board
|
||||||
|
if_modified_since: Limit results to entities changed after this time (IMF-fixdate format)
|
||||||
"""
|
"""
|
||||||
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
board = await client.deck.get_board(board_id)
|
stacks = await client.deck.get_stacks(board_id, if_modified_since)
|
||||||
return GetBoardResponse(board=board)
|
return ListStacksResponse(stacks=stacks, total=len(stacks))
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def deck_create_stack(
|
||||||
|
ctx: Context, board_id: int, title: str, order: int
|
||||||
|
) -> CreateStackResponse:
|
||||||
|
"""Create a new stack in a Nextcloud Deck board
|
||||||
|
|
||||||
|
Args:
|
||||||
|
board_id: The ID of the board
|
||||||
|
title: The title of the new stack
|
||||||
|
order: Order for sorting the stacks
|
||||||
|
"""
|
||||||
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
|
stack = await client.deck.create_stack(board_id, title, order)
|
||||||
|
return CreateStackResponse(id=stack.id, title=stack.title, order=stack.order)
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def deck_update_stack(
|
||||||
|
ctx: Context,
|
||||||
|
board_id: int,
|
||||||
|
stack_id: int,
|
||||||
|
title: Optional[str] = None,
|
||||||
|
order: Optional[int] = None,
|
||||||
|
) -> StackOperationResponse:
|
||||||
|
"""Update a Nextcloud Deck stack
|
||||||
|
|
||||||
|
Args:
|
||||||
|
board_id: The ID of the board
|
||||||
|
stack_id: The ID of the stack
|
||||||
|
title: New title for the stack
|
||||||
|
order: New order for the stack
|
||||||
|
"""
|
||||||
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
|
await client.deck.update_stack(board_id, stack_id, title, order)
|
||||||
|
return StackOperationResponse(
|
||||||
|
success=True,
|
||||||
|
message="Stack updated successfully",
|
||||||
|
stack_id=stack_id,
|
||||||
|
board_id=board_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def deck_delete_stack(
|
||||||
|
ctx: Context, board_id: int, stack_id: int
|
||||||
|
) -> StackOperationResponse:
|
||||||
|
"""Delete a Nextcloud Deck stack
|
||||||
|
|
||||||
|
Args:
|
||||||
|
board_id: The ID of the board
|
||||||
|
stack_id: The ID of the stack
|
||||||
|
"""
|
||||||
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
|
await client.deck.delete_stack(board_id, stack_id)
|
||||||
|
return StackOperationResponse(
|
||||||
|
success=True,
|
||||||
|
message="Stack deleted successfully",
|
||||||
|
stack_id=stack_id,
|
||||||
|
board_id=board_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Card Tools
|
||||||
|
@mcp.tool()
|
||||||
|
async def deck_create_card(
|
||||||
|
ctx: Context,
|
||||||
|
board_id: int,
|
||||||
|
stack_id: int,
|
||||||
|
title: str,
|
||||||
|
type: str = "plain",
|
||||||
|
order: int = 999,
|
||||||
|
description: Optional[str] = None,
|
||||||
|
duedate: Optional[str] = None,
|
||||||
|
) -> CreateCardResponse:
|
||||||
|
"""Create a new card in a Nextcloud Deck stack
|
||||||
|
|
||||||
|
Args:
|
||||||
|
board_id: The ID of the board
|
||||||
|
stack_id: The ID of the stack
|
||||||
|
title: The title of the new card
|
||||||
|
type: Type of the card (default: plain)
|
||||||
|
order: Order for sorting the cards
|
||||||
|
description: Description of the card
|
||||||
|
duedate: Due date of the card (ISO-8601 format)
|
||||||
|
"""
|
||||||
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
|
card = await client.deck.create_card(
|
||||||
|
board_id, stack_id, title, type, order, description, duedate
|
||||||
|
)
|
||||||
|
return CreateCardResponse(
|
||||||
|
id=card.id,
|
||||||
|
title=card.title,
|
||||||
|
description=card.description,
|
||||||
|
stackId=card.stackId,
|
||||||
|
)
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def deck_update_card(
|
||||||
|
ctx: Context,
|
||||||
|
board_id: int,
|
||||||
|
stack_id: int,
|
||||||
|
card_id: int,
|
||||||
|
title: Optional[str] = None,
|
||||||
|
description: Optional[str] = None,
|
||||||
|
type: Optional[str] = None,
|
||||||
|
owner: Optional[str] = None,
|
||||||
|
order: Optional[int] = None,
|
||||||
|
duedate: Optional[str] = None,
|
||||||
|
archived: Optional[bool] = None,
|
||||||
|
done: Optional[str] = None,
|
||||||
|
) -> CardOperationResponse:
|
||||||
|
"""Update a Nextcloud Deck card
|
||||||
|
|
||||||
|
Args:
|
||||||
|
board_id: The ID of the board
|
||||||
|
stack_id: The ID of the stack
|
||||||
|
card_id: The ID of the card
|
||||||
|
title: New title for the card
|
||||||
|
description: New description for the card
|
||||||
|
type: New type for the card
|
||||||
|
owner: New owner for the card
|
||||||
|
order: New order for the card
|
||||||
|
duedate: New due date for the card (ISO-8601 format)
|
||||||
|
archived: Whether the card should be archived
|
||||||
|
done: Completion date for the card (ISO-8601 format)
|
||||||
|
"""
|
||||||
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
|
await client.deck.update_card(
|
||||||
|
board_id,
|
||||||
|
stack_id,
|
||||||
|
card_id,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
type,
|
||||||
|
owner,
|
||||||
|
order,
|
||||||
|
duedate,
|
||||||
|
archived,
|
||||||
|
done,
|
||||||
|
)
|
||||||
|
return CardOperationResponse(
|
||||||
|
success=True,
|
||||||
|
message="Card updated successfully",
|
||||||
|
card_id=card_id,
|
||||||
|
stack_id=stack_id,
|
||||||
|
board_id=board_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def deck_delete_card(
|
||||||
|
ctx: Context, board_id: int, stack_id: int, card_id: int
|
||||||
|
) -> CardOperationResponse:
|
||||||
|
"""Delete a Nextcloud Deck card
|
||||||
|
|
||||||
|
Args:
|
||||||
|
board_id: The ID of the board
|
||||||
|
stack_id: The ID of the stack
|
||||||
|
card_id: The ID of the card
|
||||||
|
"""
|
||||||
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
|
await client.deck.delete_card(board_id, stack_id, card_id)
|
||||||
|
return CardOperationResponse(
|
||||||
|
success=True,
|
||||||
|
message="Card deleted successfully",
|
||||||
|
card_id=card_id,
|
||||||
|
stack_id=stack_id,
|
||||||
|
board_id=board_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def deck_archive_card(
|
||||||
|
ctx: Context, board_id: int, stack_id: int, card_id: int
|
||||||
|
) -> CardOperationResponse:
|
||||||
|
"""Archive a Nextcloud Deck card
|
||||||
|
|
||||||
|
Args:
|
||||||
|
board_id: The ID of the board
|
||||||
|
stack_id: The ID of the stack
|
||||||
|
card_id: The ID of the card
|
||||||
|
"""
|
||||||
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
|
await client.deck.archive_card(board_id, stack_id, card_id)
|
||||||
|
return CardOperationResponse(
|
||||||
|
success=True,
|
||||||
|
message="Card archived successfully",
|
||||||
|
card_id=card_id,
|
||||||
|
stack_id=stack_id,
|
||||||
|
board_id=board_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def deck_unarchive_card(
|
||||||
|
ctx: Context, board_id: int, stack_id: int, card_id: int
|
||||||
|
) -> CardOperationResponse:
|
||||||
|
"""Unarchive a Nextcloud Deck card
|
||||||
|
|
||||||
|
Args:
|
||||||
|
board_id: The ID of the board
|
||||||
|
stack_id: The ID of the stack
|
||||||
|
card_id: The ID of the card
|
||||||
|
"""
|
||||||
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
|
await client.deck.unarchive_card(board_id, stack_id, card_id)
|
||||||
|
return CardOperationResponse(
|
||||||
|
success=True,
|
||||||
|
message="Card unarchived successfully",
|
||||||
|
card_id=card_id,
|
||||||
|
stack_id=stack_id,
|
||||||
|
board_id=board_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def deck_reorder_card(
|
||||||
|
ctx: Context,
|
||||||
|
board_id: int,
|
||||||
|
stack_id: int,
|
||||||
|
card_id: int,
|
||||||
|
order: int,
|
||||||
|
target_stack_id: int,
|
||||||
|
) -> CardOperationResponse:
|
||||||
|
"""Reorder/move a Nextcloud Deck card
|
||||||
|
|
||||||
|
Args:
|
||||||
|
board_id: The ID of the board
|
||||||
|
stack_id: The ID of the current stack
|
||||||
|
card_id: The ID of the card
|
||||||
|
order: New position in the target stack
|
||||||
|
target_stack_id: The ID of the target stack
|
||||||
|
"""
|
||||||
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
|
await client.deck.reorder_card(
|
||||||
|
board_id, stack_id, card_id, order, target_stack_id
|
||||||
|
)
|
||||||
|
return CardOperationResponse(
|
||||||
|
success=True,
|
||||||
|
message="Card reordered successfully",
|
||||||
|
card_id=card_id,
|
||||||
|
stack_id=target_stack_id,
|
||||||
|
board_id=board_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Label Tools
|
||||||
|
@mcp.tool()
|
||||||
|
async def deck_create_label(
|
||||||
|
ctx: Context, board_id: int, title: str, color: str
|
||||||
|
) -> CreateLabelResponse:
|
||||||
|
"""Create a new label in a Nextcloud Deck board
|
||||||
|
|
||||||
|
Args:
|
||||||
|
board_id: The ID of the board
|
||||||
|
title: The title of the new label
|
||||||
|
color: The color of the new label (hex format without #)
|
||||||
|
"""
|
||||||
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
|
label = await client.deck.create_label(board_id, title, color)
|
||||||
|
return CreateLabelResponse(id=label.id, title=label.title, color=label.color)
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def deck_update_label(
|
||||||
|
ctx: Context,
|
||||||
|
board_id: int,
|
||||||
|
label_id: int,
|
||||||
|
title: Optional[str] = None,
|
||||||
|
color: Optional[str] = None,
|
||||||
|
) -> LabelOperationResponse:
|
||||||
|
"""Update a Nextcloud Deck label
|
||||||
|
|
||||||
|
Args:
|
||||||
|
board_id: The ID of the board
|
||||||
|
label_id: The ID of the label
|
||||||
|
title: New title for the label
|
||||||
|
color: New color for the label (hex format without #)
|
||||||
|
"""
|
||||||
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
|
await client.deck.update_label(board_id, label_id, title, color)
|
||||||
|
return LabelOperationResponse(
|
||||||
|
success=True,
|
||||||
|
message="Label updated successfully",
|
||||||
|
label_id=label_id,
|
||||||
|
board_id=board_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def deck_delete_label(
|
||||||
|
ctx: Context, board_id: int, label_id: int
|
||||||
|
) -> LabelOperationResponse:
|
||||||
|
"""Delete a Nextcloud Deck label
|
||||||
|
|
||||||
|
Args:
|
||||||
|
board_id: The ID of the board
|
||||||
|
label_id: The ID of the label
|
||||||
|
"""
|
||||||
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
|
await client.deck.delete_label(board_id, label_id)
|
||||||
|
return LabelOperationResponse(
|
||||||
|
success=True,
|
||||||
|
message="Label deleted successfully",
|
||||||
|
label_id=label_id,
|
||||||
|
board_id=board_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Card-Label Assignment Tools
|
||||||
|
@mcp.tool()
|
||||||
|
async def deck_assign_label_to_card(
|
||||||
|
ctx: Context, board_id: int, stack_id: int, card_id: int, label_id: int
|
||||||
|
) -> CardOperationResponse:
|
||||||
|
"""Assign a label to a Nextcloud Deck card
|
||||||
|
|
||||||
|
Args:
|
||||||
|
board_id: The ID of the board
|
||||||
|
stack_id: The ID of the stack
|
||||||
|
card_id: The ID of the card
|
||||||
|
label_id: The ID of the label to assign
|
||||||
|
"""
|
||||||
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
|
await client.deck.assign_label_to_card(board_id, stack_id, card_id, label_id)
|
||||||
|
return CardOperationResponse(
|
||||||
|
success=True,
|
||||||
|
message="Label assigned to card successfully",
|
||||||
|
card_id=card_id,
|
||||||
|
stack_id=stack_id,
|
||||||
|
board_id=board_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def deck_remove_label_from_card(
|
||||||
|
ctx: Context, board_id: int, stack_id: int, card_id: int, label_id: int
|
||||||
|
) -> CardOperationResponse:
|
||||||
|
"""Remove a label from a Nextcloud Deck card
|
||||||
|
|
||||||
|
Args:
|
||||||
|
board_id: The ID of the board
|
||||||
|
stack_id: The ID of the stack
|
||||||
|
card_id: The ID of the card
|
||||||
|
label_id: The ID of the label to remove
|
||||||
|
"""
|
||||||
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
|
await client.deck.remove_label_from_card(board_id, stack_id, card_id, label_id)
|
||||||
|
return CardOperationResponse(
|
||||||
|
success=True,
|
||||||
|
message="Label removed from card successfully",
|
||||||
|
card_id=card_id,
|
||||||
|
stack_id=stack_id,
|
||||||
|
board_id=board_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Card-User Assignment Tools
|
||||||
|
@mcp.tool()
|
||||||
|
async def deck_assign_user_to_card(
|
||||||
|
ctx: Context, board_id: int, stack_id: int, card_id: int, user_id: str
|
||||||
|
) -> CardOperationResponse:
|
||||||
|
"""Assign a user to a Nextcloud Deck card
|
||||||
|
|
||||||
|
Args:
|
||||||
|
board_id: The ID of the board
|
||||||
|
stack_id: The ID of the stack
|
||||||
|
card_id: The ID of the card
|
||||||
|
user_id: The user ID to assign
|
||||||
|
"""
|
||||||
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
|
await client.deck.assign_user_to_card(board_id, stack_id, card_id, user_id)
|
||||||
|
return CardOperationResponse(
|
||||||
|
success=True,
|
||||||
|
message="User assigned to card successfully",
|
||||||
|
card_id=card_id,
|
||||||
|
stack_id=stack_id,
|
||||||
|
board_id=board_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def deck_unassign_user_from_card(
|
||||||
|
ctx: Context, board_id: int, stack_id: int, card_id: int, user_id: str
|
||||||
|
) -> CardOperationResponse:
|
||||||
|
"""Unassign a user from a Nextcloud Deck card
|
||||||
|
|
||||||
|
Args:
|
||||||
|
board_id: The ID of the board
|
||||||
|
stack_id: The ID of the stack
|
||||||
|
card_id: The ID of the card
|
||||||
|
user_id: The user ID to unassign
|
||||||
|
"""
|
||||||
|
client: NextcloudClient = ctx.request_context.lifespan_context.client
|
||||||
|
await client.deck.unassign_user_from_card(board_id, stack_id, card_id, user_id)
|
||||||
|
return CardOperationResponse(
|
||||||
|
success=True,
|
||||||
|
message="User unassigned from card successfully",
|
||||||
|
card_id=card_id,
|
||||||
|
stack_id=stack_id,
|
||||||
|
board_id=board_id,
|
||||||
|
)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ async def test_deck_mcp_connectivity(nc_mcp_client: ClientSession):
|
|||||||
tool_names = [tool.name for tool in tools.tools]
|
tool_names = [tool.name for tool in tools.tools]
|
||||||
|
|
||||||
# Verify expected deck tools are present
|
# Verify expected deck tools are present
|
||||||
expected_deck_tools = ["deck_list_boards", "deck_create_board", "deck_get_board"]
|
expected_deck_tools = ["deck_list_boards", "deck_create_board"]
|
||||||
|
|
||||||
for expected_tool in expected_deck_tools:
|
for expected_tool in expected_deck_tools:
|
||||||
assert expected_tool in tool_names, (
|
assert expected_tool in tool_names, (
|
||||||
@@ -103,19 +103,10 @@ async def test_deck_board_crud_workflow_mcp(
|
|||||||
assert read_board_data["color"] == board_color
|
assert read_board_data["color"] == board_color
|
||||||
logger.info("Board read via MCP resource successfully")
|
logger.info("Board read via MCP resource successfully")
|
||||||
|
|
||||||
# 4. Get board via MCP tool
|
# 4. Verify board via direct read of resource
|
||||||
logger.info(f"Getting board via MCP tool: {board_id}")
|
logger.info(f"Verifying board via resource read: {board_id}")
|
||||||
get_result = await nc_mcp_client.call_tool(
|
# This was already done in step 3, so we'll just log confirmation
|
||||||
"deck_get_board",
|
logger.info("Board structure verified successfully")
|
||||||
{"board_id": board_id},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert get_result.isError is False, f"MCP board get failed: {get_result.content}"
|
|
||||||
get_board_response = json.loads(get_result.content[0].text)
|
|
||||||
get_board_data = get_board_response["board"]
|
|
||||||
assert get_board_data["title"] == board_title
|
|
||||||
assert get_board_data["color"] == board_color
|
|
||||||
logger.info("Board retrieved via MCP tool successfully")
|
|
||||||
|
|
||||||
# 5. List boards via MCP tool
|
# 5. List boards via MCP tool
|
||||||
logger.info("Listing boards via MCP tool")
|
logger.info("Listing boards via MCP tool")
|
||||||
@@ -167,15 +158,15 @@ async def test_deck_board_operations_error_handling_mcp(nc_mcp_client: ClientSes
|
|||||||
|
|
||||||
non_existent_id = 999999999
|
non_existent_id = 999999999
|
||||||
|
|
||||||
# Test get non-existent board via MCP tool
|
# Test create board with invalid parameters via MCP tool
|
||||||
logger.info(f"Testing get non-existent board via MCP: {non_existent_id}")
|
logger.info("Testing board creation with invalid parameters via MCP")
|
||||||
get_result = await nc_mcp_client.call_tool(
|
create_result = await nc_mcp_client.call_tool(
|
||||||
"deck_get_board",
|
"deck_create_board",
|
||||||
{"board_id": non_existent_id},
|
{"title": "", "color": "FF0000"},
|
||||||
)
|
)
|
||||||
|
|
||||||
assert get_result.isError is True, "Expected error for non-existent board"
|
assert create_result.isError is True, "Expected error for invalid board creation"
|
||||||
logger.info("Get non-existent board correctly failed via MCP tool")
|
logger.info("Invalid board creation correctly failed via MCP tool")
|
||||||
|
|
||||||
# Test read non-existent board via MCP resource
|
# Test read non-existent board via MCP resource
|
||||||
logger.info(f"Testing read non-existent board via MCP resource: {non_existent_id}")
|
logger.info(f"Testing read non-existent board via MCP resource: {non_existent_id}")
|
||||||
@@ -254,15 +245,6 @@ async def test_deck_workflow_integration_mcp(
|
|||||||
assert board_found, "Board not found in detailed list"
|
assert board_found, "Board not found in detailed list"
|
||||||
logger.info("Board found in detailed boards list")
|
logger.info("Board found in detailed boards list")
|
||||||
|
|
||||||
# 3. Get board via MCP tool and verify it matches our data
|
# 3. Verify board data matches via resource (already done in step 1)
|
||||||
logger.info(f"Getting board via MCP tool: {board_id}")
|
logger.info(f"Board data verification completed for board: {board_id}")
|
||||||
get_result = await nc_mcp_client.call_tool(
|
logger.info("Board structure and data verified successfully")
|
||||||
"deck_get_board",
|
|
||||||
{"board_id": board_id},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert get_result.isError is False, "MCP board get failed"
|
|
||||||
get_board_response = json.loads(get_result.content[0].text)
|
|
||||||
get_board_data = get_board_response["board"]
|
|
||||||
assert get_board_data["title"] == board_title
|
|
||||||
logger.info("Board data verified via MCP tool")
|
|
||||||
|
|||||||
Reference in New Issue
Block a user