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
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@

END OF TERMS AND CONDITIONS

Copyright 2026 Sire Run Inc.
Copyright 2026 Sire Run, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
2 changes: 1 addition & 1 deletion action.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: "Mint OpenAPI"
description: "Lint, validate, and diff OpenAPI specs in pull requests using Mint"
author: "Sire Run Inc"
author: "Sire Run, Inc."

branding:
icon: "check-circle"
Expand Down
54 changes: 54 additions & 0 deletions cmd/mint/install.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package main

import (
"flag"
"fmt"
"os"

"github.com/sirerun/mint/internal/install"
)

func runInstall(args []string) int {
fs := flag.NewFlagSet("mint install", flag.ContinueOnError)
registryURL := fs.String("registry", defaultRegistryURL, "Registry API base URL")
installDir := fs.String("dir", "", "Install directory (default: ~/.mint/servers)")

if err := fs.Parse(args); err != nil {
if err == flag.ErrHelp {
return 0
}
return 1
}

remaining := fs.Args()
if len(remaining) == 0 {
fmt.Fprintln(os.Stderr, "error: server name is required")
fmt.Fprintln(os.Stderr, "\nUsage: mint install <name[@version]>")
fmt.Fprintln(os.Stderr, "\nExamples:")
fmt.Fprintln(os.Stderr, " mint install stripe-mcp")
fmt.Fprintln(os.Stderr, " mint install stripe-mcp@1.2.0")
return 1
}

name := remaining[0]
parsedName, parsedVersion := install.ParseNameVersion(name)

versionStr := ""
if parsedVersion != "" {
versionStr = "@" + parsedVersion
}
fmt.Printf("Installing %s%s...\n", parsedName, versionStr)

dest, err := install.Install(install.Options{
Name: name,
RegistryURL: *registryURL,
InstallDir: *installDir,
})
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
return 1
}

fmt.Printf("Installed %s to %s\n", parsedName, dest)
return 0
}
91 changes: 91 additions & 0 deletions cmd/mint/login.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package main

import (
"encoding/json"
"flag"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"

"github.com/sirerun/mint/internal/auth"
)

const defaultRegistryURL = "https://mint.sire.run/api/v1"

func runLogin(args []string) int {
fs := flag.NewFlagSet("mint login", flag.ContinueOnError)
registryURL := fs.String("registry", defaultRegistryURL, "Registry API base URL")
githubHandle := fs.String("github", "", "GitHub username (required)")

if err := fs.Parse(args); err != nil {
if err == flag.ErrHelp {
return 0
}
return 1
}

if *githubHandle == "" {
fmt.Fprintln(os.Stderr, "error: --github flag is required")
fmt.Fprintln(os.Stderr, "\nUsage: mint login --github <username>")
return 1
}

// Register with the registry to get an API key.
url := strings.TrimRight(*registryURL, "/") + "/publishers/register"
body := fmt.Sprintf(`{"github_handle":%q}`, *githubHandle)

resp, err := http.Post(url, "application/json", strings.NewReader(body))
if err != nil {
fmt.Fprintf(os.Stderr, "error: failed to connect to registry: %v\n", err)
return 1
}
defer resp.Body.Close()

respBody, err := io.ReadAll(resp.Body)
if err != nil {
fmt.Fprintf(os.Stderr, "error: reading response: %v\n", err)
return 1
}

if resp.StatusCode == http.StatusConflict {
fmt.Fprintln(os.Stderr, "Publisher already registered. Use MINT_API_KEY env var if you have your key,")
fmt.Fprintln(os.Stderr, "or contact support to reset your API key.")
return 1
}

if resp.StatusCode != http.StatusCreated {
var errResp struct {
Error string `json:"error"`
}
if json.Unmarshal(respBody, &errResp) == nil && errResp.Error != "" {
fmt.Fprintf(os.Stderr, "error: %s\n", errResp.Error)
} else {
fmt.Fprintf(os.Stderr, "error: registration failed (%d)\n", resp.StatusCode)
}
return 1
}

var result struct {
PublisherID string `json:"publisher_id"`
APIKey string `json:"api_key"`
}
if err := json.Unmarshal(respBody, &result); err != nil {
fmt.Fprintf(os.Stderr, "error: parsing response: %v\n", err)
return 1
}

// Save credentials.
if err := auth.SaveCredentials(result.APIKey); err != nil {
fmt.Fprintf(os.Stderr, "error: saving credentials: %v\n", err)
return 1
}

fmt.Println("Login successful!")
fmt.Printf("Publisher ID: %s\n", result.PublisherID)
fmt.Printf("Credentials saved to ~/.mint/credentials\n")
fmt.Printf("Logged in at: %s\n", time.Now().Format(time.RFC3339))
return 0
}
12 changes: 12 additions & 0 deletions cmd/mint/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ func run(args []string) int {
return runTransform(args[1:])
case "deploy":
return runDeploy(args[1:])
case "login":
return runLogin(args[1:])
case "publish":
return runPublish(args[1:])
case "install":
return runInstall(args[1:])
case "seed":
return runSeed(args[1:])
default:
fmt.Fprintf(os.Stderr, "unknown command: %s\n\nRun 'mint help' for usage.\n", args[0])
return 1
Expand All @@ -61,6 +69,10 @@ Commands:
overlay Apply OpenAPI Overlay documents
transform Transform specs (filter, cleanup, format)
deploy Deploy generated MCP servers
login Authenticate with the Mint registry
publish Publish an MCP server to the registry
install Install an MCP server from the registry
seed Batch-generate MCP servers from a catalog of OpenAPI specs
version Print the version
help Show this help message

Expand Down
33 changes: 33 additions & 0 deletions cmd/mint/main_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"os"
"testing"
)

Expand Down Expand Up @@ -48,6 +49,38 @@ func TestRunUnknownCommand(t *testing.T) {
}
}

func TestRunNewCommands(t *testing.T) {
tests := []struct {
name string
args []string
want int
}{
{name: "login no github", args: []string{"login"}, want: 1},
{name: "login help", args: []string{"login", "--help"}, want: 0},
{name: "publish help", args: []string{"publish", "--help"}, want: 0},
{name: "publish no manifest", args: []string{"publish", "--dry-run", "--dir", t.TempDir()}, want: 1},
{name: "install no args", args: []string{"install"}, want: 1},
{name: "install help", args: []string{"install", "--help"}, want: 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := run(tt.args); got != tt.want {
t.Errorf("run(%v) = %d, want %d", tt.args, got, tt.want)
}
})
}
}

func TestRunPublishDryRun(t *testing.T) {
dir := t.TempDir()
manifest := `{"name":"test-server","version":"1.0.0","description":"A test"}`
os.WriteFile(dir+"/mint.json", []byte(manifest), 0o644)

if got := run([]string{"publish", "--dry-run", "--dir", dir}); got != 0 {
t.Errorf("publish --dry-run = %d, want 0", got)
}
}

func TestRunSubcommands(t *testing.T) {
tests := []struct {
name string
Expand Down
68 changes: 68 additions & 0 deletions cmd/mint/publish.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package main

import (
"flag"
"fmt"
"os"

"github.com/sirerun/mint/internal/auth"
"github.com/sirerun/mint/internal/publish"
)

func runPublish(args []string) int {
fs := flag.NewFlagSet("mint publish", flag.ContinueOnError)
dir := fs.String("dir", ".", "Project directory containing mint.json")
registryURL := fs.String("registry", defaultRegistryURL, "Registry API base URL")
dryRun := fs.Bool("dry-run", false, "Validate manifest without uploading")

if err := fs.Parse(args); err != nil {
if err == flag.ErrHelp {
return 0
}
return 1
}

// Read and validate manifest first (even for non-dry-run).
manifest, err := publish.ReadManifest(*dir)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
return 1
}

if *dryRun {
fmt.Println("Dry run: manifest is valid.")
fmt.Printf(" Name: %s\n", manifest.Name)
fmt.Printf(" Version: %s\n", manifest.Version)
fmt.Printf(" Description: %s\n", manifest.Description)
if manifest.Category != "" {
fmt.Printf(" Category: %s\n", manifest.Category)
}
return 0
}

// Load auth token.
token, err := auth.LoadToken()
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
return 1
}

fmt.Printf("Publishing %s@%s...\n", manifest.Name, manifest.Version)

resp, err := publish.Upload(publish.Options{
Dir: *dir,
RegistryURL: *registryURL,
Token: token,
})
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
return 1
}

fmt.Println("Published successfully!")
fmt.Printf(" Server ID: %s\n", resp.ServerID)
fmt.Printf(" Version: %s\n", resp.Version)
fmt.Printf(" Checksum: %s\n", resp.Checksum)
fmt.Printf(" URL: https://mint.sire.run/servers/%s\n", resp.ServerID)
return 0
}
88 changes: 88 additions & 0 deletions cmd/mint/seed.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package main

import (
"flag"
"fmt"
"os"
"path/filepath"
"runtime"

"github.com/sirerun/mint/internal/seed"
)

func runSeed(args []string) int {
fs := flag.NewFlagSet("mint seed", flag.ContinueOnError)
catalogPath := fs.String("catalog", "", "Path to catalog.json (default: built-in catalog)")
outputDir := fs.String("output", "./generated", "Output directory for generated servers")
mintBinary := fs.String("mint", "", "Path to mint binary (default: self)")
dryRun := fs.Bool("dry-run", false, "Validate catalog without generating")

if err := fs.Parse(args); err != nil {
if err == flag.ErrHelp {
return 0
}
return 1
}

// Default to built-in catalog.
if *catalogPath == "" {
// Find catalog.json relative to this binary's source.
_, thisFile, _, _ := runtime.Caller(0)
*catalogPath = filepath.Join(filepath.Dir(thisFile), "..", "..", "internal", "seed", "catalog.json")
}

// Default to self for mint binary.
if *mintBinary == "" {
self, err := os.Executable()
if err != nil {
fmt.Fprintf(os.Stderr, "error: cannot find mint binary: %v\n", err)
return 1
}
*mintBinary = self
}

cat, err := seed.LoadCatalog(*catalogPath)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
return 1
}

counts := seed.CategoryCounts(cat)
fmt.Printf("Catalog: %d specs across %d categories\n", len(cat.Specs), len(counts))
for cat, n := range counts {
fmt.Printf(" %-20s %d\n", cat, n)
}
fmt.Println()

if *dryRun {
issues := seed.ValidateCatalog(cat)
if len(issues) > 0 {
fmt.Fprintln(os.Stderr, "Validation issues:")
for _, issue := range issues {
fmt.Fprintf(os.Stderr, " - %s\n", issue)
}
return 1
}
fmt.Println("Catalog is valid. Dry run complete.")
return 0
}

fmt.Printf("Generating %d servers to %s...\n\n", len(cat.Specs), *outputDir)

report, err := seed.Run(seed.Options{
CatalogPath: *catalogPath,
OutputDir: *outputDir,
MintBinary: *mintBinary,
})
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
return 1
}

fmt.Print(seed.FormatReport(report))

if report.Failed > 0 {
return 1
}
return 0
}
Loading
Loading