Skip to content
Draft
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
9 changes: 9 additions & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ linters:
- bodyclose
- gocritic
- gosec
- godox
- makezero
- misspell
- nakedret
- revive
- unused
exclusions:
generated: lax
presets:
Expand All @@ -19,6 +21,13 @@ linters:
- legacy
- std-error-handling
settings:
godox:
keywords:
- TODO
- FIXME
- HACK
- BUG
- XXX
staticcheck:
checks:
- "all"
Expand Down
33 changes: 33 additions & 0 deletions cmd/neo4j-mcp/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ func main() {
fmt.Printf("neo4j-mcp version: %s\n", Version)
return
}

// Handle help flag
if len(os.Args) > 1 && (os.Args[1] == "-h" || os.Args[1] == "--help") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the case we want to merge this, I would move the flags logic outside the main file

printHelp()
return
}

// get config from environment variables
cfg, err := config.LoadConfig()
if err != nil {
Expand Down Expand Up @@ -70,3 +77,29 @@ func main() {
return // so that defer can run
}
}

func printHelp() {
log.Printf("Neo4j MCP Server")
log.Printf("\nUsage:")
log.Printf(" neo4j-mcp [flags]")
log.Printf("\nFlags:")
log.Printf(" -v Show version")
log.Printf(" -h, --help Show this help message")
log.Printf("\nEnvironment Variables:")
log.Printf(" NEO4J_URI Neo4j connection URI (default: bolt://localhost:7687)")
log.Printf(" NEO4J_USERNAME Neo4j username (default: neo4j)")
log.Printf(" NEO4J_PASSWORD Neo4j password (default: password)")
log.Printf(" NEO4J_DATABASE Neo4j database name (default: neo4j)")
log.Printf(" MCP_TRANSPORT Transport mode: 'stdio' or 'http' (default: stdio)")
log.Printf("\nHTTP Mode Environment Variables (when MCP_TRANSPORT=http):")
log.Printf(" MCP_HTTP_HOST HTTP server host (default: 127.0.0.1)")
log.Printf(" MCP_HTTP_PORT HTTP server port (default: 8080)")
log.Printf(" MCP_HTTP_PATH HTTP endpoint path (default: /mcp)")
log.Printf("\nExamples:")
log.Printf(" # Run in stdio mode (default)")
log.Printf(" neo4j-mcp")
log.Printf("\n # Run in HTTP mode")
log.Printf(" MCP_TRANSPORT=http neo4j-mcp")
log.Printf("\n # Run in HTTP mode on custom port")
log.Printf(" MCP_TRANSPORT=http MCP_HTTP_PORT=9000 neo4j-mcp")
}
6 changes: 5 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
module github.com/neo4j/mcp

go 1.25.1
go 1.25.3

require (
github.com/auth0/go-jwt-middleware/v2 v2.3.0
github.com/mark3labs/mcp-go v0.41.1
github.com/neo4j/neo4j-go-driver/v5 v5.28.4
go.uber.org/mock v0.6.0
Expand All @@ -17,5 +18,8 @@ require (
github.com/spf13/cast v1.10.0 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
golang.org/x/crypto v0.43.0 // indirect
golang.org/x/sync v0.17.0 // indirect
gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
16 changes: 12 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/auth0/go-jwt-middleware/v2 v2.3.0 h1:4QREj6cS3d8dS05bEm443jhnqQF97FX9sMBeWqnNRzE=
github.com/auth0/go-jwt-middleware/v2 v2.3.0/go.mod h1:dL4ObBs1/dj4/W4cYxd8rqAdDGXYyd5rqbpMIxcbVrU=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
Expand All @@ -6,8 +8,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
Expand All @@ -28,15 +30,21 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/go-jose/go-jose.v2 v2.6.3 h1:nt80fvSDlhKWQgSWyHyy5CfmlQr+asih51R8PTWNKKs=
gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
87 changes: 79 additions & 8 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,32 @@ package config

import (
"fmt"
"log"
"os"
"strings"
)

// TransportMode defines the transport mode for the MCP server
type TransportMode string

const (
TransportStdio TransportMode = "stdio"
TransportHTTP TransportMode = "http"
)

// Config holds the application configuration
type Config struct {
URI string
Username string
Password string
Database string
URI string
Username string
Password string
Database string
TransportMode TransportMode
HTTPHost string
HTTPPort string
HTTPPath string
AllowedOrigins []string
Auth0Domain string
ResourceIdentifier string // RFC 8707: Unique identifier for this resource server
}

// Validate validates the configuration and returns an error if invalid
Expand Down Expand Up @@ -39,17 +56,52 @@ func (c *Config) Validate() error {

// LoadConfig loads configuration from environment variables with defaults
func LoadConfig() (*Config, error) {
transportMode := TransportMode(getEnvWithDefault("MCP_TRANSPORT", string(TransportStdio)))

// Default allowed origins for local development
defaultOrigins := "http://localhost,http://127.0.0.1,https://localhost,https://127.0.0.1"
allowedOriginsStr := getEnvWithDefault("MCP_ALLOWED_ORIGINS", defaultOrigins)
allowedOrigins := parseAllowedOrigins(allowedOriginsStr)

// Load Auth0 configuration
auth0Domain := os.Getenv("AUTH0_DOMAIN")
resourceIdentifier := os.Getenv("MCP_RESOURCE_IDENTIFIER")

cfg := &Config{
URI: getEnvWithDefault("NEO4J_URI", "bolt://localhost:7687"),
Username: getEnvWithDefault("NEO4J_USERNAME", "neo4j"),
Password: getEnvWithDefault("NEO4J_PASSWORD", "password"),
Database: getEnvWithDefault("NEO4J_DATABASE", "neo4j"),
URI: getEnvWithDefault("NEO4J_URI", "bolt://localhost:7687"),
Username: getEnvWithDefault("NEO4J_USERNAME", "neo4j"),
Password: getEnvWithDefault("NEO4J_PASSWORD", "password"),
Database: getEnvWithDefault("NEO4J_DATABASE", "neo4j"),
TransportMode: transportMode,
HTTPHost: getEnvWithDefault("MCP_HTTP_HOST", "127.0.0.1"),
HTTPPort: getEnvWithDefault("MCP_HTTP_PORT", "8080"),
HTTPPath: getEnvWithDefault("MCP_HTTP_PATH", "/mcp"),
AllowedOrigins: allowedOrigins,
Auth0Domain: auth0Domain,
ResourceIdentifier: resourceIdentifier,
}

if err := cfg.Validate(); err != nil {
return nil, fmt.Errorf("invalid configuration: %w", err)
}

// Warn if binding to all interfaces in HTTP mode
if cfg.TransportMode == TransportHTTP {
if cfg.HTTPHost == "0.0.0.0" || cfg.HTTPHost == "" {
log.Println("WARNING: HTTP server is configured to bind to all network interfaces (0.0.0.0)")
log.Println("WARNING: For security, consider binding to localhost (127.0.0.1) instead")
log.Println("WARNING: Set MCP_HTTP_HOST=127.0.0.1 to bind only to localhost")
}

// Validate Auth0 configuration for HTTP mode
if cfg.Auth0Domain == "" || cfg.ResourceIdentifier == "" {
log.Println("WARNING: Auth0 authentication is not configured")
log.Println("WARNING: Set AUTH0_DOMAIN and MCP_RESOURCE_IDENTIFIER environment variables")
log.Println("WARNING: For RFC 8707 compliance, MCP_RESOURCE_IDENTIFIER should be this server's unique URL")
log.Println("WARNING: HTTP server will start but authentication will be disabled")
}
}

return cfg, nil
}

Expand All @@ -59,3 +111,22 @@ func getEnvWithDefault(key, defaultValue string) string {
}
return defaultValue
}

// parseAllowedOrigins parses a comma-separated list of allowed origins
func parseAllowedOrigins(originsStr string) []string {
if originsStr == "" {
return []string{}
}

parts := strings.Split(originsStr, ",")
origins := make([]string, 0, len(parts))

for _, part := range parts {
trimmed := strings.TrimSpace(part)
if trimmed != "" {
origins = append(origins, trimmed)
}
}

return origins
}
Loading
Loading