use crate::primitives::Address;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ToolTransportMode {
Mcp,
McpStdio,
McpSse,
Api,
Native,
}
impl ToolTransportMode {
pub fn as_str(&self) -> &'static str {
match self {
ToolTransportMode::Mcp => "mcp",
ToolTransportMode::McpStdio => "mcp-stdio",
ToolTransportMode::McpSse => "mcp-sse",
ToolTransportMode::Api => "api",
ToolTransportMode::Native => "native",
}
}
pub fn parse_str(s: &str) -> Option<Self> {
match s {
"mcp" => Some(ToolTransportMode::Mcp),
"mcp-stdio" => Some(ToolTransportMode::McpStdio),
"mcp-sse" => Some(ToolTransportMode::McpSse),
"api" => Some(ToolTransportMode::Api),
"native" => Some(ToolTransportMode::Native),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum UpstreamAuth {
Bearer {
sealed_secret_ref: String,
},
Header {
header_name: String,
sealed_secret_ref: String,
},
EnvVar {
env_var_name: String,
sealed_secret_ref: String,
},
QueryParam {
param_name: String,
sealed_secret_ref: String,
},
}
impl UpstreamAuth {
pub fn sealed_secret_ref(&self) -> &str {
match self {
UpstreamAuth::Bearer { sealed_secret_ref }
| UpstreamAuth::Header {
sealed_secret_ref, ..
}
| UpstreamAuth::EnvVar {
sealed_secret_ref, ..
}
| UpstreamAuth::QueryParam {
sealed_secret_ref, ..
} => sealed_secret_ref,
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct StdioSpawnSpec {
pub command: String,
pub args: Vec<String>,
pub working_dir: Option<String>,
pub env: std::collections::BTreeMap<String, String>,
pub timeout_secs: Option<u64>,
#[serde(default = "default_persistent")]
pub persistent: bool,
}
fn default_persistent() -> bool {
true
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[derive(Default)]
pub enum ToolStatus {
#[default]
Active,
Inactive,
Deprecated,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolDefinition {
pub tool_id: String,
pub name: String,
pub version: String,
pub tool_type: String,
pub endpoint: String,
pub description: String,
pub capabilities: Vec<String>,
pub category: String,
pub creator_did: Option<String>,
pub creator_wallet: Option<Address>,
pub price_per_call: u128,
pub status: ToolStatus,
pub created_at: u64,
pub invocation_count: u64,
#[serde(default = "default_last_seen")]
pub last_seen_at: u64,
#[serde(default)]
pub upstream_auth: Option<UpstreamAuth>,
#[serde(default)]
pub spawn_spec: Option<StdioSpawnSpec>,
#[serde(default)]
pub allowed_to_subjects: Option<Vec<String>>,
}
fn default_last_seen() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
impl ToolDefinition {
pub fn new(
name: String,
version: String,
tool_type: String,
endpoint: String,
description: String,
category: String,
) -> Self {
let tool_id = uuid::Uuid::new_v4().to_string();
let created_at = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
Self {
tool_id,
name,
version,
tool_type,
endpoint,
description,
capabilities: Vec::new(),
category,
creator_did: None,
creator_wallet: None,
price_per_call: 0,
status: ToolStatus::Active,
created_at,
invocation_count: 0,
last_seen_at: created_at,
upstream_auth: None,
spawn_spec: None,
allowed_to_subjects: None,
}
}
pub fn transport_mode(&self) -> Option<ToolTransportMode> {
ToolTransportMode::parse_str(&self.tool_type)
}
pub fn set_transport_mode(&mut self, mode: ToolTransportMode) {
self.tool_type = mode.as_str().to_string();
}
pub fn is_subject_allowed(&self, subject: Option<&str>) -> bool {
match (&self.allowed_to_subjects, subject) {
(None, _) => true,
(Some(list), Some(s)) => list.iter().any(|x| x == s),
(Some(_), None) => false,
}
}
pub fn is_paid(&self) -> bool {
self.price_per_call > 0
}
pub fn validate_for_registration(&self) -> Result<(), &'static str> {
if self.is_paid() && self.creator_wallet.is_none() {
return Err("Paid tool (price_per_call > 0) requires a creator_wallet");
}
Ok(())
}
pub fn is_available(&self) -> bool {
self.status == ToolStatus::Active
}
pub fn touch(&mut self) {
self.last_seen_at = default_last_seen();
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ToolFilter {
pub tool_type: Option<String>,
pub category: Option<String>,
pub status: Option<String>,
pub creator_did: Option<String>,
pub query: Option<String>,
pub limit: Option<usize>,
pub offset: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolInvocationResult {
pub tool_id: String,
pub invocation_id: String,
pub output: serde_json::Value,
pub settlement_tx: Option<String>,
pub amount_paid: u128,
pub completed_at: u64,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tool_definition_new() {
let tool = ToolDefinition::new(
"web-search".to_string(),
"1.0.0".to_string(),
"mcp".to_string(),
"http://localhost:3001/mcp".to_string(),
"MCP server providing web search capability".to_string(),
"search".to_string(),
);
assert!(!tool.tool_id.is_empty());
assert_eq!(tool.name, "web-search");
assert_eq!(tool.version, "1.0.0");
assert_eq!(tool.tool_type, "mcp");
assert!(tool.is_available());
assert_eq!(tool.price_per_call, 0);
assert!(!tool.is_paid());
assert!(tool.creator_wallet.is_none());
assert_eq!(tool.status, ToolStatus::Active);
assert_eq!(tool.invocation_count, 0);
}
#[test]
fn paid_tool_without_wallet_fails_validation() {
let mut tool = ToolDefinition::new(
"premium-mcp".to_string(),
"1.0.0".to_string(),
"mcp".to_string(),
"https://example.com/mcp".to_string(),
"Paid MCP server".to_string(),
"data".to_string(),
);
tool.price_per_call = 1_000;
assert!(tool.validate_for_registration().is_err());
}
#[test]
fn free_tool_without_wallet_passes_validation() {
let tool = ToolDefinition::new(
"free-mcp".to_string(),
"1.0.0".to_string(),
"mcp".to_string(),
"https://example.com/mcp".to_string(),
"Free MCP server".to_string(),
"data".to_string(),
);
assert!(tool.validate_for_registration().is_ok());
}
#[test]
fn paid_tool_with_wallet_passes_validation() {
let mut tool = ToolDefinition::new(
"paid-mcp".to_string(),
"1.0.0".to_string(),
"mcp".to_string(),
"https://example.com/mcp".to_string(),
"Paid MCP server".to_string(),
"data".to_string(),
);
tool.price_per_call = 1_000;
tool.creator_wallet = Some(Address::default());
assert!(tool.validate_for_registration().is_ok());
}
#[test]
fn test_tool_status_default() {
assert_eq!(ToolStatus::default(), ToolStatus::Active);
}
#[test]
fn test_tool_filter_default() {
let filter = ToolFilter::default();
assert!(filter.tool_type.is_none());
assert!(filter.category.is_none());
assert!(filter.status.is_none());
assert!(filter.query.is_none());
}
#[test]
fn test_tool_serialization() {
let mut tool = ToolDefinition::new(
"code-executor".to_string(),
"2.0.0".to_string(),
"mcp".to_string(),
"https://tools.tenzro.network/code-executor/mcp".to_string(),
"Executes code in sandboxed environments".to_string(),
"code".to_string(),
);
tool.capabilities = vec!["python".to_string(), "javascript".to_string()];
tool.creator_did = Some("did:tenzro:human:test-123".to_string());
let json = serde_json::to_string(&tool).unwrap();
let deserialized: ToolDefinition = serde_json::from_str(&json).unwrap();
assert_eq!(tool.tool_id, deserialized.tool_id);
assert_eq!(tool.name, deserialized.name);
assert_eq!(tool.capabilities.len(), 2);
}
}