1use std::fmt;
6use std::future::Future;
7use std::pin::Pin;
8use std::task::{Context, Poll};
9
10use http::request::Parts;
11use rmcp::RoleServer;
12use rmcp::model::{CallToolRequestParams, JsonObject};
13use rmcp::service::RequestContext;
14use serde_json::{Map, Value};
15use tokio::runtime::Handle;
16use tokio::task::{JoinError, JoinHandle};
17
18use crate::config::{Hook, HookError, Options, UserIdentity};
19use crate::event::{
20 ClientIdentity, project_headers, protocol_version, redact_headers, transport_session_id,
21};
22use crate::sink::Sink;
23
24#[cfg(test)]
25mod tests;
26
27#[cfg(test)]
28thread_local! {
29 static SNAPSHOTS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
31}
32
33#[cfg(test)]
37pub(crate) fn snapshots_built() -> usize {
38 SNAPSHOTS.get()
39}
40
41#[derive(Clone)]
59pub struct ToolCall {
60 tool_name: String,
61 arguments: Option<JsonObject>,
62 meta: JsonObject,
63 headers: Option<Map<String, Value>>,
64 client: Option<ClientIdentity>,
65 protocol_version: Option<String>,
66 transport_session_id: Option<String>,
67 request_context: RequestContext<RoleServer>,
68}
69
70impl ToolCall {
71 pub(crate) fn new(
74 request: &CallToolRequestParams,
75 context: &RequestContext<RoleServer>,
76 client: Option<&ClientIdentity>,
77 ) -> Self {
78 #[cfg(test)]
79 SNAPSHOTS.set(SNAPSHOTS.get() + 1);
80 let meta: &JsonObject = &context.meta;
81 Self {
82 tool_name: request.name.to_string(),
83 arguments: request.arguments.clone(),
84 meta: meta.clone(),
85 headers: context
86 .extensions
87 .get::<Parts>()
88 .map(|parts| project_headers(&parts.headers)),
89 client: client.cloned(),
90 protocol_version: protocol_version(meta),
91 transport_session_id: transport_session_id(context),
92 request_context: context.clone(),
93 }
94 }
95
96 #[must_use]
98 pub fn tool_name(&self) -> &str {
99 &self.tool_name
100 }
101
102 #[must_use]
105 pub fn arguments(&self) -> Option<&JsonObject> {
106 self.arguments.as_ref()
107 }
108
109 #[must_use]
111 pub fn meta(&self) -> &JsonObject {
112 &self.meta
113 }
114
115 #[must_use]
122 pub fn headers(&self) -> Option<&Map<String, Value>> {
123 self.headers.as_ref()
124 }
125
126 #[must_use]
128 pub fn client_name(&self) -> Option<&str> {
129 self.client.as_ref()?.name.as_deref()
130 }
131
132 #[must_use]
134 pub fn client_version(&self) -> Option<&str> {
135 self.client.as_ref()?.version.as_deref()
136 }
137
138 #[must_use]
140 pub fn protocol_version(&self) -> Option<&str> {
141 self.protocol_version.as_deref()
142 }
143
144 #[must_use]
147 pub fn transport_session_id(&self) -> Option<&str> {
148 self.transport_session_id.as_deref()
149 }
150
151 #[must_use]
154 pub fn request_context(&self) -> &RequestContext<RoleServer> {
155 &self.request_context
156 }
157}
158
159impl fmt::Debug for ToolCall {
160 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161 f.debug_struct("ToolCall")
162 .field("tool_name", &self.tool_name)
163 .field("arguments", &self.arguments)
164 .field("meta", &self.meta)
165 .field("headers", &self.headers.as_ref().map(redact_headers))
166 .field("client", &self.client)
167 .field("protocol_version", &self.protocol_version)
168 .field("transport_session_id", &self.transport_session_id)
169 .field("request_context", &ContextSummary(&self.request_context))
170 .finish()
171 }
172}
173
174struct ContextSummary<'a>(&'a RequestContext<RoleServer>);
177
178impl fmt::Debug for ContextSummary<'_> {
179 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180 f.debug_struct("RequestContext")
181 .field("id", &self.0.id)
182 .field("http_parts", &self.0.extensions.get::<Parts>().is_some())
183 .finish_non_exhaustive()
184 }
185}
186
187type Settled<T> = Result<Option<T>, HookError>;
189
190#[derive(Debug, Default)]
195pub(crate) struct PendingHooks {
196 pub(crate) resolve_session_id: Option<JoinHandle<Settled<String>>>,
198 pub(crate) identify: Option<JoinHandle<Settled<UserIdentity>>>,
200 pub(crate) event_tags: Option<JoinHandle<Settled<Map<String, Value>>>>,
202 pub(crate) event_properties: Option<JoinHandle<Settled<Map<String, Value>>>>,
204}
205
206impl PendingHooks {
207 pub(crate) fn any(&self) -> bool {
209 self.resolve_session_id.is_some()
210 || self.identify.is_some()
211 || self.event_tags.is_some()
212 || self.event_properties.is_some()
213 }
214}
215
216impl Drop for PendingHooks {
217 fn drop(&mut self) {
218 if let Some(task) = &self.resolve_session_id {
220 task.abort();
221 }
222 if let Some(task) = &self.identify {
223 task.abort();
224 }
225 if let Some(task) = &self.event_tags {
226 task.abort();
227 }
228 if let Some(task) = &self.event_properties {
229 task.abort();
230 }
231 }
232}
233
234#[derive(Debug)]
238pub(crate) struct AbortOnDrop<T>(JoinHandle<T>);
239
240impl<T> AbortOnDrop<T> {
241 pub(crate) fn new(task: JoinHandle<T>) -> Self {
243 Self(task)
244 }
245}
246
247impl<T> Future for AbortOnDrop<T> {
248 type Output = Result<T, JoinError>;
249
250 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
251 Pin::new(&mut self.0).poll(cx)
252 }
253}
254
255impl<T> Drop for AbortOnDrop<T> {
256 fn drop(&mut self) {
257 self.0.abort();
259 }
260}
261
262pub(crate) fn fire(
271 options: &Options,
272 hook_mode: bool,
273 sink: &dyn Sink,
274 snapshot: impl FnOnce() -> ToolCall,
275) -> PendingHooks {
276 let resolve_session_id = options.resolve_session_id_hook().filter(|_| hook_mode);
277 let identify = options.identify_hook();
278 let event_tags = options.event_tags_hook();
279 let event_properties = options.event_properties_hook();
280 let set = [
281 ("resolve_session_id", resolve_session_id.is_some()),
282 ("identify", identify.is_some()),
283 ("event_tags", event_tags.is_some()),
284 ("event_properties", event_properties.is_some()),
285 ];
286 if !set.iter().any(|&(_, is_set)| is_set) {
287 return PendingHooks::default();
288 }
289 let Ok(handle) = Handle::try_current() else {
290 for (label, _) in set.into_iter().filter(|&(_, is_set)| is_set) {
291 sink.log(format!(
292 "Warning: {label} hook not started - no tokio runtime"
293 ));
294 }
295 return PendingHooks::default();
296 };
297 let call = snapshot();
298 PendingHooks {
299 resolve_session_id: resolve_session_id.map(|hook| spawn(&handle, hook, &call)),
300 identify: identify.map(|hook| spawn(&handle, hook, &call)),
301 event_tags: event_tags.map(|hook| spawn(&handle, hook, &call)),
302 event_properties: event_properties.map(|hook| spawn(&handle, hook, &call)),
303 }
304}
305
306fn spawn<T: Send + 'static>(
308 handle: &Handle,
309 hook: &Hook<T>,
310 call: &ToolCall,
311) -> JoinHandle<Settled<T>> {
312 handle.spawn(hook.call(call.clone()))
313}