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.
Files changed (41) hide show
  1. package/README.md +4 -4
  2. package/package.json +1 -1
  3. package/src/cli.js +2 -0
  4. package/src/commands/about.js +3 -3
  5. package/src/commands/code-search.js +751 -0
  6. package/src/commands/doctor.js +1 -1
  7. package/src/commands/embed.js +121 -2
  8. package/src/commands/index-workspace.js +9 -5
  9. package/src/commands/playground.js +65 -4
  10. package/src/commands/quickstart.js +4 -4
  11. package/src/commands/workflow.js +132 -65
  12. package/src/lib/api.js +31 -0
  13. package/src/lib/catalog.js +4 -2
  14. package/src/lib/code-search.js +315 -0
  15. package/src/lib/codegen.js +1 -1
  16. package/src/lib/explanations.js +3 -3
  17. package/src/lib/github.js +226 -0
  18. package/src/lib/input.js +92 -1
  19. package/src/lib/template-engine.js +154 -20
  20. package/src/lib/workflow-builder.js +753 -0
  21. package/src/lib/workflow-formatters.js +454 -0
  22. package/src/lib/workflow-input-cache.js +111 -0
  23. package/src/lib/workflow-scaffold.js +1 -1
  24. package/src/lib/workflow.js +124 -8
  25. package/src/mcp/schemas/index.js +142 -0
  26. package/src/mcp/server.js +17 -4
  27. package/src/mcp/tools/authoring.js +662 -0
  28. package/src/mcp/tools/code-search.js +620 -0
  29. package/src/mcp/tools/embedding.js +72 -3
  30. package/src/mcp/tools/ingest.js +2 -5
  31. package/src/mcp/tools/retrieval.js +2 -15
  32. package/src/mcp/tools/workspace.js +1 -12
  33. package/src/mcp/utils.js +20 -0
  34. package/src/playground/help/workflow-nodes.js +127 -2
  35. package/src/playground/index.html +2013 -139
  36. package/src/workflows/code-review.json +110 -0
  37. package/src/workflows/cost-analysis.json +5 -0
  38. package/src/workflows/tests/code-review.fresh-index.test.json +83 -0
  39. package/src/workflows/tests/code-review.happy-path.test.json +121 -0
  40. package/src/workflows/tests/code-review.no-question.test.json +70 -0
  41. package/src/workflows/tests/smart-ingest.duplicate-detected.test.json +2 -2
@@ -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
- return { results, resultCount: results.length };
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 { similarity, model: res.model };
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
- text += chunk;
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,
@@ -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
- if (!allKeys.includes(token)) {
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
+ }