GraphRAG TypeScript MCP Tools
# GraphRAG TypeScript MCP Tools
A complete implementation of a GraphRAG MCP server built with TypeScript, Neo4j, and the MCP TypeScript SDK. This project demonstrates how to build production-quality MCP servers that expose graph-backed tools, resources, and advanced features like LLM sampling and completions.
Built as part of the [Neo4j GraphAcademy — Building GraphRAG TypeScript MCP tools](https://graphacademy.neo4j.com/courses/genai-mcp-build-custom-tools-typescript) course.
---
## What is MCP?
The Model Context Protocol (MCP) is an open standard by Anthropic that allows AI agents (Claude, Cursor, VS Code Copilot) to connect to external tools and data sources in a standardized way.
---
## Project Structure
genai-mcp-build-custom-tools-typescript/
├── server/
│ └── index.ts ← Main MCP server: 4 tools + 1 resource + sampling + completions
├── strawberry/
│ └── index.ts ← First MCP server: simple countLetters tool
├── solutions/ ← Course reference solutions
├── .vscode/
│ └── mcp.json ← VS Code MCP configuration
└── README.md
---
## What Was Built
### Step 1 — First MCP Server (`strawberry/index.ts`)
The simplest possible MCP server. One tool, no database, stdio transport.
```typescript
server.registerTool("countLetters", {
description: "Count occurrences of a letter in the text",
inputSchema: {
text: z.string().describe("The text to search in"),
search: z.string().describe("The letter to count"),
},
}, async ({ text, search }) => ({
content: [{
type: "text",
text: String(text.toLowerCase().split(search.toLowerCase()).length - 1),
}],
}));
```
**Test result:** `countLetters("strawberry", "r")` → `3`
Tested using the **MCP Inspector** — a browser-based tool for exploring and testing MCP servers.
---
### Step 2 — Neo4j Connection (Module Scope)
Unlike Python's lifespan context manager, TypeScript uses **module-scope variables** — the driver is created once at the top of the file and shared by all tools directly.
```typescript
// Created ONCE when file loads — shared by all tools
const driver: Driver = neo4j.driver(
process.env["NEO4J_URI"] ?? "neo4j://localhost:7687",
neo4j.auth.basic(
process.env["NEO4J_USERNAME"] ?? "neo4j",
process.env["NEO4J_PASSWORD"] ?? "password"
)
);
const database = process.env["NEO4J_DATABASE"] ?? "neo4j";
```
Graceful shutdown via SIGINT:
```typescript
process.on("SIGINT", async () => {
await driver.close();
await server.close();
process.exit(0);
});
```
---
### Step 3 — Tool 1: `graphStatistics`
Counts all nodes and relationships in Neo4j.
**Result:** `{"nodes": 28863, "relationships": 332522}`
---
### Step 4 — Tool 2: `getMoviesByGenre`
Searches movies by genre ordered by IMDB rating. Uses `console.error()` for logging — never `console.log()` in stdio servers (it corrupts the JSON-RPC channel).
```typescript
server.registerTool("getMoviesByGenre", {
description: "Get movies by genre from the Neo4j database",
inputSchema: {
genre: z.string().describe("The genre to search for (e.g., Action, Comedy, Drama)"),
limit: z.number().default(10).describe("Maximum number of movies to return"),
},
}, async ({ genre, limit }) => {
const { records } = await driver.executeQuery(query,
{ genre, limit: neo4j.int(limit) }, // neo4j.int() for 64-bit integer compatibility
{ database }
);
...
});
```
---
### Step 5 — Tool 3: `browse_movies_by_genre` (Paginated)
Cursor-based pagination using Neo4j's `SKIP` and `LIMIT`:
```typescript
const skip = parseInt(cursor, 10) || 0;
// Cypher: SKIP $skip LIMIT $limit
const nextCursor = movies.length === pageSize ? String(skip + pageSize) : null;
```
**Returns:**
```json
{
"genre": "Action",
"movies": [...],
"nextCursor": "2",
"page": 1,
"pageSize": 2,
"hasMore": true,
"count": 2
}
```
---
### Step 6 — Resource: `movie://{tmdbId}`
Exposes full movie details by TMDB ID using `ResourceTemplate`:
```typescript
server.registerResource(
"movie",
new ResourceTemplate("movie://{tmdbId}", { list: undefined }),
{ description: "Get detailed information about a specific movie", mimeType: "application/json" },
async (uri, { tmdbId }) => {
// uri.href = "movie://603"
// returns: contents array with JSON movie data
}
);
```
**Examples:** `movie://603` (The Matrix), `movie://13` (Forrest Gump)
---
### Step 7 — Advanced: Sampling (`explainMovieData`)
Tools that call the LLM during execution to convert raw Neo4j data into natural language:
```typescript
const result = await server.server.createMessage({
messages: [{
role: "user",
content: {
type: "text",
text: `Describe '${movieData.title}' (${movieData.released})...`,
},
}],
maxTokens: 200,
});
```
**Without sampling:** `{'title': 'Toy Story', 'released': '1995', 'actors': [...]}`
**With sampling (VS Code Copilot):** *"Toy Story — A clever, funny animated adventure about Woody, a jealous cowboy doll who feels displaced when Buzz Lightyear becomes the new favorite..."*
> Note: Requires setting capability on the low-level server:
> ```typescript
> server.server["_capabilities"] = { ...server.server["_capabilities"], completions: {} };
> ```
---
### Step 8 — Advanced: Completions
Real-time autocomplete suggestions for genre parameters — queries Neo4j as the user types:
```typescript
import { CompleteRequestSchema } from "@modelcontextprotocol/sdk/types.js";
server.server.setRequestHandler(CompleteRequestSchema, async (request) => {
if (request.params.argument.name === "genre") {
const { records } = await driver.executeQuery(
`MATCH (g:Genre)
WHERE g.name STARTS WITH $prefix
RETURN g.name AS name
ORDER BY name ASC LIMIT 10`,
{ prefix: request.params.argument.value },
{ database }
);
return { completion: { values: records.map(r => r.get("name")) } };
}
return { completion: { values: [] } };
});
```
---
## Key Differences from Python Version
| Concept | Python (FastMCP) | TypeScript (McpServer) |
|---|---|---|
| Tool registration | `@mcp.tool()` decorator | `server.registerTool()` method |
| Shared state | Lifespan context manager | Module-scope variables |
| Driver access | `ctx.request_context.lifespan_context.driver` | `driver` (direct) |
| Logging | `await ctx.info()` | `console.error()` |
| Sampling | `ctx.session.create_message()` | `server.server.createMessage()` |
| Completions | `@server.completion()` | `server.server.setRequestHandler(CompleteRequestSchema)` |
| File structure | Separate files per feature | Everything in one `index.ts` |
| Number params | Python int type hints | `neo4j.int()` wrapper needed |
| Prompt params | `int`, `str`, `float` | Always `z.string()`, parse manually |
---
## Setup
### Prerequisites
- Node.js 20+
- npm
- Neo4j Sandbox — Recommendations dataset from [sandbox.neo4j.com](https://sandbox.neo4j.com)
### Install
```bash
git clone https://github.com/Akakinad/genai-mcp-build-custom-tools-typescript
cd genai-mcp-build-custom-tools-typescript
npm install
```
### Configure credentials
```bash
cat > server/.env << EOF
NEO4J_URI=bolt://your-sandbox-ip:7687
NEO4J_USERNAME=neo4j
NEO4J_PASSWORD=your-password
NEO4J_DATABASE=neo4j
EOF
```
### Verify setup
```bash
npx tsx client/test_environment.ts
# Expected: All checks passed!
```
---
## Running
### Test with MCP Inspector (browser UI)
```bash
cd server
npx @modelcontextprotocol/inspector npx tsx index.ts
```
Open the URL shown in terminal → Connect → Tools tab → List Tools → select a tool → Run Tool.
### Run server for AI editor use
```bash
cd server
npx tsx index.ts
```
---
## VS Code Configuration (`.vscode/mcp.json`)
```json
{
"servers": {
"movies-ts": {
"type": "stdio",
"command": "npx",
"args": ["tsx", "/absolute/path/to/server/index.ts"]
}
}
}
```
### Test in VS Code Copilot
Explain the movie "Toy Story" using the movies-ts MCP tool
Search for Action movies using the movies-ts MCP tool
Get graph statistics using the movies-ts MCP tool
---
## Course
**Learning Path:** [Generative AI & GraphRAG](https://graphacademy.neo4j.com/categories/generative-ai)
**Course:** [Building GraphRAG TypeScript MCP tools](https://graphacademy.neo4j.com/courses/genai-mcp-build-custom-tools-typescript)
---
# Building GraphRAG TypeScript MCP Tools
Companion repository for the GraphAcademy course [Building GraphRAG TypeScript MCP Tools](https://graphacademy.neo4j.com/courses/genai-mcp-build-custom-tools-typescript/).
Students build an MCP (Model Context Protocol) server that connects to a Neo4j graph database, exposing tools and resources for use with AI assistants.
## Getting Started
1. Copy `.env.example` to `.env` and update the values with your Neo4j connection details.
2. Install dependencies:
```sh
npm install
```
3. Start the server:
```sh
npm start
```
4. Inspect the server with the MCP Inspector:
```sh
npm run inspect
```
## Solutions
The `solutions/` directory contains the completed code for each lesson checkpoint.
TDQS
Scored across 4 tools
The two tools for movies by genre (getMoviesByGenre and browse_movies_by_genre) have overlapping purposes; the only distinction is pagination support, which may cause an agent to misselect. Other tools are distinct.
Mixes camelCase (getMoviesByGenre, explainMovieData, graphStatistics) and snake_case (browse_movies_by_genre) with different verb conventions ('get' vs 'browse'), showing inconsistency.
With only 4 tools, the surface is minimal but perhaps appropriate for a read-only movie graph query server. It borders on being too few but is not extreme.
Missing basic operations like fetching a specific movie by ID, listing all genres, or querying actors/relationships. The tool set covers only a small subset of expected graph queries, leaving significant gaps.