mirror of
https://github.com/luxfi/mcp.git
synced 2026-09-24 00:25:04 +00:00
Split the flat governance MCP package into three independently-complete
packages so the generic parts are written ONCE and future MCP surfaces
(mining, treasury, bridge) are just a bag of read tools, never re-implementing
transport. One and only one way:
github.com/luxfi/mcp (root) - domain-agnostic READ-ONLY MCP
transport (Serve, Tool, Surface,
dispatch, per-call timeout, line
cap, panic->JSON-RPC recover) +
ChainObservation. Imports NO geth.
github.com/luxfi/mcp/evmread - the SOLE go-ethereum importer: a
minimal read-only Caller, Contract,
Call/CallStruct[T], and the
per-request Bounded call ceiling.
github.com/luxfi/mcp/governance - a Surface of the 8 read tools;
reads via evmread, contributes
[]mcp.Tool to the shared transport.
github.com/luxfi/mcp/cmd/lux-mcp - the binary (was aivm-gov-mcp).
What this generalizes (the real point, not just a move):
- Tool is now a value carrying its own Read closure; Serve composes
...Surface and rejects duplicate / empty / nil-Read tools.
- ChainObservation lifted to the root and WIRED INTO ALL 8 TOOLS: every read
returns (value, *ChainObservation) so an operator-LLM's signed verdict can
bind Observation.Hash() and re-verify the chain state it claims to have seen
- domain-independently. (Closes the previously-deferred MED-8.)
- The read-only AST gate is now MODULE-WIDE: it proves go-ethereum is imported
by exactly one package (evmread), no package imports bind/geth-crypto/
geth-rpc/reflect, the string-fragment denylist holds, and evmread.Caller's
method set is exactly {CallContract,ChainID,BlockNumber,HeaderByNumber}.
Behavior-preserving and adversarially re-verified (blue->red loop):
- HIGH-1 bonded-quorum tally still matches AIGovernor.settle()'s _bonded
filter exactly (PoC drives real compiled Solidity via core/vm/runtime).
- Parity tests remain real (independent ABI decode vs MCP output), struct
mirrors still field-for-field aligned with luxfi/standard.
- Per-request bounded ceiling cannot leak/bypass; line cap + timeout + recover
preserved. Read-only invariant intact and STRONGER (adding a write method to
Caller is now a compile failure).
- Tests: 33 pass, 0 fail, 0 skip across the 3 packages (was 23 pre-split;
none dropped, several strengthened); go build / vet / -race clean.
101 lines
3.6 KiB
Go
101 lines
3.6 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package mcp_test
|
|
|
|
import (
|
|
"context"
|
|
"sort"
|
|
"testing"
|
|
|
|
"github.com/luxfi/geth/common"
|
|
|
|
"github.com/luxfi/mcp"
|
|
"github.com/luxfi/mcp/governance"
|
|
)
|
|
|
|
// pingSurface is a tiny, chain-free Surface contributing ONE tool. It exists to prove the
|
|
// transport is domain-agnostic: it composes the real governance Surface with an unrelated
|
|
// surface and dispatches to each. (Decomplect proof: the transport knows nothing about any
|
|
// domain — a domain is just a []Tool.)
|
|
type pingSurface struct{}
|
|
|
|
func (pingSurface) Tools() []mcp.Tool {
|
|
return []mcp.Tool{{
|
|
Name: "ping",
|
|
Description: "returns pong",
|
|
InputSchema: map[string]interface{}{"type": "object", "properties": map[string]interface{}{}},
|
|
Read: func(_ context.Context, _ map[string]interface{}) (interface{}, *mcp.ChainObservation, error) {
|
|
return map[string]interface{}{"pong": true}, nil, nil
|
|
},
|
|
}}
|
|
}
|
|
|
|
// TestTransportComposesMultipleSurfaces proves the ONE transport serves TWO independent
|
|
// surfaces at once: tools/list is the UNION of both surfaces' tools, and tools/call
|
|
// dispatches to the correct surface. This is the composition guarantee — the transport is
|
|
// generic over surfaces, not bound to governance.
|
|
func TestTransportComposesMultipleSurfaces(t *testing.T) {
|
|
addr := common.HexToAddress("0x000000000000000000000000000000000000dEaD")
|
|
// The governance Surface registers its 8 tools regardless of the caller (Tools() and the
|
|
// composition assertions below do not read the chain), so a non-dialing fake caller is
|
|
// enough to construct it here.
|
|
gov, err := governance.NewWithCaller(noopCaller{}, governance.Config{
|
|
AIParams: addr,
|
|
AIGovernor: addr,
|
|
AIThoughtRegistry: addr,
|
|
AIReputation: addr,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("build governance surface: %v", err)
|
|
}
|
|
|
|
srv, err := mcp.NewServer(gov, pingSurface{})
|
|
if err != nil {
|
|
t.Fatalf("compose surfaces: %v", err)
|
|
}
|
|
|
|
// tools/list is the union: 8 governance tools + 1 ping = 9, all distinct.
|
|
got := make([]string, 0, len(srv.Tools()))
|
|
for _, tl := range srv.Tools() {
|
|
got = append(got, tl.Name)
|
|
}
|
|
sort.Strings(got)
|
|
want := []string{
|
|
"chain_state", "operator_reputation", "param_history", "param_value",
|
|
"pending_operations", "ping", "quorum_status", "receipt_lookup", "thought_status",
|
|
}
|
|
sort.Strings(want)
|
|
if len(got) != len(want) {
|
|
t.Fatalf("composed tools/list has %d tools, want %d: %v", len(got), len(want), got)
|
|
}
|
|
for i := range want {
|
|
if got[i] != want[i] {
|
|
t.Fatalf("composed tool[%d]=%q, want %q (full: %v)", i, got[i], want[i], got)
|
|
}
|
|
}
|
|
|
|
// Dispatch to the NON-governance surface's tool proves the transport routes by name
|
|
// across surfaces (no chain needed for ping).
|
|
res, err := srv.CallTool(context.Background(), "ping", nil)
|
|
if err != nil {
|
|
t.Fatalf("dispatch ping: %v", err)
|
|
}
|
|
m, ok := res.(map[string]interface{})
|
|
if !ok || m["pong"] != true {
|
|
t.Fatalf("ping result=%v, want {pong:true}", res)
|
|
}
|
|
|
|
// Dispatching a governance tool by name also resolves through the SAME transport. We use
|
|
// a tool whose error path is reached without a live chain read returning bad bytes:
|
|
// param_value with a malformed argument fails at argument validation (boundary), proving
|
|
// the governance tool was dispatched (not "unknown tool").
|
|
_, err = srv.CallTool(context.Background(), "param_value", map[string]interface{}{})
|
|
if err == nil {
|
|
t.Fatal("expected param_value to fail on missing args (proving it dispatched), got nil")
|
|
}
|
|
if err.Error() == `mcp: unknown tool "param_value"` {
|
|
t.Fatalf("param_value was not dispatched through the composed transport: %v", err)
|
|
}
|
|
}
|