Add CLI client subcommands and MCP server
Adds push, list, get, delete, and login subcommands backed by an HTTP API client, plus an MCP server for tool-based access to the document queue. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
61
cmd/mcq/client.go
Normal file
61
cmd/mcq/client.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"git.wntrmute.dev/mc/mcq/internal/client"
|
||||
)
|
||||
|
||||
var clientFlags struct {
|
||||
server string
|
||||
token string
|
||||
}
|
||||
|
||||
func addClientFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().StringVar(&clientFlags.server, "server", "", "MCQ server URL (env: MCQ_SERVER)")
|
||||
cmd.Flags().StringVar(&clientFlags.token, "token", "", "auth token (env: MCQ_TOKEN)")
|
||||
}
|
||||
|
||||
func newClient() (*client.Client, error) {
|
||||
server := clientFlags.server
|
||||
if server == "" {
|
||||
server = os.Getenv("MCQ_SERVER")
|
||||
}
|
||||
if server == "" {
|
||||
return nil, fmt.Errorf("server URL required: use --server or MCQ_SERVER")
|
||||
}
|
||||
|
||||
token := clientFlags.token
|
||||
if token == "" {
|
||||
token = os.Getenv("MCQ_TOKEN")
|
||||
}
|
||||
if token == "" {
|
||||
token = readCachedToken()
|
||||
}
|
||||
if token == "" {
|
||||
return nil, fmt.Errorf("auth token required: use --token, MCQ_TOKEN, or run 'mcq login'")
|
||||
}
|
||||
|
||||
return client.New(server, token), nil
|
||||
}
|
||||
|
||||
func tokenPath() string {
|
||||
if dir, err := os.UserConfigDir(); err == nil {
|
||||
return filepath.Join(dir, "mcq", "token")
|
||||
}
|
||||
home, _ := os.UserHomeDir()
|
||||
return filepath.Join(home, ".config", "mcq", "token")
|
||||
}
|
||||
|
||||
func readCachedToken() string {
|
||||
data, err := os.ReadFile(tokenPath())
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(string(data))
|
||||
}
|
||||
32
cmd/mcq/delete.go
Normal file
32
cmd/mcq/delete.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func deleteCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete <slug>",
|
||||
Short: "Delete a document from the queue",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
c, err := newClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.DeleteDocument(context.Background(), args[0]); err != nil {
|
||||
return fmt.Errorf("delete document: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("deleted %s\n", args[0])
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
addClientFlags(cmd)
|
||||
return cmd
|
||||
}
|
||||
34
cmd/mcq/get.go
Normal file
34
cmd/mcq/get.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func getCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "get <slug>",
|
||||
Short: "Get a document's markdown body",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
c, err := newClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
doc, err := c.GetDocument(context.Background(), args[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("get document: %w", err)
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprint(os.Stdout, doc.Body)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
addClientFlags(cmd)
|
||||
return cmd
|
||||
}
|
||||
44
cmd/mcq/list.go
Normal file
44
cmd/mcq/list.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func listCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List documents in the queue",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
c, err := newClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
docs, err := c.ListDocuments(context.Background())
|
||||
if err != nil {
|
||||
return fmt.Errorf("list documents: %w", err)
|
||||
}
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
|
||||
_, _ = fmt.Fprintln(w, "SLUG\tTITLE\tPUSHED BY\tPUSHED AT\tREAD")
|
||||
for _, d := range docs {
|
||||
read := "no"
|
||||
if d.Read {
|
||||
read = "yes"
|
||||
}
|
||||
_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", d.Slug, d.Title, d.PushedBy, d.PushedAt, read)
|
||||
}
|
||||
_ = w.Flush()
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
addClientFlags(cmd)
|
||||
return cmd
|
||||
}
|
||||
71
cmd/mcq/login.go
Normal file
71
cmd/mcq/login.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/term"
|
||||
|
||||
"git.wntrmute.dev/mc/mcq/internal/client"
|
||||
)
|
||||
|
||||
func loginCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "login",
|
||||
Short: "Authenticate with MCIAS and cache a token",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
server := clientFlags.server
|
||||
if server == "" {
|
||||
server = os.Getenv("MCQ_SERVER")
|
||||
}
|
||||
if server == "" {
|
||||
return fmt.Errorf("server URL required: use --server or MCQ_SERVER")
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
|
||||
fmt.Print("Username: ")
|
||||
username, _ := reader.ReadString('\n')
|
||||
username = strings.TrimSpace(username)
|
||||
|
||||
fmt.Print("Password: ")
|
||||
passBytes, err := term.ReadPassword(int(syscall.Stdin))
|
||||
if err != nil {
|
||||
return fmt.Errorf("read password: %w", err)
|
||||
}
|
||||
fmt.Println()
|
||||
password := string(passBytes)
|
||||
|
||||
fmt.Print("TOTP code (blank if none): ")
|
||||
totpCode, _ := reader.ReadString('\n')
|
||||
totpCode = strings.TrimSpace(totpCode)
|
||||
|
||||
c := client.New(server, "")
|
||||
token, err := c.Login(context.Background(), username, password, totpCode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("login: %w", err)
|
||||
}
|
||||
|
||||
path := tokenPath()
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
|
||||
return fmt.Errorf("create config dir: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(token+"\n"), 0600); err != nil {
|
||||
return fmt.Errorf("cache token: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("token cached to %s\n", path)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&clientFlags.server, "server", "", "MCQ server URL (env: MCQ_SERVER)")
|
||||
return cmd
|
||||
}
|
||||
@@ -16,6 +16,12 @@ func main() {
|
||||
}
|
||||
|
||||
root.AddCommand(serverCmd())
|
||||
root.AddCommand(pushCmd())
|
||||
root.AddCommand(listCmd())
|
||||
root.AddCommand(getCmd())
|
||||
root.AddCommand(deleteCmd())
|
||||
root.AddCommand(loginCmd())
|
||||
root.AddCommand(mcpCmd())
|
||||
|
||||
if err := root.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
|
||||
40
cmd/mcq/mcp.go
Normal file
40
cmd/mcq/mcp.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/mark3labs/mcp-go/server"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"git.wntrmute.dev/mc/mcq/internal/client"
|
||||
"git.wntrmute.dev/mc/mcq/internal/mcpserver"
|
||||
)
|
||||
|
||||
func mcpCmd() *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "mcp",
|
||||
Short: "Start MCP stdio server for Claude integration",
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
return runMCP()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func runMCP() error {
|
||||
serverURL := os.Getenv("MCQ_SERVER")
|
||||
if serverURL == "" {
|
||||
return fmt.Errorf("MCQ_SERVER environment variable required")
|
||||
}
|
||||
token := os.Getenv("MCQ_TOKEN")
|
||||
if token == "" {
|
||||
return fmt.Errorf("MCQ_TOKEN environment variable required")
|
||||
}
|
||||
|
||||
c := client.New(serverURL, token)
|
||||
mcpSrv := mcpserver.New(c, version)
|
||||
stdioSrv := server.NewStdioServer(mcpSrv)
|
||||
return stdioSrv.Listen(context.Background(), os.Stdin, os.Stdout)
|
||||
}
|
||||
58
cmd/mcq/push.go
Normal file
58
cmd/mcq/push.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"git.wntrmute.dev/mc/mcq/internal/client"
|
||||
)
|
||||
|
||||
func pushCmd() *cobra.Command {
|
||||
var title string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "push <slug> <file|->",
|
||||
Short: "Push a document to the queue",
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
slug := args[0]
|
||||
source := args[1]
|
||||
|
||||
var body []byte
|
||||
var err error
|
||||
if source == "-" {
|
||||
body, err = io.ReadAll(os.Stdin)
|
||||
} else {
|
||||
body, err = os.ReadFile(source)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("read input: %w", err)
|
||||
}
|
||||
|
||||
if title == "" {
|
||||
title = client.ExtractTitle(string(body), slug)
|
||||
}
|
||||
|
||||
c, err := newClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
doc, err := c.PutDocument(context.Background(), slug, title, string(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("push document: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("pushed %s (%s)\n", doc.Slug, doc.Title)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&title, "title", "", "document title (default: first H1 heading, or slug)")
|
||||
addClientFlags(cmd)
|
||||
return cmd
|
||||
}
|
||||
Reference in New Issue
Block a user