Skip to main content

mcp_trace_validator/checks/
mod.rs

1// SPDX-License-Identifier: MIT
2// Copyright 2026 Tom F. (https://github.com/tomtom215)
3
4//! The check inventory.
5//!
6//! A *check* is a pure function from a [`TraceContext`] to findings, registered under a
7//! stable ID that requirement-registry entries reference. The contract for every check:
8//!
9//! - **Falsifiable**: the corpus contains at least one trace it passes and one it fails
10//!   (enforced by the corpus invariant test).
11//! - **Deterministic**: findings are emitted in event order with stable details.
12//! - **Lenient input, precise output**: checks never refuse malformed messages — they
13//!   report them.
14
15mod base;
16mod lifecycle;
17mod negotiation;
18mod prompts;
19mod resources;
20mod support;
21mod tools;
22mod transport;
23mod utilities;
24
25use crate::context::TraceContext;
26use crate::report::Finding;
27
28/// A check function: examines the trace, pushes findings into the sink.
29type CheckFn = fn(&TraceContext<'_>, &mut FindingSink);
30
31/// A registered check.
32#[derive(Debug, Clone, Copy)]
33pub struct Check {
34    /// Stable check identifier referenced by registry entries (e.g.
35    /// `lifecycle.first-interaction-initialize`).
36    pub id: &'static str,
37    run: CheckFn,
38}
39
40impl Check {
41    /// Runs the check, returning its findings tagged with this check's ID.
42    #[must_use]
43    pub fn run(&self, context: &TraceContext<'_>) -> Vec<Finding> {
44        let mut sink = FindingSink {
45            check: self.id,
46            findings: Vec::new(),
47        };
48        (self.run)(context, &mut sink);
49        sink.findings
50    }
51}
52
53/// Collects findings on behalf of one check, stamping each with the check ID.
54#[derive(Debug)]
55pub struct FindingSink {
56    check: &'static str,
57    findings: Vec<Finding>,
58}
59
60impl FindingSink {
61    /// Records a finding at an event (`seq`) with an actionable detail sentence.
62    pub fn push(&mut self, seq: Option<u64>, detail: String) {
63        self.findings.push(Finding {
64            check: self.check.to_owned(),
65            seq,
66            detail,
67        });
68    }
69}
70
71/// Every check implemented by this build, in stable order.
72pub static ALL: &[Check] = &[
73    Check {
74        id: "base.request-id-type",
75        run: base::request_id_type,
76    },
77    Check {
78        id: "base.request-id-not-null",
79        run: base::request_id_not_null,
80    },
81    Check {
82        id: "base.request-id-unique",
83        run: base::request_id_unique,
84    },
85    Check {
86        id: "base.result-id-matches",
87        run: base::result_id_matches,
88    },
89    Check {
90        id: "base.notification-no-id",
91        run: base::notification_no_id,
92    },
93    Check {
94        id: "base.error-shape",
95        run: base::error_shape,
96    },
97    Check {
98        id: "base.error-code-integer",
99        run: base::error_code_integer,
100    },
101    Check {
102        id: "base.jsonrpc-version",
103        run: base::jsonrpc_version,
104    },
105    Check {
106        id: "base.error-id-matches",
107        run: base::error_id_matches,
108    },
109    Check {
110        id: "lifecycle.first-interaction-initialize",
111        run: lifecycle::first_interaction_initialize,
112    },
113    Check {
114        id: "lifecycle.initialize-params",
115        run: lifecycle::initialize_params,
116    },
117    Check {
118        id: "lifecycle.initialized-notification",
119        run: lifecycle::initialized_notification,
120    },
121    Check {
122        id: "lifecycle.client-requests-before-init-response",
123        run: lifecycle::client_requests_before_init_response,
124    },
125    Check {
126        id: "lifecycle.server-requests-before-initialized",
127        run: lifecycle::server_requests_before_initialized,
128    },
129    Check {
130        id: "lifecycle.initialize-result-version",
131        run: lifecycle::initialize_result_version,
132    },
133    Check {
134        id: "base.result-field",
135        run: base::result_field,
136    },
137    Check {
138        id: "lifecycle.initialize-protocol-version",
139        run: lifecycle::initialize_protocol_version,
140    },
141    Check {
142        id: "lifecycle.negotiated-capabilities-only",
143        run: negotiation::negotiated_capabilities_only,
144    },
145    Check {
146        id: "transport.stdio-server-output-valid",
147        run: transport::stdio_server_output_valid,
148    },
149    Check {
150        id: "transport.stdio-client-input-valid",
151        run: transport::stdio_client_input_valid,
152    },
153    Check {
154        id: "transport.session-id-visible-ascii",
155        run: transport::session_id_visible_ascii,
156    },
157    Check {
158        id: "transport.session-id-echoed",
159        run: transport::session_id_echoed,
160    },
161    Check {
162        id: "transport.protocol-version-header",
163        run: transport::protocol_version_header,
164    },
165    Check {
166        id: "transport.protocol-version-negotiated",
167        run: transport::protocol_version_negotiated,
168    },
169    Check {
170        id: "tools.capability-declared",
171        run: tools::capability_declared,
172    },
173    Check {
174        id: "tools.input-schema-object",
175        run: tools::input_schema_object,
176    },
177    Check {
178        id: "tools.name-length",
179        run: tools::name_length,
180    },
181    Check {
182        id: "tools.name-charset",
183        run: tools::name_charset,
184    },
185    Check {
186        id: "tools.name-unique",
187        run: tools::name_unique,
188    },
189    Check {
190        id: "tools.embedded-resource-capability",
191        run: tools::embedded_resource_capability,
192    },
193    Check {
194        id: "tools.structured-content-text",
195        run: tools::structured_content_text,
196    },
197    Check {
198        id: "tools.output-schema-structured-result",
199        run: tools::output_schema_structured_result,
200    },
201    Check {
202        id: "resources.capability-declared",
203        run: resources::capability_declared,
204    },
205    Check {
206        id: "resources.uri-scheme-rfc3986",
207        run: resources::uri_scheme_rfc3986,
208    },
209    Check {
210        id: "resources.blob-base64",
211        run: resources::blob_base64,
212    },
213    Check {
214        id: "prompts.capability-declared",
215        run: prompts::capability_declared,
216    },
217    Check {
218        id: "prompts.image-content-encoding",
219        run: prompts::image_content_encoding,
220    },
221    Check {
222        id: "prompts.audio-content-encoding",
223        run: prompts::audio_content_encoding,
224    },
225    Check {
226        id: "prompts.embedded-resource-shape",
227        run: prompts::embedded_resource_shape,
228    },
229    Check {
230        id: "logging.capability-declared",
231        run: utilities::logging_capability_declared,
232    },
233    Check {
234        id: "completion.capability-declared",
235        run: utilities::completion_capability_declared,
236    },
237    Check {
238        id: "pagination.cursor-opacity",
239        run: utilities::cursor_opacity,
240    },
241];
242
243/// Looks up a check by its stable ID.
244#[must_use]
245pub fn find(id: &str) -> Option<&'static Check> {
246    ALL.iter().find(|check| check.id == id)
247}
248
249#[cfg(test)]
250#[allow(clippy::unwrap_used)]
251mod tests {
252    use super::*;
253    use mcp_conformance_core::requirement::{Registry, Verification};
254    use std::collections::HashSet;
255
256    #[test]
257    fn check_ids_are_unique() {
258        let mut seen = HashSet::new();
259        for check in ALL {
260            assert!(seen.insert(check.id), "duplicate check id {}", check.id);
261        }
262    }
263
264    #[test]
265    fn builtin_registry_and_check_inventory_cover_each_other_exactly() {
266        // Every check the registry references exists, and every implemented check is
267        // referenced — drift in either direction is a defect, not a warning.
268        let registry = Registry::builtin_2025_11_25().unwrap();
269        let mut referenced = HashSet::new();
270        for requirement in registry.requirements() {
271            if let Verification::Checks { checks } = &requirement.verification {
272                for check in checks {
273                    assert!(
274                        find(check).is_some(),
275                        "{}: references unimplemented check {check}",
276                        requirement.id
277                    );
278                    referenced.insert(check.clone());
279                }
280            }
281        }
282        for check in ALL {
283            assert!(
284                referenced.contains(check.id),
285                "check {} is implemented but referenced by no requirement",
286                check.id
287            );
288        }
289    }
290}