1use binaryninjacore_sys::*;
23use serde_json::{json, Value};
24use std::ffi::{c_char, c_void, CStr, CString};
25use std::fmt;
26use std::panic::{self, AssertUnwindSafe};
27use std::ptr;
28
29use crate::binary_view::BinaryView;
30use crate::rc::*;
31use crate::string::{BnString, IntoCStr};
32
33pub use binaryninjacore_sys::BNMcpToolScope as McpToolScope;
34
35bitflags::bitflags! {
36 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
38 pub struct McpToolAnnotations: u32 {
39 const READ_ONLY = BNMcpToolAnnotation::ReadOnlyHint.0;
40 const DESTRUCTIVE = BNMcpToolAnnotation::DestructiveHint.0;
41 const IDEMPOTENT = BNMcpToolAnnotation::IdempotentHint.0;
42 const OPEN_WORLD = BNMcpToolAnnotation::OpenWorldHint.0;
43 }
44}
45
46#[derive(Clone, Debug)]
48pub struct McpToolInfo {
49 pub name: String,
50 pub title: String,
51 pub description: String,
52 pub scope: McpToolScope,
53 pub annotations: McpToolAnnotations,
54 pub output_schema: Option<Value>,
55}
56
57impl McpToolInfo {
58 pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
60 Self {
61 name: name.into(),
62 title: String::new(),
63 description: description.into(),
64 scope: McpToolScope::BinaryViewScope,
65 annotations: McpToolAnnotations::empty(),
66 output_schema: None,
67 }
68 }
69
70 pub fn with_title(mut self, title: impl Into<String>) -> Self {
71 self.title = title.into();
72 self
73 }
74
75 pub fn with_scope(mut self, scope: McpToolScope) -> Self {
76 self.scope = scope;
77 self
78 }
79
80 pub fn with_annotations(mut self, annotations: McpToolAnnotations) -> Self {
81 self.annotations = annotations;
82 self
83 }
84
85 pub fn with_output_schema(mut self, schema: Value) -> Self {
86 self.output_schema = Some(schema);
87 self
88 }
89
90 pub fn with_input_schema(self, input_schema: Value) -> McpToolDefinition {
92 McpToolDefinition {
93 info: self,
94 input_schema,
95 }
96 }
97}
98
99#[derive(Clone, Debug)]
100pub struct McpToolDefinition {
101 pub info: McpToolInfo,
102 pub input_schema: Value,
104}
105
106#[derive(Clone, Debug, PartialEq)]
108pub struct McpToolError {
109 pub code: String,
110 pub message: String,
111 pub details: Option<Value>,
112}
113
114impl McpToolError {
115 pub fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
116 Self {
117 code: code.into(),
118 message: message.into(),
119 details: None,
120 }
121 }
122
123 pub fn invalid_params(message: impl Into<String>) -> Self {
124 Self::new("invalid_params", message)
125 }
126
127 pub fn with_details(mut self, details: Value) -> Self {
128 self.details = Some(details);
129 self
130 }
131}
132
133impl fmt::Display for McpToolError {
134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135 write!(f, "{}: {}", self.code, self.message)
136 }
137}
138
139impl std::error::Error for McpToolError {}
140
141impl From<serde_json::Error> for McpToolError {
143 fn from(error: serde_json::Error) -> Self {
144 Self::invalid_params(error.to_string())
145 }
146}
147
148fn c_text(text: &str) -> CString {
150 CString::new(text.replace('\0', "\u{FFFD}")).unwrap_or_default()
151}
152
153#[derive(Clone, Debug, Default, PartialEq)]
154pub struct McpToolResult {
155 text: Vec<String>,
156 structured_content: Option<Value>,
157 error: Option<McpToolError>,
158 warnings: Vec<(String, String)>,
159}
160
161impl McpToolResult {
162 pub fn text(text: impl Into<String>) -> Self {
163 Self {
164 text: vec![text.into()],
165 ..Self::default()
166 }
167 }
168
169 pub fn structured(content: Value) -> Self {
172 Self {
173 structured_content: Some(content),
174 ..Self::default()
175 }
176 }
177
178 pub fn error(error: McpToolError) -> Self {
179 Self {
180 error: Some(error),
181 ..Self::default()
182 }
183 }
184
185 pub fn with_text(mut self, text: impl Into<String>) -> Self {
186 self.text.push(text.into());
187 self
188 }
189
190 pub fn with_warning(mut self, code: impl Into<String>, message: impl Into<String>) -> Self {
193 self.warnings.push((code.into(), message.into()));
194 self
195 }
196
197 fn apply(&self, result: *mut BNMcpToolResult) {
198 unsafe {
199 if let Some(error) = &self.error {
200 let code = c_text(&error.code);
201 let message = c_text(&error.message);
202 let details = error
203 .details
204 .as_ref()
205 .map(|details| details.to_string().to_cstr());
206 BNSetMcpToolResultError(
207 result,
208 code.as_ptr(),
209 message.as_ptr(),
210 details
211 .as_ref()
212 .map_or(ptr::null(), |details| details.as_ptr()),
213 );
214 } else if let Some(content) = &self.structured_content {
215 let json = content.to_string().to_cstr();
216 if !BNSetMcpToolResultStructuredContent(result, json.as_ptr()) {
217 let code = "internal_error".to_cstr();
218 let message =
219 "The tool produced structured content that is not a JSON object".to_cstr();
220 BNSetMcpToolResultError(result, code.as_ptr(), message.as_ptr(), ptr::null());
221 return;
222 }
223 }
224 for text in &self.text {
225 let text = c_text(text);
226 BNAddMcpToolResultText(result, text.as_ptr());
227 }
228 for (code, message) in &self.warnings {
229 let code = c_text(code);
230 let message = c_text(message);
231 BNAddMcpToolResultWarning(result, code.as_ptr(), message.as_ptr());
232 }
233 }
234 }
235}
236
237impl From<McpToolError> for McpToolResult {
238 fn from(error: McpToolError) -> Self {
239 Self::error(error)
240 }
241}
242
243pub struct McpToolCall {
246 handle: *mut BNMcpToolCall,
247}
248
249impl McpToolCall {
250 pub(crate) unsafe fn ref_from_raw(handle: *mut BNMcpToolCall) -> Ref<Self> {
251 debug_assert!(!handle.is_null());
252 Ref::new(Self { handle })
253 }
254
255 pub fn binary_view(&self) -> Option<Ref<BinaryView>> {
257 let view = unsafe { BNGetMcpToolCallBinaryView(self.handle) };
258 (!view.is_null()).then(|| unsafe { BinaryView::ref_from_raw(view) })
259 }
260
261 pub fn require_binary_view(&self) -> Result<Ref<BinaryView>, McpToolError> {
263 self.binary_view().ok_or_else(|| {
264 McpToolError::new(
265 "no_active_binary_view",
266 "No active Binary Ninja binary view is selected",
267 )
268 })
269 }
270
271 pub fn is_cancelled(&self) -> bool {
272 unsafe { BNIsMcpToolCallCancelled(self.handle) }
273 }
274
275 pub fn report_progress(&self, progress: f64, total: f64, message: &str) {
276 let message = c_text(message);
277 unsafe { BNReportMcpToolCallProgress(self.handle, progress, total, message.as_ptr()) }
278 }
279
280 pub fn parse_address(&self, value: &Value, here: u64) -> Result<u64, String> {
283 self.parse(BNParseMcpToolCallAddress, value, here)
284 }
285
286 pub fn parse_integer(&self, value: &Value, here: u64) -> Result<u64, String> {
289 self.parse(BNParseMcpToolCallInteger, value, here)
290 }
291
292 fn parse(
293 &self,
294 parse: unsafe extern "C" fn(
295 *mut BNMcpToolCall,
296 *const c_char,
297 *mut u64,
298 u64,
299 *mut *mut c_char,
300 ) -> bool,
301 value: &Value,
302 here: u64,
303 ) -> Result<u64, String> {
304 let json = value.to_string().to_cstr();
305 let mut result = 0;
306 let mut error: *mut c_char = ptr::null_mut();
307 if unsafe { parse(self.handle, json.as_ptr(), &mut result, here, &mut error) } {
308 Ok(result)
309 } else if error.is_null() {
310 Err(String::new())
311 } else {
312 Err(unsafe { BnString::into_string(error) })
313 }
314 }
315}
316
317unsafe impl Send for McpToolCall {}
318unsafe impl Sync for McpToolCall {}
319
320impl ToOwned for McpToolCall {
321 type Owned = Ref<Self>;
322
323 fn to_owned(&self) -> Self::Owned {
324 unsafe { RefCountable::inc_ref(self) }
325 }
326}
327
328unsafe impl RefCountable for McpToolCall {
329 unsafe fn inc_ref(handle: &Self) -> Ref<Self> {
330 Ref::new(Self {
331 handle: BNNewMcpToolCallReference(handle.handle),
332 })
333 }
334
335 unsafe fn dec_ref(handle: &Self) {
336 BNFreeMcpToolCall(handle.handle);
337 }
338}
339
340pub trait McpTool: 'static + Send + Sync {
342 fn definition(&self) -> McpToolDefinition;
343
344 fn invoke(&self, call: &McpToolCall, arguments: Value) -> Result<McpToolResult, McpToolError>;
347}
348
349pub fn register_mcp_tool<T: McpTool>(tool: T) -> Option<Ref<CoreMcpTool>> {
352 create_with(tool, BNRegisterMcpTool)
353}
354
355pub(super) fn create_with<T: McpTool>(
356 tool: T,
357 create: unsafe extern "C" fn(
358 *const BNMcpToolDefinition,
359 *const BNMcpToolCallbacks,
360 ) -> *mut BNMcpTool,
361) -> Option<Ref<CoreMcpTool>> {
362 struct ToolContext<T> {
363 name: String,
364 tool: T,
365 }
366
367 extern "C" fn cb_invoke<T: McpTool>(
368 ctxt: *mut c_void,
369 call: *mut BNMcpToolCall,
370 arguments: *const c_char,
371 result: *mut BNMcpToolResult,
372 ) {
373 let context = unsafe { &*(ctxt as *const ToolContext<T>) };
374 let outcome = panic::catch_unwind(AssertUnwindSafe(|| {
375 let call = unsafe { McpToolCall::ref_from_raw(BNNewMcpToolCallReference(call)) };
376 let arguments = unsafe { CStr::from_ptr(arguments) }.to_string_lossy();
377 serde_json::from_str::<Value>(&arguments)
378 .map_err(|error| {
379 McpToolError::invalid_params(format!("Arguments are not valid JSON: {error}"))
380 })
381 .and_then(|arguments| context.tool.invoke(&call, arguments))
382 .unwrap_or_else(McpToolResult::error)
383 }));
384 let tool_result = outcome.unwrap_or_else(|payload| {
385 let message = payload
386 .downcast_ref::<&str>()
387 .copied()
388 .or_else(|| payload.downcast_ref::<String>().map(String::as_str))
389 .unwrap_or("unknown panic");
390 tracing::error!("MCP tool '{}' panicked: {message}", context.name);
391 McpToolError::new("internal_error", format!("The tool panicked: {message}")).into()
392 });
393 tool_result.apply(result);
394 }
395
396 let definition = tool.definition();
397 let name = CString::new(definition.info.name.as_str()).ok()?;
398 let title = CString::new(definition.info.title.as_str()).ok()?;
399 let description = CString::new(definition.info.description.as_str()).ok()?;
400 let input_schema = definition.input_schema.to_string().to_cstr();
401 let output_schema = definition
402 .info
403 .output_schema
404 .as_ref()
405 .map(|schema| schema.to_string().to_cstr());
406 let api_definition = BNMcpToolDefinition {
407 name: name.as_ptr(),
408 title: title.as_ptr(),
409 description: description.as_ptr(),
410 inputSchema: input_schema.as_ptr(),
411 outputSchema: output_schema
412 .as_ref()
413 .map_or(ptr::null(), |schema| schema.as_ptr()),
414 scope: definition.info.scope,
415 annotations: definition.info.annotations.bits(),
416 };
417
418 extern "C" fn cb_free_object<T: McpTool>(ctxt: *mut c_void) {
419 unsafe { drop(Box::from_raw(ctxt as *mut ToolContext<T>)) };
420 }
421
422 let ctxt = Box::into_raw(Box::new(ToolContext {
423 name: definition.info.name.clone(),
424 tool,
425 }));
426 let callbacks = BNMcpToolCallbacks {
427 context: ctxt as *mut c_void,
428 invoke: Some(cb_invoke::<T>),
429 freeObject: Some(cb_free_object::<T>),
430 };
431 let handle = unsafe { create(&api_definition, &callbacks) };
432 if handle.is_null() {
433 unsafe { drop(Box::from_raw(ctxt)) };
434 return None;
435 }
436 Some(unsafe { CoreMcpTool::ref_from_raw(handle) })
437}
438
439#[derive(PartialEq, Eq, Hash)]
441pub struct CoreMcpTool {
442 pub(super) handle: *mut BNMcpTool,
443}
444
445impl CoreMcpTool {
446 pub(crate) unsafe fn from_raw(handle: *mut BNMcpTool) -> Self {
447 debug_assert!(!handle.is_null());
448 Self { handle }
449 }
450
451 pub(crate) unsafe fn ref_from_raw(handle: *mut BNMcpTool) -> Ref<Self> {
452 Ref::new(Self::from_raw(handle))
453 }
454
455 pub fn list() -> Array<CoreMcpTool> {
457 let mut count = 0;
458 let tools = unsafe { BNGetMcpToolList(&mut count) };
459 unsafe { Array::new(tools, count, ()) }
460 }
461
462 pub fn from_name(name: &str) -> Option<Ref<Self>> {
463 let name = name.to_cstr();
464 let handle = unsafe { BNGetMcpToolByName(name.as_ptr()) };
465 (!handle.is_null()).then(|| unsafe { Self::ref_from_raw(handle) })
466 }
467
468 pub fn name(&self) -> String {
469 unsafe { BnString::into_string(BNGetMcpToolName(self.handle)) }
470 }
471
472 pub fn title(&self) -> String {
473 unsafe { BnString::into_string(BNGetMcpToolTitle(self.handle)) }
474 }
475
476 pub fn description(&self) -> String {
477 unsafe { BnString::into_string(BNGetMcpToolDescription(self.handle)) }
478 }
479
480 pub fn input_schema(&self) -> Value {
481 let schema = unsafe { BnString::into_string(BNGetMcpToolInputSchema(self.handle)) };
482 serde_json::from_str(&schema).unwrap_or(Value::Null)
483 }
484
485 pub fn output_schema(&self) -> Option<Value> {
486 let schema = unsafe { BnString::into_string(BNGetMcpToolOutputSchema(self.handle)) };
487 serde_json::from_str(&schema).ok()
488 }
489
490 pub fn scope(&self) -> McpToolScope {
491 unsafe { BNGetMcpToolScope(self.handle) }
492 }
493
494 pub fn annotations(&self) -> McpToolAnnotations {
495 McpToolAnnotations::from_bits_retain(unsafe { BNGetMcpToolAnnotations(self.handle) })
496 }
497}
498
499impl fmt::Debug for CoreMcpTool {
500 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
501 f.debug_struct("CoreMcpTool")
502 .field("name", &self.name())
503 .finish()
504 }
505}
506
507unsafe impl Send for CoreMcpTool {}
508unsafe impl Sync for CoreMcpTool {}
509
510impl ToOwned for CoreMcpTool {
511 type Owned = Ref<Self>;
512
513 fn to_owned(&self) -> Self::Owned {
514 unsafe { RefCountable::inc_ref(self) }
515 }
516}
517
518unsafe impl RefCountable for CoreMcpTool {
519 unsafe fn inc_ref(handle: &Self) -> Ref<Self> {
520 Self::ref_from_raw(BNNewMcpToolReference(handle.handle))
521 }
522
523 unsafe fn dec_ref(handle: &Self) {
524 BNFreeMcpTool(handle.handle);
525 }
526}
527
528impl CoreArrayProvider for CoreMcpTool {
529 type Raw = *mut BNMcpTool;
530 type Context = ();
531 type Wrapped<'a> = Guard<'a, CoreMcpTool>;
532}
533
534unsafe impl CoreArrayProviderInner for CoreMcpTool {
535 unsafe fn free(raw: *mut Self::Raw, count: usize, _context: &Self::Context) {
536 BNFreeMcpToolList(raw, count);
537 }
538
539 unsafe fn wrap_raw<'a>(raw: &'a Self::Raw, context: &'a Self::Context) -> Self::Wrapped<'a> {
540 Guard::new(CoreMcpTool::from_raw(*raw), context)
541 }
542}
543
544#[derive(Clone, Debug, PartialEq, Eq, serde_derive::Deserialize)]
547#[serde(transparent)]
548pub struct McpAddress(pub String);
549
550impl McpAddress {
551 pub fn resolve(&self, call: &McpToolCall) -> Result<u64, McpToolError> {
553 call.parse_address(&Value::String(self.0.clone()), 0)
554 .map_err(McpToolError::invalid_params)
555 }
556}
557
558#[derive(Clone, Debug, PartialEq, Eq, Hash)]
560pub struct McpNonEmptyString(pub String);
561
562impl<'de> serde::Deserialize<'de> for McpNonEmptyString {
563 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
564 let value = <String as serde::Deserialize>::deserialize(deserializer)?;
565 if value.is_empty() {
566 return Err(serde::de::Error::invalid_value(
567 serde::de::Unexpected::Str(&value),
568 &"a non-empty string",
569 ));
570 }
571 Ok(McpNonEmptyString(value))
572 }
573}
574
575#[derive(Clone, Debug, PartialEq, Eq)]
578pub enum McpIntegerExpression {
579 Integer(u64),
580 Expression(String),
581}
582
583impl<'de> serde::Deserialize<'de> for McpIntegerExpression {
584 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
585 struct Visitor;
586
587 impl serde::de::Visitor<'_> for Visitor {
588 type Value = McpIntegerExpression;
589
590 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
591 formatter.write_str("an unsigned integer or an integer expression string")
592 }
593
594 fn visit_u64<E: serde::de::Error>(self, value: u64) -> Result<Self::Value, E> {
595 Ok(McpIntegerExpression::Integer(value))
596 }
597
598 fn visit_i64<E: serde::de::Error>(self, value: i64) -> Result<Self::Value, E> {
599 u64::try_from(value)
600 .map(McpIntegerExpression::Integer)
601 .map_err(|_| E::invalid_value(serde::de::Unexpected::Signed(value), &self))
602 }
603
604 fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
605 Ok(McpIntegerExpression::Expression(value.to_owned()))
606 }
607 }
608
609 deserializer.deserialize_any(Visitor)
610 }
611}
612
613impl McpIntegerExpression {
614 pub fn resolve(&self, call: &McpToolCall) -> Result<u64, McpToolError> {
616 self.resolve_relative_to(call, 0)
617 }
618
619 pub fn resolve_relative_to(&self, call: &McpToolCall, here: u64) -> Result<u64, McpToolError> {
622 match self {
623 McpIntegerExpression::Integer(value) => Ok(*value),
624 McpIntegerExpression::Expression(expression) => call
625 .parse_integer(&Value::String(expression.clone()), here)
626 .map_err(McpToolError::invalid_params),
627 }
628 }
629}
630
631#[cfg(feature = "schemars")]
632pub(super) mod typed {
633 use super::*;
634 use schemars::{json_schema, JsonSchema, Schema, SchemaGenerator};
635 use serde::Deserialize;
636 use std::borrow::Cow;
637 use std::sync::OnceLock;
638
639 impl JsonSchema for McpAddress {
640 fn schema_name() -> Cow<'static, str> {
641 "McpAddress".into()
642 }
643
644 fn inline_schema() -> bool {
645 true
646 }
647
648 fn json_schema(_: &mut SchemaGenerator) -> Schema {
649 json_schema!({ "type": "string" })
650 }
651 }
652
653 impl JsonSchema for McpNonEmptyString {
654 fn schema_name() -> Cow<'static, str> {
655 "McpNonEmptyString".into()
656 }
657
658 fn inline_schema() -> bool {
659 true
660 }
661
662 fn json_schema(_: &mut SchemaGenerator) -> Schema {
663 json_schema!({ "type": "string", "minLength": 1 })
664 }
665 }
666
667 impl JsonSchema for McpIntegerExpression {
668 fn schema_name() -> Cow<'static, str> {
669 "McpIntegerExpression".into()
670 }
671
672 fn inline_schema() -> bool {
673 true
674 }
675
676 fn json_schema(_: &mut SchemaGenerator) -> Schema {
677 json_schema!({ "type": ["integer", "string"], "minimum": 0 })
678 }
679 }
680
681 pub trait TypedMcpTool: 'static + Send + Sync {
690 type Args: serde::de::DeserializeOwned + JsonSchema;
691
692 fn info(&self) -> McpToolInfo;
693
694 fn invoke(
697 &self,
698 call: &McpToolCall,
699 args: Self::Args,
700 ) -> Result<McpToolResult, McpToolError>;
701 }
702
703 struct Parameters {
704 accepted: Option<Vec<String>>,
706 optional: Vec<String>,
708 }
709
710 pub(in crate::mcp) struct TypedAdapter<T> {
711 tool: T,
712 parameters: OnceLock<Parameters>,
713 }
714
715 impl<T: TypedMcpTool> TypedAdapter<T> {
716 pub(in crate::mcp) fn new(tool: T) -> Self {
717 Self {
718 tool,
719 parameters: OnceLock::new(),
720 }
721 }
722
723 fn input_schema() -> Value {
724 let settings = schemars::generate::SchemaSettings::draft2020_12().with(|settings| {
725 settings.inline_subschemas = true;
726 });
727 let mut schema = settings
728 .into_generator()
729 .into_root_schema_for::<T::Args>()
730 .to_value();
731 if let Some(object) = schema.as_object_mut() {
732 object.remove("$schema");
733 object.remove("title");
734 if object.get("type") == Some(&Value::String("object".into()))
735 && !object.contains_key("additionalProperties")
736 {
737 object.insert("additionalProperties".into(), Value::Bool(false));
738 }
739 }
740 schema
741 }
742
743 fn parameters(schema: &Value) -> Parameters {
744 let no_properties = serde_json::Map::new();
745 let properties = schema
746 .get("properties")
747 .and_then(Value::as_object)
748 .unwrap_or(&no_properties);
749 let required = schema.get("required").and_then(Value::as_array);
750 let is_required = |name: &String| {
751 required.is_some_and(|required| required.iter().any(|entry| entry == name))
752 };
753 let accepted = (schema.get("additionalProperties") == Some(&Value::Bool(false)))
754 .then(|| properties.keys().cloned().collect());
755 let optional = properties
756 .keys()
757 .filter(|name| !is_required(name))
758 .cloned()
759 .collect();
760 Parameters { accepted, optional }
761 }
762 }
763
764 struct Arguments<'de>(&'de serde_json::Map<String, Value>);
766
767 impl<'de> serde::Deserializer<'de> for Arguments<'de> {
768 type Error = serde_json::Error;
769
770 fn deserialize_any<V: serde::de::Visitor<'de>>(
771 self,
772 visitor: V,
773 ) -> Result<V::Value, Self::Error> {
774 visitor.visit_map(ArgumentsMap {
775 entries: self.0.iter(),
776 value: None,
777 })
778 }
779
780 serde::forward_to_deserialize_any! {
781 bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string bytes byte_buf
782 option unit unit_struct newtype_struct seq tuple tuple_struct map struct enum identifier
783 ignored_any
784 }
785 }
786
787 struct ArgumentsMap<'de> {
788 entries: serde_json::map::Iter<'de>,
789 value: Option<(&'de String, &'de Value)>,
790 }
791
792 impl<'de> serde::de::MapAccess<'de> for ArgumentsMap<'de> {
793 type Error = serde_json::Error;
794
795 fn next_key_seed<K: serde::de::DeserializeSeed<'de>>(
796 &mut self,
797 seed: K,
798 ) -> Result<Option<K::Value>, Self::Error> {
799 use serde::de::IntoDeserializer;
800
801 let Some((name, value)) = self.entries.next() else {
802 return Ok(None);
803 };
804 self.value = Some((name, value));
805 seed.deserialize(name.as_str().into_deserializer())
806 .map(Some)
807 }
808
809 fn next_value_seed<S: serde::de::DeserializeSeed<'de>>(
810 &mut self,
811 seed: S,
812 ) -> Result<S::Value, Self::Error> {
813 let (name, value) = self
814 .value
815 .take()
816 .expect("next_value_seed called before next_key_seed");
817 seed.deserialize(value).map_err(|error| {
818 serde::de::Error::custom(format!("Invalid parameter '{name}': {error}"))
819 })
820 }
821 }
822
823 impl<T: TypedMcpTool> McpTool for TypedAdapter<T> {
824 fn definition(&self) -> McpToolDefinition {
825 self.tool.info().with_input_schema(Self::input_schema())
826 }
827
828 fn invoke(
829 &self,
830 call: &McpToolCall,
831 mut arguments: Value,
832 ) -> Result<McpToolResult, McpToolError> {
833 let parameters = self
834 .parameters
835 .get_or_init(|| Self::parameters(&Self::input_schema()));
836 let Some(arguments) = arguments.as_object_mut() else {
837 return Err(McpToolError::invalid_params("Expected object arguments"));
838 };
839 if let Some(accepted) = ¶meters.accepted {
840 if let Some(name) = arguments.keys().find(|name| !accepted.contains(name)) {
841 return Err(McpToolError::invalid_params(format!(
842 "Unexpected parameter '{name}'"
843 )));
844 }
845 }
846 arguments.retain(|name, value| !value.is_null() || !parameters.optional.contains(name));
847 let args = T::Args::deserialize(Arguments(arguments))?;
848 self.tool.invoke(call, args)
849 }
850 }
851
852 pub fn register_typed_mcp_tool<T: TypedMcpTool>(tool: T) -> Option<Ref<CoreMcpTool>> {
854 register_mcp_tool(TypedAdapter::new(tool))
855 }
856}
857
858#[cfg(feature = "schemars")]
859pub use typed::{register_typed_mcp_tool, TypedMcpTool};
860
861pub fn empty_input_schema() -> Value {
863 json!({ "type": "object", "additionalProperties": false })
864}