use serde_json::{Map, Value, json};
use crate::message::ToolCall;
#[derive(Debug, Clone, PartialEq)]
pub struct Field {
pub name: String,
pub kind: Option<String>,
pub title: Option<String>,
pub description: Option<String>,
pub required: bool,
pub choices: Option<Vec<Value>>,
pub default: Option<Value>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct InputRequest {
pub key: String,
params: Value,
pub response: Option<Value>,
pub tool_call: Option<ToolCall>,
pub message: Option<String>,
pub url: Option<String>,
pub fields: Vec<Field>,
}
impl InputRequest {
pub fn new(key: impl Into<String>, params: Value) -> InputRequest {
let s = |k: &str| params.get(k).and_then(Value::as_str).map(str::to_string);
let url = if s("mode").as_deref() == Some("url") {
s("url")
} else {
None
};
let fields = fields_from(params.get("requestedSchema").unwrap_or(&Value::Null));
InputRequest {
key: key.into(),
message: s("message"),
url,
fields,
params,
response: None,
tool_call: None,
}
}
pub fn from_h(data: &Value, tool_call: Option<ToolCall>) -> InputRequest {
let key = data.get("key").and_then(Value::as_str).unwrap_or("");
let mut request = InputRequest::new(
key,
data.get("params").cloned().unwrap_or_else(|| json!({})),
);
request.response = data.get("response").filter(|r| !r.is_null()).cloned();
request.tool_call = tool_call;
request
}
pub fn is_url(&self) -> bool {
self.url.is_some()
}
pub fn is_form(&self) -> bool {
!self.is_url()
}
pub fn answer(&mut self, values: Map<String, Value>) {
self.response = Some(if self.is_url() {
json!({ "action": "accept" })
} else {
json!({ "action": "accept", "content": self.with_defaults(values) })
});
}
fn with_defaults(&self, values: Map<String, Value>) -> Map<String, Value> {
let mut content: Map<String, Value> = self
.fields
.iter()
.filter_map(|f| {
f.default
.clone()
.filter(|d| !d.is_null())
.map(|d| (f.name.clone(), d))
})
.collect();
content.extend(values);
content
}
pub fn decline(&mut self) {
self.response = Some(json!({ "action": "decline" }));
}
pub fn is_answered(&self) -> bool {
self.response.is_some()
}
pub fn to_h(&self) -> Value {
let mut h = json!({ "key": self.key, "params": self.params });
if let Some(response) = &self.response {
h["response"] = response.clone();
}
h
}
}
fn fields_from(schema: &Value) -> Vec<Field> {
let required: Vec<&str> = schema
.get("required")
.and_then(Value::as_array)
.into_iter()
.flatten()
.filter_map(Value::as_str)
.collect();
let Some(properties) = schema.get("properties").and_then(Value::as_object) else {
return Vec::new();
};
properties
.iter()
.map(|(name, property)| {
let s = |k: &str| property.get(k).and_then(Value::as_str).map(str::to_string);
Field {
name: name.clone(),
kind: s("type"),
title: s("title"),
description: s("description"),
required: required.contains(&name.as_str()),
choices: choices(property),
default: property.get("default").cloned(),
}
})
.collect()
}
fn choices(property: &Value) -> Option<Vec<Value>> {
let options: Vec<Value> = match property.get("enum").and_then(Value::as_array) {
Some(values) => values.clone(),
None => property
.get("oneOf")
.or_else(|| property.pointer("/items/anyOf"))
.and_then(Value::as_array)
.into_iter()
.flatten()
.map(|option| option.get("const").cloned().unwrap_or(Value::Null))
.collect(),
};
(!options.is_empty()).then_some(options)
}
#[derive(Debug, Clone, PartialEq)]
pub struct InputState {
pub requests: Vec<InputRequest>,
pub request_state: Option<Value>,
pub task: Option<Value>,
}
impl InputState {
pub fn to_h(&self) -> Value {
let mut h = match &self.task {
Some(Value::Object(task)) => Value::Object(task.clone()),
_ => json!({}),
};
h["requests"] = self.requests.iter().map(InputRequest::to_h).collect();
if let Some(state) = &self.request_state {
h["request_state"] = state.clone();
}
h
}
pub fn from_h(data: &Value) -> InputState {
let requests = data
.get("requests")
.and_then(Value::as_array)
.into_iter()
.flatten()
.map(|r| InputRequest::from_h(r, None))
.collect();
let task = data.get("task").filter(|t| !t.is_null()).map(|task| {
let mut state = json!({ "task": task });
if let Some(answered) = data.get("answered").filter(|a| !a.is_null()) {
state["answered"] = answered.clone();
}
state
});
InputState {
requests,
request_state: data.get("request_state").filter(|s| !s.is_null()).cloned(),
task,
}
}
}
#[derive(Debug, Clone)]
pub struct InputRequiredError {
pub message: String,
pub input: InputState,
}
impl InputRequiredError {
pub(crate) fn new(server: &str, input: InputState) -> InputRequiredError {
let asks: Vec<String> = input
.requests
.iter()
.filter(|r| !r.is_answered())
.map(|r| {
[r.message.as_deref(), r.url.as_deref()]
.into_iter()
.flatten()
.collect::<Vec<_>>()
.join(" ")
})
.collect();
InputRequiredError {
message: format!("{server} needs input from the user: {}", asks.join("; ")),
input,
}
}
pub fn requests(&self) -> Vec<&InputRequest> {
self.input
.requests
.iter()
.filter(|r| !r.is_answered())
.collect()
}
}
impl std::fmt::Display for InputRequiredError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.message)
}
}
impl std::error::Error for InputRequiredError {}
impl From<InputRequiredError> for crate::Error {
fn from(error: InputRequiredError) -> Self {
crate::Error::McpInputRequired(Box::new(error))
}
}