mcp_trace_validator/checks/
mod.rs1mod 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
28type CheckFn = fn(&TraceContext<'_>, &mut FindingSink);
30
31#[derive(Debug, Clone, Copy)]
33pub struct Check {
34 pub id: &'static str,
37 run: CheckFn,
38}
39
40impl Check {
41 #[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#[derive(Debug)]
55pub struct FindingSink {
56 check: &'static str,
57 findings: Vec<Finding>,
58}
59
60impl FindingSink {
61 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
71pub 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: "logging.capability-declared",
251 run: utilities::logging_capability_declared,
252 },
253 Check {
254 id: "completion.capability-declared",
255 run: utilities::completion_capability_declared,
256 },
257 Check {
258 id: "pagination.cursor-opacity",
259 run: utilities::cursor_opacity,
260 },
261];
262
263#[must_use]
265pub fn find(id: &str) -> Option<&'static Check> {
266 ALL.iter().find(|check| check.id == id)
267}
268
269#[cfg(test)]
270#[allow(clippy::unwrap_used)]
271mod tests {
272 use super::*;
273 use mcp_conformance_core::requirement::{Registry, Verification};
274 use std::collections::HashSet;
275
276 #[test]
277 fn check_ids_are_unique() {
278 let mut seen = HashSet::new();
279 for check in ALL {
280 assert!(seen.insert(check.id), "duplicate check id {}", check.id);
281 }
282 }
283
284 #[test]
285 fn builtin_registry_and_check_inventory_cover_each_other_exactly() {
286 let registry = Registry::builtin_2025_11_25().unwrap();
289 let mut referenced = HashSet::new();
290 for requirement in registry.requirements() {
291 if let Verification::Checks { checks } = &requirement.verification {
292 for check in checks {
293 assert!(
294 find(check).is_some(),
295 "{}: references unimplemented check {check}",
296 requirement.id
297 );
298 referenced.insert(check.clone());
299 }
300 }
301 }
302 for check in ALL {
303 assert!(
304 referenced.contains(check.id),
305 "check {} is implemented but referenced by no requirement",
306 check.id
307 );
308 }
309 }
310}