1use crate::schema::Schema;
9use rustlavel_core::Json;
10
11pub const PROTOCOL_VERSION: &str = "2025-06-18";
13
14pub const SUPPORTED_VERSIONS: [&str; 3] = ["2025-06-18", "2025-03-26", "2024-11-05"];
20
21pub mod method {
23 pub const INITIALIZE: &str = "initialize";
24 pub const INITIALIZED: &str = "notifications/initialized";
25 pub const PING: &str = "ping";
26 pub const TOOLS_LIST: &str = "tools/list";
27 pub const TOOLS_CALL: &str = "tools/call";
28 pub const RESOURCES_LIST: &str = "resources/list";
29 pub const RESOURCES_READ: &str = "resources/read";
30 pub const PROMPTS_LIST: &str = "prompts/list";
31 pub const PROMPTS_GET: &str = "prompts/get";
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct Implementation {
37 pub name: String,
38 pub version: String,
39}
40
41impl Implementation {
42 pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
43 Implementation { name: name.into(), version: version.into() }
44 }
45
46 pub fn to_json(&self) -> Json {
47 Json::object([
48 ("name", Json::from(self.name.clone())),
49 ("version", Json::from(self.version.clone())),
50 ])
51 }
52
53 pub fn from_json(value: &Json) -> Option<Implementation> {
54 Some(Implementation::new(
55 value.get("name")?.as_str()?,
56 value.get("version").and_then(Json::as_str).unwrap_or("unknown"),
57 ))
58 }
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
66pub enum Content {
67 Text(String),
68}
69
70impl Content {
71 pub fn text(text: impl Into<String>) -> Self {
72 Content::Text(text.into())
73 }
74
75 pub fn as_text(&self) -> &str {
76 match self {
77 Content::Text(text) => text,
78 }
79 }
80
81 pub fn to_json(&self) -> Json {
82 match self {
83 Content::Text(text) => Json::object([
84 ("type", Json::from("text")),
85 ("text", Json::from(text.clone())),
86 ]),
87 }
88 }
89
90 pub fn from_json(value: &Json) -> Option<Content> {
91 match value.get("type")?.as_str()? {
92 "text" => Some(Content::text(value.get("text")?.as_str()?)),
93 _ => None,
94 }
95 }
96}
97
98#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct ToolResult {
106 pub content: Vec<Content>,
107 pub is_error: bool,
108}
109
110impl ToolResult {
111 pub fn text(text: impl Into<String>) -> Self {
112 ToolResult { content: vec![Content::text(text)], is_error: false }
113 }
114
115 pub fn error(text: impl Into<String>) -> Self {
116 ToolResult { content: vec![Content::text(text)], is_error: true }
117 }
118
119 pub fn from_value(value: Json) -> Self {
124 match value {
125 Json::String(text) => ToolResult::text(text),
126 other => ToolResult::text(other.to_string()),
127 }
128 }
129
130 pub fn text_content(&self) -> String {
132 self.content.iter().map(Content::as_text).collect::<Vec<_>>().join("\n")
133 }
134
135 pub fn to_json(&self) -> Json {
136 Json::object([
137 ("content", Json::Array(self.content.iter().map(Content::to_json).collect())),
138 ("isError", Json::from(self.is_error)),
139 ])
140 }
141
142 pub fn from_json(value: &Json) -> Option<ToolResult> {
143 let content = value
144 .get("content")?
145 .as_array()?
146 .iter()
147 .filter_map(Content::from_json)
148 .collect();
149 Some(ToolResult {
150 content,
151 is_error: value.get("isError").and_then(Json::as_bool).unwrap_or(false),
152 })
153 }
154}
155
156#[derive(Debug, Clone, PartialEq)]
158pub struct ToolInfo {
159 pub name: String,
160 pub description: String,
161 pub input_schema: Json,
164}
165
166impl ToolInfo {
167 pub fn to_json(&self) -> Json {
168 Json::object([
169 ("name", Json::from(self.name.clone())),
170 ("description", Json::from(self.description.clone())),
171 ("inputSchema", self.input_schema.clone()),
172 ])
173 }
174
175 pub fn from_json(value: &Json) -> Option<ToolInfo> {
176 Some(ToolInfo {
177 name: value.get("name")?.as_str()?.to_string(),
178 description: value.get("description").and_then(Json::as_str).unwrap_or("").to_string(),
179 input_schema: value.get("inputSchema").cloned().unwrap_or(Json::Null),
180 })
181 }
182
183 pub fn describe(name: &str, description: &str, schema: &Schema) -> ToolInfo {
185 ToolInfo {
186 name: name.to_string(),
187 description: description.to_string(),
188 input_schema: schema.to_json(),
189 }
190 }
191}
192
193#[derive(Debug, Clone, PartialEq, Eq)]
195pub struct ResourceInfo {
196 pub uri: String,
197 pub name: String,
198 pub description: Option<String>,
199 pub mime_type: String,
200}
201
202impl ResourceInfo {
203 pub fn to_json(&self) -> Json {
204 let mut pairs = vec![
205 ("uri", Json::from(self.uri.clone())),
206 ("name", Json::from(self.name.clone())),
207 ("mimeType", Json::from(self.mime_type.clone())),
208 ];
209 if let Some(description) = &self.description {
210 pairs.push(("description", Json::from(description.clone())));
211 }
212 Json::object(pairs)
213 }
214
215 pub fn from_json(value: &Json) -> Option<ResourceInfo> {
216 Some(ResourceInfo {
217 uri: value.get("uri")?.as_str()?.to_string(),
218 name: value.get("name").and_then(Json::as_str).unwrap_or("").to_string(),
219 description: value.get("description").and_then(Json::as_str).map(str::to_string),
220 mime_type: value
221 .get("mimeType")
222 .and_then(Json::as_str)
223 .unwrap_or("text/plain")
224 .to_string(),
225 })
226 }
227}
228
229#[derive(Debug, Clone, PartialEq, Eq)]
231pub struct PromptArgument {
232 pub name: String,
233 pub description: String,
234 pub required: bool,
235}
236
237impl PromptArgument {
238 pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
239 PromptArgument { name: name.into(), description: description.into(), required: true }
240 }
241
242 pub fn optional(mut self) -> Self {
243 self.required = false;
244 self
245 }
246
247 pub fn to_json(&self) -> Json {
248 Json::object([
249 ("name", Json::from(self.name.clone())),
250 ("description", Json::from(self.description.clone())),
251 ("required", Json::from(self.required)),
252 ])
253 }
254
255 pub fn from_json(value: &Json) -> Option<PromptArgument> {
256 Some(PromptArgument {
257 name: value.get("name")?.as_str()?.to_string(),
258 description: value.get("description").and_then(Json::as_str).unwrap_or("").to_string(),
259 required: value.get("required").and_then(Json::as_bool).unwrap_or(false),
260 })
261 }
262}
263
264#[derive(Debug, Clone, PartialEq, Eq)]
266pub struct PromptInfo {
267 pub name: String,
268 pub description: String,
269 pub arguments: Vec<PromptArgument>,
270}
271
272impl PromptInfo {
273 pub fn to_json(&self) -> Json {
274 Json::object([
275 ("name", Json::from(self.name.clone())),
276 ("description", Json::from(self.description.clone())),
277 ("arguments", Json::Array(self.arguments.iter().map(PromptArgument::to_json).collect())),
278 ])
279 }
280
281 pub fn from_json(value: &Json) -> Option<PromptInfo> {
282 Some(PromptInfo {
283 name: value.get("name")?.as_str()?.to_string(),
284 description: value.get("description").and_then(Json::as_str).unwrap_or("").to_string(),
285 arguments: value
286 .get("arguments")
287 .and_then(Json::as_array)
288 .unwrap_or(&[])
289 .iter()
290 .filter_map(PromptArgument::from_json)
291 .collect(),
292 })
293 }
294}
295
296#[derive(Debug, Clone, Copy, PartialEq, Eq)]
298pub enum Role {
299 User,
300 Assistant,
301}
302
303impl Role {
304 pub fn as_str(self) -> &'static str {
305 match self {
306 Role::User => "user",
307 Role::Assistant => "assistant",
308 }
309 }
310
311 pub fn parse(value: &str) -> Option<Role> {
312 match value {
313 "user" => Some(Role::User),
314 "assistant" => Some(Role::Assistant),
315 _ => None,
316 }
317 }
318}
319
320#[derive(Debug, Clone, PartialEq, Eq)]
322pub struct PromptMessage {
323 pub role: Role,
324 pub content: Content,
325}
326
327impl PromptMessage {
328 pub fn user(text: impl Into<String>) -> Self {
329 PromptMessage { role: Role::User, content: Content::text(text) }
330 }
331
332 pub fn assistant(text: impl Into<String>) -> Self {
333 PromptMessage { role: Role::Assistant, content: Content::text(text) }
334 }
335
336 pub fn to_json(&self) -> Json {
337 Json::object([
338 ("role", Json::from(self.role.as_str())),
339 ("content", self.content.to_json()),
340 ])
341 }
342
343 pub fn from_json(value: &Json) -> Option<PromptMessage> {
344 Some(PromptMessage {
345 role: Role::parse(value.get("role")?.as_str()?)?,
346 content: Content::from_json(value.get("content")?)?,
347 })
348 }
349}
350
351#[derive(Debug, Clone, PartialEq)]
353pub struct ServerInfo {
354 pub protocol_version: String,
355 pub capabilities: Json,
358 pub server: Implementation,
359 pub instructions: Option<String>,
360}
361
362impl ServerInfo {
363 pub fn to_json(&self) -> Json {
364 let mut pairs = vec![
365 ("protocolVersion", Json::from(self.protocol_version.clone())),
366 ("capabilities", self.capabilities.clone()),
367 ("serverInfo", self.server.to_json()),
368 ];
369 if let Some(instructions) = &self.instructions {
370 pairs.push(("instructions", Json::from(instructions.clone())));
371 }
372 Json::object(pairs)
373 }
374
375 pub fn from_json(value: &Json) -> Option<ServerInfo> {
376 Some(ServerInfo {
377 protocol_version: value.get("protocolVersion")?.as_str()?.to_string(),
378 capabilities: value.get("capabilities").cloned().unwrap_or(Json::Null),
379 server: Implementation::from_json(value.get("serverInfo")?)?,
380 instructions: value.get("instructions").and_then(Json::as_str).map(str::to_string),
381 })
382 }
383
384 pub fn supports(&self, capability: &str) -> bool {
386 self.capabilities.get(capability).is_some()
387 }
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393
394 #[test]
395 fn a_tool_result_uses_the_content_array_the_specification_defines() {
396 let result = ToolResult::text("22°C and clear");
397
398 assert_eq!(
399 result.to_json().to_string(),
400 r#"{"content":[{"text":"22°C and clear","type":"text"}],"isError":false}"#
401 );
402 }
403
404 #[test]
405 fn a_failed_tool_result_sets_is_error() {
406 let result = ToolResult::error("the upstream API is down");
407
408 assert_eq!(result.to_json().get("isError").unwrap().as_bool(), Some(true));
409 assert_eq!(ToolResult::from_json(&result.to_json()).unwrap(), result);
410 }
411
412 #[test]
413 fn a_handler_returning_structured_json_becomes_readable_text() {
414 let result = ToolResult::from_value(Json::object([("temp", Json::from(22))]));
415 assert_eq!(result.text_content(), r#"{"temp":22}"#);
416
417 assert_eq!(ToolResult::from_value(Json::from("plain")).text_content(), "plain");
419 }
420
421 #[test]
422 fn a_tool_descriptor_carries_name_description_and_input_schema() {
423 let schema = Schema::object().string("city", "The city to look up");
424 let info = ToolInfo::describe("weather", "Look up the weather", &schema);
425 let json = info.to_json();
426
427 assert_eq!(json.get("name").unwrap().as_str(), Some("weather"));
428 assert_eq!(json.get("description").unwrap().as_str(), Some("Look up the weather"));
429 assert_eq!(json.get("inputSchema.type").unwrap().as_str(), Some("object"));
430 assert_eq!(ToolInfo::from_json(&json).unwrap(), info);
431 }
432
433 #[test]
434 fn server_info_round_trips_through_the_handshake_shape() {
435 let info = ServerInfo {
436 protocol_version: PROTOCOL_VERSION.to_string(),
437 capabilities: Json::object([("tools", Json::Object(Default::default()))]),
438 server: Implementation::new("shop", "1.2.3"),
439 instructions: Some("Use `orders` to look up an order.".into()),
440 };
441
442 let decoded = ServerInfo::from_json(&info.to_json()).unwrap();
443 assert_eq!(decoded, info);
444 assert!(decoded.supports("tools"));
445 assert!(!decoded.supports("prompts"));
446 }
447
448 #[test]
449 fn prompt_messages_round_trip() {
450 let message = PromptMessage::user("Summarize order 7");
451 let json = message.to_json();
452
453 assert_eq!(json.get("role").unwrap().as_str(), Some("user"));
454 assert_eq!(json.get("content.type").unwrap().as_str(), Some("text"));
455 assert_eq!(PromptMessage::from_json(&json).unwrap(), message);
456 }
457
458 #[test]
459 fn the_protocol_version_is_the_one_this_crate_claims() {
460 assert_eq!(PROTOCOL_VERSION, "2025-06-18");
461 assert_eq!(SUPPORTED_VERSIONS[0], PROTOCOL_VERSION);
462 }
463}