use std::future::Future;
use std::marker::PhantomData;
use std::pin::Pin;
use std::sync::Arc;
use serde::de::DeserializeOwned;
use turbomcp_core::{MaybeSend, MaybeSync};
use super::context::RequestContext;
use super::response::IntoToolResponse;
use super::traits::{IntoPromptResponse, IntoResourceResponse};
use super::types::{PromptResult, ResourceResult, ToolResult};
type BoxedToolHandler =
Box<dyn Fn(serde_json::Value) -> Pin<Box<dyn Future<Output = ToolResult>>> + Send + Sync>;
type BoxedToolHandlerWithCtx = Box<
dyn Fn(Arc<RequestContext>, serde_json::Value) -> Pin<Box<dyn Future<Output = ToolResult>>>
+ Send
+ Sync,
>;
type BoxedResourceHandler = Box<
dyn Fn(String) -> Pin<Box<dyn Future<Output = Result<ResourceResult, String>>>> + Send + Sync,
>;
type BoxedResourceHandlerWithCtx = Box<
dyn Fn(
Arc<RequestContext>,
String,
) -> Pin<Box<dyn Future<Output = Result<ResourceResult, String>>>>
+ Send
+ Sync,
>;
type BoxedPromptHandler = Box<
dyn Fn(Option<serde_json::Value>) -> Pin<Box<dyn Future<Output = Result<PromptResult, String>>>>
+ Send
+ Sync,
>;
type BoxedPromptHandlerWithCtx = Box<
dyn Fn(
Arc<RequestContext>,
Option<serde_json::Value>,
) -> Pin<Box<dyn Future<Output = Result<PromptResult, String>>>>
+ Send
+ Sync,
>;
pub trait IntoToolHandler<T, M>: Clone + MaybeSend + MaybeSync + 'static {
fn into_handler(self) -> BoxedToolHandler;
fn schema() -> serde_json::Value;
}
pub struct WithArgs<A>(PhantomData<A>);
impl<F, A, Fut, Res> IntoToolHandler<A, WithArgs<A>> for F
where
F: Fn(A) -> Fut + Clone + MaybeSend + MaybeSync + 'static,
A: DeserializeOwned + schemars::JsonSchema + MaybeSend + 'static,
Fut: Future<Output = Res> + MaybeSend + 'static,
Res: IntoToolResponse + 'static,
{
fn into_handler(self) -> BoxedToolHandler {
Box::new(move |params: serde_json::Value| {
let handler = self.clone();
Box::pin(async move {
match serde_json::from_value::<A>(params) {
Ok(args) => handler(args).await.into_tool_response(),
Err(e) => ToolResult::error(format!("Invalid arguments: {e}")),
}
})
})
}
fn schema() -> serde_json::Value {
let schema = schemars::schema_for!(A);
serde_json::to_value(&schema).unwrap_or_default()
}
}
pub struct RawArgs;
impl<F, Fut, Res> IntoToolHandler<serde_json::Value, RawArgs> for F
where
F: Fn(serde_json::Value) -> Fut + Clone + MaybeSend + MaybeSync + 'static,
Fut: Future<Output = Res> + MaybeSend + 'static,
Res: IntoToolResponse + 'static,
{
fn into_handler(self) -> BoxedToolHandler {
Box::new(move |params: serde_json::Value| {
let handler = self.clone();
Box::pin(async move { handler(params).await.into_tool_response() })
})
}
fn schema() -> serde_json::Value {
serde_json::json!({
"type": "object",
"additionalProperties": true
})
}
}
pub struct NoArgs;
impl<F, Fut, Res> IntoToolHandler<(), NoArgs> for F
where
F: Fn() -> Fut + Clone + MaybeSend + MaybeSync + 'static,
Fut: Future<Output = Res> + MaybeSend + 'static,
Res: IntoToolResponse + 'static,
{
fn into_handler(self) -> BoxedToolHandler {
Box::new(move |_params: serde_json::Value| {
let handler = self.clone();
Box::pin(async move { handler().await.into_tool_response() })
})
}
fn schema() -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {},
"required": []
})
}
}
pub trait IntoToolHandlerWithCtx<T, M>: Clone + MaybeSend + MaybeSync + 'static {
fn into_handler_with_ctx(self) -> BoxedToolHandlerWithCtx;
fn schema() -> serde_json::Value;
}
pub struct WithCtxArgs<A>(PhantomData<A>);
impl<F, A, Fut, Res> IntoToolHandlerWithCtx<A, WithCtxArgs<A>> for F
where
F: Fn(Arc<RequestContext>, A) -> Fut + Clone + MaybeSend + MaybeSync + 'static,
A: DeserializeOwned + schemars::JsonSchema + MaybeSend + 'static,
Fut: Future<Output = Res> + MaybeSend + 'static,
Res: IntoToolResponse + 'static,
{
fn into_handler_with_ctx(self) -> BoxedToolHandlerWithCtx {
Box::new(move |ctx: Arc<RequestContext>, params: serde_json::Value| {
let handler = self.clone();
Box::pin(async move {
match serde_json::from_value::<A>(params) {
Ok(args) => handler(ctx, args).await.into_tool_response(),
Err(e) => ToolResult::error(format!("Invalid arguments: {e}")),
}
})
})
}
fn schema() -> serde_json::Value {
let schema = schemars::schema_for!(A);
serde_json::to_value(&schema).unwrap_or_default()
}
}
pub struct WithCtxRaw;
impl<F, Fut, Res> IntoToolHandlerWithCtx<serde_json::Value, WithCtxRaw> for F
where
F: Fn(Arc<RequestContext>, serde_json::Value) -> Fut + Clone + MaybeSend + MaybeSync + 'static,
Fut: Future<Output = Res> + MaybeSend + 'static,
Res: IntoToolResponse + 'static,
{
fn into_handler_with_ctx(self) -> BoxedToolHandlerWithCtx {
Box::new(move |ctx: Arc<RequestContext>, params: serde_json::Value| {
let handler = self.clone();
Box::pin(async move { handler(ctx, params).await.into_tool_response() })
})
}
fn schema() -> serde_json::Value {
serde_json::json!({
"type": "object",
"additionalProperties": true
})
}
}
pub struct WithCtxOnly;
impl<F, Fut, Res> IntoToolHandlerWithCtx<(), WithCtxOnly> for F
where
F: Fn(Arc<RequestContext>) -> Fut + Clone + MaybeSend + MaybeSync + 'static,
Fut: Future<Output = Res> + MaybeSend + 'static,
Res: IntoToolResponse + 'static,
{
fn into_handler_with_ctx(self) -> BoxedToolHandlerWithCtx {
Box::new(
move |ctx: Arc<RequestContext>, _params: serde_json::Value| {
let handler = self.clone();
Box::pin(async move { handler(ctx).await.into_tool_response() })
},
)
}
fn schema() -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {},
"required": []
})
}
}
pub trait IntoResourceHandler<M>: Clone + MaybeSend + MaybeSync + 'static {
fn into_handler(self) -> BoxedResourceHandler;
}
pub struct ResourceMarker;
impl<F, Fut, Res> IntoResourceHandler<ResourceMarker> for F
where
F: Fn(String) -> Fut + Clone + MaybeSend + MaybeSync + 'static,
Fut: Future<Output = Res> + MaybeSend + 'static,
Res: IntoResourceResponse + 'static,
{
fn into_handler(self) -> BoxedResourceHandler {
Box::new(move |uri: String| {
let handler = self.clone();
Box::pin(async move { handler(uri).await.into_resource_response() })
})
}
}
pub trait IntoResourceHandlerWithCtx<M>: Clone + MaybeSend + MaybeSync + 'static {
fn into_handler_with_ctx(self) -> BoxedResourceHandlerWithCtx;
}
pub struct ResourceMarkerWithCtx;
impl<F, Fut, Res> IntoResourceHandlerWithCtx<ResourceMarkerWithCtx> for F
where
F: Fn(Arc<RequestContext>, String) -> Fut + Clone + MaybeSend + MaybeSync + 'static,
Fut: Future<Output = Res> + MaybeSend + 'static,
Res: IntoResourceResponse + 'static,
{
fn into_handler_with_ctx(self) -> BoxedResourceHandlerWithCtx {
Box::new(move |ctx: Arc<RequestContext>, uri: String| {
let handler = self.clone();
Box::pin(async move { handler(ctx, uri).await.into_resource_response() })
})
}
}
pub trait IntoPromptHandler<T, M>: Clone + MaybeSend + MaybeSync + 'static {
fn into_handler(self) -> BoxedPromptHandler;
fn arguments() -> Vec<turbomcp_core::types::prompts::PromptArgument>;
}
pub struct PromptWithArgs<A>(PhantomData<A>);
impl<F, A, Fut, Res> IntoPromptHandler<A, PromptWithArgs<A>> for F
where
F: Fn(Option<A>) -> Fut + Clone + MaybeSend + MaybeSync + 'static,
A: DeserializeOwned + schemars::JsonSchema + MaybeSend + 'static,
Fut: Future<Output = Res> + MaybeSend + 'static,
Res: IntoPromptResponse + 'static,
{
fn into_handler(self) -> BoxedPromptHandler {
Box::new(move |args: Option<serde_json::Value>| {
let handler = self.clone();
Box::pin(async move {
let parsed_args: Option<A> = match args {
Some(v) => Some(
serde_json::from_value(v).map_err(|e| format!("Invalid arguments: {e}"))?,
),
None => None,
};
handler(parsed_args).await.into_prompt_response()
})
})
}
fn arguments() -> Vec<turbomcp_core::types::prompts::PromptArgument> {
let schema = schemars::schema_for!(A);
let schema_value = serde_json::to_value(&schema).unwrap_or_default();
let mut arguments = Vec::new();
if let Some(properties) = schema_value.get("properties").and_then(|p| p.as_object()) {
let required: Vec<String> = schema_value
.get("required")
.and_then(|r| r.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
for (name, prop) in properties {
let description = prop
.get("description")
.and_then(|d| d.as_str())
.map(String::from);
arguments.push(turbomcp_core::types::prompts::PromptArgument {
name: name.clone(),
description,
required: Some(required.contains(name)),
});
}
}
arguments
}
}
pub struct PromptNoArgs;
impl<F, Fut, Res> IntoPromptHandler<(), PromptNoArgs> for F
where
F: Fn() -> Fut + Clone + MaybeSend + MaybeSync + 'static,
Fut: Future<Output = Res> + MaybeSend + 'static,
Res: IntoPromptResponse + 'static,
{
fn into_handler(self) -> BoxedPromptHandler {
Box::new(move |_args: Option<serde_json::Value>| {
let handler = self.clone();
Box::pin(async move { handler().await.into_prompt_response() })
})
}
fn arguments() -> Vec<turbomcp_core::types::prompts::PromptArgument> {
vec![]
}
}
pub trait IntoPromptHandlerWithCtx<T, M>: Clone + MaybeSend + MaybeSync + 'static {
fn into_handler_with_ctx(self) -> BoxedPromptHandlerWithCtx;
fn arguments() -> Vec<turbomcp_core::types::prompts::PromptArgument>;
}
pub struct PromptWithCtxArgs<A>(PhantomData<A>);
impl<F, A, Fut, Res> IntoPromptHandlerWithCtx<A, PromptWithCtxArgs<A>> for F
where
F: Fn(Arc<RequestContext>, Option<A>) -> Fut + Clone + MaybeSend + MaybeSync + 'static,
A: DeserializeOwned + schemars::JsonSchema + MaybeSend + 'static,
Fut: Future<Output = Res> + MaybeSend + 'static,
Res: IntoPromptResponse + 'static,
{
fn into_handler_with_ctx(self) -> BoxedPromptHandlerWithCtx {
Box::new(
move |ctx: Arc<RequestContext>, args: Option<serde_json::Value>| {
let handler = self.clone();
Box::pin(async move {
let parsed_args: Option<A> = match args {
Some(v) => Some(
serde_json::from_value(v)
.map_err(|e| format!("Invalid arguments: {e}"))?,
),
None => None,
};
handler(ctx, parsed_args).await.into_prompt_response()
})
},
)
}
fn arguments() -> Vec<turbomcp_core::types::prompts::PromptArgument> {
let schema = schemars::schema_for!(A);
let schema_value = serde_json::to_value(&schema).unwrap_or_default();
let mut arguments = Vec::new();
if let Some(properties) = schema_value.get("properties").and_then(|p| p.as_object()) {
let required: Vec<String> = schema_value
.get("required")
.and_then(|r| r.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
for (name, prop) in properties {
let description = prop
.get("description")
.and_then(|d| d.as_str())
.map(String::from);
arguments.push(turbomcp_core::types::prompts::PromptArgument {
name: name.clone(),
description,
required: Some(required.contains(name)),
});
}
}
arguments
}
}
pub struct PromptWithCtxNoArgs;
impl<F, Fut, Res> IntoPromptHandlerWithCtx<(), PromptWithCtxNoArgs> for F
where
F: Fn(Arc<RequestContext>) -> Fut + Clone + MaybeSend + MaybeSync + 'static,
Fut: Future<Output = Res> + MaybeSend + 'static,
Res: IntoPromptResponse + 'static,
{
fn into_handler_with_ctx(self) -> BoxedPromptHandlerWithCtx {
Box::new(
move |ctx: Arc<RequestContext>, _args: Option<serde_json::Value>| {
let handler = self.clone();
Box::pin(async move { handler(ctx).await.into_prompt_response() })
},
)
}
fn arguments() -> Vec<turbomcp_core::types::prompts::PromptArgument> {
vec![]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[allow(dead_code)]
#[derive(serde::Deserialize, schemars::JsonSchema)]
struct TestArgs {
name: String,
}
#[test]
fn test_schema_generation_with_args() {
let schema = schemars::schema_for!(TestArgs);
let schema_value = serde_json::to_value(&schema).unwrap();
assert!(schema_value.get("properties").is_some());
let props = schema_value.get("properties").unwrap().as_object().unwrap();
assert!(props.contains_key("name"));
}
#[test]
fn test_no_args_schema() {
let schema = serde_json::json!({
"type": "object",
"properties": {},
"required": []
});
let obj = schema.as_object().unwrap();
assert_eq!(obj.get("type").unwrap(), "object");
}
#[test]
fn test_context_aware_schema_generation() {
let schema = schemars::schema_for!(TestArgs);
let schema_value = serde_json::to_value(&schema).unwrap();
assert!(schema_value.get("properties").is_some());
let props = schema_value.get("properties").unwrap().as_object().unwrap();
assert!(props.contains_key("name"));
}
#[test]
fn test_context_only_schema() {
let schema = serde_json::json!({
"type": "object",
"properties": {},
"required": []
});
let obj = schema.as_object().unwrap();
assert_eq!(obj.get("type").unwrap(), "object");
assert!(
obj.get("properties")
.unwrap()
.as_object()
.unwrap()
.is_empty()
);
}
#[test]
fn test_request_context_creation() {
let ctx = RequestContext::new();
assert!(!ctx.request_id().is_empty());
assert_eq!(ctx.transport(), Some("wasm-worker"));
}
#[test]
fn test_request_context_with_session() {
let ctx = RequestContext::new()
.with_session_id("session-123")
.with_user_id("user-456");
assert_eq!(ctx.session_id(), Some("session-123"));
assert_eq!(ctx.user_id(), Some("user-456"));
}
}