Skip to main content

mcp_tester/
lib.rs

1//! MCP Server Testing Library
2//!
3//! This crate provides comprehensive testing capabilities for MCP servers.
4//! It can be used as both a standalone CLI tool (`mcp-tester`) and as a library.
5//!
6//! # Examples
7//!
8//! ## Generate Test Scenarios
9//!
10//! ```no_run
11//! use mcp_tester::{generate_scenarios, GenerateOptions};
12//!
13//! #[tokio::main]
14//! async fn main() -> anyhow::Result<()> {
15//!     let options = GenerateOptions {
16//!         all_tools: true,
17//!         with_resources: true,
18//!         with_prompts: true,
19//!     };
20//!
21//!     generate_scenarios(
22//!         "http://localhost:3000",
23//!         "test_scenario.yaml",
24//!         options
25//!     ).await?;
26//!
27//!     Ok(())
28//! }
29//! ```
30//!
31//! ## Run Test Scenarios
32//!
33//! ```no_run
34//! use mcp_tester::run_scenario;
35//!
36//! #[tokio::main]
37//! async fn main() -> anyhow::Result<()> {
38//!     run_scenario(
39//!         "test_scenario.yaml",
40//!         "http://localhost:3000",
41//!         false // detailed output
42//!     ).await?;
43//!
44//!     Ok(())
45//! }
46//! ```
47
48// Re-export key modules
49pub mod app_validator;
50pub mod conformance;
51pub mod diagnostics;
52pub mod post_deploy_report;
53pub mod report;
54pub mod scenario;
55pub mod scenario_executor;
56pub mod scenario_generator;
57pub mod tester;
58pub mod validators;
59
60// OAuth support -- re-exported from core SDK
61pub use app_validator::{AppValidationMode, AppValidator};
62pub use conformance::{ConformanceDomain, ConformanceRunner};
63pub use pmcp::client::oauth;
64pub use pmcp::client::oauth::{OAuthConfig, OAuthHelper};
65pub use post_deploy_report::{
66    FailureDetail, PostDeployReport, TestCommand as PostDeployTestCommand,
67    TestOutcome as PostDeployTestOutcome,
68};
69pub use report::{
70    expand_guide_anchor, OutputFormat, TestCategory, TestReport, TestResult, TestStatus,
71    TestSummary,
72};
73pub use scenario::TestScenario;
74pub use scenario_executor::ScenarioExecutor;
75pub use scenario_generator::ScenarioGenerator;
76pub use tester::ServerTester;
77
78use anyhow::{Context, Result};
79use std::time::Duration;
80
81/// Options for generating test scenarios
82#[derive(Debug, Clone)]
83pub struct GenerateOptions {
84    /// Include all discovered tools in scenarios
85    pub all_tools: bool,
86    /// Include resource testing
87    pub with_resources: bool,
88    /// Include prompt testing
89    pub with_prompts: bool,
90}
91
92impl Default for GenerateOptions {
93    fn default() -> Self {
94        Self {
95            all_tools: true,
96            with_resources: false,
97            with_prompts: false,
98        }
99    }
100}
101
102/// Generate test scenarios from server capabilities
103///
104/// This function connects to an MCP server, discovers its capabilities,
105/// and generates comprehensive test scenarios in YAML format.
106///
107/// # Arguments
108///
109/// * `server_url` - URL of the MCP server to test
110/// * `output_path` - Path where the generated scenario file will be written
111/// * `options` - Configuration for scenario generation
112///
113/// # Example
114///
115/// ```no_run
116/// use mcp_tester::{generate_scenarios, GenerateOptions};
117///
118/// # async fn example() -> anyhow::Result<()> {
119/// generate_scenarios(
120///     "http://localhost:3000",
121///     "scenarios/my_server.yaml",
122///     GenerateOptions::default()
123/// ).await?;
124/// # Ok(())
125/// # }
126/// ```
127pub async fn generate_scenarios(
128    server_url: &str,
129    output_path: &str,
130    options: GenerateOptions,
131) -> Result<()> {
132    generate_scenarios_with_transport(server_url, output_path, options, None).await
133}
134
135/// Generate test scenarios with explicit transport type
136///
137/// # Arguments
138///
139/// * `server_url` - URL of the MCP server to test
140/// * `output_path` - Path where the generated scenario file will be written
141/// * `options` - Configuration for scenario generation
142/// * `transport` - Transport type: "http" (SSE), "jsonrpc" (POST), "stdio", or None for auto-detect
143pub async fn generate_scenarios_with_transport(
144    server_url: &str,
145    output_path: &str,
146    options: GenerateOptions,
147    transport: Option<&str>,
148) -> Result<()> {
149    // Create a server tester
150    let mut tester = ServerTester::new(
151        server_url,
152        Duration::from_secs(30),
153        false,     // insecure
154        None,      // api_key
155        transport, // transport type
156        None,      // http_middleware_chain
157    )?;
158
159    // Create generator
160    let generator = ScenarioGenerator::new(
161        server_url.to_string(),
162        options.all_tools,
163        options.with_resources,
164        options.with_prompts,
165    );
166
167    // Generate scenarios
168    generator.generate(&mut tester, output_path).await
169}
170
171/// Run a test scenario from a file
172///
173/// # Arguments
174///
175/// * `scenario_path` - Path to the scenario YAML/JSON file
176/// * `server_url` - URL of the MCP server to test
177/// * `detailed` - Whether to show detailed step-by-step output
178///
179/// # Example
180///
181/// ```no_run
182/// use mcp_tester::run_scenario;
183///
184/// # async fn example() -> anyhow::Result<()> {
185/// run_scenario(
186///     "scenarios/my_test.yaml",
187///     "http://localhost:3000",
188///     true // show detailed output
189/// ).await?;
190/// # Ok(())
191/// # }
192/// ```
193pub async fn run_scenario(scenario_path: &str, server_url: &str, detailed: bool) -> Result<()> {
194    run_scenario_with_transport(scenario_path, server_url, detailed, None).await
195}
196
197/// Run a test scenario with explicit transport type
198///
199/// # Arguments
200///
201/// * `scenario_path` - Path to the scenario YAML/JSON file
202/// * `server_url` - URL of the MCP server to test
203/// * `detailed` - Whether to show detailed step-by-step output
204/// * `transport` - Transport type: "http" (SSE), "jsonrpc" (POST), "stdio", or None for auto-detect
205pub async fn run_scenario_with_transport(
206    scenario_path: &str,
207    server_url: &str,
208    detailed: bool,
209    transport: Option<&str>,
210) -> Result<()> {
211    use colored::*;
212
213    // Load scenario
214    let scenario_content = std::fs::read_to_string(scenario_path)
215        .with_context(|| format!("Failed to read scenario file: {}", scenario_path))?;
216
217    let scenario: TestScenario = if scenario_path.ends_with(".json") {
218        serde_json::from_str(&scenario_content)?
219    } else {
220        serde_yaml::from_str(&scenario_content)?
221    };
222
223    // Create a server tester
224    let mut tester = ServerTester::new(
225        server_url,
226        Duration::from_secs(30),
227        false,     // insecure
228        None,      // api_key
229        transport, // transport type
230        None,      // http_middleware_chain
231    )?;
232
233    // Execute scenario
234    let mut executor = ScenarioExecutor::new(&mut tester, detailed);
235
236    if detailed {
237        println!(
238            "\n{}",
239            "Running scenario with detailed output..."
240                .bright_cyan()
241                .bold()
242        );
243    }
244
245    let result = executor.execute(scenario).await?;
246
247    // Report results
248    if result.success {
249        println!("\n{} Scenario passed!", "✓".green().bold());
250        Ok(())
251    } else {
252        println!("\n{} Scenario failed!", "✗".red().bold());
253        anyhow::bail!("Scenario execution failed");
254    }
255}
256
257/// Create a server tester instance
258///
259/// This is useful for more advanced testing scenarios where you want
260/// full control over the testing process.
261///
262/// # Example
263///
264/// ```no_run
265/// use mcp_tester::create_tester;
266///
267/// # fn example() -> anyhow::Result<()> {
268/// let tester = create_tester(
269///     "http://localhost:3000",
270///     30, // timeout in seconds
271///     false, // don't skip TLS verification
272///     None, // no API key
273/// )?;
274///
275/// // Use tester for custom testing
276/// # Ok(())
277/// # }
278/// ```
279pub fn create_tester(
280    url: &str,
281    timeout_secs: u64,
282    insecure: bool,
283    api_key: Option<String>,
284) -> Result<ServerTester> {
285    ServerTester::new(
286        url,
287        Duration::from_secs(timeout_secs),
288        insecure,
289        api_key.as_deref(),
290        None, // transport
291        None, // http_middleware_chain
292    )
293}