| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184 |
- // Command wikijs-mcp exposes a WikiJS instance as a Model Context Protocol
- // server over stdio or streamable HTTP.
- package main
- import (
- "context"
- "errors"
- "flag"
- "fmt"
- "log/slog"
- "net/http"
- "os"
- "os/signal"
- "strings"
- "syscall"
- "gogs.netdome.biz/paul/go-wikijs-mcp/internal/config"
- "gogs.netdome.biz/paul/go-wikijs-mcp/internal/server"
- "gogs.netdome.biz/paul/go-wikijs-mcp/internal/transport"
- "gogs.netdome.biz/paul/go-wikijs-mcp/internal/wikijs"
- "github.com/modelcontextprotocol/go-sdk/mcp"
- )
- // version is set at build time with -ldflags "-X main.version=...".
- var version = "dev"
- func main() {
- if err := run(os.Args[1:]); err != nil {
- fmt.Fprintf(os.Stderr, "wikijs-mcp: %v\n", err)
- os.Exit(1)
- }
- }
- func run(args []string) error {
- envFile, useEnvFile, err := envFileFromArgs(args)
- if err != nil {
- return err
- }
- if useEnvFile {
- if err := config.LoadEnvFile(envFile); err != nil {
- return err
- }
- } else if err := config.LoadDotEnv(); err != nil {
- return err
- }
- cfg, err := config.Load()
- if err != nil {
- return err
- }
- fs := flag.NewFlagSet("wikijs-mcp", flag.ContinueOnError)
- fs.SetOutput(os.Stderr)
- fs.Usage = func() {
- out := fs.Output()
- fmt.Fprintln(out, "Usage: wikijs-mcp [flags]")
- fmt.Fprintln(out)
- fmt.Fprintln(out, "WikiJS connection (environment):")
- fmt.Fprintln(out, " WIKIJS_URL Base URL of the WikiJS instance (required)")
- fmt.Fprintln(out, " WIKIJS_API_KEY WikiJS API key (required)")
- fmt.Fprintln(out, " WIKIJS_ENABLE_EDIT Expose create_page and edit_page (default false)")
- fmt.Fprintln(out, " WIKIJS_TIMEOUT GraphQL request timeout (default 30s)")
- fmt.Fprintln(out)
- fmt.Fprintln(out, "Other environment:")
- fmt.Fprintln(out, " ENV_FILE env file to load (default \".env\")")
- fmt.Fprintln(out, " MCP_AUTH_TOKEN bearer token for HTTP (required for non-loopback)")
- fmt.Fprintln(out)
- fmt.Fprintln(out, "Flags:")
- fs.PrintDefaults()
- }
- fs.String("env-file", "", "env file to load (overrides ENV_FILE and .env)")
- fs.StringVar(&cfg.Transport, "transport", cfg.Transport, `"stdio" or "http"`)
- fs.StringVar(&cfg.Addr, "addr", cfg.Addr, "listen address for the HTTP transport")
- fs.StringVar(&cfg.Path, "path", cfg.Path, "MCP endpoint path for the HTTP transport")
- fs.BoolVar(&cfg.Stateless, "stateless", cfg.Stateless, "disable MCP sessions on the HTTP transport")
- logLevel := strings.ToLower(cfg.LogLevel.String())
- fs.StringVar(&logLevel, "log-level", logLevel, "debug, info, warn, or error")
- showVersion := fs.Bool("version", false, "print the version and exit")
- if err := fs.Parse(args); err != nil {
- if errors.Is(err, flag.ErrHelp) {
- return nil
- }
- return err
- }
- if *showVersion {
- fmt.Println(version)
- return nil
- }
- if fs.NArg() > 0 {
- return fmt.Errorf("unexpected arguments: %v", fs.Args())
- }
- if cfg.LogLevel, err = config.ParseLogLevel(logLevel); err != nil {
- return err
- }
- if err := cfg.Validate(); err != nil {
- return err
- }
- logger := newLogger(cfg.LogLevel)
- protocolLogger := newLogger(slog.LevelWarn)
- if cfg.LogLevel == slog.LevelDebug {
- protocolLogger = logger
- }
- wikiClient, err := wikijs.New(cfg.WikiJSURL, cfg.WikiJSAPIKey,
- wikijs.WithHTTPClient(&http.Client{Timeout: cfg.Timeout}),
- wikijs.WithLogger(logger),
- wikijs.WithUserAgent("wikijs-mcp/"+version),
- )
- if err != nil {
- return err
- }
- mcpServer := server.New(wikiClient, server.Options{
- Version: version,
- EnableEdit: cfg.EnableEdit,
- Logger: protocolLogger,
- })
- ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
- defer stop()
- switch cfg.Transport {
- case config.TransportStdio:
- logger.Info("WikiJS MCP server running", "transport", "stdio", "version", version, "edit", cfg.EnableEdit)
- if err := mcpServer.Run(ctx, &mcp.StdioTransport{}); err != nil && !errors.Is(err, context.Canceled) {
- return fmt.Errorf("stdio server: %w", err)
- }
- return nil
- case config.TransportHTTP:
- return transport.ServeHTTP(ctx, transport.HTTPOptions{
- Addr: cfg.Addr,
- Path: cfg.Path,
- Stateless: cfg.Stateless,
- AuthToken: cfg.AuthToken,
- Logger: logger,
- ProtocolLogger: protocolLogger,
- NewServer: func(*http.Request) *mcp.Server { return mcpServer },
- })
- default:
- return fmt.Errorf("unsupported transport %q", cfg.Transport)
- }
- }
- func newLogger(level slog.Level) *slog.Logger {
- return slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
- Level: level,
- }))
- }
- // envFileFromArgs finds --env-file before flag parsing, because the env file
- // must be loaded before the configuration is read. The regular flag set also
- // defines --env-file so it appears in help and is parsed consistently.
- func envFileFromArgs(args []string) (path string, found bool, err error) {
- for i := 0; i < len(args); i++ {
- arg := args[i]
- if arg == "--" {
- break
- }
- switch {
- case arg == "--env-file" || arg == "-env-file":
- if i+1 >= len(args) {
- return "", true, fmt.Errorf("flag needs an argument: %s", arg)
- }
- path = args[i+1]
- found = true
- i++
- case strings.HasPrefix(arg, "--env-file="):
- path = strings.TrimPrefix(arg, "--env-file=")
- found = true
- case strings.HasPrefix(arg, "-env-file="):
- path = strings.TrimPrefix(arg, "-env-file=")
- found = true
- }
- }
- if found && strings.TrimSpace(path) == "" {
- return "", true, errors.New("--env-file must not be empty")
- }
- return path, found, nil
- }
|