voyageai-cli 1.26.0 → 1.26.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/commands/chat.js +281 -78
- package/src/commands/playground.js +42 -19
- package/src/lib/chat.js +170 -4
- package/src/lib/llm.js +304 -2
- package/src/lib/mongo.js +6 -6
- package/src/lib/prompt.js +60 -1
- package/src/lib/tool-registry.js +194 -0
- package/src/mcp/tools/embedding.js +55 -43
- package/src/mcp/tools/ingest.js +74 -67
- package/src/mcp/tools/management.js +60 -48
- package/src/mcp/tools/retrieval.js +181 -163
- package/src/mcp/tools/utility.js +171 -153
- package/src/playground/index.html +508 -10
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tool Registry
|
|
5
|
+
*
|
|
6
|
+
* Single source of truth mapping MCP Zod schemas to LLM tool definitions
|
|
7
|
+
* and dispatching tool execution. Bridges the MCP tool handlers with the
|
|
8
|
+
* agent chat loop.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { z } = require('zod');
|
|
12
|
+
const schemas = require('../mcp/schemas');
|
|
13
|
+
|
|
14
|
+
// Lazy-loaded handlers to avoid circular deps
|
|
15
|
+
let _handlers;
|
|
16
|
+
function getHandlers() {
|
|
17
|
+
if (!_handlers) {
|
|
18
|
+
const { handleVaiQuery, handleVaiSearch, handleVaiRerank } = require('../mcp/tools/retrieval');
|
|
19
|
+
const { handleVaiEmbed, handleVaiSimilarity } = require('../mcp/tools/embedding');
|
|
20
|
+
const { handleVaiCollections, handleVaiModels } = require('../mcp/tools/management');
|
|
21
|
+
const { handleVaiTopics, handleVaiExplain, handleVaiEstimate } = require('../mcp/tools/utility');
|
|
22
|
+
const { handleVaiIngest } = require('../mcp/tools/ingest');
|
|
23
|
+
|
|
24
|
+
_handlers = {
|
|
25
|
+
vai_query: handleVaiQuery,
|
|
26
|
+
vai_search: handleVaiSearch,
|
|
27
|
+
vai_rerank: handleVaiRerank,
|
|
28
|
+
vai_embed: handleVaiEmbed,
|
|
29
|
+
vai_similarity: handleVaiSimilarity,
|
|
30
|
+
vai_collections: handleVaiCollections,
|
|
31
|
+
vai_models: handleVaiModels,
|
|
32
|
+
vai_topics: handleVaiTopics,
|
|
33
|
+
vai_explain: handleVaiExplain,
|
|
34
|
+
vai_estimate: handleVaiEstimate,
|
|
35
|
+
vai_ingest: handleVaiIngest,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return _handlers;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Tool definitions: name, description, and schema key for each tool.
|
|
43
|
+
*/
|
|
44
|
+
const TOOL_DEFINITIONS = [
|
|
45
|
+
{
|
|
46
|
+
name: 'vai_query',
|
|
47
|
+
description: 'Full RAG query: embeds the question with Voyage AI, runs vector search against MongoDB Atlas, and reranks results. Use this when you need to answer a question using the knowledge base.',
|
|
48
|
+
schemaKey: 'querySchema',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'vai_search',
|
|
52
|
+
description: 'Raw vector similarity search without reranking. Faster than vai_query but results are ordered by vector distance only. Use for exploratory searches or when you plan to rerank separately.',
|
|
53
|
+
schemaKey: 'searchSchema',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'vai_rerank',
|
|
57
|
+
description: 'Rerank documents against a query using Voyage AI reranker. Takes a query and candidate documents, returns them reordered by relevance.',
|
|
58
|
+
schemaKey: 'rerankSchema',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'vai_embed',
|
|
62
|
+
description: 'Embed text using a Voyage AI model and return the vector representation. Use for custom similarity logic, storing vectors, or debugging.',
|
|
63
|
+
schemaKey: 'embedSchema',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'vai_similarity',
|
|
67
|
+
description: 'Compare two texts semantically by embedding both and computing cosine similarity. Returns a score from -1 to 1.',
|
|
68
|
+
schemaKey: 'similaritySchema',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'vai_collections',
|
|
72
|
+
description: 'List available MongoDB collections with document counts and vector index information. Use to discover which knowledge bases exist.',
|
|
73
|
+
schemaKey: 'collectionsSchema',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: 'vai_models',
|
|
77
|
+
description: 'List available Voyage AI models with capabilities and pricing. Use when selecting a model or comparing options.',
|
|
78
|
+
schemaKey: 'modelsSchema',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'vai_topics',
|
|
82
|
+
description: 'List all available educational topics. Call this to discover what vai can explain.',
|
|
83
|
+
schemaKey: 'topicsSchema',
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'vai_explain',
|
|
87
|
+
description: 'Get a detailed explanation of a topic (embeddings, vector search, RAG, MoE, etc). Supports fuzzy matching.',
|
|
88
|
+
schemaKey: 'explainSchema',
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: 'vai_estimate',
|
|
92
|
+
description: 'Estimate costs for Voyage AI embedding and query operations at various scales.',
|
|
93
|
+
schemaKey: 'estimateSchema',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: 'vai_ingest',
|
|
97
|
+
description: 'Add a document to a collection: chunks the text, embeds each chunk with Voyage AI, and stores in MongoDB Atlas.',
|
|
98
|
+
schemaKey: 'ingestSchema',
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Convert a Zod schema fields object (as used in MCP schemas) to JSON Schema.
|
|
104
|
+
* Strips fields with defaults from the required array so the LLM doesn't
|
|
105
|
+
* have to provide them.
|
|
106
|
+
*
|
|
107
|
+
* @param {object} zodFields - Plain object of Zod field definitions
|
|
108
|
+
* @returns {object} JSON Schema object
|
|
109
|
+
*/
|
|
110
|
+
function zodSchemaToJsonSchema(zodFields) {
|
|
111
|
+
const obj = z.object(zodFields);
|
|
112
|
+
const jsonSchema = z.toJSONSchema(obj);
|
|
113
|
+
|
|
114
|
+
// Remove $schema key (not needed for tool definitions)
|
|
115
|
+
delete jsonSchema['$schema'];
|
|
116
|
+
|
|
117
|
+
// Strip fields with 'default' from required array.
|
|
118
|
+
// LLMs should not be forced to provide values that have defaults.
|
|
119
|
+
if (jsonSchema.required && jsonSchema.properties) {
|
|
120
|
+
jsonSchema.required = jsonSchema.required.filter(key => {
|
|
121
|
+
const prop = jsonSchema.properties[key];
|
|
122
|
+
return prop && !('default' in prop);
|
|
123
|
+
});
|
|
124
|
+
if (jsonSchema.required.length === 0) delete jsonSchema.required;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return jsonSchema;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get tool definitions formatted for a specific LLM provider.
|
|
132
|
+
*
|
|
133
|
+
* @param {'anthropic'|'openai'|'ollama'} format - Provider format
|
|
134
|
+
* @returns {Array} Tool definitions in provider-specific format
|
|
135
|
+
*/
|
|
136
|
+
function getToolDefinitions(format) {
|
|
137
|
+
return TOOL_DEFINITIONS.map(def => {
|
|
138
|
+
const zodFields = schemas[def.schemaKey];
|
|
139
|
+
const inputSchema = zodSchemaToJsonSchema(zodFields);
|
|
140
|
+
|
|
141
|
+
if (format === 'anthropic') {
|
|
142
|
+
return {
|
|
143
|
+
name: def.name,
|
|
144
|
+
description: def.description,
|
|
145
|
+
input_schema: inputSchema,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// OpenAI / Ollama format
|
|
150
|
+
return {
|
|
151
|
+
type: 'function',
|
|
152
|
+
function: {
|
|
153
|
+
name: def.name,
|
|
154
|
+
description: def.description,
|
|
155
|
+
parameters: inputSchema,
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Execute a tool by name with the given arguments.
|
|
163
|
+
* Validates args against the Zod schema, then calls the handler.
|
|
164
|
+
*
|
|
165
|
+
* @param {string} name - Tool name (e.g. 'vai_query')
|
|
166
|
+
* @param {object} args - Tool arguments
|
|
167
|
+
* @returns {Promise<{structuredContent: object, content: Array}>}
|
|
168
|
+
*/
|
|
169
|
+
async function executeTool(name, args) {
|
|
170
|
+
const handlers = getHandlers();
|
|
171
|
+
const handler = handlers[name];
|
|
172
|
+
if (!handler) {
|
|
173
|
+
throw new Error(`Unknown tool: "${name}". Available: ${Object.keys(handlers).join(', ')}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Find the schema for validation
|
|
177
|
+
const def = TOOL_DEFINITIONS.find(d => d.name === name);
|
|
178
|
+
if (!def) throw new Error(`No schema found for tool: "${name}"`);
|
|
179
|
+
|
|
180
|
+
const zodFields = schemas[def.schemaKey];
|
|
181
|
+
const zodObj = z.object(zodFields);
|
|
182
|
+
|
|
183
|
+
// Validate and apply defaults
|
|
184
|
+
const validated = zodObj.parse(args);
|
|
185
|
+
|
|
186
|
+
return handler(validated);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
module.exports = {
|
|
190
|
+
TOOL_DEFINITIONS,
|
|
191
|
+
zodSchemaToJsonSchema,
|
|
192
|
+
getToolDefinitions,
|
|
193
|
+
executeTool,
|
|
194
|
+
};
|
|
@@ -3,65 +3,77 @@
|
|
|
3
3
|
const { generateEmbeddings } = require('../../lib/api');
|
|
4
4
|
const { cosineSimilarity } = require('../../lib/math');
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Handler for vai_embed: embed text and return the vector.
|
|
8
|
+
* @param {object} input - Validated input matching embedSchema
|
|
9
|
+
* @returns {Promise<{structuredContent: object, content: Array}>}
|
|
10
|
+
*/
|
|
11
|
+
async function handleVaiEmbed(input) {
|
|
12
|
+
const embedOpts = { model: input.model, inputType: input.inputType };
|
|
13
|
+
if (input.dimensions) embedOpts.dimensions = input.dimensions;
|
|
14
|
+
|
|
15
|
+
const result = await generateEmbeddings([input.text], embedOpts);
|
|
16
|
+
const vector = result.data[0].embedding;
|
|
17
|
+
|
|
18
|
+
const structured = {
|
|
19
|
+
text: input.text.slice(0, 100) + (input.text.length > 100 ? '...' : ''),
|
|
20
|
+
model: input.model,
|
|
21
|
+
vector,
|
|
22
|
+
dimensions: vector.length,
|
|
23
|
+
inputType: input.inputType,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
structuredContent: structured,
|
|
28
|
+
content: [{ type: 'text', text: `Embedded text (${vector.length} dimensions, model: ${input.model}, type: ${input.inputType}). Vector: [${vector.slice(0, 5).map(v => v.toFixed(4)).join(', ')}, ... ${vector.length - 5} more]` }],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Handler for vai_similarity: compare two texts semantically.
|
|
34
|
+
* @param {object} input - Validated input matching similaritySchema
|
|
35
|
+
* @returns {Promise<{structuredContent: object, content: Array}>}
|
|
36
|
+
*/
|
|
37
|
+
async function handleVaiSimilarity(input) {
|
|
38
|
+
const result = await generateEmbeddings([input.text1, input.text2], {
|
|
39
|
+
model: input.model,
|
|
40
|
+
inputType: 'document',
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const vec1 = result.data[0].embedding;
|
|
44
|
+
const vec2 = result.data[1].embedding;
|
|
45
|
+
const similarity = cosineSimilarity(vec1, vec2);
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
structuredContent: {
|
|
49
|
+
text1: input.text1.slice(0, 100) + (input.text1.length > 100 ? '...' : ''),
|
|
50
|
+
text2: input.text2.slice(0, 100) + (input.text2.length > 100 ? '...' : ''),
|
|
51
|
+
similarity,
|
|
52
|
+
model: input.model,
|
|
53
|
+
},
|
|
54
|
+
content: [{ type: 'text', text: `Similarity: ${similarity.toFixed(4)} (model: ${input.model})\nText 1: "${input.text1.slice(0, 80)}..."\nText 2: "${input.text2.slice(0, 80)}..."` }],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
6
58
|
/**
|
|
7
59
|
* Register embedding tools: vai_embed, vai_similarity
|
|
8
60
|
* @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
|
|
9
61
|
* @param {object} schemas
|
|
10
62
|
*/
|
|
11
63
|
function registerEmbeddingTools(server, schemas) {
|
|
12
|
-
// vai_embed — embed text and return the vector
|
|
13
64
|
server.tool(
|
|
14
65
|
'vai_embed',
|
|
15
66
|
'Embed text using a Voyage AI model and return the vector representation. Use when you need the raw embedding vector for custom similarity logic, storing in another system, or debugging.',
|
|
16
67
|
schemas.embedSchema,
|
|
17
|
-
|
|
18
|
-
const embedOpts = { model: input.model, inputType: input.inputType };
|
|
19
|
-
if (input.dimensions) embedOpts.dimensions = input.dimensions;
|
|
20
|
-
|
|
21
|
-
const result = await generateEmbeddings([input.text], embedOpts);
|
|
22
|
-
const vector = result.data[0].embedding;
|
|
23
|
-
|
|
24
|
-
const structured = {
|
|
25
|
-
text: input.text.slice(0, 100) + (input.text.length > 100 ? '...' : ''),
|
|
26
|
-
model: input.model,
|
|
27
|
-
vector,
|
|
28
|
-
dimensions: vector.length,
|
|
29
|
-
inputType: input.inputType,
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
return {
|
|
33
|
-
structuredContent: structured,
|
|
34
|
-
content: [{ type: 'text', text: `Embedded text (${vector.length} dimensions, model: ${input.model}, type: ${input.inputType}). Vector: [${vector.slice(0, 5).map(v => v.toFixed(4)).join(', ')}, ... ${vector.length - 5} more]` }],
|
|
35
|
-
};
|
|
36
|
-
}
|
|
68
|
+
handleVaiEmbed
|
|
37
69
|
);
|
|
38
70
|
|
|
39
|
-
// vai_similarity — compare two texts
|
|
40
71
|
server.tool(
|
|
41
72
|
'vai_similarity',
|
|
42
73
|
'Compare two texts semantically by embedding both and computing cosine similarity. Returns a score from -1 (opposite) to 1 (identical). Use for duplicate detection, relevance checking, or topic comparison.',
|
|
43
74
|
schemas.similaritySchema,
|
|
44
|
-
|
|
45
|
-
const result = await generateEmbeddings([input.text1, input.text2], {
|
|
46
|
-
model: input.model,
|
|
47
|
-
inputType: 'document',
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
const vec1 = result.data[0].embedding;
|
|
51
|
-
const vec2 = result.data[1].embedding;
|
|
52
|
-
const similarity = cosineSimilarity(vec1, vec2);
|
|
53
|
-
|
|
54
|
-
return {
|
|
55
|
-
structuredContent: {
|
|
56
|
-
text1: input.text1.slice(0, 100) + (input.text1.length > 100 ? '...' : ''),
|
|
57
|
-
text2: input.text2.slice(0, 100) + (input.text2.length > 100 ? '...' : ''),
|
|
58
|
-
similarity,
|
|
59
|
-
model: input.model,
|
|
60
|
-
},
|
|
61
|
-
content: [{ type: 'text', text: `Similarity: ${similarity.toFixed(4)} (model: ${input.model})\nText 1: "${input.text1.slice(0, 80)}..."\nText 2: "${input.text2.slice(0, 80)}..."` }],
|
|
62
|
-
};
|
|
63
|
-
}
|
|
75
|
+
handleVaiSimilarity
|
|
64
76
|
);
|
|
65
77
|
}
|
|
66
78
|
|
|
67
|
-
module.exports = { registerEmbeddingTools };
|
|
79
|
+
module.exports = { registerEmbeddingTools, handleVaiEmbed, handleVaiSimilarity };
|
package/src/mcp/tools/ingest.js
CHANGED
|
@@ -7,83 +7,90 @@ const { loadProject } = require('../../lib/project');
|
|
|
7
7
|
const { getDefaultModel } = require('../../lib/catalog');
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
|
-
* @param {
|
|
12
|
-
* @
|
|
10
|
+
* Handler for vai_ingest: chunk, embed, and store a document.
|
|
11
|
+
* @param {object} input - Validated input matching ingestSchema
|
|
12
|
+
* @returns {Promise<{structuredContent: object, content: Array}>}
|
|
13
13
|
*/
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const { config: proj } = loadProject();
|
|
21
|
-
const db = input.db || proj.db;
|
|
22
|
-
const collName = input.collection || proj.collection;
|
|
23
|
-
if (!db) throw new Error('No database specified. Pass db parameter or configure via vai init.');
|
|
24
|
-
if (!collName) throw new Error('No collection specified. Pass collection parameter or configure via vai init.');
|
|
14
|
+
async function handleVaiIngest(input) {
|
|
15
|
+
const { config: proj } = loadProject();
|
|
16
|
+
const db = input.db || proj.db;
|
|
17
|
+
const collName = input.collection || proj.collection;
|
|
18
|
+
if (!db) throw new Error('No database specified. Pass db parameter or configure via vai init.');
|
|
19
|
+
if (!collName) throw new Error('No collection specified. Pass collection parameter or configure via vai init.');
|
|
25
20
|
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
const model = input.model || proj.model || getDefaultModel();
|
|
22
|
+
const start = Date.now();
|
|
28
23
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
24
|
+
// Step 1: Chunk the text
|
|
25
|
+
const chunks = chunk(input.text, {
|
|
26
|
+
strategy: input.chunkStrategy,
|
|
27
|
+
size: input.chunkSize,
|
|
28
|
+
});
|
|
34
29
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
30
|
+
if (chunks.length === 0) {
|
|
31
|
+
return {
|
|
32
|
+
structuredContent: { source: input.source || 'unknown', chunksCreated: 0, collection: collName },
|
|
33
|
+
content: [{ type: 'text', text: 'No chunks produced — text may be too short or empty.' }],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
41
36
|
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
// Step 2: Embed all chunks
|
|
38
|
+
const embedResult = await generateEmbeddings(chunks, {
|
|
39
|
+
model,
|
|
40
|
+
inputType: 'document',
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Step 3: Store in MongoDB
|
|
44
|
+
const { client, collection: coll } = await getMongoCollection(db, collName);
|
|
45
|
+
try {
|
|
46
|
+
const docs = chunks.map((text, i) => ({
|
|
47
|
+
text,
|
|
48
|
+
embedding: embedResult.data[i].embedding,
|
|
49
|
+
source: input.source || 'mcp-ingest',
|
|
50
|
+
metadata: {
|
|
51
|
+
...(input.metadata || {}),
|
|
52
|
+
ingestedAt: new Date().toISOString(),
|
|
53
|
+
chunkIndex: i,
|
|
54
|
+
totalChunks: chunks.length,
|
|
44
55
|
model,
|
|
45
|
-
|
|
46
|
-
}
|
|
56
|
+
chunkStrategy: input.chunkStrategy,
|
|
57
|
+
},
|
|
58
|
+
}));
|
|
47
59
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
try {
|
|
51
|
-
const docs = chunks.map((text, i) => ({
|
|
52
|
-
text,
|
|
53
|
-
embedding: embedResult.data[i].embedding,
|
|
54
|
-
source: input.source || 'mcp-ingest',
|
|
55
|
-
metadata: {
|
|
56
|
-
...(input.metadata || {}),
|
|
57
|
-
ingestedAt: new Date().toISOString(),
|
|
58
|
-
chunkIndex: i,
|
|
59
|
-
totalChunks: chunks.length,
|
|
60
|
-
model,
|
|
61
|
-
chunkStrategy: input.chunkStrategy,
|
|
62
|
-
},
|
|
63
|
-
}));
|
|
60
|
+
await coll.insertMany(docs);
|
|
61
|
+
const timeMs = Date.now() - start;
|
|
64
62
|
|
|
65
|
-
|
|
66
|
-
|
|
63
|
+
const structured = {
|
|
64
|
+
source: input.source || 'mcp-ingest',
|
|
65
|
+
chunksCreated: chunks.length,
|
|
66
|
+
collection: collName,
|
|
67
|
+
database: db,
|
|
68
|
+
model,
|
|
69
|
+
timeMs,
|
|
70
|
+
metadata: input.metadata || {},
|
|
71
|
+
};
|
|
67
72
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
};
|
|
73
|
+
return {
|
|
74
|
+
structuredContent: structured,
|
|
75
|
+
content: [{ type: 'text', text: `Ingested "${input.source || 'document'}" into ${db}.${collName}: ${chunks.length} chunks embedded with ${model} (${timeMs}ms)` }],
|
|
76
|
+
};
|
|
77
|
+
} finally {
|
|
78
|
+
await client.close();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
77
81
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Register the vai_ingest tool (write operation).
|
|
84
|
+
* @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
|
|
85
|
+
* @param {object} schemas
|
|
86
|
+
*/
|
|
87
|
+
function registerIngestTool(server, schemas) {
|
|
88
|
+
server.tool(
|
|
89
|
+
'vai_ingest',
|
|
90
|
+
'Add a document to a collection: chunks the text, embeds each chunk with Voyage AI, and stores them in MongoDB Atlas. Use when the user provides new content to add to the knowledge base.',
|
|
91
|
+
schemas.ingestSchema,
|
|
92
|
+
handleVaiIngest
|
|
86
93
|
);
|
|
87
94
|
}
|
|
88
95
|
|
|
89
|
-
module.exports = { registerIngestTool };
|
|
96
|
+
module.exports = { registerIngestTool, handleVaiIngest };
|
|
@@ -4,70 +4,82 @@ const { MODEL_CATALOG } = require('../../lib/catalog');
|
|
|
4
4
|
const { loadProject } = require('../../lib/project');
|
|
5
5
|
const { introspectCollections } = require('../../lib/workflow-utils');
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Handler for vai_collections: list collections with vector index info.
|
|
9
|
+
* @param {object} input - Validated input matching collectionsSchema
|
|
10
|
+
* @returns {Promise<{structuredContent: object, content: Array}>}
|
|
11
|
+
*/
|
|
12
|
+
async function handleVaiCollections(input) {
|
|
13
|
+
const { config: proj } = loadProject();
|
|
14
|
+
const dbName = input.db || proj.db;
|
|
15
|
+
if (!dbName) throw new Error('No database specified. Pass db parameter or configure via vai init.');
|
|
16
|
+
|
|
17
|
+
const collections = await introspectCollections(dbName);
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
structuredContent: { database: dbName, collections },
|
|
21
|
+
content: [{
|
|
22
|
+
type: 'text',
|
|
23
|
+
text: `Database: ${dbName}\n\n${collections.map(c =>
|
|
24
|
+
`• ${c.name} — ${c.documentCount} docs${c.hasVectorIndex ? ` ✓ vector index (${c.embeddingField}, ${c.dimensions}d)` : ''}`
|
|
25
|
+
).join('\n')}`,
|
|
26
|
+
}],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Handler for vai_models: list Voyage AI models.
|
|
32
|
+
* @param {object} input - Validated input matching modelsSchema
|
|
33
|
+
* @returns {Promise<{structuredContent: object, content: Array}>}
|
|
34
|
+
*/
|
|
35
|
+
async function handleVaiModels(input) {
|
|
36
|
+
let models = MODEL_CATALOG.filter(m => !m.legacy && !m.unreleased);
|
|
37
|
+
|
|
38
|
+
if (input.category !== 'all') {
|
|
39
|
+
models = models.filter(m => m.type === input.category);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const mapped = models.map(m => ({
|
|
43
|
+
id: m.name,
|
|
44
|
+
name: m.name,
|
|
45
|
+
type: m.type,
|
|
46
|
+
dimensions: m.dimensions,
|
|
47
|
+
maxTokens: m.maxTokens,
|
|
48
|
+
pricePerMToken: m.pricePerMToken,
|
|
49
|
+
...(m.architecture && { architecture: m.architecture }),
|
|
50
|
+
...(m.sharedSpace && { sharedSpace: m.sharedSpace }),
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
structuredContent: { category: input.category, models: mapped },
|
|
55
|
+
content: [{
|
|
56
|
+
type: 'text',
|
|
57
|
+
text: `Available ${input.category === 'all' ? '' : input.category + ' '}models:\n\n${mapped.map(m =>
|
|
58
|
+
`• ${m.name} (${m.type}) — ${m.dimensions}d, $${m.pricePerMToken}/M tokens`
|
|
59
|
+
).join('\n')}`,
|
|
60
|
+
}],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
7
64
|
/**
|
|
8
65
|
* Register management tools: vai_collections, vai_models
|
|
9
66
|
* @param {import('@modelcontextprotocol/sdk/server/mcp.js').McpServer} server
|
|
10
67
|
* @param {object} schemas
|
|
11
68
|
*/
|
|
12
69
|
function registerManagementTools(server, schemas) {
|
|
13
|
-
// vai_collections — list collections with vector index info
|
|
14
70
|
server.tool(
|
|
15
71
|
'vai_collections',
|
|
16
72
|
'List available MongoDB collections with document counts and vector index information. Use at the start of a task to discover which knowledge bases exist, or when the user mentions a topic and you need to find the right collection.',
|
|
17
73
|
schemas.collectionsSchema,
|
|
18
|
-
|
|
19
|
-
const { config: proj } = loadProject();
|
|
20
|
-
const dbName = input.db || proj.db;
|
|
21
|
-
if (!dbName) throw new Error('No database specified. Pass db parameter or configure via vai init.');
|
|
22
|
-
|
|
23
|
-
const collections = await introspectCollections(dbName);
|
|
24
|
-
|
|
25
|
-
return {
|
|
26
|
-
structuredContent: { database: dbName, collections },
|
|
27
|
-
content: [{
|
|
28
|
-
type: 'text',
|
|
29
|
-
text: `Database: ${dbName}\n\n${collections.map(c =>
|
|
30
|
-
`• ${c.name} — ${c.documentCount} docs${c.hasVectorIndex ? ` ✓ vector index (${c.embeddingField}, ${c.dimensions}d)` : ''}`
|
|
31
|
-
).join('\n')}`,
|
|
32
|
-
}],
|
|
33
|
-
};
|
|
34
|
-
}
|
|
74
|
+
handleVaiCollections
|
|
35
75
|
);
|
|
36
76
|
|
|
37
|
-
// vai_models — list Voyage AI models
|
|
38
77
|
server.tool(
|
|
39
78
|
'vai_models',
|
|
40
79
|
'List available Voyage AI models with capabilities, benchmarks, and pricing. Use when selecting a model for embedding or reranking, or when the user asks about model tradeoffs.',
|
|
41
80
|
schemas.modelsSchema,
|
|
42
|
-
|
|
43
|
-
let models = MODEL_CATALOG.filter(m => !m.legacy && !m.unreleased);
|
|
44
|
-
|
|
45
|
-
if (input.category !== 'all') {
|
|
46
|
-
models = models.filter(m => m.type === input.category);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const mapped = models.map(m => ({
|
|
50
|
-
id: m.name,
|
|
51
|
-
name: m.name,
|
|
52
|
-
type: m.type,
|
|
53
|
-
dimensions: m.dimensions,
|
|
54
|
-
maxTokens: m.maxTokens,
|
|
55
|
-
pricePerMToken: m.pricePerMToken,
|
|
56
|
-
...(m.architecture && { architecture: m.architecture }),
|
|
57
|
-
...(m.sharedSpace && { sharedSpace: m.sharedSpace }),
|
|
58
|
-
}));
|
|
59
|
-
|
|
60
|
-
return {
|
|
61
|
-
structuredContent: { category: input.category, models: mapped },
|
|
62
|
-
content: [{
|
|
63
|
-
type: 'text',
|
|
64
|
-
text: `Available ${input.category === 'all' ? '' : input.category + ' '}models:\n\n${mapped.map(m =>
|
|
65
|
-
`• ${m.name} (${m.type}) — ${m.dimensions}d, $${m.pricePerMToken}/M tokens`
|
|
66
|
-
).join('\n')}`,
|
|
67
|
-
}],
|
|
68
|
-
};
|
|
69
|
-
}
|
|
81
|
+
handleVaiModels
|
|
70
82
|
);
|
|
71
83
|
}
|
|
72
84
|
|
|
73
|
-
module.exports = { registerManagementTools, introspectCollections };
|
|
85
|
+
module.exports = { registerManagementTools, handleVaiCollections, handleVaiModels, introspectCollections };
|