f51b27ba19
- Guard board.labels against None in deck_get_labels and resource - Add TODO comments for calendar_display_name in single-calendar paths - Document _raw_contact_to_model scope limitation (maps only what the client returns; expanding requires changes to vCard parsing) - Log debug warning when event has no start_datetime - Verified Table model is safe with extra fields (Pydantic v2 ignores) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
727 lines
24 KiB
Python
727 lines
24 KiB
Python
import logging
|
|
from typing import Optional
|
|
|
|
from mcp.server.fastmcp import Context, FastMCP
|
|
from mcp.types import ToolAnnotations
|
|
|
|
from nextcloud_mcp_server.auth import require_scopes
|
|
from nextcloud_mcp_server.context import get_client
|
|
from nextcloud_mcp_server.models.deck import (
|
|
CardOperationResponse,
|
|
CreateBoardResponse,
|
|
CreateCardResponse,
|
|
CreateLabelResponse,
|
|
CreateStackResponse,
|
|
DeckBoard,
|
|
DeckCard,
|
|
DeckLabel,
|
|
DeckStack,
|
|
LabelOperationResponse,
|
|
ListBoardsResponse,
|
|
ListCardsResponse,
|
|
ListLabelsResponse,
|
|
ListStacksResponse,
|
|
StackOperationResponse,
|
|
)
|
|
from nextcloud_mcp_server.observability.metrics import instrument_tool
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def configure_deck_tools(mcp: FastMCP):
|
|
"""Configure Nextcloud Deck tools and resources for the MCP server."""
|
|
|
|
# Resources
|
|
@mcp.resource("nc://Deck/boards")
|
|
async def deck_boards_resource():
|
|
"""List all Nextcloud Deck boards"""
|
|
ctx: Context = mcp.get_context()
|
|
await ctx.warning("This message is deprecated, use the deck_get_board instead")
|
|
client = await get_client(ctx)
|
|
boards = await client.deck.get_boards()
|
|
return [board.model_dump() for board in boards]
|
|
|
|
@mcp.resource("nc://Deck/boards/{board_id}")
|
|
async def deck_board_resource(board_id: int):
|
|
"""Get details of a specific Nextcloud Deck board"""
|
|
ctx: Context = mcp.get_context()
|
|
await ctx.warning(
|
|
"This resource is deprecated, use the deck_get_board tool instead"
|
|
)
|
|
client = await get_client(ctx)
|
|
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()
|
|
await ctx.warning(
|
|
"This resource is deprecated, use the deck_get_stacks tool instead"
|
|
)
|
|
client = await get_client(ctx)
|
|
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()
|
|
await ctx.warning(
|
|
"This resource is deprecated, use the deck_get_stack tool instead"
|
|
)
|
|
client = await get_client(ctx)
|
|
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()
|
|
await ctx.warning(
|
|
"This resource is deprecated, use the deck_get_cards tool instead"
|
|
)
|
|
client = await get_client(ctx)
|
|
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()
|
|
await ctx.warning(
|
|
"This resource is deprecated, use the deck_get_card tool instead"
|
|
)
|
|
client = await get_client(ctx)
|
|
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()
|
|
await ctx.warning(
|
|
"This resource is deprecated, use the deck_get_labels tool instead"
|
|
)
|
|
client = await get_client(ctx)
|
|
board = await client.deck.get_board(board_id)
|
|
return [label.model_dump() for label in (board.labels or [])]
|
|
|
|
@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()
|
|
await ctx.warning(
|
|
"This resource is deprecated, use the deck_get_label tool instead"
|
|
)
|
|
client = await get_client(ctx)
|
|
label = await client.deck.get_label(board_id, label_id)
|
|
return label.model_dump()
|
|
|
|
# Read Tools (converted from resources)
|
|
|
|
@mcp.tool(
|
|
title="List Deck Boards",
|
|
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
|
)
|
|
@require_scopes("deck:read")
|
|
@instrument_tool
|
|
async def deck_get_boards(ctx: Context) -> ListBoardsResponse:
|
|
"""Get all Nextcloud Deck boards"""
|
|
client = await get_client(ctx)
|
|
boards = await client.deck.get_boards()
|
|
return ListBoardsResponse(boards=boards, total=len(boards))
|
|
|
|
@mcp.tool(
|
|
title="Get Deck Board",
|
|
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
|
)
|
|
@require_scopes("deck:read")
|
|
@instrument_tool
|
|
async def deck_get_board(ctx: Context, board_id: int) -> DeckBoard:
|
|
"""Get details of a specific Nextcloud Deck board"""
|
|
client = await get_client(ctx)
|
|
board = await client.deck.get_board(board_id)
|
|
return board
|
|
|
|
@mcp.tool(
|
|
title="List Deck Stacks",
|
|
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
|
)
|
|
@require_scopes("deck:read")
|
|
@instrument_tool
|
|
async def deck_get_stacks(ctx: Context, board_id: int) -> ListStacksResponse:
|
|
"""Get all stacks in a Nextcloud Deck board"""
|
|
client = await get_client(ctx)
|
|
stacks = await client.deck.get_stacks(board_id)
|
|
return ListStacksResponse(stacks=stacks, total=len(stacks))
|
|
|
|
@mcp.tool(
|
|
title="Get Deck Stack",
|
|
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
|
)
|
|
@require_scopes("deck:read")
|
|
@instrument_tool
|
|
async def deck_get_stack(ctx: Context, board_id: int, stack_id: int) -> DeckStack:
|
|
"""Get details of a specific Nextcloud Deck stack"""
|
|
client = await get_client(ctx)
|
|
stack = await client.deck.get_stack(board_id, stack_id)
|
|
return stack
|
|
|
|
@mcp.tool(
|
|
title="List Deck Cards",
|
|
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
|
)
|
|
@require_scopes("deck:read")
|
|
@instrument_tool
|
|
async def deck_get_cards(
|
|
ctx: Context, board_id: int, stack_id: int
|
|
) -> ListCardsResponse:
|
|
"""Get all cards in a Nextcloud Deck stack"""
|
|
client = await get_client(ctx)
|
|
stack = await client.deck.get_stack(board_id, stack_id)
|
|
cards = stack.cards or []
|
|
return ListCardsResponse(cards=cards, total=len(cards))
|
|
|
|
@mcp.tool(
|
|
title="Get Deck Card",
|
|
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
|
)
|
|
@require_scopes("deck:read")
|
|
@instrument_tool
|
|
async def deck_get_card(
|
|
ctx: Context, board_id: int, stack_id: int, card_id: int
|
|
) -> DeckCard:
|
|
"""Get details of a specific Nextcloud Deck card"""
|
|
client = await get_client(ctx)
|
|
card = await client.deck.get_card(board_id, stack_id, card_id)
|
|
return card
|
|
|
|
@mcp.tool(
|
|
title="List Deck Labels",
|
|
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
|
)
|
|
@require_scopes("deck:read")
|
|
@instrument_tool
|
|
async def deck_get_labels(ctx: Context, board_id: int) -> ListLabelsResponse:
|
|
"""Get all labels in a Nextcloud Deck board"""
|
|
client = await get_client(ctx)
|
|
board = await client.deck.get_board(board_id)
|
|
labels = board.labels or []
|
|
return ListLabelsResponse(labels=labels, total=len(labels))
|
|
|
|
@mcp.tool(
|
|
title="Get Deck Label",
|
|
annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=True),
|
|
)
|
|
@require_scopes("deck:read")
|
|
@instrument_tool
|
|
async def deck_get_label(ctx: Context, board_id: int, label_id: int) -> DeckLabel:
|
|
"""Get details of a specific Nextcloud Deck label"""
|
|
client = await get_client(ctx)
|
|
label = await client.deck.get_label(board_id, label_id)
|
|
return label
|
|
|
|
# Create/Update/Delete Tools
|
|
|
|
@mcp.tool(
|
|
title="Create Deck Board",
|
|
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
|
)
|
|
@require_scopes("deck:write")
|
|
@instrument_tool
|
|
async def deck_create_board(
|
|
ctx: Context, title: str, color: str
|
|
) -> CreateBoardResponse:
|
|
"""Create a new Nextcloud Deck board
|
|
|
|
Args:
|
|
title: The title of the new board
|
|
color: The hexadecimal color of the new board (e.g. FF0000)
|
|
"""
|
|
client = await get_client(ctx)
|
|
board = await client.deck.create_board(title, color)
|
|
return CreateBoardResponse(id=board.id, title=board.title, color=board.color)
|
|
|
|
# Stack Tools
|
|
|
|
@mcp.tool(
|
|
title="Create Deck Stack",
|
|
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
|
)
|
|
@require_scopes("deck:write")
|
|
@instrument_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 = await get_client(ctx)
|
|
stack = await client.deck.create_stack(board_id, title, order)
|
|
return CreateStackResponse(id=stack.id, title=stack.title, order=stack.order)
|
|
|
|
@mcp.tool(
|
|
title="Update Deck Stack",
|
|
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
|
)
|
|
@require_scopes("deck:write")
|
|
@instrument_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 = await get_client(ctx)
|
|
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(
|
|
title="Delete Deck Stack",
|
|
annotations=ToolAnnotations(
|
|
destructiveHint=True, idempotentHint=True, openWorldHint=True
|
|
),
|
|
)
|
|
@require_scopes("deck:write")
|
|
@instrument_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 = await get_client(ctx)
|
|
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(
|
|
title="Create Deck Card",
|
|
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
|
)
|
|
@require_scopes("deck:write")
|
|
@instrument_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 = await get_client(ctx)
|
|
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(
|
|
title="Update Deck Card",
|
|
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
|
)
|
|
@require_scopes("deck:write")
|
|
@instrument_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 = await get_client(ctx)
|
|
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(
|
|
title="Delete Deck Card",
|
|
annotations=ToolAnnotations(
|
|
destructiveHint=True, idempotentHint=True, openWorldHint=True
|
|
),
|
|
)
|
|
@require_scopes("deck:write")
|
|
@instrument_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 = await get_client(ctx)
|
|
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(
|
|
title="Archive Deck Card",
|
|
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
|
)
|
|
@require_scopes("deck:write")
|
|
@instrument_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 = await get_client(ctx)
|
|
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(
|
|
title="Unarchive Deck Card",
|
|
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
|
)
|
|
@require_scopes("deck:write")
|
|
@instrument_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 = await get_client(ctx)
|
|
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(
|
|
title="Reorder/Move Deck Card",
|
|
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
|
)
|
|
@require_scopes("deck:write")
|
|
@instrument_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 = await get_client(ctx)
|
|
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(
|
|
title="Create Deck Label",
|
|
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
|
)
|
|
@require_scopes("deck:write")
|
|
@instrument_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 = await get_client(ctx)
|
|
label = await client.deck.create_label(board_id, title, color)
|
|
return CreateLabelResponse(id=label.id, title=label.title, color=label.color)
|
|
|
|
@mcp.tool(
|
|
title="Update Deck Label",
|
|
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
|
)
|
|
@require_scopes("deck:write")
|
|
@instrument_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 = await get_client(ctx)
|
|
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(
|
|
title="Delete Deck Label",
|
|
annotations=ToolAnnotations(
|
|
destructiveHint=True, idempotentHint=True, openWorldHint=True
|
|
),
|
|
)
|
|
@require_scopes("deck:write")
|
|
@instrument_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 = await get_client(ctx)
|
|
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(
|
|
title="Assign Label to Deck Card",
|
|
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
|
)
|
|
@require_scopes("deck:write")
|
|
@instrument_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 = await get_client(ctx)
|
|
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(
|
|
title="Remove Label from Deck Card",
|
|
annotations=ToolAnnotations(
|
|
destructiveHint=True, idempotentHint=True, openWorldHint=True
|
|
),
|
|
)
|
|
@require_scopes("deck:write")
|
|
@instrument_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 = await get_client(ctx)
|
|
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(
|
|
title="Assign User to Deck Card",
|
|
annotations=ToolAnnotations(idempotentHint=False, openWorldHint=True),
|
|
)
|
|
@require_scopes("deck:write")
|
|
@instrument_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 = await get_client(ctx)
|
|
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(
|
|
title="Unassign User from Deck Card",
|
|
annotations=ToolAnnotations(
|
|
destructiveHint=True, idempotentHint=True, openWorldHint=True
|
|
),
|
|
)
|
|
@require_scopes("deck:write")
|
|
@instrument_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 = await get_client(ctx)
|
|
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,
|
|
)
|