#
tokens: 3805/50000 6/6 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .gitignore
├── dist
│   └── index.js
├── index.ts
├── package.json
├── pnpm-lock.yaml
├── prompts.yml
├── README.md
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
node_modules
test_db
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
# LanceDB Node.js Vector Search

A Node.js implementation for vector search using LanceDB and Ollama's embedding model.

## Overview

This project demonstrates how to:
- Connect to a LanceDB database
- Create custom embedding functions using Ollama
- Perform vector similarity search against stored documents
- Process and display search results

## Prerequisites

- Node.js (v14 or later)
- Ollama running locally with the `nomic-embed-text` model
- LanceDB storage location with read/write permissions

## Installation

1. Clone the repository
2. Install dependencies:

```bash
pnpm install
```

## Dependencies

- `@lancedb/lancedb`: LanceDB client for Node.js
- `apache-arrow`: For handling columnar data
- `node-fetch`: For making API calls to Ollama

## Usage

Run the vector search test script:

```bash
pnpm test-vector-search
```

Or directly execute:

```bash
node test-vector-search.js
```

## Configuration

The script connects to:
- LanceDB at the configured path
- Ollama API at `http://localhost:11434/api/embeddings`

## MCP Configuration

To integrate with Claude Desktop as an MCP service, add the following to your MCP configuration JSON:

```json
{
  "mcpServers": {
    "lanceDB": {
      "command": "node",
      "args": [
        "/path/to/lancedb-node/dist/index.js",
        "--db-path",
        "/path/to/your/lancedb/storage"
      ]
    }
  }
}
```

Replace the paths with your actual installation paths:
- `/path/to/lancedb-node/dist/index.js` - Path to the compiled index.js file
- `/path/to/your/lancedb/storage` - Path to your LanceDB storage directory

## Custom Embedding Function

The project includes a custom `OllamaEmbeddingFunction` that:
- Sends text to the Ollama API
- Receives embeddings with 768 dimensions
- Formats them for use with LanceDB

## Vector Search Example

The example searches for "how to define success criteria" in the "ai-rag" table, displaying results with their similarity scores.

## License

[MIT License](LICENSE)

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.
```

--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
    "compilerOptions": {
        "target": "ES2022",
        "strict": true,
        "esModuleInterop": true,
        "skipLibCheck": true,
        "forceConsistentCasingInFileNames": true,
        "resolveJsonModule": true,
        "outDir": "./dist",
        "rootDir": ".",
        "moduleResolution": "NodeNext",
        "module": "NodeNext"
    },
    "include": [
    "./**/*.ts"
    ],
    "exclude": [
        "node_modules"
    ]
}
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "@modelcontextprotocol/server-lancedb",
  "version": "0.1.0",
  "description": "MCP server for vector search using LanceDB",
  "license": "MIT",
  "author": "Vurtnec",
  "type": "module",
  "bin": {
    "mcp-server-lancedb": "dist/index.js"
  },
  "files": [
    "dist"
  ],
  "scripts": {
    "build": "tsc && shx chmod +x dist/*.js",
    "prepare": "npm run build",
    "watch": "tsc --watch"
  },
  "dependencies": {
    "@lancedb/lancedb": "^0.17.0",
    "@modelcontextprotocol/sdk": "0.5.0",
    "apache-arrow": "^15.0.2",
    "node-fetch": "^3.3.2",
    "zod": "^3.24.1",
    "zod-to-json-schema": "^3.24.1"
  },
  "devDependencies": {
    "@types/node": "^22",
    "shx": "^0.3.4",
    "typescript": "^5.3.3"
  }
}
```

--------------------------------------------------------------------------------
/prompts.yml:
--------------------------------------------------------------------------------

```yaml
rag_prompt: |
  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.

  1. Advanced Search Strategy:
    - First, thoroughly analyze the user's query to identify key concepts, entities, and information needs
    - Break down complex queries into multiple specific sub-queries to capture different aspects
    - Conduct your queries in English to access information
    - Execute multiple vector_search operations in parallel for these sub-queries
    - After initial retrieval, identify knowledge gaps and perform deep search by:
      a) Extracting new search terms from initial results
      b) Exploring related concepts mentioned in retrieved documents
      c) Investigating contradictions or uncertainties in the initial results
    - Continue this deep search process until you have comprehensive information or reach MAX_SEARCH_ATTEMPTS (10)

  2. Retrieval Parameters:
    - ONLY use table_name: "<your_table_name>"
    - Default limit: 10 (adjust as needed for specific queries)
    - Vary query_text formulations to capture different semantic aspects of the user's question

  3. Response Construction:
    - Rely exclusively on information from vector_search retrieval
    - Synthesize information from all search operations into a coherent, comprehensive answer
    - Clearly distinguish between retrieved information and any necessary explanations
    - Always include the source's full URL in your response for verification

  4. Quality Control:
    - Evaluate if the combined retrieved information adequately answers the user's question
    - If information is insufficient after MAX_SEARCH_ATTEMPTS, acknowledge limitations
    - Never hallucinate information when retrieval results are inadequate

  5. Restriction:
    - You are ONLY authorized to use the vector_search tool
    - Do NOT attempt to use any other tools under any circumstances
    - If a query cannot be answered using vector_search alone, explain this limitation politely

  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
#!/usr/bin/env node

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
  ToolSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
import * as lancedb from "@lancedb/lancedb";
import fetch from "node-fetch";

// Parse command line arguments
const args = process.argv.slice(2);
let dbPath: string | undefined;
let ollamaEndpoint: string = "http://localhost:11434/api/embeddings";
let ollamaModel: string = "nomic-embed-text:latest";
let showHelp: boolean = false;

for (let i = 0; i < args.length; i++) {
  switch (args[i]) {
    case '--db-path':
      dbPath = args[++i];
      break;
    case '--ollama-endpoint':
      ollamaEndpoint = args[++i];
      break;
    case '--ollama-model':
      ollamaModel = args[++i];
      break;
    case '--help':
    case '-h':
      showHelp = true;
      break;
  }
}

// Show help message if requested
if (showHelp) {
  console.error('Usage: mcp-server-lancedb --db-path <path> [--ollama-endpoint <url>] [--ollama-model <model>]');
  console.error('');
  console.error('Options:');
  console.error('  --db-path <path>           Path to the LanceDB database (required)');
  console.error('  --ollama-endpoint <url>    URL of the Ollama API embeddings endpoint (default: http://localhost:11434/api/embeddings)');
  console.error('  --ollama-model <model>     Ollama model to use for embeddings (default: nomic-embed-text:latest)');
  console.error('  --help, -h                 Show this help message');
  process.exit(0);
}

// Validate required command line arguments
if (!dbPath) {
  console.error('Error: Missing required arguments');
  console.error('Usage: mcp-server-lancedb --db-path <path> [--ollama-endpoint <url>] [--ollama-model <model>]');
  process.exit(1);
}

// Ollama API function for calling embeddings
async function generateEmbedding(text: string): Promise<number[]> {
  try {
    const response = await fetch(ollamaEndpoint, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        model: ollamaModel,
        prompt: text
      }),
    });

    if (!response.ok) {
      throw new Error(`Ollama API error: ${response.status} ${response.statusText}`);
    }

    const data = await response.json() as { embedding: number[] };
    return data.embedding;
  } catch (error) {
    console.error(`Error generating embedding: ${(error as Error).message}`);
    throw error;
  }
}

// Schema definitions
const VectorAddArgsSchema = z.object({
  table_name: z.string().describe('Name of the table to add vectors to'),
  vectors: z.array(z.object({
    vector: z.array(z.number()).describe('Vector data'),
  }).passthrough()).describe('Array of vectors with metadata to add')
});

const VectorSearchArgsSchema = z.object({
  table_name: z.string().describe('Name of the table to search in'),
  query_vector: z.array(z.number()).optional().describe('Query vector for similarity search'),
  query_text: z.string().optional().describe('Text query to be converted to a vector using Ollama embedding'),
  limit: z.number().optional().describe('Maximum number of results to return (default: 10)'),
  distance_type: z.enum(['l2', 'cosine', 'dot']).optional().describe('Distance metric to use (default: cosine)'),
  where: z.string().optional().describe('Filter condition in SQL syntax'),
  with_vectors: z.boolean().optional().describe('Whether to include vector data in results (default: false)')
}).refine((data) => {
  return data.query_vector !== undefined || data.query_text !== undefined;
}, {
  message: "Either query_vector or query_text must be provided"
});

const ListTablesArgsSchema = z.object({});

const ToolInputSchema = ToolSchema.shape.inputSchema;
type ToolInput = z.infer<typeof ToolInputSchema>;

// Connect to the LanceDB database
let db: lancedb.Connection;

async function connectDB() {
  try {
    db = await lancedb.connect(dbPath as string);
    console.error(`Connected to LanceDB at: ${dbPath}`);
  } catch (error) {
    console.error(`Failed to connect to LanceDB: ${error}`);
    process.exit(1);
  }
}

// Server setup
const server = new Server(
  {
    name: "lancedb-vector-server",
    version: "0.1.0",
  },
  {
    capabilities: {
      tools: {},
    },
  },
);

// Tool implementations

// Tool handlers
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [
      {
        name: "vector_add",
        description: "Add vectors with metadata to a LanceDB table. Creates the table if it doesn't exist.",
        inputSchema: zodToJsonSchema(VectorAddArgsSchema) as ToolInput,
      },
      {
        name: "vector_search",
        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.",
        inputSchema: zodToJsonSchema(VectorSearchArgsSchema) as ToolInput,
      },
      {
        name: "list_tables",
        description: "List all tables in the LanceDB database.",
        inputSchema: zodToJsonSchema(ListTablesArgsSchema) as ToolInput,
      }
    ]
  };
});

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  try {
    const { name, arguments: args } = request.params;

    switch (name) {
      case "vector_add": {
        const parsed = VectorAddArgsSchema.safeParse(args);
        if (!parsed.success) {
          throw new Error(`Invalid arguments for vector_add: ${parsed.error}`);
        }
        
        const { table_name, vectors } = parsed.data;
        
        // Check if table exists, if not create it
        try {
          const table = await db.openTable(table_name);
          await table.add(vectors);
          return {
            content: [{ type: "text", text: `Added ${vectors.length} vectors to table ${table_name}` }],
            isError: false,
          };
        } catch (error) {
          // Table doesn't exist, create it
          if ((error as Error).message.includes("does not exist")) {
            const table = await db.createTable(table_name, vectors);
            return {
              content: [{ type: "text", text: `Created table ${table_name} and added ${vectors.length} vectors` }],
              isError: false,
            };
          } else {
            throw error;
          }
        }
      }

      case "vector_search": {
        const parsed = VectorSearchArgsSchema.safeParse(args);
        if (!parsed.success) {
          throw new Error(`Invalid arguments for vector_search: ${parsed.error}`);
        }
        
        const { 
          table_name, 
          query_vector, 
          query_text, 
          limit = 10, 
          distance_type = 'cosine', 
          where, 
          with_vectors = false 
        } = parsed.data;
        
        try {
          const table = await db.openTable(table_name);
          
          // Determine which query vector to use
          let searchVector: number[];
          
          if (query_vector) {
            // Directly use the provided vector
            searchVector = query_vector;
          } else if (query_text) {
            // Generate embedding vector for text using Ollama
            console.error(`Generating embedding for text: "${query_text}"`);
            searchVector = await generateEmbedding(query_text);
            console.error(`Generated embedding with dimension: ${searchVector.length}`);
          } else {
            throw new Error('Either query_vector or query_text must be provided');
          }
          
          // Check table structure and vector dimensions
          const schema = await table.schema();
          const vectorField = schema.fields.find(field => 
            field.type.typeId === 16 && field.type.listSize > 10
          );
          
          if (!vectorField) {
            throw new Error('No vector column found in the table');
          }
          
          const vectorColumnName = vectorField.name;
          const vectorDimension = vectorField.type.listSize;
          
          if (searchVector.length !== vectorDimension) {
            console.error(`Warning: Query vector dimension (${searchVector.length}) doesn't match table vector dimension (${vectorDimension})`);
          }
          
          // Execute vector search
          let query = table.vectorSearch(searchVector);
          
          // Set distance type and limit
          query = query.distanceType(distance_type).limit(limit);
          
          if (where) {
            query = query.where(where);
          }
          
          const originalResults = await query.toArray();
          
          // Create new result objects instead of modifying originals
          let processedResults;
          if (!with_vectors) {
            processedResults = originalResults.map(item => {
              // Create a new object excluding the vector property
              const itemCopy = { ...item };
              delete itemCopy[vectorColumnName];
              return itemCopy;
            });
          } else {
            processedResults = originalResults;
          }
          
          return {
            content: [{ type: "text", text: JSON.stringify(processedResults, null, 2) }],
            isError: false,
          };
        } catch (error) {
          throw new Error(`Error searching table ${table_name}: ${error}`);
        }
      }

      case "list_tables": {
        try {
          const tables = await db.tableNames();
          return {
            content: [{ type: "text", text: JSON.stringify(tables, null, 2) }],
            isError: false,
          };
        } catch (error) {
          throw new Error(`Error listing tables: ${error}`);
        }
      }

      default:
        throw new Error(`Unknown tool: ${name}`);
    }
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    return {
      content: [{ type: "text", text: `Error: ${errorMessage}` }],
      isError: true,
    };
  }
});

// Start server
async function runServer() {
  await connectDB();
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error("LanceDB MCP Server running on stdio");
}

runServer().catch((error) => {
  console.error("Fatal error running server:", error);
  process.exit(1);
});
```