Skip to content

feat: add vector-search tool with query-time in-database embeddings - #306

Open
guerinjeanmarc wants to merge 1 commit into
neo4j:mainfrom
neo4j-field:feature/vector-search-tool
Open

feat: add vector-search tool with query-time in-database embeddings#306
guerinjeanmarc wants to merge 1 commit into
neo4j:mainfrom
neo4j-field:feature/vector-search-tool

Conversation

@guerinjeanmarc

Copy link
Copy Markdown

Summary

Adds a single new MCP tool, vector-search, that performs semantic vector similarity search over an existing Neo4j vector index. The tool embeds the user's natural-language query at query time, inside Neo4j (via the GenAI plugin), runs the similarity search, and returns the most similar nodes with their scores. It supports optional structured metadata filtering and works over both the STDIO and HTTP transports.

The tool is registered only when an embedding provider is configured, so servers without embedding configuration are unaffected.

Motivation / use case

The server can already run and read Cypher, but it has no first-class way to do semantic retrieval. The driving use case is a hosted deployment (Microsoft Copilot Studio → Cloud Run → Neo4j Aura) where an agent needs to find semantically relevant nodes from natural language and then chain into read-cypher for graph traversal. Doing the embedding in the database means the MCP server never has to ship vectors around or hold per-request user credentials — it holds only a single server-side embedding API key.

What's added

  • One vector-search tool (not one tool per index). The index is resolved per call via SHOW VECTOR INDEXES: pass an optional indexName, or omit it and the tool auto-selects when exactly one vector index exists (erroring with the list of candidates when there are several, and a clear message when there are none).
  • Query-time, in-database embeddings — the query text is embedded inside Neo4j; no embedding happens in the server process.
  • Provider-agnostic embedding config — OpenAI, Azure OpenAI, Vertex AI, and Bedrock Titan, all via NEO4J_EMBEDDING_* environment variables.
  • Optional structured metadata filters{property, operator, value}, applied as a post-filter.
  • New package internal/tools/vector/ (spec, index resolver, embedding-config builders, query builder, handler) plus embedding-config parsing/validation in internal/config, version detection in internal/server/server.go, and startup registration in internal/server/tools_register.go.
  • README "Vector search configuration" section, manifest.json updates, and changelog entries.
  • Extensive unit tests across the new package and the config/server changes.

Design decisions

Version-gated embedding function (ai.text.embedgenai.vector.encode)

ai.text.embed() was only introduced in Neo4j 2025.11. Instances on older versions — including freshly provisioned Aura Professional, which currently runs 5.27-aura — do not have it, so a naive implementation fails on today's Aura.

The tool detects the Neo4j version from dbms.components() at startup and:

  • uses ai.text.embed on Neo4j >= 2025.11, and
  • automatically falls back to the deprecated-but-present genai.vector.encode on older versions.

genai.vector.encode uses PascalCase provider names (OpenAI, AzureOpenAI, VertexAI, Bedrock) and different config keys (flat dimensions; Azure deployment instead of model; Vertex projectId), so a dedicated config builder produces the fallback shape. No user action is required either way.

Version-gated query strategy (SEARCH clause → db.index.vector.queryNodes())

The tool uses the SEARCH clause on Neo4j >= 2026.01 and falls back to db.index.vector.queryNodes() / queryRelationships() on 2025.x. Both paths share the same filter/return/order/limit tail.

Registered at startup (not deferred) so HTTP clients can discover it

Version-dependent behavior would normally suggest deferring registration until after version detection (as the GDS tools do). But some HTTP clients — notably Copilot Studio — import the tool list once and never re-fetch it, so a tool that appears only after the initialize handshake is invisible to them.

Instead, vector-search is registered at startup, gated solely on embedding configuration (known at startup in both transports). The two version gates are read lazily at call time via func() bool backed by atomic.Bool, which verifyRequirements sets once the Neo4j version is known. This keeps the tool in the initial tool list while still selecting the correct query and embedding strategy per call.

Metadata filters as a post-filter with over-fetch

Filters are applied as a post-filter after the vector search. To keep result quality reasonable when filters are present, the tool over-fetches candidates (topK × 5, capped at 1000) before filtering down to topK. Operators are validated against an allow-list (=, <>, <, <=, >, >=, IN, CONTAINS, STARTS WITH, ENDS WITH) and property names against a strict identifier pattern.

Configuration

The tool is enabled only when an embedding provider is configured. The target instance must have the GenAI plugin available (standard on Aura) and at least one vector index. The embedding model must match the model used to create the stored embeddings.

Variable Purpose
NEO4J_EMBEDDING_PROVIDER openai, azure-openai, vertexai, or bedrock-titan
NEO4J_EMBEDDING_MODEL Embedding model name
NEO4J_EMBEDDING_API_KEY Provider API key (server-side only)
NEO4J_EMBEDDING_DIMENSIONS Output dimensions (optional)
NEO4J_EMBEDDING_AZURE_RESOURCE Azure OpenAI resource
NEO4J_EMBEDDING_VERTEX_PROJECT Vertex AI project
NEO4J_EMBEDDING_VERTEX_REGION Vertex AI region
NEO4J_EMBEDDING_VERTEX_PUBLISHER Vertex AI publisher (default google)
NEO4J_EMBEDDING_AWS_ACCESS_KEY_ID Bedrock access key ID
NEO4J_EMBEDDING_AWS_SECRET_ACCESS_KEY Bedrock secret access key
NEO4J_EMBEDDING_AWS_REGION Bedrock region

Security considerations

  • API key never interpolated. The provider, model, and API key are assembled into an embedding-config map and passed to the embedding function as a bound Cypher parameter ($embedConfig) — never string-interpolated into query text.
  • Never a tool input, never returned. The API key is read only from the server environment. It is never accepted as a tool input and never returned to clients.
  • Errors sanitized. Client-facing errors are generic ("ensure the GenAI plugin is installed and the embedding provider credentials are valid; check server logs for details"); full errors are logged server-side only.
  • Embedding property stripped. The stored embedding vector is stripped from returned nodes via Cypher map-projection (using the property discovered from the index, not a hardcoded name).
  • Identifier hardening. Index name and label are escaped (backtick-quoted) before interpolation; filter property names are validated against a strict pattern and operators against an allow-list.

Testing

  • go build, go vet, go test -race, and golangci-lint run all green (0 lint issues).
  • Verified end-to-end against a live Neo4j Aura 5.27-aura instance through the actual MCP server in Claude Desktop, exercising the genai.vector.encode fallback path (OpenAI text-embedding-3-small, 1536-dim index): the tool embeds the query and returns ranked results.
  • Validated in Microsoft Copilot Studio (Copilot Studio → Cloud Run → Aura): the tool lists in the imported tool set, executes, and chains with read-cypher for graph traversal.

Out of scope (future work)

Intentionally deferred to keep this PR focused:

  • Hybrid and fulltext search tools
  • Graph-traversal / retrieval-query tools
  • get-schema enhancement to discover vector/fulltext indexes
  • In-index WHERE/IN filter push-down (uses a post-filter for now)

🤖 Generated with Claude Code

Adds a vector-search MCP tool that performs semantic similarity search over
a Neo4j vector index, embedding the query in-database at call time.

- Single tool; index resolved per call via SHOW VECTOR INDEXES (optional
  indexName, auto-selects the sole index otherwise).
- In-database embeddings via the GenAI plugin, provider-agnostic (OpenAI,
  Azure OpenAI, Vertex AI, Bedrock Titan) through NEO4J_EMBEDDING_* env vars.
- Version-gated embedding function: ai.text.embed on Neo4j >= 2025.11,
  falling back to genai.vector.encode on older versions (e.g. Aura 5.27-aura).
- Version-gated query strategy: SEARCH clause on Neo4j >= 2026.01, else
  db.index.vector.queryNodes()/queryRelationships().
- Optional structured metadata filters ({property, operator, value}) applied
  as a post-filter with over-fetch; operators allow-listed, identifiers escaped.
- Registered at startup gated on embedding config, so HTTP clients that fetch
  tools/list once (e.g. Copilot Studio) can discover it; the version strategy
  is read lazily at call time.

Security: provider, model, and API key are bound Cypher parameters (never
interpolated), never accepted as tool input or returned to clients, errors are
sanitized, and the stored embedding property is stripped from results.

Verified with go build/vet/test -race and golangci-lint (0 issues), end-to-end
against a live Aura 5.27-aura instance (genai.vector.encode fallback, OpenAI
1536-dim index), and in Microsoft Copilot Studio where it lists, executes, and
chains with read-cypher for graph traversal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@guerinjeanmarc
guerinjeanmarc requested a review from a team as a code owner July 3, 2026 13:44
@LackOfMorals

Copy link
Copy Markdown
Member

We have discussed this further and will not be taking this PR as it stands at the moment as we align on an approach for Tools for the official MCP server.

In general we see 3 tiers of tools

  • Those that take basic Cypher and run it e.g ReadCypher. These are the fall back tools where a LLM cannot use any of the others
  • Those that provided an outcome without needing any Cypher - they just accept parameters and return a response
  • Collection of tools that provide a solution e.g Memory Graph,

In addition we want the ability to control what tools are made visible to the LLM. This avoid overloading the LLM causing poor tool selection and context consumption.

Whilst we going through this process , we don't want to accept any tools for the moment as it is likely they will need reworking. Through in the other foundational work we need to do e.g get MCP 4 Aura up on VDC and we're a not in a position at the moment to give PRs the attention they deserve.

So for now, this PR is on hold.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants