109 lines
2.6 KiB
Go
109 lines
2.6 KiB
Go
// Vikunja MCP - Developed by acidvegas in Go (https://git.acid.vegas)
|
|
// vikunja-mcp/main.go
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
_ "embed"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
)
|
|
|
|
//go:embed instructions.txt
|
|
var instructions string
|
|
|
|
type config struct {
|
|
baseURL string
|
|
token string
|
|
httpClient *http.Client
|
|
}
|
|
|
|
func main() {
|
|
log.SetFlags(0)
|
|
log.SetPrefix("vikunja-mcp: ")
|
|
|
|
vikunjaURL := strings.TrimRight(os.Getenv("VIKUNJA_URL"), "/")
|
|
if vikunjaURL == "" {
|
|
log.Fatal("VIKUNJA_URL is not set")
|
|
}
|
|
token := os.Getenv("VIKUNJA_TOKEN")
|
|
if token == "" {
|
|
log.Fatal("VIKUNJA_TOKEN is not set")
|
|
}
|
|
|
|
defaultTransport := envOr("VIKUNJA_MCP_TRANSPORT", "stdio")
|
|
defaultHost := envOr("VIKUNJA_MCP_HOST", "127.0.0.1")
|
|
defaultPort := 8000
|
|
if v := os.Getenv("VIKUNJA_MCP_PORT"); v != "" {
|
|
if n, err := strconv.Atoi(v); err == nil {
|
|
defaultPort = n
|
|
}
|
|
}
|
|
|
|
var transport, host string
|
|
var port int
|
|
flag.StringVar(&transport, "transport", defaultTransport, "Transport: stdio or http")
|
|
flag.StringVar(&transport, "t", defaultTransport, "Transport (shorthand)")
|
|
flag.StringVar(&host, "host", defaultHost, "Host for http transport")
|
|
flag.IntVar(&port, "port", defaultPort, "Port for http transport")
|
|
flag.Parse()
|
|
|
|
cfg := &config{
|
|
baseURL: vikunjaURL + "/api/v1",
|
|
token: token,
|
|
httpClient: &http.Client{Timeout: 30 * time.Second},
|
|
}
|
|
|
|
spec, err := loadSpec(cfg.baseURL + "/docs.json")
|
|
if err != nil {
|
|
log.Fatalf("spec load failed: %v", err)
|
|
}
|
|
patchSpec(spec)
|
|
|
|
server := mcp.NewServer(
|
|
&mcp.Implementation{Name: "vikunja", Version: "1.0.0"},
|
|
&mcp.ServerOptions{Instructions: instructions},
|
|
)
|
|
|
|
n := registerTools(server, cfg, spec)
|
|
log.Printf("loaded %d tools from vikunja spec", n)
|
|
|
|
switch strings.ToLower(transport) {
|
|
case "stdio":
|
|
if err := server.Run(context.Background(), &mcp.StdioTransport{}); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
case "http":
|
|
handler := mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server { return server }, nil)
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/health", func(w http.ResponseWriter, _ *http.Request) {
|
|
w.Header().Set("Content-Type", "text/plain")
|
|
_, _ = w.Write([]byte("ok"))
|
|
})
|
|
mux.Handle("/", handler)
|
|
addr := fmt.Sprintf("%s:%d", host, port)
|
|
log.Printf("http listening on %s", addr)
|
|
if err := http.ListenAndServe(addr, mux); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
default:
|
|
log.Fatalf("unknown transport: %s (use stdio or http)", transport)
|
|
}
|
|
}
|
|
|
|
func envOr(key, fallback string) string {
|
|
if v := os.Getenv(key); v != "" {
|
|
return v
|
|
}
|
|
return fallback
|
|
}
|