use crate::mcp::{
McpError, McpResult,
manager::{MCP_QUALIFIED_NAME_MAX_BYTES, qualified_name},
protocol::Tool,
};
use serde_json::{Value, json};
pub(crate) fn provider_tool_definition(server_name: &str, tool: &Tool) -> McpResult<Value> {
validate_tool_name(&tool.name)?;
let name = qualified_name(server_name, &tool.name);
if name.len() > MCP_QUALIFIED_NAME_MAX_BYTES {
return Err(McpError::Config(format!(
"MCP qualified tool name exceeds {MCP_QUALIFIED_NAME_MAX_BYTES} bytes"
)));
}
let description = tool
.description
.as_deref()
.or(tool.title.as_deref())
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToString::to_string)
.unwrap_or_else(|| format!("MCP tool from server {server_name}"));
Ok(json!({
"type": "function",
"name": name,
"description": description,
"parameters": object_schema_or_default(&tool.input_schema),
}))
}
pub(crate) fn validate_tool_name(name: &str) -> McpResult<()> {
if name.is_empty() {
return Err(McpError::Config(
"MCP tool name must not be empty".to_string(),
));
}
if name.contains("__") {
return Err(McpError::Config(format!(
"MCP tool name '{name}' must not contain '__'"
)));
}
if !name
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_' || byte == b'-')
{
return Err(McpError::Config(format!(
"MCP tool name '{name}' must contain only ASCII letters, digits, '_' or '-'"
)));
}
Ok(())
}
fn object_schema_or_default(schema: &Value) -> Value {
if schema.as_object().and_then(|object| object.get("type"))
== Some(&Value::String("object".to_string()))
{
schema.clone()
} else {
default_schema()
}
}
fn default_schema() -> Value {
json!({"type":"object","properties":{},"additionalProperties":true})
}
#[cfg(test)]
mod tests {
use super::*;
fn tool(name: &str, input_schema: Value) -> Tool {
Tool {
name: name.to_string(),
title: Some("Title".to_string()),
description: None,
input_schema,
output_schema: None,
annotations: None,
}
}
#[test]
fn converts_tool_to_provider_definition() {
let definition = provider_tool_definition(
"mock",
&Tool {
description: Some("Echo input".to_string()),
..tool(
"echo",
json!({"type":"object","properties":{"text":{"type":"string"}}}),
)
},
)
.unwrap();
assert_eq!(definition["name"], "mcp__mock__echo");
assert_eq!(definition["description"], "Echo input");
assert_eq!(definition["parameters"]["type"], "object");
}
#[test]
fn falls_back_for_missing_description_and_non_object_schema() {
let definition =
provider_tool_definition("mock", &tool("echo", json!({"type":"string"}))).unwrap();
assert_eq!(definition["description"], "Title");
assert_eq!(
definition["parameters"],
json!({"type":"object","properties":{},"additionalProperties":true})
);
}
#[test]
fn rejects_unsafe_tool_names() {
for name in ["bad/name", "bad__name", ""] {
assert!(
provider_tool_definition("mock", &tool(name, json!({"type":"object"}))).is_err()
);
}
}
}