use rmcp::{
ErrorData as McpError,
handler::server::wrapper::Parameters,
model::{CallToolResult, ContentBlock},
};
use rudof_lib::Rudof;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::helpers::*;
use crate::service::mcp_service::RudofMcpService;
const RESET_TARGETS: &[&str] = &[
"data",
"shex",
"shex-validation",
"shacl",
"shacl-validation",
"pgschema",
"pgschema-validation",
"shapemap",
"dctap",
"service",
"query",
"sparql",
"typemap",
"rdf-config",
];
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ResetSessionStateRequest {
pub targets: Option<Vec<String>>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ResetSessionStateResponse {
pub reset: Vec<String>,
}
pub async fn reset_session_state_impl(
service: &RudofMcpService,
params: Parameters<ResetSessionStateRequest>,
) -> Result<CallToolResult, McpError> {
let Parameters(ResetSessionStateRequest { targets }) = params;
let requested = targets.unwrap_or_default();
let reset_all = requested.is_empty() || requested.iter().any(|t| t == "all");
if !reset_all {
let unknown: Vec<&str> = requested
.iter()
.map(String::as_str)
.filter(|target| !RESET_TARGETS.contains(target))
.collect();
if !unknown.is_empty() {
return Ok(ToolExecutionError::with_hint(
format!("Unknown reset target(s): {}", unknown.join(", ")),
format!("Valid targets: {}, or 'all'", RESET_TARGETS.join(", ")),
)
.into_call_tool_result());
}
}
let reset_list: Vec<String> = if reset_all {
RESET_TARGETS.iter().map(|s| s.to_string()).collect()
} else {
requested
};
let data_touched = reset_all || reset_list.iter().any(|t| t == "data");
{
let mut rudof = service.rudof.lock().await;
if reset_all {
rudof.reset_all().execute();
} else {
for target in &reset_list {
reset_target(&mut rudof, target);
}
}
}
if data_touched && let Err(e) = service.persist_state().await {
tracing::warn!("Failed to persist state after reset: {}", e);
}
let response = ResetSessionStateResponse {
reset: reset_list.clone(),
};
let structured = serialize_structured(&response, "reset_session_state_impl")?;
let summary = format!("Reset: {}.", reset_list.join(", "));
let mut result = CallToolResult::success(vec![ContentBlock::text(summary)]);
result.structured_content = Some(structured);
Ok(result)
}
fn reset_target(rudof: &mut Rudof, target: &str) {
match target {
"data" => rudof.reset_data().execute(),
"shex" => rudof.reset_shex_schema().execute(),
"shex-validation" => rudof.reset_shex().execute(),
"shacl" => rudof.reset_shacl_shapes().execute(),
"shacl-validation" => rudof.reset_shacl().execute(),
"pgschema" => rudof.reset_pg_schema().execute(),
"pgschema-validation" => rudof.reset_pg_schema_validation().execute(),
"shapemap" => rudof.reset_shapemap().execute(),
"dctap" => rudof.reset_dctap().execute(),
"service" => rudof.reset_service_description().execute(),
"query" => {
rudof.reset_sparql_query().execute();
rudof.reset_query_results().execute();
},
"sparql" => rudof.reset_sparql_query().execute(),
"typemap" => rudof.reset_typemap().execute(),
"rdf-config" => rudof.reset_rdf_config().execute(),
_ => unreachable!("target validated against RESET_TARGETS by reset_session_state_impl"),
}
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ChangeDirectoryRequest {
pub path: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
pub struct ChangeDirectoryResponse {
pub cwd: String,
}
pub async fn change_directory_impl(
service: &RudofMcpService,
params: Parameters<ChangeDirectoryRequest>,
) -> Result<CallToolResult, McpError> {
let Parameters(ChangeDirectoryRequest { path }) = params;
let mut session_dir = service.session_dir.write().await;
if let Some(path) = path {
let candidate = session_dir.join(&path);
let canonical = match candidate.canonicalize() {
Ok(p) => p,
Err(e) => {
return Ok(ToolExecutionError::with_hint(
format!("Failed to change directory to '{}': {}", path, e),
"Provide a path to an existing directory, absolute or relative to the current session directory",
)
.into_call_tool_result());
},
};
if !canonical.is_dir() {
return Ok(ToolExecutionError::with_hint(
format!("'{}' is not a directory", path),
"Provide a path to an existing directory",
)
.into_call_tool_result());
}
*session_dir = canonical;
}
let response = ChangeDirectoryResponse {
cwd: session_dir.display().to_string(),
};
let structured = serialize_structured(&response, "change_directory_impl")?;
let summary = format!("Session directory: {}", response.cwd);
let mut result = CallToolResult::success(vec![ContentBlock::text(summary)]);
result.structured_content = Some(structured);
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
use rudof_lib::formats::{DataFormat, InputSpec as RudofInputSpec};
async fn load_one_triple(service: &RudofMcpService) {
let mut rudof = service.rudof.lock().await;
rudof
.load_data()
.with_data(&[RudofInputSpec::Str(
"<http://a.example/s> <http://a.example/p> <http://a.example/o> .".to_string(),
)])
.with_data_format(&DataFormat::NTriples)
.execute()
.expect("loading inline test data should succeed");
}
async fn is_data_empty(service: &RudofMcpService) -> bool {
let mut rudof = service.rudof.lock().await;
let mut buf = Vec::new();
match rudof.serialize_data(&mut buf).execute() {
Ok(()) => buf.is_empty(),
Err(_) => true,
}
}
#[tokio::test]
async fn reset_all_clears_data() {
let service = RudofMcpService::new();
load_one_triple(&service).await;
assert!(!is_data_empty(&service).await);
let result = reset_session_state_impl(&service, Parameters(ResetSessionStateRequest { targets: None }))
.await
.expect("reset should not be a protocol error");
assert_ne!(result.is_error, Some(true));
assert!(is_data_empty(&service).await);
}
#[tokio::test]
async fn reset_specific_target_clears_only_that_state() {
let service = RudofMcpService::new();
load_one_triple(&service).await;
let result = reset_session_state_impl(
&service,
Parameters(ResetSessionStateRequest {
targets: Some(vec!["shex".to_string()]),
}),
)
.await
.expect("reset should not be a protocol error");
assert_ne!(result.is_error, Some(true));
assert!(!is_data_empty(&service).await);
}
#[tokio::test]
async fn reset_unknown_target_is_a_tool_error() {
let service = RudofMcpService::new();
let result = reset_session_state_impl(
&service,
Parameters(ResetSessionStateRequest {
targets: Some(vec!["not-a-real-target".to_string()]),
}),
)
.await
.expect("invalid target should be a tool error, not a protocol error");
assert_eq!(result.is_error, Some(true));
}
#[tokio::test]
async fn change_directory_with_no_path_reports_current_dir() {
let service = RudofMcpService::new();
let before = service.session_dir.read().await.clone();
let result = change_directory_impl(&service, Parameters(ChangeDirectoryRequest { path: None }))
.await
.expect("querying cwd should not be a protocol error");
assert_ne!(result.is_error, Some(true));
assert_eq!(*service.session_dir.read().await, before);
}
#[tokio::test]
async fn change_directory_updates_session_dir_not_process_cwd() {
let service = RudofMcpService::new();
let tmp_dir = std::env::temp_dir().canonicalize().expect("temp dir should exist");
let process_cwd_before = std::env::current_dir().unwrap();
let result = change_directory_impl(
&service,
Parameters(ChangeDirectoryRequest {
path: Some(tmp_dir.display().to_string()),
}),
)
.await
.expect("changing to an existing directory should not be a protocol error");
assert_ne!(result.is_error, Some(true));
assert_eq!(*service.session_dir.read().await, tmp_dir);
assert_eq!(std::env::current_dir().unwrap(), process_cwd_before);
}
#[tokio::test]
async fn change_directory_rejects_nonexistent_path() {
let service = RudofMcpService::new();
let result = change_directory_impl(
&service,
Parameters(ChangeDirectoryRequest {
path: Some("/no/such/directory/hopefully".to_string()),
}),
)
.await
.expect("a bad path should be a tool error, not a protocol error");
assert_eq!(result.is_error, Some(true));
}
#[tokio::test]
async fn change_directory_rejects_a_file_path() {
let service = RudofMcpService::new();
let file = tempfile::NamedTempFile::new().expect("should create temp file");
let result = change_directory_impl(
&service,
Parameters(ChangeDirectoryRequest {
path: Some(file.path().display().to_string()),
}),
)
.await
.expect("a file path should be a tool error, not a protocol error");
assert_eq!(result.is_error, Some(true));
}
}