Vercel AI SDK + Neo4j Integration
Overview
Vercel AI SDK is a TypeScript-first, provider-agnostic toolkit for building AI-powered applications and agents. It supports streaming, structured output, and multi-step agentic tool loops with a unified interface across OpenAI, Google Gemini, Anthropic, Mistral, and more.
Key Features:
-
generateText/streamTextfor one-shot and streaming LLM calls -
Multi-step agentic loops via
stopWhen: stepCountIs(N)(AI SDK v6+) -
tool()helper withjsonSchema()for type-safe tool definitions (no Zod required) -
Provider-agnostic — swap LLMs with a single environment variable change
-
MCP client support via
@ai-sdk/mcp
Official Resources:
-
Website: sdk.vercel.ai
-
Documentation: sdk.vercel.ai/docs
-
MCP client docs: sdk.vercel.ai/docs/ai-sdk-core/mcp-clients
Notebooks
| File | Description |
|---|---|
Observable notebook — interactive browser demo (direct queries, custom tools, memory agent) |
|
Jupyter setup notebook — Node.js install, credentials, MCP server start |
Extension Points
1. MCP Integration
The Vercel AI SDK supports MCP via the @ai-sdk/mcp package. The experimental_createMCPClient function connects to any MCP server over HTTP or SSE transport, and mcpClient.tools() returns a tools object ready for generateText.
npm install @ai-sdk/mcp
import { generateText, stepCountIs } from 'ai';
import { experimental_createMCPClient } from '@ai-sdk/mcp';
// Credentials passed per-request via Basic Auth header
const creds = Buffer.from(`${process.env.NEO4J_USERNAME}:${process.env.NEO4J_PASSWORD}`)
.toString('base64');
const mcpClient = await experimental_createMCPClient({
transport: {
type: 'http',
url: `http://localhost:${process.env.MCP_PORT}/mcp`,
headers: { Authorization: `Basic ${creds}` },
},
});
const mcpTools = await mcpClient.tools(); // get-schema, read-cypher, write-cypher, ...
const { text, steps } = await generateText({
model,
system: 'You are a graph database assistant. Run get-schema first if unfamiliar.',
prompt: 'How many organizations are in the database?',
tools: mcpTools,
stopWhen: stepCountIs(10), // AI SDK v6+ — replaces the removed maxSteps
});
await mcpClient.close();
Example output:
Connected to Neo4j MCP ✓
Available tools: get-schema, list-gds-procedures, read-cypher, write-cypher
Query: How many organizations are in the database?
Result: There are 46,088 organizations in the database.
[Completed in 3 step(s)]
When to use: Start here. Covers most graph queries with zero Cypher knowledge required — the agent uses get-schema + read-cypher autonomously.
2. Direct Neo4j Integration
For queries that need hand-tuned Cypher or access patterns the MCP server doesn’t expose, use the neo4j-driver directly. Custom tools are defined with tool() + jsonSchema() and can be merged with MCP tools in the same generateText call.
npm install neo4j-driver
import { generateText, tool, jsonSchema, stepCountIs } from 'ai';
import neo4j from 'neo4j-driver';
const driver = neo4j.driver(
process.env.NEO4J_URI,
neo4j.auth.basic(process.env.NEO4J_USERNAME, process.env.NEO4J_PASSWORD),
{ disableLosslessIntegers: true }
);
const getInvestments = tool({
description: 'Returns investments made by a company.',
inputSchema: jsonSchema({
type: 'object',
properties: {
company: { type: 'string', description: 'Company or organization name' },
},
required: ['company'],
}),
execute: async ({ company }) => {
const { records } = await driver.executeQuery(
`MATCH (o:Organization)-[:HAS_INVESTOR]->(i)
WHERE o.name = $company
RETURN i.id AS id, i.name AS name, head(labels(i)) AS type`,
{ company },
{ database: process.env.NEO4J_DATABASE }
);
return records.map(r => r.toObject());
},
});
// Merge custom tool with MCP tools — the framework routes each call automatically
const { text } = await generateText({
model,
prompt: 'Which companies did Google invest in?',
tools: { ...mcpTools, getInvestments },
stopWhen: stepCountIs(10),
});
await driver.close();
Example output:
Result: Google has made investments in several notable companies:
- Ionic Security
- Avere Systems
- FlexiDAO
- Cloudflare
- Trifacta
[Completed in 4 step(s)]
When to use: When you need precise Cypher beyond what the MCP server provides, or want to mix domain-specific tools (e.g. custom aggregations, write operations) with MCP tools in a single agent.
3. Custom Tools / Persistent Memory
Memory is stored directly in Neo4j using neo4j-driver — no additional packages needed. The pattern wraps generateText with two hooks:
-
Before hook (
injectMemoryContext) — queries recent messages from Neo4j and injects them into the system prompt -
After hook (
saveInteraction) — saves the interaction as(:MemoryMessage)nodes for future recall
Memory schema:
(:MemorySession {id})-[:HAS_MESSAGE]->(:MemoryMessage {role, content, timestamp})
import neo4j from 'neo4j-driver';
const memDriver = neo4j.driver(MEMORY_URI, neo4j.auth.basic(MEMORY_USER, MEMORY_PASS));
async function getRecentMessages(limit = 10) {
const { records } = await memDriver.executeQuery(
`MATCH (s:MemorySession {id: $sessionId})-[:HAS_MESSAGE]->(m:MemoryMessage)
RETURN m.role AS role, m.content AS content
ORDER BY m.timestamp ASC LIMIT $limit`,
{ sessionId: SESSION_ID, limit },
{ database: MEMORY_DB }
);
return records.map(r => `${r.get('role').toUpperCase()}: ${r.get('content')}`);
}
async function runWithMemory(query) {
// BEFORE: inject conversation history into system prompt
const history = await getRecentMessages();
const systemWithContext = history.length
? `${SYSTEM_PROMPT}\n\n--- CONVERSATION HISTORY ---\n${history.join('\n')}\n----------------------------`
: SYSTEM_PROMPT;
const { text } = await generateText({
model,
system: systemWithContext,
prompt: query,
tools: mcpTools,
stopWhen: stepCountIs(10),
});
// AFTER: save interaction to memory graph
await memDriver.executeQuery(
`MERGE (s:MemorySession {id: $sessionId})
CREATE (m:MemoryMessage {role: $role, content: $content, timestamp: datetime()})
CREATE (s)-[:HAS_MESSAGE]->(m)`,
{ sessionId: SESSION_ID, role: 'user', content: query },
{ database: MEMORY_DB }
);
return text;
}
Example output (two-turn demo):
[USER]: I am conducting a competitive analysis of 'Google'. I am specifically
worried about their subsidiaries and top-tier competitors in the AI space.
[AGENT]: Understood. I've noted that we're tracking Google for competitive intelligence...
[Hook] Saving interaction to Neo4j memory graph...
--- Indexing memory (5s)... ---
[USER]: What are the main risks in the supply chain for the company I am currently tracking?
↳ Injecting 1 memories into context.
Memory 1: Conducting competitive analysis of Google — focused on subsidiaries and AI competitors...
[AGENT]: Based on our ongoing analysis of Google, the main supply chain risks include...
When to use: Multi-session agents that need to remember past queries, user context, or analysis state. Particularly useful for research assistants and monitoring agents.
MCP Authentication
Supported Mechanisms:
✅ HTTP Headers (HTTP transport) — Pass credentials via the headers parameter of experimental_createMCPClient. Used to authenticate per-request against neo4j-mcp-server running in HTTP mode.
const creds = Buffer.from(`${NEO4J_USERNAME}:${NEO4J_PASSWORD}`).toString('base64');
const mcpClient = await experimental_createMCPClient({
transport: {
type: 'http',
url: 'http://localhost:8443/mcp',
headers: { Authorization: `Basic ${creds}` },
},
});
|
Important: Do not export |
LLM Provider Configuration
All three agent files import getModel() from providers.mjs, which selects the LLM based on the AI_PROVIDER environment variable. No code changes are needed to switch providers.
| Provider | AI_PROVIDER |
API Key Variable | Extra install |
|---|---|---|---|
OpenAI (default) |
|
|
— |
Google Gemini |
|
|
|
Anthropic Claude |
|
|
|
Mistral |
|
|
|
Challenges and Gaps
| Area | Detail |
|---|---|
JavaScript only |
The Vercel AI SDK has no Python support — all agent code runs in Node.js |
`stopWhen` is v6+ |
|
MCP transport type |
|
Memory DB |
Memory uses |
Edge runtime |
Neo4j driver needs persistent TCP — incompatible with Vercel edge functions; use Node.js serverless runtime |
`experimental_createMCPClient` |
Still experimental; API may change in future SDK versions |