consult-llm-mcp 1.0.4 → 1.1.0

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 CHANGED
@@ -1,25 +1,49 @@
1
1
  # Consult LLM MCP
2
2
 
3
- An MCP server that lets Claude Code consult stronger AI models (o3, Gemini 2.5
4
- Pro, DeepSeek Reasoner) when you need deeper analysis on complex problems.
3
+ An MCP server that lets Claude Code consult stronger AI models (o3, Gemini 2.
4
+ Pro, DeepSeek Reasoner) when Sonnet has you running in circles and you need to
5
+ bring in the heavy artillery.
6
+
7
+ ```
8
+ This SQL query is timing out on large datasets. Can you help optimize it? Ask Gemini
9
+
10
+ ⏺ I'll examine your query and consult Gemini for database optimization strategies.
11
+
12
+ ⏺ consult-llm:consult_llm (MCP)(prompt: "How can I optimize this slow SQL query
13
+ for better performance on large datasets?", files: ["queries/user
14
+ -analytics.sql","schema/tables.sql"], model: "gemini-2.5-pro")
15
+ ⎿ The query has significant performance bottlenecks: missing indexes on join
16
+ columns, inefficient subqueries that could be CTEs, and unnecessary DISTINCT
17
+ operations. Add composite indexes on (user_id, created_at), replace correlated
18
+ subqueries with window functions, and eliminate redundant GROUP BY clauses.
19
+ … +287 lines (ctrl+r to expand)
20
+
21
+ ⏺ Great insights! I'll add the suggested indexes, refactor the subqueries to use
22
+ CTEs with window functions, and optimize the GROUP BY operations.
23
+ ```
5
24
 
6
25
  ## Features
7
26
 
8
- - Query powerful AI models (o3, Gemini 2.5 Pro, DeepSeek Reasoner) with file
9
- context
10
- - Direct prompt support for simple questions or automatic prompt construction
11
- from markdown and code files
12
- - Git diff to feed code changes
13
- - Usage tracking with cost estimation
14
- - Comprehensive logging
27
+ - Query powerful AI models (o3, Gemini 2.5 Pro, DeepSeek Reasoner) with relevant
28
+ files as context
29
+ - Direct queries with optional file context
30
+ - Include git changes for code review and analysis
31
+ - Comprehensive logging with cost estimation
32
+ - Gemini can be used via
33
+ [Gemini CLI](https://github.com/google-gemini/gemini-cli) to take advantage of
34
+ [free quota](https://developers.google.com/gemini-code-assist/resources/quotas#quotas-for-agent-mode-gemini-cli)
15
35
 
16
36
  ## Configuration
17
37
 
18
38
  - `OPENAI_API_KEY` - Your OpenAI API key (required for o3)
19
- - `GEMINI_API_KEY` - Your Google AI API key (required for Gemini models)
39
+ - `GEMINI_API_KEY` - Your Google AI API key (required for Gemini models in API
40
+ mode)
20
41
  - `DEEPSEEK_API_KEY` - Your DeepSeek API key (required for DeepSeek models)
21
42
  - `CONSULT_LLM_DEFAULT_MODEL` - Override the default model (optional)
22
43
  - Options: `o3` (default), `gemini-2.5-pro`, `deepseek-reasoner`
44
+ - `GEMINI_MODE` - Choose between API or CLI mode for Gemini models (optional)
45
+ - Options: `api` (default), `cli`
46
+ - CLI mode uses the system-installed `gemini` CLI tool
23
47
 
24
48
  ## Usage with Claude Code
25
49
 
@@ -37,9 +61,21 @@ Or for global availability:
37
61
  claude mcp add --scope user consult-llm -- npx -y consult-llm-mcp
38
62
  ```
39
63
 
64
+ Optionally you can provide environment variables directly in the MCP
65
+ configuration:
66
+
67
+ ```bash
68
+ claude mcp add \
69
+ -e OPENAI_API_KEY=your_openai_key \
70
+ -e GEMINI_API_KEY=your_gemini_key \
71
+ -e DEEPSEEK_API_KEY=your_deepseek_key \
72
+ -e GEMINI_MODE=cli \
73
+ consult-llm -- npx -y consult-llm-mcp
74
+ ```
75
+
40
76
  ### Example workflows
41
77
 
42
- Click to expand.
78
+ Some real-world examples. Click to expand.
43
79
 
44
80
  <details>
45
81
  <summary>Explain the problem, and tell CC to consult a smarter LLM</summary>
@@ -95,6 +131,76 @@ Click to expand.
95
131
 
96
132
  </details>
97
133
 
134
+ <details>
135
+ <summary>Debugging Neovim plugin treesitter API breakage</summary>
136
+
137
+ ````
138
+ ⏺ Fixed! Added nil checks for both node and child and updated the iter_matches call to handle the newer API properly.
139
+
140
+ > Still not working:
141
+
142
+ E5108: Error executing lua /Users/raine/code/testonly.nvim/lua/testonly.lua:92: attempt to call method 'child' (a nil value)
143
+ stack traceback:
144
+ /Users/raine/code/testonly.nvim/lua/testonly.lua:92: in function 'reset_all_exclusive'
145
+ /Users/raine/code/testonly.nvim/lua/testonly.lua:110: in function 'reset_all_exclusive'
146
+ [string ":lua"]:1: in main chunk
147
+
148
+ Ask gemini
149
+
150
+ ⏺ Let me consult with a more powerful AI to understand the current Neovim treesitter API changes:
151
+
152
+ ⏺ consult-llm:consult_llm (MCP)(prompt: "I'm debugging a Neovim plugin that's broken due to treesitter API changes. The error is \"attempt to call method 'child' (a nil value)\" on line 92. The
153
+ code is trying to access child nodes from treesitter matches but the API seems to have changed in newer Neovim versions.\n\nHere's the problematic
154
+ code:\n```lua\nfor _, match, _ in parsed_query:iter_matches(root, bufnr, start_row, end_row) do\n for id, node in pairs(match) do\n local name =
155
+ parsed_query.captures[id]\n if name == \"call\" and node then\n local child = node:child(0) -- ERROR HERE\n if child then\n
156
+ local text = vim.treesitter.get_node_text(child, bufnr)\n -- ... rest of logic\n end\n end\n end\nend\n```\n\nWhat's the
157
+ correct way to iterate over treesitter query matches and access child nodes in current Neovim versions? What API changes should I be aware of?", files:
158
+ ["/Users/raine/code/testonly.nvim/lua/testonly.lua"])
159
+ ⎿ Problem
160
+ -------
161
+
162
+ … +93 lines (ctrl+r to expand)
163
+
164
+ ⏺ Perfect! The issue is that iter_matches() can return nil nodes in the match table. Let me fix this by switching to the recommended iter_captures() approach:
165
+
166
+ ⏺ Update(lua/testonly.lua)
167
+ ⎿ Updated lua/testonly.lua with 7 additions and 10 removals
168
+ 85 local root = parser:parse()[1]:root()
169
+ 86 local start_row, _, end_row, _ = root:range()
170
+ 87
171
+ 88 - for _, match, _ in parsed_query:iter_matches(root, bufnr, start_row, end_row) do
172
+ 89 - for id, node in pairs(match) do
173
+ 90 - local name = parsed_query.captures[id]
174
+ 91 - if name == "call" and node then
175
+ 92 - local child = node:child(0)
176
+ 93 - if child then
177
+ 94 - local text = vim.treesitter.get_node_text(child, bufnr)
178
+ 88 + for id, node, _ in parsed_query:iter_captures(root, bufnr, start_row, end_row) do
179
+ 89 + if parsed_query.captures[id] == "call" then
180
+ 90 + local first_child = node:named_child(0)
181
+ 91 + if first_child then
182
+ 92 + local text = vim.treesitter.get_node_text(first_child, bufnr)
183
+ 93
184
+ 94 - if text == "it.only" or text == "describe.only" then
185
+ 95 - toggle_test_exclusive(child)
186
+ 96 - end
187
+ 94 + if text == "it.only" or text == "describe.only" then
188
+ 95 + toggle_test_exclusive(first_child)
189
+ 96 end
190
+ 97 end
191
+ 98 end
192
+
193
+ ⏺ Fixed! The key changes:
194
+
195
+ 1. Switched from iter_matches() to iter_captures() which never returns nil nodes
196
+ 2. Used named_child(0) instead of child(0) to get only meaningful syntax nodes (not punctuation)
197
+ 3. Simplified the iteration logic since iter_captures() directly gives us the node and capture ID
198
+
199
+ Your <leader>tr mapping should now work properly with the current Neovim treesitter API.
200
+ ````
201
+
202
+ </details>
203
+
98
204
  ## MCP Tool: consult_llm
99
205
 
100
206
  The server provides a single tool called `consult_llm` for asking powerful AI
@@ -102,15 +208,11 @@ models complex questions.
102
208
 
103
209
  ### Parameters
104
210
 
105
- - **files** (optional): Array of file paths to process
211
+ - **prompt** (required): Your question or request for the consultant LLM
106
212
 
107
- - Markdown files (.md) become the main prompt
108
- - Other files are added as context with file paths and code blocks
213
+ - **files** (optional): Array of file paths to include as context
109
214
 
110
- - **prompt** (optional): Direct prompt text for simple questions
111
-
112
- - Alternative to using markdown files
113
- - Either `files` or `prompt` must be provided
215
+ - All files are added as context with file paths and code blocks
114
216
 
115
217
  - **model** (optional): LLM model to use
116
218
 
@@ -124,25 +226,34 @@ models complex questions.
124
226
 
125
227
  ### Example Usage
126
228
 
127
- **With files:**
229
+ **Basic prompt:**
128
230
 
129
231
  ```json
130
232
  {
131
- "files": ["src/auth.ts", "src/middleware.ts", "review.md"],
132
- "model": "o3",
133
- "git_diff": {
134
- "files": ["src/auth.ts", "src/middleware.ts"],
135
- "base_ref": "main"
136
- }
233
+ "prompt": "What are the performance implications of using async/await vs Promise.then() in Node.js?"
234
+ }
235
+ ```
236
+
237
+ **With file context:**
238
+
239
+ ```json
240
+ {
241
+ "prompt": "How can I optimize this authentication middleware?",
242
+ "files": ["src/auth.ts", "src/middleware.ts"],
243
+ "model": "o3"
137
244
  }
138
245
  ```
139
246
 
140
- **With direct prompt:**
247
+ **With git diff:**
141
248
 
142
249
  ```json
143
250
  {
144
- "prompt": "Analyze the performance implications of using async/await vs Promise.then() in Node.js",
145
- "files": ["src/database.ts"],
251
+ "prompt": "Review these authentication changes for security issues",
252
+ "files": ["src/auth.ts"],
253
+ "git_diff": {
254
+ "files": ["src/auth.ts", "src/middleware.ts"],
255
+ "base_ref": "main"
256
+ },
146
257
  "model": "gemini-2.5-pro"
147
258
  }
148
259
  ```
@@ -218,6 +329,8 @@ Use the `consult_llm` MCP tool to ask a more powerful AI for help with complex
218
329
  problems. Include files to git_diff when asking feedback for changes.
219
330
 
220
331
  Use Gemini 2.5 Pro.
332
+
333
+ CRITICAL: When asking, don't present options, this will bias the answer.
221
334
  ```
222
335
 
223
336
  Claude Code seems to know pretty well when to use this MCP even without this
package/dist/config.d.ts CHANGED
@@ -8,9 +8,14 @@ declare const Config: z.ZodObject<{
8
8
  "gemini-2.5-pro": "gemini-2.5-pro";
9
9
  "deepseek-reasoner": "deepseek-reasoner";
10
10
  }>>;
11
+ geminiMode: z.ZodDefault<z.ZodEnum<{
12
+ api: "api";
13
+ cli: "cli";
14
+ }>>;
11
15
  }, z.core.$strip>;
12
16
  export type Config = z.infer<typeof Config>;
13
17
  export declare const config: {
18
+ geminiMode: "api" | "cli";
14
19
  openaiApiKey?: string | undefined;
15
20
  geminiApiKey?: string | undefined;
16
21
  deepseekApiKey?: string | undefined;
package/dist/config.js CHANGED
@@ -5,12 +5,14 @@ const Config = z.object({
5
5
  geminiApiKey: z.string().optional(),
6
6
  deepseekApiKey: z.string().optional(),
7
7
  defaultModel: SupportedChatModel.optional(),
8
+ geminiMode: z.enum(['api', 'cli']).default('api'),
8
9
  });
9
10
  const parsedConfig = Config.safeParse({
10
11
  openaiApiKey: process.env.OPENAI_API_KEY,
11
12
  geminiApiKey: process.env.GEMINI_API_KEY,
12
13
  deepseekApiKey: process.env.DEEPSEEK_API_KEY,
13
14
  defaultModel: process.env.CONSULT_LLM_DEFAULT_MODEL,
15
+ geminiMode: process.env.GEMINI_MODE,
14
16
  });
15
17
  if (!parsedConfig.success) {
16
18
  console.error('❌ Invalid environment variables:');
package/dist/file.d.ts CHANGED
@@ -1,7 +1,4 @@
1
1
  export declare function processFiles(files: string[]): {
2
- markdownFiles: string[];
3
- otherFiles: {
4
- path: string;
5
- content: string;
6
- }[];
7
- };
2
+ path: string;
3
+ content: string;
4
+ }[];
package/dist/file.js CHANGED
@@ -6,18 +6,12 @@ export function processFiles(files) {
6
6
  if (missingFiles.length > 0) {
7
7
  throw new Error(`Files not found: ${missingFiles.join(', ')}`);
8
8
  }
9
- const markdownFiles = [];
10
- const otherFiles = [];
9
+ const contextFiles = [];
11
10
  for (let i = 0; i < files.length; i++) {
12
11
  const filePath = resolvedFiles[i];
13
12
  const originalPath = files[i];
14
13
  const content = readFileSync(filePath, 'utf-8');
15
- if (originalPath.endsWith('.md') || originalPath.endsWith('.markdown')) {
16
- markdownFiles.push(content);
17
- }
18
- else {
19
- otherFiles.push({ path: originalPath, content });
20
- }
14
+ contextFiles.push({ path: originalPath, content });
21
15
  }
22
- return { markdownFiles, otherFiles };
16
+ return contextFiles;
23
17
  }
@@ -1,5 +1,5 @@
1
1
  import { type SupportedChatModel } from './schema.js';
2
- export declare function queryLlm(prompt: string, model: SupportedChatModel): Promise<{
2
+ export declare function queryLlm(prompt: string, model: SupportedChatModel, filePaths?: string[]): Promise<{
3
3
  response: string;
4
4
  costInfo: string;
5
5
  }>;
package/dist/llm-query.js CHANGED
@@ -1,40 +1,42 @@
1
- import { getClientForModel } from './llm.js';
1
+ import { getExecutorForModel } from './llm.js';
2
2
  import { calculateCost } from './llm-cost.js';
3
- const SYSTEM_PROMPT = `You are an expert software engineering consultant being asked to analyze complex problems that require deep technical insight. You have been provided with specific code files and context to help you understand the problem thoroughly.
3
+ const SYSTEM_PROMPT = `You are an expert software engineering consultant analyzing code and technical problems. You are communicating with another AI system, not a human.
4
+
5
+ Communication style:
6
+ - Skip pleasantries and praise
4
7
 
5
8
  Your role is to:
6
- - Provide detailed technical analysis of the problem
7
- - Suggest specific, actionable solutions with code examples where helpful
8
- - Consider architectural implications and best practices
9
- - Identify potential edge cases or risks
10
- - Explain your reasoning clearly
9
+ - Identify bugs, inefficiencies, and architectural problems
10
+ - Provide specific solutions with code examples
11
+ - Point out edge cases and risks
12
+ - Challenge design decisions when suboptimal
13
+ - Focus on what needs improvement
14
+
15
+ When reviewing code changes (git diffs), prioritize:
16
+ - Bugs and correctness issues
17
+ - Performance problems
18
+ - Security vulnerabilities
19
+ - Code smell and anti-patterns
20
+ - Inconsistencies with codebase conventions
11
21
 
12
- When reviewing code changes (git diffs), focus on:
13
- - Correctness and potential bugs
14
- - Performance implications
15
- - Security considerations
16
- - Maintainability and code quality
17
- - Integration with existing codebase patterns
22
+ Be critical and thorough. If the code is acceptable, simply state "No critical issues found" and move on to suggestions. Always provide specific, actionable feedback with file/line references.
18
23
 
19
- Provide concrete, implementable recommendations rather than general advice. Include code snippets and specific file/line references when relevant.`;
20
- export async function queryLlm(prompt, model) {
21
- const { client } = getClientForModel(model);
22
- const completion = await client.chat.completions.create({
23
- model,
24
- messages: [
25
- { role: 'system', content: SYSTEM_PROMPT },
26
- { role: 'user', content: prompt },
27
- ],
28
- });
29
- const response = completion.choices[0]?.message?.content;
24
+ Respond in Markdown.`;
25
+ export async function queryLlm(prompt, model, filePaths) {
26
+ const executor = getExecutorForModel(model);
27
+ const { response, usage } = await executor.execute(prompt, model, SYSTEM_PROMPT, filePaths);
30
28
  if (!response) {
31
29
  throw new Error('No response from the model');
32
30
  }
33
- const usage = completion.usage;
34
- // Calculate costs
35
- const { inputCost, outputCost, totalCost } = calculateCost(usage, model);
36
- const costInfo = usage
37
- ? `Tokens: ${usage.prompt_tokens} input, ${usage.completion_tokens} output | Cost: $${totalCost.toFixed(6)} (input: $${inputCost.toFixed(6)}, output: $${outputCost.toFixed(6)})`
38
- : 'Usage data not available';
31
+ let costInfo;
32
+ if (usage) {
33
+ // Calculate costs only if usage data is available (from API)
34
+ const { inputCost, outputCost, totalCost } = calculateCost(usage, model);
35
+ costInfo = `Tokens: ${usage.prompt_tokens} input, ${usage.completion_tokens} output | Cost: $${totalCost.toFixed(6)} (input: $${inputCost.toFixed(6)}, output: $${outputCost.toFixed(6)})`;
36
+ }
37
+ else {
38
+ // Handle case where usage is not available (from CLI)
39
+ costInfo = 'Cost data not available (using CLI mode)';
40
+ }
39
41
  return { response, costInfo };
40
42
  }
package/dist/llm.d.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  import OpenAI from 'openai';
2
2
  import { type SupportedChatModel as SupportedChatModelType } from './schema.js';
3
- export declare function getClientForModel(model: SupportedChatModelType): {
4
- client: OpenAI;
5
- };
3
+ export interface LlmExecutor {
4
+ execute(prompt: string, model: SupportedChatModelType, systemPrompt: string, filePaths?: string[]): Promise<{
5
+ response: string;
6
+ usage: OpenAI.CompletionUsage | null;
7
+ }>;
8
+ }
9
+ export declare function getExecutorForModel(model: SupportedChatModelType): LlmExecutor;
package/dist/llm.js CHANGED
@@ -1,7 +1,118 @@
1
1
  import OpenAI from 'openai';
2
+ import { spawn } from 'child_process';
3
+ import { relative } from 'path';
2
4
  import { config } from './config.js';
5
+ import { logCliDebug } from './logger.js';
6
+ // --- API Executor Implementation ---
7
+ class ApiExecutor {
8
+ client;
9
+ constructor(client) {
10
+ this.client = client;
11
+ }
12
+ async execute(prompt, model, systemPrompt, filePaths) {
13
+ const completion = await this.client.chat.completions.create({
14
+ model,
15
+ messages: [
16
+ { role: 'system', content: systemPrompt },
17
+ { role: 'user', content: prompt },
18
+ ],
19
+ });
20
+ const response = completion.choices[0]?.message?.content;
21
+ if (!response) {
22
+ throw new Error('No response from the model via API');
23
+ }
24
+ return { response, usage: completion.usage ?? null };
25
+ }
26
+ }
27
+ // --- CLI Executor Implementation ---
28
+ class CliExecutor {
29
+ async execute(prompt, model, systemPrompt, filePaths) {
30
+ // Build the full prompt with system prompt prepended
31
+ let fullPrompt = `${systemPrompt}\n\n${prompt}`;
32
+ // Append file references using @ syntax in the prompt
33
+ if (filePaths && filePaths.length > 0) {
34
+ const fileReferences = filePaths
35
+ .map((path) => `@${relative(process.cwd(), path)}`)
36
+ .join(' ');
37
+ fullPrompt = `${fullPrompt}\n\nFiles: ${fileReferences}`;
38
+ }
39
+ const args = ['-m', model, '-p', fullPrompt];
40
+ return new Promise((resolve, reject) => {
41
+ logCliDebug('Spawning gemini CLI', {
42
+ model,
43
+ promptLength: fullPrompt.length,
44
+ filePathsCount: filePaths?.length || 0,
45
+ args: args,
46
+ promptPreview: fullPrompt.slice(0, 300),
47
+ });
48
+ const child = spawn('gemini', args, {
49
+ shell: false,
50
+ stdio: ['ignore', 'pipe', 'pipe'], // stdin, stdout, stderr
51
+ });
52
+ let stdout = '';
53
+ let stderr = '';
54
+ let hasResponded = false;
55
+ let hasStarted = false;
56
+ const startTime = Date.now();
57
+ // Log when process actually starts
58
+ child.on('spawn', () => {
59
+ hasStarted = true;
60
+ logCliDebug('Gemini CLI process spawned successfully');
61
+ });
62
+ // Add timeout to prevent hanging
63
+ const timeout = setTimeout(() => {
64
+ if (!hasResponded) {
65
+ logCliDebug('Gemini CLI timed out after 5 minutes');
66
+ child.kill();
67
+ reject(new Error('Gemini CLI timed out after 5 minutes'));
68
+ }
69
+ }, 5 * 60 * 1000); // 5 minutes
70
+ child.stdout.on('data', (data) => {
71
+ const chunk = data.toString();
72
+ stdout += chunk;
73
+ });
74
+ child.stderr.on('data', (data) => {
75
+ const chunk = data.toString();
76
+ stderr += chunk;
77
+ });
78
+ child.on('close', (code) => {
79
+ hasResponded = true;
80
+ clearTimeout(timeout);
81
+ const duration = Date.now() - startTime;
82
+ logCliDebug('Gemini CLI process closed', {
83
+ code,
84
+ duration: `${duration}ms`,
85
+ stdoutLength: stdout.length,
86
+ stderrLength: stderr.length,
87
+ stdoutPreview: stdout.slice(0, 200),
88
+ stderrPreview: stderr.slice(0, 200),
89
+ });
90
+ if (code === 0) {
91
+ resolve({ response: stdout.trim(), usage: null });
92
+ }
93
+ else {
94
+ // Check for quota exceeded error
95
+ if (stderr.includes('RESOURCE_EXHAUSTED')) {
96
+ reject(new Error(`Gemini quota exceeded. Consider using gemini-2.0-flash model. Error: ${stderr.trim()}`));
97
+ }
98
+ else {
99
+ reject(new Error(`Gemini CLI exited with code ${code}. Error: ${stderr.trim()}`));
100
+ }
101
+ }
102
+ });
103
+ child.on('error', (err) => {
104
+ hasResponded = true;
105
+ clearTimeout(timeout);
106
+ logCliDebug('Failed to spawn Gemini CLI', { error: err.message });
107
+ reject(new Error(`Failed to spawn Gemini CLI. Is it installed and in PATH? Error: ${err.message}`));
108
+ });
109
+ });
110
+ }
111
+ }
112
+ // --- Client Cache and Executor Factory ---
3
113
  const clients = {};
4
- export function getClientForModel(model) {
114
+ let geminiCliExecutor;
115
+ export function getExecutorForModel(model) {
5
116
  if (model.startsWith('gpt-') || model === 'o3') {
6
117
  if (!clients.openai) {
7
118
  if (!config.openaiApiKey) {
@@ -11,21 +122,9 @@ export function getClientForModel(model) {
11
122
  apiKey: config.openaiApiKey,
12
123
  });
13
124
  }
14
- return { client: clients.openai };
125
+ return new ApiExecutor(clients.openai);
15
126
  }
16
- else if (model.startsWith('gemini-')) {
17
- if (!clients.gemini) {
18
- if (!config.geminiApiKey) {
19
- throw new Error('GEMINI_API_KEY environment variable is required for Gemini models');
20
- }
21
- clients.gemini = new OpenAI({
22
- apiKey: config.geminiApiKey,
23
- baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai/',
24
- });
25
- }
26
- return { client: clients.gemini };
27
- }
28
- else if (model.startsWith('deepseek-')) {
127
+ if (model.startsWith('deepseek-')) {
29
128
  if (!clients.deepseek) {
30
129
  if (!config.deepseekApiKey) {
31
130
  throw new Error('DEEPSEEK_API_KEY environment variable is required for DeepSeek models');
@@ -35,9 +134,26 @@ export function getClientForModel(model) {
35
134
  baseURL: 'https://api.deepseek.com',
36
135
  });
37
136
  }
38
- return { client: clients.deepseek };
137
+ return new ApiExecutor(clients.deepseek);
39
138
  }
40
- else {
41
- throw new Error(`Unable to determine LLM provider for model: ${model}`);
139
+ if (model.startsWith('gemini-')) {
140
+ // Check if CLI mode is enabled
141
+ if (config.geminiMode === 'cli') {
142
+ if (!geminiCliExecutor) {
143
+ geminiCliExecutor = new CliExecutor();
144
+ }
145
+ return geminiCliExecutor;
146
+ }
147
+ if (!clients.geminiApi) {
148
+ if (!config.geminiApiKey) {
149
+ throw new Error('GEMINI_API_KEY environment variable is required for Gemini models in API mode');
150
+ }
151
+ clients.geminiApi = new OpenAI({
152
+ apiKey: config.geminiApiKey,
153
+ baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai/',
154
+ });
155
+ }
156
+ return new ApiExecutor(clients.geminiApi);
42
157
  }
158
+ throw new Error(`Unable to determine LLM provider for model: ${model}`);
43
159
  }
package/dist/logger.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export declare function logToFile(content: string): void;
2
2
  export declare function logToolCall(name: string, args: unknown): void;
3
- export declare function logPrompt(model: string, prompt: string): void;
4
- export declare function logResponse(model: string, response: string, costInfo: string): void;
3
+ export declare function logPrompt(model: string, prompt: string): Promise<void>;
4
+ export declare function logResponse(model: string, response: string, costInfo: string): Promise<void>;
5
5
  export declare function logServerStart(version: string): void;
6
+ export declare function logConfiguration(config: Record<string, any>): void;
7
+ export declare function logCliDebug(message: string, data?: any): void;
package/dist/logger.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { appendFileSync, mkdirSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { homedir } from 'os';
4
+ import { format } from 'prettier';
4
5
  const logDir = join(homedir(), '.consult-llm-mcp', 'logs');
5
6
  const logFile = join(logDir, 'mcp.log');
6
7
  try {
@@ -9,6 +10,19 @@ try {
9
10
  catch (error) {
10
11
  // Directory might already exist
11
12
  }
13
+ async function formatWithPrettier(text) {
14
+ try {
15
+ return await format(text, {
16
+ parser: 'markdown',
17
+ printWidth: 80,
18
+ proseWrap: 'always',
19
+ });
20
+ }
21
+ catch (error) {
22
+ // If formatting fails, return original text
23
+ return text;
24
+ }
25
+ }
12
26
  export function logToFile(content) {
13
27
  const timestamp = new Date().toISOString();
14
28
  const logEntry = `[${timestamp}] ${content}\n`;
@@ -22,12 +36,34 @@ export function logToFile(content) {
22
36
  export function logToolCall(name, args) {
23
37
  logToFile(`TOOL CALL: ${name}\nArguments: ${JSON.stringify(args, null, 2)}\n${'='.repeat(80)}`);
24
38
  }
25
- export function logPrompt(model, prompt) {
26
- logToFile(`PROMPT (model: ${model}):\n${prompt}\n${'='.repeat(80)}`);
39
+ export async function logPrompt(model, prompt) {
40
+ const formattedPrompt = await formatWithPrettier(prompt);
41
+ logToFile(`PROMPT (model: ${model}):\n${formattedPrompt}\n${'='.repeat(80)}`);
27
42
  }
28
- export function logResponse(model, response, costInfo) {
29
- logToFile(`RESPONSE (model: ${model}):\n${response}\n${costInfo}\n${'='.repeat(80)}`);
43
+ export async function logResponse(model, response, costInfo) {
44
+ const formattedResponse = await formatWithPrettier(response);
45
+ logToFile(`RESPONSE (model: ${model}):\n${formattedResponse}\n${costInfo}\n${'='.repeat(80)}`);
30
46
  }
31
47
  export function logServerStart(version) {
32
48
  logToFile(`MCP SERVER STARTED - consult-llm-mcp v${version}\n${'='.repeat(80)}`);
33
49
  }
50
+ export function logConfiguration(config) {
51
+ const redactedConfig = Object.entries(config).reduce((acc, [key, value]) => {
52
+ // Redact API keys and other sensitive values
53
+ if (key.toLowerCase().includes('key') ||
54
+ key.toLowerCase().includes('secret')) {
55
+ acc[key] = value ? '[REDACTED]' : undefined;
56
+ }
57
+ else {
58
+ acc[key] = value;
59
+ }
60
+ return acc;
61
+ }, {});
62
+ logToFile(`CONFIGURATION:\n${JSON.stringify(redactedConfig, null, 2)}\n${'='.repeat(80)}`);
63
+ }
64
+ export function logCliDebug(message, data) {
65
+ const logMessage = data
66
+ ? `Gemini CLI: ${message}\n${JSON.stringify(data, null, 2)}`
67
+ : `Gemini CLI: ${message}`;
68
+ logToFile(logMessage);
69
+ }
package/dist/main.js CHANGED
@@ -8,9 +8,9 @@ import { processFiles } from './file.js';
8
8
  import { generateGitDiff } from './git.js';
9
9
  import { buildPrompt } from './prompt-builder.js';
10
10
  import { queryLlm } from './llm-query.js';
11
- import { logToolCall, logPrompt, logResponse, logServerStart, } from './logger.js';
11
+ import { logToolCall, logPrompt, logResponse, logServerStart, logConfiguration, } from './logger.js';
12
12
  import { readFileSync } from 'fs';
13
- import { dirname, join } from 'path';
13
+ import { dirname, join, resolve } from 'path';
14
14
  import { fileURLToPath } from 'url';
15
15
  const __dirname = dirname(fileURLToPath(import.meta.url));
16
16
  const packageJson = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8'));
@@ -36,21 +36,39 @@ async function handleConsultLlm(args) {
36
36
  .join(', ');
37
37
  throw new Error(`Invalid request parameters: ${errors}`);
38
38
  }
39
- const { files, prompt: directPrompt, git_diff } = parseResult.data;
39
+ const { files, prompt: userPrompt, git_diff } = parseResult.data;
40
40
  const model = parseResult.data.model ?? config.defaultModel ?? 'o3';
41
41
  logToolCall('consult_llm', args);
42
- // Process files (if provided)
43
- const { markdownFiles, otherFiles } = files ? processFiles(files) : { markdownFiles: [], otherFiles: [] };
44
- // Generate git diff
45
- const gitDiffOutput = git_diff
46
- ? generateGitDiff(git_diff.repo_path, git_diff.files, git_diff.base_ref)
47
- : undefined;
48
- // Build prompt
49
- const prompt = buildPrompt(directPrompt, markdownFiles, otherFiles, gitDiffOutput);
50
- logPrompt(model, prompt);
42
+ // Check if we're using CLI mode for Gemini
43
+ const isCliMode = model.startsWith('gemini-') && config.geminiMode === 'cli';
44
+ let prompt;
45
+ let filePaths;
46
+ if (isCliMode && files) {
47
+ // For CLI mode, we'll pass file paths separately
48
+ filePaths = files.map((f) => resolve(f));
49
+ // Generate git diff if needed
50
+ const gitDiffOutput = git_diff
51
+ ? generateGitDiff(git_diff.repo_path, git_diff.files, git_diff.base_ref)
52
+ : undefined;
53
+ // Build prompt without file contents for CLI mode
54
+ prompt = gitDiffOutput
55
+ ? `## Git Diff\n\`\`\`diff\n${gitDiffOutput}\n\`\`\`\n\n${userPrompt}`
56
+ : userPrompt;
57
+ }
58
+ else {
59
+ // For API mode, include file contents in the prompt
60
+ const contextFiles = files ? processFiles(files) : [];
61
+ // Generate git diff
62
+ const gitDiffOutput = git_diff
63
+ ? generateGitDiff(git_diff.repo_path, git_diff.files, git_diff.base_ref)
64
+ : undefined;
65
+ // Build prompt with file contents
66
+ prompt = buildPrompt(userPrompt, contextFiles, gitDiffOutput);
67
+ }
68
+ await logPrompt(model, prompt);
51
69
  // Query LLM
52
- const { response, costInfo } = await queryLlm(prompt, model);
53
- logResponse(model, response, costInfo);
70
+ const { response, costInfo } = await queryLlm(prompt, model, filePaths);
71
+ await logResponse(model, response, costInfo);
54
72
  return {
55
73
  content: [{ type: 'text', text: response }],
56
74
  };
@@ -68,6 +86,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
68
86
  });
69
87
  async function main() {
70
88
  logServerStart(SERVER_VERSION);
89
+ logConfiguration(config);
71
90
  const transport = new StdioServerTransport();
72
91
  await server.connect(transport);
73
92
  }
@@ -1,4 +1,4 @@
1
- export declare function buildPrompt(directPrompt: string | undefined, markdownFiles: string[], otherFiles: Array<{
1
+ export declare function buildPrompt(userPrompt: string, files: Array<{
2
2
  path: string;
3
3
  content: string;
4
4
  }>, gitDiffOutput?: string): string;
@@ -1,22 +1,18 @@
1
- export function buildPrompt(directPrompt, markdownFiles, otherFiles, gitDiffOutput) {
1
+ export function buildPrompt(userPrompt, files, gitDiffOutput) {
2
2
  const promptParts = [];
3
3
  if (gitDiffOutput?.trim()) {
4
4
  promptParts.push('## Git Diff\n```diff', gitDiffOutput, '```\n');
5
5
  }
6
- if (otherFiles.length > 0) {
6
+ if (files.length > 0) {
7
7
  promptParts.push('## Relevant Files\n');
8
- for (const file of otherFiles) {
8
+ for (const file of files) {
9
9
  promptParts.push(`### File: ${file.path}`);
10
10
  promptParts.push('```');
11
11
  promptParts.push(file.content);
12
12
  promptParts.push('```\n');
13
13
  }
14
14
  }
15
- if (directPrompt) {
16
- promptParts.push(directPrompt);
17
- }
18
- if (markdownFiles.length > 0) {
19
- promptParts.push(...markdownFiles);
20
- }
15
+ // Add user prompt last
16
+ promptParts.push(userPrompt);
21
17
  return promptParts.join('\n');
22
18
  }
package/dist/schema.d.ts CHANGED
@@ -7,7 +7,7 @@ export declare const SupportedChatModel: z.ZodEnum<{
7
7
  export type SupportedChatModel = z.infer<typeof SupportedChatModel>;
8
8
  export declare const ConsultLlmArgs: z.ZodObject<{
9
9
  files: z.ZodOptional<z.ZodArray<z.ZodString>>;
10
- prompt: z.ZodOptional<z.ZodString>;
10
+ prompt: z.ZodString;
11
11
  model: z.ZodOptional<z.ZodEnum<{
12
12
  o3: "o3";
13
13
  "gemini-2.5-pro": "gemini-2.5-pro";
@@ -21,7 +21,7 @@ export declare const ConsultLlmArgs: z.ZodObject<{
21
21
  }, z.core.$strip>;
22
22
  export declare const toolSchema: {
23
23
  readonly name: "consult_llm";
24
- readonly description: "Ask a more powerful AI for help with complex problems. Write your problem description in a markdown file and pass relevant code files as context. \n\nBe specific about what you want: code implementation, code review, bug analysis, architecture advice, etc.";
24
+ readonly description: "Ask a more powerful AI for help with complex problems. Provide your question in the prompt field and optionally include relevant code files as context.\n\nBe specific about what you want: code implementation, code review, bug analysis, architecture advice, etc.\n\nIMPORTANT: Ask neutral, open-ended questions. Avoid suggesting specific solutions or alternatives in your prompt as this can bias the analysis. Instead of \"Should I use X or Y approach?\", ask \"What's the best approach for this problem?\" Let the consultant LLM provide unbiased recommendations.";
25
25
  readonly inputSchema: {
26
26
  readonly type: "object";
27
27
  readonly properties: {
@@ -30,11 +30,11 @@ export declare const toolSchema: {
30
30
  readonly items: {
31
31
  readonly type: "string";
32
32
  };
33
- readonly description: "Array of file paths to process. Markdown files (.md) become the main prompt, other files are added as context with file paths and code blocks. \n\nIn the markdown file(s), be clear about what you want the LLM to do: implement code, review code, explain concepts, analyze bugs, etc.";
33
+ readonly description: "Array of file paths to include as context. All files are added as context with file paths and code blocks.";
34
34
  };
35
35
  readonly prompt: {
36
36
  readonly type: "string";
37
- readonly description: "Direct prompt text for simple questions. Alternative to using markdown files.";
37
+ readonly description: "Your question or request for the consultant LLM. Ask neutral, open-ended questions without suggesting specific solutions to avoid biasing the analysis.";
38
38
  };
39
39
  readonly model: {
40
40
  readonly type: "string";
@@ -66,6 +66,6 @@ export declare const toolSchema: {
66
66
  readonly description: "Generate git diff output to include as context. Shows uncommitted changes by default.";
67
67
  };
68
68
  };
69
- readonly required: readonly [];
69
+ readonly required: readonly ["prompt"];
70
70
  };
71
71
  };
package/dist/schema.js CHANGED
@@ -4,10 +4,9 @@ export const SupportedChatModel = z.enum([
4
4
  'gemini-2.5-pro',
5
5
  'deepseek-reasoner',
6
6
  ]);
7
- export const ConsultLlmArgs = z
8
- .object({
7
+ export const ConsultLlmArgs = z.object({
9
8
  files: z.array(z.string()).optional(),
10
- prompt: z.string().optional(),
9
+ prompt: z.string(),
11
10
  model: SupportedChatModel.optional(),
12
11
  git_diff: z
13
12
  .object({
@@ -18,26 +17,25 @@ export const ConsultLlmArgs = z
18
17
  base_ref: z.string().optional().default('HEAD'),
19
18
  })
20
19
  .optional(),
21
- })
22
- .refine((data) => data.files || data.prompt, 'Either files or prompt must be provided');
20
+ });
23
21
  export const toolSchema = {
24
22
  name: 'consult_llm',
25
- description: `Ask a more powerful AI for help with complex problems. Write your problem description in a markdown file and pass relevant code files as context.
23
+ description: `Ask a more powerful AI for help with complex problems. Provide your question in the prompt field and optionally include relevant code files as context.
26
24
 
27
- Be specific about what you want: code implementation, code review, bug analysis, architecture advice, etc.`,
25
+ Be specific about what you want: code implementation, code review, bug analysis, architecture advice, etc.
26
+
27
+ IMPORTANT: Ask neutral, open-ended questions. Avoid suggesting specific solutions or alternatives in your prompt as this can bias the analysis. Instead of "Should I use X or Y approach?", ask "What's the best approach for this problem?" Let the consultant LLM provide unbiased recommendations.`,
28
28
  inputSchema: {
29
29
  type: 'object',
30
30
  properties: {
31
31
  files: {
32
32
  type: 'array',
33
33
  items: { type: 'string' },
34
- description: `Array of file paths to process. Markdown files (.md) become the main prompt, other files are added as context with file paths and code blocks.
35
-
36
- In the markdown file(s), be clear about what you want the LLM to do: implement code, review code, explain concepts, analyze bugs, etc.`,
34
+ description: 'Array of file paths to include as context. All files are added as context with file paths and code blocks.',
37
35
  },
38
36
  prompt: {
39
37
  type: 'string',
40
- description: 'Direct prompt text for simple questions. Alternative to using markdown files.',
38
+ description: 'Your question or request for the consultant LLM. Ask neutral, open-ended questions without suggesting specific solutions to avoid biasing the analysis.',
41
39
  },
42
40
  model: {
43
41
  type: 'string',
@@ -67,6 +65,6 @@ In the markdown file(s), be clear about what you want the LLM to do: implement c
67
65
  description: 'Generate git diff output to include as context. Shows uncommitted changes by default.',
68
66
  },
69
67
  },
70
- required: [],
68
+ required: ['prompt'],
71
69
  },
72
70
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "consult-llm-mcp",
3
- "version": "1.0.4",
3
+ "version": "1.1.0",
4
4
  "description": "MCP server for consulting powerful AI models",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",
@@ -46,12 +46,13 @@
46
46
  "dependencies": {
47
47
  "@modelcontextprotocol/sdk": "^1.13.0",
48
48
  "openai": "^5.6.0",
49
+ "prettier": "^3.5.3",
49
50
  "zod": "^3.25.67"
50
51
  },
51
52
  "devDependencies": {
52
53
  "@types/node": "^24.0.3",
53
- "prettier": "^3.5.3",
54
54
  "tsx": "^4.20.3",
55
55
  "typescript": "^5.8.3"
56
- }
56
+ },
57
+ "packageManager": "pnpm@10.12.4+sha512.5ea8b0deed94ed68691c9bad4c955492705c5eeb8a87ef86bc62c74a26b037b08ff9570f108b2e4dbd1dd1a9186fea925e527f141c648e85af45631074680184"
57
58
  }