Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import at.bitfire.labs.davmcp.icalendar.simpleEventSchema
import kotlinx.serialization.json.JsonObjectBuilder
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
Expand All @@ -10,4 +11,22 @@ internal fun JsonObjectBuilder.collectionIdSchema() {
"Optional ID of the targeted calendar collection. Must be empty (= default calendar) or a collection ID as returned by collections.list."
)
})
}

internal fun JsonObjectBuilder.eventListOutputSchema() {
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()
})
})
})
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package at.bitfire.labs.davmcp.tools

import at.bitfire.dav4jvm.ktor.Response
import at.bitfire.dav4jvm.property.caldav.CalendarData
import at.bitfire.labs.davmcp.icalendar.SimpleEvent
import at.bitfire.labs.davmcp.icalendar.SimpleEventConverter
import kotlinx.serialization.Serializable
import javax.inject.Inject

class EventResponseHandler @Inject constructor(
private val simpleEventConverter: SimpleEventConverter
) {

fun processCalendarResponse(response: Response, relation: Response.HrefRelation): EventWithName? {
if (relation != Response.HrefRelation.MEMBER)
return null

val calendarData = response[CalendarData::class.java]?.iCalendar ?: return null
val event = simpleEventConverter.fromICalendar(calendarData)
return event?.let {
EventWithName(
fileName = response.hrefName(),
eventData = it
)
}
}

@Serializable
data class EventWithName(
val fileName: String,
val eventData: SimpleEvent
)

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package at.bitfire.labs.davmcp.tools

import at.bitfire.dav4jvm.ktor.DavResource
import at.bitfire.dav4jvm.property.caldav.CalDAV
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.json.McpJson
import collectionIdSchema
import eventListOutputSchema
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.buildJsonObject
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.encodeToJsonElement
import kotlinx.serialization.json.jsonObject
import javax.inject.Inject

class ListAllEventsTool @Inject constructor(
private val database: Database,
private val httpClientBuilder: HttpClientBuilder,
private val eventResponseHandler: EventResponseHandler
) : 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 {
eventListOutputSchema()
},
required = listOf("events")
),
annotations = ToolAnnotations(
readOnlyHint = true,
destructiveHint = false
)
)

override suspend fun handle(connection: ClientConnection, user: User, request: CallToolRequest): CallToolResult {
val input = McpJson.decodeFromJsonElement<InputData>(
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<EventResponseHandler.EventWithName>()
davResource.propfind(1, WebDAV.GetETag, CalDAV.CalendarData) { response, relation ->
val eventWithName = eventResponseHandler.processCalendarResponse(response, relation)
if (eventWithName != null)
events += eventWithName
}
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 OutputData(
val events: List<EventResponseHandler.EventWithName>
)

}
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
package at.bitfire.labs.davmcp.tools

import at.bitfire.dav4jvm.ktor.DavCalendar
import at.bitfire.dav4jvm.ktor.Response
import at.bitfire.dav4jvm.property.caldav.CalDAV
import at.bitfire.dav4jvm.property.caldav.CalendarData
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 eventListOutputSchema
import io.ktor.http.*
import io.modelcontextprotocol.kotlin.sdk.server.ClientConnection
import io.modelcontextprotocol.kotlin.sdk.types.*
Expand All @@ -24,7 +20,7 @@ import javax.inject.Inject
class QueryEventsByTimeTool @Inject constructor(
private val database: Database,
private val httpClientBuilder: HttpClientBuilder,
private val simpleEventConverter: SimpleEventConverter
private val eventResponseHandler: EventResponseHandler
) : BaseMcpTool() {

override fun tool() = Tool(
Expand Down Expand Up @@ -54,21 +50,7 @@ class QueryEventsByTimeTool @Inject constructor(
),
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()
})
})
})
})
eventListOutputSchema()
},
required = listOf("events")
),
Expand All @@ -94,18 +76,11 @@ class QueryEventsByTimeTool @Inject constructor(
val start: Instant? = input.start?.let { Instant.parse(it) }
val end: Instant? = input.end?.let { Instant.parse(it) }

val events = mutableListOf<EventWithName>()
val events = mutableListOf<EventResponseHandler.EventWithName>()
calendar.calendarQuery(Component.VEVENT, start, end, setOf(CalDAV.CalendarData)) { response, relation ->
if (relation != Response.HrefRelation.MEMBER)
return@calendarQuery

val calendarData = response[CalendarData::class.java]?.iCalendar ?: return@calendarQuery
val event = simpleEventConverter.fromICalendar(calendarData)
if (event != null)
events += EventWithName(
fileName = response.hrefName(),
eventData = event
)
val eventWithName = eventResponseHandler.processCalendarResponse(response, relation)
if (eventWithName != null)
events += eventWithName
}
return CallToolResult(
content = listOf(TextContent(McpJson.encodeToString(events))),
Expand All @@ -123,15 +98,9 @@ class QueryEventsByTimeTool @Inject constructor(
val end: String?
)

@Serializable
data class EventWithName(
val fileName: String,
val eventData: SimpleEvent
)

@Serializable
data class OutputData(
val events: List<EventWithName>
val events: List<EventResponseHandler.EventWithName>
) {

}
Expand Down
Loading