Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 27 additions & 9 deletions internal/registry/types.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,36 @@
package registry

// Verification records the functional-verification state of a registry entry.
// It is populated by the mcp-registry verification harness (see ADR-179/180).
// Tier is one of "t0".."t5"; the remaining fields are optional and only set
// once an entry has been through the harness.
type Verification struct {
Tier string `json:"tier"`
VerifiedAt string `json:"verified_at,omitempty"`
VerifierVersion string `json:"verifier_version,omitempty"`
FailureCode string `json:"failure_code,omitempty"`
Notes string `json:"notes,omitempty"`
}

// RegistryEntry represents a single MCP server in the registry.
type RegistryEntry struct {
Name string `json:"name"`
Description string `json:"description"`
Tags []string `json:"tags"`
SpecURL string `json:"spec_url"`
AuthType string `json:"auth_type"`
AuthEnvVar string `json:"auth_env_var"`
MinMintVersion string `json:"min_mint_version"`
Name string `json:"name"`
Description string `json:"description"`
Tags []string `json:"tags"`
SpecURL string `json:"spec_url"`
AuthType string `json:"auth_type"`
AuthEnvVar string `json:"auth_env_var"`
MinMintVersion string `json:"min_mint_version"`
Verification *Verification `json:"verification,omitempty"`
}

// RegistryIndex represents the full registry index.
//
// The on-disk registry.json uses the keys {schema_version, apis}; the in-memory
// field names stay Version/Entries. The JSON tags map the two so mint parses the
// real on-disk shape directly (previously the tags read {version, entries}, which
// silently produced an empty index against the real catalog).
type RegistryIndex struct {
Version int `json:"version"`
Entries []RegistryEntry `json:"entries"`
Version int `json:"schema_version"`
Entries []RegistryEntry `json:"apis"`
}
140 changes: 140 additions & 0 deletions internal/registry/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package registry

import (
"encoding/json"
"os"
"testing"
)

Expand Down Expand Up @@ -148,3 +149,142 @@ func TestRegistryIndex_EmptyEntries(t *testing.T) {
t.Errorf("Entries = %v, want nil", got.Entries)
}
}

// goldenIndexJSON is a minimal on-disk registry document in the real
// {schema_version, apis} shape, including a populated verification block. It is
// the always-on assertion that mint parses the canonical on-disk shape and the
// verification record, without depending on the sibling mcp-registry checkout.
const goldenIndexJSON = `{
"schema_version": 1,
"apis": [
{
"name": "stripe",
"description": "Stripe payments API",
"tags": ["fintech", "payments"],
"spec_url": "https://example.com/stripe.json",
"auth_type": "bearer",
"auth_env_var": "STRIPE_API_KEY",
"min_mint_version": "0.2.0",
"verification": {
"tier": "t1",
"verified_at": "2026-07-03T00:00:00Z",
"verifier_version": "0.1.0"
}
},
{
"name": "unverified",
"description": "An entry with no verification block yet",
"tags": ["misc"],
"spec_url": "https://example.com/other.json",
"auth_type": "none",
"auth_env_var": "OTHER_TOKEN",
"min_mint_version": "0.2.0"
}
]
}`

// TestRegistryIndex_ParsesOnDiskShape proves mint parses the canonical on-disk
// document ({schema_version, apis}) rather than the legacy {version, entries}.
// Before the tag fix this unmarshaled to Version=0, Entries=nil.
func TestRegistryIndex_ParsesOnDiskShape(t *testing.T) {
var idx RegistryIndex
if err := json.Unmarshal([]byte(goldenIndexJSON), &idx); err != nil {
t.Fatalf("unmarshal golden: %v", err)
}
if idx.Version != 1 {
t.Errorf("Version = %d, want 1 (schema_version not mapped)", idx.Version)
}
if len(idx.Entries) != 2 {
t.Fatalf("Entries len = %d, want 2 (apis not mapped)", len(idx.Entries))
}
if idx.Entries[0].Name != "stripe" {
t.Errorf("Entries[0].Name = %q, want %q", idx.Entries[0].Name, "stripe")
}
}

// TestRegistryEntry_Verification checks the optional verification block: present
// entries expose the full record, absent ones leave it nil.
func TestRegistryEntry_Verification(t *testing.T) {
var idx RegistryIndex
if err := json.Unmarshal([]byte(goldenIndexJSON), &idx); err != nil {
t.Fatalf("unmarshal golden: %v", err)
}

got := idx.Entries[0].Verification
if got == nil {
t.Fatal("Verification = nil, want populated block")
}
if got.Tier != "t1" {
t.Errorf("Tier = %q, want %q", got.Tier, "t1")
}
if got.VerifiedAt != "2026-07-03T00:00:00Z" {
t.Errorf("VerifiedAt = %q, want %q", got.VerifiedAt, "2026-07-03T00:00:00Z")
}
if got.VerifierVersion != "0.1.0" {
t.Errorf("VerifierVersion = %q, want %q", got.VerifierVersion, "0.1.0")
}

if idx.Entries[1].Verification != nil {
t.Errorf("Entries[1].Verification = %v, want nil (optional block absent)", idx.Entries[1].Verification)
}
}

// TestVerification_OmitEmpty confirms the verification block is omitted entirely
// when absent, so the backfill only adds keys and never rewrites existing entries.
func TestVerification_OmitEmpty(t *testing.T) {
entry := RegistryEntry{Name: "x", Description: "no verification"}
data, err := json.Marshal(entry)
if err != nil {
t.Fatalf("marshal: %v", err)
}
var m map[string]any
if err := json.Unmarshal(data, &m); err != nil {
t.Fatalf("unmarshal to map: %v", err)
}
if _, ok := m["verification"]; ok {
t.Errorf("verification key present on empty entry, want omitted")
}
}

// TestRegistryIndex_ParsesRealRegistry reconciles the on-disk vs struct drift
// against the REAL registry.json in the sibling mcp-registry checkout. It guards
// the zero-entries regression: parsing the real catalog must yield entries.
//
// The sibling repo is not present in mint CI, so the test skips when the file is
// absent; set MINT_REGISTRY_FILE to point at a specific registry.json to force it.
func TestRegistryIndex_ParsesRealRegistry(t *testing.T) {
path := os.Getenv("MINT_REGISTRY_FILE")
if path == "" {
for _, cand := range []string{
"../../../mcp-registry/registry.json",
"../../../../mcp-registry/registry.json",
} {
if _, err := os.Stat(cand); err == nil {
path = cand
break
}
}
}
if path == "" {
t.Skip("sibling mcp-registry/registry.json not found; set MINT_REGISTRY_FILE to run")
}

data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
var idx RegistryIndex
if err := json.Unmarshal(data, &idx); err != nil {
t.Fatalf("parse %s: %v", path, err)
}
if idx.Version != 1 {
t.Errorf("Version = %d, want 1", idx.Version)
}
if len(idx.Entries) == 0 {
t.Fatalf("real registry parsed to zero entries: on-disk/struct drift regressed")
}
if idx.Entries[0].Name == "" {
t.Errorf("first entry has empty name; struct did not bind apis[]")
}
t.Logf("parsed %d entries from %s (schema_version=%d)", len(idx.Entries), path, idx.Version)
}
Loading