#
tokens: 5009/50000 6/6 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | });
```