Skip to content

IntoCallToolResult implementations over constrain error conversion #440

Description

@Paul-E

Is your feature request related to a problem? Please describe.

The trait implementations on IntoCallToolResult over constrain error results, making use of the tool macro more difficult. The implementations on IntoCallToolResult only permit the error to be converted to either unstructured contents via IntoContents, or protocol level errors via manual conversion into ErrorData.

When using the tool macro, it is not possible to have a return signature of Result<T, E> where:

  • E could become either a protocol error or an application error, depending on its variant
  • E becomes an application error that bears both structured content and unstructured content. Eg display a user friendly message via an unstructured message with user audience, while giving the LLM a detailed error in the structured content.

Describe the solution you'd like

The trait implementation below would work

impl<T:IntoCallToolResult,E:IntoCallToolResult>IntoCallToolResultforResult<T,E>{fninto_call_tool_result(self) -> Result<CallToolResult,crate::ErrorData>{matchself{Ok(value) => value.into_call_tool_result(),Err(error) => error.into_call_tool_result(),}}}

This would require removing some of the other implementations. It would allow errors to produce arbitrary outputs.

One drawback is that this would be over-permissive. Error variants would be able to produce CallToolResult objects that are not errors. Avoiding that would probably require new types and traits, along the lines of:

structSuccessResult{/// All fields in `CallToolResult`, except `is_error`}implFrom<SuccessResult>forCallToolResult{fnfrom(value:SuccessResult) -> Self{todo!()}}structErrorResult{/// All fields in `CallToolResult`, except `is_error`}enumMcpError{ProtocolError(ErrorData),ApplicationError(ErrorResult)}implIntoCallToolResultforMcpError{fninto_call_tool_result(self) -> Result<CallToolResult,crate::ErrorData>{matchself{ProtocolError(protocol_error) => Err(protocol_error),ApplicationError(application_error) => todo!(),}}}implIntoCallToolResult<T:Into<SuccessResult>,E:Into<McpError>>forResult<T,E>{fninto_call_tool_result(self) -> Result<CallToolResult,crate::ErrorData>{matchself{Ok(value) => {// Convert into SuccessResultlet res:SuccessResult = value.into();// Convert into CallToolResultOk(res.into())},Err(error) => {// Convert into McpErrorlet err:McpError = error.into();// this is constrained to produce error results, as rmcp defines the into_call_tool_result// implementation on McpError
error.into_call_tool_result()}}}}

This would ensure the Ok variant only ever produces happy path results, and the Err variant only ever produces error results.

Describe alternatives you've considered

Right now I'm using an approach that defines a Result newtype

pubstructUnstructuredContent<T>(T);pubstructToolResult<T,E>(pubResult<T,E>);impl<T,E>From<Result<T,E>>forToolResult<T,E>{fnfrom(result:Result<T,E>) -> Self{Self(result)}}impl<T,E>IntoCallToolResultforToolResult<UnstructuredContent<T>,E>whereT:IntoContents,E:IntoCallToolResult,{fninto_call_tool_result(self) -> Result<CallToolResult,McpError>{matchself.0{Ok(content) => Ok(CallToolResult::success(content.0.into_contents())),Err(err) => err.into_call_tool_result(),}}}impl<T,E>IntoCallToolResultforToolResult<Json<T>,E>whereT:Serialize + 'static,E:IntoCallToolResult,{fninto_call_tool_result(self) -> Result<CallToolResult,McpError>{matchself.0{Ok(structured_content) => serde_json::to_value(structured_content.0).map_err(|serialize_err| {McpError::internal_error(format!("failed to serialize response: {serialize_err}"),None,)}).map(CallToolResult::structured),Err(err) => err.into_call_tool_result(),}}}

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium: important but non-blocking improvementT-handlerHandler implementation changesenhancementNew feature or requestready for workIssue is well-defined and ready to be picked up

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions