use std::cell::RefCell;
use std::ffi::{c_char, CStr, CString};
use std::ptr;
use crate::ToolRegistry;
struct FFIContext {
runtime: tokio::runtime::Runtime,
registry: ToolRegistry,
}
pub type HanzoMCPHandle = *mut FFIContext;
thread_local! {
static LAST_ERROR: RefCell<Option<CString>> = RefCell::new(None);
}
fn set_last_error(msg: &str) {
LAST_ERROR.with(|cell| {
*cell.borrow_mut() = CString::new(msg).ok();
});
}
unsafe fn cstr_to_str<'a>(ptr: *const c_char) -> Option<&'a str> {
if ptr.is_null() {
return None;
}
CStr::from_ptr(ptr).to_str().ok()
}
fn to_c_string(s: &str) -> *mut c_char {
match CString::new(s) {
Ok(cs) => cs.into_raw(),
Err(_) => {
let sanitized = s.replace('\0', "\\0");
CString::new(sanitized)
.unwrap_or_else(|_| CString::new("internal error").unwrap())
.into_raw()
}
}
}
#[no_mangle]
pub extern "C" fn hanzo_mcp_init(config_json: *const c_char) -> HanzoMCPHandle {
let result = std::panic::catch_unwind(|| {
let _config: Option<serde_json::Value> = unsafe {
cstr_to_str(config_json).and_then(|s| serde_json::from_str(s).ok())
};
let runtime = tokio::runtime::Builder::new_multi_thread()
.enable_all()
.thread_name("hanzo-mcp-ffi")
.build()
.map_err(|e| format!("Failed to create tokio runtime: {}", e))?;
let registry = ToolRegistry::with_defaults();
let ctx = Box::new(FFIContext { runtime, registry });
Ok::<*mut FFIContext, String>(Box::into_raw(ctx))
});
match result {
Ok(Ok(ptr)) => ptr,
Ok(Err(msg)) => {
set_last_error(&msg);
ptr::null_mut()
}
Err(_) => {
set_last_error("panic during hanzo_mcp_init");
ptr::null_mut()
}
}
}
#[no_mangle]
pub extern "C" fn hanzo_mcp_call_tool(
handle: HanzoMCPHandle,
tool_name: *const c_char,
params_json: *const c_char,
) -> *mut c_char {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if handle.is_null() {
return Err("null handle".to_string());
}
let ctx = unsafe { &*handle };
let name = unsafe { cstr_to_str(tool_name) }
.ok_or_else(|| "invalid tool_name".to_string())?;
let params_str = unsafe { cstr_to_str(params_json) }.unwrap_or("{}");
let params: serde_json::Value = serde_json::from_str(params_str)
.map_err(|e| format!("invalid params JSON: {}", e))?;
let tool_result = ctx
.runtime
.block_on(ctx.registry.execute(name, params))
.map_err(|e| format!("tool execution error: {}", e))?;
serde_json::to_string(&tool_result)
.map_err(|e| format!("serialization error: {}", e))
}));
match result {
Ok(Ok(json)) => to_c_string(&json),
Ok(Err(msg)) => {
let err_json = serde_json::json!({
"success": false,
"content": null,
"error": msg
});
to_c_string(&err_json.to_string())
}
Err(_) => {
set_last_error("panic during hanzo_mcp_call_tool");
ptr::null_mut()
}
}
}
#[no_mangle]
pub extern "C" fn hanzo_mcp_list_tools(handle: HanzoMCPHandle) -> *mut c_char {
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
if handle.is_null() {
return Err("null handle".to_string());
}
let ctx = unsafe { &*handle };
let defs = ctx.registry.get_definitions();
serde_json::to_string(&defs).map_err(|e| format!("serialization error: {}", e))
}));
match result {
Ok(Ok(json)) => to_c_string(&json),
Ok(Err(msg)) => {
set_last_error(&msg);
ptr::null_mut()
}
Err(_) => {
set_last_error("panic during hanzo_mcp_list_tools");
ptr::null_mut()
}
}
}
#[no_mangle]
pub extern "C" fn hanzo_mcp_last_error() -> *mut c_char {
LAST_ERROR.with(|cell| {
let err = cell.borrow();
match err.as_ref() {
Some(cs) => {
match CString::new(cs.to_bytes()) {
Ok(clone) => clone.into_raw(),
Err(_) => ptr::null_mut(),
}
}
None => ptr::null_mut(),
}
})
}
#[no_mangle]
pub extern "C" fn hanzo_mcp_free_string(ptr: *mut c_char) {
if !ptr.is_null() {
unsafe {
drop(CString::from_raw(ptr));
}
}
}
#[no_mangle]
pub extern "C" fn hanzo_mcp_destroy(handle: HanzoMCPHandle) {
if !handle.is_null() {
unsafe {
drop(Box::from_raw(handle));
}
}
}
#[no_mangle]
pub extern "C" fn hanzo_mcp_version() -> *mut c_char {
let v = crate::version();
to_c_string(&v.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::CString;
#[test]
fn test_init_and_destroy() {
let handle = hanzo_mcp_init(ptr::null());
assert!(!handle.is_null());
hanzo_mcp_destroy(handle);
}
#[test]
fn test_list_tools() {
let handle = hanzo_mcp_init(ptr::null());
assert!(!handle.is_null());
let result = hanzo_mcp_list_tools(handle);
assert!(!result.is_null());
let json_str = unsafe { CStr::from_ptr(result).to_str().unwrap() };
let tools: Vec<serde_json::Value> = serde_json::from_str(json_str).unwrap();
assert!(tools.len() >= 9);
hanzo_mcp_free_string(result);
hanzo_mcp_destroy(handle);
}
#[test]
fn test_call_tool_think() {
let handle = hanzo_mcp_init(ptr::null());
assert!(!handle.is_null());
let tool = CString::new("think").unwrap();
let params = CString::new(r#"{"action":"think","thought":"FFI test"}"#).unwrap();
let result = hanzo_mcp_call_tool(handle, tool.as_ptr(), params.as_ptr());
assert!(!result.is_null());
let json_str = unsafe { CStr::from_ptr(result).to_str().unwrap() };
let parsed: serde_json::Value = serde_json::from_str(json_str).unwrap();
assert_eq!(parsed["success"], true);
hanzo_mcp_free_string(result);
hanzo_mcp_destroy(handle);
}
#[test]
fn test_call_tool_fs_help() {
let handle = hanzo_mcp_init(ptr::null());
let tool = CString::new("fs").unwrap();
let params = CString::new(r#"{"action":"help"}"#).unwrap();
let result = hanzo_mcp_call_tool(handle, tool.as_ptr(), params.as_ptr());
assert!(!result.is_null());
let json_str = unsafe { CStr::from_ptr(result).to_str().unwrap() };
let parsed: serde_json::Value = serde_json::from_str(json_str).unwrap();
assert_eq!(parsed["success"], true);
hanzo_mcp_free_string(result);
hanzo_mcp_destroy(handle);
}
#[test]
fn test_null_handle() {
let tool = CString::new("think").unwrap();
let params = CString::new("{}").unwrap();
let result = hanzo_mcp_call_tool(ptr::null_mut(), tool.as_ptr(), params.as_ptr());
assert!(!result.is_null());
let json_str = unsafe { CStr::from_ptr(result).to_str().unwrap() };
let parsed: serde_json::Value = serde_json::from_str(json_str).unwrap();
assert_eq!(parsed["success"], false);
hanzo_mcp_free_string(result);
}
#[test]
fn test_unknown_tool() {
let handle = hanzo_mcp_init(ptr::null());
let tool = CString::new("nonexistent").unwrap();
let params = CString::new("{}").unwrap();
let result = hanzo_mcp_call_tool(handle, tool.as_ptr(), params.as_ptr());
assert!(!result.is_null());
let json_str = unsafe { CStr::from_ptr(result).to_str().unwrap() };
let parsed: serde_json::Value = serde_json::from_str(json_str).unwrap();
assert_eq!(parsed["success"], false);
hanzo_mcp_free_string(result);
hanzo_mcp_destroy(handle);
}
#[test]
fn test_version() {
let result = hanzo_mcp_version();
assert!(!result.is_null());
let json_str = unsafe { CStr::from_ptr(result).to_str().unwrap() };
let parsed: serde_json::Value = serde_json::from_str(json_str).unwrap();
assert_eq!(parsed["name"], "hanzo-mcp");
hanzo_mcp_free_string(result);
}
#[test]
fn test_free_null() {
hanzo_mcp_free_string(ptr::null_mut());
}
#[test]
fn test_destroy_null() {
hanzo_mcp_destroy(ptr::null_mut());
}
}