From d98e89fe1ffa64ab9675c1ad49f03b099066ba50 Mon Sep 17 00:00:00 2001 From: Ricki Hirner Date: Mon, 27 Apr 2026 20:30:47 +0200 Subject: [PATCH] Add ListAllEventsTool to retrieve all events from a calendar collection --- .../at/bitfire/labs/davmcp/di/ToolsModule.kt | 4 + .../labs/davmcp/tools/ListAllEventsTool.kt | 115 ++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 server/src/main/kotlin/at/bitfire/labs/davmcp/tools/ListAllEventsTool.kt diff --git a/server/src/main/kotlin/at/bitfire/labs/davmcp/di/ToolsModule.kt b/server/src/main/kotlin/at/bitfire/labs/davmcp/di/ToolsModule.kt index 86fae3f..7eeb2bb 100644 --- a/server/src/main/kotlin/at/bitfire/labs/davmcp/di/ToolsModule.kt +++ b/server/src/main/kotlin/at/bitfire/labs/davmcp/di/ToolsModule.kt @@ -20,6 +20,10 @@ object ToolsModule { @IntoSet fun provideListCollectionsTool(listCollectionsTool: ListCollectionsTool): McpTool = listCollectionsTool + @Provides + @IntoSet + fun provideListAllEventsTool(listAllEventsTool: ListAllEventsTool): McpTool = listAllEventsTool + @Provides @IntoSet fun provideQueryByTimeTool(queryEventsByTimeTool: QueryEventsByTimeTool): McpTool = queryEventsByTimeTool diff --git a/server/src/main/kotlin/at/bitfire/labs/davmcp/tools/ListAllEventsTool.kt b/server/src/main/kotlin/at/bitfire/labs/davmcp/tools/ListAllEventsTool.kt new file mode 100644 index 0000000..0c70a24 --- /dev/null +++ b/server/src/main/kotlin/at/bitfire/labs/davmcp/tools/ListAllEventsTool.kt @@ -0,0 +1,115 @@ +package at.bitfire.labs.davmcp.tools + +import at.bitfire.dav4jvm.ktor.DavResource +import at.bitfire.dav4jvm.ktor.Response +import at.bitfire.dav4jvm.property.caldav.CalDAV +import at.bitfire.dav4jvm.property.caldav.CalendarData +import at.bitfire.dav4jvm.property.webdav.WebDAV +import at.bitfire.labs.davmcp.HttpClientBuilder +import at.bitfire.labs.davmcp.db.Database +import at.bitfire.labs.davmcp.db.User +import at.bitfire.labs.davmcp.icalendar.SimpleEvent +import at.bitfire.labs.davmcp.icalendar.SimpleEventConverter +import at.bitfire.labs.davmcp.icalendar.simpleEventSchema +import at.bitfire.labs.davmcp.json.McpJson +import collectionIdSchema +import io.ktor.http.* +import io.modelcontextprotocol.kotlin.sdk.server.ClientConnection +import io.modelcontextprotocol.kotlin.sdk.types.* +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.* +import javax.inject.Inject + +class ListAllEventsTool @Inject constructor( + private val database: Database, + private val httpClientBuilder: HttpClientBuilder, + private val simpleEventConverter: SimpleEventConverter +) : BaseMcpTool() { + + override fun tool() = Tool( + name = "events.listAll", + description = "List all events in a calendar collection", + inputSchema = ToolSchema( + properties = buildJsonObject { + collectionIdSchema() + }, + required = listOf() + ), + outputSchema = ToolSchema( + properties = buildJsonObject { + put("events", buildJsonObject { + put("type", "array") + put("items", buildJsonObject { + put("type", "object") + put("properties", buildJsonObject { + put("fileName", buildJsonObject { + put("type", "string") + put("description", "File name of the event (iCalendar)") + }) + put("eventData", buildJsonObject { + simpleEventSchema() + }) + }) + }) + }) + }, + required = listOf("events") + ), + annotations = ToolAnnotations( + readOnlyHint = true, + destructiveHint = false + ) + ) + + override suspend fun handle(connection: ClientConnection, user: User, request: CallToolRequest): CallToolResult { + val input = McpJson.decodeFromJsonElement( + request.arguments ?: throw IllegalArgumentException("Request arguments are required") + ) + logToolCall("ListAllEventsTool", user, input) + + val service = getCalDavService(database, user) + val collection = resolveCollection(database, service, input.collectionId) + val collectionUrl = Url(collection.url) + + httpClientBuilder.buildFromService(service).use { client -> + val davResource = DavResource(client, collectionUrl) + + val events = mutableListOf() + davResource.propfind(1, WebDAV.GetETag, CalDAV.CalendarData) { response, relation -> + if (relation != Response.HrefRelation.MEMBER) + return@propfind + + val calendarData = response[CalendarData::class.java]?.iCalendar ?: return@propfind + val event = simpleEventConverter.fromICalendar(calendarData) + if (event != null) + events += EventWithName( + fileName = response.hrefName(), + eventData = event + ) + } + return CallToolResult( + content = listOf(TextContent(McpJson.encodeToString(events))), + isError = false, + structuredContent = McpJson.encodeToJsonElement(OutputData(events)).jsonObject + ).also { logger.info("Result: $it") } + } + } + + + @Serializable + data class InputData( + val collectionId: Long? = null + ) + + @Serializable + data class EventWithName( + val fileName: String, + val eventData: SimpleEvent + ) + + @Serializable + data class OutputData( + val events: List + ) + +} \ No newline at end of file