use std::collections::BTreeMap; use crank_community_mcp::manifest::{analyze_published_tool_catalog, tool_definitions}; use crank_core::{ ExecutionConfig, HttpMethod, Operation, OperationId, OperationSecurityLevel, OperationStatus, Protocol, RestTarget, Target, ToolDescription, WorkspaceId, }; use crank_mapping::MappingSet; use crank_registry::{PublishedAgentTool, RegistryOperation}; use crank_schema::{Schema, SchemaKind}; use serde_json::json; use time::{OffsetDateTime, format_description::well_known::Rfc3339}; #[test] fn builds_tools_list_manifest_from_published_agent_tool() { let definitions = tool_definitions(&published_tool()); assert_eq!(definitions.len(), 1); let definition = &definitions[0]; assert_eq!(definition["name"], "frankfurter_latest_rate"); assert_eq!(definition["title"], "Последний курс валюты"); assert_eq!( definition["description"], "Возвращает последний курс валюты через Frankfurter." ); assert_eq!(definition["inputSchema"]["type"], "object"); assert_eq!(definition["inputSchema"]["required"], json!(["base"])); assert_eq!( definition["inputSchema"]["properties"]["base"], json!({ "type": "string" }) ); assert_eq!( definition["inputSchema"]["properties"]["mode"], json!({ "type": "string", "enum": ["latest", "historical"] }) ); } #[test] fn marks_destructive_tools_as_two_step_confirmation_calls() { let mut tool = published_tool(); let Target::Rest(target) = &mut tool.operation.target; target.method = HttpMethod::Delete; let definitions = tool_definitions(&tool); let definition = &definitions[0]; assert!( definition["description"] .as_str() .unwrap() .contains("_crank_confirmation_token") ); assert_eq!( definition["inputSchema"]["properties"]["_crank_confirmation_token"]["type"], "string" ); assert_eq!(definition["inputSchema"]["required"], json!(["base"])); } #[test] fn catalog_budget_uses_the_same_definitions_as_tools_list() { let tool = published_tool(); let definitions = tool_definitions(&tool); let analysis = analyze_published_tool_catalog(&[tool]).unwrap(); assert_eq!(analysis.budget.tool_count, definitions.len()); assert_eq!( analysis.budget.serialized_bytes, serde_json::to_vec(&definitions[0]).unwrap().len() ); assert!(!analysis.budget.exceeds_recommended_budget); } fn published_tool() -> PublishedAgentTool { PublishedAgentTool { workspace_id: WorkspaceId::new("ws_01"), workspace_slug: "default".to_owned(), agent_id: "agent_01".into(), agent_slug: "fx-agent".to_owned(), operation: operation(), tool_name: "frankfurter_latest_rate".to_owned(), tool_title: "Последний курс валюты".to_owned(), tool_description: "Возвращает последний курс валюты через Frankfurter.".to_owned(), } } fn operation() -> RegistryOperation { Operation { id: OperationId::new("op_01"), name: "frankfurter_latest_rate".to_owned(), display_name: "Последний курс валюты".to_owned(), category: "finance".to_owned(), protocol: Protocol::Rest, security_level: OperationSecurityLevel::Standard, status: OperationStatus::Published, version: 1, target: Target::Rest(RestTarget { base_url: "https://api.frankfurter.dev".to_owned(), method: HttpMethod::Get, path_template: "/v2/rates".to_owned(), static_headers: BTreeMap::new(), }), input_schema: input_schema(), output_schema: Schema { kind: SchemaKind::Object, description: None, required: false, nullable: false, default_value: None, fields: BTreeMap::new(), items: None, enum_values: Vec::new(), variants: Vec::new(), }, input_mapping: MappingSet { rules: Vec::new() }, output_mapping: MappingSet { rules: Vec::new() }, execution_config: ExecutionConfig { timeout_ms: 10_000, retry_policy: None, response_cache: None, idempotency: None, safety: None, approval_policy: None, auth_profile_ref: None, headers: BTreeMap::new(), }, tool_description: ToolDescription { title: "Последний курс валюты".to_owned(), description: "Возвращает последний курс валюты через Frankfurter.".to_owned(), tags: Vec::new(), examples: Vec::new(), }, samples: None, generated_draft: None, config_export: None, wizard_state: None, created_at: timestamp("2026-06-20T00:00:00Z"), updated_at: timestamp("2026-06-20T00:00:00Z"), published_at: Some(timestamp("2026-06-20T00:00:00Z")), } } fn input_schema() -> Schema { let mut fields = BTreeMap::new(); fields.insert( "base".to_owned(), Schema { kind: SchemaKind::String, description: None, required: true, nullable: false, default_value: None, fields: BTreeMap::new(), items: None, enum_values: Vec::new(), variants: Vec::new(), }, ); fields.insert( "mode".to_owned(), Schema { kind: SchemaKind::Enum, description: None, required: false, nullable: false, default_value: None, fields: BTreeMap::new(), items: None, enum_values: vec!["latest".to_owned(), "historical".to_owned()], variants: Vec::new(), }, ); Schema { kind: SchemaKind::Object, description: None, required: false, nullable: false, default_value: None, fields, items: None, enum_values: Vec::new(), variants: Vec::new(), } } fn timestamp(value: &str) -> OffsetDateTime { OffsetDateTime::parse(value, &Rfc3339).unwrap() }