talon_cli/mcp/tool/
mod.rs1mod dispatch;
2mod error;
3pub(super) mod hook;
4mod hook_recall;
5mod public;
6mod status;
7mod sync;
8
9#[cfg(test)]
10mod tests;
11
12use std::sync::Arc;
13
14use serde::Deserialize;
15use serde_json::{Value, json};
16use talon_core::{ErrorCode, TalonEnvelope};
17
18use self::error::ToolError;
19
20#[derive(Debug, Deserialize)]
21struct ToolCallParams {
22 name: String,
23 #[serde(default)]
24 arguments: Value,
25}
26
27#[must_use]
29pub fn tools_list_result() -> Value {
30 let mut tools = public::tools_list_entries();
31 tools.extend(hook::hook_tools_list_entries());
32 json!({ "tools": tools })
33}
34
35#[must_use]
38pub fn tools_call_result_with_state(
39 params: Option<Value>,
40 state: &Arc<crate::mcp::state::McpServerState>,
41) -> Value {
42 let (name, arguments) = match parse_name_and_arguments(params) {
43 Ok(pair) => pair,
44 Err(error) => return content_result(&error.envelope()),
45 };
46
47 if let Some(result) = hook::dispatch_hook(&name, &arguments, state) {
49 return result;
50 }
51
52 if let Some(result) = public::dispatch_named(&name, arguments) {
54 let envelope = result.unwrap_or_else(ToolError::envelope);
55 return public::named_content_result(&envelope);
56 }
57
58 content_result(&unknown_tool_error(&name).envelope())
59}
60
61#[must_use]
63pub fn tools_call_result(params: Option<Value>) -> Value {
64 let (name, arguments) = match parse_name_and_arguments(params) {
66 Ok(pair) => pair,
67 Err(error) => return content_result(&error.envelope()),
68 };
69
70 if let Some(result) = public::dispatch_named(&name, arguments) {
72 let envelope = result.unwrap_or_else(ToolError::envelope);
73 return public::named_content_result(&envelope);
74 }
75
76 content_result(&unknown_tool_error(&name).envelope())
77}
78
79fn parse_name_and_arguments(params: Option<Value>) -> Result<(String, Value), ToolError> {
80 let params = params.ok_or_else(|| {
81 ToolError::new(
82 "talon",
83 ErrorCode::Internal,
84 "tools/call requires params with name and arguments",
85 )
86 })?;
87 let call: ToolCallParams = serde_json::from_value(params).map_err(|error| {
88 ToolError::with_detail(
89 "talon",
90 ErrorCode::Internal,
91 "invalid tools/call params",
92 json!({ "message": error.to_string() }),
93 )
94 })?;
95 Ok((call.name, call.arguments))
96}
97
98fn unknown_tool_error(name: &str) -> ToolError {
99 ToolError::with_detail(
100 "talon",
101 ErrorCode::Internal,
102 format!("unknown tool '{name}'"),
103 json!({ "expected": ["talon_search", "talon_read", "talon_related"] }),
104 )
105}
106
107fn content_result(envelope: &TalonEnvelope) -> Value {
108 json!({
109 "content": [
110 {
111 "type": "text",
112 "text": serde_json::to_string(envelope).unwrap_or_else(|_| "{}".to_owned())
113 }
114 ],
115 "isError": !envelope.ok,
116 "structuredContent": envelope
117 })
118}
119
120#[must_use]
121pub fn panic_tool_result() -> Value {
122 json!({
123 "content": [
124 {
125 "type": "text",
126 "text": "{\"error\":\"talon MCP tool handler panicked; see talon status for diagnostics\"}"
127 }
128 ],
129 "isError": true
130 })
131}