Files
zeekay e9a4d37d1f feat: C FFI + Go cgo bindings for Rust MCP tools, fix TS conformance
Rust FFI (rust/src/ffi.rs):
- Expose all 13 HIP-0300 tools via C-compatible API
- 7 extern "C" functions: init, call_tool, list_tools, last_error,
  free_string, destroy, version
- Persistent multi-threaded tokio runtime behind opaque handle
- panic::catch_unwind on all FFI boundaries
- Wire 5 missing tools (code, git, fetch, workspace, tasks, hanzo)
  into ToolRegistry::execute() match arms
- Build as cdylib + staticlib (libhanzo_mcp.a / .dylib)

Go bindings (go/hanzomcp/):
- Handle type with Init/CallTool/CallToolRaw/ListTools/Close
- JSON in, JSON out across cgo boundary
- runtime.SetFinalizer safety net
- 11 tests including 20-goroutine concurrent stress test
- Race detector clean

TypeScript fixes:
- tasks: handle stale todos.json with wrong schema (todos vs items)
- code: add symbols + summarize actions for Python parity
- fetch: add fetch + head actions for Python parity

Results: Rust 66/66 tests, Go 11/11 tests, TS 22/22 conformance
2026-03-11 11:37:15 -07:00

205 lines
5.6 KiB
Go

// Package hanzomcp provides Go bindings to the Hanzo MCP tool runtime via cgo.
//
// It wraps the Rust libhanzo_mcp library, giving Go agents in-process access
// to all 13 HIP-0300 tools (fs, exec, code, git, fetch, workspace, computer,
// think, memory, hanzo, plan, tasks, mode) with zero-copy JSON marshalling.
//
// Usage:
//
// h, err := hanzomcp.Init(nil)
// if err != nil { log.Fatal(err) }
// defer h.Close()
//
// result, err := h.CallTool("think", map[string]any{
// "action": "think",
// "thought": "Planning next steps...",
// })
// fmt.Println(result.Success, result.Content)
package hanzomcp
/*
#cgo LDFLAGS: -L${SRCDIR}/../../rust/target/release -lhanzo_mcp
#cgo LDFLAGS: -ldl -lm -lpthread
#cgo darwin LDFLAGS: -framework CoreGraphics -framework CoreFoundation -framework Cocoa -framework Security
#cgo linux LDFLAGS: -lX11
#include "../../rust/include/hanzo_mcp.h"
#include <stdlib.h>
*/
import "C"
import (
"encoding/json"
"errors"
"fmt"
"runtime"
"sync"
"unsafe"
)
// Handle wraps an opaque pointer to the Rust MCP tool runtime.
// It is safe to use from multiple goroutines concurrently.
type Handle struct {
ptr C.HanzoMCPHandle
mu sync.Mutex
once sync.Once
}
// ToolResult is the result of a tool call.
type ToolResult struct {
Success bool `json:"success"`
Content json.RawMessage `json:"content"`
Error string `json:"error,omitempty"`
}
// ToolDef describes a tool's name, description, and parameter schema.
type ToolDef struct {
Name string `json:"name"`
Description string `json:"description"`
InputSchema json.RawMessage `json:"inputSchema"`
}
// Init creates a new MCP tool runtime with all 13 HIP-0300 tools.
//
// Pass nil for default configuration, or a map with configuration options.
// The returned Handle must be closed with Close() when done.
func Init(config map[string]any) (*Handle, error) {
var configPtr *C.char
if config != nil {
data, err := json.Marshal(config)
if err != nil {
return nil, fmt.Errorf("hanzomcp: invalid config: %w", err)
}
configPtr = C.CString(string(data))
defer C.free(unsafe.Pointer(configPtr))
}
ptr := C.hanzo_mcp_init(configPtr)
if ptr == nil {
return nil, fmt.Errorf("hanzomcp: init failed: %s", lastError())
}
h := &Handle{ptr: ptr}
runtime.SetFinalizer(h, (*Handle).Close)
return h, nil
}
// CallTool executes a tool by name with the given parameters.
//
// The params argument is marshalled to JSON. Common usage:
//
// result, err := h.CallTool("fs", map[string]any{
// "action": "read",
// "path": "/tmp/hello.txt",
// })
//
// This method blocks until the tool completes. It is safe to call
// from multiple goroutines concurrently.
func (h *Handle) CallTool(name string, params any) (*ToolResult, error) {
if h.ptr == nil {
return nil, errors.New("hanzomcp: handle is closed")
}
paramsJSON := "{}"
if params != nil {
data, err := json.Marshal(params)
if err != nil {
return nil, fmt.Errorf("hanzomcp: invalid params: %w", err)
}
paramsJSON = string(data)
}
cName := C.CString(name)
defer C.free(unsafe.Pointer(cName))
cParams := C.CString(paramsJSON)
defer C.free(unsafe.Pointer(cParams))
cResult := C.hanzo_mcp_call_tool(h.ptr, cName, cParams)
if cResult == nil {
return nil, fmt.Errorf("hanzomcp: call_tool panic: %s", lastError())
}
defer C.hanzo_mcp_free_string(cResult)
var result ToolResult
if err := json.Unmarshal([]byte(C.GoString(cResult)), &result); err != nil {
return nil, fmt.Errorf("hanzomcp: invalid result JSON: %w", err)
}
return &result, nil
}
// CallToolRaw is like CallTool but accepts and returns raw JSON bytes,
// avoiding double-marshalling when the caller already has JSON.
func (h *Handle) CallToolRaw(name string, paramsJSON []byte) ([]byte, error) {
if h.ptr == nil {
return nil, errors.New("hanzomcp: handle is closed")
}
cName := C.CString(name)
defer C.free(unsafe.Pointer(cName))
cParams := C.CString(string(paramsJSON))
defer C.free(unsafe.Pointer(cParams))
cResult := C.hanzo_mcp_call_tool(h.ptr, cName, cParams)
if cResult == nil {
return nil, fmt.Errorf("hanzomcp: call_tool panic: %s", lastError())
}
defer C.hanzo_mcp_free_string(cResult)
return []byte(C.GoString(cResult)), nil
}
// ListTools returns definitions for all available tools.
func (h *Handle) ListTools() ([]ToolDef, error) {
if h.ptr == nil {
return nil, errors.New("hanzomcp: handle is closed")
}
cResult := C.hanzo_mcp_list_tools(h.ptr)
if cResult == nil {
return nil, fmt.Errorf("hanzomcp: list_tools failed: %s", lastError())
}
defer C.hanzo_mcp_free_string(cResult)
var tools []ToolDef
if err := json.Unmarshal([]byte(C.GoString(cResult)), &tools); err != nil {
return nil, fmt.Errorf("hanzomcp: invalid tools JSON: %w", err)
}
return tools, nil
}
// Version returns version information as a map.
func Version() (map[string]any, error) {
cResult := C.hanzo_mcp_version()
if cResult == nil {
return nil, errors.New("hanzomcp: version failed")
}
defer C.hanzo_mcp_free_string(cResult)
var v map[string]any
if err := json.Unmarshal([]byte(C.GoString(cResult)), &v); err != nil {
return nil, fmt.Errorf("hanzomcp: invalid version JSON: %w", err)
}
return v, nil
}
// Close destroys the handle and releases all resources.
// Safe to call multiple times. Must not be called while CallTool is in progress.
func (h *Handle) Close() {
h.once.Do(func() {
h.mu.Lock()
defer h.mu.Unlock()
if h.ptr != nil {
C.hanzo_mcp_destroy(h.ptr)
h.ptr = nil
}
})
}
func lastError() string {
cErr := C.hanzo_mcp_last_error()
if cErr == nil {
return "(unknown error)"
}
defer C.hanzo_mcp_free_string(cErr)
return C.GoString(cErr)
}