-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtools.rs
More file actions
148 lines (131 loc) · 5.39 KB
/
Copy pathtools.rs
File metadata and controls
148 lines (131 loc) · 5.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
//! Exposes every flow connected to this action as an MCP tool: a flow's
//! `output_schema` becomes the tool's input schema (this action doesn't use
//! `input_schema` -- naming holdover from the generic flow model), and
//! executing the flow (via `Connected::execute_flow`) produces the tool's
//! result, returned as-is without a declared output schema.
//!
//! There's no per-flow `description` anywhere in the stack yet (only
//! `name`), so tools are described using just the flow name until that gap
//! is closed upstream.
use std::sync::Arc;
use hercules_sdk::Connected;
use rmcp::ErrorData as McpError;
use rmcp::RoleServer;
use rmcp::handler::server::ServerHandler;
use rmcp::model::{
CallToolRequestParams, CallToolResult, Implementation, ListToolsResult,
PaginatedRequestParams, ProtocolVersion, ServerCapabilities, ServerInfo, Tool,
};
use rmcp::service::RequestContext;
use serde_json::{Map, Value};
use tucana::aquila::ActionFlow;
use tucana::shared::Struct;
use tucana::shared::Value as WireValue;
use tucana::shared::helper::value::to_json_value;
use tucana::shared::value::Kind;
/// Identifier of the module-level configuration value (see
/// [`crate::build_action`]) callers must present to invoke a tool.
pub const TOKEN_CONFIG_ID: &str = "TOKEN";
#[derive(Clone)]
pub struct FlowToolServer {
connected: Connected,
}
impl FlowToolServer {
pub fn new(connected: Connected) -> Self {
Self { connected }
}
}
/// MCP tool names are opaque identifiers, not display strings, so a flow's
/// `name` (which may contain spaces or collide across flows) can't be used
/// directly -- `flow_id` is already unique and stable.
fn tool_name(flow: &ActionFlow) -> String {
format!("flow-{}", flow.flow_id)
}
fn parse_tool_name(name: &str) -> Option<i64> {
name.strip_prefix("flow-")?.parse().ok()
}
/// Pulls the `Authorization: Bearer <token>` header out of the underlying
/// HTTP request, as rmcp's streamable-http transport stashes the original
/// `http::request::Parts` in the request context's extensions.
fn bearer_token(context: &RequestContext<RoleServer>) -> Option<String> {
let parts = context.extensions.get::<http::request::Parts>()?;
let header = parts.headers.get(http::header::AUTHORIZATION)?.to_str().ok()?;
header.strip_prefix("Bearer ").map(str::to_string)
}
fn schema_object(schema: Option<&Struct>) -> Arc<Map<String, Value>> {
let Some(schema) = schema.filter(|schema| !schema.fields.is_empty()) else {
return Arc::new(Map::new());
};
match to_json_value(WireValue {
kind: Some(Kind::StructValue(schema.clone())),
}) {
Value::Object(map) => Arc::new(map),
_ => Arc::new(Map::new()),
}
}
fn flow_to_tool(flow: &ActionFlow) -> Tool {
let mut tool = Tool::default();
tool.name = tool_name(flow).into();
tool.title = Some(flow.name.clone());
tool.description = Some(flow.name.clone().into());
tool.input_schema = schema_object(flow.output_schema.as_ref());
tool
}
impl ServerHandler for FlowToolServer {
fn get_info(&self) -> ServerInfo {
ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
.with_protocol_version(ProtocolVersion::default())
.with_server_info(Implementation::from_build_env())
.with_instructions(
"Every tool corresponds to a flow connected to this action. Call it with \
arguments matching its input schema to execute that flow; the result is the \
flow's own execution result.",
)
}
async fn list_tools(
&self,
_request: Option<PaginatedRequestParams>,
_context: RequestContext<RoleServer>,
) -> Result<ListToolsResult, McpError> {
let tools = self.connected.flows().iter().map(flow_to_tool).collect();
Ok(ListToolsResult::with_all_items(tools))
}
async fn call_tool(
&self,
request: CallToolRequestParams,
context: RequestContext<RoleServer>,
) -> Result<CallToolResult, McpError> {
let flow = parse_tool_name(&request.name).and_then(|id| self.connected.flow(id));
let Some(flow) = flow else {
return Err(McpError::invalid_params(
format!("unknown tool {:?}", request.name),
None,
));
};
let required_token = self
.connected
.config(flow.project_id)
.and_then(|config| config.get(TOKEN_CONFIG_ID).and_then(Value::as_str).map(str::to_string))
.filter(|token| !token.is_empty());
if let Some(required_token) = required_token {
if bearer_token(&context).as_deref() != Some(required_token.as_str()) {
return Err(McpError::invalid_request(
"missing or invalid bearer token for this tool",
None,
));
}
}
let flow_id = flow.flow_id;
let payload = Value::Object(request.arguments.unwrap_or_default());
log::info!("executing flow {flow_id} via tool {:?}", request.name);
match self.connected.execute_flow(flow_id.to_string(), payload).await {
Ok(value) => Ok(CallToolResult::structured(value)),
Err(err) => {
log::warn!("flow {flow_id} failed: {err}");
Ok(CallToolResult::structured_error(Value::String(
err.to_string(),
)))
}
}
}
}