Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ pub(crate) fn tool_attribute<T: ToolBase>() -> crate::model::Tool {
input_schema: T::input_schema().unwrap_or_else(schema_for_empty_input),
output_schema: T::output_schema(),
annotations: T::annotations(),
security_schemes: None,
icons: T::icons(),
meta: T::meta(),
}
Expand Down
16 changes: 16 additions & 0 deletions crates/rmcp/src/model/tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ pub struct Tool {
#[serde(skip_serializing_if = "Option::is_none")]
/// Optional additional tool information.
pub annotations: Option<ToolAnnotations>,
/// Optional authentication schemes advertised to clients as `securitySchemes`.
///
/// This extension follows [draft SEP-1488](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1488).
/// Each object describes a scheme, such as `{"type": "noauth"}` or
/// `{"type": "oauth2", "scopes": ["documents.read"]}`. These declarations
/// do not enforce authentication. Servers must validate credentials and permissions.
#[serde(skip_serializing_if = "Option::is_none")]
pub security_schemes: Option<Vec<JsonObject>>,
/// Optional list of icons for the tool
#[serde(skip_serializing_if = "Option::is_none")]
pub icons: Option<Vec<Icon>>,
Expand Down Expand Up @@ -173,6 +181,7 @@ impl Tool {
input_schema: input_schema.into(),
output_schema: None,
annotations: None,
security_schemes: None,
icons: None,
meta: None,
}
Expand All @@ -195,6 +204,7 @@ impl Tool {
input_schema: input_schema.into(),
output_schema: None,
annotations: None,
security_schemes: None,
icons: None,
meta: None,
}
Expand All @@ -218,6 +228,12 @@ impl Tool {
self
}

/// Set the advertised authentication schemes without changing authorization rules.
pub fn with_security_schemes(mut self, security_schemes: Vec<JsonObject>) -> Self {
self.security_schemes = Some(security_schemes);
self
}

/// Set the icons
pub fn with_icons(mut self, icons: Vec<Icon>) -> Self {
self.icons = Some(icons);
Expand Down
65 changes: 65 additions & 0 deletions crates/rmcp/tests/test_tool_security_schemes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use rmcp::model::{JsonObject, ListToolsResult, Tool};
use serde_json::json;

#[test]
fn constructors_omit_unset_security_schemes() {
let tools = [
Tool::default(),
Tool::new("search", "Search documents", JsonObject::new()),
Tool::new_with_raw("search", None, JsonObject::new()),
];

for tool in tools {
assert!(tool.security_schemes.is_none());

let wire = serde_json::to_value(tool).unwrap();
assert!(wire.get("securitySchemes").is_none());
assert!(wire.get("security_schemes").is_none());
}
}

#[test]
fn tools_without_security_schemes_keep_the_existing_wire_shape() {
let wire = json!({
"name": "search",
"description": "Search documents",
"inputSchema": {"type": "object"}
});
let tool: Tool = serde_json::from_value(wire.clone()).unwrap();

assert!(tool.security_schemes.is_none());
assert_eq!(serde_json::to_value(tool).unwrap(), wire);
}

#[test]
fn list_tools_preserves_noauth_and_oauth2_schemes() {
let schemes = json!([
{"type": "noauth"},
{"type": "oauth2", "scopes": ["documents.read", "documents.write"]}
]);
let tool = Tool::new("search", "Search documents", JsonObject::new())
.with_security_schemes(serde_json::from_value(schemes.clone()).unwrap());
let result = ListToolsResult::with_all_items(vec![tool]);

let wire = serde_json::to_value(&result).unwrap();
assert_eq!(wire["tools"][0]["securitySchemes"], schemes);
assert!(wire["tools"][0].get("_meta").is_none());
assert!(wire["tools"][0].get("security_schemes").is_none());

let restored: ListToolsResult = serde_json::from_value(wire).unwrap();
assert_eq!(restored, result);
}

#[test]
fn security_schemes_and_application_metadata_remain_independent() {
let wire = json!({
"name": "search",
"inputSchema": {"type": "object"},
"securitySchemes": [{"type": "oauth2", "scopes": ["documents.read"]}],
"_meta": {"application/example": "retained"}
});
let tool: Tool = serde_json::from_value(wire.clone()).unwrap();

assert_eq!(tool.security_schemes.as_ref().unwrap().len(), 1);
assert_eq!(serde_json::to_value(tool).unwrap(), wire);
}
Loading