mirror of
https://github.com/luxfi/zap.git
synced 2026-07-27 05:54:26 +00:00
reconcile: carry MCP<->ZAP bridge onto refactored main
WIP main was an orphaned old base; remote main is the canonical refactor and already contains the WIP intent, advanced further: PQ handshake (bigger handshake/), the full transport/ suite WITH C kernels (dpdk_pmd/gpudirect_rdma/ uma_cuda) and QUIC (quic-go in go.mod), plus larger node.go/zap.go. All 15 modified source files + go.mod/go.sum in the WIP were the older base and are superseded (accept remote). LLM.md/MIGRATION.md: remote canonical, no unique local notes to fold. The 416 mdns/rust/target/* build artifacts the WIP had committed are dropped (compiled output, never committed). Genuinely-new, ported: mcp/ package (MCP<->ZAP bridge for fast tool calling), absent from remote. Builds clean against the refactored zap API (go build ./... OK). Full WIP preserved on branch wip/local + tag backup/pre-reconcile-main.
This commit is contained in:
+466
@@ -0,0 +1,466 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
// Package mcp provides a bridge between MCP (Model Context Protocol) servers
|
||||
// and ZAP for high-performance tool calling.
|
||||
//
|
||||
// This bridge auto-discovers MCP servers and exposes their capabilities via ZAP,
|
||||
// providing 10-30x performance improvement over native MCP JSON-RPC.
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sync"
|
||||
|
||||
"github.com/luxfi/zap"
|
||||
)
|
||||
|
||||
// Message types for MCP-ZAP bridge
|
||||
const (
|
||||
MsgTypeToolList uint16 = 100 // List available tools
|
||||
MsgTypeToolCall uint16 = 101 // Call a tool
|
||||
MsgTypeToolResult uint16 = 102 // Tool result
|
||||
|
||||
// Field offsets
|
||||
FieldToolCount = 0 // uint32 - number of tools
|
||||
FieldToolID = 0 // uint32 - tool ID
|
||||
FieldToolName = 4 // 64 bytes - tool name
|
||||
FieldArgsLen = 68 // uint32 - arguments JSON length
|
||||
FieldArgs = 72 // bytes - arguments JSON
|
||||
FieldResultLen = 0 // uint32 - result length
|
||||
FieldResultData = 4 // bytes - result data
|
||||
)
|
||||
|
||||
// Tool represents an MCP tool capability
|
||||
type Tool struct {
|
||||
ID uint32 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
InputSchema map[string]interface{} `json:"inputSchema"`
|
||||
}
|
||||
|
||||
// MCPServer represents a connection to an MCP server
|
||||
type MCPServer struct {
|
||||
Name string
|
||||
Command string
|
||||
Args []string
|
||||
Tools []Tool
|
||||
|
||||
cmd *exec.Cmd
|
||||
stdin io.WriteCloser
|
||||
stdout *bufio.Reader
|
||||
mu sync.Mutex
|
||||
reqID int
|
||||
}
|
||||
|
||||
// Bridge manages multiple MCP servers and exposes them via ZAP
|
||||
type Bridge struct {
|
||||
node *zap.Node
|
||||
servers map[string]*MCPServer
|
||||
tools map[uint32]*Tool // toolID -> tool
|
||||
toolMCP map[uint32]*MCPServer // toolID -> server
|
||||
mu sync.RWMutex
|
||||
nextID uint32
|
||||
}
|
||||
|
||||
// NewBridge creates a new MCP-ZAP bridge
|
||||
func NewBridge(node *zap.Node) *Bridge {
|
||||
b := &Bridge{
|
||||
node: node,
|
||||
servers: make(map[string]*MCPServer),
|
||||
tools: make(map[uint32]*Tool),
|
||||
toolMCP: make(map[uint32]*MCPServer),
|
||||
nextID: 1,
|
||||
}
|
||||
|
||||
// Register handlers
|
||||
node.Handle(MsgTypeToolList, b.handleToolList)
|
||||
node.Handle(MsgTypeToolCall, b.handleToolCall)
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// AddServer adds and starts an MCP server
|
||||
func (b *Bridge) AddServer(name, command string, args ...string) error {
|
||||
server := &MCPServer{
|
||||
Name: name,
|
||||
Command: command,
|
||||
Args: args,
|
||||
}
|
||||
|
||||
// Start the MCP server process
|
||||
if err := server.Start(); err != nil {
|
||||
return fmt.Errorf("failed to start MCP server %s: %w", name, err)
|
||||
}
|
||||
|
||||
// Discover tools
|
||||
tools, err := server.ListTools()
|
||||
if err != nil {
|
||||
server.Stop()
|
||||
return fmt.Errorf("failed to list tools from %s: %w", name, err)
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
// Assign IDs and register tools
|
||||
for i := range tools {
|
||||
tools[i].ID = b.nextID
|
||||
b.nextID++
|
||||
b.tools[tools[i].ID] = &tools[i]
|
||||
b.toolMCP[tools[i].ID] = server
|
||||
}
|
||||
server.Tools = tools
|
||||
b.servers[name] = server
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddServerConfig adds a server from a config
|
||||
func (b *Bridge) AddServerConfig(cfg ServerConfig) error {
|
||||
return b.AddServer(cfg.Name, cfg.Command, cfg.Args...)
|
||||
}
|
||||
|
||||
// ServerConfig configures an MCP server
|
||||
type ServerConfig struct {
|
||||
Name string `json:"name"`
|
||||
Command string `json:"command"`
|
||||
Args []string `json:"args"`
|
||||
Env []string `json:"env,omitempty"`
|
||||
}
|
||||
|
||||
// GetTools returns all registered tools
|
||||
func (b *Bridge) GetTools() []Tool {
|
||||
b.mu.RLock()
|
||||
defer b.mu.RUnlock()
|
||||
|
||||
tools := make([]Tool, 0, len(b.tools))
|
||||
for _, t := range b.tools {
|
||||
tools = append(tools, *t)
|
||||
}
|
||||
return tools
|
||||
}
|
||||
|
||||
// CallTool calls a tool by ID
|
||||
func (b *Bridge) CallTool(ctx context.Context, toolID uint32, args map[string]interface{}) (interface{}, error) {
|
||||
b.mu.RLock()
|
||||
tool, ok := b.tools[toolID]
|
||||
server := b.toolMCP[toolID]
|
||||
b.mu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("tool not found: %d", toolID)
|
||||
}
|
||||
|
||||
return server.CallTool(tool.Name, args)
|
||||
}
|
||||
|
||||
// CallToolByName calls a tool by name
|
||||
func (b *Bridge) CallToolByName(ctx context.Context, name string, args map[string]interface{}) (interface{}, error) {
|
||||
b.mu.RLock()
|
||||
var tool *Tool
|
||||
var server *MCPServer
|
||||
for _, t := range b.tools {
|
||||
if t.Name == name {
|
||||
tool = t
|
||||
server = b.toolMCP[t.ID]
|
||||
break
|
||||
}
|
||||
}
|
||||
b.mu.RUnlock()
|
||||
|
||||
if tool == nil {
|
||||
return nil, fmt.Errorf("tool not found: %s", name)
|
||||
}
|
||||
|
||||
return server.CallTool(name, args)
|
||||
}
|
||||
|
||||
// handleToolList returns all available tools
|
||||
func (b *Bridge) handleToolList(ctx context.Context, from string, msg *zap.Message) (*zap.Message, error) {
|
||||
tools := b.GetTools()
|
||||
|
||||
// Build response with tool list
|
||||
builder := zap.NewBuilder(1024)
|
||||
obj := builder.StartObject(512)
|
||||
obj.SetUint32(FieldToolCount, uint32(len(tools)))
|
||||
|
||||
// Encode tools as JSON in the message (for simplicity)
|
||||
toolsJSON, _ := json.Marshal(tools)
|
||||
for i, c := range toolsJSON {
|
||||
if i >= 500 {
|
||||
break
|
||||
}
|
||||
obj.SetUint8(4+i, c)
|
||||
}
|
||||
obj.FinishAsRoot()
|
||||
|
||||
resp, _ := zap.Parse(builder.FinishWithFlags(MsgTypeToolList << 8))
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// handleToolCall handles a tool call request
|
||||
func (b *Bridge) handleToolCall(ctx context.Context, from string, msg *zap.Message) (*zap.Message, error) {
|
||||
root := msg.Root()
|
||||
toolID := root.Uint32(FieldToolID)
|
||||
|
||||
// Extract tool name
|
||||
var toolName string
|
||||
nameBytes := make([]byte, 64)
|
||||
for i := 0; i < 64; i++ {
|
||||
c := root.Uint8(FieldToolName + i)
|
||||
if c == 0 {
|
||||
toolName = string(nameBytes[:i])
|
||||
break
|
||||
}
|
||||
nameBytes[i] = c
|
||||
}
|
||||
|
||||
// Extract args JSON
|
||||
argsLen := root.Uint32(FieldArgsLen)
|
||||
argsBytes := make([]byte, argsLen)
|
||||
for i := uint32(0); i < argsLen; i++ {
|
||||
argsBytes[i] = root.Uint8(int(FieldArgs + int(i)))
|
||||
}
|
||||
|
||||
var args map[string]interface{}
|
||||
json.Unmarshal(argsBytes, &args)
|
||||
|
||||
// Call the tool
|
||||
var result interface{}
|
||||
var err error
|
||||
if toolID > 0 {
|
||||
result, err = b.CallTool(ctx, toolID, args)
|
||||
} else {
|
||||
result, err = b.CallToolByName(ctx, toolName, args)
|
||||
}
|
||||
|
||||
// Build response
|
||||
builder := zap.NewBuilder(4096)
|
||||
obj := builder.StartObject(4000)
|
||||
|
||||
if err != nil {
|
||||
errStr := err.Error()
|
||||
obj.SetUint32(FieldResultLen, uint32(len(errStr)))
|
||||
for i, c := range []byte(errStr) {
|
||||
obj.SetUint8(FieldResultData+i, c)
|
||||
}
|
||||
} else {
|
||||
resultJSON, _ := json.Marshal(result)
|
||||
obj.SetUint32(FieldResultLen, uint32(len(resultJSON)))
|
||||
for i, c := range resultJSON {
|
||||
if i >= 3900 {
|
||||
break
|
||||
}
|
||||
obj.SetUint8(FieldResultData+i, c)
|
||||
}
|
||||
}
|
||||
obj.FinishAsRoot()
|
||||
|
||||
resp, _ := zap.Parse(builder.FinishWithFlags(MsgTypeToolResult << 8))
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Stop stops all MCP servers
|
||||
func (b *Bridge) Stop() {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
for _, server := range b.servers {
|
||||
server.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// MCP Server Process Management
|
||||
// ============================================================================
|
||||
|
||||
// Start starts the MCP server process
|
||||
func (s *MCPServer) Start() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.cmd = exec.Command(s.Command, s.Args...)
|
||||
s.cmd.Stderr = os.Stderr
|
||||
|
||||
var err error
|
||||
s.stdin, err = s.cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
stdout, err := s.cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.stdout = bufio.NewReader(stdout)
|
||||
|
||||
if err := s.cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Initialize MCP connection
|
||||
return s.initialize()
|
||||
}
|
||||
|
||||
// Stop stops the MCP server process
|
||||
func (s *MCPServer) Stop() {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.stdin != nil {
|
||||
s.stdin.Close()
|
||||
}
|
||||
if s.cmd != nil && s.cmd.Process != nil {
|
||||
s.cmd.Process.Kill()
|
||||
}
|
||||
}
|
||||
|
||||
// initialize performs MCP handshake
|
||||
func (s *MCPServer) initialize() error {
|
||||
// Send initialize request
|
||||
req := map[string]interface{}{
|
||||
"jsonrpc": "2.0",
|
||||
"id": s.nextReqID(),
|
||||
"method": "initialize",
|
||||
"params": map[string]interface{}{
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": map[string]interface{}{},
|
||||
"clientInfo": map[string]interface{}{
|
||||
"name": "zap-mcp-bridge",
|
||||
"version": "1.0.0",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if _, err := s.call(req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Send initialized notification
|
||||
notif := map[string]interface{}{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "notifications/initialized",
|
||||
}
|
||||
return s.send(notif)
|
||||
}
|
||||
|
||||
// ListTools discovers available tools from the MCP server
|
||||
func (s *MCPServer) ListTools() ([]Tool, error) {
|
||||
req := map[string]interface{}{
|
||||
"jsonrpc": "2.0",
|
||||
"id": s.nextReqID(),
|
||||
"method": "tools/list",
|
||||
}
|
||||
|
||||
resp, err := s.call(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result, ok := resp["result"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid response")
|
||||
}
|
||||
|
||||
toolsRaw, ok := result["tools"].([]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no tools in response")
|
||||
}
|
||||
|
||||
tools := make([]Tool, len(toolsRaw))
|
||||
for i, t := range toolsRaw {
|
||||
tm := t.(map[string]interface{})
|
||||
tools[i] = Tool{
|
||||
Name: tm["name"].(string),
|
||||
Description: getString(tm, "description"),
|
||||
}
|
||||
if schema, ok := tm["inputSchema"].(map[string]interface{}); ok {
|
||||
tools[i].InputSchema = schema
|
||||
}
|
||||
}
|
||||
|
||||
return tools, nil
|
||||
}
|
||||
|
||||
// CallTool calls a tool on the MCP server
|
||||
func (s *MCPServer) CallTool(name string, args map[string]interface{}) (interface{}, error) {
|
||||
req := map[string]interface{}{
|
||||
"jsonrpc": "2.0",
|
||||
"id": s.nextReqID(),
|
||||
"method": "tools/call",
|
||||
"params": map[string]interface{}{
|
||||
"name": name,
|
||||
"arguments": args,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := s.call(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if errObj, ok := resp["error"].(map[string]interface{}); ok {
|
||||
return nil, fmt.Errorf("MCP error: %v", errObj["message"])
|
||||
}
|
||||
|
||||
return resp["result"], nil
|
||||
}
|
||||
|
||||
func (s *MCPServer) call(req map[string]interface{}) (map[string]interface{}, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if err := s.sendLocked(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.readLocked()
|
||||
}
|
||||
|
||||
func (s *MCPServer) send(msg map[string]interface{}) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.sendLocked(msg)
|
||||
}
|
||||
|
||||
func (s *MCPServer) sendLocked(msg map[string]interface{}) error {
|
||||
data, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = fmt.Fprintf(s.stdin, "%s\n", data)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *MCPServer) readLocked() (map[string]interface{}, error) {
|
||||
line, err := s.stdout.ReadString('\n')
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resp map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(line), &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *MCPServer) nextReqID() int {
|
||||
s.reqID++
|
||||
return s.reqID
|
||||
}
|
||||
|
||||
func getString(m map[string]interface{}, key string) string {
|
||||
if v, ok := m[key].(string); ok {
|
||||
return v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
// Copyright (C) 2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package mcp
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/luxfi/zap"
|
||||
)
|
||||
|
||||
// buildToolCallMessage builds a valid ZAP message representing an MCP tool call.
|
||||
// The message has: toolID (uint32 at 0), toolName (64 bytes at 4), argsLen (uint32 at 68), args (bytes at 72).
|
||||
func buildToolCallMessage(toolID uint32, toolName string, args map[string]interface{}) []byte {
|
||||
b := zap.NewBuilder(4096)
|
||||
|
||||
// We need enough space: 72 (fixed fields before args) + argsJSON length
|
||||
argsJSON, _ := json.Marshal(args)
|
||||
objSize := FieldArgs + len(argsJSON)
|
||||
// Align to 8
|
||||
objSize = (objSize + 7) &^ 7
|
||||
|
||||
ob := b.StartObject(objSize)
|
||||
ob.SetUint32(FieldToolID, toolID)
|
||||
|
||||
// Write tool name as fixed 64-byte field
|
||||
nameBytes := []byte(toolName)
|
||||
for i := 0; i < 64 && i < len(nameBytes); i++ {
|
||||
ob.SetUint8(FieldToolName+i, nameBytes[i])
|
||||
}
|
||||
|
||||
// Write args length and args bytes
|
||||
ob.SetUint32(FieldArgsLen, uint32(len(argsJSON)))
|
||||
for i, c := range argsJSON {
|
||||
ob.SetUint8(FieldArgs+i, c)
|
||||
}
|
||||
ob.FinishAsRoot()
|
||||
|
||||
return b.Finish()
|
||||
}
|
||||
|
||||
// buildToolResultMessage builds a valid ZAP message representing an MCP tool result.
|
||||
// The message has: resultLen (uint32 at 0), resultData (bytes at 4).
|
||||
func buildToolResultMessage(result interface{}) []byte {
|
||||
b := zap.NewBuilder(4096)
|
||||
|
||||
resultJSON, _ := json.Marshal(result)
|
||||
objSize := FieldResultData + len(resultJSON)
|
||||
objSize = (objSize + 7) &^ 7
|
||||
|
||||
ob := b.StartObject(objSize)
|
||||
ob.SetUint32(FieldResultLen, uint32(len(resultJSON)))
|
||||
for i, c := range resultJSON {
|
||||
ob.SetUint8(FieldResultData+i, c)
|
||||
}
|
||||
ob.FinishAsRoot()
|
||||
|
||||
return b.Finish()
|
||||
}
|
||||
|
||||
// FuzzMCPBridgeToolCall parses arbitrary bytes as a ZAP-encoded MCP tool call
|
||||
// message. Extracts toolID, toolName, and args JSON exactly as handleToolCall
|
||||
// does. Must never panic.
|
||||
func FuzzMCPBridgeToolCall(f *testing.F) {
|
||||
// Seed 1: valid tool call
|
||||
f.Add(buildToolCallMessage(1, "read_file", map[string]interface{}{
|
||||
"path": "/tmp/test.txt",
|
||||
}))
|
||||
|
||||
// Seed 2: tool call by name only (ID=0)
|
||||
f.Add(buildToolCallMessage(0, "search", map[string]interface{}{
|
||||
"query": "hello world",
|
||||
"limit": 10,
|
||||
}))
|
||||
|
||||
// Seed 3: empty args
|
||||
f.Add(buildToolCallMessage(42, "list_tools", map[string]interface{}{}))
|
||||
|
||||
// Seed 4: long tool name (truncated to 64 bytes by builder)
|
||||
f.Add(buildToolCallMessage(99, "this_is_a_very_long_tool_name_that_exceeds_the_64_byte_fixed_field_limit_in_the_protocol", map[string]interface{}{
|
||||
"a": "b",
|
||||
}))
|
||||
|
||||
// Seed 5: empty bytes
|
||||
f.Add([]byte{})
|
||||
|
||||
// Seed 6: just a header
|
||||
header := make([]byte, zap.HeaderSize)
|
||||
copy(header[0:4], zap.Magic)
|
||||
binary.LittleEndian.PutUint16(header[4:6], zap.Version)
|
||||
binary.LittleEndian.PutUint32(header[12:16], zap.HeaderSize)
|
||||
f.Add(header)
|
||||
|
||||
// Seed 7: nested JSON args with special characters
|
||||
f.Add(buildToolCallMessage(5, "exec", map[string]interface{}{
|
||||
"cmd": "echo 'hello\"world'",
|
||||
"args": []string{"--flag", "-v"},
|
||||
"env": map[string]interface{}{"PATH": "/usr/bin"},
|
||||
"empty": nil,
|
||||
}))
|
||||
|
||||
f.Fuzz(func(t *testing.T, data []byte) {
|
||||
msg, err := zap.Parse(data)
|
||||
if err != nil {
|
||||
return // invalid ZAP message, that's fine
|
||||
}
|
||||
|
||||
root := msg.Root()
|
||||
|
||||
// Extract tool ID -- same as handleToolCall
|
||||
toolID := root.Uint32(FieldToolID)
|
||||
_ = toolID
|
||||
|
||||
// Extract tool name from fixed 64-byte field
|
||||
nameBytes := make([]byte, 64)
|
||||
var toolName string
|
||||
for i := 0; i < 64; i++ {
|
||||
c := root.Uint8(FieldToolName + i)
|
||||
if c == 0 {
|
||||
toolName = string(nameBytes[:i])
|
||||
break
|
||||
}
|
||||
nameBytes[i] = c
|
||||
}
|
||||
if toolName == "" && nameBytes[0] != 0 {
|
||||
toolName = string(nameBytes)
|
||||
}
|
||||
_ = toolName
|
||||
|
||||
// Extract args JSON
|
||||
argsLen := root.Uint32(FieldArgsLen)
|
||||
|
||||
// Cap to prevent OOM -- real handler would also need this
|
||||
if argsLen > 1<<20 {
|
||||
argsLen = 1 << 20
|
||||
}
|
||||
|
||||
argsBytes := make([]byte, argsLen)
|
||||
for i := uint32(0); i < argsLen; i++ {
|
||||
argsBytes[i] = root.Uint8(int(FieldArgs + int(i)))
|
||||
}
|
||||
|
||||
// Attempt JSON parse -- errors are fine, panics are not
|
||||
var args map[string]interface{}
|
||||
_ = json.Unmarshal(argsBytes, &args)
|
||||
})
|
||||
}
|
||||
|
||||
// FuzzMCPBridgeToolResult parses arbitrary bytes as a ZAP-encoded MCP tool
|
||||
// result message. Extracts resultLen and resultData exactly as the bridge
|
||||
// protocol defines. Must never panic.
|
||||
func FuzzMCPBridgeToolResult(f *testing.F) {
|
||||
// Seed 1: simple string result
|
||||
f.Add(buildToolResultMessage("file contents here"))
|
||||
|
||||
// Seed 2: structured result
|
||||
f.Add(buildToolResultMessage(map[string]interface{}{
|
||||
"content": []map[string]interface{}{
|
||||
{"type": "text", "text": "result data"},
|
||||
},
|
||||
"isError": false,
|
||||
}))
|
||||
|
||||
// Seed 3: error result
|
||||
f.Add(buildToolResultMessage(map[string]interface{}{
|
||||
"error": "tool execution failed",
|
||||
}))
|
||||
|
||||
// Seed 4: empty result
|
||||
f.Add(buildToolResultMessage(nil))
|
||||
|
||||
// Seed 5: large result
|
||||
bigData := make([]byte, 4000)
|
||||
for i := range bigData {
|
||||
bigData[i] = byte(i % 256)
|
||||
}
|
||||
f.Add(buildToolResultMessage(string(bigData)))
|
||||
|
||||
// Seed 6: empty bytes
|
||||
f.Add([]byte{})
|
||||
|
||||
// Seed 7: just a header
|
||||
header := make([]byte, zap.HeaderSize)
|
||||
copy(header[0:4], zap.Magic)
|
||||
binary.LittleEndian.PutUint16(header[4:6], zap.Version)
|
||||
binary.LittleEndian.PutUint32(header[12:16], zap.HeaderSize)
|
||||
f.Add(header)
|
||||
|
||||
f.Fuzz(func(t *testing.T, data []byte) {
|
||||
msg, err := zap.Parse(data)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
root := msg.Root()
|
||||
|
||||
// Extract result length
|
||||
resultLen := root.Uint32(FieldResultLen)
|
||||
|
||||
// Cap to prevent OOM
|
||||
if resultLen > 1<<20 {
|
||||
resultLen = 1 << 20
|
||||
}
|
||||
|
||||
// Extract result data byte-by-byte, same as handleToolCall response pattern
|
||||
resultBytes := make([]byte, resultLen)
|
||||
for i := uint32(0); i < resultLen; i++ {
|
||||
resultBytes[i] = root.Uint8(int(FieldResultData + int(i)))
|
||||
}
|
||||
|
||||
// Attempt JSON parse
|
||||
var result interface{}
|
||||
_ = json.Unmarshal(resultBytes, &result)
|
||||
|
||||
// Also try as string
|
||||
_ = string(resultBytes)
|
||||
|
||||
// Verify the message flags accessor doesn't panic
|
||||
_ = msg.Flags()
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user