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}