mcp.go

  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}