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: "lifecycle.initialize-result-shape",
135        run: lifecycle::initialize_result_shape,
136    },
137    Check {
138        id: "base.meta-key-format",
139        run: base::meta_key_format,
140    },
141    Check {
142        id: "base.result-field",
143        run: base::result_field,
144    },
145    Check {
146        id: "lifecycle.initialize-protocol-version",
147        run: lifecycle::initialize_protocol_version,
148    },
149    Check {
150        id: "lifecycle.negotiated-capabilities-only",
151        run: negotiation::negotiated_capabilities_only,
152    },
153    Check {
154        id: "transport.stdio-server-output-valid",
155        run: transport::stdio_server_output_valid,
156    },
157    Check {
158        id: "transport.stdio-client-input-valid",
159        run: transport::stdio_client_input_valid,
160    },
161    Check {
162        id: "transport.session-id-visible-ascii",
163        run: transport::session_id_visible_ascii,
164    },
165    Check {
166        id: "transport.session-id-echoed",
167        run: transport::session_id_echoed,
168    },
169    Check {
170        id: "transport.protocol-version-header",
171        run: transport::protocol_version_header,
172    },
173    Check {
174        id: "transport.protocol-version-negotiated",
175        run: transport::protocol_version_negotiated,
176    },
177    Check {
178        id: "transport.http-post-single-message",
179        run: transport::http_post_single_message,
180    },
181    Check {
182        id: "transport.client-accept-header",
183        run: transport::client_accept_header,
184    },
185    Check {
186        id: "transport.success-content-type",
187        run: transport::success_content_type,
188    },
189    Check {
190        id: "tools.capability-declared",
191        run: tools::capability_declared,
192    },
193    Check {
194        id: "tools.input-schema-object",
195        run: tools::input_schema_object,
196    },
197    Check {
198        id: "tools.name-length",
199        run: tools::name_length,
200    },
201    Check {
202        id: "tools.name-charset",
203        run: tools::name_charset,
204    },
205    Check {
206        id: "tools.name-unique",
207        run: tools::name_unique,
208    },
209    Check {
210        id: "tools.embedded-resource-capability",
211        run: tools::embedded_resource_capability,
212    },
213    Check {
214        id: "tools.structured-content-text",
215        run: tools::structured_content_text,
216    },
217    Check {
218        id: "tools.output-schema-structured-result",
219        run: tools::output_schema_structured_result,
220    },
221    Check {
222        id: "resources.capability-declared",
223        run: resources::capability_declared,
224    },
225    Check {
226        id: "resources.uri-scheme-rfc3986",
227        run: resources::uri_scheme_rfc3986,
228    },
229    Check {
230        id: "resources.blob-base64",
231        run: resources::blob_base64,
232    },
233    Check {
234        id: "prompts.capability-declared",
235        run: prompts::capability_declared,
236    },
237    Check {
238        id: "prompts.image-content-encoding",
239        run: prompts::image_content_encoding,
240    },
241    Check {
242        id: "prompts.audio-content-encoding",
243        run: prompts::audio_content_encoding,
244    },
245    Check {
246        id: "prompts.embedded-resource-shape",
247        run: prompts::embedded_resource_shape,
248    },
249    Check {
250        id: "prompts.arguments-validated",
251        run: prompts::arguments_validated,
252    },
253    Check {
254        id: "logging.capability-declared",
255        run: utilities::logging_capability_declared,
256    },
257    Check {
258        id: "completion.capability-declared",
259        run: utilities::completion_capability_declared,
260    },
261    Check {
262        id: "pagination.cursor-opacity",
263        run: utilities::cursor_opacity,
264    },
265];
266
267/// Looks up a check by its stable ID.
268#[must_use]
269pub fn find(id: &str) -> Option<&'static Check> {
270    ALL.iter().find(|check| check.id == id)
271}
272
273#[cfg(test)]
274#[allow(clippy::unwrap_used)]
275mod tests {
276    use super::*;
277    use mcp_conformance_core::requirement::{Registry, Verification};
278    use std::collections::HashSet;
279
280    #[test]
281    fn check_ids_are_unique() {
282        let mut seen = HashSet::new();
283        for check in ALL {
284            assert!(seen.insert(check.id), "duplicate check id {}", check.id);
285        }
286    }
287
288    #[test]
289    fn builtin_registry_and_check_inventory_cover_each_other_exactly() {
290        // Every check the registry references exists, and every implemented check is
291        // referenced — drift in either direction is a defect, not a warning.
292        let registry = Registry::builtin_2025_11_25().unwrap();
293        let mut referenced = HashSet::new();
294        for requirement in registry.requirements() {
295            if let Verification::Checks { checks } = &requirement.verification {
296                for check in checks {
297                    assert!(
298                        find(check).is_some(),
299                        "{}: references unimplemented check {check}",
300                        requirement.id
301                    );
302                    referenced.insert(check.clone());
303                }
304            }
305        }
306        for check in ALL {
307            assert!(
308                referenced.contains(check.id),
309                "check {} is implemented but referenced by no requirement",
310                check.id
311            );
312        }
313    }
314}