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>;
200pub type ToolNamePredicate = Arc<dyn Fn(&str) -> bool + Send + Sync>;
201
202#[derive(Clone, Debug, Default, Serialize, Deserialize)]
226pub struct AssertionSet {
227 pub rules: Vec<AssertionRule>,
229}
230
231#[derive(Clone, Debug, Serialize, Deserialize)]
233#[serde(tag = "scope", content = "rule", rename_all = "snake_case")]
234pub enum AssertionRule {
235 Response(ResponseAssertion),
237 Sequence(SequenceAssertion),
239}
240
241#[derive(Clone, Debug, Serialize, Deserialize)]
243pub struct ResponseAssertion {
244 pub tool: Option<String>,
246 pub checks: Vec<AssertionCheck>,
248}
249
250#[derive(Clone, Debug, Serialize, Deserialize)]
252pub struct SequenceAssertion {
253 pub checks: Vec<AssertionCheck>,
255}
256
257#[derive(Clone, Debug, Serialize, Deserialize)]
261pub struct AssertionCheck {
262 pub target: AssertionTarget,
264 pub pointer: String,
266 pub expected: JsonValue,
268}
269
270#[derive(Clone, Debug, Serialize, Deserialize)]
272#[serde(rename_all = "snake_case")]
273pub enum AssertionTarget {
274 Input,
276 Output,
278 StructuredOutput,
280 Sequence,
282}
283
284#[derive(Clone)]
286pub struct RunConfig {
287 pub schema: SchemaConfig,
289 pub predicate: Option<ToolPredicate>,
291 pub tool_filter: Option<ToolNamePredicate>,
293 pub assertions: AssertionSet,
295 pub state_machine: StateMachineConfig,
297 pub pre_run_hook: Option<PreRunHook>,
299}
300
301impl RunConfig {
302 pub fn new() -> Self {
307 Self {
308 schema: SchemaConfig::default(),
309 predicate: None,
310 tool_filter: None,
311 assertions: AssertionSet::default(),
312 state_machine: StateMachineConfig::default(),
313 pre_run_hook: None,
314 }
315 }
316
317 pub fn with_schema(mut self, schema: SchemaConfig) -> Self {
319 self.schema = schema;
320 self
321 }
322
323 pub fn with_predicate(mut self, predicate: ToolPredicate) -> Self {
325 self.predicate = Some(predicate);
326 self
327 }
328
329 pub fn with_tool_filter(mut self, predicate: ToolNamePredicate) -> Self {
331 self.tool_filter = Some(predicate);
332 self
333 }
334
335 pub fn with_assertions(mut self, assertions: AssertionSet) -> Self {
337 self.assertions = assertions;
338 self
339 }
340
341 pub fn with_state_machine(mut self, state_machine: StateMachineConfig) -> Self {
343 self.state_machine = state_machine;
344 self
345 }
346
347 pub fn with_pre_run_hook(mut self, hook: PreRunHook) -> Self {
349 self.pre_run_hook = Some(hook);
350 self
351 }
352
353 pub(crate) fn apply_stdio_pre_run_context(&mut self, endpoint: &StdioConfig) {
354 if let Some(hook) = self.pre_run_hook.as_mut() {
355 hook.apply_stdio_context(endpoint);
356 }
357 }
358}
359
360impl Default for RunConfig {
361 fn default() -> Self {
362 Self::new()
363 }
364}
365
366impl fmt::Debug for RunConfig {
367 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
368 f.debug_struct("RunConfig")
369 .field("schema", &self.schema)
370 .field("predicate", &self.predicate.is_some())
371 .field("tool_filter", &self.tool_filter.is_some())
372 .field("assertions", &self.assertions)
373 .field("state_machine", &self.state_machine)
374 .field("pre_run_hook", &self.pre_run_hook.is_some())
375 .finish()
376 }
377}
378
379pub type ToolInvocation = CallToolRequestParam;
381
382#[derive(Clone, Debug, Serialize, Deserialize)]
384#[serde(tag = "kind", rename_all = "snake_case")]
385pub enum TraceEntry {
386 ListTools {
388 #[serde(skip_serializing_if = "Option::is_none")]
390 failure_reason: Option<String>,
391 },
392 ToolCall {
394 invocation: ToolInvocation,
396 #[serde(skip_serializing_if = "Option::is_none")]
398 response: Option<CallToolResult>,
399 #[serde(skip_serializing_if = "Option::is_none")]
401 failure_reason: Option<String>,
402 },
403}
404
405impl TraceEntry {
406 pub fn list_tools() -> Self {
408 Self::ListTools {
409 failure_reason: None,
410 }
411 }
412
413 pub fn list_tools_with_failure(reason: String) -> Self {
415 Self::ListTools {
416 failure_reason: Some(reason),
417 }
418 }
419
420 pub fn tool_call(invocation: ToolInvocation) -> Self {
422 Self::ToolCall {
423 invocation,
424 response: None,
425 failure_reason: None,
426 }
427 }
428
429 pub fn tool_call_with_response(invocation: ToolInvocation, response: CallToolResult) -> Self {
431 Self::ToolCall {
432 invocation,
433 response: Some(response),
434 failure_reason: None,
435 }
436 }
437
438 pub fn as_tool_call(&self) -> Option<(&ToolInvocation, Option<&CallToolResult>)> {
440 match self {
441 TraceEntry::ToolCall {
442 invocation,
443 response,
444 ..
445 } => Some((invocation, response.as_ref())),
446 TraceEntry::ListTools { .. } => None,
447 }
448 }
449}
450
451#[derive(Clone, Debug, Serialize, Deserialize)]
453pub struct MinimizedSequence {
454 pub invocations: Vec<ToolInvocation>,
456}
457
458#[derive(Clone, Debug, Serialize, Deserialize)]
460#[serde(tag = "status", rename_all = "snake_case")]
461pub enum RunOutcome {
462 Success,
464 Failure(RunFailure),
466}
467
468#[derive(Clone, Debug, Serialize, Deserialize)]
470pub struct RunFailure {
471 pub reason: String,
473 pub code: Option<String>,
475 pub details: Option<JsonValue>,
477}
478
479impl RunFailure {
480 pub fn new(reason: impl Into<String>) -> Self {
482 Self {
483 reason: reason.into(),
484 code: None,
485 details: None,
486 }
487 }
488}
489
490#[derive(Clone, Debug, Serialize, Deserialize)]
492pub struct RunWarning {
493 pub code: RunWarningCode,
495 pub message: String,
497 #[serde(skip_serializing_if = "Option::is_none")]
499 pub tool: Option<String>,
500}
501
502#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
504#[serde(rename_all = "snake_case")]
505pub enum RunWarningCode {
506 SchemaUnsupportedKeyword,
507}
508
509#[derive(Clone, Debug, Serialize, Deserialize)]
511pub struct CoverageWarning {
512 pub tool: String,
514 pub reason: CoverageWarningReason,
516}
517
518#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
520#[serde(rename_all = "snake_case")]
521pub enum CoverageWarningReason {
522 MissingString,
523 MissingInteger,
524 MissingNumber,
525 MissingRequiredValue,
526}
527
528#[derive(Clone, Debug, Serialize, Deserialize)]
530pub struct CoverageReport {
531 pub counts: BTreeMap<String, u64>,
533 pub warnings: Vec<CoverageWarning>,
535}
536
537#[derive(Clone, Debug, Serialize, Deserialize)]
539pub struct CorpusReport {
540 pub numbers: Vec<Number>,
542 pub integers: Vec<i64>,
544 pub strings: Vec<String>,
546}
547
548#[derive(Clone, Debug, Serialize, Deserialize)]
550#[serde(tag = "rule", rename_all = "snake_case")]
551pub enum CoverageRule {
552 MinCallsPerTool { min: u64 },
554 NoUncalledTools,
556 PercentCalled { min_percent: f64 },
558}
559
560impl CoverageRule {
561 pub fn min_calls_per_tool(min: u64) -> Self {
563 Self::MinCallsPerTool { min }
564 }
565
566 pub fn no_uncalled_tools() -> Self {
568 Self::NoUncalledTools
569 }
570
571 pub fn percent_called(min_percent: f64) -> Self {
573 Self::PercentCalled { min_percent }
574 }
575}
576
577#[derive(Clone, Debug, Serialize, Deserialize)]
579pub struct RunResult {
580 pub outcome: RunOutcome,
582 pub trace: Vec<TraceEntry>,
584 pub minimized: Option<MinimizedSequence>,
586 pub warnings: Vec<RunWarning>,
588 pub coverage: Option<CoverageReport>,
590 #[serde(skip_serializing_if = "Option::is_none")]
592 pub corpus: Option<CorpusReport>,
593}