voyageai-cli 1.30.1 → 1.30.3
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/README.md +4 -4
- package/package.json +1 -1
- package/src/cli.js +2 -0
- package/src/commands/about.js +3 -3
- package/src/commands/code-search.js +751 -0
- package/src/commands/doctor.js +1 -1
- package/src/commands/embed.js +121 -2
- package/src/commands/index-workspace.js +9 -5
- package/src/commands/playground.js +65 -4
- package/src/commands/quickstart.js +4 -4
- package/src/commands/workflow.js +132 -65
- package/src/lib/api.js +31 -0
- package/src/lib/catalog.js +4 -2
- package/src/lib/code-search.js +315 -0
- package/src/lib/codegen.js +1 -1
- package/src/lib/explanations.js +3 -3
- package/src/lib/github.js +226 -0
- package/src/lib/input.js +92 -1
- package/src/lib/template-engine.js +154 -20
- package/src/lib/workflow-builder.js +753 -0
- package/src/lib/workflow-formatters.js +454 -0
- package/src/lib/workflow-input-cache.js +111 -0
- package/src/lib/workflow-scaffold.js +1 -1
- package/src/lib/workflow.js +124 -8
- package/src/mcp/schemas/index.js +142 -0
- package/src/mcp/server.js +17 -4
- package/src/mcp/tools/authoring.js +662 -0
- package/src/mcp/tools/code-search.js +620 -0
- package/src/mcp/tools/embedding.js +72 -3
- package/src/mcp/tools/ingest.js +2 -5
- package/src/mcp/tools/retrieval.js +2 -15
- package/src/mcp/tools/workspace.js +1 -12
- package/src/mcp/utils.js +20 -0
- package/src/playground/help/workflow-nodes.js +127 -2
- package/src/playground/index.html +2013 -139
- package/src/workflows/code-review.json +110 -0
- package/src/workflows/cost-analysis.json +5 -0
- package/src/workflows/tests/code-review.fresh-index.test.json +83 -0
- package/src/workflows/tests/code-review.happy-path.test.json +121 -0
- package/src/workflows/tests/code-review.no-question.test.json +70 -0
- package/src/workflows/tests/smart-ingest.duplicate-detected.test.json +2 -2
package/src/lib/workflow.js
CHANGED
|
@@ -16,6 +16,7 @@ const {
|
|
|
16
16
|
const VAI_TOOLS = new Set([
|
|
17
17
|
'query', 'search', 'rerank', 'embed', 'similarity',
|
|
18
18
|
'ingest', 'collections', 'models', 'explain', 'estimate',
|
|
19
|
+
'code_index', 'code_search', 'code_query', 'code_find_similar', 'code_status',
|
|
19
20
|
]);
|
|
20
21
|
|
|
21
22
|
const CONTROL_FLOW_TOOLS = new Set(['merge', 'filter', 'transform', 'generate', 'conditional', 'loop', 'template']);
|
|
@@ -98,6 +99,25 @@ function validateWorkflow(definition, { mode = 'strict' } = {}) {
|
|
|
98
99
|
}
|
|
99
100
|
}
|
|
100
101
|
|
|
102
|
+
// Validate formatters section (optional)
|
|
103
|
+
if (definition.formatters) {
|
|
104
|
+
if (typeof definition.formatters !== 'object' || Array.isArray(definition.formatters)) {
|
|
105
|
+
addIssue('error', null, 'INVALID_FORMATTERS', '"formatters" must be a plain object');
|
|
106
|
+
} else {
|
|
107
|
+
const f = definition.formatters;
|
|
108
|
+
const validFormats = ['json', 'table', 'markdown', 'text', 'csv'];
|
|
109
|
+
if (f.default && !validFormats.includes(f.default)) {
|
|
110
|
+
addIssue('warning', null, 'INVALID_FORMATTER_DEFAULT', `formatters.default "${f.default}" is not a recognized format (${validFormats.join(', ')})`);
|
|
111
|
+
}
|
|
112
|
+
if (f.columns && !Array.isArray(f.columns)) {
|
|
113
|
+
addIssue('error', null, 'INVALID_FORMATTER_COLUMNS', 'formatters.columns must be an array of strings');
|
|
114
|
+
}
|
|
115
|
+
if (f.title && typeof f.title !== 'string') {
|
|
116
|
+
addIssue('error', null, 'INVALID_FORMATTER_TITLE', 'formatters.title must be a string');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
101
121
|
// Step-level validation
|
|
102
122
|
const stepIds = new Set();
|
|
103
123
|
const duplicateIds = new Set();
|
|
@@ -1238,6 +1258,9 @@ async function executeQuery(inputs, defaults) {
|
|
|
1238
1258
|
},
|
|
1239
1259
|
]).toArray();
|
|
1240
1260
|
|
|
1261
|
+
// Track embed usage
|
|
1262
|
+
const _usage = [{ op: 'embed', model: embRes.model, tokens: embRes.usage?.total_tokens || 0 }];
|
|
1263
|
+
|
|
1241
1264
|
// Rerank if requested and results exist
|
|
1242
1265
|
if (doRerank && results.length > 0) {
|
|
1243
1266
|
const documents = results.map(r => r.text || r.content || '');
|
|
@@ -1248,15 +1271,17 @@ async function executeQuery(inputs, defaults) {
|
|
|
1248
1271
|
documents,
|
|
1249
1272
|
});
|
|
1250
1273
|
|
|
1274
|
+
_usage.push({ op: 'rerank', model: rerankRes.model || inputs.rerankModel || DEFAULT_RERANK_MODEL, tokens: rerankRes.usage?.total_tokens || 0 });
|
|
1275
|
+
|
|
1251
1276
|
const reranked = (rerankRes.data || []).map(r => ({
|
|
1252
1277
|
...results[r.index],
|
|
1253
1278
|
score: r.relevance_score,
|
|
1254
1279
|
}));
|
|
1255
1280
|
|
|
1256
|
-
return { results: reranked, resultCount: reranked.length };
|
|
1281
|
+
return { results: reranked, resultCount: reranked.length, _usage };
|
|
1257
1282
|
}
|
|
1258
1283
|
|
|
1259
|
-
return { results, resultCount: results.length };
|
|
1284
|
+
return { results, resultCount: results.length, _usage };
|
|
1260
1285
|
} finally {
|
|
1261
1286
|
await client.close();
|
|
1262
1287
|
}
|
|
@@ -1295,7 +1320,8 @@ async function executeRerank(inputs) {
|
|
|
1295
1320
|
score: r.relevance_score,
|
|
1296
1321
|
}));
|
|
1297
1322
|
|
|
1298
|
-
|
|
1323
|
+
const _usage = [{ op: 'rerank', model: res.model || model, tokens: res.usage?.total_tokens || 0 }];
|
|
1324
|
+
return { results, resultCount: results.length, _usage };
|
|
1299
1325
|
}
|
|
1300
1326
|
|
|
1301
1327
|
/**
|
|
@@ -1319,6 +1345,7 @@ async function executeEmbed(inputs, defaults) {
|
|
|
1319
1345
|
embedding: res.data[0].embedding,
|
|
1320
1346
|
model: res.model,
|
|
1321
1347
|
dimensions: res.data[0].embedding.length,
|
|
1348
|
+
_usage: [{ op: 'embed', model: res.model, tokens: res.usage?.total_tokens || 0 }],
|
|
1322
1349
|
};
|
|
1323
1350
|
}
|
|
1324
1351
|
|
|
@@ -1340,7 +1367,11 @@ async function executeSimilarity(inputs, defaults) {
|
|
|
1340
1367
|
const res = await generateEmbeddings([text1, text2], opts);
|
|
1341
1368
|
const similarity = cosineSimilarity(res.data[0].embedding, res.data[1].embedding);
|
|
1342
1369
|
|
|
1343
|
-
return {
|
|
1370
|
+
return {
|
|
1371
|
+
similarity,
|
|
1372
|
+
model: res.model,
|
|
1373
|
+
_usage: [{ op: 'similarity', model: res.model, tokens: res.usage?.total_tokens || 0 }],
|
|
1374
|
+
};
|
|
1344
1375
|
}
|
|
1345
1376
|
|
|
1346
1377
|
/**
|
|
@@ -1421,6 +1452,7 @@ async function executeIngest(inputs, defaults) {
|
|
|
1421
1452
|
source,
|
|
1422
1453
|
model: embRes.model,
|
|
1423
1454
|
indexCreated,
|
|
1455
|
+
_usage: [{ op: 'ingest', model: embRes.model, tokens: embRes.usage?.total_tokens || 0 }],
|
|
1424
1456
|
};
|
|
1425
1457
|
} finally {
|
|
1426
1458
|
await client.close();
|
|
@@ -1565,17 +1597,72 @@ async function executeGenerate(inputs) {
|
|
|
1565
1597
|
|
|
1566
1598
|
// Collect streaming response
|
|
1567
1599
|
let text = '';
|
|
1600
|
+
let llmUsage = { inputTokens: 0, outputTokens: 0 };
|
|
1568
1601
|
for await (const chunk of provider.chat(messages, { stream: true })) {
|
|
1569
|
-
|
|
1602
|
+
if (chunk && typeof chunk === 'object' && chunk.__usage) {
|
|
1603
|
+
llmUsage = chunk.__usage;
|
|
1604
|
+
} else {
|
|
1605
|
+
text += chunk;
|
|
1606
|
+
}
|
|
1570
1607
|
}
|
|
1571
1608
|
|
|
1572
1609
|
return {
|
|
1573
1610
|
text,
|
|
1574
1611
|
model: provider.model,
|
|
1575
1612
|
provider: provider.name,
|
|
1613
|
+
_usage: [{ op: 'llm', model: provider.model, provider: provider.name, inputTokens: llmUsage.inputTokens, outputTokens: llmUsage.outputTokens }],
|
|
1576
1614
|
};
|
|
1577
1615
|
}
|
|
1578
1616
|
|
|
1617
|
+
// ════════════════════════════════════════════════════════════════════
|
|
1618
|
+
// Code Search Tool Executors
|
|
1619
|
+
// ════════════════════════════════════════════════════════════════════
|
|
1620
|
+
|
|
1621
|
+
/**
|
|
1622
|
+
* Execute a code_index step: index a local directory or GitHub repo.
|
|
1623
|
+
*/
|
|
1624
|
+
async function executeCodeIndex(inputs) {
|
|
1625
|
+
const { handleCodeIndex } = require('../mcp/tools/code-search');
|
|
1626
|
+
const result = await handleCodeIndex(inputs);
|
|
1627
|
+
return result.structuredContent;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
/**
|
|
1631
|
+
* Execute a code_search step: semantic search across an indexed codebase.
|
|
1632
|
+
*/
|
|
1633
|
+
async function executeCodeSearch(inputs) {
|
|
1634
|
+
const { handleCodeSearch } = require('../mcp/tools/code-search');
|
|
1635
|
+
const result = await handleCodeSearch(inputs);
|
|
1636
|
+
return result.structuredContent;
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
/**
|
|
1640
|
+
* Execute a code_query step: RAG query against indexed code.
|
|
1641
|
+
*/
|
|
1642
|
+
async function executeCodeQuery(inputs) {
|
|
1643
|
+
const { handleCodeQuery } = require('../mcp/tools/code-search');
|
|
1644
|
+
const result = await handleCodeQuery(inputs);
|
|
1645
|
+
return result.structuredContent;
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
/**
|
|
1649
|
+
* Execute a code_find_similar step: find code similar to a snippet.
|
|
1650
|
+
*/
|
|
1651
|
+
async function executeCodeFindSimilar(inputs) {
|
|
1652
|
+
const { handleCodeFindSimilar } = require('../mcp/tools/code-search');
|
|
1653
|
+
const result = await handleCodeFindSimilar(inputs);
|
|
1654
|
+
return result.structuredContent;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
/**
|
|
1658
|
+
* Execute a code_status step: check index health.
|
|
1659
|
+
*/
|
|
1660
|
+
async function executeCodeStatus(inputs) {
|
|
1661
|
+
const { handleCodeStatus } = require('../mcp/tools/code-search');
|
|
1662
|
+
const result = await handleCodeStatus(inputs);
|
|
1663
|
+
return result.structuredContent;
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1579
1666
|
// ════════════════════════════════════════════════════════════════════
|
|
1580
1667
|
// Step Dispatcher
|
|
1581
1668
|
// ════════════════════════════════════════════════════════════════════
|
|
@@ -1635,6 +1722,18 @@ async function executeStep(step, resolvedInputs, defaults, context) {
|
|
|
1635
1722
|
case 'estimate':
|
|
1636
1723
|
return executeEstimate(resolvedInputs);
|
|
1637
1724
|
|
|
1725
|
+
// Code search tools
|
|
1726
|
+
case 'code_index':
|
|
1727
|
+
return executeCodeIndex(resolvedInputs);
|
|
1728
|
+
case 'code_search':
|
|
1729
|
+
return executeCodeSearch(resolvedInputs);
|
|
1730
|
+
case 'code_query':
|
|
1731
|
+
return executeCodeQuery(resolvedInputs);
|
|
1732
|
+
case 'code_find_similar':
|
|
1733
|
+
return executeCodeFindSimilar(resolvedInputs);
|
|
1734
|
+
case 'code_status':
|
|
1735
|
+
return executeCodeStatus(resolvedInputs);
|
|
1736
|
+
|
|
1638
1737
|
default:
|
|
1639
1738
|
throw new Error(`Unknown tool: "${step.tool}"`);
|
|
1640
1739
|
}
|
|
@@ -1825,14 +1924,22 @@ async function executeWorkflow(definition, opts = {}) {
|
|
|
1825
1924
|
}
|
|
1826
1925
|
|
|
1827
1926
|
const durationMs = Date.now() - stepStart;
|
|
1828
|
-
context[stepId] = { output };
|
|
1829
1927
|
|
|
1928
|
+
// Pass full output (with _usage) to onStepComplete for cost tracking
|
|
1830
1929
|
if (opts.onStepComplete) opts.onStepComplete(stepId, output, durationMs);
|
|
1831
1930
|
|
|
1931
|
+
// Strip _usage from context so downstream steps don't receive it
|
|
1932
|
+
let cleanOutput = output;
|
|
1933
|
+
if (output && output._usage) {
|
|
1934
|
+
cleanOutput = { ...output };
|
|
1935
|
+
delete cleanOutput._usage;
|
|
1936
|
+
}
|
|
1937
|
+
context[stepId] = { output: cleanOutput };
|
|
1938
|
+
|
|
1832
1939
|
stepResults.push({
|
|
1833
1940
|
id: stepId,
|
|
1834
1941
|
tool: step.tool,
|
|
1835
|
-
output,
|
|
1942
|
+
output: cleanOutput,
|
|
1836
1943
|
durationMs,
|
|
1837
1944
|
});
|
|
1838
1945
|
} catch (err) {
|
|
@@ -1867,6 +1974,7 @@ async function executeWorkflow(definition, opts = {}) {
|
|
|
1867
1974
|
steps: stepResults,
|
|
1868
1975
|
totalTimeMs: Date.now() - startTime,
|
|
1869
1976
|
layers,
|
|
1977
|
+
formatters: definition.formatters || null,
|
|
1870
1978
|
};
|
|
1871
1979
|
}
|
|
1872
1980
|
|
|
@@ -1894,9 +2002,10 @@ function coerceInput(value, type) {
|
|
|
1894
2002
|
* prompt users for missing inputs before execution.
|
|
1895
2003
|
*
|
|
1896
2004
|
* @param {object} definition - Workflow definition with an `inputs` property
|
|
2005
|
+
* @param {object} [cachedInputs] - Previously cached input values (from last run)
|
|
1897
2006
|
* @returns {import('./wizard').Step[]}
|
|
1898
2007
|
*/
|
|
1899
|
-
function buildInputSteps(definition) {
|
|
2008
|
+
function buildInputSteps(definition, cachedInputs = {}) {
|
|
1900
2009
|
if (!definition.inputs) return [];
|
|
1901
2010
|
return Object.entries(definition.inputs).map(([key, spec]) => ({
|
|
1902
2011
|
id: key,
|
|
@@ -1905,6 +2014,10 @@ function buildInputSteps(definition) {
|
|
|
1905
2014
|
required: !!spec.required,
|
|
1906
2015
|
placeholder: spec.type === 'number' ? 'number' : (spec.type || 'string'),
|
|
1907
2016
|
defaultValue: spec.default !== undefined ? String(spec.default) : undefined,
|
|
2017
|
+
getDefault: () => {
|
|
2018
|
+
if (key in cachedInputs) return String(cachedInputs[key]);
|
|
2019
|
+
return spec.default !== undefined ? String(spec.default) : undefined;
|
|
2020
|
+
},
|
|
1908
2021
|
validate: (val) => {
|
|
1909
2022
|
if (spec.type === 'number' && val && isNaN(Number(val))) {
|
|
1910
2023
|
return 'Must be a number';
|
|
@@ -2063,6 +2176,9 @@ module.exports = {
|
|
|
2063
2176
|
executeIngest,
|
|
2064
2177
|
executeAggregate,
|
|
2065
2178
|
|
|
2179
|
+
// Dependency graph
|
|
2180
|
+
buildDependencyGraph,
|
|
2181
|
+
|
|
2066
2182
|
// Main execution
|
|
2067
2183
|
executeStep,
|
|
2068
2184
|
executeWorkflow,
|
package/src/mcp/schemas/index.js
CHANGED
|
@@ -124,6 +124,140 @@ const explainCodeSchema = {
|
|
|
124
124
|
model: z.string().optional().describe('Voyage AI embedding model'),
|
|
125
125
|
};
|
|
126
126
|
|
|
127
|
+
/** vai_code_index input schema */
|
|
128
|
+
const codeIndexSchema = {
|
|
129
|
+
source: z.string().min(1).describe(
|
|
130
|
+
'Local directory path or GitHub repo URL (e.g., "/path/to/project" or "https://github.com/org/repo")'
|
|
131
|
+
),
|
|
132
|
+
db: z.string().optional().describe('MongoDB database name. Default: "vai_code_search"'),
|
|
133
|
+
collection: z.string().optional().describe(
|
|
134
|
+
'Collection name. Auto-derived from project name if omitted.'
|
|
135
|
+
),
|
|
136
|
+
model: z.string().optional().describe(
|
|
137
|
+
'Embedding model. Default: auto-detected (voyage-code-3 for code, voyage-4-large for docs)'
|
|
138
|
+
),
|
|
139
|
+
branch: z.string().default('main').describe('Git branch for remote repos'),
|
|
140
|
+
maxFiles: z.number().int().min(1).max(10000).default(5000)
|
|
141
|
+
.describe('Maximum files to index'),
|
|
142
|
+
maxFileSize: z.number().int().min(1000).max(1000000).default(100000)
|
|
143
|
+
.describe('Maximum file size in bytes'),
|
|
144
|
+
chunkSize: z.number().int().min(100).max(4000).default(512)
|
|
145
|
+
.describe('Target chunk size in characters'),
|
|
146
|
+
chunkOverlap: z.number().int().min(0).max(500).default(50)
|
|
147
|
+
.describe('Overlap between chunks in characters'),
|
|
148
|
+
batchSize: z.number().int().min(1).max(50).default(20)
|
|
149
|
+
.describe('Files per embedding batch'),
|
|
150
|
+
refresh: z.boolean().default(false)
|
|
151
|
+
.describe('Incremental refresh: only re-index changed files'),
|
|
152
|
+
contentType: z.enum(['code', 'docs', 'config', 'all']).default('code')
|
|
153
|
+
.describe('Type of content to index'),
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/** vai_code_search input schema */
|
|
157
|
+
const codeSearchSchema = {
|
|
158
|
+
query: z.string().min(1).max(5000).describe(
|
|
159
|
+
'Natural language search query (e.g., "where do we handle auth timeouts")'
|
|
160
|
+
),
|
|
161
|
+
db: z.string().optional().describe('MongoDB database name'),
|
|
162
|
+
collection: z.string().optional().describe('Collection with indexed code'),
|
|
163
|
+
limit: z.number().int().min(1).max(50).default(10)
|
|
164
|
+
.describe('Maximum number of results'),
|
|
165
|
+
language: z.string().optional()
|
|
166
|
+
.describe('Filter by programming language (e.g., "js", "py", "go")'),
|
|
167
|
+
category: z.enum(['code', 'docs', 'config']).optional()
|
|
168
|
+
.describe('Filter by content category'),
|
|
169
|
+
rerank: z.boolean().default(true)
|
|
170
|
+
.describe('Rerank results with Voyage AI reranker for better relevance'),
|
|
171
|
+
rerankModel: z.enum(['rerank-2.5', 'rerank-2.5-lite']).default('rerank-2.5')
|
|
172
|
+
.describe('Reranking model'),
|
|
173
|
+
model: z.string().optional()
|
|
174
|
+
.describe('Embedding model for query. Default: voyage-code-3'),
|
|
175
|
+
filter: z.record(z.string(), z.unknown()).optional()
|
|
176
|
+
.describe('Additional MongoDB filter on metadata fields'),
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/** vai_code_query input schema */
|
|
180
|
+
const codeQuerySchema = {
|
|
181
|
+
query: z.string().min(1).max(5000).describe(
|
|
182
|
+
'Question about the codebase (e.g., "how does the auth middleware work")'
|
|
183
|
+
),
|
|
184
|
+
db: z.string().optional().describe('MongoDB database name'),
|
|
185
|
+
collection: z.string().optional().describe('Collection with indexed code'),
|
|
186
|
+
limit: z.number().int().min(1).max(20).default(5)
|
|
187
|
+
.describe('Maximum results (fewer, higher quality)'),
|
|
188
|
+
language: z.string().optional()
|
|
189
|
+
.describe('Filter by programming language'),
|
|
190
|
+
model: z.string().optional()
|
|
191
|
+
.describe('Embedding model. Default: voyage-code-3'),
|
|
192
|
+
filter: z.record(z.string(), z.unknown()).optional()
|
|
193
|
+
.describe('Additional MongoDB filter'),
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
/** vai_code_find_similar input schema */
|
|
197
|
+
const codeFindSimilarSchema = {
|
|
198
|
+
code: z.string().min(1).max(10000).describe(
|
|
199
|
+
'Code snippet to find similar implementations for'
|
|
200
|
+
),
|
|
201
|
+
db: z.string().optional().describe('MongoDB database name'),
|
|
202
|
+
collection: z.string().optional().describe('Collection with indexed code'),
|
|
203
|
+
limit: z.number().int().min(1).max(50).default(10)
|
|
204
|
+
.describe('Maximum results'),
|
|
205
|
+
language: z.string().optional()
|
|
206
|
+
.describe('Filter by programming language'),
|
|
207
|
+
model: z.string().optional()
|
|
208
|
+
.describe('Embedding model. Default: voyage-code-3'),
|
|
209
|
+
threshold: z.number().min(0).max(1).default(0.5)
|
|
210
|
+
.describe('Minimum similarity score (0-1)'),
|
|
211
|
+
filter: z.record(z.string(), z.unknown()).optional()
|
|
212
|
+
.describe('Additional MongoDB filter'),
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
/** vai_code_status input schema */
|
|
216
|
+
const codeStatusSchema = {
|
|
217
|
+
db: z.string().optional().describe('MongoDB database name'),
|
|
218
|
+
collection: z.string().optional().describe('Collection to check'),
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
/** vai_generate_workflow input schema */
|
|
222
|
+
const generateWorkflowSchema = {
|
|
223
|
+
description: z.string().min(1).max(500).describe('Natural language description of the workflow to generate'),
|
|
224
|
+
category: z.enum(['retrieval', 'analysis', 'ingestion', 'domain-specific', 'utility', 'integration']).optional()
|
|
225
|
+
.describe('Workflow category'),
|
|
226
|
+
tools: z.array(z.string()).optional()
|
|
227
|
+
.describe('Explicit list of tools to include (e.g., ["query", "rerank", "generate"]). If omitted, tools are inferred from the description.'),
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/** vai_multimodal_embed input schema */
|
|
231
|
+
const multimodalEmbedSchema = {
|
|
232
|
+
text: z.string().max(32000).optional().describe('Optional text content to embed alongside media'),
|
|
233
|
+
image_base64: z.string().optional().describe('Base64 data URL for an image (e.g., data:image/jpeg;base64,...)'),
|
|
234
|
+
video_base64: z.string().optional().describe('Base64 data URL for a video (e.g., data:video/mp4;base64,...)'),
|
|
235
|
+
model: z.string().default('voyage-multimodal-3.5').describe('Multimodal embedding model'),
|
|
236
|
+
inputType: z.enum(['document', 'query']).optional()
|
|
237
|
+
.describe('Whether this input is a document or a query (affects embedding)'),
|
|
238
|
+
outputDimension: z.number().int().optional().describe('Output dimensions (256, 512, 1024, or 2048)'),
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
/** vai_validate_workflow input schema */
|
|
242
|
+
const validateWorkflowSchema = {
|
|
243
|
+
workflow: z.object({
|
|
244
|
+
name: z.string().optional(),
|
|
245
|
+
description: z.string().optional(),
|
|
246
|
+
version: z.string().optional(),
|
|
247
|
+
inputs: z.record(z.string(), z.unknown()).optional(),
|
|
248
|
+
defaults: z.record(z.string(), z.unknown()).optional(),
|
|
249
|
+
steps: z.array(z.object({
|
|
250
|
+
id: z.string(),
|
|
251
|
+
tool: z.string(),
|
|
252
|
+
name: z.string().optional(),
|
|
253
|
+
inputs: z.record(z.string(), z.unknown()).optional(),
|
|
254
|
+
condition: z.string().optional(),
|
|
255
|
+
forEach: z.string().optional(),
|
|
256
|
+
})),
|
|
257
|
+
output: z.record(z.string(), z.unknown()).optional(),
|
|
258
|
+
}).describe('The workflow JSON definition to validate'),
|
|
259
|
+
};
|
|
260
|
+
|
|
127
261
|
module.exports = {
|
|
128
262
|
querySchema,
|
|
129
263
|
searchSchema,
|
|
@@ -139,4 +273,12 @@ module.exports = {
|
|
|
139
273
|
indexWorkspaceSchema,
|
|
140
274
|
searchCodeSchema,
|
|
141
275
|
explainCodeSchema,
|
|
276
|
+
codeIndexSchema,
|
|
277
|
+
codeSearchSchema,
|
|
278
|
+
codeQuerySchema,
|
|
279
|
+
codeFindSimilarSchema,
|
|
280
|
+
codeStatusSchema,
|
|
281
|
+
multimodalEmbedSchema,
|
|
282
|
+
generateWorkflowSchema,
|
|
283
|
+
validateWorkflowSchema,
|
|
142
284
|
};
|
package/src/mcp/server.js
CHANGED
|
@@ -9,6 +9,8 @@ const { registerManagementTools } = require('./tools/management');
|
|
|
9
9
|
const { registerUtilityTools } = require('./tools/utility');
|
|
10
10
|
const { registerIngestTool } = require('./tools/ingest');
|
|
11
11
|
const { registerWorkspaceTools } = require('./tools/workspace');
|
|
12
|
+
const { registerCodeSearchTools } = require('./tools/code-search');
|
|
13
|
+
const { registerAuthoringTools } = require('./tools/authoring');
|
|
12
14
|
|
|
13
15
|
const VERSION = require('../../package.json').version;
|
|
14
16
|
|
|
@@ -29,6 +31,8 @@ function createServer() {
|
|
|
29
31
|
registerUtilityTools(server, schemas);
|
|
30
32
|
registerIngestTool(server, schemas);
|
|
31
33
|
registerWorkspaceTools(server, schemas);
|
|
34
|
+
registerCodeSearchTools(server, schemas);
|
|
35
|
+
registerAuthoringTools(server, schemas);
|
|
32
36
|
|
|
33
37
|
return server;
|
|
34
38
|
}
|
|
@@ -68,7 +72,7 @@ async function runHttpServer({ port = 3100, host = '127.0.0.1', sse = false } =
|
|
|
68
72
|
const allKeys = envKey ? [...serverKeys, envKey] : serverKeys;
|
|
69
73
|
const requireAuth = allKeys.length > 0;
|
|
70
74
|
|
|
71
|
-
/** Bearer token authentication middleware */
|
|
75
|
+
/** Bearer token authentication middleware (timing-safe comparison) */
|
|
72
76
|
function authenticateRequest(req, res, next) {
|
|
73
77
|
if (!requireAuth) return next();
|
|
74
78
|
const authHeader = req.headers.authorization;
|
|
@@ -76,7 +80,13 @@ async function runHttpServer({ port = 3100, host = '127.0.0.1', sse = false } =
|
|
|
76
80
|
return res.status(401).json({ error: 'Missing or invalid Authorization header' });
|
|
77
81
|
}
|
|
78
82
|
const token = authHeader.slice(7);
|
|
79
|
-
|
|
83
|
+
const tokenBuf = Buffer.from(token);
|
|
84
|
+
const match = allKeys.some(key => {
|
|
85
|
+
const keyBuf = Buffer.from(key);
|
|
86
|
+
if (keyBuf.length !== tokenBuf.length) return false;
|
|
87
|
+
return crypto.timingSafeEqual(keyBuf, tokenBuf);
|
|
88
|
+
});
|
|
89
|
+
if (!match) {
|
|
80
90
|
return res.status(401).json({ error: 'Invalid API key' });
|
|
81
91
|
}
|
|
82
92
|
next();
|
|
@@ -95,7 +105,6 @@ async function runHttpServer({ port = 3100, host = '127.0.0.1', sse = false } =
|
|
|
95
105
|
|
|
96
106
|
// Check Voyage AI connectivity
|
|
97
107
|
try {
|
|
98
|
-
const { getConfigValue } = require('../lib/config');
|
|
99
108
|
const hasKey = !!(process.env.VOYAGE_API_KEY || getConfigValue('apiKey'));
|
|
100
109
|
health.voyageAi = hasKey ? 'configured' : 'not configured';
|
|
101
110
|
} catch {
|
|
@@ -104,7 +113,6 @@ async function runHttpServer({ port = 3100, host = '127.0.0.1', sse = false } =
|
|
|
104
113
|
|
|
105
114
|
// Check MongoDB connectivity
|
|
106
115
|
try {
|
|
107
|
-
const { getConfigValue } = require('../lib/config');
|
|
108
116
|
const hasUri = !!(process.env.MONGODB_URI || getConfigValue('mongodbUri'));
|
|
109
117
|
health.mongodb = hasUri ? 'configured' : 'not configured';
|
|
110
118
|
} catch {
|
|
@@ -175,3 +183,8 @@ function generateKey() {
|
|
|
175
183
|
}
|
|
176
184
|
|
|
177
185
|
module.exports = { createServer, runStdioServer, runHttpServer, generateKey };
|
|
186
|
+
|
|
187
|
+
// Allow direct execution: `node src/mcp/server.js`
|
|
188
|
+
if (require.main === module) {
|
|
189
|
+
runStdioServer();
|
|
190
|
+
}
|