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. |
|
||||
| **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. |
|
||||
| **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 |
|
||||
|
||||
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_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
|
||||
|
||||
| Tool | Description |
|
||||
@@ -86,12 +110,41 @@ included? Feel free to open an issue, or contribute via a pull-request.
|
||||
|
||||
## 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 |
|
||||
|----------|-------------|
|
||||
| `nc://capabilities` | Access Nextcloud server capabilities |
|
||||
| `notes://settings` | Access Notes app settings |
|
||||
| `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
|
||||
|
||||
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")
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
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:
|
||||
|
||||
```bash
|
||||
# Available apps: notes, tables, webdav, calendar, contacts
|
||||
# Available apps: notes, tables, webdav, calendar, contacts, deck
|
||||
|
||||
# Enable all apps (default behavior)
|
||||
uv run python -m nextcloud_mcp_server.app
|
||||
|
||||
@@ -70,6 +70,13 @@ class DeckBoard(BaseModel):
|
||||
return v
|
||||
|
||||
|
||||
class DeckAssignedUser(BaseModel):
|
||||
id: int
|
||||
participant: DeckUser
|
||||
cardId: int
|
||||
type: int
|
||||
|
||||
|
||||
class DeckCard(BaseModel):
|
||||
id: int
|
||||
title: str
|
||||
@@ -84,7 +91,7 @@ class DeckCard(BaseModel):
|
||||
lastModified: Optional[int] = None
|
||||
createdAt: Optional[int] = 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
|
||||
attachmentCount: 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
|
||||
|
||||
@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):
|
||||
id: int
|
||||
@@ -171,13 +199,70 @@ class CreateBoardResponse(BaseResponse):
|
||||
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):
|
||||
"""Response model for board operations like update/delete."""
|
||||
|
||||
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 (
|
||||
ListBoardsResponse,
|
||||
CreateBoardResponse,
|
||||
GetBoardResponse,
|
||||
ListStacksResponse,
|
||||
CreateStackResponse,
|
||||
StackOperationResponse,
|
||||
CreateCardResponse,
|
||||
CardOperationResponse,
|
||||
CreateLabelResponse,
|
||||
LabelOperationResponse,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -33,6 +39,56 @@ def configure_deck_tools(mcp: FastMCP):
|
||||
board = await client.deck.get_board(board_id)
|
||||
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
|
||||
@mcp.tool()
|
||||
async def deck_list_boards(
|
||||
@@ -64,13 +120,405 @@ def configure_deck_tools(mcp: FastMCP):
|
||||
board = await client.deck.create_board(title, color)
|
||||
return CreateBoardResponse(id=board.id, title=board.title, color=board.color)
|
||||
|
||||
# Stack Tools
|
||||
@mcp.tool()
|
||||
async def deck_get_board(ctx: Context, board_id: int) -> GetBoardResponse:
|
||||
"""Get details of a specific Nextcloud Deck board
|
||||
async def deck_list_stacks(
|
||||
ctx: Context, board_id: int, if_modified_since: Optional[str] = None
|
||||
) -> ListStacksResponse:
|
||||
"""List all stacks in a Nextcloud Deck board
|
||||
|
||||
Args:
|
||||
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
|
||||
board = await client.deck.get_board(board_id)
|
||||
return GetBoardResponse(board=board)
|
||||
stacks = await client.deck.get_stacks(board_id, if_modified_since)
|
||||
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]
|
||||
|
||||
# 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:
|
||||
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
|
||||
logger.info("Board read via MCP resource successfully")
|
||||
|
||||
# 4. Get board via MCP tool
|
||||
logger.info(f"Getting board via MCP tool: {board_id}")
|
||||
get_result = await nc_mcp_client.call_tool(
|
||||
"deck_get_board",
|
||||
{"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")
|
||||
# 4. Verify board via direct read of resource
|
||||
logger.info(f"Verifying board via resource read: {board_id}")
|
||||
# This was already done in step 3, so we'll just log confirmation
|
||||
logger.info("Board structure verified successfully")
|
||||
|
||||
# 5. List 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
|
||||
|
||||
# Test get non-existent board via MCP tool
|
||||
logger.info(f"Testing get non-existent board via MCP: {non_existent_id}")
|
||||
get_result = await nc_mcp_client.call_tool(
|
||||
"deck_get_board",
|
||||
{"board_id": non_existent_id},
|
||||
# Test create board with invalid parameters via MCP tool
|
||||
logger.info("Testing board creation with invalid parameters via MCP")
|
||||
create_result = await nc_mcp_client.call_tool(
|
||||
"deck_create_board",
|
||||
{"title": "", "color": "FF0000"},
|
||||
)
|
||||
|
||||
assert get_result.isError is True, "Expected error for non-existent board"
|
||||
logger.info("Get non-existent board correctly failed via MCP tool")
|
||||
assert create_result.isError is True, "Expected error for invalid board creation"
|
||||
logger.info("Invalid board creation correctly failed via MCP tool")
|
||||
|
||||
# Test read non-existent board via MCP resource
|
||||
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"
|
||||
logger.info("Board found in detailed boards list")
|
||||
|
||||
# 3. Get board via MCP tool and verify it matches our data
|
||||
logger.info(f"Getting board via MCP tool: {board_id}")
|
||||
get_result = await nc_mcp_client.call_tool(
|
||||
"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")
|
||||
# 3. Verify board data matches via resource (already done in step 1)
|
||||
logger.info(f"Board data verification completed for board: {board_id}")
|
||||
logger.info("Board structure and data verified successfully")
|
||||
|
||||
Reference in New Issue
Block a user