headless_lms_mcp/tools/
mod.rs1pub mod args;
5pub mod catalog;
6mod error;
7mod handler;
8pub mod schema;
9
10use serde::Serialize;
11use serde_json::{Map, Value, json};
12
13use crate::hints::Hint;
14use crate::protocol::context::ClientContext;
15use crate::protocol::progress::ProgressReporter;
16
17pub use error::{ToolError, ToolErrorCode};
18pub use handler::{Tool, ToolHandler, ToolRegistry};
19pub use headless_lms_models::library::oauth::mcp::{McpPrincipal, McpScope};
20
21#[derive(Debug, Clone)]
23pub struct RetryState {
24 pub input_responses: Map<String, Value>,
26}
27
28impl RetryState {
29 pub fn confirmation_action(&self) -> Option<&str> {
31 self.input_responses
32 .get(CONFIRMATION_KEY)?
33 .get("action")?
34 .as_str()
35 }
36}
37
38pub(crate) const CONFIRMATION_KEY: &str = "confirm";
40
41pub struct RequestContext {
43 pub principal: McpPrincipal,
44 pub client: ClientContext,
45 pub tool_name: &'static str,
46 pub retry: Option<RetryState>,
48 pub progress: ProgressReporter,
49}
50
51impl RequestContext {
52 pub fn require_scope(&self, scope: McpScope) -> Result<(), ToolError> {
54 if self.principal.has_scope(scope) {
55 Ok(())
56 } else {
57 Err(ToolError::InsufficientScope { required: scope })
58 }
59 }
60}
61
62#[derive(Debug, Clone, PartialEq)]
65pub struct ToolResult {
66 pub structured: Map<String, Value>,
67 pub hints: Vec<Hint>,
68 pub is_error: bool,
69}
70
71impl ToolResult {
72 pub fn success(value: impl Serialize) -> Self {
74 let structured = match serde_json::to_value(value) {
75 Ok(Value::Object(map)) => map,
76 Ok(other) => Map::from_iter([("result".to_string(), other)]),
77 Err(error) => {
78 Map::from_iter([("serialization_error".to_string(), json!(error.to_string()))])
79 }
80 };
81 Self {
82 structured,
83 hints: Vec::new(),
84 is_error: false,
85 }
86 }
87
88 pub fn error(error: Value) -> Self {
90 Self {
91 structured: Map::from_iter([("error".to_string(), error)]),
92 hints: Vec::new(),
93 is_error: true,
94 }
95 }
96
97 pub fn with_hint(mut self, hint: Hint) -> Self {
99 self.hints.push(hint);
100 self
101 }
102
103 pub fn with_hints(mut self, hints: impl IntoIterator<Item = Hint>) -> Self {
105 self.hints.extend(hints);
106 self
107 }
108
109 pub fn structured_content(&self) -> Value {
111 let mut object = self.structured.clone();
112 if !self.hints.is_empty() {
113 object.insert("hints".into(), json!(self.hints));
114 }
115 Value::Object(object)
116 }
117}
118
119#[derive(Debug, Clone)]
121pub struct ConfirmationRequest {
122 pub message: String,
124 pub url: String,
126 pub elicitation_id: String,
127 pub fallback: ToolResult,
129}
130
131#[derive(Debug, Clone)]
133pub enum ToolOutcome {
134 Complete(ToolResult),
135 NeedsConfirmation(ConfirmationRequest),
136}
137
138impl From<ToolResult> for ToolOutcome {
139 fn from(result: ToolResult) -> Self {
140 ToolOutcome::Complete(result)
141 }
142}
143
144pub(crate) async fn off_worker<T: Send + 'static>(
147 work: impl FnOnce() -> T + Send + 'static,
148) -> Result<T, ToolError> {
149 tokio::task::spawn_blocking(work)
150 .await
151 .map_err(|e| ToolError::Internal(format!("a blocking task failed: {e}")))
152}