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 = crate::schema_dialect::DEFAULT_SCHEMA_ID;
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<Result<Validator, String>> = OnceLock::new();
21static CALL_TOOL_REQUEST_VALIDATOR: OnceLock<Result<Validator, String>> = OnceLock::new();
22#[derive(Debug)]
24pub enum SchemaError {
25 InvalidListTools(String),
27 InvalidCallToolRequest(String),
29 InvalidCallToolResult(String),
31 UnsupportedSchemaVersion(String),
33}
34
35impl fmt::Display for SchemaError {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 match self {
38 SchemaError::InvalidListTools(message) => write!(f, "invalid tools/list: {message}"),
39 SchemaError::InvalidCallToolRequest(message) => {
40 write!(f, "invalid tools/call request: {message}")
41 }
42 SchemaError::InvalidCallToolResult(message) => {
43 write!(f, "invalid tools/call result: {message}")
44 }
45 SchemaError::UnsupportedSchemaVersion(version) => {
46 write!(f, "unsupported MCP schema version: {version}")
47 }
48 }
49 }
50}
51
52impl std::error::Error for SchemaError {}
53
54pub fn parse_list_tools(
56 payload: JsonValue,
57 config: &SchemaConfig,
58) -> Result<ListToolsResult, SchemaError> {
59 validate_list_tools(&payload, config)?;
60 parse_list_tools_payload(payload)
61}
62
63pub fn parse_call_tool_request(
65 payload: JsonValue,
66 config: &SchemaConfig,
67) -> Result<CallToolRequestParam, SchemaError> {
68 validate_call_tool_request(&payload, config)?;
69 parse_call_tool_request_payload(payload)
70}
71
72pub fn parse_call_tool_result(
74 payload: JsonValue,
75 config: &SchemaConfig,
76) -> Result<CallToolResult, SchemaError> {
77 let _ = schema_json_for(config)?;
78 serde_json::from_value(payload)
79 .map_err(|err| SchemaError::InvalidCallToolResult(err.to_string()))
80}
81
82#[inline(never)]
83fn validate_list_tools(payload: &JsonValue, config: &SchemaConfig) -> Result<(), SchemaError> {
84 let validator = list_tools_validator(config)?;
85 if let Err(error) = validator.validate(payload) {
86 return Err(SchemaError::InvalidListTools(error.to_string()));
87 }
88 Ok(())
89}
90
91#[inline(never)]
92fn validate_call_tool_request(
93 payload: &JsonValue,
94 config: &SchemaConfig,
95) -> Result<(), SchemaError> {
96 let validator = call_tool_request_validator(config)?;
97 if let Err(error) = validator.validate(payload) {
98 return Err(SchemaError::InvalidCallToolRequest(error.to_string()));
99 }
100 Ok(())
101}
102
103fn parse_list_tools_payload(payload: JsonValue) -> Result<ListToolsResult, SchemaError> {
104 match serde_json::from_value(payload) {
105 Ok(result) => Ok(result),
106 Err(err) => Err(SchemaError::InvalidListTools(err.to_string())),
107 }
108}
109
110fn parse_call_tool_request_payload(
111 payload: JsonValue,
112) -> Result<CallToolRequestParam, SchemaError> {
113 match serde_json::from_value(payload) {
114 Ok(result) => Ok(result),
115 Err(err) => Err(SchemaError::InvalidCallToolRequest(err.to_string())),
116 }
117}
118
119fn validator_for_def<'a>(
120 lock: &'a OnceLock<Result<Validator, String>>,
121 schema_json: &str,
122 def_name: &str,
123 wrap_error: impl FnOnce(String) -> SchemaError,
124) -> Result<&'a Validator, SchemaError> {
125 lock.get_or_init(|| build_validator_for_def(schema_json, def_name))
126 .as_ref()
127 .map_err(|message| wrap_error(message.clone()))
128}
129
130#[inline(never)]
131fn list_tools_validator(config: &SchemaConfig) -> Result<&'static Validator, SchemaError> {
132 let schema_json = schema_json_for(config)?;
133 validator_for_def(
134 &LIST_TOOLS_VALIDATOR,
135 schema_json,
136 "ListToolsResult",
137 SchemaError::InvalidListTools,
138 )
139}
140
141#[inline(never)]
142fn call_tool_request_validator(config: &SchemaConfig) -> Result<&'static Validator, SchemaError> {
143 let schema_json = schema_json_for(config)?;
144 validator_for_def(
145 &CALL_TOOL_REQUEST_VALIDATOR,
146 schema_json,
147 "CallToolRequestParams",
148 SchemaError::InvalidCallToolRequest,
149 )
150}
151
152#[inline(never)]
153fn build_validator_for_def(schema_json: &str, def_name: &str) -> Result<Validator, String> {
154 let schema: JsonValue = serde_json::from_str(schema_json)
155 .map_err(|err| format!("failed to parse MCP schema JSON: {err}"))?;
156 let defs = schema
157 .get("$defs")
158 .cloned()
159 .ok_or_else(|| "MCP schema missing $defs".to_string())?;
160 let schema_id = schema_id_for(&schema);
161 let list_tools_schema = serde_json::json!({
162 "$schema": schema_id,
163 "$defs": defs,
164 "$ref": format!("#/$defs/{def_name}")
165 });
166 draft202012::new(&list_tools_schema)
167 .map_err(|err| format!("failed to compile MCP schema: {err}"))
168}
169
170fn schema_id_for(schema: &JsonValue) -> JsonValue {
171 schema
172 .get("$schema")
173 .cloned()
174 .unwrap_or_else(|| JsonValue::String(DEFAULT_SCHEMA_ID.to_string()))
175}
176
177fn schema_json_for(config: &SchemaConfig) -> Result<&'static str, SchemaError> {
178 match &config.version {
179 SchemaVersion::V2025_11_25 => Ok(MCP_SCHEMA),
180 SchemaVersion::Other(value) => Err(SchemaError::UnsupportedSchemaVersion(value.clone())),
181 }
182}
183
184pub fn schema_version_label(version: &SchemaVersion) -> Cow<'_, str> {
185 match version {
186 SchemaVersion::V2025_11_25 => Cow::Borrowed(SUPPORTED_SCHEMA_VERSION),
187 SchemaVersion::Other(value) => Cow::Borrowed(value),
188 }
189}
190
191#[cfg(test)]
192#[path = "../tests/internal/schema_unit_tests.rs"]
193mod tests;