Neo4j’s GraphAcademy course “Developing with Neo4j MCP Tools” is excellent, but it assumes you’ll run everything inside a GitHub Codespace. Codespaces have a monthly free limit, and once you hit it you’re stuck — unless you self-host.
Good news: you don’t need Codespaces at all. The MCP server is just a small program, and you can run the whole stack locally with Docker in about half an hour. This guide walks through exactly that, and along the way you’ll pick up how MCP actually fits together.
Everything below has been tested end-to-end.
VS Code (host) ──HTTP /mcp + auth──► neo4j-mcp (server) ──Bolt──► Neo4j (database)
Three separate pieces — and understanding the split is half the battle:
A common misconception: there is no remote “MCP database” somewhere in the cloud. The MCP server is a program you run. It connects out to whatever database you point it at.
That’s it. No local Python, no Neo4j install — Docker handles both.
Make a project folder and drop in a docker-compose.yml:
services: neo4j: image: neo4j:5.26 container_name: neo4j ports: - "7474:7474" # Neo4j Browser -> http://localhost:7474 - "7687:7687" # Bolt environment: NEO4J_AUTH: "neo4j/password123" NEO4J_PLUGINS: '["apoc","graph-data-science"]' # needed for get-schema + GDS tool volumes: - neo4j_data:/data healthcheck: test: ["CMD-SHELL", "cypher-shell -u neo4j -p password123 'RETURN 1' || exit 1"] interval: 10s timeout: 10s retries: 12 start_period: 30s
neo4j-mcp: build: context: "https://github.com/neo4j/mcp.git#v1.5.2" # builds the official server from source container_name: neo4j-mcp depends_on: neo4j: condition: service_healthy ports: - "8000:8000" # MCP endpoint -> http://localhost:8000/mcp environment: NEO4J_URI: "bolt://neo4j:7687" NEO4J_DATABASE: "neo4j" NEO4J_TRANSPORT_MODE: "http" # networked service, not a stdio subprocess NEO4J_MCP_HTTP_HOST: "0.0.0.0" # bind all interfaces so the host can reach it NEO4J_MCP_HTTP_PORT: "8000" # must be >1024 (container runs as non-root)
volumes: neo4j_data:
Two things worth knowing here:
docker compose up -d --build
First run pulls the Neo4j image, builds the Go MCP server, and downloads the APOC + GDS plugins. Give it a minute, then check both containers are up:
docker compose ps
Wait until neo4j shows (healthy).
The MCP endpoint is http://localhost:8000/mcp. Don't open it in a browser — you'll get Method Not Allowed: only POST is supported, which is actually a good sign (the server is up; browsers just send GET).
Test it properly with curl. First, confirm auth is enforced:
curl -i -X POST http://localhost:8000/mcp # expect 401 Unauthorized
Now list the tools, authenticating with the database credentials (neo4j:password123):
curl -s -u neo4j:password123 -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"tools/list","id":1}' \ http://localhost:8000/mcp
You should see: get-schema, read-cypher, write-cypher.
The course lists four tools — the fourth is list-gds-procedures. If it's missing, don't panic. That tool is registered *lazily during the MCP *initialize handshake, and only if Graph Data Science is installed. A bare tools/list curl skips initialize, so it never appears that way.
Send an initialize request first, then list again:
curl -s -u neo4j:password123 -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"1"}},"id":1}' \ http://localhost:8000/mcp
curl -s -u neo4j:password123 -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"tools/list","id":2}' \ http://localhost:8000/mcp
Now all four appear. (Real MCP clients like VS Code do the initialize step automatically — this only bites you with raw curl.)
In your project, create .vscode/mcp.json:
{ "servers": { "neo4j": { "type": "http", "url": "http://localhost:8000/mcp", "headers": { "Authorization": "Basic bmVvNGo6cGFzc3dvcmQxMjM=" } } }}
That Authorization value is just base64 of neo4j:password123. If you change the password, regenerate it:
printf 'neo4j:yourpassword' | base64
⚠️Most common mistake:using the course’sstdioconfig ("type": "stdio", "command": "neo4j-mcp") with this Docker setup. That tells VS Code to launch a local binary you don't have. For the containerized server you must use thehttpconfig above.
Then in VS Code: Cmd/Ctrl + Shift + P → MCP: List Servers → select neo4j → Start Server. Open Chat in Agent mode (Cmd/Ctrl + Shift + I) and ask:
Which MCP tools are available? List their ID and description.
You’ll get all four tools back.
Your local database is empty, so let’s point a second, throwaway MCP container at Neo4j’s public movies demo database — the same dataset the course uses — on a different port. This leaves your main stack untouched:
docker run --rm -d --name mcp-movies \ -e NEO4J_URI="neo4j+s://demo.neo4jlabs.com" \ -e NEO4J_DATABASE="recommendations" \ -e NEO4J_TRANSPORT_MODE="http" \ -e NEO4J_MCP_HTTP_HOST="0.0.0.0" \ -e NEO4J_MCP_HTTP_PORT="8001" \ -p 8001:8001 \ <your-project-name>-neo4j-mcp
(The image name is whatever docker compose built — check docker images; it looks like yourfolder-neo4j-mcp.)
Read the schema — what relationships does a User have?
curl -s -u recommendations:recommendations -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"read-cypher","arguments":{"query":"MATCH (u:User)-[r]->() RETURN DISTINCT type(r) AS rel"}},"id":2}' \ http://localhost:8001/mcp
Result: RATED — i.e. (:User)-[:RATED]->(:Movie).
Generate a query — the highest-rated movie (note the non-null guard, since not every movie has a rating):
curl -s -u recommendations:recommendations -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"read-cypher","arguments":{"query":"MATCH (m:Movie) WHERE m.imdbRating IS NOT NULL RETURN m.title AS title, m.imdbRating AS rating ORDER BY m.imdbRating DESC LIMIT 1"}},"id":3}' \ http://localhost:8001/mcp
When you’re done with the demo:
docker stop mcp-movies
The course’s capstone is “vibe coding” a Python CLI that returns the top 5 movies in a genre. Here’s a clean, dependency-light version. The genre is passed as a query parameter (never string-concatenated) and the query filters out null ratings:
import os, sysfrom neo4j import GraphDatabase
php
QUERY = """MATCH (m:Movie)-[:IN_GENRE]->(:Genre {name: $genre})WHERE m.imdbRating IS NOT NULLRETURN m.title AS title, m.imdbRating AS ratingORDER BY m.imdbRating DESCLIMIT 5"""
python
def main(): uri = os.environ["NEO4J_URI"] user = os.environ["NEO4J_USERNAME"] pwd = os.environ["NEO4J_PASSWORD"] db = os.environ.get("NEO4J_DATABASE", "neo4j")
genre = input("Enter a movie genre: ").strip() with GraphDatabase.driver(uri, auth=(user, pwd)) as driver: driver.verify_connectivity() records, _, _ = driver.execute_query(QUERY, genre=genre, database_=db)
print(f"\nTop {len(records)} '{genre}' movies:") for i, r in enumerate(records, 1): print(f" {i}. {r['title']} ({r['rating']})")
if __name__ == "__main__": main()
Run it against the demo database:
pip install neo4jexport NEO4J_URI="neo4j+s://demo.neo4jlabs.com"export NEO4J_USERNAME="recommendations"export NEO4J_PASSWORD="recommendations"export NEO4J_DATABASE="recommendations"python3 movie_recommender.py # then type: Mystery
In about 30 minutes you’ve stood up the full Neo4j MCP stack locally — no Codespaces, no cloud bill:
It’s tempting to see this as just “avoiding a paywall,” but the self-hosted setup teaches something more useful. MCP is quietly becoming the standard way AI agents reach real systems — databases, APIs, internal tools — and the interesting engineering questions live exactly where we ended up: How is the server transported? How are requests authenticated? What stops an agent from running a destructive query?
Running the server yourself over HTTP, with per-request auth and a read-only switch, surfaces those questions in a way a pre-baked cloud sandbox hides. As more teams put agents in front of production data, the ability to run an MCP server as a controlled, observable service — rather than a mystery box someone else hosts — stops being a nice-to-have. The 30 minutes you spent here map directly onto how you’d deploy the same server behind a gateway with TLS and a secrets manager for real.
SymptomCause / FixMethod Not Allowed in browserYou sent a GET. The endpoint is POST-only — use curl or a client.401 UnauthorizedMissing/wrong Basic Auth. Use the DB username:password.Only 3 tools listedSend an initialize request first; ensure APOC + GDS are installed.get-schema errorsAPOC not installed — add it to NEO4J_PLUGINS.MCP server won't start, complains about username/passwordYou set NEO4J_USERNAME/PASSWORD in HTTP mode. Remove them.VS Code says server "requires interaction to start"Your mcp.json is using the stdio config. Switch to the http config.Permission denied binding portKeep NEO4J_MCP_HTTP_PORT above 1024 (container is non-root).
Happy graphing.
Run the Neo4j MCP Server Locally with Docker (No Codespaces Needed) was originally published in Towards AI on Medium, where people are continuing the conversation by highlighting and responding to this story.