mithril-go/cmd/mithril-go/mcp.go
Kayos e9557ca05b json schema polish: count on list, hash alias on verify_certificate (v1)
Acceptance test report flagged three cosmetic JSON-schema gaps:

1. 'list -json' had no top-level 'count' — caller had to use .snapshots|length.
   Added 'count' alongside 'snapshots'.
2. 'verify -json chain' top-level 'kind' was reported null — actually a
   missing field rather than null. Chain results have per-step kind in
   .steps[]; chain-level kind would be misleading. Documented intent
   in README rather than adding the field.
3. MCP 'mithril_verify_certificate' returned 'cert_hash' but agents
   often look for 'hash'. Added 'hash' alias alongside 'cert_hash' in
   both genesis and STM result paths so either lookup works.

End-to-end loop test on mainnet+preprod: full PASS (89-cert mainnet
chain + 90-cert preprod chain both verified, MCP all 8 tools work,
exit codes correct, manifest detection clean). v1-tag-able now.
2026-04-23 16:31:27 -07:00

376 lines
11 KiB
Go

package main
import (
"context"
"fmt"
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/aggregator"
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/chain"
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/mcp"
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/networks"
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/stm"
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/verify"
)
func errString(e error) string {
if e == nil {
return ""
}
return e.Error()
}
// networkArgOrDefault pulls a "network" string from the args map, defaulting
// to "preprod" if absent. Returns the resolved network + client.
func networkArgOrDefault(args map[string]any) (networks.Network, *aggregator.Client, error) {
name := mcp.ArgString(args, "network")
if name == "" {
name = "preprod"
}
n, ok := networks.ByName(name)
if !ok {
return networks.Network{}, nil, fmt.Errorf("unknown network: %s", name)
}
return n, aggregator.New(n.AggregatorURL), nil
}
// networkEnum is a shared JSON Schema fragment used across tools.
var networkEnum = map[string]any{
"type": "string",
"enum": []string{"mainnet", "preprod", "preview"},
"default": "preprod",
"description": "Cardano network",
}
func registerMCPTools(s *mcp.Server) {
s.RegisterTool(mcp.Tool{
Name: "mithril_info",
Description: "Get aggregator URL and genesis verification key for a Cardano network.",
InputSchema: map[string]any{
"type": "object",
"properties": map[string]any{"network": networkEnum},
},
Handler: func(ctx context.Context, args map[string]any) (any, error) {
n, _, err := networkArgOrDefault(args)
if err != nil {
return nil, err
}
return map[string]any{
"network": n.Name,
"aggregator": n.AggregatorURL,
"genesis_verify_key": n.GenesisVerifyKey,
}, nil
},
})
s.RegisterTool(mcp.Tool{
Name: "mithril_list_snapshots",
Description: "List available cardano-database snapshots on a Mithril aggregator, newest first.",
InputSchema: map[string]any{
"type": "object",
"properties": map[string]any{"network": networkEnum},
},
Handler: func(ctx context.Context, args map[string]any) (any, error) {
_, c, err := networkArgOrDefault(args)
if err != nil {
return nil, err
}
snaps, err := c.ListCardanoDBSnapshots(ctx)
if err != nil {
return nil, err
}
return map[string]any{"count": len(snaps), "snapshots": snaps}, nil
},
})
s.RegisterTool(mcp.Tool{
Name: "mithril_show_snapshot",
Description: "Fetch detail for a specific cardano-database snapshot. Pass 'latest' for the newest.",
InputSchema: map[string]any{
"type": "object",
"properties": map[string]any{
"network": networkEnum,
"hash": map[string]any{
"type": "string",
"description": "Snapshot hash or the literal 'latest'",
"default": "latest",
},
},
},
Handler: func(ctx context.Context, args map[string]any) (any, error) {
_, c, err := networkArgOrDefault(args)
if err != nil {
return nil, err
}
hash := mcp.ArgString(args, "hash")
if hash == "" {
hash = "latest"
}
return resolveSnapshot(ctx, c, hash)
},
})
s.RegisterTool(mcp.Tool{
Name: "mithril_get_certificate",
Description: "Fetch a Mithril certificate by hash. Pass 'head' to resolve via the latest snapshot's certificate_hash.",
InputSchema: map[string]any{
"type": "object",
"required": []string{"hash"},
"properties": map[string]any{
"network": networkEnum,
"hash": map[string]any{
"type": "string",
"description": "Certificate hash or the literal 'head'",
},
},
},
Handler: func(ctx context.Context, args map[string]any) (any, error) {
_, c, err := networkArgOrDefault(args)
if err != nil {
return nil, err
}
hash := mcp.ArgString(args, "hash")
if hash == "" {
return nil, fmt.Errorf("hash is required")
}
if hash == "head" {
snap, err := resolveSnapshot(ctx, c, "latest")
if err != nil {
return nil, err
}
hash = snap.CertificateHash
}
return c.GetCertificate(ctx, hash)
},
})
s.RegisterTool(mcp.Tool{
Name: "mithril_walk_cert_chain",
Description: "Walk the certificate chain from head back to the genesis cert. Returns each cert's hash, epoch, and genesis-or-not role.",
InputSchema: map[string]any{
"type": "object",
"properties": map[string]any{
"network": networkEnum,
"head_hash": map[string]any{
"type": "string",
"description": "Starting certificate hash; defaults to the latest snapshot's cert",
},
"max_depth": map[string]any{
"type": "integer",
"default": 1024,
},
},
},
Handler: func(ctx context.Context, args map[string]any) (any, error) {
_, c, err := networkArgOrDefault(args)
if err != nil {
return nil, err
}
head := mcp.ArgString(args, "head_hash")
if head == "" {
snap, err := resolveSnapshot(ctx, c, "latest")
if err != nil {
return nil, err
}
head = snap.CertificateHash
}
maxDepth := 1024
if v, ok := args["max_depth"]; ok {
if f, ok := v.(float64); ok {
maxDepth = int(f)
}
}
chain, err := c.CertChain(ctx, head, maxDepth)
if err != nil {
return nil, err
}
brief := make([]map[string]any, len(chain))
for i, ct := range chain {
brief[i] = map[string]any{
"hash": ct.Hash,
"previous_hash": ct.PreviousHash,
"epoch": ct.Epoch,
"is_genesis": ct.GenesisSignature != "",
}
}
return map[string]any{"length": len(brief), "certs": brief}, nil
},
})
s.RegisterTool(mcp.Tool{
Name: "mithril_verify_certificate",
Description: "Verify a Mithril certificate. Genesis certs are checked with Ed25519; STM certs with full BLS12-381 aggregate + Merkle membership + lottery-win checks. Returns verified: true|false with context.",
InputSchema: map[string]any{
"type": "object",
"properties": map[string]any{
"network": networkEnum,
"hash": map[string]any{
"type": "string",
"description": "Certificate hash, 'head' for the latest snapshot's cert, or 'genesis' to walk to the chain root",
"default": "head",
},
},
},
Handler: func(ctx context.Context, args map[string]any) (any, error) {
n, c, err := networkArgOrDefault(args)
if err != nil {
return nil, err
}
hash := mcp.ArgString(args, "hash")
if hash == "" {
hash = "head"
}
var cert *aggregator.Certificate
switch hash {
case "head":
snap, err := resolveSnapshot(ctx, c, "latest")
if err != nil {
return nil, err
}
cert, err = c.GetCertificate(ctx, snap.CertificateHash)
if err != nil {
return nil, err
}
case "genesis":
snap, err := resolveSnapshot(ctx, c, "latest")
if err != nil {
return nil, err
}
chain, err := c.CertChain(ctx, snap.CertificateHash, 2048)
if err != nil {
return nil, err
}
cert = chain[len(chain)-1]
default:
cert, err = c.GetCertificate(ctx, hash)
if err != nil {
return nil, err
}
}
if cert.GenesisSignature != "" {
vk, err := verify.DecodeGenesisVerifyKey(n.GenesisVerifyKey)
if err != nil {
return nil, err
}
verr := verify.GenesisFromJSON(vk, cert.SignedMessage, cert.GenesisSignature, cert.ProtocolMessage)
return map[string]any{
"kind": "genesis",
"hash": cert.Hash,
"cert_hash": cert.Hash,
"epoch": cert.Epoch,
"verified": verr == nil,
"error": errString(verr),
}, nil
}
// STM path
raw, err := fetchCertRaw(ctx, n.AggregatorURL, cert.Hash)
if err != nil {
return nil, err
}
ms, err := stm.DecodeMultiSig(raw.MultiSignature)
if err != nil {
return nil, err
}
avk, err := stm.DecodeAVK(raw.AggregateVerificationKey)
if err != nil {
return nil, err
}
params := stm.Parameters{K: raw.Metadata.Parameters.K, M: raw.Metadata.Parameters.M, PhiF: raw.Metadata.Parameters.PhiF}
verr := stm.Verify([]byte(cert.SignedMessage), ms, avk, params)
return map[string]any{
"kind": "stm",
"hash": cert.Hash,
"cert_hash": cert.Hash,
"epoch": cert.Epoch,
"signers": len(ms.Signatures),
"total_wins": ms.TotalWins(),
"distinct_wins": len(ms.DistinctWins()),
"params_k": params.K,
"params_m": params.M,
"params_phi_f": params.PhiF,
"verified": verr == nil,
"error": errString(verr),
}, nil
},
})
s.RegisterTool(mcp.Tool{
Name: "mithril_verify_chain",
Description: "End-to-end verification: walk from the latest snapshot's head cert back to genesis, verify every cert (Ed25519 or STM BLS as appropriate), and check epoch + hash + AVK continuity at every boundary. Returns a full step-by-step report.",
InputSchema: map[string]any{
"type": "object",
"properties": map[string]any{
"network": networkEnum,
"max_depth": map[string]any{
"type": "integer",
"default": 2048,
"description": "Safety cap on the chain walk length",
},
},
},
Handler: func(ctx context.Context, args map[string]any) (any, error) {
n, c, err := networkArgOrDefault(args)
if err != nil {
return nil, err
}
maxDepth := 2048
if v, ok := args["max_depth"]; ok {
if f, ok := v.(float64); ok {
maxDepth = int(f)
}
}
snap, err := resolveSnapshot(ctx, c, "latest")
if err != nil {
return nil, err
}
return chain.Verify(ctx, nil, n, snap.CertificateHash, maxDepth)
},
})
s.RegisterTool(mcp.Tool{
Name: "mithril_verify_genesis",
Description: "Walk the certificate chain back to the genesis cert and verify its Ed25519 signature against the network's " +
"baked-in genesis verification key. Returns verified: true|false with context.",
InputSchema: map[string]any{
"type": "object",
"properties": map[string]any{"network": networkEnum},
},
Handler: func(ctx context.Context, args map[string]any) (any, error) {
n, c, err := networkArgOrDefault(args)
if err != nil {
return nil, err
}
snap, err := resolveSnapshot(ctx, c, "latest")
if err != nil {
return nil, err
}
chain, err := c.CertChain(ctx, snap.CertificateHash, 2048)
if err != nil {
return nil, err
}
if len(chain) == 0 {
return nil, fmt.Errorf("empty chain")
}
gen := chain[len(chain)-1]
if gen.GenesisSignature == "" {
return nil, fmt.Errorf("chain tail is not a genesis certificate")
}
vk, err := verify.DecodeGenesisVerifyKey(n.GenesisVerifyKey)
if err != nil {
return nil, err
}
verr := verify.GenesisFromJSON(vk, gen.SignedMessage, gen.GenesisSignature, gen.ProtocolMessage)
out := map[string]any{
"network": n.Name,
"cert_hash": gen.Hash,
"epoch": gen.Epoch,
"signed_message": gen.SignedMessage,
"chain_length": len(chain),
"verified": verr == nil,
}
if verr != nil {
out["error"] = verr.Error()
}
return out, nil
},
})
}