McpServerTool output schema (#35069)
Add an `Output` associated type to `McpServerTool`, so that we can include its schema in `tools/list`. Release Notes: - N/A
This commit is contained in:
parent
15c9da4ea4
commit
af0c909924
3 changed files with 59 additions and 13 deletions
|
@ -124,13 +124,19 @@ enum PermissionToolBehavior {
|
||||||
|
|
||||||
impl McpServerTool for PermissionTool {
|
impl McpServerTool for PermissionTool {
|
||||||
type Input = PermissionToolParams;
|
type Input = PermissionToolParams;
|
||||||
|
type Output = ();
|
||||||
|
|
||||||
const NAME: &'static str = "Confirmation";
|
const NAME: &'static str = "Confirmation";
|
||||||
|
|
||||||
fn description(&self) -> &'static str {
|
fn description(&self) -> &'static str {
|
||||||
"Request permission for tool calls"
|
"Request permission for tool calls"
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run(&self, input: Self::Input, cx: &mut AsyncApp) -> Result<ToolResponse> {
|
async fn run(
|
||||||
|
&self,
|
||||||
|
input: Self::Input,
|
||||||
|
cx: &mut AsyncApp,
|
||||||
|
) -> Result<ToolResponse<Self::Output>> {
|
||||||
let mut thread_rx = self.thread_rx.clone();
|
let mut thread_rx = self.thread_rx.clone();
|
||||||
let Some(thread) = thread_rx.recv().await?.upgrade() else {
|
let Some(thread) = thread_rx.recv().await?.upgrade() else {
|
||||||
anyhow::bail!("Thread closed");
|
anyhow::bail!("Thread closed");
|
||||||
|
@ -178,7 +184,7 @@ impl McpServerTool for PermissionTool {
|
||||||
content: vec![ToolResponseContent::Text {
|
content: vec![ToolResponseContent::Text {
|
||||||
text: serde_json::to_string(&response)?,
|
text: serde_json::to_string(&response)?,
|
||||||
}],
|
}],
|
||||||
structured_content: None,
|
structured_content: (),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -190,6 +196,8 @@ pub struct ReadTool {
|
||||||
|
|
||||||
impl McpServerTool for ReadTool {
|
impl McpServerTool for ReadTool {
|
||||||
type Input = ReadToolParams;
|
type Input = ReadToolParams;
|
||||||
|
type Output = ();
|
||||||
|
|
||||||
const NAME: &'static str = "Read";
|
const NAME: &'static str = "Read";
|
||||||
|
|
||||||
fn description(&self) -> &'static str {
|
fn description(&self) -> &'static str {
|
||||||
|
@ -206,7 +214,11 @@ impl McpServerTool for ReadTool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run(&self, input: Self::Input, cx: &mut AsyncApp) -> Result<ToolResponse> {
|
async fn run(
|
||||||
|
&self,
|
||||||
|
input: Self::Input,
|
||||||
|
cx: &mut AsyncApp,
|
||||||
|
) -> Result<ToolResponse<Self::Output>> {
|
||||||
let mut thread_rx = self.thread_rx.clone();
|
let mut thread_rx = self.thread_rx.clone();
|
||||||
let Some(thread) = thread_rx.recv().await?.upgrade() else {
|
let Some(thread) = thread_rx.recv().await?.upgrade() else {
|
||||||
anyhow::bail!("Thread closed");
|
anyhow::bail!("Thread closed");
|
||||||
|
@ -220,7 +232,7 @@ impl McpServerTool for ReadTool {
|
||||||
|
|
||||||
Ok(ToolResponse {
|
Ok(ToolResponse {
|
||||||
content: vec![ToolResponseContent::Text { text: content }],
|
content: vec![ToolResponseContent::Text { text: content }],
|
||||||
structured_content: None,
|
structured_content: (),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -232,6 +244,8 @@ pub struct EditTool {
|
||||||
|
|
||||||
impl McpServerTool for EditTool {
|
impl McpServerTool for EditTool {
|
||||||
type Input = EditToolParams;
|
type Input = EditToolParams;
|
||||||
|
type Output = ();
|
||||||
|
|
||||||
const NAME: &'static str = "Edit";
|
const NAME: &'static str = "Edit";
|
||||||
|
|
||||||
fn description(&self) -> &'static str {
|
fn description(&self) -> &'static str {
|
||||||
|
@ -248,7 +262,11 @@ impl McpServerTool for EditTool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn run(&self, input: Self::Input, cx: &mut AsyncApp) -> Result<ToolResponse> {
|
async fn run(
|
||||||
|
&self,
|
||||||
|
input: Self::Input,
|
||||||
|
cx: &mut AsyncApp,
|
||||||
|
) -> Result<ToolResponse<Self::Output>> {
|
||||||
let mut thread_rx = self.thread_rx.clone();
|
let mut thread_rx = self.thread_rx.clone();
|
||||||
let Some(thread) = thread_rx.recv().await?.upgrade() else {
|
let Some(thread) = thread_rx.recv().await?.upgrade() else {
|
||||||
anyhow::bail!("Thread closed");
|
anyhow::bail!("Thread closed");
|
||||||
|
@ -273,7 +291,7 @@ impl McpServerTool for EditTool {
|
||||||
|
|
||||||
Ok(ToolResponse {
|
Ok(ToolResponse {
|
||||||
content: vec![],
|
content: vec![],
|
||||||
structured_content: None,
|
structured_content: (),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,8 +41,12 @@ struct RegisteredTool {
|
||||||
handler: ToolHandler,
|
handler: ToolHandler,
|
||||||
}
|
}
|
||||||
|
|
||||||
type ToolHandler =
|
type ToolHandler = Box<
|
||||||
Box<dyn Fn(Option<serde_json::Value>, &mut AsyncApp) -> Task<Result<ToolResponse>>>;
|
dyn Fn(
|
||||||
|
Option<serde_json::Value>,
|
||||||
|
&mut AsyncApp,
|
||||||
|
) -> Task<Result<ToolResponse<serde_json::Value>>>,
|
||||||
|
>;
|
||||||
type RequestHandler = Box<dyn Fn(RequestId, Option<Box<RawValue>>, &App) -> Task<String>>;
|
type RequestHandler = Box<dyn Fn(RequestId, Option<Box<RawValue>>, &App) -> Task<String>>;
|
||||||
|
|
||||||
impl McpServer {
|
impl McpServer {
|
||||||
|
@ -79,11 +83,19 @@ impl McpServer {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_tool<T: McpServerTool + Clone + 'static>(&mut self, tool: T) {
|
pub fn add_tool<T: McpServerTool + Clone + 'static>(&mut self, tool: T) {
|
||||||
|
let output_schema = schemars::schema_for!(T::Output);
|
||||||
|
let unit_schema = schemars::schema_for!(());
|
||||||
|
|
||||||
let registered_tool = RegisteredTool {
|
let registered_tool = RegisteredTool {
|
||||||
tool: Tool {
|
tool: Tool {
|
||||||
name: T::NAME.into(),
|
name: T::NAME.into(),
|
||||||
description: Some(tool.description().into()),
|
description: Some(tool.description().into()),
|
||||||
input_schema: schemars::schema_for!(T::Input).into(),
|
input_schema: schemars::schema_for!(T::Input).into(),
|
||||||
|
output_schema: if output_schema == unit_schema {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(output_schema.into())
|
||||||
|
},
|
||||||
annotations: Some(tool.annotations()),
|
annotations: Some(tool.annotations()),
|
||||||
},
|
},
|
||||||
handler: Box::new({
|
handler: Box::new({
|
||||||
|
@ -96,7 +108,15 @@ impl McpServer {
|
||||||
|
|
||||||
let tool = tool.clone();
|
let tool = tool.clone();
|
||||||
match input {
|
match input {
|
||||||
Ok(input) => cx.spawn(async move |cx| tool.run(input, cx).await),
|
Ok(input) => cx.spawn(async move |cx| {
|
||||||
|
let output = tool.run(input, cx).await?;
|
||||||
|
|
||||||
|
Ok(ToolResponse {
|
||||||
|
content: output.content,
|
||||||
|
structured_content: serde_json::to_value(output.structured_content)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
})
|
||||||
|
}),
|
||||||
Err(err) => Task::ready(Err(err.into())),
|
Err(err) => Task::ready(Err(err.into())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -259,7 +279,11 @@ impl McpServer {
|
||||||
content: result.content,
|
content: result.content,
|
||||||
is_error: Some(false),
|
is_error: Some(false),
|
||||||
meta: None,
|
meta: None,
|
||||||
structured_content: result.structured_content,
|
structured_content: if result.structured_content.is_null() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(result.structured_content)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Err(err) => CallToolResponse {
|
Err(err) => CallToolResponse {
|
||||||
content: vec![ToolResponseContent::Text {
|
content: vec![ToolResponseContent::Text {
|
||||||
|
@ -367,6 +391,8 @@ impl McpServer {
|
||||||
|
|
||||||
pub trait McpServerTool {
|
pub trait McpServerTool {
|
||||||
type Input: DeserializeOwned + JsonSchema;
|
type Input: DeserializeOwned + JsonSchema;
|
||||||
|
type Output: Serialize + JsonSchema;
|
||||||
|
|
||||||
const NAME: &'static str;
|
const NAME: &'static str;
|
||||||
|
|
||||||
fn description(&self) -> &'static str;
|
fn description(&self) -> &'static str;
|
||||||
|
@ -385,12 +411,12 @@ pub trait McpServerTool {
|
||||||
&self,
|
&self,
|
||||||
input: Self::Input,
|
input: Self::Input,
|
||||||
cx: &mut AsyncApp,
|
cx: &mut AsyncApp,
|
||||||
) -> impl Future<Output = Result<ToolResponse>>;
|
) -> impl Future<Output = Result<ToolResponse<Self::Output>>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ToolResponse {
|
pub struct ToolResponse<T> {
|
||||||
pub content: Vec<ToolResponseContent>,
|
pub content: Vec<ToolResponseContent>,
|
||||||
pub structured_content: Option<serde_json::Value>,
|
pub structured_content: T,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
|
|
@ -502,6 +502,8 @@ pub struct Tool {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub input_schema: serde_json::Value,
|
pub input_schema: serde_json::Value,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub output_schema: Option<serde_json::Value>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub annotations: Option<ToolAnnotations>,
|
pub annotations: Option<ToolAnnotations>,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue