add caldav mcp server

refactoring
This commit is contained in:
milan
2026-05-03 18:28:20 +02:00
parent 20a5b01311
commit 3dd897c422
14 changed files with 1342 additions and 97 deletions
+2 -1
View File
@@ -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
+5 -3
View File
@@ -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)?)
}
+731 -13
View File
File diff suppressed because it is too large Load Diff
+15 -5
View File
@@ -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"]
+149
View File
@@ -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)
}
+179
View File
@@ -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")))
}
}
+80
View File
@@ -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
}
}
+6 -3
View File
@@ -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 {
+36 -14
View File
@@ -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")))
}
}
+58 -31
View File
@@ -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|{
let config = Config::from_file()
.inspect_err(|e| {
eprintln!("Error loading config: {e}");
std::process::exit(1);
}).unwrap();
})
.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|{
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();
})
.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(),
})
}
+5
View File
@@ -221,6 +221,10 @@ impl AgentChat {
log::debug!("calling tool {}", tool_call.function.name);
let result = self.call_tool(tool_call, &mut permission_request_callback).await?;
// 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()
@@ -235,6 +239,7 @@ impl AgentChat {
self.message_history
.push(ChatMessage::tool(contents.join("\n")));
}
}
response = self
.ollama_client
-6
View File
@@ -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;