# Directory Structure
```
├── .gitignore
├── dist
│ └── index.js
├── index.ts
├── package.json
├── pnpm-lock.yaml
├── prompts.yml
├── README.md
└── tsconfig.json
```
# Files
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
```
1 | node_modules
2 | test_db
```
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
```markdown
1 | # LanceDB Node.js Vector Search
2 |
3 | A Node.js implementation for vector search using LanceDB and Ollama's embedding model.
4 |
5 | ## Overview
6 |
7 | This project demonstrates how to:
8 | - Connect to a LanceDB database
9 | - Create custom embedding functions using Ollama
10 | - Perform vector similarity search against stored documents
11 | - Process and display search results
12 |
13 | ## Prerequisites
14 |
15 | - Node.js (v14 or later)
16 | - Ollama running locally with the `nomic-embed-text` model
17 | - LanceDB storage location with read/write permissions
18 |
19 | ## Installation
20 |
21 | 1. Clone the repository
22 | 2. Install dependencies:
23 |
24 | ```bash
25 | pnpm install
26 | ```
27 |
28 | ## Dependencies
29 |
30 | - `@lancedb/lancedb`: LanceDB client for Node.js
31 | - `apache-arrow`: For handling columnar data
32 | - `node-fetch`: For making API calls to Ollama
33 |
34 | ## Usage
35 |
36 | Run the vector search test script:
37 |
38 | ```bash
39 | pnpm test-vector-search
40 | ```
41 |
42 | Or directly execute:
43 |
44 | ```bash
45 | node test-vector-search.js
46 | ```
47 |
48 | ## Configuration
49 |
50 | The script connects to:
51 | - LanceDB at the configured path
52 | - Ollama API at `http://localhost:11434/api/embeddings`
53 |
54 | ## MCP Configuration
55 |
56 | To integrate with Claude Desktop as an MCP service, add the following to your MCP configuration JSON:
57 |
58 | ```json
59 | {
60 | "mcpServers": {
61 | "lanceDB": {
62 | "command": "node",
63 | "args": [
64 | "/path/to/lancedb-node/dist/index.js",
65 | "--db-path",
66 | "/path/to/your/lancedb/storage"
67 | ]
68 | }
69 | }
70 | }
71 | ```
72 |
73 | Replace the paths with your actual installation paths:
74 | - `/path/to/lancedb-node/dist/index.js` - Path to the compiled index.js file
75 | - `/path/to/your/lancedb/storage` - Path to your LanceDB storage directory
76 |
77 | ## Custom Embedding Function
78 |
79 | The project includes a custom `OllamaEmbeddingFunction` that:
80 | - Sends text to the Ollama API
81 | - Receives embeddings with 768 dimensions
82 | - Formats them for use with LanceDB
83 |
84 | ## Vector Search Example
85 |
86 | The example searches for "how to define success criteria" in the "ai-rag" table, displaying results with their similarity scores.
87 |
88 | ## License
89 |
90 | [MIT License](LICENSE)
91 |
92 | ## Contributing
93 |
94 | Contributions are welcome! Please feel free to submit a Pull Request.
```
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "strict": true,
5 | "esModuleInterop": true,
6 | "skipLibCheck": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "resolveJsonModule": true,
9 | "outDir": "./dist",
10 | "rootDir": ".",
11 | "moduleResolution": "NodeNext",
12 | "module": "NodeNext"
13 | },
14 | "include": [
15 | "./**/*.ts"
16 | ],
17 | "exclude": [
18 | "node_modules"
19 | ]
20 | }
```
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
```json
1 | {
2 | "name": "@modelcontextprotocol/server-lancedb",
3 | "version": "0.1.0",
4 | "description": "MCP server for vector search using LanceDB",
5 | "license": "MIT",
6 | "author": "Vurtnec",
7 | "type": "module",
8 | "bin": {
9 | "mcp-server-lancedb": "dist/index.js"
10 | },
11 | "files": [
12 | "dist"
13 | ],
14 | "scripts": {
15 | "build": "tsc && shx chmod +x dist/*.js",
16 | "prepare": "npm run build",
17 | "watch": "tsc --watch"
18 | },
19 | "dependencies": {
20 | "@lancedb/lancedb": "^0.17.0",
21 | "@modelcontextprotocol/sdk": "0.5.0",
22 | "apache-arrow": "^15.0.2",
23 | "node-fetch": "^3.3.2",
24 | "zod": "^3.24.1",
25 | "zod-to-json-schema": "^3.24.1"
26 | },
27 | "devDependencies": {
28 | "@types/node": "^22",
29 | "shx": "^0.3.4",
30 | "typescript": "^5.3.3"
31 | }
32 | }
```
--------------------------------------------------------------------------------
/prompts.yml:
--------------------------------------------------------------------------------
```yaml
1 | rag_prompt: |
2 | You are an intelligent AI assistant with access to a vector database through the vector_search tool. Your primary goal is to provide accurate answers by strategically using ONLY the vector_search tool to retrieve information. Other tools are not available to you.
3 |
4 | 1. Advanced Search Strategy:
5 | - First, thoroughly analyze the user's query to identify key concepts, entities, and information needs
6 | - Break down complex queries into multiple specific sub-queries to capture different aspects
7 | - Conduct your queries in English to access information
8 | - Execute multiple vector_search operations in parallel for these sub-queries
9 | - After initial retrieval, identify knowledge gaps and perform deep search by:
10 | a) Extracting new search terms from initial results
11 | b) Exploring related concepts mentioned in retrieved documents
12 | c) Investigating contradictions or uncertainties in the initial results
13 | - Continue this deep search process until you have comprehensive information or reach MAX_SEARCH_ATTEMPTS (10)
14 |
15 | 2. Retrieval Parameters:
16 | - ONLY use table_name: "<your_table_name>"
17 | - Default limit: 10 (adjust as needed for specific queries)
18 | - Vary query_text formulations to capture different semantic aspects of the user's question
19 |
20 | 3. Response Construction:
21 | - Rely exclusively on information from vector_search retrieval
22 | - Synthesize information from all search operations into a coherent, comprehensive answer
23 | - Clearly distinguish between retrieved information and any necessary explanations
24 | - Always include the source's full URL in your response for verification
25 |
26 | 4. Quality Control:
27 | - Evaluate if the combined retrieved information adequately answers the user's question
28 | - If information is insufficient after MAX_SEARCH_ATTEMPTS, acknowledge limitations
29 | - Never hallucinate information when retrieval results are inadequate
30 |
31 | 5. Restriction:
32 | - You are ONLY authorized to use the vector_search tool
33 | - Do NOT attempt to use any other tools under any circumstances
34 | - If a query cannot be answered using vector_search alone, explain this limitation politely
35 |
36 | Remember: Your responses must be exclusively based on information retrieved through the vector_search tool. No other tools are available or permitted.
```
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
```typescript
1 | #!/usr/bin/env node
2 |
3 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5 | import {
6 | CallToolRequestSchema,
7 | ListToolsRequestSchema,
8 | ToolSchema,
9 | } from "@modelcontextprotocol/sdk/types.js";
10 | import { z } from "zod";
11 | import { zodToJsonSchema } from "zod-to-json-schema";
12 | import * as lancedb from "@lancedb/lancedb";
13 | import fetch from "node-fetch";
14 |
15 | // Parse command line arguments
16 | const args = process.argv.slice(2);
17 | let dbPath: string | undefined;
18 | let ollamaEndpoint: string = "http://localhost:11434/api/embeddings";
19 | let ollamaModel: string = "nomic-embed-text:latest";
20 | let showHelp: boolean = false;
21 |
22 | for (let i = 0; i < args.length; i++) {
23 | switch (args[i]) {
24 | case '--db-path':
25 | dbPath = args[++i];
26 | break;
27 | case '--ollama-endpoint':
28 | ollamaEndpoint = args[++i];
29 | break;
30 | case '--ollama-model':
31 | ollamaModel = args[++i];
32 | break;
33 | case '--help':
34 | case '-h':
35 | showHelp = true;
36 | break;
37 | }
38 | }
39 |
40 | // Show help message if requested
41 | if (showHelp) {
42 | console.error('Usage: mcp-server-lancedb --db-path <path> [--ollama-endpoint <url>] [--ollama-model <model>]');
43 | console.error('');
44 | console.error('Options:');
45 | console.error(' --db-path <path> Path to the LanceDB database (required)');
46 | console.error(' --ollama-endpoint <url> URL of the Ollama API embeddings endpoint (default: http://localhost:11434/api/embeddings)');
47 | console.error(' --ollama-model <model> Ollama model to use for embeddings (default: nomic-embed-text:latest)');
48 | console.error(' --help, -h Show this help message');
49 | process.exit(0);
50 | }
51 |
52 | // Validate required command line arguments
53 | if (!dbPath) {
54 | console.error('Error: Missing required arguments');
55 | console.error('Usage: mcp-server-lancedb --db-path <path> [--ollama-endpoint <url>] [--ollama-model <model>]');
56 | process.exit(1);
57 | }
58 |
59 | // Ollama API function for calling embeddings
60 | async function generateEmbedding(text: string): Promise<number[]> {
61 | try {
62 | const response = await fetch(ollamaEndpoint, {
63 | method: 'POST',
64 | headers: {
65 | 'Content-Type': 'application/json',
66 | },
67 | body: JSON.stringify({
68 | model: ollamaModel,
69 | prompt: text
70 | }),
71 | });
72 |
73 | if (!response.ok) {
74 | throw new Error(`Ollama API error: ${response.status} ${response.statusText}`);
75 | }
76 |
77 | const data = await response.json() as { embedding: number[] };
78 | return data.embedding;
79 | } catch (error) {
80 | console.error(`Error generating embedding: ${(error as Error).message}`);
81 | throw error;
82 | }
83 | }
84 |
85 | // Schema definitions
86 | const VectorAddArgsSchema = z.object({
87 | table_name: z.string().describe('Name of the table to add vectors to'),
88 | vectors: z.array(z.object({
89 | vector: z.array(z.number()).describe('Vector data'),
90 | }).passthrough()).describe('Array of vectors with metadata to add')
91 | });
92 |
93 | const VectorSearchArgsSchema = z.object({
94 | table_name: z.string().describe('Name of the table to search in'),
95 | query_vector: z.array(z.number()).optional().describe('Query vector for similarity search'),
96 | query_text: z.string().optional().describe('Text query to be converted to a vector using Ollama embedding'),
97 | limit: z.number().optional().describe('Maximum number of results to return (default: 10)'),
98 | distance_type: z.enum(['l2', 'cosine', 'dot']).optional().describe('Distance metric to use (default: cosine)'),
99 | where: z.string().optional().describe('Filter condition in SQL syntax'),
100 | with_vectors: z.boolean().optional().describe('Whether to include vector data in results (default: false)')
101 | }).refine((data) => {
102 | return data.query_vector !== undefined || data.query_text !== undefined;
103 | }, {
104 | message: "Either query_vector or query_text must be provided"
105 | });
106 |
107 | const ListTablesArgsSchema = z.object({});
108 |
109 | const ToolInputSchema = ToolSchema.shape.inputSchema;
110 | type ToolInput = z.infer<typeof ToolInputSchema>;
111 |
112 | // Connect to the LanceDB database
113 | let db: lancedb.Connection;
114 |
115 | async function connectDB() {
116 | try {
117 | db = await lancedb.connect(dbPath as string);
118 | console.error(`Connected to LanceDB at: ${dbPath}`);
119 | } catch (error) {
120 | console.error(`Failed to connect to LanceDB: ${error}`);
121 | process.exit(1);
122 | }
123 | }
124 |
125 | // Server setup
126 | const server = new Server(
127 | {
128 | name: "lancedb-vector-server",
129 | version: "0.1.0",
130 | },
131 | {
132 | capabilities: {
133 | tools: {},
134 | },
135 | },
136 | );
137 |
138 | // Tool implementations
139 |
140 | // Tool handlers
141 | server.setRequestHandler(ListToolsRequestSchema, async () => {
142 | return {
143 | tools: [
144 | {
145 | name: "vector_add",
146 | description: "Add vectors with metadata to a LanceDB table. Creates the table if it doesn't exist.",
147 | inputSchema: zodToJsonSchema(VectorAddArgsSchema) as ToolInput,
148 | },
149 | {
150 | name: "vector_search",
151 | description: "Search for similar vectors in a LanceDB table using either a direct vector or text that will be converted to a vector using Ollama embedding.",
152 | inputSchema: zodToJsonSchema(VectorSearchArgsSchema) as ToolInput,
153 | },
154 | {
155 | name: "list_tables",
156 | description: "List all tables in the LanceDB database.",
157 | inputSchema: zodToJsonSchema(ListTablesArgsSchema) as ToolInput,
158 | }
159 | ]
160 | };
161 | });
162 |
163 | server.setRequestHandler(CallToolRequestSchema, async (request) => {
164 | try {
165 | const { name, arguments: args } = request.params;
166 |
167 | switch (name) {
168 | case "vector_add": {
169 | const parsed = VectorAddArgsSchema.safeParse(args);
170 | if (!parsed.success) {
171 | throw new Error(`Invalid arguments for vector_add: ${parsed.error}`);
172 | }
173 |
174 | const { table_name, vectors } = parsed.data;
175 |
176 | // Check if table exists, if not create it
177 | try {
178 | const table = await db.openTable(table_name);
179 | await table.add(vectors);
180 | return {
181 | content: [{ type: "text", text: `Added ${vectors.length} vectors to table ${table_name}` }],
182 | isError: false,
183 | };
184 | } catch (error) {
185 | // Table doesn't exist, create it
186 | if ((error as Error).message.includes("does not exist")) {
187 | const table = await db.createTable(table_name, vectors);
188 | return {
189 | content: [{ type: "text", text: `Created table ${table_name} and added ${vectors.length} vectors` }],
190 | isError: false,
191 | };
192 | } else {
193 | throw error;
194 | }
195 | }
196 | }
197 |
198 | case "vector_search": {
199 | const parsed = VectorSearchArgsSchema.safeParse(args);
200 | if (!parsed.success) {
201 | throw new Error(`Invalid arguments for vector_search: ${parsed.error}`);
202 | }
203 |
204 | const {
205 | table_name,
206 | query_vector,
207 | query_text,
208 | limit = 10,
209 | distance_type = 'cosine',
210 | where,
211 | with_vectors = false
212 | } = parsed.data;
213 |
214 | try {
215 | const table = await db.openTable(table_name);
216 |
217 | // Determine which query vector to use
218 | let searchVector: number[];
219 |
220 | if (query_vector) {
221 | // Directly use the provided vector
222 | searchVector = query_vector;
223 | } else if (query_text) {
224 | // Generate embedding vector for text using Ollama
225 | console.error(`Generating embedding for text: "${query_text}"`);
226 | searchVector = await generateEmbedding(query_text);
227 | console.error(`Generated embedding with dimension: ${searchVector.length}`);
228 | } else {
229 | throw new Error('Either query_vector or query_text must be provided');
230 | }
231 |
232 | // Check table structure and vector dimensions
233 | const schema = await table.schema();
234 | const vectorField = schema.fields.find(field =>
235 | field.type.typeId === 16 && field.type.listSize > 10
236 | );
237 |
238 | if (!vectorField) {
239 | throw new Error('No vector column found in the table');
240 | }
241 |
242 | const vectorColumnName = vectorField.name;
243 | const vectorDimension = vectorField.type.listSize;
244 |
245 | if (searchVector.length !== vectorDimension) {
246 | console.error(`Warning: Query vector dimension (${searchVector.length}) doesn't match table vector dimension (${vectorDimension})`);
247 | }
248 |
249 | // Execute vector search
250 | let query = table.vectorSearch(searchVector);
251 |
252 | // Set distance type and limit
253 | query = query.distanceType(distance_type).limit(limit);
254 |
255 | if (where) {
256 | query = query.where(where);
257 | }
258 |
259 | const originalResults = await query.toArray();
260 |
261 | // Create new result objects instead of modifying originals
262 | let processedResults;
263 | if (!with_vectors) {
264 | processedResults = originalResults.map(item => {
265 | // Create a new object excluding the vector property
266 | const itemCopy = { ...item };
267 | delete itemCopy[vectorColumnName];
268 | return itemCopy;
269 | });
270 | } else {
271 | processedResults = originalResults;
272 | }
273 |
274 | return {
275 | content: [{ type: "text", text: JSON.stringify(processedResults, null, 2) }],
276 | isError: false,
277 | };
278 | } catch (error) {
279 | throw new Error(`Error searching table ${table_name}: ${error}`);
280 | }
281 | }
282 |
283 | case "list_tables": {
284 | try {
285 | const tables = await db.tableNames();
286 | return {
287 | content: [{ type: "text", text: JSON.stringify(tables, null, 2) }],
288 | isError: false,
289 | };
290 | } catch (error) {
291 | throw new Error(`Error listing tables: ${error}`);
292 | }
293 | }
294 |
295 | default:
296 | throw new Error(`Unknown tool: ${name}`);
297 | }
298 | } catch (error) {
299 | const errorMessage = error instanceof Error ? error.message : String(error);
300 | return {
301 | content: [{ type: "text", text: `Error: ${errorMessage}` }],
302 | isError: true,
303 | };
304 | }
305 | });
306 |
307 | // Start server
308 | async function runServer() {
309 | await connectDB();
310 | const transport = new StdioServerTransport();
311 | await server.connect(transport);
312 | console.error("LanceDB MCP Server running on stdio");
313 | }
314 |
315 | runServer().catch((error) => {
316 | console.error("Fatal error running server:", error);
317 | process.exit(1);
318 | });
```