Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ MCP Server for VictoriaLogs is configured via environment variables:
| `VL_INSTANCE_BEARER_TOKEN` | Authentication token for VictoriaLogs API | No | - | - |
| `MCP_SERVER_MODE` | Server operation mode. See [Modes](#modes) for details. | No | `stdio` | `stdio`, `sse`, `http` |
| `MCP_LISTEN_ADDR` | Address for SSE or HTTP server to listen on | No | `localhost:8081` | - |
| `MCP_PATH_PREFIX` | Path prefix for all endpoints (useful for ingress deployments) | No | - | - |
| `MCP_DISABLED_TOOLS` | Comma-separated list of tools to disable | No | - | - |
| `MCP_HEARTBEAT_INTERVAL` | Defines the heartbeat interval for the streamable-http protocol. <br /> It means the MCP server will send a heartbeat to the client through the GET connection, <br /> to keep the connection alive from being closed by the network infrastructure (e.g. gateways) | No | `30s` | - |

Expand All @@ -176,6 +177,9 @@ export VL_INSTANCE_ENTRYPOINT="https://play-vmlogs.victoriametrics.com"
export MCP_SERVER_MODE="sse"
export MCP_SSE_ADDR="0.0.0.0:8081"
export MCP_DISABLED_TOOLS="hits,facets"

# Path prefix for ingress deployments
export MCP_PATH_PREFIX="/victoria-logs/production"
```

## Endpoints
Expand All @@ -190,6 +194,8 @@ In SSE and HTTP modes the MCP server provides the following endpoints:
| `/health/liveness` | Liveness check endpoint to ensure the server is running |
| `/health/readiness` | Readiness check endpoint to ensure the server is ready to accept requests |

**Note:** When `MCP_PATH_PREFIX` is configured, all endpoints will be prefixed with the specified path. For example, with `MCP_PATH_PREFIX="/victoria-logs/production"`, the `/metrics` endpoint becomes `/victoria-logs/production/metrics`.

## Setup in clients

### Cursor
Expand Down
6 changes: 6 additions & 0 deletions cmd/mcp-victorialogs/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type Config struct {
bearerToken string
disabledTools map[string]bool
heartbeatInterval time.Duration
pathPrefix string

entryPointURL *url.URL
}
Expand Down Expand Up @@ -51,6 +52,7 @@ func InitConfig() (*Config, error) {
bearerToken: os.Getenv("VL_INSTANCE_BEARER_TOKEN"),
disabledTools: disabledToolsMap,
heartbeatInterval: heartbeatInterval,
pathPrefix: os.Getenv("MCP_PATH_PREFIX"),
}
// Left for backward compatibility
if result.listenAddr == "" {
Expand Down Expand Up @@ -117,3 +119,7 @@ func (c *Config) HeartbeatInterval() time.Duration {
}
return c.heartbeatInterval
}

func (c *Config) PathPrefix() string {
return c.pathPrefix
}
32 changes: 27 additions & 5 deletions cmd/mcp-victorialogs/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (
"net/http"
"os"
"os/signal"
"path"
"strings"
"sync/atomic"
"syscall"
"time"
Expand All @@ -35,6 +37,20 @@ const (
_readinessDrainDelay = 3 * time.Second
)

// buildPath constructs a path with the configured prefix
func buildPath(prefix, endpoint string) string {
if prefix == "" {
return endpoint
}
// Ensure prefix starts with / and doesn't end with /
prefix = strings.TrimSuffix(prefix, "/")
if !strings.HasPrefix(prefix, "/") {
prefix = "/" + prefix
}
// Join the paths properly
return path.Join(prefix, endpoint)
}

func main() {
c, err := config.InitConfig()
if err != nil {
Expand Down Expand Up @@ -101,17 +117,19 @@ Try not to second guess information - if you don't know something or lack inform
defer stop()

mux := http.NewServeMux()
mux.HandleFunc("/metrics", func(w http.ResponseWriter, _ *http.Request) {
pathPrefix := c.PathPrefix()

mux.HandleFunc(buildPath(pathPrefix, "/metrics"), func(w http.ResponseWriter, _ *http.Request) {
ms.WritePrometheus(w)
metrics.WriteProcessMetrics(w)
})
mux.HandleFunc("/health/liveness", func(w http.ResponseWriter, _ *http.Request) {
mux.HandleFunc(buildPath(pathPrefix, "/health/liveness"), func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff")
_, _ = w.Write([]byte("OK\n"))
})
mux.HandleFunc("/health/readiness", func(w http.ResponseWriter, _ *http.Request) {
mux.HandleFunc(buildPath(pathPrefix, "/health/readiness"), func(w http.ResponseWriter, _ *http.Request) {
if !isReady.Load() {
http.Error(w, "Not ready", http.StatusServiceUnavailable)
}
Expand All @@ -124,14 +142,18 @@ Try not to second guess information - if you don't know something or lack inform
switch c.ServerMode() {
case "sse":
log.Printf("Starting server in SSE mode on %s", c.ListenAddr())
srv := server.NewSSEServer(s)
var sseOptions []server.SSEOption
if pathPrefix != "" {
sseOptions = append(sseOptions, server.WithStaticBasePath(pathPrefix))
}
srv := server.NewSSEServer(s, sseOptions...)
mux.Handle(srv.CompleteSsePath(), srv.SSEHandler())
mux.Handle(srv.CompleteMessagePath(), srv.MessageHandler())
case "http":
log.Printf("Starting server in HTTP mode on %s", c.ListenAddr())
heartBeatOption := server.WithHeartbeatInterval(c.HeartbeatInterval())
srv := server.NewStreamableHTTPServer(s, heartBeatOption)
mux.Handle("/mcp", srv)
mux.Handle(buildPath(pathPrefix, "/mcp"), srv)
default:
log.Fatalf("Unknown server mode: %s", c.ServerMode())
}
Expand Down