Skip to content
Open
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
3 changes: 3 additions & 0 deletions .changes/unreleased/Minor-vector-search.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
kind: Minor
body: 'Add `vector-search` tool for semantic vector search. It embeds the query text at query time inside Neo4j via the GenAI plugin (`ai.text.embed`) and returns the most similar nodes with similarity scores. Embedding providers (OpenAI, Azure OpenAI, Vertex AI, Bedrock Titan) are configured via `NEO4J_EMBEDDING_*` environment variables; the tool is only registered when an embedding provider is configured. Supports structured metadata filters and is version-aware (uses the `SEARCH` clause on Neo4j 2026.01+, falling back to `db.index.vector.queryNodes()` on 2025.x).'
time: 2026-06-26T00:00:00.000000-00:00
3 changes: 3 additions & 0 deletions .changes/unreleased/Patch-vector-search-embed-fallback.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
kind: Patch
body: 'Fix `vector-search` on Neo4j versions older than 2025.11 (including current Neo4j Aura 5.x releases), where the GenAI plugin does not yet have `ai.text.embed()`. The tool now detects the Neo4j version and automatically falls back to the deprecated-but-present `genai.vector.encode()` function, with its PascalCase provider names and config keys, so embedding continues to work with no user action needed.'
time: 2026-07-03T09:22:16.000000-00:00
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,6 @@ dist/
CLAUDE.md
gemini.md
*.pem

# macOS
.DS_Store
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ By implementing the Model Context Protocol (MCP), it acts as a bridge between an
- `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_READ_ONLY=true`)
- `list-gds-procedures` — list available GDS procedures
- `vector-search` — semantic vector search over a Neo4j vector index. Embeds the query text at query time (via the Neo4j GenAI plugin) and returns the most similar nodes with similarity scores, with optional metadata filters. Only registered when an embedding provider is configured (see [Vector search configuration](#vector-search-configuration)).

## Installation

Expand Down Expand Up @@ -58,6 +59,44 @@ Create / edit `mcp.json`:

See [MCP documentation > Configuration](https://neo4j.com/docs/mcp/current/configuration) for more details.

## Vector search configuration

The `vector-search` tool is enabled only when an embedding provider is configured. The
server embeds the query text inside Neo4j using the GenAI plugin, so the target instance
must have the GenAI plugin available (standard on Neo4j Aura) and at least one vector
index. It uses the GenAI plugin's `ai.text.embed()` function on Neo4j 2025.11 and later,
and automatically falls back to the deprecated-but-present `genai.vector.encode()`
function on older versions — including the 5.x releases currently deployed on Neo4j Aura —
so no user action is needed. The embedding **model must match the model used to
create the stored embeddings**, otherwise similarity scores are meaningless.

Configure via environment variables:

| Variable | Applies to | Description |
|---|---|---|
| `NEO4J_EMBEDDING_PROVIDER` | all | `openai`, `azure-openai`, `vertexai`, or `bedrock-titan`. Empty disables `vector-search`. |
| `NEO4J_EMBEDDING_MODEL` | all | Embedding model id, e.g. `text-embedding-3-small`. |
| `NEO4J_EMBEDDING_API_KEY` | openai, azure-openai, vertexai | Provider API token. |
| `NEO4J_EMBEDDING_DIMENSIONS` | optional | Output dimensions, only if your model/index uses a reduced size. |
| `NEO4J_EMBEDDING_AZURE_RESOURCE` | azure-openai | Azure resource name. |
| `NEO4J_EMBEDDING_VERTEX_PROJECT` | vertexai | Google Cloud project id. |
| `NEO4J_EMBEDDING_VERTEX_REGION` | vertexai | GCP region. |
| `NEO4J_EMBEDDING_VERTEX_PUBLISHER` | vertexai | Optional, defaults to `google`. |
| `NEO4J_EMBEDDING_AWS_ACCESS_KEY_ID` | bedrock-titan | AWS access key id. |
| `NEO4J_EMBEDDING_AWS_SECRET_ACCESS_KEY` | bedrock-titan | AWS secret access key. |
| `NEO4J_EMBEDDING_AWS_REGION` | bedrock-titan | AWS region. |

The API key is read only from the server environment, is passed to Neo4j as a bound query
parameter (obfuscated in the query log), and is never accepted as a tool input or returned
to clients. In production deployments, source it from a secret manager rather than plain
text. Example (OpenAI) for an `mcp.json` `env` block:

```json
"NEO4J_EMBEDDING_PROVIDER": "openai",
"NEO4J_EMBEDDING_MODEL": "text-embedding-3-small",
"NEO4J_EMBEDDING_API_KEY": "sk-..."
```

## Links

- [Documentation](https://neo4j.com/docs/mcp/current/): The official Neo4j MCP documentation.
Expand Down
130 changes: 130 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,30 @@ const (
TransportModeStdio TransportMode = "stdio"
TransportModeHTTP TransportMode = "http"
DeprecatedVariableMessage string = "Warning: deprecated environment variable \"%s\". Please use: \"%s\" instead\n"

// Embedding provider constants
EmbeddingProviderOpenAI = "openai"
EmbeddingProviderAzureOpenAI = "azure-openai"
EmbeddingProviderVertexAI = "vertexai"
EmbeddingProviderBedrockTitan = "bedrock-titan"
)

// EmbeddingConfig holds optional embedding provider configuration.
// All fields are optional; an empty Provider means embedding is disabled.
type EmbeddingConfig struct {
Provider string
Model string
APIKey string
Dimensions string
AzureResource string
VertexProject string
VertexRegion string
VertexPublisher string
AWSAccessKeyID string
AWSSecretAccessKey string
AWSRegion string
}

// ValidTransportModes defines the allowed transport mode values
var ValidTransportModes = []TransportMode{TransportModeStdio, TransportModeHTTP}

Expand All @@ -49,6 +71,33 @@ type Config struct {
AuthHeaderName string // HTTP header name to read auth credentials from (default: "Authorization")
AllowUnauthenticatedPing bool // If true, allows unauthenticated ping health checks in HTTP mode
AllowUnauthenticatedToolsList bool // If true, allows unauthenticated tools list in HTTP mode
embeddingCfg EmbeddingConfig
}

// EmbeddingConfig returns the embedding provider configuration.
func (c *Config) EmbeddingConfig() EmbeddingConfig {
return c.embeddingCfg
}

// IsEmbeddingConfigured returns true only when Provider is non-empty and all
// required credentials for that provider are present.
func (c *Config) IsEmbeddingConfigured() bool {
emb := c.embeddingCfg
if emb.Provider == "" {
return false
}
switch emb.Provider {
case EmbeddingProviderOpenAI:
return emb.Model != "" && emb.APIKey != ""
case EmbeddingProviderAzureOpenAI:
return emb.Model != "" && emb.APIKey != "" && emb.AzureResource != ""
case EmbeddingProviderVertexAI:
return emb.Model != "" && emb.APIKey != "" && emb.VertexProject != "" && emb.VertexRegion != ""
case EmbeddingProviderBedrockTitan:
return emb.Model != "" && emb.AWSAccessKeyID != "" && emb.AWSSecretAccessKey != "" && emb.AWSRegion != ""
default:
return false
}
}

// Validate validates the configuration and returns an error if invalid
Expand Down Expand Up @@ -101,6 +150,74 @@ func (c *Config) Validate() error {
}
}

// Validate embedding configuration only when a provider is set
if err := validateEmbeddingConfig(c.embeddingCfg); err != nil {
return err
}

return nil
}

// validateEmbeddingConfig validates the embedding configuration.
// If Provider is empty, embedding is disabled and no error is returned.
func validateEmbeddingConfig(emb EmbeddingConfig) error {
if emb.Provider == "" {
return nil
}

switch emb.Provider {
case EmbeddingProviderOpenAI:
if emb.Model == "" {
return fmt.Errorf("NEO4J_EMBEDDING_MODEL is required when NEO4J_EMBEDDING_PROVIDER is '%s'", emb.Provider)
}
if emb.APIKey == "" {
return fmt.Errorf("NEO4J_EMBEDDING_API_KEY is required when NEO4J_EMBEDDING_PROVIDER is '%s'", emb.Provider)
}
case EmbeddingProviderAzureOpenAI:
if emb.Model == "" {
return fmt.Errorf("NEO4J_EMBEDDING_MODEL is required when NEO4J_EMBEDDING_PROVIDER is '%s'", emb.Provider)
}
if emb.APIKey == "" {
return fmt.Errorf("NEO4J_EMBEDDING_API_KEY is required when NEO4J_EMBEDDING_PROVIDER is '%s'", emb.Provider)
}
if emb.AzureResource == "" {
return fmt.Errorf("NEO4J_EMBEDDING_AZURE_RESOURCE is required when NEO4J_EMBEDDING_PROVIDER is '%s'", emb.Provider)
}
case EmbeddingProviderVertexAI:
if emb.Model == "" {
return fmt.Errorf("NEO4J_EMBEDDING_MODEL is required when NEO4J_EMBEDDING_PROVIDER is '%s'", emb.Provider)
}
if emb.APIKey == "" {
return fmt.Errorf("NEO4J_EMBEDDING_API_KEY is required when NEO4J_EMBEDDING_PROVIDER is '%s'", emb.Provider)
}
if emb.VertexProject == "" {
return fmt.Errorf("NEO4J_EMBEDDING_VERTEX_PROJECT is required when NEO4J_EMBEDDING_PROVIDER is '%s'", emb.Provider)
}
if emb.VertexRegion == "" {
return fmt.Errorf("NEO4J_EMBEDDING_VERTEX_REGION is required when NEO4J_EMBEDDING_PROVIDER is '%s'", emb.Provider)
}
case EmbeddingProviderBedrockTitan:
if emb.Model == "" {
return fmt.Errorf("NEO4J_EMBEDDING_MODEL is required when NEO4J_EMBEDDING_PROVIDER is '%s'", emb.Provider)
}
if emb.AWSAccessKeyID == "" {
return fmt.Errorf("NEO4J_EMBEDDING_AWS_ACCESS_KEY_ID is required when NEO4J_EMBEDDING_PROVIDER is '%s'", emb.Provider)
}
if emb.AWSSecretAccessKey == "" {
return fmt.Errorf("NEO4J_EMBEDDING_AWS_SECRET_ACCESS_KEY is required when NEO4J_EMBEDDING_PROVIDER is '%s'", emb.Provider)
}
if emb.AWSRegion == "" {
return fmt.Errorf("NEO4J_EMBEDDING_AWS_REGION is required when NEO4J_EMBEDDING_PROVIDER is '%s'", emb.Provider)
}
default:
return fmt.Errorf("invalid NEO4J_EMBEDDING_PROVIDER '%s', must be one of: %s, %s, %s, %s",
emb.Provider,
EmbeddingProviderOpenAI,
EmbeddingProviderAzureOpenAI,
EmbeddingProviderVertexAI,
EmbeddingProviderBedrockTitan,
)
}
return nil
}

Expand Down Expand Up @@ -167,6 +284,19 @@ func LoadConfig(cliOverrides *CLIOverrides) (*Config, error) {
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),
embeddingCfg: EmbeddingConfig{
Provider: GetEnv("NEO4J_EMBEDDING_PROVIDER"),
Model: GetEnv("NEO4J_EMBEDDING_MODEL"),
APIKey: GetEnv("NEO4J_EMBEDDING_API_KEY"),
Dimensions: GetEnv("NEO4J_EMBEDDING_DIMENSIONS"),
AzureResource: GetEnv("NEO4J_EMBEDDING_AZURE_RESOURCE"),
VertexProject: GetEnv("NEO4J_EMBEDDING_VERTEX_PROJECT"),
VertexRegion: GetEnv("NEO4J_EMBEDDING_VERTEX_REGION"),
VertexPublisher: GetEnv("NEO4J_EMBEDDING_VERTEX_PUBLISHER"),
AWSAccessKeyID: GetEnv("NEO4J_EMBEDDING_AWS_ACCESS_KEY_ID"),
AWSSecretAccessKey: GetEnv("NEO4J_EMBEDDING_AWS_SECRET_ACCESS_KEY"),
AWSRegion: GetEnv("NEO4J_EMBEDDING_AWS_REGION"),
},
}

// Apply CLI overrides if provided
Expand Down
Loading
Loading