fix: wrap raw list returns in response models to produce single TextContent block

MCP tools returning raw lists caused FastMCP's _convert_to_content() to create
one TextContent block per element. Most MCP clients only read content[0], so
they saw a single result instead of the full list.

Wrapped 9 tool functions in proper response objects:
- deck: deck_get_boards, deck_get_stacks, deck_get_cards, deck_get_labels
- calendar: nc_calendar_list_events, nc_calendar_get_upcoming_events
- contacts: nc_contacts_list_addressbooks, nc_contacts_list_contacts
- tables: nc_tables_list_tables

Closes #568

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-02-20 09:22:16 +01:00
co-authored by Claude Opus 4.6
parent 39d160ce48
commit 8887aa241a
7 changed files with 141 additions and 55 deletions
+34 -4
View File
@@ -5,6 +5,12 @@ 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.contacts import (
AddressBook,
Contact,
ListAddressBooksResponse,
ListContactsResponse,
)
from nextcloud_mcp_server.observability.metrics import instrument_tool
logger = logging.getLogger(__name__)
@@ -18,10 +24,21 @@ def configure_contacts_tools(mcp: FastMCP):
)
@require_scopes("contacts:read")
@instrument_tool
async def nc_contacts_list_addressbooks(ctx: Context):
async def nc_contacts_list_addressbooks(ctx: Context) -> ListAddressBooksResponse:
"""List all addressbooks for the user."""
client = await get_client(ctx)
return await client.contacts.list_addressbooks()
addressbooks_data = await client.contacts.list_addressbooks()
addressbooks = [
AddressBook(
uri=ab["name"],
displayname=ab.get("display_name", ab["name"]),
ctag=ab.get("getctag"),
)
for ab in addressbooks_data
]
return ListAddressBooksResponse(
addressbooks=addressbooks, total_count=len(addressbooks)
)
@mcp.tool(
title="List Contacts",
@@ -29,10 +46,23 @@ def configure_contacts_tools(mcp: FastMCP):
)
@require_scopes("contacts:read")
@instrument_tool
async def nc_contacts_list_contacts(ctx: Context, *, addressbook: str):
async def nc_contacts_list_contacts(
ctx: Context, *, addressbook: str
) -> ListContactsResponse:
"""List all contacts in the specified addressbook."""
client = await get_client(ctx)
return await client.contacts.list_contacts(addressbook=addressbook)
contacts_data = await client.contacts.list_contacts(addressbook=addressbook)
contacts = [
Contact(
uid=c["vcard_id"],
fn=c.get("contact", {}).get("fullname", ""),
etag=c.get("getetag"),
)
for c in contacts_data
]
return ListContactsResponse(
contacts=contacts, addressbook=addressbook, total_count=len(contacts)
)
@mcp.tool(
title="Create Address Book",