use crate::extraction::{
CompiledExtractionRules, ExtractionError, ExtractionSource, RequestContext,
};
use crate::gateway_config::{ExtractionResult, ToolConfig};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[cfg(not(target_arch = "wasm32"))]
use std::path::Path;
const MAX_TOOLS_COUNT: usize = 200;
const MAX_CONSTRAINT_COUNT_PER_TOOL: usize = crate::wire::MAX_CONSTRAINTS_PER_TOOL;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct McpConfig {
pub version: String,
#[serde(default)]
pub settings: McpSettings,
pub tools: HashMap<String, ToolConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct McpSettings {
#[serde(default)]
pub trusted_issuers: Vec<String>,
}
pub struct CompiledMcpConfig {
pub settings: McpSettings,
pub tools: HashMap<String, CompiledTool>,
}
pub struct CompiledTool {
pub config: ToolConfig,
pub extraction_rules: CompiledExtractionRules,
}
impl McpConfig {
#[cfg(not(target_arch = "wasm32"))]
pub fn from_file(path: impl AsRef<Path>) -> Result<Self, crate::gateway_config::ConfigError> {
let content = std::fs::read_to_string(path.as_ref()).map_err(|e| {
crate::gateway_config::ConfigError::FileRead(path.as_ref().display().to_string(), e)
})?;
serde_yaml::from_str(&content).map_err(crate::gateway_config::ConfigError::YamlParse)
}
}
impl CompiledMcpConfig {
pub fn compile(config: McpConfig) -> crate::error::Result<Self> {
if config.tools.len() > MAX_TOOLS_COUNT {
return Err(crate::error::Error::ConfigurationError(format!(
"Too many tools: {} (maximum {})",
config.tools.len(),
MAX_TOOLS_COUNT
)));
}
let mut tools = HashMap::new();
for (name, tool_config) in config.tools {
if tool_config.constraints.len() > MAX_CONSTRAINT_COUNT_PER_TOOL {
return Err(crate::error::Error::ConfigurationError(format!(
"Too many constraints for tool '{}': {} (maximum {})",
name,
tool_config.constraints.len(),
MAX_CONSTRAINT_COUNT_PER_TOOL
)));
}
let rules = CompiledExtractionRules::compile(tool_config.constraints.clone());
tools.insert(
name,
CompiledTool {
config: tool_config,
extraction_rules: rules,
},
);
}
Ok(Self {
settings: config.settings,
tools,
})
}
pub fn validate(&self) -> Vec<String> {
let mut warnings = Vec::new();
for (tool_name, compiled_tool) in &self.tools {
for (field_name, rule) in &compiled_tool.extraction_rules.rules {
match rule.rule.from {
ExtractionSource::Path | ExtractionSource::Query | ExtractionSource::Header => {
warnings.push(format!(
"Tool '{}' field '{}' uses {:?} source, which won't work in MCP (only body/literal supported)",
tool_name, field_name, rule.rule.from
));
}
ExtractionSource::Body | ExtractionSource::Literal => {
}
}
}
}
warnings
}
pub fn extract_constraints(
&self,
tool_name: &str,
arguments: &serde_json::Value,
) -> Result<ExtractionResult, ExtractionError> {
let tool = self.tools.get(tool_name).ok_or_else(|| ExtractionError {
field: "tool".to_string(),
source: ExtractionSource::Literal,
path: tool_name.to_string(),
hint: format!("Tool '{}' not defined in Tenuo configuration", tool_name),
required: true,
})?;
let ctx = RequestContext::with_body(arguments.clone());
let (constraints, traces) = tool.extraction_rules.extract_all(&ctx)?;
Ok(ExtractionResult {
constraints,
traces,
tool: tool_name.to_string(),
warrant_base64: None,
signature_base64: None,
approvals_base64: Vec::new(),
})
}
}
pub fn to_jsonrpc_error(error: &ExtractionError) -> (i32, String) {
if error.required {
(-32602, format!("Invalid params: {}", error.hint))
} else {
(-32602, format!("Invalid params: {}", error.hint))
}
}
pub fn auth_error_to_jsonrpc(error: &crate::error::Error) -> (i32, String) {
use crate::error::Error;
match error {
Error::ConstraintNotSatisfied { field, reason } => (
-32001,
format!(
"Access denied: Constraint '{}' not satisfied: {}",
field, reason
),
),
Error::WarrantExpired(_) => (-32001, "Access denied: Warrant expired".to_string()),
Error::SignatureInvalid(_) => (-32001, "Access denied: Invalid signature".to_string()),
Error::ToolMismatch { .. } => (-32001, "Access denied: Tool not authorized".to_string()),
Error::ApprovalRequired { tool, .. } => (
-32002,
format!(
"Approval required for tool '{}'. Supply approvals in params._meta.tenuo.approvals.",
tool
),
),
_ => (-32001, format!("Access denied: {}", error)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::extraction::{ExtractionRule, ExtractionSource};
use std::collections::HashMap;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_mcp_config_from_file() {
let yaml_content = r#"
version: "1"
settings:
trusted_issuers:
- "f32e74b5a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8"
tools:
filesystem_read:
description: "Read files from the filesystem"
constraints:
path:
from: body
path: "path"
required: true
max_size:
from: body
path: "maxSize"
type: integer
default: 1048576
"#;
let mut file = NamedTempFile::new().unwrap();
write!(file, "{}", yaml_content).unwrap();
let path = file.path();
let config = McpConfig::from_file(path).unwrap();
assert_eq!(config.version, "1");
assert_eq!(config.settings.trusted_issuers.len(), 1);
assert!(config.tools.contains_key("filesystem_read"));
}
#[test]
fn test_mcp_config_invalid_file() {
let result = McpConfig::from_file("/nonexistent/path.yaml");
assert!(result.is_err());
}
#[test]
fn test_compiled_mcp_config_compile() {
let mut tools = HashMap::new();
let mut constraints = HashMap::new();
constraints.insert(
"path".to_string(),
ExtractionRule {
from: ExtractionSource::Body,
path: "path".to_string(),
required: true,
default: None,
description: None,
value_type: None,
allowed_values: None,
},
);
tools.insert(
"read_file".to_string(),
ToolConfig {
description: "Read a file".to_string(),
constraints,
},
);
let config = McpConfig {
version: "1".to_string(),
settings: McpSettings {
trusted_issuers: vec![],
},
tools,
};
let compiled = CompiledMcpConfig::compile(config).unwrap();
assert!(compiled.tools.contains_key("read_file"));
assert_eq!(compiled.settings.trusted_issuers.len(), 0);
}
#[test]
fn test_compiled_mcp_config_validate() {
let mut tools = HashMap::new();
let mut constraints = HashMap::new();
constraints.insert(
"path".to_string(),
ExtractionRule {
from: ExtractionSource::Body,
path: "path".to_string(),
required: true,
default: None,
description: None,
value_type: None,
allowed_values: None,
},
);
tools.insert(
"read_file".to_string(),
ToolConfig {
description: "Read a file".to_string(),
constraints,
},
);
let config = McpConfig {
version: "1".to_string(),
settings: McpSettings {
trusted_issuers: vec![],
},
tools,
};
let compiled = CompiledMcpConfig::compile(config).unwrap();
let warnings = compiled.validate();
assert_eq!(warnings.len(), 0); }
#[test]
fn test_compiled_mcp_config_validate_incompatible_source() {
let mut tools = HashMap::new();
let mut constraints = HashMap::new();
constraints.insert(
"path".to_string(),
ExtractionRule {
from: ExtractionSource::Path,
path: "path".to_string(),
required: true,
default: None,
description: None,
value_type: None,
allowed_values: None,
},
);
tools.insert(
"read_file".to_string(),
ToolConfig {
description: "Read a file".to_string(),
constraints,
},
);
let config = McpConfig {
version: "1".to_string(),
settings: McpSettings {
trusted_issuers: vec![],
},
tools,
};
let compiled = CompiledMcpConfig::compile(config).unwrap();
let warnings = compiled.validate();
assert!(!warnings.is_empty()); assert!(warnings[0].contains("path") || warnings[0].contains("Path"));
}
#[test]
fn test_mcp_config_minimal_without_settings() {
let yaml_content = r#"
version: "1"
tools:
read_file:
description: "Read file contents"
constraints:
path:
from: body
path: "path"
"#;
let mut file = NamedTempFile::new().unwrap();
write!(file, "{}", yaml_content).unwrap();
let path = file.path();
let config = McpConfig::from_file(path).unwrap();
assert_eq!(config.version, "1");
assert!(config.tools.contains_key("read_file"));
assert!(config.settings.trusted_issuers.is_empty());
}
#[test]
fn test_extract_constraints_no_warrant_fields() {
let mut tools = HashMap::new();
let mut constraints = HashMap::new();
constraints.insert(
"path".to_string(),
ExtractionRule {
from: ExtractionSource::Body,
path: "path".to_string(),
required: true,
default: None,
description: None,
value_type: None,
allowed_values: None,
},
);
tools.insert(
"read_file".to_string(),
ToolConfig {
description: "Read a file".to_string(),
constraints,
},
);
let config = McpConfig {
version: "1".to_string(),
settings: McpSettings {
trusted_issuers: vec![],
},
tools,
};
let compiled = CompiledMcpConfig::compile(config).unwrap();
let args = serde_json::json!({
"path": "/data/file.txt"
});
let result = compiled.extract_constraints("read_file", &args).unwrap();
assert!(result.warrant_base64.is_none());
assert!(result.signature_base64.is_none());
assert!(result.approvals_base64.is_empty());
assert!(result.constraints.contains_key("path"));
}
}