Skip to content
Merged
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-20251117-145956.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
kind: Minor
body: The server now performs comprehensive pre-flight checks to verify your environment, including Neo4j connection, query capabilities, APOC installation, and will gracefully start without GDS-specific tools if the GDS library is not found.
time: 2025-11-17T14:59:56.826192Z
18 changes: 12 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,10 +162,12 @@ func MyToolHandler(deps *ToolDependencies) mcp.ToolHandler {
)
}
```
**Note:** WithReadOnlyHintAnnotation marks a tool with a read-only hint is used for filtering.
When set to true, the tool will be considered read-only and included when selecting
tools for read-only mode. If the annotation is not present or set to false,
the tool is treated as a write-capable tool (i.e., not considered read-only).

**Note:** WithReadOnlyHintAnnotation marks a tool with a read-only hint is used for filtering.
When set to true, the tool will be considered read-only and included when selecting
tools for read-only mode. If the annotation is not present or set to false,
the tool is treated as a write-capable tool (i.e., not considered read-only).

2. **Implement tool handler**:

```go
Expand All @@ -180,8 +182,12 @@ func MyToolHandler(deps *ToolDependencies) mcp.ToolHandler {

```go
{
Tool: NewMyToolSpec(),
Handler: NewMyToolHandler(deps),
category: cypherCategory,
definition: server.ServerTool{
Tool: cypher.GetSchemaSpec(),
Handler: cypher.GetSchemaHandler(deps),
},
readonly: true,
},
```

Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@ BETA - Active development; not yet suitable for production.
- APOC plugin installed in the Neo4j instance.
- Any MCP-compatible client (e.g. [VSCode](https://code.visualstudio.com/) with [MCP support](https://code.visualstudio.com/docs/copilot/customization/mcp-servers))

## Startup Checks & Adaptive Operation

The server performs several pre-flight checks at startup to ensure your environment is correctly configured.

**Mandatory Requirements**
The server verifies the following core requirements. If any of these checks fail (e.g., due to an invalid configuration, incorrect credentials, or a missing APOC installation), the server will not start:

- A valid connection to your Neo4j instance.
- The ability to execute queries.
- The presence of the APOC plugin.

**Optional Requirements**
If an optional dependency is missing, the server will start in an adaptive mode. For instance, if the Graph Data Science (GDS) library is not detected in your Neo4j installation, the server will still launch but will automatically disable all GDS-related tools, such as `list-gds-procedures`. All other tools will remain available.

## Installation (Binary)

Releases: https://github.com/neo4j/mcp/releases
Expand Down
6 changes: 0 additions & 6 deletions cmd/neo4j-mcp/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,6 @@ func main() {
}
}()

// Verify database connectivity
if err := driver.VerifyConnectivity(ctx); err != nil {
log.Printf("Failed to verify database connectivity: %v", err)
return
}

// Create database service
dbService, err := database.NewNeo4jService(driver, cfg.Database)
if err != nil {
Expand Down
5 changes: 5 additions & 0 deletions internal/database/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,13 @@ type RecordFormatter interface {
Neo4jRecordsToJSON(records []*neo4j.Record) (string, error)
}

type Helpers interface {
VerifyConnectivity(ctx context.Context) error
}

// Service combines query execution and record formatting
type Service interface {
QueryExecutor
RecordFormatter
Helpers
}
14 changes: 14 additions & 0 deletions internal/database/mocks/mock_database.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 10 additions & 1 deletion internal/database/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,18 @@ func NewNeo4jService(driver neo4j.DriverWithContext, database string) (*Neo4jSer
}, nil
}

// VerifyConnectivity checks the driver can establish a valid connection with a Neo4j instance;
func (s *Neo4jService) VerifyConnectivity(ctx context.Context) error {
// Verify database connectivity
if err := s.driver.VerifyConnectivity(ctx); err != nil {
log.Printf("Failed to verify database connectivity: %s", err.Error())
return err
}
return nil
}

// ExecuteReadQuery executes a read-only Cypher query and returns raw records
func (s *Neo4jService) ExecuteReadQuery(ctx context.Context, cypher string, params map[string]any) ([]*neo4j.Record, error) {

res, err := neo4j.ExecuteQuery(ctx, s.driver, cypher, params, neo4j.EagerResultTransformer, neo4j.ExecuteQueryWithDatabase(s.database), neo4j.ExecuteQueryWithReadersRouting())
if err != nil {
wrappedErr := fmt.Errorf("failed to execute read query: %w", err)
Expand Down
85 changes: 74 additions & 11 deletions internal/server/server.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package server

import (
"context"
"fmt"
"log"

Expand All @@ -12,11 +13,12 @@ import (

// Neo4jMCPServer represents the MCP server instance
type Neo4jMCPServer struct {
MCPServer *server.MCPServer
config *config.Config
dbService database.Service
version string
anService analytics.Service
MCPServer *server.MCPServer
config *config.Config
dbService database.Service
version string
anService analytics.Service
gdsInstalled bool
}

// NewNeo4jMCPServer creates a new MCP server instance
Expand All @@ -31,30 +33,91 @@ func NewNeo4jMCPServer(version string, cfg *config.Config, dbService database.Se
)

return &Neo4jMCPServer{
MCPServer: mcpServer,
config: cfg,
dbService: dbService,
version: version,
anService: anService,
MCPServer: mcpServer,
config: cfg,
dbService: dbService,
version: version,
anService: anService,
gdsInstalled: false,
}
}

// Start initializes and starts the MCP server using stdio transport
func (s *Neo4jMCPServer) Start() error {
log.Println("Starting Neo4j MCP Server...")
err := s.verifyRequirements()
if err != nil {
return err
}

// track startup event
s.anService.EmitEvent(s.anService.NewStartupEvent())

// Register tools
if err := s.RegisterTools(); err != nil {
if err := s.registerTools(); err != nil {
return fmt.Errorf("failed to register tools: %w", err)
}
log.Println("Started Neo4j MCP Server. Now listening for input...")
// Note: ServeStdio handles its own signal management for graceful shutdown
return server.ServeStdio(s.MCPServer)
}

// verifyRequirements check the Neo4j requirements:
// - A valid connection with a Neo4j instance.
// - The ability to perform a read query (database name is correctly defined).
// - Required plugin installed: APOC (specifically apoc.meta.schema as it's used for get-schema)
// - In case GDS is not installed a flag is set in the server and tools will be registered accordingly
func (s *Neo4jMCPServer) verifyRequirements() error {
err := s.dbService.VerifyConnectivity(context.Background())
if err != nil {
return fmt.Errorf("impossible to verify connectivity with the Neo4j instance: %w", err)
}
// Perform a dummy query to verify correctness of the connection, VerifyConnectivity is not exhaustive.
records, err := s.dbService.ExecuteReadQuery(context.Background(), "RETURN 1 as first", map[string]any{})

if err != nil {
return fmt.Errorf("impossible to verify connectivity with the Neo4j instance: %w", err)
}
if len(records) != 1 || len(records[0].Values) != 1 {
return fmt.Errorf("failed to verify connectivity with the Neo4j instance: unexpected response from test query")
}
one, ok := records[0].Values[0].(int64)
if !ok || one != 1 {
return fmt.Errorf("failed to verify connectivity with the Neo4j instance: unexpected response from test query")
}
// Check for apoc.meta.schema procedure
checkApocMetaSchemaQuery := "SHOW PROCEDURES YIELD name WHERE name = 'apoc.meta.schema' RETURN count(name) > 0 AS apocMetaSchemaAvailable"

// Check for apoc.meta.schema availability
records, err = s.dbService.ExecuteReadQuery(context.Background(), checkApocMetaSchemaQuery, nil)
if err != nil {
return fmt.Errorf("failed to check for APOC availability: %w", err)
}
if len(records) != 1 || len(records[0].Values) != 1 {
return fmt.Errorf("failed to verify APOC availability: unexpected response from test query")
}
apocMetaSchemaAvailable, ok := records[0].Values[0].(bool)
if !ok || !apocMetaSchemaAvailable {
return fmt.Errorf("please ensure the APOC plugin is installed and includes the 'meta' component")
}
// Call gds.version procedure to determine if GDS is installed
records, err = s.dbService.ExecuteReadQuery(context.Background(), "RETURN gds.version() as gdsVersion", nil)
if err != nil {
// GDS is optional, so we log a warning and continue, assuming it's not installed.
log.Print("Impossible to verify GDS installation.")
s.gdsInstalled = false
return nil
}
if len(records) == 1 && len(records[0].Values) == 1 {
_, ok := records[0].Values[0].(string)
if ok {
s.gdsInstalled = true
}
}

return nil
}

// Stop gracefully stops the server
func (s *Neo4jMCPServer) Stop() error {
log.Println("Stopping Neo4j MCP Server...")
Expand Down
Loading