tooltest_core/validation/
listing.rs1use std::fmt;
2use std::future::Future;
3
4use rmcp::model::Tool;
5
6use crate::schema::parse_list_tools;
7use crate::{HttpConfig, SchemaConfig, SchemaError, SessionDriver, SessionError, StdioConfig};
8
9#[derive(Debug)]
11pub enum ListToolsError {
12 Session(SessionError),
14 Schema(SchemaError),
16}
17
18impl fmt::Display for ListToolsError {
19 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20 match self {
21 ListToolsError::Session(error) => write!(f, "session error: {error}"),
22 ListToolsError::Schema(error) => write!(f, "schema error: {error}"),
23 }
24 }
25}
26
27impl std::error::Error for ListToolsError {}
28
29impl From<SessionError> for ListToolsError {
30 fn from(error: SessionError) -> Self {
31 ListToolsError::Session(error)
32 }
33}
34
35impl From<SchemaError> for ListToolsError {
36 fn from(error: SchemaError) -> Self {
37 ListToolsError::Schema(error)
38 }
39}
40
41pub async fn list_tools_http(
57 config: &HttpConfig,
58 schema: &SchemaConfig,
59) -> Result<Vec<Tool>, ListToolsError> {
60 list_tools_with_connector(config, schema, SessionDriver::connect_http).await
61}
62
63pub async fn list_tools_stdio(
79 config: &StdioConfig,
80 schema: &SchemaConfig,
81) -> Result<Vec<Tool>, ListToolsError> {
82 list_tools_with_connector(config, schema, SessionDriver::connect_stdio).await
83}
84
85pub async fn list_tools_with_session(
103 session: &SessionDriver,
104 schema: &SchemaConfig,
105) -> Result<Vec<Tool>, ListToolsError> {
106 let tools = session.list_tools().await?;
107 let payload = serde_json::to_value(&rmcp::model::ListToolsResult {
108 tools,
109 next_cursor: None,
110 meta: None,
111 })
112 .unwrap_or(serde_json::Value::Null);
113 let parsed = parse_list_tools(payload, schema)?;
114 Ok(parsed.tools)
115}
116
117async fn list_tools_with_connector<T, F, Fut>(
118 config: T,
119 schema: &SchemaConfig,
120 connector: F,
121) -> Result<Vec<Tool>, ListToolsError>
122where
123 F: FnOnce(T) -> Fut,
124 Fut: Future<Output = Result<SessionDriver, SessionError>>,
125{
126 let session = connector(config).await?;
127 list_tools_with_session(&session, schema).await
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use std::sync::Arc;
134 use tooltest_test_support::{stub_tool, FaultyListToolsTransport, ListToolsTransport};
135
136 #[test]
137 fn list_tools_error_display_formats() {
138 let error = ListToolsError::Schema(SchemaError::InvalidListTools("boom".to_string()));
139 assert!(format!("{error}").contains("schema error"));
140
141 let io_error = std::io::Error::other("nope");
142 let error = ListToolsError::Session(SessionError::from(io_error));
143 assert!(format!("{error}").contains("session error"));
144 assert!(format!("{error}").contains("transport error"));
145 }
146
147 #[tokio::test]
148 async fn list_tools_with_connector_returns_tools() {
149 let tool = stub_tool("echo");
150 let transport = ListToolsTransport::new(vec![tool.clone()]);
151 let tools = list_tools_with_connector((), &SchemaConfig::default(), move |_| async move {
152 SessionDriver::connect_with_transport(transport).await
153 })
154 .await
155 .expect("list tools");
156 assert_eq!(tools.len(), 1);
157 assert_eq!(tools[0].name.as_ref(), tool.name.as_ref());
158 }
159
160 #[tokio::test]
161 async fn list_tools_with_connector_propagates_session_error() {
162 let error = list_tools_with_connector((), &SchemaConfig::default(), |_| async {
163 Err(SessionError::from(std::io::Error::other("nope")))
164 })
165 .await
166 .expect_err("session error");
167 assert!(error.to_string().contains("session error"));
168 }
169
170 #[tokio::test]
171 async fn list_tools_with_session_propagates_list_error() {
172 let transport = FaultyListToolsTransport::default();
173 let session = SessionDriver::connect_with_transport(transport)
174 .await
175 .expect("connect");
176
177 let error = list_tools_with_session(&session, &SchemaConfig::default())
178 .await
179 .expect_err("list tools error");
180 assert!(error.to_string().contains("session error"));
181 }
182
183 #[tokio::test]
184 async fn list_tools_with_session_reports_schema_error() {
185 let mut tool = stub_tool("echo");
186 tool.output_schema = Some(Arc::new(
187 serde_json::json!({ "type": 5 })
188 .as_object()
189 .cloned()
190 .unwrap(),
191 ));
192 let transport = ListToolsTransport::new(vec![tool]);
193 let session = SessionDriver::connect_with_transport(transport)
194 .await
195 .expect("connect");
196
197 let error = list_tools_with_session(&session, &SchemaConfig::default())
198 .await
199 .expect_err("schema error");
200 assert!(error.to_string().contains("schema error"));
201 }
202
203 #[cfg(coverage)]
204 #[tokio::test]
205 async fn list_tools_http_reports_session_error() {
206 let config = HttpConfig::new("http://127.0.0.1:0/mcp").expect("http config");
207
208 let error = list_tools_http(&config, &SchemaConfig::default())
209 .await
210 .expect_err("list tools error");
211 assert!(error.to_string().contains("session error"));
212 }
213
214 #[cfg(coverage)]
215 #[tokio::test]
216 async fn list_tools_stdio_reports_session_error() {
217 let config = StdioConfig::new("/no/such/tooltest-binary").expect("stdio config");
218
219 let error = list_tools_stdio(&config, &SchemaConfig::default())
220 .await
221 .expect_err("list tools error");
222 assert!(error.to_string().contains("session error"));
223 }
224}