1// SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
2//
3// SPDX-License-Identifier: AGPL-3.0-or-later
4
5// Package mcp provides the MCP server command for lune.
6package mcp
7
8import (
9 "errors"
10 "fmt"
11
12 "git.secluded.site/lune/internal/client"
13 "git.secluded.site/lune/internal/config"
14 "github.com/spf13/cobra"
15)
16
17// Transport constants.
18const (
19 TransportStdio = "stdio"
20 TransportSSE = "sse"
21 TransportHTTP = "http"
22)
23
24var (
25 errUnknownTransport = errors.New("unknown transport; use stdio, sse, or http")
26 errNoToken = errors.New("no access token; run 'lune init' first")
27)
28
29var (
30 transport string
31 host string
32 port int
33)
34
35// Cmd is the mcp command for starting the MCP server.
36var Cmd = &cobra.Command{
37 Use: "mcp",
38 Short: "Start the MCP server",
39 Long: `Start a Model Context Protocol server for LLM tool integration.
40
41The MCP server exposes Lunatask resources and tools that can be used by
42LLM assistants (like Claude) to interact with your Lunatask data.
43
44Transports:
45 stdio - Standard input/output (default, for local integrations)
46 sse - Server-sent events over HTTP
47 http - Streamable HTTP
48
49Examples:
50 lune mcp # Start with stdio (default)
51 lune mcp -t sse # Start SSE server on configured host:port
52 lune mcp -t sse --port 9000 # Override port`,
53 RunE: runMCP,
54}
55
56func init() {
57 Cmd.Flags().StringVarP(&transport, "transport", "t", "",
58 "Transport type: stdio, sse, http (default: stdio or config)")
59 Cmd.Flags().StringVar(&host, "host", "", "Server host (for sse/http)")
60 Cmd.Flags().IntVar(&port, "port", 0, "Server port (for sse/http)")
61}
62
63func runMCP(cmd *cobra.Command, _ []string) error {
64 cfg, err := loadConfig()
65 if err != nil {
66 return err
67 }
68
69 token, err := client.GetToken()
70 if err != nil {
71 return fmt.Errorf("getting access token: %w", err)
72 }
73
74 if token == "" {
75 return errNoToken
76 }
77
78 mcpServer := newMCPServer(cfg, token)
79
80 effectiveTransport := resolveTransport(cfg)
81
82 switch effectiveTransport {
83 case TransportStdio:
84 return runStdio(mcpServer)
85 case TransportSSE:
86 return runSSE(cmd, mcpServer, cfg)
87 case TransportHTTP:
88 return runHTTP(cmd, mcpServer, cfg)
89 default:
90 return errUnknownTransport
91 }
92}
93
94func loadConfig() (*config.Config, error) {
95 cfg, err := config.Load()
96 if err != nil {
97 if errors.Is(err, config.ErrNotFound) {
98 cfg = &config.Config{}
99 } else {
100 return nil, fmt.Errorf("loading config: %w", err)
101 }
102 }
103
104 cfg.MCP.MCPDefaults()
105
106 return cfg, nil
107}
108
109func resolveTransport(_ *config.Config) string {
110 if transport != "" {
111 return transport
112 }
113
114 return TransportStdio
115}
116
117func resolveHost(cfg *config.Config) string {
118 if host != "" {
119 return host
120 }
121
122 return cfg.MCP.Host
123}
124
125func resolvePort(cfg *config.Config) int {
126 if port != 0 {
127 return port
128 }
129
130 return cfg.MCP.Port
131}