diff --git a/.changes/unreleased/Minor-20260818-110500.yaml b/.changes/unreleased/Minor-20260818-110500.yaml new file mode 100644 index 00000000..9c17ea6c --- /dev/null +++ b/.changes/unreleased/Minor-20260818-110500.yaml @@ -0,0 +1,46 @@ +kind: Minor +body: | + Deprecates the existing configuration environment variables and `--neo4j-*` CLI flags in favor of consistently scoped `NEO4J_MCP_*` environment variables and concise CLI flags. + + Deprecated environment variables: + + | Deprecated | Canonical replacement | + | --- | --- | + | `NEO4J_URI` | `NEO4J_MCP_URI` | + | `NEO4J_USERNAME` | `NEO4J_MCP_USERNAME` | + | `NEO4J_PASSWORD` | `NEO4J_MCP_PASSWORD` | + | `NEO4J_DATABASE` | `NEO4J_MCP_DATABASE` | + | `NEO4J_READ_ONLY` | `NEO4J_MCP_READ_ONLY` | + | `NEO4J_TELEMETRY` | `NEO4J_MCP_TELEMETRY` | + | `NEO4J_LOG_LEVEL` | `NEO4J_MCP_LOG_LEVEL` | + | `NEO4J_LOG_FORMAT` | `NEO4J_MCP_LOG_FORMAT` | + | `NEO4J_SCHEMA_SAMPLE_SIZE` | `NEO4J_MCP_SCHEMA_SAMPLE_SIZE` | + | `NEO4J_TRANSPORT_MODE` | `NEO4J_MCP_TRANSPORT_MODE` | + | `NEO4J_MCP_TRANSPORT` | `NEO4J_MCP_TRANSPORT_MODE` | + | `NEO4J_HTTP_AUTH_HEADER_NAME` | `NEO4J_MCP_HTTP_AUTH_HEADER_NAME` | + | `NEO4J_HTTP_ALLOW_UNAUTHENTICATED_PING` | `NEO4J_MCP_HTTP_ALLOW_UNAUTHENTICATED_PING` | + | `NEO4J_HTTP_ALLOW_UNAUTHENTICATED_TOOLS_LIST` | `NEO4J_MCP_HTTP_ALLOW_UNAUTHENTICATED_TOOLS_LIST` | + + Deprecated CLI flags: + + | Deprecated | Canonical replacement | + | --- | --- | + | `--neo4j-uri` | `--uri` | + | `--neo4j-username` | `--username` | + | `--neo4j-password` | `--password` | + | `--neo4j-database` | `--database` | + | `--neo4j-read-only` | `--read-only` | + | `--neo4j-telemetry` | `--telemetry` | + | `--neo4j-schema-sample-size` | `--schema-sample-size` | + | `--neo4j-transport-mode` | `--transport-mode` | + | `--neo4j-http-port` | `--http-port` | + | `--neo4j-http-host` | `--http-host` | + | `--neo4j-http-allowed-origins` | `--http-allowed-origins` | + | `--neo4j-http-tls-enabled` | `--http-tls-enabled` | + | `--neo4j-http-tls-cert-file` | `--http-tls-cert-file` | + | `--neo4j-http-tls-key-file` | `--http-tls-key-file` | + | `--neo4j-http-auth-header-name` | `--http-auth-header-name` | + | `--neo4j-http-allow-unauthenticated-ping` | `--http-allow-unauthenticated-ping` | + | `--neo4j-http-allow-unauthenticated-tools-list` | `--http-allow-unauthenticated-tools-list` | + +time: 2026-08-18T11:05:00+01:00 diff --git a/.env.example b/.env.example index 21675e82..c9a72336 100644 --- a/.env.example +++ b/.env.example @@ -4,7 +4,7 @@ # These environment variables are loaded automatically by Taskfile (dotenv). # Only used when running commands through `task`. -NEO4J_URI=bolt://localhost:7687 -NEO4J_USERNAME=neo4j -NEO4J_PASSWORD=password -NEO4J_DATABASE=neo4j +NEO4J_MCP_URI=bolt://localhost:7687 +NEO4J_MCP_USERNAME=neo4j +NEO4J_MCP_PASSWORD=password +NEO4J_MCP_DATABASE=neo4j diff --git a/.github/workflows/docker-release-alpha.yml b/.github/workflows/docker-release-alpha.yml index 7a66e45f..ec14400d 100644 --- a/.github/workflows/docker-release-alpha.yml +++ b/.github/workflows/docker-release-alpha.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4 - name: Log in to DockerHub uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4 diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index 1cfb54af..5fcb4079 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -44,7 +44,7 @@ jobs: platforms: arm64 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 + uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4 - name: Log in to DockerHub uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e9ed25e9..b4dbb9ce 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,6 +39,18 @@ go run ./cmd/neo4j-mcp go install -C cmd/neo4j-mcp ``` +## User-facing configuration naming (environment variables and CLI args) + +When adding a user-facing configuration setting: + +- Prefix environment variables with `NEO4J_MCP_` and use uppercase snake case, for example `NEO4J_MCP_SCHEMA_SAMPLE_SIZE`. +- Keep CLI flags short and use kebab case. Avoid redundant `neo4j-` or `mcp-` prefixes because the executable already provides that scope, for example `--schema-sample-size`. +- Keep the environment variable and CLI flag names aligned, and update their help text, tests, examples, and changelog together. + +Note: +These conventions apply only to environment variables and CLI flags exposed as supported configuration for users of the Neo4j MCP server. Do not add the `MCP` scope to variables consumed by internal development, test, build, or CI helpers, because those variables configure the helper rather than the MCP server. +Internal tooling may intentionally use organization-wide names such as `NEO4J_URI`, `NEO4J_USERNAME`, and `NEO4J_PASSWORD` for the Neo4j instance used in CI, alongside controls such as `NEO4J_IMAGE` and `USE_CONTAINER`. The unscoped connection variables are deprecated only when consumed as user-facing MCP server configuration. + ## Mocks We rely on interface-based dependency injection plus generated mocks (gomock) so tests run without a live Neo4j instance. @@ -215,12 +227,6 @@ When adding new database operations: - Tests failing unexpectedly → regenerate mocks, verify env vars, rerun full test suite. - Dependency/build issues → `go mod tidy`. -## Update the MCPB bundle (for Claude Desktop) - -If your changes impact the end-user configuration (e.g., adding new environment variables or modifying tool definitions), you must update the `manifest.json` file. This ensures that integrations like Claude Desktop are aware of the new server configuration. - -For more information refer to the dedicated guide: [the MCPB build documentation](https://neo4j.com/docs/mcp/current/installation#mcpb). - ### Getting help - Check existing [GitHub Issues](https://github.com/neo4j/mcp/issues) diff --git a/README.md b/README.md index 8d0cf3ec..54a88cf7 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ By implementing the Model Context Protocol (MCP), it acts as a bridge between an ## Tools - `get-schema` — introspect labels, relationship types, property keys -- `read-cypher` — execute read-only Cypher queries -- `write-cypher` — execute write Cypher queries (disabled if `NEO4J_READ_ONLY=true`) +- `read-cypher` — execute read-only Cypher queries that do not modify database data, enforced via `EXPLAIN` and Neo4j's query-type classification. **Note:** custom procedures or functions incorrectly classified as read-only by Neo4j may bypass this check; ensuring correct classification is the responsibility of the procedure/function maintainer. +- `write-cypher` — execute write Cypher queries (disabled if `NEO4J_MCP_READ_ONLY=true`) - `list-gds-procedures` — list available GDS procedures ## Migrating from v1 to v2 @@ -74,10 +74,10 @@ Create / edit `mcp.json`: "command": "python", "args": ["-m", "neo4j_mcp_server"], "env": { - "NEO4J_URI": "bolt://localhost:7687", - "NEO4J_USERNAME": "neo4j", - "NEO4J_PASSWORD": "password", - "NEO4J_DATABASE": "neo4j" + "NEO4J_MCP_URI": "bolt://localhost:7687", + "NEO4J_MCP_USERNAME": "neo4j", + "NEO4J_MCP_PASSWORD": "password", + "NEO4J_MCP_DATABASE": "neo4j", } } } diff --git a/cmd/neo4j-mcp/main.go b/cmd/neo4j-mcp/main.go index ee18bab3..392ab4d9 100644 --- a/cmd/neo4j-mcp/main.go +++ b/cmd/neo4j-mcp/main.go @@ -95,7 +95,7 @@ func main() { if cfg.Telemetry && Version != "development" { anService.Enable() log.Println("Telemetry is enabled to help us improve the product by collecting anonymous usage data such as: tools being used, the operating system, and CPU architecture.") - log.Println("To disable telemetry, set the NEO4J_TELEMETRY environment variable to \"false\".") + log.Println("To disable telemetry, set the NEO4J_MCP_TELEMETRY environment variable to \"false\" or use --telemetry false.") } else { log.Println("Telemetry disabled.") anService.Disable() diff --git a/docs/TLS_SETUP.md b/docs/TLS_SETUP.md index fbf6f6b7..505d1d4a 100644 --- a/docs/TLS_SETUP.md +++ b/docs/TLS_SETUP.md @@ -45,18 +45,20 @@ openssl req -x509 -newkey rsa:4096 \ ```bash # Default port 443 when TLS is enabled ./bin/neo4j-mcp \ - --neo4j-transport-mode http \ - --neo4j-http-tls-enabled true \ - --neo4j-http-tls-cert-file cert.pem \ - --neo4j-http-tls-key-file key.pem + --uri bolt://localhost:7687 \ + --transport-mode http \ + --http-tls-enabled true \ + --http-tls-cert-file cert.pem \ + --http-tls-key-file key.pem # Or specify a custom port like 8443 ./bin/neo4j-mcp \ - --neo4j-transport-mode http \ - --neo4j-http-port 8443 \ - --neo4j-http-tls-enabled true \ - --neo4j-http-tls-cert-file cert.pem \ - --neo4j-http-tls-key-file key.pem + --uri bolt://localhost:7687 \ + --transport-mode http \ + --http-port 8443 \ + --http-tls-enabled true \ + --http-tls-cert-file cert.pem \ + --http-tls-key-file key.pem ``` @@ -149,4 +151,4 @@ openssl s_client -connect 127.0.0.1:8443 /dev/null | grep "Cipher" - **`-k` flag**: Skips certificate verification (needed for self-signed certificates) - **Basic Auth**: All requests require `-u username:password` - **Content-Type**: MCP requests need `Content-Type: application/json` header -- **Port**: Default port is 443 when TLS is enabled, 80 when TLS is disabled (configurable via `--neo4j-http-port` or `NEO4J_MCP_HTTP_PORT`) \ No newline at end of file +- **Port**: Default port is 443 when TLS is enabled, 80 when TLS is disabled (configurable via `--http-port` or `NEO4J_MCP_HTTP_PORT`) \ No newline at end of file diff --git a/internal/cli/args.go b/internal/cli/args.go index 3670c8ff..a7c66ad8 100644 --- a/internal/cli/args.go +++ b/internal/cli/args.go @@ -22,56 +22,59 @@ Usage: Options: -h, --help Show this help message -v, --version Show version information - --neo4j-uri Neo4j connection URI (overrides environment variable NEO4J_URI) - --neo4j-username Database username (overrides environment variable NEO4J_USERNAME) - --neo4j-password Database password (overrides environment variable NEO4J_PASSWORD) - --neo4j-database Database name (overrides environment variable NEO4J_DATABASE) - --neo4j-read-only Enable read-only mode: true or false (overrides environment variable NEO4J_READ_ONLY) - --neo4j-tools Define tools available by filtering tools returned in tools/list response - --neo4j-telemetry Enable telemetry: true or false (overrides environment variable NEO4J_TELEMETRY) - --neo4j-schema-sample-size Number of nodes to sample for schema inference (overrides environment variable NEO4J_SCHEMA_SAMPLE_SIZE) - --neo4j-transport-mode MCP Transport mode (e.g., 'stdio', 'http') (overrides environment variable NEO4J_TRANSPORT_MODE & NEO4J_MCP_TRANSPORT(deprecated)) - --neo4j-http-port HTTP server port (overrides environment variable NEO4J_MCP_HTTP_PORT) - --neo4j-http-host HTTP server host (overrides environment variable NEO4J_MCP_HTTP_HOST) - --neo4j-http-allowed-origins Comma-separated list of allowed CORS origins (overrides environment variable NEO4J_MCP_HTTP_ALLOWED_ORIGINS) - --neo4j-http-tls-enabled Enable TLS/HTTPS for HTTP server: true or false (overrides environment variable NEO4J_MCP_HTTP_TLS_ENABLED) - --neo4j-http-tls-cert-file Path to TLS certificate file (overrides environment variable NEO4J_MCP_HTTP_TLS_CERT_FILE) - --neo4j-http-tls-key-file Path to TLS private key file (overrides environment variable NEO4J_MCP_HTTP_TLS_KEY_FILE) - --neo4j-http-auth-header-name
Name of the HTTP header to read auth credentials from (overrides NEO4J_HTTP_AUTH_HEADER_NAME) - --neo4j-http-allow-unauthenticated-ping Allow unauthenticated ping health checks: true or false (overrides NEO4J_HTTP_ALLOW_UNAUTHENTICATED_PING) - --neo4j-http-allow-unauthenticated-tools-list Allow unauthenticated tools list: true or false (overrides NEO4J_HTTP_ALLOW_UNAUTHENTICATED_TOOLS_LIST) + --uri Neo4j connection URI (overrides NEO4J_MCP_URI) + --username Database username (overrides NEO4J_MCP_USERNAME) + --password Database password (overrides NEO4J_MCP_PASSWORD) + --database Database name (overrides NEO4J_MCP_DATABASE) + --read-only Enable read-only mode: true or false (overrides NEO4J_MCP_READ_ONLY) + --tools Define tools available by filtering tools returned in tools/list response + --telemetry Enable telemetry: true or false (overrides NEO4J_MCP_TELEMETRY) + --schema-sample-size Number of nodes to sample for schema inference (overrides NEO4J_MCP_SCHEMA_SAMPLE_SIZE) + --transport-mode MCP transport mode: 'stdio' or 'http' (overrides NEO4J_MCP_TRANSPORT_MODE) + --http-port HTTP server port (overrides NEO4J_MCP_HTTP_PORT) + --http-host HTTP server host (overrides NEO4J_MCP_HTTP_HOST) + --http-allowed-origins Comma-separated list of allowed CORS origins (overrides NEO4J_MCP_HTTP_ALLOWED_ORIGINS) + --http-tls-enabled Enable TLS/HTTPS for HTTP server: true or false (overrides NEO4J_MCP_HTTP_TLS_ENABLED) + --http-tls-cert-file Path to TLS certificate file (overrides NEO4J_MCP_HTTP_TLS_CERT_FILE) + --http-tls-key-file Path to TLS private key file (overrides NEO4J_MCP_HTTP_TLS_KEY_FILE) + --http-auth-header-name
Name of the HTTP header to read auth credentials from (overrides NEO4J_MCP_HTTP_AUTH_HEADER_NAME) + --http-allow-unauthenticated-ping Allow unauthenticated ping (overrides NEO4J_MCP_HTTP_ALLOW_UNAUTHENTICATED_PING) + --http-allow-unauthenticated-tools-list Allow unauthenticated tools/list (overrides NEO4J_MCP_HTTP_ALLOW_UNAUTHENTICATED_TOOLS_LIST) --neo4j-request-timeout Maximum duration for a single MCP request, up to 30m. Also caps the per-request X-Neo4j-MCP-Request-Timeout header in HTTP mode (overrides environment variable NEO4J_MCP_REQUEST_TIMEOUT) -Required Environment Variables: - NEO4J_URI Neo4j database URI - NEO4J_USERNAME Database username - NEO4J_PASSWORD Database password +Required Environment Variables (STDIO mode): + NEO4J_MCP_URI Neo4j database URI + NEO4J_MCP_USERNAME Database username + NEO4J_MCP_PASSWORD Database password Optional Environment Variables: - NEO4J_DATABASE Database name (default: neo4j) - NEO4J_TELEMETRY Enable/disable telemetry (default: true) - NEO4J_READ_ONLY Enable read-only mode (default: false) - NEO4J_SCHEMA_SAMPLE_SIZE Number of nodes to sample for schema inference (default: 100) - NEO4J_TRANSPORT_MODE MCP Transport mode (e.g., 'stdio', 'http') (default: stdio) + NEO4J_MCP_DATABASE Database name (default: neo4j) + NEO4J_MCP_TELEMETRY Enable/disable telemetry (default: true) + NEO4J_MCP_READ_ONLY Enable read-only mode (default: false) + NEO4J_MCP_SCHEMA_SAMPLE_SIZE Number of nodes to sample for schema inference (default: 100) + NEO4J_MCP_LOG_LEVEL Log level (default: info) + NEO4J_MCP_LOG_FORMAT Log format: text or json (default: text) NEO4J_MCP_TOOLS Define tools available by filtering tools returned in tools/list response (default: all tools enabled) - NEO4J_MCP_TRANSPORT MCP Transport mode (e.g., 'stdio', 'http') (default: stdio) + NEO4J_MCP_TRANSPORT_MODE MCP transport mode (default: stdio) NEO4J_MCP_HTTP_PORT HTTP server port (default: 443 with TLS, 80 without TLS) NEO4J_MCP_HTTP_HOST HTTP server host (default: 127.0.0.1) NEO4J_MCP_HTTP_ALLOWED_ORIGINS Comma-separated list of allowed CORS origins (optional) NEO4J_MCP_HTTP_TLS_ENABLED Enable TLS/HTTPS for HTTP server (default: false) NEO4J_MCP_HTTP_TLS_CERT_FILE Path to TLS certificate file (required when TLS is enabled) NEO4J_MCP_HTTP_TLS_KEY_FILE Path to TLS private key file (required when TLS is enabled) - NEO4J_HTTP_AUTH_HEADER_NAME Name of the HTTP header to read auth credentials from (default: Authorization) - NEO4J_HTTP_ALLOW_UNAUTHENTICATED_PING Allow unauthenticated ping health checks (default: false) - NEO4J_HTTP_ALLOW_UNAUTHENTICATED_TOOLS_LIST Allow unauthenticated tool listing (default: false) + NEO4J_MCP_HTTP_AUTH_HEADER_NAME Name of the HTTP header to read auth credentials from (default: Authorization) + NEO4J_MCP_HTTP_ALLOW_UNAUTHENTICATED_PING Allow unauthenticated ping health checks (default: false) + NEO4J_MCP_HTTP_ALLOW_UNAUTHENTICATED_TOOLS_LIST Allow unauthenticated tool listing (default: false) NEO4J_MCP_REQUEST_TIMEOUT Maximum duration for a single MCP request (default: 3m, maximum: 30m) +Deprecated environment variables and --neo4j-* flags remain accepted in v1 and emit a warning. They will be removed in v2. The deprecated environment aliases are the previous unscoped names shown in the project changelog. + Examples: # Using environment variables - NEO4J_URI=bolt://localhost:7687 NEO4J_USERNAME=neo4j NEO4J_PASSWORD=password neo4j-mcp + NEO4J_MCP_URI=bolt://localhost:7687 NEO4J_MCP_USERNAME=neo4j NEO4J_MCP_PASSWORD=password NEO4J_MCP_DATABASE=neo4j neo4j-mcp # Using CLI flags (takes precedence over environment variables) - neo4j-mcp --neo4j-uri bolt://localhost:7687 --neo4j-username neo4j --neo4j-password password + neo4j-mcp --uri bolt://localhost:7687 --username neo4j --password password --database neo4j For more information, visit: https://github.com/neo4j/mcp ` @@ -102,35 +105,69 @@ type Args struct { // this is a list of known configuration flags to be skipped in HandleArgs // add new config flags here as needed var argsSlice = []string{ + "--uri", "--neo4j-uri", + "--username", "--neo4j-username", + "--password", "--neo4j-password", + "--database", "--neo4j-database", + "--read-only", "--neo4j-read-only", "--neo4j-tools", + "--telemetry", "--neo4j-telemetry", + "--schema-sample-size", "--neo4j-schema-sample-size", + "--transport-mode", "--neo4j-transport-mode", + "--http-port", "--neo4j-http-port", + "--http-host", "--neo4j-http-host", + "--http-allowed-origins", "--neo4j-http-allowed-origins", + "--http-tls-enabled", "--neo4j-http-tls-enabled", + "--http-tls-cert-file", "--neo4j-http-tls-cert-file", + "--http-tls-key-file", "--neo4j-http-tls-key-file", + "--http-auth-header-name", "--neo4j-http-auth-header-name", + "--http-allow-unauthenticated-ping", "--neo4j-http-allow-unauthenticated-ping", + "--http-allow-unauthenticated-tools-list", "--neo4j-http-allow-unauthenticated-tools-list", "--neo4j-request-timeout", } // ParseConfigFlags parses CLI flags and returns configuration values. // It should be called after HandleArgs to ensure help/version flags are processed first. +const deprecatedFlagMessage = "Warning: deprecated CLI flag %q; use %q instead. Support will be removed in v2.\n" + +func mergeFlagValue(canonical, deprecated *string, canonicalName, deprecatedName string) string { + if *deprecated != "" { + fmt.Fprintf(os.Stderr, deprecatedFlagMessage, deprecatedName, canonicalName) + } + if *canonical != "" { + return *canonical + } + return *deprecated +} + func ParseConfigFlags() *Args { - neo4jURI := flag.String("neo4j-uri", "", "Neo4j connection URI (overrides NEO4J_URI env var)") - neo4jUsername := flag.String("neo4j-username", "", "Neo4j username (overrides NEO4J_USERNAME env var)") - neo4jPassword := flag.String("neo4j-password", "", "Neo4j password (overrides NEO4J_PASSWORD env var)") - neo4jDatabase := flag.String("neo4j-database", "", "Neo4j database name (overrides NEO4J_DATABASE env var)") - neo4jReadOnly := flag.String("neo4j-read-only", "", "Enable read-only mode: true or false (overrides NEO4J_READ_ONLY env var)") + uri := flag.String("uri", "", "Neo4j connection URI (overrides NEO4J_MCP_URI env var)") + neo4jURI := flag.String("neo4j-uri", "", "Deprecated alias for --uri") + username := flag.String("username", "", "Neo4j username (overrides NEO4J_MCP_USERNAME env var)") + neo4jUsername := flag.String("neo4j-username", "", "Deprecated alias for --username") + password := flag.String("password", "", "Neo4j password (overrides NEO4J_MCP_PASSWORD env var)") + neo4jPassword := flag.String("neo4j-password", "", "Deprecated alias for --password") + database := flag.String("database", "", "Neo4j database name (overrides NEO4J_MCP_DATABASE env var)") + neo4jDatabase := flag.String("neo4j-database", "", "Deprecated alias for --database") + readOnly := flag.String("read-only", "", "Enable read-only mode: true or false (overrides NEO4J_MCP_READ_ONLY env var)") + neo4jReadOnly := flag.String("neo4j-read-only", "", "Deprecated alias for --read-only") var neo4jTools *string flag.Func("neo4j-tools", "Define tools available by filtering tools returned in tools/list response", func(s string) error { if s == "" { @@ -139,41 +176,52 @@ func ParseConfigFlags() *Args { neo4jTools = &s return nil }) - neo4jTelemetry := flag.String("neo4j-telemetry", "", "Enable telemetry: true or false (overrides NEO4J_TELEMETRY env var)") - neo4jSchemaSampleSize := flag.String("neo4j-schema-sample-size", "", "Number of nodes to sample for schema inference (overrides NEO4J_SCHEMA_SAMPLE_SIZE env var)") - neo4jTransportMode := flag.String("neo4j-transport-mode", "", "MCP Transport mode (e.g., 'stdio', 'http') (overrides NEO4J_TRANSPORT_MODE env var)") - neo4jHTTPPort := flag.String("neo4j-http-port", "", "HTTP server port (overrides NEO4J_MCP_HTTP_PORT env var)") - neo4jHTTPHost := flag.String("neo4j-http-host", "", "HTTP server host (overrides NEO4J_MCP_HTTP_HOST env var)") - neo4jHTTPAllowedOrigins := flag.String("neo4j-http-allowed-origins", "", "Comma-separated list of allowed CORS origins (overrides NEO4J_MCP_HTTP_ALLOWED_ORIGINS env var)") - neo4jHTTPTLSEnabled := flag.String("neo4j-http-tls-enabled", "", "Enable TLS/HTTPS for HTTP server: true or false (overrides NEO4J_MCP_HTTP_TLS_ENABLED env var)") - neo4jHTTPTLSCertFile := flag.String("neo4j-http-tls-cert-file", "", "Path to TLS certificate file (overrides NEO4J_MCP_HTTP_TLS_CERT_FILE env var)") - neo4jHTTPTLSKeyFile := flag.String("neo4j-http-tls-key-file", "", "Path to TLS private key file (overrides NEO4J_MCP_HTTP_TLS_KEY_FILE env var)") - neo4jAuthHeaderName := flag.String("neo4j-http-auth-header-name", "", "Name of the HTTP header to read auth credentials from (overrides NEO4J_HTTP_AUTH_HEADER_NAME env var)") - neo4jHTTPAllowUnauthenticatedPing := flag.String("neo4j-http-allow-unauthenticated-ping", "", "Allow unauthenticated ping health checks: true or false (overrides NEO4J_HTTP_ALLOW_UNAUTHENTICATED_PING env var)") - neo4jHTTPAllowUnauthenticatedToolsList := flag.String("neo4j-http-allow-unauthenticated-tools-list", "", "Allow unauthenticated tools listing: true or false (overrides NEO4J_HTTP_ALLOW_UNAUTHENTICATED_TOOLS_LIST env var)") + telemetry := flag.String("telemetry", "", "Enable telemetry: true or false (overrides NEO4J_MCP_TELEMETRY env var)") + neo4jTelemetry := flag.String("neo4j-telemetry", "", "Deprecated alias for --telemetry") + schemaSampleSize := flag.String("schema-sample-size", "", "Number of nodes to sample for schema inference (overrides NEO4J_MCP_SCHEMA_SAMPLE_SIZE env var)") + neo4jSchemaSampleSize := flag.String("neo4j-schema-sample-size", "", "Deprecated alias for --schema-sample-size") + transportMode := flag.String("transport-mode", "", "MCP transport mode: stdio or http (overrides NEO4J_MCP_TRANSPORT_MODE env var)") + neo4jTransportMode := flag.String("neo4j-transport-mode", "", "Deprecated alias for --transport-mode") + httpPort := flag.String("http-port", "", "HTTP server port (overrides NEO4J_MCP_HTTP_PORT env var)") + neo4jHTTPPort := flag.String("neo4j-http-port", "", "Deprecated alias for --http-port") + httpHost := flag.String("http-host", "", "HTTP server host (overrides NEO4J_MCP_HTTP_HOST env var)") + neo4jHTTPHost := flag.String("neo4j-http-host", "", "Deprecated alias for --http-host") + httpAllowedOrigins := flag.String("http-allowed-origins", "", "Comma-separated list of allowed CORS origins (overrides NEO4J_MCP_HTTP_ALLOWED_ORIGINS env var)") + neo4jHTTPAllowedOrigins := flag.String("neo4j-http-allowed-origins", "", "Deprecated alias for --http-allowed-origins") + httpTLSEnabled := flag.String("http-tls-enabled", "", "Enable TLS/HTTPS for HTTP server: true or false (overrides NEO4J_MCP_HTTP_TLS_ENABLED env var)") + neo4jHTTPTLSEnabled := flag.String("neo4j-http-tls-enabled", "", "Deprecated alias for --http-tls-enabled") + httpTLSCertFile := flag.String("http-tls-cert-file", "", "Path to TLS certificate file (overrides NEO4J_MCP_HTTP_TLS_CERT_FILE env var)") + neo4jHTTPTLSCertFile := flag.String("neo4j-http-tls-cert-file", "", "Deprecated alias for --http-tls-cert-file") + httpTLSKeyFile := flag.String("http-tls-key-file", "", "Path to TLS private key file (overrides NEO4J_MCP_HTTP_TLS_KEY_FILE env var)") + neo4jHTTPTLSKeyFile := flag.String("neo4j-http-tls-key-file", "", "Deprecated alias for --http-tls-key-file") + authHeaderName := flag.String("http-auth-header-name", "", "Name of the HTTP header to read auth credentials from (overrides NEO4J_MCP_HTTP_AUTH_HEADER_NAME env var)") + neo4jAuthHeaderName := flag.String("neo4j-http-auth-header-name", "", "Deprecated alias for --http-auth-header-name") + allowUnauthenticatedPing := flag.String("http-allow-unauthenticated-ping", "", "Allow unauthenticated ping: true or false (overrides NEO4J_MCP_HTTP_ALLOW_UNAUTHENTICATED_PING env var)") + neo4jHTTPAllowUnauthenticatedPing := flag.String("neo4j-http-allow-unauthenticated-ping", "", "Deprecated alias for --http-allow-unauthenticated-ping") + allowUnauthenticatedToolsList := flag.String("http-allow-unauthenticated-tools-list", "", "Allow unauthenticated tools/list: true or false (overrides NEO4J_MCP_HTTP_ALLOW_UNAUTHENTICATED_TOOLS_LIST env var)") + neo4jHTTPAllowUnauthenticatedToolsList := flag.String("neo4j-http-allow-unauthenticated-tools-list", "", "Deprecated alias for --http-allow-unauthenticated-tools-list") neo4jRequestTimeout := flag.String("neo4j-request-timeout", "", "Maximum duration for a single MCP request, up to 30m (overrides NEO4J_MCP_REQUEST_TIMEOUT env var)") - flag.Parse() return &Args{ - URI: *neo4jURI, - Username: *neo4jUsername, - Password: *neo4jPassword, - Database: *neo4jDatabase, - ReadOnly: *neo4jReadOnly, + URI: mergeFlagValue(uri, neo4jURI, "--uri", "--neo4j-uri"), + Username: mergeFlagValue(username, neo4jUsername, "--username", "--neo4j-username"), + Password: mergeFlagValue(password, neo4jPassword, "--password", "--neo4j-password"), + Database: mergeFlagValue(database, neo4jDatabase, "--database", "--neo4j-database"), + ReadOnly: mergeFlagValue(readOnly, neo4jReadOnly, "--read-only", "--neo4j-read-only"), Tools: neo4jTools, - Telemetry: *neo4jTelemetry, - SchemaSampleSize: *neo4jSchemaSampleSize, - TransportMode: *neo4jTransportMode, - HTTPPort: *neo4jHTTPPort, - HTTPHost: *neo4jHTTPHost, - HTTPAllowedOrigins: *neo4jHTTPAllowedOrigins, - HTTPTLSEnabled: *neo4jHTTPTLSEnabled, - HTTPTLSCertFile: *neo4jHTTPTLSCertFile, - HTTPTLSKeyFile: *neo4jHTTPTLSKeyFile, - HTTPAllowUnauthenticatedPing: *neo4jHTTPAllowUnauthenticatedPing, - HTTPAllowUnauthenticatedToolsList: *neo4jHTTPAllowUnauthenticatedToolsList, - AuthHeaderName: *neo4jAuthHeaderName, + Telemetry: mergeFlagValue(telemetry, neo4jTelemetry, "--telemetry", "--neo4j-telemetry"), + SchemaSampleSize: mergeFlagValue(schemaSampleSize, neo4jSchemaSampleSize, "--schema-sample-size", "--neo4j-schema-sample-size"), + TransportMode: mergeFlagValue(transportMode, neo4jTransportMode, "--transport-mode", "--neo4j-transport-mode"), + HTTPPort: mergeFlagValue(httpPort, neo4jHTTPPort, "--http-port", "--neo4j-http-port"), + HTTPHost: mergeFlagValue(httpHost, neo4jHTTPHost, "--http-host", "--neo4j-http-host"), + HTTPAllowedOrigins: mergeFlagValue(httpAllowedOrigins, neo4jHTTPAllowedOrigins, "--http-allowed-origins", "--neo4j-http-allowed-origins"), + HTTPTLSEnabled: mergeFlagValue(httpTLSEnabled, neo4jHTTPTLSEnabled, "--http-tls-enabled", "--neo4j-http-tls-enabled"), + HTTPTLSCertFile: mergeFlagValue(httpTLSCertFile, neo4jHTTPTLSCertFile, "--http-tls-cert-file", "--neo4j-http-tls-cert-file"), + HTTPTLSKeyFile: mergeFlagValue(httpTLSKeyFile, neo4jHTTPTLSKeyFile, "--http-tls-key-file", "--neo4j-http-tls-key-file"), + HTTPAllowUnauthenticatedPing: mergeFlagValue(allowUnauthenticatedPing, neo4jHTTPAllowUnauthenticatedPing, "--http-allow-unauthenticated-ping", "--neo4j-http-allow-unauthenticated-ping"), + HTTPAllowUnauthenticatedToolsList: mergeFlagValue(allowUnauthenticatedToolsList, neo4jHTTPAllowUnauthenticatedToolsList, "--http-allow-unauthenticated-tools-list", "--neo4j-http-allow-unauthenticated-tools-list"), + AuthHeaderName: mergeFlagValue(authHeaderName, neo4jAuthHeaderName, "--http-auth-header-name", "--neo4j-http-auth-header-name"), RequestTimeout: *neo4jRequestTimeout, } } diff --git a/internal/cli/args_test.go b/internal/cli/args_test.go index 4eba51bd..38f77072 100644 --- a/internal/cli/args_test.go +++ b/internal/cli/args_test.go @@ -4,6 +4,7 @@ package cli import ( + "flag" "io" "os" "strings" @@ -53,7 +54,7 @@ func (m *exitMock) Exit(code int) { panic(m) } -func TestHandleArgs(t *testing.T) { +func TestHandleArgs_DeprecatedFlag(t *testing.T) { tests := []struct { name string args []string @@ -62,99 +63,12 @@ func TestHandleArgs(t *testing.T) { expectedOutput string // substring to find in stdout or stderr expectedStderr string // substring to find in stderr (if non-empty, output is checked in stderr instead of stdout) }{ - { - name: "no flags", - args: []string{testProgramName}, - version: testVersion, - expectedExitCode: -1, - }, - { - name: "version flag short form", - args: []string{testProgramName, "-v"}, - version: testVersion, - expectedExitCode: 0, - expectedOutput: testVersionText, - }, - { - name: "version flag long form", - args: []string{testProgramName, "--version"}, - version: testVersion, - expectedExitCode: 0, - expectedOutput: testVersionText, - }, - { - name: "help flag short form", - args: []string{testProgramName, "-h"}, - version: testVersion, - expectedExitCode: 0, - expectedOutput: testHelpText, - }, - { - name: "help flag long form", - args: []string{testProgramName, "--help"}, - version: testVersion, - expectedExitCode: 0, - expectedOutput: testHelpText, - }, - { - name: "unknown flag", - args: []string{testProgramName, "-x"}, - version: testVersion, - expectedExitCode: 1, - expectedStderr: "unknown flag or argument: -x", - }, - { - name: "version flag with extra arguments", - args: []string{testProgramName, "-v", "extra"}, - version: testVersion, - expectedExitCode: 1, - expectedStderr: "unknown flag or argument: extra", - }, - { - name: "version flag at end", - args: []string{testProgramName, "extra", "-v"}, - version: testVersion, - expectedExitCode: 1, - expectedStderr: "unknown flag or argument: extra", - }, - { - name: "help and version flags together - help takes precedence", - args: []string{testProgramName, "-v", "-h"}, - version: testVersion, - expectedExitCode: 0, - expectedOutput: testHelpText, - }, - { - name: "help flag at end", - args: []string{testProgramName, "extra", "-h"}, - version: testVersion, - expectedExitCode: 1, - expectedStderr: "unknown flag or argument: extra", - }, { name: "neo4j-uri configuration flag", args: []string{testProgramName, "--neo4j-uri", "bolt://localhost:7687"}, version: testVersion, expectedExitCode: -1, // Should not exit, flag is allowed }, - { - name: "neo4j-tools flag with one tool name", - args: []string{testProgramName, "--neo4j-tools", "get-schema"}, - version: testVersion, - expectedExitCode: -1, // Should not exit, flag is allowed - }, - { - name: "neo4j-tools flag with trailing comma after tool name", - args: []string{testProgramName, "--neo4j-tools", "get-schema,"}, - version: testVersion, - expectedExitCode: -1, // Should not exit, flag is allowed - }, - { - name: "neo4j-tools flag with two tool names", - args: []string{testProgramName, "--neo4j-tools", "get-schema,read-cypher"}, - version: testVersion, - expectedExitCode: -1, // Should not exit, flag is allowed - }, { name: "multiple configuration flags", args: []string{testProgramName, "--neo4j-uri", "bolt://localhost:7687", "--neo4j-username", "user"}, @@ -324,6 +238,271 @@ func TestHandleArgs(t *testing.T) { expectedExitCode: 1, expectedStderr: "--neo4j-http-allow-unauthenticated-ping requires a value", }, + { + name: "neo4j-tools flag with one tool name", + args: []string{testProgramName, "--neo4j-tools", "get-schema"}, + version: testVersion, + expectedExitCode: -1, // Should not exit, flag is allowed + }, + { + name: "neo4j-tools flag with trailing comma after tool name", + args: []string{testProgramName, "--neo4j-tools", "get-schema,"}, + version: testVersion, + expectedExitCode: -1, // Should not exit, flag is allowed + }, + { + name: "neo4j-tools flag with two tool names", + args: []string{testProgramName, "--neo4j-tools", "get-schema,read-cypher"}, + version: testVersion, + expectedExitCode: -1, // Should not exit, flag is allowed + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + originalArgs := os.Args + originalOsExit := osExit + t.Cleanup(func() { + os.Args = originalArgs + osExit = originalOsExit + }) + + os.Args = tt.args + mock := &exitMock{} + osExit = mock.Exit + + stdout, stderr := captureOutput(func() { + defer func() { + if r := recover(); r != mock { + if r != nil { + panic(r) + } + } + }() + HandleArgs(tt.version) + }) + + shouldExit := tt.expectedExitCode != -1 + if shouldExit != mock.called { + t.Errorf("exit called: got %v, want %v", mock.called, shouldExit) + } + if mock.called && mock.code != tt.expectedExitCode { + t.Errorf("exit code: got %d, want %d", mock.code, tt.expectedExitCode) + } + if tt.expectedStderr != "" && !strings.Contains(stderr, tt.expectedStderr) { + t.Errorf("stderr: got %q, want to contain %q", stderr, tt.expectedStderr) + } + if tt.expectedOutput != "" && !strings.Contains(stdout, tt.expectedOutput) { + t.Errorf("stdout: got %q, want to contain %q", stdout, tt.expectedOutput) + } + }) + } +} + +func TestHandleArgs_CanonicalFlags(t *testing.T) { + tests := []struct { + name string + args []string + version string + expectedExitCode int + expectedOutput string + expectedStderr string + }{ + { + name: "no flags", + args: []string{testProgramName}, + version: testVersion, + expectedExitCode: -1, + }, + { + name: "no flags", + args: []string{testProgramName}, + version: testVersion, + expectedExitCode: -1, + }, + { + name: "version flag short form", + args: []string{testProgramName, "-v"}, + version: testVersion, + expectedExitCode: 0, + expectedOutput: testVersionText, + }, + { + name: "version flag long form", + args: []string{testProgramName, "--version"}, + version: testVersion, + expectedExitCode: 0, + expectedOutput: testVersionText, + }, + { + name: "help flag short form", + args: []string{testProgramName, "-h"}, + version: testVersion, + expectedExitCode: 0, + expectedOutput: testHelpText, + }, + { + name: "help flag long form", + args: []string{testProgramName, "--help"}, + version: testVersion, + expectedExitCode: 0, + expectedOutput: testHelpText, + }, + { + name: "unknown flag", + args: []string{testProgramName, "-x"}, + version: testVersion, + expectedExitCode: 1, + expectedStderr: "unknown flag or argument: -x", + }, + { + name: "version flag with extra arguments", + args: []string{testProgramName, "-v", "extra"}, + version: testVersion, + expectedExitCode: 1, + expectedStderr: "unknown flag or argument: extra", + }, + { + name: "version flag at end", + args: []string{testProgramName, "extra", "-v"}, + version: testVersion, + expectedExitCode: 1, + expectedStderr: "unknown flag or argument: extra", + }, + { + name: "help and version flags together - help takes precedence", + args: []string{testProgramName, "-v", "-h"}, + version: testVersion, + expectedExitCode: 0, + expectedOutput: testHelpText, + }, + { + name: "help flag at end", + args: []string{testProgramName, "extra", "-h"}, + version: testVersion, + expectedExitCode: 1, + expectedStderr: "unknown flag or argument: extra", + }, + { + name: "uri", + args: []string{testProgramName, "--uri", "bolt://localhost:7687"}, + version: testVersion, + expectedExitCode: -1, + }, + { + name: "username", + args: []string{testProgramName, "--username", "neo4j"}, + version: testVersion, + expectedExitCode: -1, + }, + { + name: "password", + args: []string{testProgramName, "--password", "password"}, + version: testVersion, + expectedExitCode: -1, + }, + { + name: "database", + args: []string{testProgramName, "--database", "neo4j"}, + version: testVersion, + expectedExitCode: -1, + }, + { + name: "read-only", + args: []string{testProgramName, "--read-only", "true"}, + version: testVersion, + expectedExitCode: -1, + }, + { + name: "telemetry", + args: []string{testProgramName, "--telemetry", "false"}, + version: testVersion, + expectedExitCode: -1, + }, + { + name: "schema sample size", + args: []string{testProgramName, "--schema-sample-size", "500"}, + version: testVersion, + expectedExitCode: -1, + }, + { + name: "transport mode", + args: []string{testProgramName, "--transport-mode", "http"}, + version: testVersion, + expectedExitCode: -1, + }, + { + name: "http port", + args: []string{testProgramName, "--http-port", "8443"}, + version: testVersion, + expectedExitCode: -1, + }, + { + name: "http host", + args: []string{testProgramName, "--http-host", "localhost"}, + version: testVersion, + expectedExitCode: -1, + }, + { + name: "http allowed origins", + args: []string{testProgramName, "--http-allowed-origins", "https://example.com"}, + version: testVersion, + expectedExitCode: -1, + }, + { + name: "http tls enabled", + args: []string{testProgramName, "--http-tls-enabled", "true"}, + version: testVersion, + expectedExitCode: -1, + }, + { + name: "http tls cert file", + args: []string{testProgramName, "--http-tls-cert-file", "cert.pem"}, + version: testVersion, + expectedExitCode: -1, + }, + { + name: "http tls key file", + args: []string{testProgramName, "--http-tls-key-file", "key.pem"}, + version: testVersion, + expectedExitCode: -1, + }, + { + name: "http auth header name", + args: []string{testProgramName, "--http-auth-header-name", "X-Custom-Auth"}, + version: testVersion, + expectedExitCode: -1, + }, + { + name: "http unauthenticated ping", + args: []string{testProgramName, "--http-allow-unauthenticated-ping", "true"}, + version: testVersion, + expectedExitCode: -1, + }, + { + name: "http unauthenticated tools list", + args: []string{testProgramName, "--http-allow-unauthenticated-tools-list", "true"}, + version: testVersion, + expectedExitCode: -1, + }, + { + name: "neo4j-tools flag with one tool name", + args: []string{testProgramName, "--neo4j-tools", "get-schema"}, + version: testVersion, + expectedExitCode: -1, // Should not exit, flag is allowed + }, + { + name: "neo4j-tools flag with trailing comma after tool name", + args: []string{testProgramName, "--neo4j-tools", "get-schema,"}, + version: testVersion, + expectedExitCode: -1, // Should not exit, flag is allowed + }, + { + name: "neo4j-tools flag with two tool names", + args: []string{testProgramName, "--neo4j-tools", "get-schema,read-cypher"}, + version: testVersion, + expectedExitCode: -1, // Should not exit, flag is allowed + }, { name: "neo4j-request-timeout flag with valid value", args: []string{testProgramName, "--neo4j-request-timeout", "45s"}, @@ -363,29 +542,57 @@ func TestHandleArgs(t *testing.T) { HandleArgs(tt.version) }) - // Verify exit behaviour shouldExit := tt.expectedExitCode != -1 if shouldExit != mock.called { t.Errorf("exit called: got %v, want %v", mock.called, shouldExit) } - if mock.called && mock.code != tt.expectedExitCode { t.Errorf("exit code: got %d, want %d", mock.code, tt.expectedExitCode) } - - // Verify stderr output - if tt.expectedStderr != "" { - if !strings.Contains(stderr, tt.expectedStderr) { - t.Errorf("stderr: got %q, want to contain %q", stderr, tt.expectedStderr) - } + if tt.expectedStderr != "" && !strings.Contains(stderr, tt.expectedStderr) { + t.Errorf("stderr: got %q, want to contain %q", stderr, tt.expectedStderr) } - - // Verify output - if tt.expectedOutput != "" { - if !strings.Contains(stdout, tt.expectedOutput) { - t.Errorf("stdout: got %q, want to contain %q", stdout, tt.expectedOutput) - } + if tt.expectedOutput != "" && !strings.Contains(stdout, tt.expectedOutput) { + t.Errorf("stdout: got %q, want to contain %q", stdout, tt.expectedOutput) } }) } } + +func TestParseConfigFlags_CanonicalFlagPrecedenceAndWarning(t *testing.T) { + originalArgs := os.Args + originalCommandLine := flag.CommandLine + t.Cleanup(func() { + os.Args = originalArgs + flag.CommandLine = originalCommandLine + }) + + os.Args = []string{ + testProgramName, + "--uri", "bolt://canonical-host:7687", + "--neo4j-uri", "bolt://legacy-host:7687", + "--transport-mode", "http", + "--neo4j-transport-mode", "stdio", + } + flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ContinueOnError) + flag.CommandLine.SetOutput(io.Discard) + + var args *Args + _, stderr := captureOutput(func() { + args = ParseConfigFlags() + }) + + if args.URI != "bolt://canonical-host:7687" { + t.Fatalf("ParseConfigFlags() URI = %q, want canonical value", args.URI) + } + if args.TransportMode != "http" { + t.Fatalf("ParseConfigFlags() TransportMode = %q, want canonical value", args.TransportMode) + } + if !strings.Contains(stderr, `deprecated CLI flag "--neo4j-uri"`) || + !strings.Contains(stderr, `deprecated CLI flag "--neo4j-transport-mode"`) { + t.Fatalf("deprecation warnings = %q, want warnings for deprecated flags", stderr) + } + if strings.Contains(stderr, "legacy-host") { + t.Fatalf("deprecation warning exposed configured value: %q", stderr) + } +} diff --git a/internal/config/config.go b/internal/config/config.go index c5856b0b..79261100 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -29,7 +29,7 @@ const ( MaxRequestTimeout time.Duration = 30 * time.Minute TransportModeStdio TransportMode = "stdio" TransportModeHTTP TransportMode = "http" - DeprecatedVariableMessage string = "Warning: deprecated environment variable \"%s\". Please use: \"%s\" instead\n" + DeprecatedVariableMessage string = "Warning: deprecated %s %q; use %q instead. Support will be removed in v2.\n" ) // ValidTransportModes defines the allowed transport mode values @@ -96,7 +96,7 @@ func (c *Config) Validate() error { return fmt.Errorf("Neo4j password is required for STDIO mode") } if c.Database == "" { - return fmt.Errorf("Neo4j database is required for STDIO mode (set NEO4J_DATABASE or use --neo4j-database flag)") + return fmt.Errorf("Neo4j database is required for STDIO mode (set NEO4J_MCP_DATABASE or use --database flag)") } } else { if c.URI != "" { @@ -106,7 +106,7 @@ func (c *Config) Validate() error { return fmt.Errorf("Neo4j username and password should not be set for HTTP transport mode; credentials are provided per-request via Auth headers") } if c.Database != "" { - return fmt.Errorf("NEO4J_DATABASE environment variable or --neo4j-database flag should not be set for HTTP transport mode; database is selected per-request via URL path (e.g., /db/{databaseName}/mcp)") + return fmt.Errorf("NEO4J_MCP_DATABASE environment variable or ---database flag should not be set for HTTP transport mode; database is selected per-request via URL path (e.g., /db/{databaseName}/mcp)") } } @@ -168,45 +168,42 @@ type CLIOverrides struct { // CLI flag values take precedence over environment variables. // Returns an error if required configuration is missing or invalid. func LoadConfig(cliOverrides *CLIOverrides) (*Config, error) { - logLevel := GetEnvWithDefault("NEO4J_LOG_LEVEL", "info") - logFormat := GetEnvWithDefault("NEO4J_LOG_FORMAT", "text") + warnOnDeprecatedUsage() + logLevel := GetEnvWithAliasesDefault("NEO4J_MCP_LOG_LEVEL", "info", "NEO4J_LOG_LEVEL") + logFormat := GetEnvWithAliasesDefault("NEO4J_MCP_LOG_FORMAT", "text", "NEO4J_LOG_FORMAT") // Validate log level and use default if invalid if !slices.Contains(logger.ValidLogLevels, logLevel) { - fmt.Fprintf(os.Stderr, "Warning: invalid NEO4J_LOG_LEVEL '%s', using default 'info'. Valid values: %v\n", logLevel, logger.ValidLogLevels) + fmt.Fprintf(os.Stderr, "Warning: invalid NEO4J_MCP_LOG_LEVEL '%s', using default 'info'. Valid values: %v\n", logLevel, logger.ValidLogLevels) logLevel = "info" } // Validate log format and use default if invalid if !slices.Contains(logger.ValidLogFormats, logFormat) { - fmt.Fprintf(os.Stderr, "Warning: invalid NEO4J_LOG_FORMAT '%s', using default 'text'. Valid values: %v\n", logFormat, logger.ValidLogFormats) + fmt.Fprintf(os.Stderr, "Warning: invalid NEO4J_MCP_LOG_FORMAT '%s', using default 'text'. Valid values: %v\n", logFormat, logger.ValidLogFormats) logFormat = "text" } - if GetEnv("NEO4J_MCP_TRANSPORT") != "" { - fmt.Fprintf(os.Stderr, DeprecatedVariableMessage, "NEO4J_MCP_TRANSPORT", "NEO4J_TRANSPORT_MODE") - } - cfg := &Config{ - URI: GetEnv("NEO4J_URI"), - Username: GetEnv("NEO4J_USERNAME"), - Password: GetEnv("NEO4J_PASSWORD"), - Database: GetEnv("NEO4J_DATABASE"), - ReadOnly: ParseBool(GetEnv("NEO4J_READ_ONLY"), false), - Telemetry: ParseBool(GetEnv("NEO4J_TELEMETRY"), true), + URI: GetEnvWithAliases("NEO4J_MCP_URI", "NEO4J_URI"), + Username: GetEnvWithAliases("NEO4J_MCP_USERNAME", "NEO4J_USERNAME"), + Password: GetEnvWithAliases("NEO4J_MCP_PASSWORD", "NEO4J_PASSWORD"), + Database: GetEnvWithAliases("NEO4J_MCP_DATABASE", "NEO4J_DATABASE"), + ReadOnly: ParseBool(GetEnvWithAliases("NEO4J_MCP_READ_ONLY", "NEO4J_READ_ONLY"), false), + Telemetry: ParseBool(GetEnvWithAliases("NEO4J_MCP_TELEMETRY", "NEO4J_TELEMETRY"), true), LogLevel: logLevel, LogFormat: logFormat, - SchemaSampleSize: ParseInt32(GetEnv("NEO4J_SCHEMA_SAMPLE_SIZE"), DefaultSchemaSampleSize), - TransportMode: GetTransportModeWithDefault("NEO4J_TRANSPORT_MODE", GetTransportModeWithDefault("NEO4J_MCP_TRANSPORT", TransportModeStdio)), + SchemaSampleSize: ParseInt32(GetEnvWithAliases("NEO4J_MCP_SCHEMA_SAMPLE_SIZE", "NEO4J_SCHEMA_SAMPLE_SIZE"), DefaultSchemaSampleSize), + TransportMode: TransportMode(GetEnvWithAliasesDefault("NEO4J_MCP_TRANSPORT_MODE", string(TransportModeStdio), "NEO4J_TRANSPORT_MODE", "NEO4J_MCP_TRANSPORT")), HTTPPort: GetEnv("NEO4J_MCP_HTTP_PORT"), // Default set after TLS determination HTTPHost: GetEnvWithDefault("NEO4J_MCP_HTTP_HOST", "127.0.0.1"), HTTPAllowedOrigins: GetEnv("NEO4J_MCP_HTTP_ALLOWED_ORIGINS"), HTTPTLSEnabled: ParseBool(GetEnv("NEO4J_MCP_HTTP_TLS_ENABLED"), false), HTTPTLSCertFile: GetEnv("NEO4J_MCP_HTTP_TLS_CERT_FILE"), HTTPTLSKeyFile: GetEnv("NEO4J_MCP_HTTP_TLS_KEY_FILE"), - AuthHeaderName: GetEnvWithDefault("NEO4J_HTTP_AUTH_HEADER_NAME", "Authorization"), - AllowUnauthenticatedPing: ParseBool(GetEnv("NEO4J_HTTP_ALLOW_UNAUTHENTICATED_PING"), false), - AllowUnauthenticatedToolsList: ParseBool(GetEnv("NEO4J_HTTP_ALLOW_UNAUTHENTICATED_TOOLS_LIST"), false), + AuthHeaderName: GetEnvWithAliasesDefault("NEO4J_MCP_HTTP_AUTH_HEADER_NAME", "Authorization", "NEO4J_HTTP_AUTH_HEADER_NAME"), + AllowUnauthenticatedPing: ParseBool(GetEnvWithAliases("NEO4J_MCP_HTTP_ALLOW_UNAUTHENTICATED_PING", "NEO4J_HTTP_ALLOW_UNAUTHENTICATED_PING"), false), + AllowUnauthenticatedToolsList: ParseBool(GetEnvWithAliases("NEO4J_MCP_HTTP_ALLOW_UNAUTHENTICATED_TOOLS_LIST", "NEO4J_HTTP_ALLOW_UNAUTHENTICATED_TOOLS_LIST"), false), } requestTimeout, err := ParseDuration(GetEnv("NEO4J_MCP_REQUEST_TIMEOUT"), DefaultRequestTimeout) @@ -303,7 +300,7 @@ func LoadConfig(cliOverrides *CLIOverrides) (*Config, error) { // Normalize and validate headName := strings.TrimSpace(cfg.AuthHeaderName) if headName == "" { - return nil, fmt.Errorf("invalid auth header name: explicitly configured header name cannot be empty; unset NEO4J_HTTP_AUTH_HEADER_NAME or provide a valid header name") + return nil, fmt.Errorf("invalid auth header name: explicitly configured header name cannot be empty; unset NEO4J_MCP_HTTP_AUTH_HEADER_NAME or provide a valid header name") } // store normalized value cfg.AuthHeaderName = headName @@ -321,6 +318,57 @@ func GetEnv(key string) string { return os.Getenv(key) } +// GetEnvWithAliases returns the canonical environment variable value, falling +// back to deprecated aliases when the canonical variable is unset or empty. +func GetEnvWithAliases(canonical string, aliases ...string) string { + if value := os.Getenv(canonical); value != "" { + return value + } + for _, alias := range aliases { + if value := os.Getenv(alias); value != "" { + return value + } + } + return "" +} + +var deprecatedEnvironmentVariables = []struct { + alias string + canonical string +}{ + {alias: "NEO4J_URI", canonical: "NEO4J_MCP_URI"}, + {alias: "NEO4J_USERNAME", canonical: "NEO4J_MCP_USERNAME"}, + {alias: "NEO4J_PASSWORD", canonical: "NEO4J_MCP_PASSWORD"}, + {alias: "NEO4J_DATABASE", canonical: "NEO4J_MCP_DATABASE"}, + {alias: "NEO4J_READ_ONLY", canonical: "NEO4J_MCP_READ_ONLY"}, + {alias: "NEO4J_TELEMETRY", canonical: "NEO4J_MCP_TELEMETRY"}, + {alias: "NEO4J_LOG_LEVEL", canonical: "NEO4J_MCP_LOG_LEVEL"}, + {alias: "NEO4J_LOG_FORMAT", canonical: "NEO4J_MCP_LOG_FORMAT"}, + {alias: "NEO4J_TRANSPORT_MODE", canonical: "NEO4J_MCP_TRANSPORT_MODE"}, + {alias: "NEO4J_MCP_TRANSPORT", canonical: "NEO4J_MCP_TRANSPORT_MODE"}, + {alias: "NEO4J_SCHEMA_SAMPLE_SIZE", canonical: "NEO4J_MCP_SCHEMA_SAMPLE_SIZE"}, + {alias: "NEO4J_HTTP_AUTH_HEADER_NAME", canonical: "NEO4J_MCP_HTTP_AUTH_HEADER_NAME"}, + {alias: "NEO4J_HTTP_ALLOW_UNAUTHENTICATED_PING", canonical: "NEO4J_MCP_HTTP_ALLOW_UNAUTHENTICATED_PING"}, + {alias: "NEO4J_HTTP_ALLOW_UNAUTHENTICATED_TOOLS_LIST", canonical: "NEO4J_MCP_HTTP_ALLOW_UNAUTHENTICATED_TOOLS_LIST"}, +} + +func warnOnDeprecatedUsage() { + for _, variable := range deprecatedEnvironmentVariables { + if os.Getenv(variable.alias) != "" { + fmt.Fprintf(os.Stderr, DeprecatedVariableMessage, "environment variable", variable.alias, variable.canonical) + } + } +} + +// GetEnvWithAliasesDefault returns the canonical or deprecated environment +// variable value, or defaultValue when all names are unset or empty. +func GetEnvWithAliasesDefault(canonical, defaultValue string, aliases ...string) string { + if value := GetEnvWithAliases(canonical, aliases...); value != "" { + return value + } + return defaultValue +} + // GetEnvWithDefault returns the value of an environment variable or a default value func GetEnvWithDefault(key, defaultValue string) string { if value := os.Getenv(key); value != "" { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 3b7343aa..61ecf778 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -4,6 +4,8 @@ package config import ( + "io" + "os" "strings" "testing" "time" @@ -105,7 +107,7 @@ func TestConfig_Validate(t *testing.T) { TransportMode: TransportModeHTTP, }, wantErr: true, - errMsg: "NEO4J_DATABASE environment variable or --neo4j-database flag should not be set for HTTP transport mode", + errMsg: "NEO4J_MCP_DATABASE environment variable or ---database flag should not be set for HTTP transport mode; database is selected per-request via URL path (e.g., /db/{databaseName}/mcp)", }, { name: "credentials set for HTTP mode should raise error", @@ -157,11 +159,11 @@ func TestConfig_Validate(t *testing.T) { func TestLoadConfig_ValidConfig(t *testing.T) { // Unit test: set required env variables and verify LoadConfig works - t.Setenv("NEO4J_TRANSPORT_MODE", "stdio") - t.Setenv("NEO4J_URI", "bolt://localhost:7687") - t.Setenv("NEO4J_USERNAME", "testuser") - t.Setenv("NEO4J_PASSWORD", "testpass") - t.Setenv("NEO4J_DATABASE", "neo4j") + t.Setenv("NEO4J_MCP_TRANSPORT_MODE", "stdio") + t.Setenv("NEO4J_MCP_URI", "bolt://localhost:7687") + t.Setenv("NEO4J_MCP_USERNAME", "testuser") + t.Setenv("NEO4J_MCP_PASSWORD", "testpass") + t.Setenv("NEO4J_MCP_DATABASE", "neo4j") cfg, err := LoadConfig(nil) if err != nil { @@ -194,9 +196,25 @@ func TestLoadConfig_DeprecatedValidConfig(t *testing.T) { t.Setenv("NEO4J_PASSWORD", "testpass") t.Setenv("NEO4J_DATABASE", "neo4j") - cfg, err := LoadConfig(nil) - if err != nil { - t.Fatalf("LoadConfig() unexpected error: %v", err) + var cfg *Config + var loadErr error + _, stderr := captureOutput(func() { + cfg, loadErr = LoadConfig(nil) + }) + if loadErr != nil { + t.Fatalf("LoadConfig() unexpected error: %v", loadErr) + } + + for _, alias := range []string{ + "NEO4J_MCP_TRANSPORT", + "NEO4J_URI", + "NEO4J_USERNAME", + "NEO4J_PASSWORD", + "NEO4J_DATABASE", + } { + if !strings.Contains(stderr, `deprecated environment variable "`+alias+`"`) { + t.Errorf("deprecation warnings = %q, want warning for %s", stderr, alias) + } } if cfg == nil { @@ -219,10 +237,10 @@ func TestLoadConfig_DeprecatedValidConfig(t *testing.T) { func TestLoadConfig_MissingRequiredEnvVars(t *testing.T) { // Unit test: verify LoadConfig returns error when required env vars are missing - t.Setenv("NEO4J_TRANSPORT_MODE", "stdio") - t.Setenv("NEO4J_URI", "") - t.Setenv("NEO4J_USERNAME", "") - t.Setenv("NEO4J_PASSWORD", "") + t.Setenv("NEO4J_MCP_TRANSPORT_MODE", "stdio") + t.Setenv("NEO4J_MCP_URI", "") + t.Setenv("NEO4J_MCP_USERNAME", "") + t.Setenv("NEO4J_MCP_PASSWORD", "") cfg, err := LoadConfig(nil) @@ -245,11 +263,11 @@ func TestLoadConfig_MissingRequiredEnvVars(t *testing.T) { func TestLoadConfig_CLIOverrides(t *testing.T) { // Unit test: verify CLI overrides take precedence over environment variables - t.Setenv("NEO4J_TRANSPORT_MODE", "stdio") - t.Setenv("NEO4J_URI", "bolt://env-host:7687") - t.Setenv("NEO4J_USERNAME", "env-user") - t.Setenv("NEO4J_PASSWORD", "env-pass") - t.Setenv("NEO4J_DATABASE", "env-db") + t.Setenv("NEO4J_MCP_TRANSPORT_MODE", "stdio") + t.Setenv("NEO4J_MCP_URI", "bolt://env-host:7687") + t.Setenv("NEO4J_MCP_USERNAME", "env-user") + t.Setenv("NEO4J_MCP_PASSWORD", "env-pass") + t.Setenv("NEO4J_MCP_DATABASE", "env-db") overrides := &CLIOverrides{ URI: "bolt://cli-host:7687", @@ -280,11 +298,11 @@ func TestLoadConfig_CLIOverrides(t *testing.T) { func TestLoadConfig_PartialCLIOverrides(t *testing.T) { // Unit test: verify partial CLI overrides work (some from CLI, some from env) - t.Setenv("NEO4J_TRANSPORT_MODE", "stdio") - t.Setenv("NEO4J_URI", "bolt://env-host:7687") - t.Setenv("NEO4J_USERNAME", "env-user") - t.Setenv("NEO4J_PASSWORD", "env-pass") - t.Setenv("NEO4J_DATABASE", "env-db") + t.Setenv("NEO4J_MCP_TRANSPORT_MODE", "stdio") + t.Setenv("NEO4J_MCP_URI", "bolt://env-host:7687") + t.Setenv("NEO4J_MCP_USERNAME", "env-user") + t.Setenv("NEO4J_MCP_PASSWORD", "env-pass") + t.Setenv("NEO4J_MCP_DATABASE", "env-db") // Only override URI and Username, leave Password and Database from env overrides := &CLIOverrides{ @@ -317,25 +335,25 @@ func TestLoadConfig_PartialCLIOverrides(t *testing.T) { func TestLoadConfig_InvalidBooleanValues(t *testing.T) { // Unit test: verify invalid boolean values fall back to defaults - t.Setenv("NEO4J_TRANSPORT_MODE", "stdio") - t.Setenv("NEO4J_URI", "bolt://localhost:7687") - t.Setenv("NEO4J_USERNAME", "testuser") - t.Setenv("NEO4J_PASSWORD", "testpass") - t.Setenv("NEO4J_DATABASE", "neo4j") - t.Setenv("NEO4J_TELEMETRY", "invalid-value") - t.Setenv("NEO4J_READ_ONLY", "not-a-boolean") + t.Setenv("NEO4J_MCP_TRANSPORT_MODE", "stdio") + t.Setenv("NEO4J_MCP_DATABASE", "neo4j") + t.Setenv("NEO4J_MCP_URI", "bolt://localhost:7687") + t.Setenv("NEO4J_MCP_USERNAME", "testuser") + t.Setenv("NEO4J_MCP_PASSWORD", "testpass") + t.Setenv("NEO4J_MCP_TELEMETRY", "invalid-value") + t.Setenv("NEO4J_MCP_READ_ONLY", "not-a-boolean") cfg, err := LoadConfig(nil) if err != nil { t.Fatalf("LoadConfig() unexpected error: %v", err) } - // Invalid NEO4J_TELEMETRY should fall back to default (true) + // Invalid NEO4J_MCP_TELEMETRY should fall back to default (true) if cfg.Telemetry != true { t.Errorf("LoadConfig() Telemetry = %v, want true (default for invalid value)", cfg.Telemetry) } - // Invalid NEO4J_READ_ONLY should fall back to default (false) + // Invalid NEO4J_MCP_READ_ONLY should fall back to default (false) if cfg.ReadOnly != false { t.Errorf("LoadConfig() ReadOnly = %v, want false (default for invalid value)", cfg.ReadOnly) } @@ -343,13 +361,13 @@ func TestLoadConfig_InvalidBooleanValues(t *testing.T) { func TestLoadConfig_ValidBooleanValues(t *testing.T) { // Unit test: verify valid boolean values are parsed correctly - t.Setenv("NEO4J_TRANSPORT_MODE", "stdio") - t.Setenv("NEO4J_URI", "bolt://localhost:7687") - t.Setenv("NEO4J_USERNAME", "testuser") - t.Setenv("NEO4J_PASSWORD", "testpass") - t.Setenv("NEO4J_DATABASE", "neo4j") - t.Setenv("NEO4J_TELEMETRY", "false") - t.Setenv("NEO4J_READ_ONLY", "true") + t.Setenv("NEO4J_MCP_TRANSPORT_MODE", "stdio") + t.Setenv("NEO4J_MCP_DATABASE", "neo4j") + t.Setenv("NEO4J_MCP_URI", "bolt://localhost:7687") + t.Setenv("NEO4J_MCP_USERNAME", "testuser") + t.Setenv("NEO4J_MCP_PASSWORD", "testpass") + t.Setenv("NEO4J_MCP_TELEMETRY", "false") + t.Setenv("NEO4J_MCP_READ_ONLY", "true") cfg, err := LoadConfig(nil) if err != nil { @@ -369,15 +387,15 @@ func TestLoadConfig_ValidBooleanValues(t *testing.T) { func TestLoadConfig_ValidIntValue(t *testing.T) { // Set required env variables for basic validation to pass - t.Setenv("NEO4J_TRANSPORT_MODE", "stdio") - t.Setenv("NEO4J_URI", "bolt://localhost:7687") - t.Setenv("NEO4J_USERNAME", "testuser") - t.Setenv("NEO4J_PASSWORD", "testpass") - t.Setenv("NEO4J_DATABASE", "neo4j") + t.Setenv("NEO4J_MCP_TRANSPORT_MODE", "stdio") + t.Setenv("NEO4J_MCP_DATABASE", "neo4j") + t.Setenv("NEO4J_MCP_URI", "bolt://localhost:7687") + t.Setenv("NEO4J_MCP_USERNAME", "testuser") + t.Setenv("NEO4J_MCP_PASSWORD", "testpass") t.Run("default value", func(t *testing.T) { // Unset the env var to test default - t.Setenv("NEO4J_SCHEMA_SAMPLE_SIZE", "") + t.Setenv("NEO4J_MCP_SCHEMA_SAMPLE_SIZE", "") cfg, err := LoadConfig(nil) if err != nil { @@ -390,7 +408,7 @@ func TestLoadConfig_ValidIntValue(t *testing.T) { }) t.Run("value from env", func(t *testing.T) { - t.Setenv("NEO4J_SCHEMA_SAMPLE_SIZE", "500") + t.Setenv("NEO4J_MCP_SCHEMA_SAMPLE_SIZE", "500") cfg, err := LoadConfig(nil) if err != nil { @@ -403,7 +421,7 @@ func TestLoadConfig_ValidIntValue(t *testing.T) { }) t.Run("invalid value from env", func(t *testing.T) { - t.Setenv("NEO4J_SCHEMA_SAMPLE_SIZE", "invalid") + t.Setenv("NEO4J_MCP_SCHEMA_SAMPLE_SIZE", "invalid") cfg, err := LoadConfig(nil) if err != nil { @@ -417,6 +435,155 @@ func TestLoadConfig_ValidIntValue(t *testing.T) { }) } +func captureOutput(fn func()) (stdout, stderr string) { + rOut, wOut, _ := os.Pipe() + rErr, wErr, _ := os.Pipe() + + oldStdout := os.Stdout + oldStderr := os.Stderr + os.Stdout = wOut + os.Stderr = wErr + + fn() + + wOut.Close() + wErr.Close() + os.Stdout = oldStdout + os.Stderr = oldStderr + + outBytes, _ := io.ReadAll(rOut) + errBytes, _ := io.ReadAll(rErr) + + return string(outBytes), string(errBytes) +} + +func TestLoadConfig_CanonicalEnvironmentVariables(t *testing.T) { + t.Setenv("NEO4J_MCP_URI", "bolt://canonical-host:7687") + t.Setenv("NEO4J_MCP_USERNAME", "canonical-user") + t.Setenv("NEO4J_MCP_PASSWORD", "canonical-password") + t.Setenv("NEO4J_MCP_DATABASE", "canonical-db") + t.Setenv("NEO4J_MCP_READ_ONLY", "true") + t.Setenv("NEO4J_MCP_TELEMETRY", "false") + t.Setenv("NEO4J_MCP_SCHEMA_SAMPLE_SIZE", "55") + t.Setenv("NEO4J_MCP_TRANSPORT_MODE", "stdio") + t.Setenv("NEO4J_MCP_LOG_LEVEL", "debug") + t.Setenv("NEO4J_MCP_LOG_FORMAT", "json") + t.Setenv("NEO4J_MCP_HTTP_AUTH_HEADER_NAME", "X-Canonical-Auth") + + var cfg *Config + var loadErr error + _, stderr := captureOutput(func() { + cfg, loadErr = LoadConfig(nil) + }) + if loadErr != nil { + t.Fatalf("LoadConfig() unexpected error: %v", loadErr) + } + if stderr != "" { + t.Fatalf("LoadConfig() emitted unexpected deprecation warning: %q", stderr) + } + + if cfg.URI != "bolt://canonical-host:7687" || + cfg.Username != "canonical-user" || + cfg.Password != "canonical-password" || + cfg.Database != "canonical-db" || + !cfg.ReadOnly || + cfg.Telemetry || + cfg.SchemaSampleSize != 55 || + cfg.TransportMode != TransportModeStdio || + cfg.LogLevel != "debug" || + cfg.LogFormat != "json" || + cfg.AuthHeaderName != "X-Canonical-Auth" { + t.Fatalf("LoadConfig() did not load canonical environment variables: %+v", cfg) + } +} + +func TestLoadConfig_InvalidLogConfigurationFallsBackToDefaults(t *testing.T) { + t.Setenv("NEO4J_MCP_URI", "bolt://localhost:7687") + t.Setenv("NEO4J_MCP_USERNAME", "neo4j") + t.Setenv("NEO4J_MCP_PASSWORD", "password") + t.Setenv("NEO4J_MCP_DATABASE", "neo4j") + t.Setenv("NEO4J_MCP_TRANSPORT_MODE", "stdio") + t.Setenv("NEO4J_MCP_LOG_LEVEL", "DEBUG") + t.Setenv("NEO4J_MCP_LOG_FORMAT", "JSON") + + var cfg *Config + var loadErr error + _, stderr := captureOutput(func() { + cfg, loadErr = LoadConfig(nil) + }) + if loadErr != nil { + t.Fatalf("LoadConfig() unexpected error: %v", loadErr) + } + if cfg.LogLevel != "info" { + t.Errorf("LoadConfig() LogLevel = %q, want default %q", cfg.LogLevel, "info") + } + if cfg.LogFormat != "text" { + t.Errorf("LoadConfig() LogFormat = %q, want default %q", cfg.LogFormat, "text") + } + if !strings.Contains(stderr, "invalid NEO4J_MCP_LOG_LEVEL 'DEBUG'") { + t.Errorf("stderr = %q, want invalid log level warning", stderr) + } + if !strings.Contains(stderr, "invalid NEO4J_MCP_LOG_FORMAT 'JSON'") { + t.Errorf("stderr = %q, want invalid log format warning", stderr) + } +} + +func TestLoadConfig_DeprecatedEnvironmentAliasWarningAndPrecedence(t *testing.T) { + t.Setenv("NEO4J_MCP_URI", "bolt://canonical-host:7687") + t.Setenv("NEO4J_MCP_USERNAME", "canonical-user") + t.Setenv("NEO4J_MCP_PASSWORD", "canonical-password") + t.Setenv("NEO4J_MCP_DATABASE", "canonical-db") + t.Setenv("NEO4J_MCP_TRANSPORT_MODE", "stdio") + t.Setenv("NEO4J_URI", "bolt://legacy-host:7687") + + var cfg *Config + var loadErr error + _, stderr := captureOutput(func() { + cfg, loadErr = LoadConfig(nil) + }) + if loadErr != nil { + t.Fatalf("LoadConfig() unexpected error: %v", loadErr) + } + if cfg.URI != "bolt://canonical-host:7687" { + t.Fatalf("LoadConfig() URI = %q, want canonical value", cfg.URI) + } + if !strings.Contains(stderr, `deprecated environment variable "NEO4J_URI"`) { + t.Fatalf("deprecation warning = %q, want NEO4J_URI warning", stderr) + } + if !strings.Contains(stderr, "NEO4J_MCP_URI") { + t.Fatalf("deprecation warning = %q, want canonical replacement", stderr) + } + if strings.Contains(stderr, "legacy-host") { + t.Fatalf("deprecation warning exposed configured value: %q", stderr) + } +} + +func TestLoadConfig_TransportEnvironmentAliasPrecedence(t *testing.T) { + t.Setenv("NEO4J_MCP_URI", "bolt://canonical-host:7687") + t.Setenv("NEO4J_MCP_USERNAME", "canonical-user") + t.Setenv("NEO4J_MCP_PASSWORD", "canonical-password") + t.Setenv("NEO4J_MCP_DATABASE", "canonical-db") + t.Setenv("NEO4J_MCP_TRANSPORT_MODE", "") + t.Setenv("NEO4J_TRANSPORT_MODE", "stdio") + t.Setenv("NEO4J_MCP_TRANSPORT", "http") + + var cfg *Config + var loadErr error + _, stderr := captureOutput(func() { + cfg, loadErr = LoadConfig(nil) + }) + if loadErr != nil { + t.Fatalf("LoadConfig() unexpected error: %v", loadErr) + } + if cfg.TransportMode != TransportModeStdio { + t.Fatalf("LoadConfig() TransportMode = %q, want NEO4J_TRANSPORT_MODE precedence", cfg.TransportMode) + } + if !strings.Contains(stderr, `deprecated environment variable "NEO4J_TRANSPORT_MODE"`) || + !strings.Contains(stderr, `deprecated environment variable "NEO4J_MCP_TRANSPORT"`) { + t.Fatalf("deprecation warnings = %q, want both transport aliases", stderr) + } +} + func TestConfig_Validate_TLS(t *testing.T) { // Generate test certificates once for all test cases certPath, keyPath := testutil.GenerateTestTLSCertificate(t) @@ -512,7 +679,7 @@ func TestLoadConfig_TLS(t *testing.T) { // Generate test certificates dynamically certPath, keyPath := testutil.GenerateTestTLSCertificate(t) - t.Setenv("NEO4J_TRANSPORT_MODE", "http") + t.Setenv("NEO4J_MCP_TRANSPORT_MODE", "http") t.Setenv("NEO4J_MCP_HTTP_TLS_ENABLED", "true") t.Setenv("NEO4J_MCP_HTTP_TLS_CERT_FILE", certPath) t.Setenv("NEO4J_MCP_HTTP_TLS_KEY_FILE", keyPath) @@ -534,11 +701,12 @@ func TestLoadConfig_TLS(t *testing.T) { }) t.Run("TLS disabled by default", func(t *testing.T) { - t.Setenv("NEO4J_TRANSPORT_MODE", "stdio") - t.Setenv("NEO4J_URI", "bolt://localhost:7687") - t.Setenv("NEO4J_USERNAME", "neo4j") - t.Setenv("NEO4J_PASSWORD", "password") - t.Setenv("NEO4J_DATABASE", "neo4j") + // TODO this test does not make sense, TLS of course is disabled in STDIO + t.Setenv("NEO4J_MCP_TRANSPORT_MODE", "stdio") + t.Setenv("NEO4J_MCP_URI", "bolt://localhost:7687") + t.Setenv("NEO4J_MCP_USERNAME", "neo4j") + t.Setenv("NEO4J_MCP_PASSWORD", "password") + t.Setenv("NEO4J_MCP_DATABASE", "neo4j") cfg, err := LoadConfig(nil) if err != nil { @@ -554,7 +722,7 @@ func TestLoadConfig_TLS(t *testing.T) { // Generate test certificates dynamically certPath, keyPath := testutil.GenerateTestTLSCertificate(t) - t.Setenv("NEO4J_TRANSPORT_MODE", "http") + t.Setenv("NEO4J_MCP_TRANSPORT_MODE", "http") t.Setenv("NEO4J_MCP_HTTP_TLS_ENABLED", "false") t.Setenv("NEO4J_MCP_HTTP_TLS_CERT_FILE", certPath) t.Setenv("NEO4J_MCP_HTTP_TLS_KEY_FILE", keyPath) @@ -580,7 +748,7 @@ func TestLoadConfig_TLS(t *testing.T) { }) t.Run("TLS validation error when missing cert file", func(t *testing.T) { - t.Setenv("NEO4J_TRANSPORT_MODE", "http") + t.Setenv("NEO4J_MCP_TRANSPORT_MODE", "http") t.Setenv("NEO4J_MCP_HTTP_TLS_ENABLED", "true") t.Setenv("NEO4J_MCP_HTTP_TLS_KEY_FILE", "/path/to/key.pem") @@ -598,7 +766,7 @@ func TestLoadConfig_TLS(t *testing.T) { }) t.Run("TLS validation error with invalid cert/key files", func(t *testing.T) { - t.Setenv("NEO4J_TRANSPORT_MODE", "http") + t.Setenv("NEO4J_MCP_TRANSPORT_MODE", "http") t.Setenv("NEO4J_MCP_HTTP_TLS_ENABLED", "true") t.Setenv("NEO4J_MCP_HTTP_TLS_CERT_FILE", "/nonexistent/cert.pem") t.Setenv("NEO4J_MCP_HTTP_TLS_KEY_FILE", "/nonexistent/key.pem") @@ -619,7 +787,7 @@ func TestLoadConfig_TLS(t *testing.T) { func TestLoadConfig_DefaultHTTPPort(t *testing.T) { t.Run("Default port 80 when TLS disabled", func(t *testing.T) { - t.Setenv("NEO4J_TRANSPORT_MODE", "http") + t.Setenv("NEO4J_MCP_TRANSPORT_MODE", "http") // NEO4J_MCP_HTTP_TLS_ENABLED is not set (defaults to false) cfg, err := LoadConfig(nil) @@ -635,7 +803,7 @@ func TestLoadConfig_DefaultHTTPPort(t *testing.T) { t.Run("Default port 443 when TLS enabled", func(t *testing.T) { certPath, keyPath := testutil.GenerateTestTLSCertificate(t) - t.Setenv("NEO4J_TRANSPORT_MODE", "http") + t.Setenv("NEO4J_MCP_TRANSPORT_MODE", "http") t.Setenv("NEO4J_MCP_HTTP_TLS_ENABLED", "true") t.Setenv("NEO4J_MCP_HTTP_TLS_CERT_FILE", certPath) t.Setenv("NEO4J_MCP_HTTP_TLS_KEY_FILE", keyPath) @@ -654,7 +822,7 @@ func TestLoadConfig_DefaultHTTPPort(t *testing.T) { t.Run("Explicit port overrides default", func(t *testing.T) { certPath, keyPath := testutil.GenerateTestTLSCertificate(t) - t.Setenv("NEO4J_TRANSPORT_MODE", "http") + t.Setenv("NEO4J_MCP_TRANSPORT_MODE", "http") t.Setenv("NEO4J_MCP_HTTP_TLS_ENABLED", "true") t.Setenv("NEO4J_MCP_HTTP_TLS_CERT_FILE", certPath) t.Setenv("NEO4J_MCP_HTTP_TLS_KEY_FILE", keyPath) @@ -673,7 +841,7 @@ func TestLoadConfig_DefaultHTTPPort(t *testing.T) { t.Run("CLI override for port takes precedence", func(t *testing.T) { certPath, keyPath := testutil.GenerateTestTLSCertificate(t) - t.Setenv("NEO4J_TRANSPORT_MODE", "http") + t.Setenv("NEO4J_MCP_TRANSPORT_MODE", "http") t.Setenv("NEO4J_MCP_HTTP_TLS_ENABLED", "true") t.Setenv("NEO4J_MCP_HTTP_TLS_CERT_FILE", certPath) t.Setenv("NEO4J_MCP_HTTP_TLS_KEY_FILE", keyPath) @@ -696,7 +864,7 @@ func TestLoadConfig_DefaultHTTPPort(t *testing.T) { t.Run("CLI TLS enable changes default port", func(t *testing.T) { certPath, keyPath := testutil.GenerateTestTLSCertificate(t) - t.Setenv("NEO4J_TRANSPORT_MODE", "http") + t.Setenv("NEO4J_MCP_TRANSPORT_MODE", "http") t.Setenv("NEO4J_MCP_HTTP_TLS_ENABLED", "false") t.Setenv("NEO4J_MCP_HTTP_TLS_CERT_FILE", certPath) t.Setenv("NEO4J_MCP_HTTP_TLS_KEY_FILE", keyPath) @@ -719,11 +887,11 @@ func TestLoadConfig_DefaultHTTPPort(t *testing.T) { func TestLoadConfig_HTTPAllowedOrigins(t *testing.T) { t.Run("HTTPAllowedOrigins from environment variable", func(t *testing.T) { - t.Setenv("NEO4J_TRANSPORT_MODE", "stdio") - t.Setenv("NEO4J_URI", "bolt://localhost:7687") - t.Setenv("NEO4J_USERNAME", "neo4j") - t.Setenv("NEO4J_PASSWORD", "password") - t.Setenv("NEO4J_DATABASE", "neo4j") + t.Setenv("NEO4J_MCP_TRANSPORT_MODE", "stdio") + t.Setenv("NEO4J_MCP_URI", "bolt://localhost:7687") + t.Setenv("NEO4J_MCP_USERNAME", "neo4j") + t.Setenv("NEO4J_MCP_PASSWORD", "password") + t.Setenv("NEO4J_MCP_DATABASE", "neo4j") t.Setenv("NEO4J_MCP_HTTP_ALLOWED_ORIGINS", "https://example.com,https://example2.com") cfg, err := LoadConfig(nil) @@ -737,11 +905,11 @@ func TestLoadConfig_HTTPAllowedOrigins(t *testing.T) { }) t.Run("HTTPAllowedOrigins with wildcard from environment variable", func(t *testing.T) { - t.Setenv("NEO4J_TRANSPORT_MODE", "stdio") - t.Setenv("NEO4J_URI", "bolt://localhost:7687") - t.Setenv("NEO4J_USERNAME", "neo4j") - t.Setenv("NEO4J_PASSWORD", "password") - t.Setenv("NEO4J_DATABASE", "neo4j") + t.Setenv("NEO4J_MCP_TRANSPORT_MODE", "stdio") + t.Setenv("NEO4J_MCP_URI", "bolt://localhost:7687") + t.Setenv("NEO4J_MCP_USERNAME", "neo4j") + t.Setenv("NEO4J_MCP_PASSWORD", "password") + t.Setenv("NEO4J_MCP_DATABASE", "neo4j") t.Setenv("NEO4J_MCP_HTTP_ALLOWED_ORIGINS", "*") cfg, err := LoadConfig(nil) @@ -755,10 +923,11 @@ func TestLoadConfig_HTTPAllowedOrigins(t *testing.T) { }) t.Run("HTTPAllowedOrigins empty by default", func(t *testing.T) { - t.Setenv("NEO4J_TRANSPORT_MODE", "stdio") - t.Setenv("NEO4J_URI", "bolt://localhost:7687") - t.Setenv("NEO4J_USERNAME", "neo4j") - t.Setenv("NEO4J_PASSWORD", "password") + t.Setenv("NEO4J_MCP_TRANSPORT_MODE", "stdio") + t.Setenv("NEO4J_MCP_URI", "bolt://localhost:7687") + t.Setenv("NEO4J_MCP_USERNAME", "neo4j") + t.Setenv("NEO4J_MCP_PASSWORD", "password") + t.Setenv("NEO4J_MCP_DATABASE", "neo4j") t.Setenv("NEO4J_DATABASE", "neo4j") // Don't set NEO4J_MCP_HTTP_ALLOWED_ORIGINS @@ -773,10 +942,10 @@ func TestLoadConfig_HTTPAllowedOrigins(t *testing.T) { }) t.Run("HTTPAllowedOrigins CLI override takes precedence over environment", func(t *testing.T) { - t.Setenv("NEO4J_TRANSPORT_MODE", "stdio") - t.Setenv("NEO4J_URI", "bolt://localhost:7687") - t.Setenv("NEO4J_USERNAME", "neo4j") - t.Setenv("NEO4J_PASSWORD", "password") + t.Setenv("NEO4J_MCP_TRANSPORT_MODE", "stdio") + t.Setenv("NEO4J_MCP_URI", "bolt://localhost:7687") + t.Setenv("NEO4J_MCP_USERNAME", "neo4j") + t.Setenv("NEO4J_MCP_PASSWORD", "password") t.Setenv("NEO4J_DATABASE", "neo4j") t.Setenv("NEO4J_MCP_HTTP_ALLOWED_ORIGINS", "https://env-example.com") @@ -798,11 +967,11 @@ func TestLoadConfig_HTTPAllowedOrigins(t *testing.T) { func TestLoadConfig_AuthHeaderName(t *testing.T) { // Default header name when not set t.Run("default header name", func(t *testing.T) { - t.Setenv("NEO4J_TRANSPORT_MODE", "stdio") - t.Setenv("NEO4J_URI", "bolt://localhost:7687") - t.Setenv("NEO4J_USERNAME", "neo4j") - t.Setenv("NEO4J_PASSWORD", "password") - t.Setenv("NEO4J_DATABASE", "neo4j") + t.Setenv("NEO4J_MCP_TRANSPORT_MODE", "stdio") + t.Setenv("NEO4J_MCP_URI", "bolt://localhost:7687") + t.Setenv("NEO4J_MCP_USERNAME", "neo4j") + t.Setenv("NEO4J_MCP_PASSWORD", "password") + t.Setenv("NEO4J_MCP_DATABASE", "neo4j") cfg, err := LoadConfig(nil) if err != nil { @@ -816,12 +985,12 @@ func TestLoadConfig_AuthHeaderName(t *testing.T) { // Custom header name from environment variable t.Run("custom header from env", func(t *testing.T) { - t.Setenv("NEO4J_TRANSPORT_MODE", "stdio") - t.Setenv("NEO4J_URI", "bolt://localhost:7687") - t.Setenv("NEO4J_USERNAME", "neo4j") - t.Setenv("NEO4J_PASSWORD", "password") - t.Setenv("NEO4J_DATABASE", "neo4j") - t.Setenv("NEO4J_HTTP_AUTH_HEADER_NAME", "X-Test-Auth") + t.Setenv("NEO4J_MCP_TRANSPORT_MODE", "stdio") + t.Setenv("NEO4J_MCP_URI", "bolt://localhost:7687") + t.Setenv("NEO4J_MCP_USERNAME", "neo4j") + t.Setenv("NEO4J_MCP_PASSWORD", "password") + t.Setenv("NEO4J_MCP_HTTP_AUTH_HEADER_NAME", "X-Test-Auth") + t.Setenv("NEO4J_MCP_DATABASE", "neo4j") cfg, err := LoadConfig(nil) if err != nil { @@ -835,12 +1004,12 @@ func TestLoadConfig_AuthHeaderName(t *testing.T) { // CLI override should take precedence over environment variable t.Run("cli override takes precedence", func(t *testing.T) { - t.Setenv("NEO4J_TRANSPORT_MODE", "stdio") - t.Setenv("NEO4J_URI", "bolt://localhost:7687") - t.Setenv("NEO4J_USERNAME", "neo4j") - t.Setenv("NEO4J_PASSWORD", "password") - t.Setenv("NEO4J_DATABASE", "neo4j") - t.Setenv("NEO4J_HTTP_AUTH_HEADER_NAME", "X-Env-Auth") + t.Setenv("NEO4J_MCP_TRANSPORT_MODE", "stdio") + t.Setenv("NEO4J_MCP_URI", "bolt://localhost:7687") + t.Setenv("NEO4J_MCP_USERNAME", "neo4j") + t.Setenv("NEO4J_MCP_PASSWORD", "password") + t.Setenv("NEO4J_MCP_HTTP_AUTH_HEADER_NAME", "X-Env-Auth") + t.Setenv("NEO4J_MCP_DATABASE", "neo4j") overrides := &CLIOverrides{ AuthHeaderName: "X-CLI-Auth", @@ -858,11 +1027,11 @@ func TestLoadConfig_AuthHeaderName(t *testing.T) { // Whitespace-only CLI override should be rejected (validation) t.Run("whitespace-only cli override invalid", func(t *testing.T) { - t.Setenv("NEO4J_TRANSPORT_MODE", "stdio") - t.Setenv("NEO4J_URI", "bolt://localhost:7687") - t.Setenv("NEO4J_USERNAME", "neo4j") - t.Setenv("NEO4J_PASSWORD", "password") - t.Setenv("NEO4J_DATABASE", "neo4j") + t.Setenv("NEO4J_MCP_TRANSPORT_MODE", "stdio") + t.Setenv("NEO4J_MCP_URI", "bolt://localhost:7687") + t.Setenv("NEO4J_MCP_USERNAME", "neo4j") + t.Setenv("NEO4J_MCP_PASSWORD", "password") + t.Setenv("NEO4J_MCP_DATABASE", "neo4j") overrides := &CLIOverrides{ AuthHeaderName: " ", // non-empty but only whitespace -> should be trimmed to empty and cause an error @@ -891,28 +1060,28 @@ func TestLoadConfig_HTTPModeDatabase(t *testing.T) { wantDatabase string }{ { - name: "HTTP mode: NEO4J_DATABASE env var should raise error", + name: "HTTP mode: NEO4J_MCP_DATABASE env var should raise error", transport: "http", databaseEnv: "neo4j", - wantErr: "NEO4J_DATABASE environment variable", + wantErr: "NEO4J_MCP_DATABASE environment variable", }, { - name: "--neo4j-database flag in HTTP mode should raise error", + name: "--database flag in HTTP mode should raise error", transport: "http", cliOverrides: &CLIOverrides{Database: "custom-db"}, - wantErr: "--neo4j-database flag", + wantErr: "--database flag", }, { - name: "HTTP mode without NEO4J_DATABASE should have empty database", + name: "HTTP mode without NEO4J_MCP_DATABASE should have empty database", transport: "http", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - t.Setenv("NEO4J_TRANSPORT_MODE", tt.transport) + t.Setenv("NEO4J_MCP_TRANSPORT_MODE", tt.transport) if tt.databaseEnv != "" { - t.Setenv("NEO4J_DATABASE", tt.databaseEnv) + t.Setenv("NEO4J_MCP_DATABASE", tt.databaseEnv) } cfg, err := LoadConfig(tt.cliOverrides) diff --git a/internal/server/tools_register.go b/internal/server/tools_register.go index 36ed9b20..e91aa92c 100644 --- a/internal/server/tools_register.go +++ b/internal/server/tools_register.go @@ -16,9 +16,9 @@ import ( // registerTools registers all enabled MCP tools and adds them to the provided MCP server. // Tools are filtered according to the server configuration. For example, when the read-only -// mode is enabled (e.g. via the Config.ReadOnly flag, which can be set by the NEO4J_READ_ONLY environment variable or --neo4j-read-only flag), +// mode is enabled (e.g. via the Config.ReadOnly flag, which can be set by the NEO4J_MCP_READ_ONLY environment variable or --read-only flag), // any tool that performs state mutation will be excluded. -// Individual tools can also be selected via Config.Tools, which can be set by the NEO4J_MCP_TOOLS environment variable or --neo4j-tools flag, with Config.ReadOnly taking precedence. +// Individual tools can also be selected via Config.Tools, which can be set by the NEO4J_MCP_TOOLS environment variable or -tools flag, with Config.ReadOnly taking precedence. func (s *Neo4jMCPServer) registerTools() { tools := s.getTools() s.MCPServer.AddTools(tools...) diff --git a/test/dbservice/dbservice.go b/test/dbservice/dbservice.go index b5a8af9e..079171b5 100644 --- a/test/dbservice/dbservice.go +++ b/test/dbservice/dbservice.go @@ -70,7 +70,7 @@ func (dbs *dbService) GetDriverConf() *config.Config { return containerrunner.GetDriverConf() } - transportMode := config.GetTransportModeWithDefault("NEO4J_MCP_TRANSPORT", config.TransportModeStdio) + transportMode := config.GetTransportModeWithDefault("NEO4J_TRANSPORT_MODE", config.TransportModeStdio) cfg := &config.Config{ URI: config.GetEnvWithDefault("NEO4J_URI", "bolt://localhost:7687"), Username: config.GetEnvWithDefault("NEO4J_USERNAME", "neo4j"), diff --git a/test/e2e/README.md b/test/e2e/README.md index e9b63f58..96315e73 100644 --- a/test/e2e/README.md +++ b/test/e2e/README.md @@ -12,10 +12,10 @@ func TestMyE2EFeature(t *testing.T) { ctx := context.Background() cfg := dbs.GetDriverConf() args := []string{ - "--neo4j-uri", cfg.URI, - "--neo4j-username", cfg.Username, - "--neo4j-password", cfg.Password, - "--neo4j-database", cfg.Database, + "--uri", cfg.URI, + "--username", cfg.Username, + "--password", cfg.Password, + "--database", cfg.Database, } mcpClient, err := client.NewStdioMCPClient(server, []string{}, args...) diff --git a/test/e2e/empty_params_test.go b/test/e2e/empty_params_test.go index ab36a728..2addea77 100644 --- a/test/e2e/empty_params_test.go +++ b/test/e2e/empty_params_test.go @@ -22,10 +22,10 @@ func TestEmptyParamsE2E(t *testing.T) { ctx := context.Background() cfg := dbs.GetDriverConf() args := []string{ - "--neo4j-uri", cfg.URI, - "--neo4j-username", cfg.Username, - "--neo4j-password", cfg.Password, - "--neo4j-database", cfg.Database, + "--uri", cfg.URI, + "--username", cfg.Username, + "--password", cfg.Password, + "--database", cfg.Database, } mcpClient, err := client.NewStdioMCPClient(server, []string{}, args...) diff --git a/test/e2e/get_schema_test.go b/test/e2e/get_schema_test.go index 0d1e5cc4..eeb79936 100644 --- a/test/e2e/get_schema_test.go +++ b/test/e2e/get_schema_test.go @@ -22,10 +22,10 @@ func TestGetSchemaE2E(t *testing.T) { cfg := dbs.GetDriverConf() args := []string{ - "--neo4j-uri", cfg.URI, - "--neo4j-username", cfg.Username, - "--neo4j-password", cfg.Password, - "--neo4j-database", cfg.Database, + "--uri", cfg.URI, + "--username", cfg.Username, + "--password", cfg.Password, + "--database", cfg.Database, } mcpClient, err := client.NewStdioMCPClient(server, []string{}, args...) diff --git a/test/e2e/http_server_helpers_test.go b/test/e2e/http_server_helpers_test.go index 8d921ea6..5a64f6cd 100644 --- a/test/e2e/http_server_helpers_test.go +++ b/test/e2e/http_server_helpers_test.go @@ -38,7 +38,7 @@ func startHTTPModeServer(t *testing.T, extraArgs ...string) string { baseURL := fmt.Sprintf("http://127.0.0.1:%d", port) // In HTTP mode the config validation rejects NEO4J_URI, NEO4J_USERNAME, NEO4J_PASSWORD, and - // NEO4J_DATABASE — the URI, credentials, and database are supplied per-request via the + // NEO4J_MCP_DATABASE — the URI, credentials, and database are supplied per-request via the // X-Neo4j-MCP-URI header, Auth headers, and URL path respectively. // Strip those keys so any locally-set env values don't cause a startup validation error. args := append([]string{ @@ -48,7 +48,7 @@ func startHTTPModeServer(t *testing.T, extraArgs ...string) string { "--neo4j-telemetry", "false", }, extraArgs...) cmd := exec.Command(server, args...) // #nosec G204 -- server is a binary path built by the test harness, not user input - cmd.Env = stripEnv(os.Environ(), "NEO4J_URI", "NEO4J_USERNAME", "NEO4J_PASSWORD", "NEO4J_DATABASE") + cmd.Env = stripEnv(os.Environ(), "NEO4J_URI", "NEO4J_USERNAME", "NEO4J_PASSWORD", "NEO4J_DATABASE", "NEO4J_MCP_URI", "NEO4J_MCP_USERNAME", "NEO4J_MCP_PASSWORD", "NEO4J_MCP_DATABASE") require.NoError(t, cmd.Start(), "failed to start HTTP server") diff --git a/test/e2e/issue_157_test.go b/test/e2e/issue_157_test.go index 48a6d7cc..e2ca0f03 100644 --- a/test/e2e/issue_157_test.go +++ b/test/e2e/issue_157_test.go @@ -38,10 +38,10 @@ func TestIssue157(t *testing.T) { cfg := dbs.GetDriverConf() args := []string{ - "--neo4j-uri", cfg.URI, - "--neo4j-username", cfg.Username, - "--neo4j-password", cfg.Password, - "--neo4j-database", cfg.Database, + "--uri", cfg.URI, + "--username", cfg.Username, + "--password", cfg.Password, + "--database", cfg.Database, } mcpClient, err := client.NewStdioMCPClient(server, []string{}, args...) diff --git a/test/e2e/mcp_server_lifecycle_test.go b/test/e2e/mcp_server_lifecycle_test.go index ffa847fc..0107dfb6 100644 --- a/test/e2e/mcp_server_lifecycle_test.go +++ b/test/e2e/mcp_server_lifecycle_test.go @@ -23,10 +23,10 @@ func TestSeverLifecycleMCPE2E(t *testing.T) { cfg := dbs.GetDriverConf() args := []string{ - "--neo4j-uri", cfg.URI, - "--neo4j-username", cfg.Username, - "--neo4j-password", cfg.Password, - "--neo4j-database", cfg.Database, + "--uri", cfg.URI, + "--username", cfg.Username, + "--password", cfg.Password, + "--database", cfg.Database, } mcpClient, err := client.NewStdioMCPClient(server, []string{}, args...) diff --git a/test/e2e/server_initialization_test.go b/test/e2e/server_initialization_test.go index 108ae113..54719d06 100644 --- a/test/e2e/server_initialization_test.go +++ b/test/e2e/server_initialization_test.go @@ -24,10 +24,10 @@ func TestServerInitializationE2E(t *testing.T) { t.Parallel() args := []string{ - "--neo4j-uri", cfg.URI, - "--neo4j-username", cfg.Username, - "--neo4j-password", cfg.Password, - "--neo4j-database", cfg.Database, + "--uri", cfg.URI, + "--username", cfg.Username, + "--password", cfg.Password, + "--database", cfg.Database, } mcpClient, err := client.NewStdioMCPClient(server, []string{}, args...) @@ -55,11 +55,11 @@ func TestServerInitializationE2E(t *testing.T) { t.Parallel() args := []string{ - "--neo4j-uri", cfg.URI, - "--neo4j-username", cfg.Username, - "--neo4j-password", cfg.Password, - "--neo4j-database", cfg.Database, - "--neo4j-read-only", "true", + "--uri", cfg.URI, + "--username", cfg.Username, + "--password", cfg.Password, + "--database", cfg.Database, + "--read-only", "true", } mcpClient, err := client.NewStdioMCPClient(server, []string{}, args...) @@ -90,11 +90,11 @@ func TestServerInitializationE2E(t *testing.T) { t.Parallel() args := []string{ - "--neo4j-uri", cfg.URI, - "--neo4j-username", cfg.Username, - "--neo4j-password", cfg.Password, - "--neo4j-database", cfg.Database, - "--neo4j-read-only", "false", + "--uri", cfg.URI, + "--username", cfg.Username, + "--password", cfg.Password, + "--database", cfg.Database, + "--read-only", "false", } mcpClient, err := client.NewStdioMCPClient(server, []string{}, args...) @@ -116,11 +116,11 @@ func TestServerInitializationE2E(t *testing.T) { t.Parallel() args := []string{ - "--neo4j-uri", cfg.URI, - "--neo4j-username", cfg.Username, - "--neo4j-password", cfg.Password, - "--neo4j-database", cfg.Database, - "--neo4j-telemetry", "false", + "--uri", cfg.URI, + "--username", cfg.Username, + "--password", cfg.Password, + "--database", cfg.Database, + "--telemetry", "false", } mcpClient, err := client.NewStdioMCPClient(server, []string{}, args...) @@ -142,11 +142,11 @@ func TestServerInitializationE2E(t *testing.T) { t.Parallel() args := []string{ - "--neo4j-uri", cfg.URI, - "--neo4j-username", cfg.Username, - "--neo4j-password", cfg.Password, - "--neo4j-database", cfg.Database, - "--neo4j-schema-sample-size", "50", + "--uri", cfg.URI, + "--username", cfg.Username, + "--password", cfg.Password, + "--database", cfg.Database, + "--schema-sample-size", "50", } mcpClient, err := client.NewStdioMCPClient(server, []string{}, args...) @@ -168,11 +168,11 @@ func TestServerInitializationE2E(t *testing.T) { t.Parallel() args := []string{ - "--neo4j-uri", cfg.URI, - "--neo4j-username", cfg.Username, - "--neo4j-password", cfg.Password, - "--neo4j-database", cfg.Database, - "--neo4j-schema-sample-size", "not-a-number", + "--uri", cfg.URI, + "--username", cfg.Username, + "--password", cfg.Password, + "--database", cfg.Database, + "--schema-sample-size", "not-a-number", } mcpClient, err := client.NewStdioMCPClient(server, []string{}, args...) @@ -194,10 +194,10 @@ func TestServerInitializationE2E(t *testing.T) { t.Parallel() args := []string{ - "--neo4j-uri", cfg.URI, - "--neo4j-username", cfg.Username, - "--neo4j-password", cfg.Password, - "--neo4j-database", cfg.Database, + "--uri", cfg.URI, + "--username", cfg.Username, + "--password", cfg.Password, + "--database", cfg.Database, } mcpClient, err := client.NewStdioMCPClient(server, []string{}, args...)