mirror of
https://github.com/luxfi/zap.git
synced 2026-07-27 05:54:26 +00:00
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.
224 lines
5.9 KiB
Go
224 lines
5.9 KiB
Go
// 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()
|
|
})
|
|
}
|