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:
parent
20853a9d44
commit
8e3a46e90f
3 changed files with 512 additions and 1 deletions
|
|
@ -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
237
cmd/mithril-go/mcp.go
Normal 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
|
||||
},
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue