main.go

  1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
  2//
  3// SPDX-License-Identifier: LicenseRef-MutuaL-1.2
  4
  5package main
  6
  7import (
  8	"context"
  9	"errors"
 10	"flag"
 11	"fmt"
 12	"os"
 13	"os/signal"
 14	"runtime/debug"
 15	"syscall"
 16
 17	"git.secluded.site/cooked-mcp/internal/appconfig"
 18	"git.secluded.site/cooked-mcp/internal/configvalue"
 19	"git.secluded.site/cooked-mcp/internal/cooked"
 20	"git.secluded.site/cooked-mcp/internal/mcp"
 21)
 22
 23var version = "dev"
 24
 25type runOptions struct {
 26	configPath string
 27	transport  string
 28	httpAddr   string
 29}
 30
 31type mcpRunner interface {
 32	RunStdio(context.Context) error
 33	RunHTTP(context.Context, string, string) error
 34}
 35
 36func init() {
 37	buildVersion := builtVersion()
 38	if buildVersion != "" {
 39		version = buildVersion
 40	}
 41}
 42
 43func main() {
 44	if err := run(); err != nil {
 45		if errors.Is(err, flag.ErrHelp) {
 46			os.Exit(0)
 47		}
 48
 49		fmt.Fprintf(os.Stderr, "cooked-mcp: %v\n", err)
 50		os.Exit(1)
 51	}
 52}
 53
 54func run() error {
 55	options, err := parseRunOptions(os.Args[1:])
 56	if err != nil {
 57		return err
 58	}
 59
 60	if err := validateTransport(options.transport); err != nil {
 61		return err
 62	}
 63
 64	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
 65	defer stop()
 66
 67	config, err := appconfig.Load(
 68		ctx,
 69		options.configPath,
 70		configvalue.NewResolver(),
 71		appconfig.LoadOptions{HTTPAddr: options.httpAddr},
 72	)
 73	if err != nil {
 74		return err
 75	}
 76
 77	client, err := cooked.NewClient(config.BaseURL, config.Username, config.Password)
 78	if err != nil {
 79		return err
 80	}
 81
 82	return runMCPServer(ctx, mcp.NewServer(client, version), config, options.transport)
 83}
 84
 85func parseRunOptions(args []string) (runOptions, error) {
 86	flagSet := flag.NewFlagSet("cooked-mcp", flag.ContinueOnError)
 87
 88	var options runOptions
 89	flagSet.StringVar(&options.configPath, "config", "", "path to TOML configuration file")
 90	flagSet.StringVar(&options.transport, "transport", "stdio", "MCP transport: stdio or http")
 91	flagSet.StringVar(&options.httpAddr, "http-addr", "", "MCP HTTP listen address")
 92
 93	if err := flagSet.Parse(args); err != nil {
 94		return runOptions{}, err
 95	}
 96
 97	return options, nil
 98}
 99
100func validateTransport(transport string) error {
101	switch transport {
102	case "stdio", "http":
103		return nil
104	default:
105		return fmt.Errorf("unsupported transport %q (supported: stdio, http)", transport)
106	}
107}
108
109func runMCPServer(ctx context.Context, server mcpRunner, config appconfig.Config, transport string) error {
110	var err error
111	switch transport {
112	case "stdio":
113		err = server.RunStdio(ctx)
114	case "http":
115		err = server.RunHTTP(ctx, config.HTTPAddr, config.HTTPToken)
116	default:
117		return fmt.Errorf("unsupported transport %q (supported: stdio, http)", transport)
118	}
119
120	if errors.Is(err, context.Canceled) {
121		return nil
122	}
123
124	return err
125}
126
127func builtVersion() string {
128	info, ok := debug.ReadBuildInfo()
129	if !ok {
130		return ""
131	}
132
133	return versionFromBuildInfo(info)
134}
135
136func versionFromBuildInfo(info *debug.BuildInfo) string {
137	if info == nil {
138		return ""
139	}
140	if info.Main.Version != "" && info.Main.Version != "(devel)" {
141		return info.Main.Version
142	}
143
144	revision := ""
145	modified := false
146	for _, setting := range info.Settings {
147		switch setting.Key {
148		case "vcs.revision":
149			revision = setting.Value
150		case "vcs.modified":
151			modified = setting.Value == "true"
152		}
153	}
154	if revision == "" {
155		return ""
156	}
157	if modified {
158		return revision + "-dirty"
159	}
160
161	return revision
162}