fix: remove unnecessary fields from tools' inputSchema (#856)
This commit is contained in:
parent
c330fede90
commit
53e4410d99
10 changed files with 114 additions and 10 deletions
|
|
@ -169,6 +169,8 @@ async fn main() -> anyhow::Result<()> {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The generated tool `inputSchema` is derived from the fields of `T`. The type name and documentation on `T` are ignored; only field names, field types, and field documentation are used.
|
||||||
|
|
||||||
When you need custom server metadata or multiple capabilities (tools + prompts), use explicit `#[tool_handler]`:
|
When you need custom server metadata or multiple capabilities (tools + prompts), use explicit `#[tool_handler]`:
|
||||||
|
|
||||||
```rust,ignore
|
```rust,ignore
|
||||||
|
|
|
||||||
|
|
@ -231,7 +231,7 @@ pub fn tool(attr: TokenStream, input: TokenStream) -> syn::Result<TokenStream> {
|
||||||
if let Some(params_ty) = params_ty {
|
if let Some(params_ty) = params_ty {
|
||||||
// if found, use the Parameters schema
|
// if found, use the Parameters schema
|
||||||
syn::parse2::<Expr>(quote! {
|
syn::parse2::<Expr>(quote! {
|
||||||
rmcp::handler::server::common::schema_for_type::<#params_ty>()
|
rmcp::handler::server::common::schema_for_input::<#params_ty>()
|
||||||
})?
|
})?
|
||||||
} else {
|
} else {
|
||||||
// if not found, use a default empty JSON schema object
|
// if not found, use a default empty JSON schema object
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,36 @@ pub fn schema_for_type<T: JsonSchema + std::any::Any>() -> Arc<JsonObject> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generate a JSON schema for inputSchema (does not need "title" or "description" fields for the top-level object)
|
||||||
|
pub fn schema_for_input<T: JsonSchema + std::any::Any>() -> Arc<JsonObject> {
|
||||||
|
thread_local! {
|
||||||
|
static CACHE_FOR_INPUT: std::sync::RwLock<HashMap<TypeId, Arc<JsonObject>>> = Default::default();
|
||||||
|
};
|
||||||
|
CACHE_FOR_INPUT.with(|cache| {
|
||||||
|
if let Some(schema) = cache
|
||||||
|
.read()
|
||||||
|
.expect("input schema cache lock poisoned")
|
||||||
|
.get(&TypeId::of::<T>())
|
||||||
|
{
|
||||||
|
schema.clone()
|
||||||
|
} else {
|
||||||
|
let mut schema = schema_for_type::<T>().as_ref().clone();
|
||||||
|
|
||||||
|
// Remove unnecessary top-level fields
|
||||||
|
schema.remove("title");
|
||||||
|
schema.remove("description");
|
||||||
|
|
||||||
|
let schema = Arc::new(schema);
|
||||||
|
cache
|
||||||
|
.write()
|
||||||
|
.expect("input schema cache lock poisoned")
|
||||||
|
.insert(TypeId::of::<T>(), schema.clone());
|
||||||
|
|
||||||
|
schema
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: should be updated according to the new specifications
|
// TODO: should be updated according to the new specifications
|
||||||
/// Schema used when input is empty.
|
/// Schema used when input is empty.
|
||||||
pub fn schema_for_empty_input() -> Arc<JsonObject> {
|
pub fn schema_for_empty_input() -> Arc<JsonObject> {
|
||||||
|
|
|
||||||
|
|
@ -133,7 +133,8 @@ pub use tool_traits::{AsyncTool, SyncTool, ToolBase};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
handler::server::{
|
handler::server::{
|
||||||
tool::{CallToolHandler, DynCallToolHandler, ToolCallContext, schema_for_type},
|
common::schema_for_input,
|
||||||
|
tool::{CallToolHandler, DynCallToolHandler, ToolCallContext},
|
||||||
tool_name_validation::validate_and_warn_tool_name,
|
tool_name_validation::validate_and_warn_tool_name,
|
||||||
},
|
},
|
||||||
model::{CallToolResult, Tool, ToolAnnotations},
|
model::{CallToolResult, Tool, ToolAnnotations},
|
||||||
|
|
@ -249,7 +250,7 @@ where
|
||||||
attr: Tool::new(
|
attr: Tool::new(
|
||||||
name.into(),
|
name.into(),
|
||||||
"",
|
"",
|
||||||
schema_for_type::<crate::model::JsonObject>(),
|
schema_for_input::<crate::model::JsonObject>(),
|
||||||
),
|
),
|
||||||
call: self,
|
call: self,
|
||||||
_marker: std::marker::PhantomData,
|
_marker: std::marker::PhantomData,
|
||||||
|
|
@ -286,7 +287,7 @@ where
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
pub fn parameters<T: JsonSchema + 'static>(mut self) -> Self {
|
pub fn parameters<T: JsonSchema + 'static>(mut self) -> Self {
|
||||||
self.attr.input_schema = schema_for_type::<T>();
|
self.attr.input_schema = schema_for_input::<T>();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
pub fn parameters_value(mut self, schema: serde_json::Value) -> Self {
|
pub fn parameters_value(mut self, schema: serde_json::Value) -> Self {
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ use serde::{Deserialize, Serialize};
|
||||||
use crate::{
|
use crate::{
|
||||||
ErrorData,
|
ErrorData,
|
||||||
handler::server::{
|
handler::server::{
|
||||||
common::schema_for_empty_input,
|
common::{schema_for_empty_input, schema_for_input},
|
||||||
tool::{schema_for_output, schema_for_type},
|
tool::schema_for_output,
|
||||||
wrapper::{Json, Parameters},
|
wrapper::{Json, Parameters},
|
||||||
},
|
},
|
||||||
model::{Icon, JsonObject, Meta, ToolAnnotations, ToolExecution},
|
model::{Icon, JsonObject, Meta, ToolAnnotations, ToolExecution},
|
||||||
|
|
@ -49,7 +49,7 @@ pub trait ToolBase {
|
||||||
/// If the tool does not have any parameters, you should override this methods to return [`None`],
|
/// If the tool does not have any parameters, you should override this methods to return [`None`],
|
||||||
/// and when invoked, the parameter will get default values.
|
/// and when invoked, the parameter will get default values.
|
||||||
fn input_schema() -> Option<Arc<JsonObject>> {
|
fn input_schema() -> Option<Arc<JsonObject>> {
|
||||||
Some(schema_for_type::<Parameters<Self::Parameter>>())
|
Some(schema_for_input::<Parameters<Self::Parameter>>())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Json schema for tool output.
|
/// Json schema for tool output.
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ use serde::de::DeserializeOwned;
|
||||||
|
|
||||||
use super::common::{AsRequestContext, FromContextPart};
|
use super::common::{AsRequestContext, FromContextPart};
|
||||||
pub use super::{
|
pub use super::{
|
||||||
common::{Extension, RequestId, schema_for_output, schema_for_type},
|
common::{Extension, RequestId, schema_for_input, schema_for_output, schema_for_type},
|
||||||
router::tool::{ToolRoute, ToolRouter},
|
router::tool::{ToolRoute, ToolRouter},
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
|
||||||
|
|
@ -330,7 +330,7 @@ impl Tool {
|
||||||
/// Set the input schema using a type that implements JsonSchema
|
/// Set the input schema using a type that implements JsonSchema
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
pub fn with_input_schema<T: JsonSchema + 'static>(mut self) -> Self {
|
pub fn with_input_schema<T: JsonSchema + 'static>(mut self) -> Self {
|
||||||
self.input_schema = crate::handler::server::tool::schema_for_type::<T>();
|
self.input_schema = crate::handler::server::tool::schema_for_input::<T>();
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,6 @@ fn expected_schema() -> serde_json::Value {
|
||||||
"required": [
|
"required": [
|
||||||
"messages"
|
"messages"
|
||||||
],
|
],
|
||||||
"title": "ChatRequest",
|
|
||||||
"type": "object"
|
"type": "object"
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
40
crates/rmcp/tests/test_list_tools_result.rs
Normal file
40
crates/rmcp/tests/test_list_tools_result.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
#![cfg(all(feature = "server", feature = "macros", not(feature = "local")))]
|
||||||
|
|
||||||
|
use rmcp::{
|
||||||
|
handler::server::wrapper::Parameters,
|
||||||
|
model::{ListToolsResult, NumberOrString, ServerJsonRpcMessage, ServerResult},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Parameters for adding two numbers.
|
||||||
|
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
||||||
|
struct AddRequest {
|
||||||
|
/// The left-hand number.
|
||||||
|
a: f64,
|
||||||
|
/// The right-hand number.
|
||||||
|
b: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add two numbers.
|
||||||
|
#[rmcp::tool]
|
||||||
|
fn add(Parameters(AddRequest { a, b }): Parameters<AddRequest>) -> String {
|
||||||
|
(a + b).to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn list_tools_result_matches_expected_json() {
|
||||||
|
let expected_json = std::fs::read("tests/test_list_tools_result/list_tools_result.json")
|
||||||
|
.expect("missing expected list tools result JSON fixture");
|
||||||
|
let expected: serde_json::Value =
|
||||||
|
serde_json::from_slice(&expected_json).expect("invalid expected JSON fixture");
|
||||||
|
|
||||||
|
assert_eq!(add(Parameters(AddRequest { a: 1.0, b: 2.0 })), "3");
|
||||||
|
|
||||||
|
let result = ListToolsResult::with_all_items(vec![add_tool_attr()]);
|
||||||
|
let response = ServerJsonRpcMessage::response(
|
||||||
|
ServerResult::ListToolsResult(result),
|
||||||
|
NumberOrString::Number(2),
|
||||||
|
);
|
||||||
|
|
||||||
|
let actual = serde_json::to_value(response).expect("failed to serialize list tools response");
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
{
|
||||||
|
"result": {
|
||||||
|
"tools": [
|
||||||
|
{
|
||||||
|
"name": "add",
|
||||||
|
"description": "Add two numbers.",
|
||||||
|
"inputSchema": {
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"a": {
|
||||||
|
"description": "The left-hand number.",
|
||||||
|
"format": "double",
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"b": {
|
||||||
|
"description": "The right-hand number.",
|
||||||
|
"format": "double",
|
||||||
|
"type": "number"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"a",
|
||||||
|
"b"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"id": 2
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue