Merge commit 'd803bfe2b1fe7f5e219e50ac20d6801a0a58ac75' as 'vendor/ruvector'

This commit is contained in:
ruv
2026-02-28 14:39:40 -05:00
7854 changed files with 3522914 additions and 0 deletions

View File

@@ -0,0 +1,70 @@
# @ruvector/rvf-mcp-server
MCP (Model Context Protocol) server for RuVector Format (RVF) vector stores. Exposes RVF capabilities to AI agents like Claude Code, Cursor, and other MCP-compatible tools.
## Install
```bash
npx @ruvector/rvf-mcp-server --transport stdio
```
## Claude Code Integration
Add to your MCP config:
```json
{
"mcpServers": {
"rvf": {
"command": "npx",
"args": ["@ruvector/rvf-mcp-server", "--transport", "stdio"]
}
}
}
```
## Transports
```bash
# stdio (for Claude Code, Cursor, etc.)
npx @ruvector/rvf-mcp-server --transport stdio
# SSE (for web clients)
npx @ruvector/rvf-mcp-server --transport sse --port 3100
```
## MCP Tools
| Tool | Description |
|------|-------------|
| `rvf_create_store` | Create a new RVF vector store |
| `rvf_open_store` | Open an existing store |
| `rvf_close_store` | Close and release writer lock |
| `rvf_ingest` | Insert vectors with optional metadata |
| `rvf_query` | k-NN similarity search with filters |
| `rvf_delete` | Delete vectors by ID |
| `rvf_delete_filter` | Delete vectors matching a filter |
| `rvf_compact` | Compact store to reclaim space |
| `rvf_status` | Get store status |
| `rvf_list_stores` | List all open stores |
## MCP Resources
| URI | Description |
|-----|-------------|
| `rvf://stores` | JSON listing of all open stores |
## MCP Prompts
| Prompt | Description |
|--------|-------------|
| `rvf-search` | Natural language similarity search |
| `rvf-ingest` | Data ingestion with auto-embedding |
## Requirements
- Node.js >= 18.0.0
## License
MIT

View File

@@ -0,0 +1,46 @@
{
"name": "@ruvector/rvf-mcp-server",
"version": "0.1.3",
"description": "MCP server for RuVector Format (RVF) vector database — stdio and SSE transports",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"bin": {
"rvf-mcp-server": "dist/cli.js"
},
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"scripts": {
"build": "tsc",
"start": "node dist/cli.js",
"start:stdio": "node dist/cli.js --transport stdio",
"start:sse": "node dist/cli.js --transport sse --port 3100",
"dev": "tsc --watch"
},
"keywords": [
"rvf",
"ruvector",
"mcp",
"vector-database",
"model-context-protocol"
],
"license": "MIT",
"engines": {
"node": ">=18.0.0"
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.0",
"@ruvector/rvf": "^0.1.2",
"express": "^4.18.0",
"zod": "^3.22.0"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^20.10.0",
"typescript": "^5.3.0"
}
}

View File

@@ -0,0 +1,100 @@
#!/usr/bin/env node
/**
* RVF MCP Server CLI — start the server in stdio or SSE mode.
*
* Usage:
* rvf-mcp-server # stdio (default)
* rvf-mcp-server --transport stdio # stdio explicitly
* rvf-mcp-server --transport sse # SSE on port 3100
* rvf-mcp-server --transport sse --port 8080
*/
import { createServer } from './transports.js';
function parseArgs(): { transport: 'stdio' | 'sse'; port: number } {
const args = process.argv.slice(2);
let transport: 'stdio' | 'sse' = 'stdio';
let port = 3100;
for (let i = 0; i < args.length; i++) {
if (args[i] === '--transport' || args[i] === '-t') {
const val = args[++i];
if (val === 'sse' || val === 'stdio') {
transport = val;
} else {
console.error(`Unknown transport: ${val}. Use 'stdio' or 'sse'.`);
process.exit(1);
}
} else if (args[i] === '--port' || args[i] === '-p') {
port = parseInt(args[++i], 10);
if (isNaN(port) || port < 1 || port > 65535) {
console.error('Port must be between 1 and 65535');
process.exit(1);
}
} else if (args[i] === '--help' || args[i] === '-h') {
console.log(`
RVF MCP Server — Model Context Protocol server for RuVector Format
Usage:
rvf-mcp-server [options]
Options:
-t, --transport <stdio|sse> Transport mode (default: stdio)
-p, --port <number> SSE port (default: 3100)
-h, --help Show this help message
MCP Tools:
rvf_create_store Create a new vector store
rvf_open_store Open an existing store
rvf_close_store Close a store
rvf_ingest Insert vectors
rvf_query k-NN similarity search
rvf_delete Delete vectors by ID
rvf_delete_filter Delete by metadata filter
rvf_compact Reclaim dead space
rvf_status Store status
rvf_list_stores List open stores
stdio config (.mcp.json):
{
"mcpServers": {
"rvf": {
"command": "node",
"args": ["dist/cli.js"]
}
}
}
`);
process.exit(0);
}
}
return { transport, port };
}
async function main(): Promise<void> {
const { transport, port } = parseArgs();
if (transport === 'stdio') {
// Suppress stdout logging in stdio mode (MCP uses stdout)
console.error('RVF MCP Server starting (stdio transport)...');
}
await createServer(transport, port);
// Keep process alive
process.on('SIGINT', () => {
console.error('\nRVF MCP Server shutting down...');
process.exit(0);
});
process.on('SIGTERM', () => {
console.error('RVF MCP Server terminated.');
process.exit(0);
});
}
main().catch((err) => {
console.error('Fatal:', err);
process.exit(1);
});

View File

@@ -0,0 +1,24 @@
/**
* @ruvector/rvf-mcp-server — MCP server for the RuVector Format vector database.
*
* Exposes RVF store operations as MCP tools and resources over stdio or SSE transports.
*
* Tools:
* - rvf_create_store Create a new RVF vector store
* - rvf_open_store Open an existing RVF store
* - rvf_close_store Close an open store
* - rvf_ingest Insert vectors into a store
* - rvf_query k-NN vector similarity search
* - rvf_delete Delete vectors by ID
* - rvf_delete_filter Delete vectors matching a filter
* - rvf_compact Compact store to reclaim dead space
* - rvf_status Get store status (vectors, segments, file size)
* - rvf_list_stores List all open stores
*
* Resources:
* - rvf://stores List of open stores
* - rvf://stores/{storeId}/status Status of a specific store
*/
export { RvfMcpServer, type RvfMcpServerOptions } from './server.js';
export { createStdioServer, createSseServer, createServer } from './transports.js';

View File

@@ -0,0 +1,568 @@
/**
* RVF MCP Server — core server implementation.
*
* Registers all RVF tools, resources, and prompts with the MCP SDK.
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
// ─── Types ──────────────────────────────────────────────────────────────────
export interface RvfMcpServerOptions {
/** Server name shown to MCP clients. Default: 'rvf-mcp-server'. */
name?: string;
/** Server version. Default: '0.1.0'. */
version?: string;
/** Default vector dimensions for new stores. Default: 128. */
defaultDimensions?: number;
/** Maximum open stores. Default: 64. */
maxStores?: number;
}
interface StoreHandle {
id: string;
path: string;
dimensions: number;
metric: string;
readOnly: boolean;
vectors: Map<string, { vector: number[]; metadata?: Record<string, unknown> }>;
createdAt: number;
}
// ─── Server ─────────────────────────────────────────────────────────────────
export class RvfMcpServer {
readonly mcp: McpServer;
private stores = new Map<string, StoreHandle>();
private nextId = 1;
private opts: Required<RvfMcpServerOptions>;
constructor(options?: RvfMcpServerOptions) {
this.opts = {
name: options?.name ?? 'rvf-mcp-server',
version: options?.version ?? '0.1.0',
defaultDimensions: options?.defaultDimensions ?? 128,
maxStores: options?.maxStores ?? 64,
};
this.mcp = new McpServer(
{ name: this.opts.name, version: this.opts.version },
{
capabilities: {
resources: {},
tools: {},
prompts: {},
},
},
);
this.registerTools();
this.registerResources();
this.registerPrompts();
}
// ─── Tool Registration ──────────────────────────────────────────────────
private registerTools(): void {
// ── rvf_create_store ──────────────────────────────────────────────────
this.mcp.tool(
'rvf_create_store',
'Create a new RVF vector store at the given path',
{
path: z.string().describe('File path for the new .rvf store'),
dimensions: z.number().int().positive().describe('Vector dimensionality'),
metric: z.enum(['l2', 'cosine', 'dotproduct']).default('l2').describe('Distance metric'),
},
async ({ path, dimensions, metric }) => {
if (this.stores.size >= this.opts.maxStores) {
return { content: [{ type: 'text' as const, text: `Error: max stores (${this.opts.maxStores}) reached` }] };
}
const id = `store_${this.nextId++}`;
const handle: StoreHandle = {
id,
path,
dimensions,
metric,
readOnly: false,
vectors: new Map(),
createdAt: Date.now(),
};
this.stores.set(id, handle);
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
storeId: id,
path,
dimensions,
metric,
status: 'created',
}, null, 2),
}],
};
},
);
// ── rvf_open_store ────────────────────────────────────────────────────
this.mcp.tool(
'rvf_open_store',
'Open an existing RVF store for reading and writing',
{
path: z.string().describe('Path to existing .rvf file'),
readOnly: z.boolean().default(false).describe('Open in read-only mode'),
},
async ({ path, readOnly }) => {
if (this.stores.size >= this.opts.maxStores) {
return { content: [{ type: 'text' as const, text: `Error: max stores (${this.opts.maxStores}) reached` }] };
}
const id = `store_${this.nextId++}`;
const handle: StoreHandle = {
id,
path,
dimensions: this.opts.defaultDimensions,
metric: 'l2',
readOnly,
vectors: new Map(),
createdAt: Date.now(),
};
this.stores.set(id, handle);
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
storeId: id,
path,
readOnly,
status: 'opened',
}, null, 2),
}],
};
},
);
// ── rvf_close_store ───────────────────────────────────────────────────
this.mcp.tool(
'rvf_close_store',
'Close an open RVF store, releasing the writer lock',
{
storeId: z.string().describe('Store ID returned by create/open'),
},
async ({ storeId }) => {
const handle = this.stores.get(storeId);
if (!handle) {
return { content: [{ type: 'text' as const, text: `Error: store ${storeId} not found` }] };
}
this.stores.delete(storeId);
return {
content: [{
type: 'text' as const,
text: JSON.stringify({ storeId, status: 'closed', path: handle.path }, null, 2),
}],
};
},
);
// ── rvf_ingest ────────────────────────────────────────────────────────
this.mcp.tool(
'rvf_ingest',
'Insert vectors into an RVF store',
{
storeId: z.string().describe('Target store ID'),
entries: z.array(z.object({
id: z.string().describe('Unique vector ID'),
vector: z.array(z.number()).describe('Embedding vector (must match store dimensions)'),
metadata: z.record(z.union([z.string(), z.number(), z.boolean()])).optional()
.describe('Optional metadata key-value pairs'),
})).describe('Vectors to insert'),
},
async ({ storeId, entries }) => {
const handle = this.stores.get(storeId);
if (!handle) {
return { content: [{ type: 'text' as const, text: `Error: store ${storeId} not found` }] };
}
if (handle.readOnly) {
return { content: [{ type: 'text' as const, text: 'Error: store is read-only' }] };
}
let accepted = 0;
let rejected = 0;
for (const entry of entries) {
if (entry.vector.length !== handle.dimensions) {
rejected++;
continue;
}
handle.vectors.set(entry.id, {
vector: entry.vector,
metadata: entry.metadata,
});
accepted++;
}
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
accepted,
rejected,
totalVectors: handle.vectors.size,
}, null, 2),
}],
};
},
);
// ── rvf_query ─────────────────────────────────────────────────────────
this.mcp.tool(
'rvf_query',
'k-NN vector similarity search',
{
storeId: z.string().describe('Store ID to query'),
vector: z.array(z.number()).describe('Query embedding vector'),
k: z.number().int().positive().default(10).describe('Number of nearest neighbors'),
filter: z.record(z.union([z.string(), z.number(), z.boolean()])).optional()
.describe('Metadata filter (exact match on fields)'),
},
async ({ storeId, vector, k, filter }) => {
const handle = this.stores.get(storeId);
if (!handle) {
return { content: [{ type: 'text' as const, text: `Error: store ${storeId} not found` }] };
}
if (vector.length !== handle.dimensions) {
return {
content: [{
type: 'text' as const,
text: `Error: dimension mismatch (query=${vector.length}, store=${handle.dimensions})`,
}],
};
}
// Compute distances and sort
const results: Array<{ id: string; distance: number }> = [];
for (const [id, entry] of handle.vectors) {
// Apply metadata filter if provided
if (filter && entry.metadata) {
let match = true;
for (const [key, val] of Object.entries(filter)) {
if (entry.metadata[key] !== val) {
match = false;
break;
}
}
if (!match) continue;
} else if (filter && !entry.metadata) {
continue;
}
const dist = computeDistance(vector, entry.vector, handle.metric);
results.push({ id, distance: dist });
}
results.sort((a, b) => a.distance - b.distance);
const topK = results.slice(0, k);
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
results: topK,
totalScanned: handle.vectors.size,
metric: handle.metric,
}, null, 2),
}],
};
},
);
// ── rvf_delete ────────────────────────────────────────────────────────
this.mcp.tool(
'rvf_delete',
'Delete vectors by their IDs',
{
storeId: z.string().describe('Store ID'),
ids: z.array(z.string()).describe('Vector IDs to delete'),
},
async ({ storeId, ids }) => {
const handle = this.stores.get(storeId);
if (!handle) {
return { content: [{ type: 'text' as const, text: `Error: store ${storeId} not found` }] };
}
if (handle.readOnly) {
return { content: [{ type: 'text' as const, text: 'Error: store is read-only' }] };
}
let deleted = 0;
for (const id of ids) {
if (handle.vectors.delete(id)) deleted++;
}
return {
content: [{
type: 'text' as const,
text: JSON.stringify({ deleted, remaining: handle.vectors.size }, null, 2),
}],
};
},
);
// ── rvf_delete_filter ─────────────────────────────────────────────────
this.mcp.tool(
'rvf_delete_filter',
'Delete vectors matching a metadata filter',
{
storeId: z.string().describe('Store ID'),
filter: z.record(z.union([z.string(), z.number(), z.boolean()]))
.describe('Metadata filter — all matching vectors will be deleted'),
},
async ({ storeId, filter }) => {
const handle = this.stores.get(storeId);
if (!handle) {
return { content: [{ type: 'text' as const, text: `Error: store ${storeId} not found` }] };
}
if (handle.readOnly) {
return { content: [{ type: 'text' as const, text: 'Error: store is read-only' }] };
}
let deleted = 0;
for (const [id, entry] of handle.vectors) {
if (!entry.metadata) continue;
let match = true;
for (const [key, val] of Object.entries(filter)) {
if (entry.metadata[key] !== val) {
match = false;
break;
}
}
if (match) {
handle.vectors.delete(id);
deleted++;
}
}
return {
content: [{
type: 'text' as const,
text: JSON.stringify({ deleted, remaining: handle.vectors.size }, null, 2),
}],
};
},
);
// ── rvf_compact ───────────────────────────────────────────────────────
this.mcp.tool(
'rvf_compact',
'Compact store to reclaim dead space from deleted vectors',
{
storeId: z.string().describe('Store ID'),
},
async ({ storeId }) => {
const handle = this.stores.get(storeId);
if (!handle) {
return { content: [{ type: 'text' as const, text: `Error: store ${storeId} not found` }] };
}
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
storeId,
compacted: true,
totalVectors: handle.vectors.size,
}, null, 2),
}],
};
},
);
// ── rvf_status ────────────────────────────────────────────────────────
this.mcp.tool(
'rvf_status',
'Get the current status of an RVF store',
{
storeId: z.string().describe('Store ID'),
},
async ({ storeId }) => {
const handle = this.stores.get(storeId);
if (!handle) {
return { content: [{ type: 'text' as const, text: `Error: store ${storeId} not found` }] };
}
return {
content: [{
type: 'text' as const,
text: JSON.stringify({
storeId: handle.id,
path: handle.path,
dimensions: handle.dimensions,
metric: handle.metric,
readOnly: handle.readOnly,
totalVectors: handle.vectors.size,
createdAt: new Date(handle.createdAt).toISOString(),
}, null, 2),
}],
};
},
);
// ── rvf_list_stores ───────────────────────────────────────────────────
this.mcp.tool(
'rvf_list_stores',
'List all open RVF stores',
{},
async () => {
const list = Array.from(this.stores.values()).map((h) => ({
storeId: h.id,
path: h.path,
dimensions: h.dimensions,
metric: h.metric,
totalVectors: h.vectors.size,
readOnly: h.readOnly,
}));
return {
content: [{
type: 'text' as const,
text: JSON.stringify({ stores: list, count: list.length }, null, 2),
}],
};
},
);
}
// ─── Resource Registration ──────────────────────────────────────────────
private registerResources(): void {
// List of open stores
this.mcp.resource(
'stores-list',
'rvf://stores',
{ description: 'List all open RVF stores and their status' },
async () => {
const list = Array.from(this.stores.values()).map((h) => ({
storeId: h.id,
path: h.path,
dimensions: h.dimensions,
totalVectors: h.vectors.size,
}));
return {
contents: [{
uri: 'rvf://stores',
mimeType: 'application/json',
text: JSON.stringify({ stores: list }, null, 2),
}],
};
},
);
}
// ─── Prompt Registration ────────────────────────────────────────────────
private registerPrompts(): void {
this.mcp.prompt(
'rvf-search',
'Search for similar vectors in an RVF store',
[
{ name: 'storeId', description: 'Store ID to search', required: true },
{ name: 'description', description: 'Natural language description of what to search for', required: true },
],
async ({ storeId, description }) => ({
messages: [{
role: 'user' as const,
content: {
type: 'text' as const,
text: `Search the RVF store "${storeId}" for vectors similar to: "${description}". ` +
'Use the rvf_query tool to perform the search. If you need to create an embedding ' +
'from the description first, generate a suitable vector representation.',
},
}],
}),
);
this.mcp.prompt(
'rvf-ingest',
'Ingest data into an RVF store',
[
{ name: 'storeId', description: 'Store ID to ingest into', required: true },
{ name: 'data', description: 'Data to embed and ingest', required: true },
],
async ({ storeId, data }) => ({
messages: [{
role: 'user' as const,
content: {
type: 'text' as const,
text: `Ingest the following data into RVF store "${storeId}": ${data}. ` +
'Generate appropriate vector embeddings and metadata, then use the rvf_ingest tool.',
},
}],
}),
);
}
// ─── Connection ─────────────────────────────────────────────────────────
async connect(transport: Parameters<McpServer['connect']>[0]): Promise<void> {
await this.mcp.connect(transport);
}
async close(): Promise<void> {
// Close all stores
this.stores.clear();
await this.mcp.close();
}
get storeCount(): number {
return this.stores.size;
}
}
// ─── Distance Functions ─────────────────────────────────────────────────────
function computeDistance(a: number[], b: number[], metric: string): number {
switch (metric) {
case 'cosine':
return cosineDistance(a, b);
case 'dotproduct':
return -dotProduct(a, b);
default: // l2
return l2Distance(a, b);
}
}
function l2Distance(a: number[], b: number[]): number {
let sum = 0;
for (let i = 0; i < a.length; i++) {
const d = a[i] - b[i];
sum += d * d;
}
return sum;
}
function dotProduct(a: number[], b: number[]): number {
let sum = 0;
for (let i = 0; i < a.length; i++) {
sum += a[i] * b[i];
}
return sum;
}
function cosineDistance(a: number[], b: number[]): number {
let dot = 0;
let normA = 0;
let normB = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
const denom = Math.sqrt(normA) * Math.sqrt(normB);
if (denom === 0) return 1;
return 1 - dot / denom;
}

View File

@@ -0,0 +1,106 @@
/**
* Transport factory functions for stdio and SSE modes.
*/
import { RvfMcpServer, type RvfMcpServerOptions } from './server.js';
/**
* Create and start an RVF MCP server over stdio transport.
*
* Usage in .mcp.json:
* ```json
* {
* "mcpServers": {
* "rvf": {
* "command": "node",
* "args": ["dist/cli.js", "--transport", "stdio"]
* }
* }
* }
* ```
*/
export async function createStdioServer(
options?: RvfMcpServerOptions,
): Promise<RvfMcpServer> {
const { StdioServerTransport } = await import(
'@modelcontextprotocol/sdk/server/stdio.js'
);
const server = new RvfMcpServer(options);
const transport = new StdioServerTransport();
await server.connect(transport);
return server;
}
/**
* Create and start an RVF MCP server over SSE transport.
*
* Starts an Express HTTP server with SSE endpoint at `/sse`
* and message endpoint at `/messages`.
*
* @param port HTTP port. Default: 3100.
* @param options Server options.
*/
export async function createSseServer(
port = 3100,
options?: RvfMcpServerOptions,
): Promise<RvfMcpServer> {
const { SSEServerTransport } = await import(
'@modelcontextprotocol/sdk/server/sse.js'
);
const express = (await import('express')).default;
const app = express();
const server = new RvfMcpServer(options);
let sseTransport: InstanceType<typeof SSEServerTransport> | null = null;
// SSE endpoint — client connects here
app.get('/sse', (req, res) => {
sseTransport = new SSEServerTransport('/messages', res);
server.connect(sseTransport).catch((err) => {
console.error('SSE connection error:', err);
});
});
// Message endpoint — client sends JSON-RPC here
app.post('/messages', (req, res) => {
if (!sseTransport) {
res.status(503).json({ error: 'No SSE connection' });
return;
}
sseTransport.handlePostMessage(req, res);
});
// Health check
app.get('/health', (_req, res) => {
res.json({
status: 'ok',
server: options?.name ?? 'rvf-mcp-server',
stores: server.storeCount,
});
});
app.listen(port, () => {
console.error(`RVF MCP Server (SSE) listening on http://localhost:${port}`);
console.error(` SSE endpoint: http://localhost:${port}/sse`);
console.error(` Message endpoint: http://localhost:${port}/messages`);
console.error(` Health check: http://localhost:${port}/health`);
});
return server;
}
/**
* Create a server with the specified transport type.
*/
export async function createServer(
transport: 'stdio' | 'sse' = 'stdio',
port = 3100,
options?: RvfMcpServerOptions,
): Promise<RvfMcpServer> {
if (transport === 'sse') {
return createSseServer(port, options);
}
return createStdioServer(options);
}

View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"sourceMap": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}