use crate::common::types::{GeneratedCode, GeneratedFile};
use crate::common::typescript::{extract_properties, to_camel_case};
use crate::progressive::types::{
BridgeContext, CategoryInfo, IndexContext, PropertyInfo, ToolCategorization, ToolContext,
ToolSummary,
};
use crate::template_engine::TemplateEngine;
use mcp_execution_core::{Error, Result};
use mcp_execution_introspector::ServerInfo;
use std::collections::HashMap;
#[derive(Debug)]
pub struct ProgressiveGenerator<'a> {
engine: TemplateEngine<'a>,
}
impl<'a> ProgressiveGenerator<'a> {
pub fn new() -> Result<Self> {
let engine = TemplateEngine::new()?;
Ok(Self { engine })
}
pub fn generate(&self, server_info: &ServerInfo) -> Result<GeneratedCode> {
tracing::info!(
"Generating progressive loading code for server: {}",
server_info.name
);
let mut code = GeneratedCode::new();
let server_id = server_info.id.as_str();
for tool in &server_info.tools {
let tool_context = self.create_tool_context(server_id, tool, None)?;
let tool_code = self.engine.render("progressive/tool", &tool_context)?;
code.add_file(GeneratedFile {
path: format!("{}.ts", tool_context.typescript_name),
content: tool_code,
});
tracing::debug!("Generated tool file: {}.ts", tool_context.typescript_name);
}
let index_context = self.create_index_context(server_info, None)?;
let index_code = self.engine.render("progressive/index", &index_context)?;
code.add_file(GeneratedFile {
path: "index.ts".to_string(),
content: index_code,
});
tracing::debug!("Generated index.ts");
let bridge_context = BridgeContext::default();
let bridge_code = self
.engine
.render("progressive/runtime-bridge", &bridge_context)?;
code.add_file(GeneratedFile {
path: "_runtime/mcp-bridge.ts".to_string(),
content: bridge_code,
});
tracing::debug!("Generated _runtime/mcp-bridge.ts");
tracing::info!(
"Successfully generated {} files for {} (progressive loading)",
code.file_count(),
server_info.name
);
Ok(code)
}
pub fn generate_with_categories(
&self,
server_info: &ServerInfo,
categorizations: &HashMap<String, ToolCategorization>,
) -> Result<GeneratedCode> {
tracing::info!(
"Generating progressive loading code with categorizations for server: {}",
server_info.name
);
let mut code = GeneratedCode::new();
let server_id = server_info.id.as_str();
for tool in &server_info.tools {
let tool_name = tool.name.as_str();
let categorization = categorizations.get(tool_name);
let tool_context = self.create_tool_context(server_id, tool, categorization)?;
let tool_code = self.engine.render("progressive/tool", &tool_context)?;
code.add_file(GeneratedFile {
path: format!("{}.ts", tool_context.typescript_name),
content: tool_code,
});
tracing::debug!(
"Generated tool file: {}.ts (category: {:?})",
tool_context.typescript_name,
categorization.map(|c| &c.category)
);
}
let index_context = self.create_index_context(server_info, Some(categorizations))?;
let index_code = self.engine.render("progressive/index", &index_context)?;
code.add_file(GeneratedFile {
path: "index.ts".to_string(),
content: index_code,
});
tracing::debug!(
"Generated index.ts with {} categorizations",
categorizations.len()
);
let bridge_context = BridgeContext::default();
let bridge_code = self
.engine
.render("progressive/runtime-bridge", &bridge_context)?;
code.add_file(GeneratedFile {
path: "_runtime/mcp-bridge.ts".to_string(),
content: bridge_code,
});
tracing::debug!("Generated _runtime/mcp-bridge.ts");
tracing::info!(
"Successfully generated {} files for {} with categorizations (progressive loading)",
code.file_count(),
server_info.name
);
Ok(code)
}
fn create_tool_context(
&self,
server_id: &str,
tool: &mcp_execution_introspector::ToolInfo,
categorization: Option<&ToolCategorization>,
) -> Result<ToolContext> {
let typescript_name = to_camel_case(tool.name.as_str());
let properties = self.extract_property_infos(&tool.input_schema)?;
Ok(ToolContext {
server_id: server_id.to_string(),
name: tool.name.as_str().to_string(),
typescript_name,
description: tool.description.clone(),
input_schema: tool.input_schema.clone(),
properties,
category: categorization.map(|c| c.category.clone()),
keywords: categorization.map(|c| c.keywords.clone()),
short_description: categorization.map(|c| c.short_description.clone()),
})
}
fn create_index_context(
&self,
server_info: &ServerInfo,
categorizations: Option<&HashMap<String, ToolCategorization>>,
) -> Result<IndexContext> {
let tools: Vec<ToolSummary> = server_info
.tools
.iter()
.map(|tool| {
let tool_name = tool.name.as_str();
let cat = categorizations.and_then(|c| c.get(tool_name));
ToolSummary {
typescript_name: to_camel_case(tool_name),
description: tool.description.clone(),
category: cat.map(|c| c.category.clone()),
keywords: cat.map(|c| c.keywords.clone()),
short_description: cat.map(|c| c.short_description.clone()),
}
})
.collect();
let category_groups = categorizations.map(|_| {
let mut groups: HashMap<String, Vec<ToolSummary>> = HashMap::new();
for tool in &tools {
let cat_name = tool
.category
.clone()
.unwrap_or_else(|| "uncategorized".to_string());
groups.entry(cat_name).or_default().push(tool.clone());
}
let mut result: Vec<CategoryInfo> = groups
.into_iter()
.map(|(name, tools)| CategoryInfo { name, tools })
.collect();
result.sort_by(|a, b| {
if a.name == "uncategorized" {
std::cmp::Ordering::Greater
} else if b.name == "uncategorized" {
std::cmp::Ordering::Less
} else {
a.name.cmp(&b.name)
}
});
result
});
Ok(IndexContext {
server_name: server_info.name.clone(),
server_version: server_info.version.clone(),
tool_count: server_info.tools.len(),
tools,
categories: category_groups,
})
}
fn extract_property_infos(&self, schema: &serde_json::Value) -> Result<Vec<PropertyInfo>> {
let raw_properties = extract_properties(schema);
let mut properties = Vec::new();
for prop in raw_properties {
let name = prop["name"]
.as_str()
.ok_or_else(|| Error::ValidationError {
field: "name".to_string(),
reason: "Property name is not a string".to_string(),
})?
.to_string();
let typescript_type = prop["type"]
.as_str()
.ok_or_else(|| Error::ValidationError {
field: "type".to_string(),
reason: "Property type is not a string".to_string(),
})?
.to_string();
let required = prop["required"].as_bool().unwrap_or(false);
let description = if let Some(obj) = schema.as_object() {
obj.get("properties")
.and_then(|props| props.as_object())
.and_then(|props| props.get(&name))
.and_then(|prop_schema| prop_schema.as_object())
.and_then(|obj| obj.get("description"))
.and_then(|desc| desc.as_str())
.map(String::from)
} else {
None
};
properties.push(PropertyInfo {
name,
typescript_type,
description,
required,
});
}
Ok(properties)
}
}
#[cfg(test)]
mod tests {
use super::*;
use mcp_execution_core::{ServerId, ToolName};
use mcp_execution_introspector::{ServerCapabilities, ToolInfo};
use serde_json::json;
fn create_test_server_info() -> ServerInfo {
ServerInfo {
id: ServerId::new("test-server"),
name: "Test Server".to_string(),
version: "1.0.0".to_string(),
tools: vec![
ToolInfo {
name: ToolName::new("create_issue"),
description: "Creates a new issue".to_string(),
input_schema: json!({
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Issue title"
},
"body": {
"type": "string",
"description": "Issue body"
}
},
"required": ["title"]
}),
output_schema: None,
},
ToolInfo {
name: ToolName::new("update_issue"),
description: "Updates an existing issue".to_string(),
input_schema: json!({
"type": "object",
"properties": {
"id": {
"type": "number"
}
},
"required": ["id"]
}),
output_schema: None,
},
],
capabilities: ServerCapabilities {
supports_tools: true,
supports_resources: false,
supports_prompts: false,
},
}
}
#[test]
fn test_progressive_generator_new() {
let generator = ProgressiveGenerator::new();
assert!(generator.is_ok());
}
#[test]
fn test_generate_progressive_files() {
let generator = ProgressiveGenerator::new().unwrap();
let server_info = create_test_server_info();
let code = generator.generate(&server_info).unwrap();
assert_eq!(code.file_count(), 4);
let tool_files: Vec<_> = code.files.iter().map(|f| f.path.as_str()).collect();
assert!(tool_files.contains(&"createIssue.ts"));
assert!(tool_files.contains(&"updateIssue.ts"));
assert!(tool_files.contains(&"index.ts"));
assert!(tool_files.contains(&"_runtime/mcp-bridge.ts"));
}
#[test]
fn test_create_tool_context() {
let generator = ProgressiveGenerator::new().unwrap();
let tool = ToolInfo {
name: ToolName::new("send_message"),
description: "Sends a message".to_string(),
input_schema: json!({
"type": "object",
"properties": {
"text": {"type": "string"}
},
"required": ["text"]
}),
output_schema: None,
};
let categorization = ToolCategorization {
category: "messaging".to_string(),
keywords: "send,message,chat".to_string(),
short_description: "Send a message".to_string(),
};
let context = generator
.create_tool_context("test-server", &tool, Some(&categorization))
.unwrap();
assert_eq!(context.server_id, "test-server");
assert_eq!(context.name, "send_message");
assert_eq!(context.typescript_name, "sendMessage");
assert_eq!(context.description, "Sends a message");
assert_eq!(context.properties.len(), 1);
assert_eq!(context.properties[0].name, "text");
assert_eq!(context.category, Some("messaging".to_string()));
assert_eq!(context.keywords, Some("send,message,chat".to_string()));
assert_eq!(
context.short_description,
Some("Send a message".to_string())
);
}
#[test]
fn test_create_index_context() {
let generator = ProgressiveGenerator::new().unwrap();
let server_info = create_test_server_info();
let context = generator.create_index_context(&server_info, None).unwrap();
assert_eq!(context.server_name, "Test Server");
assert_eq!(context.server_version, "1.0.0");
assert_eq!(context.tool_count, 2);
assert_eq!(context.tools.len(), 2);
assert_eq!(context.tools[0].typescript_name, "createIssue");
assert!(context.categories.is_none());
}
#[test]
fn test_extract_property_infos() {
let generator = ProgressiveGenerator::new().unwrap();
let schema = json!({
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "User name"
},
"age": {
"type": "number"
}
},
"required": ["name"]
});
let props = generator.extract_property_infos(&schema).unwrap();
assert_eq!(props.len(), 2);
let name_prop = props.iter().find(|p| p.name == "name").unwrap();
assert_eq!(name_prop.typescript_type, "string");
assert_eq!(name_prop.description, Some("User name".to_string()));
assert!(name_prop.required);
let age_prop = props.iter().find(|p| p.name == "age").unwrap();
assert_eq!(age_prop.typescript_type, "number");
assert!(!age_prop.required);
}
}