1#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
3
4use std::collections::BTreeMap;
5use std::fmt;
6use std::sync::Arc;
7
8use serde::{Deserialize, Serialize};
9use serde_json::{Number, Value as JsonValue};
10
11mod generator;
12mod output_schema;
13mod runner;
14pub mod schema;
15pub mod session;
16mod validation;
17
18pub use rmcp::model::{
19 CallToolRequestParam, CallToolResult, ErrorCode, ErrorData, JsonObject, Tool,
20};
21pub use rmcp::service::{ClientInitializeError, ServiceError};
22pub use runner::{run_http, run_stdio, run_with_session, RunnerOptions};
23pub use schema::{
24 parse_call_tool_request, parse_call_tool_result, parse_list_tools, schema_version_label,
25 SchemaError,
26};
27pub use session::{SessionDriver, SessionError};
28pub use validation::{
29 list_tools_http, list_tools_stdio, list_tools_with_session, validate_tool, validate_tools,
30 BulkToolValidationSummary, ListToolsError, ToolValidationConfig, ToolValidationDecision,
31 ToolValidationError, ToolValidationFailure, ToolValidationFn,
32};
33
34#[cfg(test)]
35#[path = "../tests/internal/mod.rs"]
36mod tests;
37
38#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
40#[serde(rename_all = "kebab-case")]
41pub enum SchemaVersion {
42 #[default]
44 V2025_11_25,
45 Other(String),
47}
48
49#[derive(Clone, Debug, Default, Serialize, Deserialize)]
53pub struct StateMachineConfig {
54 pub seed_numbers: Vec<Number>,
56 pub seed_strings: Vec<String>,
58 pub mine_text: bool,
60 pub dump_corpus: bool,
62 pub log_corpus_deltas: bool,
64 pub lenient_sourcing: bool,
66 pub coverage_allowlist: Option<Vec<String>>,
68 pub coverage_blocklist: Option<Vec<String>>,
70 pub coverage_rules: Vec<CoverageRule>,
72}
73
74impl StateMachineConfig {
75 pub fn with_seed_numbers(mut self, seed_numbers: Vec<Number>) -> Self {
77 self.seed_numbers = seed_numbers;
78 self
79 }
80
81 pub fn with_seed_strings(mut self, seed_strings: Vec<String>) -> Self {
83 self.seed_strings = seed_strings;
84 self
85 }
86
87 pub fn with_mine_text(mut self, mine_text: bool) -> Self {
89 self.mine_text = mine_text;
90 self
91 }
92
93 pub fn with_dump_corpus(mut self, dump_corpus: bool) -> Self {
95 self.dump_corpus = dump_corpus;
96 self
97 }
98
99 pub fn with_log_corpus_deltas(mut self, log_corpus_deltas: bool) -> Self {
101 self.log_corpus_deltas = log_corpus_deltas;
102 self
103 }
104
105 pub fn with_lenient_sourcing(mut self, lenient_sourcing: bool) -> Self {
107 self.lenient_sourcing = lenient_sourcing;
108 self
109 }
110
111 pub fn with_coverage_allowlist(mut self, coverage_allowlist: Vec<String>) -> Self {
113 self.coverage_allowlist = Some(coverage_allowlist);
114 self
115 }
116
117 pub fn with_coverage_blocklist(mut self, coverage_blocklist: Vec<String>) -> Self {
119 self.coverage_blocklist = Some(coverage_blocklist);
120 self
121 }
122
123 pub fn with_coverage_rules(mut self, coverage_rules: Vec<CoverageRule>) -> Self {
125 self.coverage_rules = coverage_rules;
126 self
127 }
128}
129
130#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
132pub struct SchemaConfig {
133 pub version: SchemaVersion,
135}
136
137#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
139pub struct StdioConfig {
140 pub command: String,
142 pub args: Vec<String>,
144 pub env: BTreeMap<String, String>,
146 pub cwd: Option<String>,
148}
149
150impl StdioConfig {
151 pub fn new(command: impl Into<String>) -> Self {
153 Self {
154 command: command.into(),
155 args: Vec::new(),
156 env: BTreeMap::new(),
157 cwd: None,
158 }
159 }
160}
161
162#[derive(Clone, Debug, Eq, PartialEq)]
164pub struct PreRunHook {
165 pub command: String,
167 pub env: BTreeMap<String, String>,
169 pub cwd: Option<String>,
171}
172
173impl PreRunHook {
174 pub fn new(command: impl Into<String>) -> Self {
176 Self {
177 command: command.into(),
178 env: BTreeMap::new(),
179 cwd: None,
180 }
181 }
182
183 fn apply_stdio_context(&mut self, endpoint: &StdioConfig) {
184 self.env = endpoint.env.clone();
185 self.cwd = endpoint.cwd.clone();
186 }
187}
188
189#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
191pub struct HttpConfig {
192 pub url: String,
194 pub auth_token: Option<String>,
196}
197
198pub type ToolPredicate = Arc<dyn Fn(&str, &JsonValue) -> bool + Send + Sync>;
200
201#[derive(Clone, Debug, Default, Serialize, Deserialize)]
225pub struct AssertionSet {
226 pub rules: Vec<AssertionRule>,
228}
229
230#[derive(Clone, Debug, Serialize, Deserialize)]
232#[serde(tag = "scope", content = "rule", rename_all = "snake_case")]
233pub enum AssertionRule {
234 Response(ResponseAssertion),
236 Sequence(SequenceAssertion),
238}
239
240#[derive(Clone, Debug, Serialize, Deserialize)]
242pub struct ResponseAssertion {
243 pub tool: Option<String>,
245 pub checks: Vec<AssertionCheck>,
247}
248
249#[derive(Clone, Debug, Serialize, Deserialize)]
251pub struct SequenceAssertion {
252 pub checks: Vec<AssertionCheck>,
254}
255
256#[derive(Clone, Debug, Serialize, Deserialize)]
260pub struct AssertionCheck {
261 pub target: AssertionTarget,
263 pub pointer: String,
265 pub expected: JsonValue,
267}
268
269#[derive(Clone, Debug, Serialize, Deserialize)]
271#[serde(rename_all = "snake_case")]
272pub enum AssertionTarget {
273 Input,
275 Output,
277 StructuredOutput,
279 Sequence,
281}
282
283#[derive(Clone)]
285pub struct RunConfig {
286 pub schema: SchemaConfig,
288 pub predicate: Option<ToolPredicate>,
290 pub assertions: AssertionSet,
292 pub state_machine: StateMachineConfig,
294 pub pre_run_hook: Option<PreRunHook>,
296}
297
298impl RunConfig {
299 pub fn new() -> Self {
304 Self {
305 schema: SchemaConfig::default(),
306 predicate: None,
307 assertions: AssertionSet::default(),
308 state_machine: StateMachineConfig::default(),
309 pre_run_hook: None,
310 }
311 }
312
313 pub fn with_schema(mut self, schema: SchemaConfig) -> Self {
315 self.schema = schema;
316 self
317 }
318
319 pub fn with_predicate(mut self, predicate: ToolPredicate) -> Self {
321 self.predicate = Some(predicate);
322 self
323 }
324
325 pub fn with_assertions(mut self, assertions: AssertionSet) -> Self {
327 self.assertions = assertions;
328 self
329 }
330
331 pub fn with_state_machine(mut self, state_machine: StateMachineConfig) -> Self {
333 self.state_machine = state_machine;
334 self
335 }
336
337 pub fn with_pre_run_hook(mut self, hook: PreRunHook) -> Self {
339 self.pre_run_hook = Some(hook);
340 self
341 }
342
343 pub(crate) fn apply_stdio_pre_run_context(&mut self, endpoint: &StdioConfig) {
344 if let Some(hook) = self.pre_run_hook.as_mut() {
345 hook.apply_stdio_context(endpoint);
346 }
347 }
348}
349
350impl Default for RunConfig {
351 fn default() -> Self {
352 Self::new()
353 }
354}
355
356impl fmt::Debug for RunConfig {
357 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
358 f.debug_struct("RunConfig")
359 .field("schema", &self.schema)
360 .field("predicate", &self.predicate.is_some())
361 .field("assertions", &self.assertions)
362 .field("state_machine", &self.state_machine)
363 .field("pre_run_hook", &self.pre_run_hook.is_some())
364 .finish()
365 }
366}
367
368pub type ToolInvocation = CallToolRequestParam;
370
371#[derive(Clone, Debug, Serialize, Deserialize)]
373#[serde(tag = "kind", rename_all = "snake_case")]
374pub enum TraceEntry {
375 ListTools {
377 #[serde(skip_serializing_if = "Option::is_none")]
379 failure_reason: Option<String>,
380 },
381 ToolCall {
383 invocation: ToolInvocation,
385 #[serde(skip_serializing_if = "Option::is_none")]
387 response: Option<CallToolResult>,
388 #[serde(skip_serializing_if = "Option::is_none")]
390 failure_reason: Option<String>,
391 },
392}
393
394impl TraceEntry {
395 pub fn list_tools() -> Self {
397 Self::ListTools {
398 failure_reason: None,
399 }
400 }
401
402 pub fn list_tools_with_failure(reason: String) -> Self {
404 Self::ListTools {
405 failure_reason: Some(reason),
406 }
407 }
408
409 pub fn tool_call(invocation: ToolInvocation) -> Self {
411 Self::ToolCall {
412 invocation,
413 response: None,
414 failure_reason: None,
415 }
416 }
417
418 pub fn tool_call_with_response(invocation: ToolInvocation, response: CallToolResult) -> Self {
420 Self::ToolCall {
421 invocation,
422 response: Some(response),
423 failure_reason: None,
424 }
425 }
426
427 pub fn as_tool_call(&self) -> Option<(&ToolInvocation, Option<&CallToolResult>)> {
429 match self {
430 TraceEntry::ToolCall {
431 invocation,
432 response,
433 ..
434 } => Some((invocation, response.as_ref())),
435 TraceEntry::ListTools { .. } => None,
436 }
437 }
438}
439
440#[derive(Clone, Debug, Serialize, Deserialize)]
442pub struct MinimizedSequence {
443 pub invocations: Vec<ToolInvocation>,
445}
446
447#[derive(Clone, Debug, Serialize, Deserialize)]
449#[serde(tag = "status", rename_all = "snake_case")]
450pub enum RunOutcome {
451 Success,
453 Failure(RunFailure),
455}
456
457#[derive(Clone, Debug, Serialize, Deserialize)]
459pub struct RunFailure {
460 pub reason: String,
462 pub code: Option<String>,
464 pub details: Option<JsonValue>,
466}
467
468impl RunFailure {
469 pub fn new(reason: impl Into<String>) -> Self {
471 Self {
472 reason: reason.into(),
473 code: None,
474 details: None,
475 }
476 }
477}
478
479#[derive(Clone, Debug, Serialize, Deserialize)]
481pub struct RunWarning {
482 pub code: RunWarningCode,
484 pub message: String,
486 #[serde(skip_serializing_if = "Option::is_none")]
488 pub tool: Option<String>,
489}
490
491#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
493#[serde(rename_all = "snake_case")]
494pub enum RunWarningCode {
495 SchemaUnsupportedKeyword,
496}
497
498#[derive(Clone, Debug, Serialize, Deserialize)]
500pub struct CoverageWarning {
501 pub tool: String,
503 pub reason: CoverageWarningReason,
505}
506
507#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
509#[serde(rename_all = "snake_case")]
510pub enum CoverageWarningReason {
511 MissingString,
512 MissingInteger,
513 MissingNumber,
514 MissingRequiredValue,
515}
516
517#[derive(Clone, Debug, Serialize, Deserialize)]
519pub struct CoverageReport {
520 pub counts: BTreeMap<String, u64>,
522 pub warnings: Vec<CoverageWarning>,
524}
525
526#[derive(Clone, Debug, Serialize, Deserialize)]
528pub struct CorpusReport {
529 pub numbers: Vec<Number>,
531 pub integers: Vec<i64>,
533 pub strings: Vec<String>,
535}
536
537#[derive(Clone, Debug, Serialize, Deserialize)]
539#[serde(tag = "rule", rename_all = "snake_case")]
540pub enum CoverageRule {
541 MinCallsPerTool { min: u64 },
543 NoUncalledTools,
545 PercentCalled { min_percent: f64 },
547}
548
549impl CoverageRule {
550 pub fn min_calls_per_tool(min: u64) -> Self {
552 Self::MinCallsPerTool { min }
553 }
554
555 pub fn no_uncalled_tools() -> Self {
557 Self::NoUncalledTools
558 }
559
560 pub fn percent_called(min_percent: f64) -> Self {
562 Self::PercentCalled { min_percent }
563 }
564}
565
566#[derive(Clone, Debug, Serialize, Deserialize)]
568pub struct RunResult {
569 pub outcome: RunOutcome,
571 pub trace: Vec<TraceEntry>,
573 pub minimized: Option<MinimizedSequence>,
575 pub warnings: Vec<RunWarning>,
577 pub coverage: Option<CoverageReport>,
579 #[serde(skip_serializing_if = "Option::is_none")]
581 pub corpus: Option<CorpusReport>,
582}