add caldav mcp server
refactoring
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
## Assistant config
|
||||
```toml
|
||||
interface = "[Cli|Audio]" # optional, default is Cli
|
||||
permissions = [
|
||||
{tool-name = "mcp-server:tool", permission="[Allowed|Ask|Denied]"}
|
||||
] # optional
|
||||
@@ -14,7 +15,7 @@ system_prompt = "..." # optional
|
||||
context_size = 2048 # optional
|
||||
temperature = 0.8 # optional
|
||||
|
||||
[mcp-servers]
|
||||
[[mcp-servers]]
|
||||
name="..." # optional, otherwise provided server name or random server name is chosen
|
||||
url="..."
|
||||
authorization="..." #optional
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::path::Path;
|
||||
use thiserror::Error;
|
||||
use serde::de::DeserializeOwned;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ConfigLoadingError{
|
||||
pub enum ConfigLoadingError {
|
||||
#[error(transparent)]
|
||||
IoError(#[from] std::io::Error),
|
||||
#[error(transparent)]
|
||||
ParseError(#[from] toml::de::Error),
|
||||
}
|
||||
|
||||
pub fn config_from_file<T: DeserializeOwned>(filepath: impl AsRef<Path>) -> Result<T, ConfigLoadingError> {
|
||||
pub fn config_from_file<T: DeserializeOwned>(
|
||||
filepath: impl AsRef<Path>,
|
||||
) -> Result<T, ConfigLoadingError> {
|
||||
let toml_string: String = std::fs::read_to_string(filepath)?;
|
||||
Ok(toml::from_str(&toml_string)?)
|
||||
}
|
||||
Generated
+731
-13
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "mcp_server_collection"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
@@ -9,11 +9,21 @@ chrono = "0.4.44"
|
||||
axum = "0.8.8"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
tokio = { version = "1.51.0", features = ["full"] }
|
||||
own_assist_common = {path="../common"}
|
||||
own_assist_common = { path = "../common" }
|
||||
tokio-util = "0.7.18"
|
||||
tracing-subscriber = {version = "0.3.23", features = ["env-filter"]}
|
||||
url = {version = "2.5.8", features = ["serde"]}
|
||||
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
|
||||
url = { version = "2.5.8", features = ["serde"] }
|
||||
uuid = { version = "1.23.0", features = ["v4"], optional = true }
|
||||
tower-http = { version = "0.6.8", features = ["auth"], optional = true }
|
||||
thiserror = { version = "2.0.18", optional = true }
|
||||
libdav = { version = "0.10.3", optional = true }
|
||||
icalendar = { version = "0.17.10", optional = true, features = ["chrono-tz"] }
|
||||
hyper-util = { version = "0.1.20", optional = true }
|
||||
http = { version = "1.4.0", optional = true }
|
||||
hyper-rustls = { version = "0.27.7", optional = true }
|
||||
toml = "1.1.2+spec-1.1.0"
|
||||
|
||||
[features]
|
||||
default = ["datetime"]
|
||||
default = ["datetime", "caldav"]
|
||||
datetime = []
|
||||
caldav = ["dep:uuid", "dep:tower-http", "dep:thiserror", "dep:libdav", "dep:icalendar", "dep:hyper-util", "dep:http", "dep:hyper-rustls"]
|
||||
@@ -0,0 +1,149 @@
|
||||
use http::{StatusCode, Uri};
|
||||
use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder};
|
||||
use hyper_util::client::legacy::Client;
|
||||
use hyper_util::client::legacy::connect::HttpConnector;
|
||||
use hyper_util::rt::TokioExecutor;
|
||||
use icalendar::{Calendar, CalendarComponent};
|
||||
use libdav::caldav::{FindCalendarHomeSet, FindCalendars, GetCalendarResources};
|
||||
use libdav::dav::{FoundCollection, PutResource, PutResourceResponse, WebDavClient, WebDavError};
|
||||
use libdav::{CalDavClient, FetchedResource};
|
||||
use rmcp::ErrorData;
|
||||
use rmcp::model::ErrorCode;
|
||||
use thiserror::Error;
|
||||
use tower_http::auth::AddAuthorization;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub(crate) type AuthorizedCaldavClient =
|
||||
CalDavClient<AddAuthorization<Client<HttpsConnector<HttpConnector>, String>>>;
|
||||
|
||||
pub(crate) fn get_caldav_client(
|
||||
webdav_uri: Uri,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> AuthorizedCaldavClient {
|
||||
let https_connector: HttpsConnector<HttpConnector> = HttpsConnectorBuilder::new()
|
||||
.with_native_roots()
|
||||
.unwrap()
|
||||
.https_only()
|
||||
.enable_http1()
|
||||
.build();
|
||||
|
||||
let https_client: Client<HttpsConnector<HttpConnector>, _> =
|
||||
Client::builder(TokioExecutor::new()).build(https_connector);
|
||||
|
||||
let https_client_with_auth = AddAuthorization::basic(https_client, username, password);
|
||||
|
||||
CalDavClient::new(WebDavClient::new(webdav_uri, https_client_with_auth))
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub(crate) enum CalendarError {
|
||||
#[error("failed to get user principal")]
|
||||
UserPrincipalError(
|
||||
#[from] libdav::dav::FindCurrentUserPrincipalError<hyper_util::client::legacy::Error>,
|
||||
),
|
||||
#[error("User Principal does not exist")]
|
||||
NoUserPrincipal,
|
||||
#[error("webdav error")]
|
||||
WebDavError(#[from] WebDavError<hyper_util::client::legacy::Error>),
|
||||
#[error("unavailable resource")]
|
||||
UnavailableResourceError(StatusCode),
|
||||
#[error("failed Parsing a caldav resource")]
|
||||
ParsingError(String),
|
||||
}
|
||||
|
||||
impl From<CalendarError> for ErrorData {
|
||||
fn from(value: CalendarError) -> Self {
|
||||
use crate::caldav::caldav::CalendarError::*;
|
||||
|
||||
match value {
|
||||
UserPrincipalError(e) => ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None),
|
||||
NoUserPrincipal => ErrorData::new(
|
||||
ErrorCode::RESOURCE_NOT_FOUND,
|
||||
"no user principal".to_string(),
|
||||
None,
|
||||
),
|
||||
WebDavError(e) => ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None),
|
||||
UnavailableResourceError(e) => {
|
||||
ErrorData::new(ErrorCode::RESOURCE_NOT_FOUND, e.to_string(), None)
|
||||
}
|
||||
ParsingError(e) => ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn find_calendars(
|
||||
caldav_client: &AuthorizedCaldavClient,
|
||||
) -> Result<Vec<FoundCollection>, CalendarError> {
|
||||
use CalendarError::*;
|
||||
|
||||
let user_principal = caldav_client
|
||||
.find_current_user_principal()
|
||||
.await?
|
||||
.ok_or(NoUserPrincipal)?;
|
||||
|
||||
let calendar_home_sets = caldav_client
|
||||
.request(FindCalendarHomeSet::new(&user_principal))
|
||||
.await?
|
||||
.home_sets;
|
||||
|
||||
Ok(caldav_client
|
||||
.request(FindCalendars::new(&calendar_home_sets[0]))
|
||||
.await?
|
||||
.calendars)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_calendar_resources(
|
||||
caldav_client: &AuthorizedCaldavClient,
|
||||
calendar: &FoundCollection,
|
||||
) -> Result<Vec<FetchedResource>, CalendarError> {
|
||||
let calendar_resources = caldav_client
|
||||
.request(GetCalendarResources::new(calendar.href.as_str()))
|
||||
.await
|
||||
.map_err(CalendarError::WebDavError)?
|
||||
.resources;
|
||||
|
||||
Ok(calendar_resources)
|
||||
}
|
||||
|
||||
pub(crate) async fn upload_components(
|
||||
authorized_client: &AuthorizedCaldavClient,
|
||||
calendar: &FoundCollection,
|
||||
components: Vec<impl Into<CalendarComponent>>,
|
||||
) -> Result<PutResourceResponse, CalendarError> {
|
||||
println!("uploading components to {}", calendar.href);
|
||||
let mut upload_calendar = Calendar::new();
|
||||
|
||||
for todo in components {
|
||||
upload_calendar.push(todo);
|
||||
}
|
||||
|
||||
Ok(authorized_client
|
||||
.request(
|
||||
PutResource::new(format!("{}{}.ics", calendar.href, Uuid::new_v4()).as_str())
|
||||
.create(upload_calendar.to_string().leak(), "text/caldav"),
|
||||
)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_components(
|
||||
authorized_client: &AuthorizedCaldavClient,
|
||||
calendar: &FoundCollection,
|
||||
) -> Result<Vec<CalendarComponent>, CalendarError> {
|
||||
use CalendarError::*;
|
||||
|
||||
let mut components: Vec<CalendarComponent> = Vec::new();
|
||||
|
||||
let resources = get_calendar_resources(authorized_client, calendar).await?;
|
||||
for i in resources {
|
||||
let calendar: Calendar = i
|
||||
.content
|
||||
.map_err(UnavailableResourceError)?
|
||||
.data
|
||||
.parse()
|
||||
.map_err(ParsingError)?;
|
||||
components.extend(calendar.components);
|
||||
}
|
||||
|
||||
Ok(components)
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
use crate::caldav::caldav::{AuthorizedCaldavClient, find_calendars, get_caldav_client, get_components, upload_components};
|
||||
use crate::server_handler::{
|
||||
McpServerHandler, McpServerHandlerError, get_additional_property, get_property_as_string,
|
||||
};
|
||||
use http::Uri;
|
||||
use icalendar::{Todo};
|
||||
use libdav::dav::FoundCollection;
|
||||
use rmcp::handler::server::tool::ToolRouter;
|
||||
use rmcp::handler::server::wrapper::Parameters;
|
||||
use rmcp::model::{CallToolResult, ErrorCode, Implementation, ServerCapabilities, ServerInfo};
|
||||
use rmcp::schemars::JsonSchema;
|
||||
use rmcp::{ErrorData, schemars};
|
||||
use rmcp::{ServerHandler, serde_json, tool, tool_handler, tool_router};
|
||||
use serde::{Deserialize};
|
||||
use std::str::FromStr;
|
||||
use toml::Value;
|
||||
use toml::map::Map;
|
||||
use crate::caldav::todo::McpTodo;
|
||||
|
||||
mod caldav;
|
||||
mod todo;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct CalDavHandler {
|
||||
client: AuthorizedCaldavClient,
|
||||
calendar_references: Vec<CalendarReference>,
|
||||
tool_router: ToolRouter<Self>,
|
||||
default_calendar_name: String
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct CalendarReference {
|
||||
friendly_name: String,
|
||||
collection: FoundCollection,
|
||||
}
|
||||
|
||||
impl McpServerHandler for CalDavHandler {
|
||||
async fn new(additional_properties: Map<String, Value>) -> Result<Self, McpServerHandlerError> {
|
||||
use McpServerHandlerError::*;
|
||||
|
||||
let caldav_uri = get_property_as_string("caldav-uri", &additional_properties)?;
|
||||
let caldav_uri = Uri::from_str(caldav_uri).map_err(|_| PropertyParseError {
|
||||
value: caldav_uri.to_string(),
|
||||
})?;
|
||||
|
||||
let username = get_property_as_string("username", &additional_properties)?;
|
||||
let password = get_property_as_string("password", &additional_properties)?;
|
||||
|
||||
let client = get_caldav_client(caldav_uri, username, password);
|
||||
|
||||
let mut calendar_references = Vec::new();
|
||||
|
||||
let configured_calendars = get_additional_property("calendars", &additional_properties)?;
|
||||
let configured_calendars = configured_calendars.as_table().ok_or(WrongPropertyType {
|
||||
value: configured_calendars.to_string(),
|
||||
})?;
|
||||
|
||||
let found_calendars = find_calendars(&client)
|
||||
.await
|
||||
.map_err(|e| InternalError(e.to_string()))?;
|
||||
|
||||
for configured_cal in configured_calendars {
|
||||
let friendly_name = configured_cal.0.to_owned();
|
||||
let href = configured_cal
|
||||
.1
|
||||
.as_str()
|
||||
.ok_or(WrongPropertyType {
|
||||
value: friendly_name.to_string(),
|
||||
})?
|
||||
.to_string();
|
||||
|
||||
let found_cal = found_calendars
|
||||
.iter()
|
||||
.find(|calendar| calendar.href == href)
|
||||
.ok_or(InternalError(format!(
|
||||
"Could not find calendar href `{href}`. Available calendars: {found_calendars:?}"
|
||||
)))?;
|
||||
|
||||
calendar_references.push(CalendarReference {
|
||||
friendly_name,
|
||||
collection: found_cal.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
let default_calendar = get_property_as_string("default-calendar", &additional_properties)?.to_string();
|
||||
|
||||
Ok(Self {
|
||||
client,
|
||||
calendar_references,
|
||||
tool_router: Self::tool_router(),
|
||||
default_calendar_name: default_calendar
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl CalDavHandler {
|
||||
fn get_calendar_by_name(&self, calendar_name: Option<String>) -> Result<FoundCollection, ErrorData> {
|
||||
let calendar_name = calendar_name.unwrap_or(self.default_calendar_name.clone());
|
||||
|
||||
self.calendar_references
|
||||
.iter()
|
||||
.find(|calendar_ref| calendar_ref.friendly_name == calendar_name)
|
||||
.map(|calendar_ref| calendar_ref.collection.clone())
|
||||
.ok_or(ErrorData::new(
|
||||
ErrorCode::INVALID_PARAMS,
|
||||
"could not find calendar",
|
||||
None,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(JsonSchema, Deserialize, Debug)]
|
||||
struct GetTodosParameters {
|
||||
#[schemars(description = "uses default if null")]
|
||||
calendar_name: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(JsonSchema, Deserialize, Debug)]
|
||||
struct AddTodoParameters {
|
||||
#[schemars(description = "uses default if null")]
|
||||
calendar_name: Option<String>,
|
||||
todo: McpTodo
|
||||
}
|
||||
|
||||
#[tool_router]
|
||||
impl CalDavHandler {
|
||||
#[tool(description = "lists available calendars", annotations(read_only_hint = true))]
|
||||
async fn get_calendars(&self) -> Result<CallToolResult, ErrorData> {
|
||||
let names = self
|
||||
.calendar_references
|
||||
.iter()
|
||||
.map(|calendar_reference| calendar_reference.friendly_name.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(CallToolResult::structured(serde_json::value::Value::from(
|
||||
names,
|
||||
)))
|
||||
}
|
||||
|
||||
#[tool(description = "get uncompleted todos", annotations(read_only_hint = true))]
|
||||
async fn get_todos(
|
||||
&self,
|
||||
parameters: Parameters<GetTodosParameters>,
|
||||
) -> Result<CallToolResult, ErrorData> {
|
||||
let calendar = self.get_calendar_by_name(parameters.0.calendar_name)?;
|
||||
|
||||
let components = get_components(&self.client, &calendar).await?;
|
||||
let mut todos: Vec<McpTodo> = Vec::new();
|
||||
|
||||
for component in components {
|
||||
if let Some(todo_component) = component.as_todo() && todo_component.get_completed().is_none() {
|
||||
todos.push(todo_component.clone().try_into()?);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(CallToolResult::structured(serde_json::json!(todos)))
|
||||
}
|
||||
|
||||
#[tool(description = "add a new todo to a calendar")]
|
||||
async fn add_todo(&self, parameters: Parameters<AddTodoParameters>) -> Result<CallToolResult, ErrorData> {
|
||||
let calendar = self.get_calendar_by_name(parameters.0.calendar_name)?;
|
||||
|
||||
let todo: Todo = parameters.0.todo.into();
|
||||
upload_components(&self.client, &calendar, vec![todo]).await?;
|
||||
|
||||
Ok(CallToolResult::success(vec![]))
|
||||
}
|
||||
}
|
||||
|
||||
#[tool_handler]
|
||||
impl ServerHandler for CalDavHandler {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
|
||||
.with_instructions(
|
||||
"enables interaction with calendars".to_string(),
|
||||
)
|
||||
.with_server_info(Implementation::new("caldav", env!("CARGO_PKG_VERSION")))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
use rmcp::schemars;
|
||||
use chrono::{DateTime, NaiveTime, TimeZone};
|
||||
use icalendar::{Component, DatePerhapsTime, EventLike, Todo};
|
||||
use rmcp::ErrorData;
|
||||
use rmcp::model::ErrorCode;
|
||||
use rmcp::schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(JsonSchema, Deserialize, Serialize, Debug, Clone)]
|
||||
pub (in crate::caldav) struct McpTodo {
|
||||
pub (in crate::caldav) summary: Option<String>,
|
||||
pub (in crate::caldav) description: Option<String>,
|
||||
pub (in crate::caldav) start: Option<DateTime<chrono::Local>>,
|
||||
pub (in crate::caldav) due: Option<DateTime<chrono::Local>>,
|
||||
}
|
||||
|
||||
impl McpTodo {
|
||||
fn date_perhaps_time_to_local_dt(
|
||||
date_perhaps_time: Option<DatePerhapsTime>,
|
||||
) -> Result<Option<DateTime<chrono::Local>>, ErrorData> {
|
||||
use DatePerhapsTime;
|
||||
|
||||
if date_perhaps_time.is_none() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
use icalendar::CalendarDateTime::*;
|
||||
let local_datetime = match date_perhaps_time.unwrap() {
|
||||
DatePerhapsTime::DateTime(datetime) => match datetime {
|
||||
Floating(naive_dt) => chrono::Local.from_local_datetime(&naive_dt).single(), // assuming local timezone
|
||||
timezone_dt => timezone_dt
|
||||
.try_into_utc()
|
||||
.map(DateTime::<chrono::Local>::from),
|
||||
},
|
||||
DatePerhapsTime::Date(date) => chrono::Local.from_local_datetime(&date.and_time(NaiveTime::from_hms_opt(0, 0, 0).unwrap())).single(), // assuming local timezone
|
||||
};
|
||||
|
||||
match local_datetime {
|
||||
Some(local_datetime) => Ok(Some(local_datetime)),
|
||||
None => Err(ErrorData::new(ErrorCode::INVALID_PARAMS, "DateTime conversion failed", None)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Todo> for McpTodo {
|
||||
type Error = ErrorData;
|
||||
|
||||
fn try_from(value: Todo) -> Result<Self, Self::Error> {
|
||||
Ok(McpTodo{
|
||||
summary: value.get_summary().map(|s| s.to_string()),
|
||||
description: value.get_description().map(|s| s.to_string()),
|
||||
start: Self::date_perhaps_time_to_local_dt(value.get_start())?,
|
||||
due: Self::date_perhaps_time_to_local_dt(value.get_due())?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<McpTodo> for Todo {
|
||||
fn from(value: McpTodo) -> Self {
|
||||
let mut todo = Todo::new();
|
||||
|
||||
if let Some(summary) = value.summary {
|
||||
todo.summary(summary.as_str());
|
||||
}
|
||||
|
||||
if let Some(description) = value.description {
|
||||
todo.description(description.as_str());
|
||||
}
|
||||
|
||||
if let Some(start) = value.start {
|
||||
todo.starts(start.to_utc());
|
||||
}
|
||||
|
||||
if let Some(due) = value.due {
|
||||
todo.due(due.to_utc());
|
||||
}
|
||||
|
||||
todo
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::McpServiceType;
|
||||
use own_assist_common::config_from_file;
|
||||
use own_assist_common::config_loader::ConfigLoadingError;
|
||||
use crate::McpServiceType;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use toml::map::Map;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub(crate) struct Config {
|
||||
@@ -13,7 +14,9 @@ pub(crate) struct Config {
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub(crate) struct ServerConfig {
|
||||
pub(crate) path: String,
|
||||
pub(crate) r#type: McpServiceType
|
||||
pub(crate) r#type: McpServiceType,
|
||||
#[serde(rename = "additional-properties")]
|
||||
pub(crate) additional_properties: Map<String, toml::Value>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
|
||||
@@ -1,38 +1,56 @@
|
||||
use chrono::{Datelike, Local};
|
||||
use rmcp::model::{Implementation, ServerCapabilities, ServerInfo};
|
||||
use rmcp::{tool, tool_handler, tool_router, ServerHandler};
|
||||
use crate::server_handler::{McpServerHandler, McpServerHandlerError};
|
||||
use chrono::{Datelike, Local, Utc};
|
||||
use rmcp::handler::server::tool::ToolRouter;
|
||||
use crate::McpServerHandler;
|
||||
use rmcp::model::{Implementation, ServerCapabilities, ServerInfo};
|
||||
use rmcp::{ServerHandler, tool, tool_handler, tool_router};
|
||||
use toml::Value;
|
||||
use toml::map::Map;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DateTimeHandler {
|
||||
tool_router: ToolRouter<Self>
|
||||
pub(crate) struct DateTimeHandler {
|
||||
tool_router: ToolRouter<Self>,
|
||||
}
|
||||
|
||||
impl McpServerHandler for DateTimeHandler {
|
||||
fn new() -> Self {
|
||||
DateTimeHandler { tool_router: Self::tool_router() }
|
||||
async fn new(
|
||||
_additional_properties: Map<String, Value>,
|
||||
) -> Result<Self, McpServerHandlerError> {
|
||||
Ok(DateTimeHandler {
|
||||
tool_router: Self::tool_router(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[tool_router]
|
||||
impl DateTimeHandler {
|
||||
#[tool(description = "returns the local datetime in ISO 8601", annotations(read_only_hint = true))]
|
||||
#[tool(
|
||||
description = "returns the local datetime in ISO 8601",
|
||||
annotations(read_only_hint = true)
|
||||
)]
|
||||
fn get_local_datetime() -> String {
|
||||
Local::now().format("%+").to_string()
|
||||
}
|
||||
|
||||
#[tool(description = "returns the utc datetime in ISO 8601", annotations(read_only_hint = true))]
|
||||
#[tool(
|
||||
description = "returns the utc datetime in ISO 8601",
|
||||
annotations(read_only_hint = true)
|
||||
)]
|
||||
fn get_utc_datetime() -> String {
|
||||
Local::now().format("%+").to_string()
|
||||
Utc::now().format("%+").to_string()
|
||||
}
|
||||
|
||||
#[tool(description = "return current week number in year", annotations(read_only_hint = true))]
|
||||
#[tool(
|
||||
description = "return current week number in year",
|
||||
annotations(read_only_hint = true)
|
||||
)]
|
||||
fn get_week() -> String {
|
||||
Local::now().iso_week().week().to_string()
|
||||
}
|
||||
|
||||
#[tool(description = "return current weekday", annotations(read_only_hint = true))]
|
||||
#[tool(
|
||||
description = "return current weekday",
|
||||
annotations(read_only_hint = true)
|
||||
)]
|
||||
fn get_weekday() -> String {
|
||||
Local::now().weekday().to_string()
|
||||
}
|
||||
@@ -42,6 +60,10 @@ impl DateTimeHandler {
|
||||
impl ServerHandler for DateTimeHandler {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
|
||||
.with_instructions("A simple clock/datetime provider. It is read only and thus safe to use.".to_string()).with_server_info(Implementation::new("datetime", env!("CARGO_PKG_VERSION")))
|
||||
.with_instructions(
|
||||
"A simple clock/datetime provider. It is read only and thus safe to use."
|
||||
.to_string(),
|
||||
)
|
||||
.with_server_info(Implementation::new("datetime", env!("CARGO_PKG_VERSION")))
|
||||
}
|
||||
}
|
||||
@@ -1,51 +1,57 @@
|
||||
mod config;
|
||||
#[cfg(feature = "datetime")]
|
||||
pub mod datetime;
|
||||
mod config;
|
||||
|
||||
use axum::response::Json;
|
||||
#[cfg(feature = "caldav")]
|
||||
pub mod caldav;
|
||||
mod server_handler;
|
||||
|
||||
use crate::caldav::CalDavHandler;
|
||||
use crate::config::{Config, ServerConfig};
|
||||
#[cfg(feature = "datetime")]
|
||||
use crate::datetime::DateTimeHandler;
|
||||
use crate::server_handler::{McpServerHandler, McpServerHandlerError};
|
||||
use axum::Router;
|
||||
use rmcp::transport::{
|
||||
StreamableHttpServerConfig,
|
||||
streamable_http_server::{session::local::LocalSessionManager, tower::StreamableHttpService},
|
||||
};
|
||||
use axum::response::Json;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::main;
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
use crate::config::{Config, ServerConfig};
|
||||
#[cfg(feature = "datetime")]
|
||||
use crate::datetime::DateTimeHandler;
|
||||
|
||||
pub trait McpServerHandler: rmcp::ServerHandler {
|
||||
fn new() -> Self;
|
||||
|
||||
fn mcp_service() -> StreamableHttpService<Self, LocalSessionManager> {
|
||||
StreamableHttpService::new(|| Ok(Self::new()), LocalSessionManager::default().into(), StreamableHttpServerConfig::default())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
#[non_exhaustive]
|
||||
pub enum McpServiceType {
|
||||
DateTime
|
||||
DateTime,
|
||||
CalDav,
|
||||
}
|
||||
|
||||
pub(crate) fn mcp_router(server_configs: &Vec<ServerConfig>) -> Router {
|
||||
pub(crate) async fn mcp_router(
|
||||
server_configs: &Vec<ServerConfig>,
|
||||
) -> Result<Router, McpServerHandlerError> {
|
||||
let mut router = Router::new();
|
||||
|
||||
for config in server_configs {
|
||||
router = match config.r#type {
|
||||
McpServiceType::DateTime => {
|
||||
router.route_service(config.path.as_str(), DateTimeHandler::mcp_service())
|
||||
}
|
||||
McpServiceType::DateTime => router.route_service(
|
||||
config.path.as_str(),
|
||||
DateTimeHandler::mcp_service(config.additional_properties.clone()).await?,
|
||||
),
|
||||
McpServiceType::CalDav => router.route_service(
|
||||
config.path.as_str(),
|
||||
CalDavHandler::mcp_service(config.additional_properties.clone()).await?,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
router
|
||||
Ok(router)
|
||||
}
|
||||
|
||||
fn bind_address_format(url: url::Url) -> String {
|
||||
format!("{}:{}", url.host_str().expect("No host specified"), url.port().unwrap_or(8000))
|
||||
format!(
|
||||
"{}:{}",
|
||||
url.host_str().expect("No host specified"),
|
||||
url.port().unwrap_or(8000)
|
||||
)
|
||||
}
|
||||
|
||||
#[main]
|
||||
@@ -58,22 +64,43 @@ async fn main() {
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.init();
|
||||
|
||||
let config = Config::from_file().inspect_err(|e|{
|
||||
eprintln!("Error loading config: {e}");
|
||||
std::process::exit(1);
|
||||
}).unwrap();
|
||||
let config = Config::from_file()
|
||||
.inspect_err(|e| {
|
||||
eprintln!("Error loading config: {e}");
|
||||
std::process::exit(1);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
println!("config: {config:#?}");
|
||||
let routes = Json(
|
||||
config
|
||||
.servers
|
||||
.clone()
|
||||
.iter()
|
||||
.map(|server| server.path.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
|
||||
let router = mcp_router(&config.servers).route("/", axum::routing::get(|| async { Json(config.servers) }));
|
||||
let router = mcp_router(&config.servers)
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
eprintln!("Error loading config: {e}");
|
||||
std::process::exit(1);
|
||||
})
|
||||
.unwrap()
|
||||
.route("/", axum::routing::get(|| async { routes }));
|
||||
|
||||
let bind_address = config.bind_address.unwrap_or(url::Url::parse("http://localhost:8000").unwrap());
|
||||
let bind_address = config
|
||||
.bind_address
|
||||
.unwrap_or(url::Url::parse("http://localhost:8000").unwrap());
|
||||
println!("binding address at {bind_address}");
|
||||
|
||||
let tcp_listener = tokio::net::TcpListener::bind(bind_address_format(bind_address)).await.inspect_err(|e|{
|
||||
eprintln!("Error bind tcp listener: {e:#?}");
|
||||
std::process::exit(1);
|
||||
}).unwrap();
|
||||
let tcp_listener = tokio::net::TcpListener::bind(bind_address_format(bind_address))
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
eprintln!("Error bind tcp listener: {e:#?}");
|
||||
std::process::exit(1);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let ct = tokio_util::sync::CancellationToken::new();
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
use rmcp::transport::streamable_http_server::session::local::LocalSessionManager;
|
||||
use rmcp::transport::{StreamableHttpServerConfig, StreamableHttpService};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum McpServerHandlerError {
|
||||
#[error("Could not find property `{name}`")]
|
||||
PropertyMissingError { name: String },
|
||||
#[error("Property is of wrong type: `{value}` could not be parsed")]
|
||||
WrongPropertyType { value: String },
|
||||
#[error("Could not parse property `{value}`")]
|
||||
PropertyParseError { value: String },
|
||||
#[error("internal error: {0}")]
|
||||
InternalError(String),
|
||||
}
|
||||
|
||||
pub(crate) trait McpServerHandler: rmcp::ServerHandler + Clone {
|
||||
fn new(
|
||||
additional_properties: toml::map::Map<String, toml::Value>,
|
||||
) -> impl Future<Output = Result<Self, McpServerHandlerError>>;
|
||||
|
||||
async fn mcp_service(
|
||||
additional_properties: toml::map::Map<String, toml::Value>,
|
||||
) -> Result<StreamableHttpService<Self, LocalSessionManager>, McpServerHandlerError> {
|
||||
let handler = Self::new(additional_properties).await?;
|
||||
|
||||
Ok(StreamableHttpService::new(
|
||||
move || Ok(handler.clone()),
|
||||
LocalSessionManager::default().into(),
|
||||
StreamableHttpServerConfig::default(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_additional_property<'a>(
|
||||
key: &'a str,
|
||||
additional_properties: &'a toml::map::Map<String, toml::Value>,
|
||||
) -> Result<&'a toml::Value, McpServerHandlerError> {
|
||||
additional_properties.get::<str>(key.as_ref()).ok_or(
|
||||
McpServerHandlerError::PropertyMissingError {
|
||||
name: key.to_string(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn get_property_as_string<'a>(
|
||||
key: &'a str,
|
||||
additional_properties: &'a toml::map::Map<String, toml::Value>,
|
||||
) -> Result<&'a str, McpServerHandlerError> {
|
||||
get_additional_property(key, additional_properties)?
|
||||
.as_str()
|
||||
.ok_or(McpServerHandlerError::PropertyMissingError {
|
||||
name: key.to_string(),
|
||||
})
|
||||
}
|
||||
+16
-11
@@ -221,19 +221,24 @@ impl AgentChat {
|
||||
log::debug!("calling tool {}", tool_call.function.name);
|
||||
let result = self.call_tool(tool_call, &mut permission_request_callback).await?;
|
||||
|
||||
let contents = match result {
|
||||
Some(result) => result.content
|
||||
.iter()
|
||||
.filter_map(|content| content.as_text())
|
||||
.map(|text_content| text_content.text.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
None => vec![String::from("Tool Permission Denied by the user")],
|
||||
};
|
||||
// serialize structured content if it exists
|
||||
if let Some(structured_content) = result.clone().and_then(|result|result.structured_content) {
|
||||
self.message_history.push(ChatMessage::tool(structured_content.to_string()));
|
||||
} else {
|
||||
let contents = match result {
|
||||
Some(result) => result.content
|
||||
.iter()
|
||||
.filter_map(|content| content.as_text())
|
||||
.map(|text_content| text_content.text.clone())
|
||||
.collect::<Vec<_>>(),
|
||||
None => vec![String::from("Tool Permission Denied by the user")],
|
||||
};
|
||||
|
||||
log::debug!("contents: {:#?}", contents);
|
||||
log::debug!("contents: {:#?}", contents);
|
||||
|
||||
self.message_history
|
||||
.push(ChatMessage::tool(contents.join("\n")));
|
||||
self.message_history
|
||||
.push(ChatMessage::tool(contents.join("\n")));
|
||||
}
|
||||
}
|
||||
|
||||
response = self
|
||||
|
||||
@@ -81,12 +81,6 @@ async fn main() {
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
/*let audio_client = config.audio_client();
|
||||
log::debug!("Audio server status {:?}", audio_client.status().await.inspect_err(|e|{
|
||||
log::error!("audio server error {}", e);
|
||||
std::process::exit(1);
|
||||
}).unwrap());*/
|
||||
|
||||
let ollama = config.ollama_instance();
|
||||
let model_name = &config.ollama_config().model.name;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user