1use std::borrow::Cow;
2use std::fmt;
3use std::sync::OnceLock;
4
5use jsonschema::{draft202012, Validator};
6use rmcp::model::{CallToolRequestParam, CallToolResult, ListToolsResult};
7use serde_json::Value as JsonValue;
8
9use crate::{SchemaConfig, SchemaVersion};
10
11const SUPPORTED_SCHEMA_VERSION: &str = "2025-11-25";
12const DEFAULT_SCHEMA_ID: &str = "https://json-schema.org/draft/2020-12/schema";
13const MCP_SCHEMA: &str = include_str!(concat!(
17 env!("CARGO_MANIFEST_DIR"),
18 "/resources/mcp-schema-2025-11-25.json"
19));
20static LIST_TOOLS_VALIDATOR: OnceLock<Validator> = OnceLock::new();
21static CALL_TOOL_REQUEST_VALIDATOR: OnceLock<Validator> = OnceLock::new();
22#[derive(Debug)]
24pub enum SchemaError {
25 InvalidListTools(String),
27 InvalidCallToolRequest(String),
29 InvalidCallToolResult(String),
31}
32
33impl fmt::Display for SchemaError {
34 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35 match self {
36 SchemaError::InvalidListTools(message) => write!(f, "invalid tools/list: {message}"),
37 SchemaError::InvalidCallToolRequest(message) => {
38 write!(f, "invalid tools/call request: {message}")
39 }
40 SchemaError::InvalidCallToolResult(message) => {
41 write!(f, "invalid tools/call result: {message}")
42 }
43 }
44 }
45}
46
47impl std::error::Error for SchemaError {}
48
49pub fn parse_list_tools(
51 payload: JsonValue,
52 config: &SchemaConfig,
53) -> Result<ListToolsResult, SchemaError> {
54 let _ = config;
55 validate_list_tools(&payload)?;
56 parse_list_tools_payload(payload)
57}
58
59pub fn parse_call_tool_request(
61 payload: JsonValue,
62 config: &SchemaConfig,
63) -> Result<CallToolRequestParam, SchemaError> {
64 let _ = config;
65 validate_call_tool_request(&payload)?;
66 parse_call_tool_request_payload(payload)
67}
68
69pub fn parse_call_tool_result(
71 payload: JsonValue,
72 config: &SchemaConfig,
73) -> Result<CallToolResult, SchemaError> {
74 let _ = config;
75 serde_json::from_value(payload)
76 .map_err(|err| SchemaError::InvalidCallToolResult(err.to_string()))
77}
78
79#[inline(never)]
80fn validate_list_tools(payload: &JsonValue) -> Result<(), SchemaError> {
81 let validator = list_tools_validator();
82 if let Err(error) = validator.validate(payload) {
83 return Err(SchemaError::InvalidListTools(error.to_string()));
84 }
85 Ok(())
86}
87
88#[inline(never)]
89fn validate_call_tool_request(payload: &JsonValue) -> Result<(), SchemaError> {
90 let validator = call_tool_request_validator();
91 if let Err(error) = validator.validate(payload) {
92 return Err(SchemaError::InvalidCallToolRequest(error.to_string()));
93 }
94 Ok(())
95}
96
97fn parse_list_tools_payload(payload: JsonValue) -> Result<ListToolsResult, SchemaError> {
98 match serde_json::from_value(payload) {
99 Ok(result) => Ok(result),
100 Err(err) => Err(SchemaError::InvalidListTools(err.to_string())),
101 }
102}
103
104fn parse_call_tool_request_payload(
105 payload: JsonValue,
106) -> Result<CallToolRequestParam, SchemaError> {
107 match serde_json::from_value(payload) {
108 Ok(result) => Ok(result),
109 Err(err) => Err(SchemaError::InvalidCallToolRequest(err.to_string())),
110 }
111}
112
113#[inline(never)]
114fn list_tools_validator() -> &'static Validator {
115 if let Some(validator) = LIST_TOOLS_VALIDATOR.get() {
116 return validator;
117 }
118 let validator =
119 build_validator_for_def("ListToolsResult").expect("list tools validator compiles");
120 let _ = LIST_TOOLS_VALIDATOR.set(validator);
121 LIST_TOOLS_VALIDATOR
122 .get()
123 .expect("list tools validator initialized")
124}
125
126#[inline(never)]
127fn call_tool_request_validator() -> &'static Validator {
128 if let Some(validator) = CALL_TOOL_REQUEST_VALIDATOR.get() {
129 return validator;
130 }
131 let validator = build_validator_for_def("CallToolRequestParams")
132 .expect("call tool request validator compiles");
133 let _ = CALL_TOOL_REQUEST_VALIDATOR.set(validator);
134 CALL_TOOL_REQUEST_VALIDATOR
135 .get()
136 .expect("call tool request validator initialized")
137}
138
139#[inline(never)]
140fn build_validator_for_def(def_name: &str) -> Result<Validator, String> {
141 let schema: JsonValue = serde_json::from_str(MCP_SCHEMA).expect("MCP schema JSON parses");
142 let defs = schema
143 .get("$defs")
144 .cloned()
145 .expect("MCP schema defines $defs");
146 let schema_id = schema_id_for(&schema);
147 let list_tools_schema = serde_json::json!({
148 "$schema": schema_id,
149 "$defs": defs,
150 "$ref": format!("#/$defs/{def_name}")
151 });
152 draft202012::new(&list_tools_schema)
153 .map_err(|err| format!("failed to compile MCP schema: {err}"))
154}
155
156fn schema_id_for(schema: &JsonValue) -> JsonValue {
157 schema
158 .get("$schema")
159 .cloned()
160 .unwrap_or_else(|| JsonValue::String(DEFAULT_SCHEMA_ID.to_string()))
161}
162
163pub fn schema_version_label(version: &SchemaVersion) -> Cow<'_, str> {
164 match version {
165 SchemaVersion::V2025_11_25 => Cow::Borrowed(SUPPORTED_SCHEMA_VERSION),
166 SchemaVersion::Other(value) => Cow::Borrowed(value),
167 }
168}
169
170#[cfg(test)]
171#[path = "../tests/internal/schema_unit_tests.rs"]
172mod tests;