Squashed 'vendor/ruvector/' content from commit b64c2172
git-subtree-dir: vendor/ruvector git-subtree-split: b64c21726f2bb37286d9ee36a7869fef60cc6900
This commit is contained in:
70
npm/packages/rvf-mcp-server/README.md
Normal file
70
npm/packages/rvf-mcp-server/README.md
Normal 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
|
||||
46
npm/packages/rvf-mcp-server/package.json
Normal file
46
npm/packages/rvf-mcp-server/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
100
npm/packages/rvf-mcp-server/src/cli.ts
Normal file
100
npm/packages/rvf-mcp-server/src/cli.ts
Normal 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);
|
||||
});
|
||||
24
npm/packages/rvf-mcp-server/src/index.ts
Normal file
24
npm/packages/rvf-mcp-server/src/index.ts
Normal 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';
|
||||
568
npm/packages/rvf-mcp-server/src/server.ts
Normal file
568
npm/packages/rvf-mcp-server/src/server.ts
Normal 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;
|
||||
}
|
||||
106
npm/packages/rvf-mcp-server/src/transports.ts
Normal file
106
npm/packages/rvf-mcp-server/src/transports.ts
Normal 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);
|
||||
}
|
||||
19
npm/packages/rvf-mcp-server/tsconfig.json
Normal file
19
npm/packages/rvf-mcp-server/tsconfig.json
Normal 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"]
|
||||
}
|
||||
Reference in New Issue
Block a user