use serde::Deserialize;
use crate::error::NikaError;
#[derive(Debug, Clone, Deserialize)]
pub struct InvokeParams {
#[serde(alias = "server", default)]
pub mcp: Option<String>,
#[serde(default)]
pub tool: Option<String>,
#[serde(default)]
pub params: Option<serde_json::Value>,
#[serde(default)]
pub resource: Option<String>,
#[serde(default)]
pub timeout: Option<u64>,
}
impl InvokeParams {
#[inline]
pub fn is_builtin_tool(&self) -> bool {
self.tool.as_ref().is_some_and(|t| t.starts_with("nika:"))
}
}
impl InvokeParams {
pub fn validate(&self) -> Result<(), NikaError> {
if !self.is_builtin_tool() {
match &self.mcp {
None => {
return Err(NikaError::ValidationError {
reason: "'mcp' server name is required for non-builtin tools".into(),
})
}
Some(mcp) if mcp.trim().is_empty() => {
return Err(NikaError::ValidationError {
reason: "'mcp' server name cannot be empty".into(),
});
}
_ => {}
}
}
match (&self.tool, &self.resource) {
(Some(tool), Some(_)) if !tool.trim().is_empty() => Err(NikaError::ValidationError {
reason: "'tool' and 'resource' are mutually exclusive - specify only one".into(),
}),
(Some(tool), None) if tool.trim().is_empty() => Err(NikaError::ValidationError {
reason: "'tool' name cannot be empty".into(),
}),
(None, Some(resource)) if resource.trim().is_empty() => {
Err(NikaError::ValidationError {
reason: "'resource' URI cannot be empty".into(),
})
}
(Some(_), Some(_)) => Err(NikaError::ValidationError {
reason: "'tool' and 'resource' are mutually exclusive - specify only one".into(),
}),
(None, None) => Err(NikaError::ValidationError {
reason: "either 'tool' or 'resource' must be specified".into(),
}),
_ => Ok(()),
}
}
#[inline]
pub fn is_tool_call(&self) -> bool {
self.tool.is_some()
}
#[inline]
pub fn is_resource_read(&self) -> bool {
self.resource.is_some()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::serde_yaml;
use serde_json::json;
#[test]
fn parse_tool_call() {
let yaml = r#"
mcp: novanet
tool: novanet_context
params:
entity: qr-code
"#;
let params: InvokeParams = serde_yaml::from_str(yaml).unwrap();
assert_eq!(params.mcp, Some("novanet".to_string()));
assert_eq!(params.tool, Some("novanet_context".to_string()));
assert_eq!(params.params, Some(json!({"entity": "qr-code"})));
assert!(params.resource.is_none());
}
#[test]
fn parse_resource_read() {
let yaml = r#"
mcp: novanet
resource: entity://qr-code/fr-FR
"#;
let params: InvokeParams = serde_yaml::from_str(yaml).unwrap();
assert_eq!(params.mcp, Some("novanet".to_string()));
assert!(params.tool.is_none());
assert_eq!(params.resource, Some("entity://qr-code/fr-FR".to_string()));
}
#[test]
fn validate_ok_tool() {
let params = InvokeParams {
mcp: Some("test".to_string()),
tool: Some("test_tool".to_string()),
params: None,
resource: None,
timeout: None,
};
assert!(params.validate().is_ok());
assert!(params.is_tool_call());
assert!(!params.is_resource_read());
}
#[test]
fn validate_ok_resource() {
let params = InvokeParams {
mcp: Some("test".to_string()),
tool: None,
params: None,
resource: Some("test://resource".to_string()),
timeout: None,
};
assert!(params.validate().is_ok());
assert!(!params.is_tool_call());
assert!(params.is_resource_read());
}
#[test]
fn validate_err_both() {
let params = InvokeParams {
mcp: Some("test".to_string()),
tool: Some("test_tool".to_string()),
params: None,
resource: Some("test://resource".to_string()),
timeout: None,
};
let result = params.validate();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("mutually exclusive"));
}
#[test]
fn validate_err_neither() {
let params = InvokeParams {
mcp: Some("test".to_string()),
tool: None,
params: None,
resource: None,
timeout: None,
};
let result = params.validate();
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("must be specified"));
}
#[test]
fn validate_err_empty_mcp() {
let params = InvokeParams {
mcp: Some("".to_string()),
tool: Some("test_tool".to_string()),
params: None,
resource: None,
timeout: None,
};
let result = params.validate();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("mcp"));
}
#[test]
fn validate_err_whitespace_mcp() {
let params = InvokeParams {
mcp: Some(" ".to_string()),
tool: Some("test_tool".to_string()),
params: None,
resource: None,
timeout: None,
};
let result = params.validate();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("mcp"));
}
#[test]
fn validate_err_empty_tool() {
let params = InvokeParams {
mcp: Some("test".to_string()),
tool: Some("".to_string()),
params: None,
resource: None,
timeout: None,
};
let result = params.validate();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("tool"));
}
#[test]
fn validate_err_whitespace_tool() {
let params = InvokeParams {
mcp: Some("test".to_string()),
tool: Some(" \t ".to_string()),
params: None,
resource: None,
timeout: None,
};
let result = params.validate();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("tool"));
}
#[test]
fn validate_err_empty_resource() {
let params = InvokeParams {
mcp: Some("test".to_string()),
tool: None,
params: None,
resource: Some("".to_string()),
timeout: None,
};
let result = params.validate();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("resource"));
}
#[test]
fn validate_err_whitespace_resource() {
let params = InvokeParams {
mcp: Some("test".to_string()),
tool: None,
params: None,
resource: Some(" ".to_string()),
timeout: None,
};
let result = params.validate();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("resource"));
}
#[test]
fn validate_ok_builtin_tool_without_mcp() {
let params = InvokeParams {
mcp: None,
tool: Some("nika:sleep".to_string()),
params: Some(json!({"duration": "1s"})),
resource: None,
timeout: None,
};
assert!(params.validate().is_ok());
assert!(params.is_builtin_tool());
}
#[test]
fn validate_ok_builtin_tool_with_mcp() {
let params = InvokeParams {
mcp: Some("ignored".to_string()),
tool: Some("nika:log".to_string()),
params: Some(json!({"level": "info", "message": "test"})),
resource: None,
timeout: None,
};
assert!(params.validate().is_ok());
assert!(params.is_builtin_tool());
}
#[test]
fn validate_err_non_builtin_without_mcp() {
let params = InvokeParams {
mcp: None,
tool: Some("novanet_context".to_string()),
params: None,
resource: None,
timeout: None,
};
let result = params.validate();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("mcp"));
}
#[test]
fn is_builtin_tool_detects_nika_prefix() {
let params = InvokeParams {
mcp: None,
tool: Some("nika:sleep".to_string()),
params: None,
resource: None,
timeout: None,
};
assert!(params.is_builtin_tool());
}
#[test]
fn is_builtin_tool_rejects_non_nika() {
let params = InvokeParams {
mcp: Some("test".to_string()),
tool: Some("novanet_context".to_string()),
params: None,
resource: None,
timeout: None,
};
assert!(!params.is_builtin_tool());
}
#[test]
fn parse_builtin_tool_without_mcp() {
let yaml = r#"
tool: nika:sleep
params:
duration: "1s"
"#;
let params: InvokeParams = serde_yaml::from_str(yaml).unwrap();
assert!(params.mcp.is_none());
assert_eq!(params.tool, Some("nika:sleep".to_string()));
assert!(params.validate().is_ok());
}
#[test]
fn parse_tool_call_with_timeout() {
let yaml = r#"
mcp: novanet
tool: novanet_context
timeout: 60
params:
entity: qr-code
"#;
let params: InvokeParams = serde_yaml::from_str(yaml).unwrap();
assert_eq!(params.timeout, Some(60));
assert!(params.validate().is_ok());
}
#[test]
fn parse_tool_call_without_timeout() {
let yaml = r#"
mcp: novanet
tool: novanet_context
"#;
let params: InvokeParams = serde_yaml::from_str(yaml).unwrap();
assert_eq!(params.timeout, None);
}
}