use super::common::*; use std::time::Duration; use crank_core::{ PlatformApiKeyScope, ToolAccessMode, ToolGroup, ToolSearchSettings, ToolSelectionPolicy, }; use crank_registry::PublishRequest; use serde_json::json; use time::{OffsetDateTime, format_description::well_known::Rfc3339}; #[tokio::test] async fn search_mode_discovers_and_calls_tools_through_meta_tools() { let registry = test_registry().await; let upstream_base_url = spawn_upstream_server().await; let invoice = test_operation(&upstream_base_url, "create_invoice"); let ticket = test_operation(&upstream_base_url, "create_support_ticket"); for operation in [&invoice, &ticket] { registry .create_operation(&test_workspace_id(), operation, Some("alice")) .await .unwrap(); registry .publish_operation(PublishRequest { workspace_id: &test_workspace_id(), operation_id: &operation.id, version: 1, published_at: &OffsetDateTime::parse("2026-03-26T10:00:00Z", &Rfc3339).unwrap(), published_by: Some("alice"), }) .await .unwrap(); } publish_agent_with_policy( ®istry, "business-search", vec![ binding_for_operation(&invoice), binding_for_operation(&ticket), ], ToolSelectionPolicy { mode: ToolAccessMode::Search, groups: vec![ ToolGroup { id: "finance".to_owned(), name: "Finance".to_owned(), description: "Invoices and payments".to_owned(), tool_names: vec![invoice.name.clone()], }, ToolGroup { id: "support".to_owned(), name: "Support".to_owned(), description: "Customer support tickets".to_owned(), tool_names: vec![ticket.name.clone()], }, ], search: ToolSearchSettings { max_results: 5 }, }, ) .await; let api_key = create_platform_api_key( ®istry, "business-search", "mcp-search", &[PlatformApiKeyScope::Read, PlatformApiKeyScope::Write], ) .await; let base_url = spawn_mcp_server(build_test_app( registry, Duration::from_millis(0), Some("https://crank.example.com".to_owned()), )) .await; let client = reqwest::Client::new(); let mcp_url = agent_mcp_url(&base_url, "business-search"); let session = initialize_session(&client, &mcp_url, &api_key).await; let listed = post_jsonrpc( &client, &mcp_url, &api_key, Some(&session), json!({"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}), ) .await; assert_eq!( listed["result"]["tools"] .as_array() .unwrap() .iter() .map(|tool| tool["name"].as_str().unwrap()) .collect::>(), vec!["search_tools", "call_tool"] ); let search = post_jsonrpc( &client, &mcp_url, &api_key, Some(&session), json!({ "jsonrpc":"2.0","id":3,"method":"tools/call", "params":{"name":"search_tools","arguments":{"query":"invoice","group_ids":["finance"]}} }), ) .await; assert_eq!( search["result"]["structuredContent"]["tools"][0]["name"], "create_invoice" ); assert_eq!( search["result"]["structuredContent"]["catalog_revision"], "agent-version-1" ); let stale_call = post_jsonrpc( &client, &mcp_url, &api_key, Some(&session), json!({ "jsonrpc":"2.0","id":4,"method":"tools/call", "params":{"name":"call_tool","arguments":{ "name":"create_invoice", "arguments":{"email":"user@example.com"}, "catalog_revision":"agent-version-0" }} }), ) .await; assert_eq!(stale_call["result"]["isError"], true); assert_eq!( stale_call["result"]["structuredContent"]["error"]["code"], "catalog_revision_changed" ); let call = post_jsonrpc( &client, &mcp_url, &api_key, Some(&session), json!({ "jsonrpc":"2.0","id":5,"method":"tools/call", "params":{"name":"call_tool","arguments":{ "name":"create_invoice", "arguments":{"email":"user@example.com"}, "catalog_revision":"agent-version-1" }} }), ) .await; assert_eq!(call["result"]["isError"], false); assert_eq!(call["result"]["structuredContent"]["id"], "lead_123"); }