Skip to main content
Glama
Akakinad

GraphRAG TypeScript MCP Tools

by Akakinad
README.md
# 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

C2.9/5.0

Scored across 4 tools

Disambiguation2/5

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.

Naming Consistency2/5

Mixes camelCase (getMoviesByGenre, explainMovieData, graphStatistics) and snake_case (browse_movies_by_genre) with different verb conventions ('get' vs 'browse'), showing inconsistency.

Tool Count3/5

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.

Completeness2/5

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.

Maintenance

ActivityMaintained
ResponsivenessNo issues