MCP stdio server with 6 tools

- internal/mcp: minimal JSON-RPC 2.0 over newline-delimited JSON, stdio
  transport. Handles initialize / tools/list / tools/call / ping /
  notifications. No deps — stdlib only.
- cmd: 'mithril-go mcp' subcommand brings up the server. Tools:
    mithril_info
    mithril_list_snapshots
    mithril_show_snapshot
    mithril_get_certificate
    mithril_walk_cert_chain
    mithril_verify_genesis
- verified end-to-end against mainnet via tools/call: verify_genesis walks
  the 89-cert chain and returns verified=true

Any MCP client (Claude Code, Cursor, Zed, etc.) can now point at this
binary and get a discoverable, typed tool surface.
This commit is contained in:
Kayos 2026-04-23 15:40:34 -07:00
parent 20853a9d44
commit 8e3a46e90f
3 changed files with 512 additions and 1 deletions

View file

@ -24,6 +24,7 @@ import (
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/aggregator"
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/artifact"
"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/verify"
)
@ -63,6 +64,8 @@ func main() {
os.Exit(cmdVerify(ctx, args))
case "cert":
os.Exit(cmdCert(ctx, args))
case "mcp":
os.Exit(cmdMCP(ctx, args))
case "info":
os.Exit(cmdInfo(args))
case "version", "--version", "-v":
@ -87,7 +90,8 @@ Commands:
show Show detail for one snapshot (hash or "latest")
cert Show a certificate or walk the chain back to genesis
download Download a snapshot to a target directory
verify Verify an already-downloaded snapshot (not yet implemented)
verify Verify a certificate (genesis Ed25519 working; STM BLS pending)
mcp Run as a Model Context Protocol server over stdio
info Show network + aggregator info
version Print version
help Show this help
@ -467,6 +471,23 @@ func cmdInfo(args []string) int {
return exitOK
}
func cmdMCP(ctx context.Context, args []string) int {
s := mcp.New(mcp.ServerInfo{
Name: "mithril-go",
Version: version,
})
registerMCPTools(s)
fmt.Fprintf(os.Stderr, "[mcp] mithril-go %s MCP server ready on stdio (%d tools)\n", version, 6)
if err := s.Run(ctx); err != nil {
if err == context.Canceled || err == context.DeadlineExceeded {
return exitCanceled
}
fmt.Fprintln(os.Stderr, "mcp:", err)
return exitGeneric
}
return exitOK
}
func resolveSnapshot(ctx context.Context, c *aggregator.Client, hashOrLatest string) (*aggregator.CardanoDBSnapshot, error) {
if hashOrLatest == "latest" {
snaps, err := c.ListCardanoDBSnapshots(ctx)

237
cmd/mithril-go/mcp.go Normal file
View file

@ -0,0 +1,237 @@
package main
import (
"context"
"fmt"
"git.sulkta.coop/Sulkta-Coop/mithril-go/internal/aggregator"
"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/verify"
)
// 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_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
},
})
}