use super::*;
pub(super) fn build_call_request(
adapter: &str,
caller: &str,
tool_name: &str,
arguments: JsonValue,
auth: AuthRequest,
cancel_token: Arc<AtomicBool>,
progress: Option<harn_vm::mcp_progress::ProgressContext>,
request_id: Option<String>,
) -> Result<CallRequest, String> {
let arguments = match arguments {
JsonValue::Null => CallArguments::Named(BTreeMap::new()),
JsonValue::Object(values) => CallArguments::Named(
values
.into_iter()
.collect::<BTreeMap<String, serde_json::Value>>(),
),
JsonValue::Array(values) => CallArguments::Positional(values),
_ => {
return Err("tool arguments must be an object, array, or null".to_string());
}
};
Ok(CallRequest {
adapter: adapter.to_string(),
function: tool_name.to_string(),
arguments,
auth,
caller: caller.to_string(),
replay_key: None,
trace_id: None,
parent_span_id: None,
metadata: BTreeMap::new(),
cancel_token: Some(cancel_token),
agent_session_id: None,
agent_event_sink: None,
actor_chain: None,
actor_chain_hop: None,
progress,
tenant_id: None,
request_id,
auth_context: None,
auth_principal: None,
})
}
pub(super) fn paged_result(key: &str, entries: Vec<JsonValue>, params: &JsonValue) -> JsonValue {
let (offset, page_size) = parse_cursor(params);
let page_end = (offset + page_size).min(entries.len());
let page_entries = entries[offset..page_end].to_vec();
let mut result = json!({ key: page_entries });
if page_end < entries.len() {
result["nextCursor"] = json!(encode_cursor(page_end));
}
result
}
pub(super) fn encode_cursor(offset: usize) -> String {
use base64::Engine;
base64::engine::general_purpose::STANDARD.encode(offset.to_string().as_bytes())
}
pub(super) fn parse_cursor(params: &JsonValue) -> (usize, usize) {
let offset = params
.get("cursor")
.and_then(JsonValue::as_str)
.and_then(|cursor| {
use base64::Engine;
let bytes = base64::engine::general_purpose::STANDARD
.decode(cursor)
.ok()?;
let text = String::from_utf8(bytes).ok()?;
text.parse::<usize>().ok()
})
.unwrap_or(0);
(offset, 50)
}
pub(super) fn tool_entry(function: &crate::ExportedFunction) -> JsonValue {
let title = function
.title
.clone()
.unwrap_or_else(|| function.name.clone());
let description = function
.description
.clone()
.unwrap_or_else(|| format!("Exported Harn function '{}'.", function.name));
let mut annotations = serde_json::Map::new();
annotations.insert("title".to_string(), json!(title));
if let Some(hints) = function.annotations {
if let Some(value) = hints.read_only {
annotations.insert("readOnlyHint".to_string(), json!(value));
}
if let Some(value) = hints.destructive {
annotations.insert("destructiveHint".to_string(), json!(value));
}
if let Some(value) = hints.idempotent {
annotations.insert("idempotentHint".to_string(), json!(value));
}
if let Some(value) = hints.open_world {
annotations.insert("openWorldHint".to_string(), json!(value));
}
}
let mut entry = json!({
"name": function.name,
"title": title,
"description": description,
"annotations": annotations,
"inputSchema": function.input_schema,
});
if let Some(output_schema) = function.output_schema.as_ref() {
entry["outputSchema"] = mcp_output_schema(output_schema);
}
if function.job.is_some() {
entry["execution"] = json!({
"taskSupport": harn_vm::mcp_tasks::McpTaskSupport::Optional.wire_name(),
});
}
entry
}
pub(super) fn tool_call_success(
response: CallResponse,
output_schema: Option<&JsonValue>,
) -> JsonValue {
let mut result = json!({
"content": content_blocks(&response.value),
"isError": false,
});
if let Some(output_schema) = output_schema {
result["structuredContent"] = if schema_guarantees_object(output_schema) {
response.value
} else {
json!({"result": response.value})
};
}
result
}
fn mcp_output_schema(output_schema: &JsonValue) -> JsonValue {
if schema_guarantees_object(output_schema) {
return output_schema.clone();
}
json!({
"type": "object",
"properties": {"result": output_schema},
"required": ["result"],
"additionalProperties": false,
})
}
fn schema_guarantees_object(schema: &JsonValue) -> bool {
if schema.get("type").and_then(JsonValue::as_str) == Some("object") {
return true;
}
["oneOf", "anyOf"].iter().any(|keyword| {
schema
.get(keyword)
.and_then(JsonValue::as_array)
.is_some_and(|branches| {
!branches.is_empty() && branches.iter().all(schema_guarantees_object)
})
})
}
pub(super) fn tool_call_error(message: String) -> JsonValue {
json!({
"content": [{
"type": "text",
"text": message,
}],
"isError": true,
})
}
pub(super) fn content_blocks(value: &JsonValue) -> JsonValue {
match value {
JsonValue::String(text) => json!([{ "type": "text", "text": text }]),
_ => json!([{
"type": "text",
"text": serde_json::to_string_pretty(value).unwrap_or_else(|_| value.to_string()),
}]),
}
}
pub(super) fn request_key(id: &JsonValue) -> String {
serde_json::to_string(id).unwrap_or_else(|_| "null".to_string())
}
pub(super) fn parse_error_response(message: &str) -> JsonValue {
harn_vm::jsonrpc::error_response(JsonValue::Null, -32700, &format!("Parse error: {message}"))
}
pub(super) fn derived_server_name(catalog: &ExportCatalog) -> String {
catalog
.script_path
.file_stem()
.and_then(|value| value.to_str())
.unwrap_or("harn-serve")
.to_string()
}