monoloop_loop/transaction/mcp/
handler.rs1use crate::transaction::dispatcher::{DispatchOutcome, DispatchRequest, TransactionToolDispatcher};
4use crate::transaction::resolved_tools::ResolvedToolSet;
5use monoloop_contracts::{
6 CanonicalToolResultOutcome, ExchangeId, ToolActionId, ToolName, TransactionId,
7};
8use rmcp::{
9 model::{
10 CallToolRequestParams, CallToolResponse, CallToolResult, ContentBlock, ErrorCode,
11 ListToolsResult, PaginatedRequestParams, ServerCapabilities, ServerInfo, Tool,
12 },
13 service::RequestContext,
14 ErrorData as McpError, RoleServer, ServerHandler,
15};
16use std::sync::atomic::{AtomicU8, Ordering};
17use std::sync::Arc;
18
19const STATE_ACTIVE: u8 = 1;
20
21pub fn tool_definitions_from_resolved(tools: &ResolvedToolSet) -> Vec<Tool> {
23 tools
24 .specs()
25 .into_iter()
26 .map(|spec| {
27 let schema = spec.input_schema.as_value().clone();
28 let obj = match schema {
29 serde_json::Value::Object(m) => Arc::new(m),
30 _ => Arc::new(serde_json::Map::new()),
31 };
32 Tool::new(
33 spec.name.as_str().to_string(),
34 spec.description.clone(),
35 obj,
36 )
37 })
38 .collect()
39}
40
41#[derive(Clone)]
43pub struct TransactionMcpHandler {
44 state: Arc<AtomicU8>,
45 tools: ResolvedToolSet,
46 dispatcher: Arc<TransactionToolDispatcher>,
47 transaction_id: TransactionId,
48 exchange_id: ExchangeId,
49 tool_defs: Vec<Tool>,
50 transaction_deadline: std::time::Instant,
52}
53
54impl TransactionMcpHandler {
55 pub fn new(
57 state: Arc<AtomicU8>,
58 tools: ResolvedToolSet,
59 dispatcher: Arc<TransactionToolDispatcher>,
60 transaction_id: TransactionId,
61 exchange_id: ExchangeId,
62 transaction_deadline: std::time::Instant,
63 ) -> Self {
64 let tool_defs = tool_definitions_from_resolved(&tools);
65 Self {
66 state,
67 tools,
68 dispatcher,
69 transaction_id,
70 exchange_id,
71 tool_defs,
72 transaction_deadline,
73 }
74 }
75
76 fn ensure_active(&self) -> Result<(), McpError> {
77 if self.state.load(Ordering::SeqCst) == STATE_ACTIVE {
78 Ok(())
79 } else {
80 Err(McpError::new(
81 ErrorCode::INVALID_REQUEST,
82 "MCP capability is not active",
83 None,
84 ))
85 }
86 }
87
88 pub fn list_tool_defs(&self) -> Result<Vec<Tool>, McpError> {
90 self.ensure_active()?;
91 Ok(self.tool_defs.clone())
92 }
93
94 pub async fn call_tool_direct(
96 &self,
97 name: &str,
98 arguments: Option<serde_json::Map<String, serde_json::Value>>,
99 ) -> Result<CallToolResult, McpError> {
100 self.ensure_active()?;
101 let tool_name = ToolName::try_new(name)
102 .map_err(|_| McpError::new(ErrorCode::INVALID_PARAMS, "invalid tool name", None))?;
103 if !self.tools.contains_name(&tool_name) {
104 return Err(McpError::new(
105 ErrorCode::METHOD_NOT_FOUND,
106 "tool not in resolved set",
107 None,
108 ));
109 }
110 let args = arguments.unwrap_or_default();
111 let arguments_json = serde_json::Value::Object(args).to_string();
112 let action = ToolActionId::new(format!("mcp:{}:{}", self.transaction_id.as_uuid(), name));
113 let outcome = self
114 .dispatcher
115 .dispatch(DispatchRequest {
116 exchange_id: self.exchange_id,
117 tool_action_id: action,
118 tool_name,
119 provider_tool_call_id: format!("mcp-{}", uuid::Uuid::new_v4()),
120 request_ordinal: 0,
121 arguments_json,
122 transaction_deadline: self.transaction_deadline,
123 })
124 .await;
125 map_dispatch_to_call_result(outcome)
126 }
127}
128
129impl ServerHandler for TransactionMcpHandler {
130 fn get_info(&self) -> ServerInfo {
131 ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
132 }
133
134 async fn list_tools(
135 &self,
136 _request: Option<PaginatedRequestParams>,
137 _context: RequestContext<RoleServer>,
138 ) -> Result<ListToolsResult, McpError> {
139 self.ensure_active()?;
140 Ok(ListToolsResult::with_all_items(self.tool_defs.clone()))
142 }
143
144 async fn call_tool(
145 &self,
146 request: CallToolRequestParams,
147 _context: RequestContext<RoleServer>,
148 ) -> Result<CallToolResponse, McpError> {
149 let name = request.name.as_ref();
150 let args = request.arguments;
151 let result = self.call_tool_direct(name, args).await?;
152 Ok(CallToolResponse::Complete(result))
153 }
154
155 fn get_tool(&self, name: &str) -> Option<Tool> {
156 if self.state.load(Ordering::SeqCst) != STATE_ACTIVE {
157 return None;
158 }
159 self.tool_defs.iter().find(|t| t.name == name).cloned()
160 }
161}
162
163fn map_dispatch_to_call_result(outcome: DispatchOutcome) -> Result<CallToolResult, McpError> {
164 match outcome {
165 DispatchOutcome::Canonical { result, .. } => match result.outcome {
166 CanonicalToolResultOutcome::Succeeded(output) => {
167 let text = match output {
168 monoloop_contracts::CanonicalToolOutput::Json(v) => {
169 serde_json::to_string(&v).unwrap_or_else(|_| "{}".into())
170 }
171 monoloop_contracts::CanonicalToolOutput::Text(t) => t,
172 };
173 Ok(CallToolResult::success(vec![ContentBlock::text(text)]))
174 }
175 CanonicalToolResultOutcome::DomainFailed(err) => {
176 Ok(CallToolResult::error(vec![ContentBlock::text(format!(
177 "{}: {}",
178 err.code, err.message
179 ))]))
180 }
181 },
182 DispatchOutcome::Rejected { code, message, .. } => {
183 Ok(CallToolResult::error(vec![ContentBlock::text(format!(
185 "{code}: {message}"
186 ))]))
187 }
188 DispatchOutcome::RuntimeFailed { code, .. } => Err(McpError::new(
189 ErrorCode::INTERNAL_ERROR,
190 format!("tool runtime failure: {code}"),
191 None,
192 )),
193 }
194}