askgrokmcp 1.3.0 → 1.4.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,8 +1,14 @@
1
1
  # Grok MCP Server
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/askgrokmcp)](https://www.npmjs.com/package/askgrokmcp)
4
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
+
3
7
  A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that brings xAI's Grok API into [Claude Code](https://docs.anthropic.com/en/docs/claude-code) as native tools.
4
8
 
5
- Ask Grok questions, generate images with Aurora, and explore available models — directly from your terminal.
9
+ Ask Grok questions, generate images with Aurora, run multi-round consensus analysis, and explore available models — directly from your terminal.
10
+
11
+ ---
6
12
 
7
13
  ## Tools
8
14
 
@@ -11,12 +17,31 @@ Ask Grok questions, generate images with Aurora, and explore available models
11
17
  | `ask_grok` | Send a prompt to Grok with optional system prompt and sampling parameters |
12
18
  | `generate_image` | Generate images using Grok's Aurora model and save them locally |
13
19
  | `list_models` | List all xAI models available to your account |
20
+ | `grok_consensus` | Run a full Consensus Validation Protocol (CVP) for deep, multi-round analysis |
21
+
22
+ ## Built-in Protocols
23
+
24
+ ### Consensus Validation Protocol (CVP)
25
+
26
+ The `grok_consensus` tool implements a structured, multi-round analysis protocol. Instead of a single prompt-and-response, it runs 3-10 iterative rounds where Grok progressively deepens its analysis — challenging its own assumptions, evaluating evidence strength, and synthesizing a balanced conclusion.
27
+
28
+ ```
29
+ > Run CVP on whether large language models can reason
30
+ > Ask Grok to validate the claim that sleep deprivation affects decision-making — use 5 rounds
31
+ > Consensus check with Grok on the future of nuclear energy
32
+ ```
33
+
34
+ The entire protocol executes server-side in a single tool call. Each round builds on the full conversation history for genuine iterative refinement.
35
+
36
+ **Default:** 3 rounds | **Max:** 10 rounds | [Full protocol documentation](protocols/consensus-validation.md)
37
+
38
+ ---
14
39
 
15
40
  ## Prerequisites
16
41
 
17
42
  - **Node.js** >= 18
18
43
  - **Claude Code** CLI installed
19
- - **xAI API key** -- get one at [console.x.ai](https://console.x.ai)
44
+ - **xAI API key** get one at [console.x.ai](https://console.x.ai)
20
45
 
21
46
  ## Setup
22
47
 
@@ -50,7 +75,7 @@ Replace `/path/to/askgrokmcp` with the actual path where you cloned the reposito
50
75
 
51
76
  ---
52
77
 
53
- Replace `your_api_key_here` with your xAI API key in either option. That's it -- the tools are now available in Claude Code.
78
+ Replace `your_api_key_here` with your xAI API key in either option. That's it the tools are now available in Claude Code.
54
79
 
55
80
  ## Usage
56
81
 
@@ -94,6 +119,13 @@ Once registered, you can use the tools naturally in Claude Code:
94
119
 
95
120
  When generating multiple images, files are automatically numbered (e.g., `logo-1.png`, `logo-2.png`, ...).
96
121
 
122
+ ### Run a consensus analysis
123
+
124
+ ```
125
+ > run CVP on the effectiveness of carbon capture technology
126
+ > ask grok to validate whether quantum computers will break RSA by 2030 — use 5 rounds
127
+ ```
128
+
97
129
  ### List available models
98
130
 
99
131
  ```
@@ -169,10 +201,9 @@ claude mcp add grok \
169
201
  | Variable | Default | Description |
170
202
  |----------|---------|-------------|
171
203
  | `XAI_API_KEY` | *(required)* | Your xAI API key |
172
- | `GROK_CHAT_MODEL` | `grok-3-fast` | Default model for `ask_grok` |
204
+ | `GROK_CHAT_MODEL` | `grok-3-fast` | Default model for `ask_grok` and `grok_consensus` |
173
205
  | `GROK_IMAGE_MODEL` | `grok-2-image` | Default model for `generate_image` |
174
206
  | `SAFE_WRITE_BASE_DIR` | `process.cwd()` | Base directory for image writes |
175
- | `MAX_PROMPT_LENGTH` | `128000` | Maximum prompt length in characters (fail-fast guard) |
176
207
  | `XAI_REQUEST_TIMEOUT_MS` | `30000` | Timeout per xAI API request in milliseconds |
177
208
  | `XAI_MAX_RETRIES` | `2` | Number of retries for transient errors (429/5xx/network/timeout) |
178
209
  | `XAI_RETRY_BASE_DELAY_MS` | `500` | Base delay for exponential retry backoff |
@@ -198,6 +229,17 @@ export LOG_REQUEST_PAYLOADS=true
198
229
 
199
230
  > **Important:** Logs are written to stderr (not stdout) so MCP protocol communication remains safe.
200
231
 
232
+ ## Project Structure
233
+
234
+ ```
235
+ askgrokmcp/
236
+ grok-mcp.mjs Server entry point, config, HTTP client
237
+ src/tools.js Tool definitions and handler implementations
238
+ protocols/ Protocol documentation
239
+ consensus-validation.md
240
+ grok-mcp.test.mjs Test suite
241
+ ```
242
+
201
243
  ## How it works
202
244
 
203
245
  This server implements the MCP protocol over stdio. When Claude Code starts, it launches the server as a subprocess and communicates with it via JSON-RPC over stdin/stdout. The server translates MCP tool calls into xAI API requests and returns the results.
@@ -208,6 +250,22 @@ flowchart LR
208
250
  B -- HTTPS --> C[xAI API]
209
251
  ```
210
252
 
253
+ For the `grok_consensus` tool, the server manages a multi-round conversation loop with Grok internally, returning the complete analysis in a single response:
254
+
255
+ ```mermaid
256
+ sequenceDiagram
257
+ participant C as Claude Code
258
+ participant S as grok-mcp
259
+ participant G as xAI API
260
+
261
+ C->>S: grok_consensus(topic, rounds)
262
+ loop Each round
263
+ S->>G: chat/completions (with full history)
264
+ G-->>S: Round analysis
265
+ end
266
+ S-->>C: Structured CVP results
267
+ ```
268
+
211
269
  ## License
212
270
 
213
271
  [MIT](LICENSE)
package/grok-mcp.mjs CHANGED
@@ -4,11 +4,12 @@
4
4
  * Grok MCP Server
5
5
  *
6
6
  * A Model Context Protocol (MCP) server that exposes xAI's Grok API
7
- * as tools for AI assistants like Claude Code. Provides three capabilities:
7
+ * as tools for AI assistants like Claude Code. Provides four capabilities:
8
8
  *
9
- * - ask_grok: Send prompts to Grok and receive text responses.
10
- * - generate_image: Generate images using Grok's Aurora model and save them locally.
11
- * - list_models: List all models available to your xAI account.
9
+ * - ask_grok: Send prompts to Grok and receive text responses.
10
+ * - generate_image: Generate images using Grok's Aurora model and save them locally.
11
+ * - list_models: List all models available to your xAI account.
12
+ * - grok_consensus: Run a full Consensus Validation Protocol (CVP) with Grok.
12
13
  *
13
14
  * Model selection (highest priority wins):
14
15
  * 1. Per-call `model` argument
@@ -28,6 +29,7 @@ import {
28
29
  CallToolRequestSchema,
29
30
  ListToolsRequestSchema,
30
31
  } from "@modelcontextprotocol/sdk/types.js";
32
+ import { getToolDefinitions, createToolHandlers } from "./src/tools.js";
31
33
 
32
34
  // -- Configuration -----------------------------------------------------------
33
35
 
@@ -43,7 +45,7 @@ const FALLBACK_IMAGE_MODEL = "grok-2-image";
43
45
 
44
46
  /**
45
47
  * Active defaults. Env vars take top priority; otherwise resolved at startup
46
- * by probing the xAI /models endpoint (frontier fallback).
48
+ * by probing the xAI /models endpoint (frontier -> fallback).
47
49
  */
48
50
  let CHAT_MODEL = process.env.GROK_CHAT_MODEL ?? FRONTIER_CHAT_MODEL;
49
51
  let IMAGE_MODEL = process.env.GROK_IMAGE_MODEL ?? FRONTIER_IMAGE_MODEL;
@@ -55,7 +57,7 @@ const MAX_RETRIES = parseNonNegativeIntEnv("XAI_MAX_RETRIES",
55
57
  const RETRY_BASE_DELAY_MS = parsePositiveIntEnv("XAI_RETRY_BASE_DELAY_MS", 500);
56
58
  const LOG_REQUESTS = parseBooleanEnv("LOG_REQUESTS", false);
57
59
  const LOG_REQUEST_PAYLOADS = parseBooleanEnv("LOG_REQUEST_PAYLOADS", false);
58
- const SERVER_VERSION = "1.3.0";
60
+ const SERVER_VERSION = "1.4.0";
59
61
 
60
62
  const SAFE_WRITE_BASE_DIR = process.env.SAFE_WRITE_BASE_DIR;
61
63
  if (SAFE_WRITE_BASE_DIR && !isAbsolute(SAFE_WRITE_BASE_DIR)) {
@@ -71,128 +73,25 @@ if (!API_KEY && !__testing) {
71
73
  process.exit(1);
72
74
  }
73
75
 
74
- // -- Tool definitions --------------------------------------------------------
75
-
76
- const tools = [
77
- {
78
- name: "ask_grok",
79
- description:
80
- "Ask Grok a question and get a response. " +
81
- `Default model: ${CHAT_MODEL}. ` +
82
- "Supports system prompts and sampling parameters (temperature, max_tokens, top_p). " +
83
- "Run list_models to see all available model options.",
84
- inputSchema: {
85
- type: "object",
86
- properties: {
87
- prompt: {
88
- type: "string",
89
- description: "The question or prompt to send to Grok",
90
- },
91
- system_prompt: {
92
- type: "string",
93
- description:
94
- "Optional system prompt to set Grok's behavior and persona for this request.",
95
- },
96
- model: {
97
- type: "string",
98
- description:
99
- `Chat model to use for this request. Defaults to "${CHAT_MODEL}". ` +
100
- "Use list_models to see available chat models.",
101
- },
102
- temperature: {
103
- type: "number",
104
- description:
105
- "Sampling temperature (0-2). Lower values make output more deterministic. Default: model-dependent.",
106
- },
107
- max_tokens: {
108
- type: "number",
109
- description:
110
- "Maximum number of tokens to generate in the response.",
111
- },
112
- top_p: {
113
- type: "number",
114
- description:
115
- "Nucleus sampling: only consider tokens with cumulative probability up to this value (0-1).",
116
- },
117
- },
118
- required: ["prompt"],
119
- },
120
- },
121
- {
122
- name: "generate_image",
123
- description:
124
- "Generate an image using Grok's Aurora image model and save it to a local file. " +
125
- `Default model: ${IMAGE_MODEL}. ` +
126
- "Use the optional 'model' parameter to use a different image model.",
127
- inputSchema: {
128
- type: "object",
129
- properties: {
130
- prompt: {
131
- type: "string",
132
- description: "Text description of the image to generate",
133
- },
134
- file_path: {
135
- type: "string",
136
- description:
137
- "Path where the image file should be saved. Relative paths resolve from cwd; " +
138
- "absolute paths must be within SAFE_WRITE_BASE_DIR (or cwd if unset). Example: images/output.png",
139
- },
140
- n: {
141
- type: "number",
142
- description: "Number of image variations to generate (1-10, default 1)",
143
- },
144
- model: {
145
- type: "string",
146
- description:
147
- `Image model to use for this request. Defaults to "${IMAGE_MODEL}". ` +
148
- "Use list_models to see available image models.",
149
- },
150
- },
151
- required: ["prompt", "file_path"],
152
- },
153
- },
154
- {
155
- name: "list_models",
156
- description:
157
- "List all xAI models available to your account, including their IDs and capabilities. " +
158
- "Use this to discover which models you can pass to ask_grok or generate_image. " +
159
- "You can also filter by type: 'chat' for language models or 'image' for image generation.",
160
- inputSchema: {
161
- type: "object",
162
- properties: {
163
- filter: {
164
- type: "string",
165
- enum: ["all", "chat", "image"],
166
- description:
167
- "Filter models by capability. " +
168
- "'chat' returns language/reasoning models, " +
169
- "'image' returns image generation models, " +
170
- "'all' returns everything (default).",
171
- },
172
- },
173
- required: [],
174
- },
175
- },
176
- ];
76
+ // -- Shared mutable config ---------------------------------------------------
77
+ // Handlers hold a reference to this object so they always see resolved values.
78
+
79
+ const config = {
80
+ get chatModel() { return CHAT_MODEL; },
81
+ get imageModel() { return IMAGE_MODEL; },
82
+ maxPromptLength: MAX_PROMPT_LENGTH,
83
+ maxImageVariations: MAX_IMAGE_VARIATIONS,
84
+ };
177
85
 
178
86
  // -- Helpers -----------------------------------------------------------------
179
87
 
180
88
  /**
181
89
  * Writes data to a file, enforcing that the destination is inside the
182
90
  * allowed base directory. Creates parent directories as needed.
183
- *
184
- * Base dir precedence:
185
- * 1. SAFE_WRITE_BASE_DIR env var (must be an absolute path)
186
- * 2. process.cwd() as the default fallback
187
- *
188
- * @param {string} dest - Resolved absolute destination path.
189
- * @param {Buffer|string} data - File contents to write.
190
- * @throws {Error} If dest resolves outside the allowed base.
191
91
  */
192
92
  async function safeWrite(dest, data) {
193
93
  const rel = relative(WRITE_BASE_DIR, dest);
194
94
 
195
- // Starts with ".." → outside base; isAbsolute guards cross-drive (Windows)
196
95
  if (rel.startsWith("..") || isAbsolute(rel)) {
197
96
  throw new Error(
198
97
  `Path "${dest}" is outside the allowed write directory "${WRITE_BASE_DIR}". ` +
@@ -206,11 +105,6 @@ async function safeWrite(dest, data) {
206
105
 
207
106
  /**
208
107
  * Makes an authenticated POST request to the xAI API with retries.
209
- *
210
- * @param {string} endpoint - API path relative to the base URL (e.g. "/chat/completions").
211
- * @param {object} body - JSON-serializable request body.
212
- * @returns {Promise<object>} Parsed JSON response.
213
- * @throws {Error} On non-2xx responses after retries.
214
108
  */
215
109
  async function xaiPost(endpoint, body) {
216
110
  return xaiRequest("POST", endpoint, body);
@@ -218,10 +112,6 @@ async function xaiPost(endpoint, body) {
218
112
 
219
113
  /**
220
114
  * Makes an authenticated GET request to the xAI API.
221
- *
222
- * @param {string} endpoint - API path relative to the base URL (e.g. "/models").
223
- * @returns {Promise<object>} Parsed JSON response.
224
- * @throws {Error} On non-2xx responses.
225
115
  */
226
116
  async function xaiGet(endpoint) {
227
117
  return xaiRequest("GET", endpoint, null);
@@ -229,11 +119,6 @@ async function xaiGet(endpoint) {
229
119
 
230
120
  /**
231
121
  * Core HTTP request handler for the xAI API with retry logic.
232
- *
233
- * @param {"GET"|"POST"} method - HTTP method.
234
- * @param {string} endpoint - API path relative to the base URL.
235
- * @param {object|null} body - JSON body (POST only; null for GET).
236
- * @returns {Promise<object>} Parsed JSON response.
237
122
  */
238
123
  async function xaiRequest(method, endpoint, body) {
239
124
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
@@ -343,11 +228,6 @@ async function xaiRequest(method, endpoint, body) {
343
228
 
344
229
  /**
345
230
  * Downloads a remote URL and returns its contents as a Buffer.
346
- * Uses the same timeout and retry strategy as xaiRequest().
347
- *
348
- * @param {string} url - The URL to download.
349
- * @returns {Promise<Buffer>} The downloaded file contents.
350
- * @throws {Error} On non-2xx responses after retries.
351
231
  */
352
232
  async function downloadBuffer(url) {
353
233
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
@@ -390,14 +270,6 @@ async function downloadBuffer(url) {
390
270
 
391
271
  /**
392
272
  * Builds a numbered file path for multi-image generation.
393
- * For a single image, returns the path unchanged.
394
- * For multiple images, inserts an index before the extension:
395
- * /tmp/cat.png -> /tmp/cat-1.png, /tmp/cat-2.png, ...
396
- *
397
- * @param {string} basePath - The original file path.
398
- * @param {number} index - Zero-based image index.
399
- * @param {number} total - Total number of images being saved.
400
- * @returns {string} The resolved, possibly indexed, file path.
401
273
  */
402
274
  function buildFilePath(basePath, index, total) {
403
275
  const dest = resolve(basePath);
@@ -410,183 +282,17 @@ function buildFilePath(basePath, index, total) {
410
282
  return `${dest}-${index + 1}`;
411
283
  }
412
284
 
413
- // -- Tool handlers -----------------------------------------------------------
414
-
415
- /**
416
- * Sends a prompt to Grok's chat completion endpoint and returns the response.
417
- * Honors the optional per-call `model` argument.
418
- */
419
- async function handleAskGrok(args) {
420
- if (!args || typeof args.prompt !== "string" || !args.prompt.trim()) {
421
- throw new Error("Invalid arguments: 'prompt' must be a non-empty string");
422
- }
423
- if (args.prompt.length > MAX_PROMPT_LENGTH) {
424
- throw new Error(
425
- `Prompt too long: ${args.prompt.length} chars exceeds the ${MAX_PROMPT_LENGTH} char limit`,
426
- );
427
- }
428
-
429
- const model = (typeof args.model === "string" && args.model.trim())
430
- ? args.model.trim()
431
- : CHAT_MODEL;
432
-
433
- const messages = [];
434
- if (typeof args.system_prompt === "string" && args.system_prompt.trim()) {
435
- messages.push({ role: "system", content: args.system_prompt });
436
- }
437
- messages.push({ role: "user", content: args.prompt });
438
-
439
- const requestBody = { model, messages };
440
- if (typeof args.temperature === "number") requestBody.temperature = args.temperature;
441
- if (typeof args.max_tokens === "number") requestBody.max_tokens = args.max_tokens;
442
- if (typeof args.top_p === "number") requestBody.top_p = args.top_p;
443
-
444
- const data = await xaiPost("/chat/completions", requestBody);
445
-
446
- const messageContent = data?.choices?.[0]?.message?.content;
447
- const text =
448
- typeof messageContent === "string"
449
- ? messageContent
450
- : messageContent != null
451
- ? JSON.stringify(messageContent)
452
- : "No response";
453
- return { content: [{ type: "text", text }] };
454
- }
455
-
456
- /**
457
- * Generates images via Grok's Aurora model, downloads them, and saves to disk.
458
- * Honors the optional per-call `model` argument.
459
- */
460
- async function handleGenerateImage(args) {
461
- if (!args || typeof args.prompt !== "string" || !args.prompt.trim()) {
462
- throw new Error("Invalid arguments: 'prompt' must be a non-empty string");
463
- }
464
- if (args.prompt.length > MAX_PROMPT_LENGTH) {
465
- throw new Error(
466
- `Prompt too long: ${args.prompt.length} chars exceeds the ${MAX_PROMPT_LENGTH} char limit`,
467
- );
468
- }
469
- if (typeof args.file_path !== "string" || !args.file_path.trim()) {
470
- throw new Error("Invalid arguments: 'file_path' must be a non-empty string");
471
- }
472
- if (args.n != null && (!Number.isInteger(args.n) || args.n < 1)) {
473
- throw new Error("Invalid arguments: 'n' must be a positive integer");
474
- }
475
-
476
- const n = Math.min(Math.max(args.n ?? 1, 1), MAX_IMAGE_VARIATIONS);
477
- const model = (typeof args.model === "string" && args.model.trim())
478
- ? args.model.trim()
479
- : IMAGE_MODEL;
480
-
481
- const data = await xaiPost("/images/generations", {
482
- model,
483
- prompt: args.prompt,
484
- n,
485
- });
486
-
487
- const images = Array.isArray(data?.data) ? data.data : [];
488
- if (images.length === 0) {
489
- throw new Error("xAI API did not return any images");
490
- }
491
-
492
- const saved = [];
493
- for (let i = 0; i < images.length; i++) {
494
- const imageUrl = images[i]?.url;
495
- if (typeof imageUrl !== "string" || !imageUrl) {
496
- throw new Error(`xAI API returned an invalid image URL at index ${i}`);
497
- }
498
-
499
- const buffer = await downloadBuffer(imageUrl);
500
- const dest = buildFilePath(args.file_path, i, images.length);
501
- await safeWrite(dest, buffer);
502
- saved.push(dest);
503
- }
504
-
505
- return {
506
- content: [
507
- {
508
- type: "text",
509
- text: `Generated and saved ${saved.length} image(s):\n${saved.join("\n")}`,
510
- },
511
- ],
512
- };
513
- }
514
-
515
- /**
516
- * Fetches available models from the xAI API and formats them for display.
517
- * Supports optional filtering by capability (chat or image).
518
- */
519
- async function handleListModels(args) {
520
- const filter = args?.filter ?? "all";
521
- if (!["all", "chat", "image"].includes(filter)) {
522
- throw new Error("Invalid arguments: 'filter' must be 'all', 'chat', or 'image'");
523
- }
524
-
525
- const data = await xaiGet("/models");
526
- const models = Array.isArray(data?.data) ? data.data : [];
527
-
528
- if (models.length === 0) {
529
- return { content: [{ type: "text", text: "No models returned by the xAI API." }] };
530
- }
285
+ // -- Tool handlers (created from src/tools.js) -------------------------------
531
286
 
532
- // xAI model IDs contain hints about their capability:
533
- // image generation models have "image" or "imagine" in the ID.
534
- const isImageModel = (id) =>
535
- /image|imagine|aurora/i.test(id);
536
-
537
- const filtered = models.filter((m) => {
538
- if (filter === "all") return true;
539
- const isImg = isImageModel(m.id ?? "");
540
- return filter === "image" ? isImg : !isImg;
541
- });
542
-
543
- if (filtered.length === 0) {
544
- return {
545
- content: [{
546
- type: "text",
547
- text: `No ${filter} models found. Try filter: "all" to see everything.`,
548
- }],
549
- };
550
- }
551
-
552
- // Sort: alphabetically, images last
553
- filtered.sort((a, b) => {
554
- const aImg = isImageModel(a.id ?? "");
555
- const bImg = isImageModel(b.id ?? "");
556
- if (aImg !== bImg) return aImg ? 1 : -1;
557
- return (a.id ?? "").localeCompare(b.id ?? "");
558
- });
559
-
560
- const lines = [
561
- `${filtered.length} model(s) available${filter !== "all" ? ` (filter: ${filter})` : ""}:`,
562
- "",
563
- ];
564
-
565
- for (const m of filtered) {
566
- const id = m.id ?? "unknown";
567
- const type = isImageModel(id) ? "image" : "chat";
568
- const isDefaultChat = id === CHAT_MODEL;
569
- const isDefaultImage = id === IMAGE_MODEL;
570
- const defaultTag = isDefaultChat
571
- ? " ← current default (chat)"
572
- : isDefaultImage
573
- ? " ← current default (image)"
574
- : "";
575
- lines.push(` ${id} [${type}]${defaultTag}`);
576
- }
577
-
578
- lines.push("");
579
- lines.push(`To change the default: set GROK_CHAT_MODEL or GROK_IMAGE_MODEL env vars.`);
580
- lines.push(`To use once: pass model="<id>" to ask_grok or generate_image.`);
581
-
582
- return { content: [{ type: "text", text: lines.join("\n") }] };
583
- }
584
-
585
- const toolHandlers = {
586
- ask_grok: handleAskGrok,
587
- generate_image: handleGenerateImage,
588
- list_models: handleListModels,
589
- };
287
+ const toolHandlers = createToolHandlers({
288
+ xaiPost,
289
+ xaiGet,
290
+ safeWrite,
291
+ buildFilePath,
292
+ downloadBuffer,
293
+ resolve,
294
+ config,
295
+ });
590
296
 
591
297
  // -- Model resolution --------------------------------------------------------
592
298
 
@@ -600,7 +306,6 @@ async function resolveDefaults() {
600
306
  const chatFromEnv = !!process.env.GROK_CHAT_MODEL;
601
307
  const imageFromEnv = !!process.env.GROK_IMAGE_MODEL;
602
308
 
603
- // Nothing to resolve if both were explicitly set.
604
309
  if (chatFromEnv && imageFromEnv) return;
605
310
 
606
311
  let availableIds;
@@ -609,7 +314,6 @@ async function resolveDefaults() {
609
314
  const models = Array.isArray(data?.data) ? data.data : [];
610
315
  availableIds = new Set(models.map((m) => m.id));
611
316
  } catch {
612
- // If the models endpoint is unreachable, fall back to safe defaults.
613
317
  logEvent("resolve_defaults", { status: "models_fetch_failed", action: "using_fallbacks" });
614
318
  if (!chatFromEnv) CHAT_MODEL = FALLBACK_CHAT_MODEL;
615
319
  if (!imageFromEnv) IMAGE_MODEL = FALLBACK_IMAGE_MODEL;
@@ -661,7 +365,9 @@ if (!__testing) {
661
365
  { capabilities: { tools: {} } },
662
366
  );
663
367
 
664
- server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
368
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
369
+ tools: getToolDefinitions({ chatModel: CHAT_MODEL, imageModel: IMAGE_MODEL }),
370
+ }));
665
371
 
666
372
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
667
373
  const { name } = request.params;
@@ -717,14 +423,20 @@ if (!__testing) {
717
423
  export {
718
424
  safeWrite,
719
425
  buildFilePath,
720
- handleAskGrok,
721
- handleGenerateImage,
722
- handleListModels,
723
426
  toolHandlers,
724
427
  WRITE_BASE_DIR,
725
428
  MAX_PROMPT_LENGTH,
726
429
  };
727
430
 
431
+ // Re-export individual handlers for backwards-compatible test access.
432
+ const { ask_grok, generate_image, list_models, grok_consensus } = toolHandlers;
433
+ export {
434
+ ask_grok as handleAskGrok,
435
+ generate_image as handleGenerateImage,
436
+ list_models as handleListModels,
437
+ grok_consensus as handleGrokConsensus,
438
+ };
439
+
728
440
  // -- Utility functions -------------------------------------------------------
729
441
 
730
442
  function parseBooleanEnv(name, defaultValue) {
@@ -773,7 +485,6 @@ function isNetworkError(error) {
773
485
  }
774
486
 
775
487
  function backoffDelay(attempt) {
776
- // Exponential backoff: base, 2×base, 4×base, …
777
488
  return RETRY_BASE_DELAY_MS * Math.pow(2, attempt);
778
489
  }
779
490
 
@@ -790,6 +501,9 @@ function summarizeArguments(args) {
790
501
  if (typeof summary.prompt === "string") {
791
502
  summary.prompt = `[redacted:${summary.prompt.length} chars]`;
792
503
  }
504
+ if (typeof summary.topic === "string") {
505
+ summary.topic = `[redacted:${summary.topic.length} chars]`;
506
+ }
793
507
  return summary;
794
508
  }
795
509
 
@@ -799,6 +513,5 @@ function logEvent(event, fields) {
799
513
  event,
800
514
  ...fields,
801
515
  };
802
- // MCP uses stdout for protocol; logs must go to stderr.
803
516
  console.error(JSON.stringify(payload));
804
517
  }
package/grok-mcp.test.mjs CHANGED
@@ -13,6 +13,7 @@ const {
13
13
  handleAskGrok,
14
14
  handleGenerateImage,
15
15
  handleListModels,
16
+ handleGrokConsensus,
16
17
  toolHandlers,
17
18
  WRITE_BASE_DIR,
18
19
  MAX_PROMPT_LENGTH,
@@ -179,6 +180,42 @@ describe("handleListModels input validation", () => {
179
180
  });
180
181
  });
181
182
 
183
+ // ---------------------------------------------------------------------------
184
+ // handleGrokConsensus — input validation
185
+ // ---------------------------------------------------------------------------
186
+
187
+ describe("handleGrokConsensus input validation", () => {
188
+ it("rejects missing topic", async () => {
189
+ await assert.rejects(() => handleGrokConsensus({}), {
190
+ message: /topic.*must be a non-empty string/,
191
+ });
192
+ });
193
+
194
+ it("rejects empty topic", async () => {
195
+ await assert.rejects(() => handleGrokConsensus({ topic: " " }), {
196
+ message: /topic.*must be a non-empty string/,
197
+ });
198
+ });
199
+
200
+ it("rejects invalid rounds value", async () => {
201
+ await assert.rejects(() => handleGrokConsensus({ topic: "test", rounds: 0 }), {
202
+ message: /rounds.*must be an integer/,
203
+ });
204
+ });
205
+
206
+ it("rejects rounds exceeding maximum", async () => {
207
+ await assert.rejects(() => handleGrokConsensus({ topic: "test", rounds: 11 }), {
208
+ message: /rounds.*must be an integer/,
209
+ });
210
+ });
211
+
212
+ it("rejects non-integer rounds", async () => {
213
+ await assert.rejects(() => handleGrokConsensus({ topic: "test", rounds: 2.5 }), {
214
+ message: /rounds.*must be an integer/,
215
+ });
216
+ });
217
+ });
218
+
182
219
  // ---------------------------------------------------------------------------
183
220
  // toolHandlers — routing
184
221
  // ---------------------------------------------------------------------------
@@ -196,8 +233,12 @@ describe("toolHandlers", () => {
196
233
  assert.equal(typeof toolHandlers.list_models, "function");
197
234
  });
198
235
 
199
- it("has exactly 3 handlers", () => {
200
- assert.equal(Object.keys(toolHandlers).length, 3);
236
+ it("maps grok_consensus to a function", () => {
237
+ assert.equal(typeof toolHandlers.grok_consensus, "function");
238
+ });
239
+
240
+ it("has exactly 4 handlers", () => {
241
+ assert.equal(Object.keys(toolHandlers).length, 4);
201
242
  });
202
243
  });
203
244
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "askgrokmcp",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "MCP server that exposes xAI's Grok API as tools for Claude Code",
5
5
  "license": "MIT",
6
6
  "author": "Marcelo Ceccon",
@@ -0,0 +1,105 @@
1
+ # Consensus Validation Protocol (CVP) v1.4
2
+
3
+ A structured, multi-round analysis protocol powered by the `grok_consensus` MCP tool. CVP enables Claude to leverage Grok as an independent analytical counterpart for deep, iterative reasoning on any topic.
4
+
5
+ ## Overview
6
+
7
+ The Consensus Validation Protocol runs multiple rounds of progressively deeper analysis through Grok's language model. Each round builds on the full conversation history, ensuring coherent, non-repetitive refinement. The heavy lifting is performed server-side by the `grok_consensus` tool for efficiency — Claude only needs to make a single tool call.
8
+
9
+ ## Activation Triggers
10
+
11
+ This protocol activates when the user says any of the following (or close variants):
12
+
13
+ - **"Ask Grok"** — followed by a topic or claim
14
+ - **"Ask Grok to validate..."**
15
+ - **"Run CVP on..."**
16
+ - **"Consensus check with Grok"**
17
+ - **"Validate this with Grok"**
18
+
19
+ ### Examples
20
+
21
+ ```
22
+ Ask Grok to validate whether intermittent fasting improves longevity
23
+ Run CVP on the claim that remote work reduces productivity
24
+ Consensus check with Grok on quantum computing timelines
25
+ Run CVP on climate change mitigation strategies for 5 rounds
26
+ ```
27
+
28
+ ## How It Works
29
+
30
+ ### 1. Claude calls `grok_consensus` once
31
+
32
+ When a CVP trigger is detected, Claude invokes the `grok_consensus` tool with:
33
+
34
+ | Argument | Type | Required | Description |
35
+ |----------|------|----------|-------------|
36
+ | `topic` | string | Yes | The topic, claim, or question to analyze |
37
+ | `rounds` | number | No | Number of rounds (default: 3, max: 10) |
38
+
39
+ ### 2. The tool runs the protocol server-side
40
+
41
+ The `grok_consensus` tool internally executes the full multi-round protocol:
42
+
43
+ - **Round 1 — Initial Analysis:** Grok provides a comprehensive, objective analysis of the topic covering key claims, evidence, uncertainties, and misconceptions.
44
+ - **Round 2 — Counterarguments:** Grok challenges its own analysis, identifying the strongest counterarguments and alternative viewpoints.
45
+ - **Round 3 — Evidence Assessment:** Grok evaluates the strength of evidence on all sides, distinguishing well-established facts from contested claims.
46
+ - **Round 4 — Synthesis:** Grok integrates all rounds into a balanced conclusion with confidence levels.
47
+ - **Round 5+ — Refinement:** Additional rounds deepen the analysis with new perspectives and edge cases.
48
+
49
+ Conversation history is maintained properly across rounds — each round sees the full prior context, enabling genuine iterative refinement rather than redundant restating.
50
+
51
+ ### 3. Claude receives structured results
52
+
53
+ The tool returns a structured Markdown report with all round-by-round analysis, which Claude can then summarize, quote, or present directly to the user.
54
+
55
+ ## Custom Round Count
56
+
57
+ Users can request a specific number of rounds:
58
+
59
+ ```
60
+ Run CVP on AI safety concerns for 7 rounds
61
+ Ask Grok to validate this claim — use 5 rounds
62
+ ```
63
+
64
+ - **Default:** 3 rounds (good balance of depth and speed)
65
+ - **Minimum:** 1 round (quick single-pass analysis)
66
+ - **Maximum:** 10 rounds (exhaustive deep-dive)
67
+
68
+ Higher round counts yield more thorough analysis at the cost of additional latency, since each round is a separate API call to Grok.
69
+
70
+ ## Output Format
71
+
72
+ The tool returns results in this structure:
73
+
74
+ ```markdown
75
+ ## Consensus Validation Protocol — Results
76
+
77
+ | Field | Value |
78
+ |-------|-------|
79
+ | **Topic** | {topic} |
80
+ | **Rounds completed** | {n} |
81
+ | **Model** | {model} |
82
+
83
+ ### Round 1
84
+ {Initial analysis content}
85
+
86
+ ### Round 2
87
+ {Counterarguments and critique}
88
+
89
+ ### Round 3
90
+ {Evidence assessment and final synthesis}
91
+ ```
92
+
93
+ ## Design Principles
94
+
95
+ - **Concise and factual.** Each round advances the analysis — no filler or repetition.
96
+ - **Collaborative tone.** Grok acts as an analytical partner, not an adversary.
97
+ - **Evidence-based.** Claims are grounded in reasoning and evidence, with uncertainty explicitly acknowledged.
98
+ - **Server-side efficiency.** The entire loop runs within a single MCP tool call, minimizing round-trips between Claude and the server.
99
+
100
+ ## Version History
101
+
102
+ | Version | Changes |
103
+ |---------|---------|
104
+ | 1.4 | Protocol moved server-side into `grok_consensus` tool. Single tool call replaces client-side loop. |
105
+ | 1.3 | Initial CVP as a Claude Code skill with client-side loop management. |
package/src/tools.js ADDED
@@ -0,0 +1,468 @@
1
+ /**
2
+ * Tool definitions and handlers for the Grok MCP Server.
3
+ *
4
+ * This module owns all MCP tool schemas and their implementation.
5
+ * The main server module provides the HTTP client and configuration.
6
+ */
7
+
8
+ // -- Consensus Validation Protocol -------------------------------------------
9
+
10
+ const CVP_SYSTEM_PROMPT =
11
+ "You are participating in a Consensus Validation Protocol (CVP). " +
12
+ "Your role is to provide rigorous, evidence-based analysis through multiple rounds " +
13
+ "of iterative refinement. Be thorough but concise. Avoid repetition — each round " +
14
+ "must meaningfully advance the analysis. Stay objective and acknowledge uncertainty.";
15
+
16
+ /**
17
+ * Returns the user prompt for a given CVP round.
18
+ *
19
+ * @param {string} topic - The topic under analysis.
20
+ * @param {number} round - Current round (1-based).
21
+ * @param {number} total - Total number of rounds.
22
+ * @returns {string}
23
+ */
24
+ function cvpRoundPrompt(topic, round, total) {
25
+ if (round === 1) {
26
+ return (
27
+ `Round ${round}/${total} — Initial Analysis\n\n` +
28
+ `Analyze the following topic thoroughly and objectively. Identify the key claims, ` +
29
+ `supporting evidence, areas of genuine uncertainty, and any common misconceptions.\n\n` +
30
+ `Topic: ${topic}`
31
+ );
32
+ }
33
+ if (round === total) {
34
+ return (
35
+ `Round ${round}/${total} — Final Synthesis\n\n` +
36
+ `Synthesize your full multi-round analysis into a coherent, balanced conclusion. ` +
37
+ `Clearly state: (1) points of strong consensus, (2) remaining uncertainties, and ` +
38
+ `(3) your confidence level for each major conclusion. Be definitive where the evidence supports it.`
39
+ );
40
+ }
41
+
42
+ // Intermediate rounds cycle through deepening strategies
43
+ const strategies = [
44
+ `Round ${round}/${total} — Counterarguments & Critique\n\n` +
45
+ `Critically examine your previous analysis. What are the strongest counterarguments? ` +
46
+ `Where might you be wrong or overconfident? What evidence supports alternative viewpoints?`,
47
+ `Round ${round}/${total} — Evidence Assessment\n\n` +
48
+ `Assess the quality and strength of evidence on all sides. Distinguish between what is ` +
49
+ `well-established, what is probable, and what remains genuinely uncertain or contested.`,
50
+ `Round ${round}/${total} — Perspectives & Edge Cases\n\n` +
51
+ `Consider perspectives you have not yet explored. What would domain experts disagree on? ` +
52
+ `Are there edge cases, regional differences, or temporal factors that affect the analysis?`,
53
+ ];
54
+ return strategies[(round - 2) % strategies.length];
55
+ }
56
+
57
+ /**
58
+ * Formats the final CVP output as structured Markdown.
59
+ */
60
+ function formatConsensusResult(topic, rounds, roundResults, model) {
61
+ const lines = [
62
+ `## Consensus Validation Protocol — Results`,
63
+ ``,
64
+ `| Field | Value |`,
65
+ `|-------|-------|`,
66
+ `| **Topic** | ${topic} |`,
67
+ `| **Rounds completed** | ${rounds} |`,
68
+ `| **Model** | ${model} |`,
69
+ ``,
70
+ ];
71
+
72
+ for (const r of roundResults) {
73
+ lines.push(`### Round ${r.round}`);
74
+ lines.push(``);
75
+ lines.push(r.content);
76
+ lines.push(``);
77
+ }
78
+
79
+ return lines.join("\n");
80
+ }
81
+
82
+ // -- Tool definitions --------------------------------------------------------
83
+
84
+ /**
85
+ * Returns the MCP tool schema array. Called on each ListTools request
86
+ * so descriptions always reflect the current resolved model names.
87
+ *
88
+ * @param {{ chatModel: string, imageModel: string }} config
89
+ * @returns {Array<object>}
90
+ */
91
+ export function getToolDefinitions({ chatModel, imageModel }) {
92
+ return [
93
+ {
94
+ name: "ask_grok",
95
+ description:
96
+ "Ask Grok a question and get a response. " +
97
+ `Default model: ${chatModel}. ` +
98
+ "Supports system prompts and sampling parameters (temperature, max_tokens, top_p). " +
99
+ "Run list_models to see all available model options.",
100
+ inputSchema: {
101
+ type: "object",
102
+ properties: {
103
+ prompt: {
104
+ type: "string",
105
+ description: "The question or prompt to send to Grok",
106
+ },
107
+ system_prompt: {
108
+ type: "string",
109
+ description:
110
+ "Optional system prompt to set Grok's behavior and persona for this request.",
111
+ },
112
+ model: {
113
+ type: "string",
114
+ description:
115
+ `Chat model to use for this request. Defaults to "${chatModel}". ` +
116
+ "Use list_models to see available chat models.",
117
+ },
118
+ temperature: {
119
+ type: "number",
120
+ description:
121
+ "Sampling temperature (0-2). Lower values make output more deterministic. Default: model-dependent.",
122
+ },
123
+ max_tokens: {
124
+ type: "number",
125
+ description:
126
+ "Maximum number of tokens to generate in the response.",
127
+ },
128
+ top_p: {
129
+ type: "number",
130
+ description:
131
+ "Nucleus sampling: only consider tokens with cumulative probability up to this value (0-1).",
132
+ },
133
+ },
134
+ required: ["prompt"],
135
+ },
136
+ },
137
+ {
138
+ name: "generate_image",
139
+ description:
140
+ "Generate an image using Grok's Aurora image model and save it to a local file. " +
141
+ `Default model: ${imageModel}. ` +
142
+ "Use the optional 'model' parameter to use a different image model.",
143
+ inputSchema: {
144
+ type: "object",
145
+ properties: {
146
+ prompt: {
147
+ type: "string",
148
+ description: "Text description of the image to generate",
149
+ },
150
+ file_path: {
151
+ type: "string",
152
+ description:
153
+ "Path where the image file should be saved. Relative paths resolve from cwd; " +
154
+ "absolute paths must be within SAFE_WRITE_BASE_DIR (or cwd if unset). Example: images/output.png",
155
+ },
156
+ n: {
157
+ type: "number",
158
+ description: "Number of image variations to generate (1-10, default 1)",
159
+ },
160
+ model: {
161
+ type: "string",
162
+ description:
163
+ `Image model to use for this request. Defaults to "${imageModel}". ` +
164
+ "Use list_models to see available image models.",
165
+ },
166
+ },
167
+ required: ["prompt", "file_path"],
168
+ },
169
+ },
170
+ {
171
+ name: "list_models",
172
+ description:
173
+ "List all xAI models available to your account, including their IDs and capabilities. " +
174
+ "Use this to discover which models you can pass to ask_grok or generate_image. " +
175
+ "You can also filter by type: 'chat' for language models or 'image' for image generation.",
176
+ inputSchema: {
177
+ type: "object",
178
+ properties: {
179
+ filter: {
180
+ type: "string",
181
+ enum: ["all", "chat", "image"],
182
+ description:
183
+ "Filter models by capability. " +
184
+ "'chat' returns language/reasoning models, " +
185
+ "'image' returns image generation models, " +
186
+ "'all' returns everything (default).",
187
+ },
188
+ },
189
+ required: [],
190
+ },
191
+ },
192
+ {
193
+ name: "grok_consensus",
194
+ description:
195
+ "Runs a full iterative Consensus Validation Protocol (CVP) between Claude and Grok. " +
196
+ "Returns a structured final summary. Default 3-5 rounds. " +
197
+ "Supports custom round count via the 'rounds' argument.",
198
+ inputSchema: {
199
+ type: "object",
200
+ properties: {
201
+ topic: {
202
+ type: "string",
203
+ description:
204
+ "The topic, claim, or question to analyze through the consensus protocol.",
205
+ },
206
+ rounds: {
207
+ type: "number",
208
+ description:
209
+ "Number of analysis rounds to run. Omit for the default (3 rounds). " +
210
+ "Higher values (up to 10) yield deeper analysis at the cost of latency.",
211
+ },
212
+ },
213
+ required: ["topic"],
214
+ },
215
+ },
216
+ ];
217
+ }
218
+
219
+ // -- Tool handlers -----------------------------------------------------------
220
+
221
+ /**
222
+ * Creates tool handler functions bound to the provided server context.
223
+ *
224
+ * @param {object} ctx
225
+ * @param {Function} ctx.xaiPost - Authenticated POST to xAI API.
226
+ * @param {Function} ctx.xaiGet - Authenticated GET from xAI API.
227
+ * @param {Function} ctx.safeWrite - Safe file writer.
228
+ * @param {Function} ctx.buildFilePath - Multi-image path builder.
229
+ * @param {Function} ctx.downloadBuffer - URL downloader.
230
+ * @param {Function} ctx.resolve - path.resolve.
231
+ * @param {object} ctx.config - Mutable config object with chatModel, imageModel, etc.
232
+ * @returns {Record<string, Function>}
233
+ */
234
+ export function createToolHandlers(ctx) {
235
+ const {
236
+ xaiPost,
237
+ xaiGet,
238
+ safeWrite,
239
+ buildFilePath,
240
+ downloadBuffer,
241
+ resolve,
242
+ config,
243
+ } = ctx;
244
+
245
+ // -- ask_grok --------------------------------------------------------------
246
+
247
+ async function handleAskGrok(args) {
248
+ if (!args || typeof args.prompt !== "string" || !args.prompt.trim()) {
249
+ throw new Error("Invalid arguments: 'prompt' must be a non-empty string");
250
+ }
251
+ if (args.prompt.length > config.maxPromptLength) {
252
+ throw new Error(
253
+ `Prompt too long: ${args.prompt.length} chars exceeds the ${config.maxPromptLength} char limit`,
254
+ );
255
+ }
256
+
257
+ const model =
258
+ typeof args.model === "string" && args.model.trim()
259
+ ? args.model.trim()
260
+ : config.chatModel;
261
+
262
+ const messages = [];
263
+ if (typeof args.system_prompt === "string" && args.system_prompt.trim()) {
264
+ messages.push({ role: "system", content: args.system_prompt });
265
+ }
266
+ messages.push({ role: "user", content: args.prompt });
267
+
268
+ const requestBody = { model, messages };
269
+ if (typeof args.temperature === "number") requestBody.temperature = args.temperature;
270
+ if (typeof args.max_tokens === "number") requestBody.max_tokens = args.max_tokens;
271
+ if (typeof args.top_p === "number") requestBody.top_p = args.top_p;
272
+
273
+ const data = await xaiPost("/chat/completions", requestBody);
274
+
275
+ const messageContent = data?.choices?.[0]?.message?.content;
276
+ const text =
277
+ typeof messageContent === "string"
278
+ ? messageContent
279
+ : messageContent != null
280
+ ? JSON.stringify(messageContent)
281
+ : "No response";
282
+ return { content: [{ type: "text", text }] };
283
+ }
284
+
285
+ // -- generate_image --------------------------------------------------------
286
+
287
+ async function handleGenerateImage(args) {
288
+ if (!args || typeof args.prompt !== "string" || !args.prompt.trim()) {
289
+ throw new Error("Invalid arguments: 'prompt' must be a non-empty string");
290
+ }
291
+ if (args.prompt.length > config.maxPromptLength) {
292
+ throw new Error(
293
+ `Prompt too long: ${args.prompt.length} chars exceeds the ${config.maxPromptLength} char limit`,
294
+ );
295
+ }
296
+ if (typeof args.file_path !== "string" || !args.file_path.trim()) {
297
+ throw new Error("Invalid arguments: 'file_path' must be a non-empty string");
298
+ }
299
+ if (args.n != null && (!Number.isInteger(args.n) || args.n < 1)) {
300
+ throw new Error("Invalid arguments: 'n' must be a positive integer");
301
+ }
302
+
303
+ const n = Math.min(Math.max(args.n ?? 1, 1), config.maxImageVariations);
304
+ const model =
305
+ typeof args.model === "string" && args.model.trim()
306
+ ? args.model.trim()
307
+ : config.imageModel;
308
+
309
+ const data = await xaiPost("/images/generations", {
310
+ model,
311
+ prompt: args.prompt,
312
+ n,
313
+ });
314
+
315
+ const images = Array.isArray(data?.data) ? data.data : [];
316
+ if (images.length === 0) {
317
+ throw new Error("xAI API did not return any images");
318
+ }
319
+
320
+ const saved = [];
321
+ for (let i = 0; i < images.length; i++) {
322
+ const imageUrl = images[i]?.url;
323
+ if (typeof imageUrl !== "string" || !imageUrl) {
324
+ throw new Error(`xAI API returned an invalid image URL at index ${i}`);
325
+ }
326
+
327
+ const buffer = await downloadBuffer(imageUrl);
328
+ const dest = buildFilePath(args.file_path, i, images.length);
329
+ await safeWrite(dest, buffer);
330
+ saved.push(dest);
331
+ }
332
+
333
+ return {
334
+ content: [
335
+ {
336
+ type: "text",
337
+ text: `Generated and saved ${saved.length} image(s):\n${saved.join("\n")}`,
338
+ },
339
+ ],
340
+ };
341
+ }
342
+
343
+ // -- list_models -----------------------------------------------------------
344
+
345
+ async function handleListModels(args) {
346
+ const filter = args?.filter ?? "all";
347
+ if (!["all", "chat", "image"].includes(filter)) {
348
+ throw new Error("Invalid arguments: 'filter' must be 'all', 'chat', or 'image'");
349
+ }
350
+
351
+ const data = await xaiGet("/models");
352
+ const models = Array.isArray(data?.data) ? data.data : [];
353
+
354
+ if (models.length === 0) {
355
+ return { content: [{ type: "text", text: "No models returned by the xAI API." }] };
356
+ }
357
+
358
+ const isImageModel = (id) => /image|imagine|aurora/i.test(id);
359
+
360
+ const filtered = models.filter((m) => {
361
+ if (filter === "all") return true;
362
+ const isImg = isImageModel(m.id ?? "");
363
+ return filter === "image" ? isImg : !isImg;
364
+ });
365
+
366
+ if (filtered.length === 0) {
367
+ return {
368
+ content: [
369
+ {
370
+ type: "text",
371
+ text: `No ${filter} models found. Try filter: "all" to see everything.`,
372
+ },
373
+ ],
374
+ };
375
+ }
376
+
377
+ filtered.sort((a, b) => {
378
+ const aImg = isImageModel(a.id ?? "");
379
+ const bImg = isImageModel(b.id ?? "");
380
+ if (aImg !== bImg) return aImg ? 1 : -1;
381
+ return (a.id ?? "").localeCompare(b.id ?? "");
382
+ });
383
+
384
+ const lines = [
385
+ `${filtered.length} model(s) available${filter !== "all" ? ` (filter: ${filter})` : ""}:`,
386
+ "",
387
+ ];
388
+
389
+ for (const m of filtered) {
390
+ const id = m.id ?? "unknown";
391
+ const type = isImageModel(id) ? "image" : "chat";
392
+ const isDefaultChat = id === config.chatModel;
393
+ const isDefaultImage = id === config.imageModel;
394
+ const defaultTag = isDefaultChat
395
+ ? " <- current default (chat)"
396
+ : isDefaultImage
397
+ ? " <- current default (image)"
398
+ : "";
399
+ lines.push(` ${id} [${type}]${defaultTag}`);
400
+ }
401
+
402
+ lines.push("");
403
+ lines.push(`To change the default: set GROK_CHAT_MODEL or GROK_IMAGE_MODEL env vars.`);
404
+ lines.push(`To use once: pass model="<id>" to ask_grok or generate_image.`);
405
+
406
+ return { content: [{ type: "text", text: lines.join("\n") }] };
407
+ }
408
+
409
+ // -- grok_consensus --------------------------------------------------------
410
+
411
+ const CVP_DEFAULT_ROUNDS = 3;
412
+ const CVP_MAX_ROUNDS = 10;
413
+
414
+ async function handleGrokConsensus(args) {
415
+ if (!args || typeof args.topic !== "string" || !args.topic.trim()) {
416
+ throw new Error("Invalid arguments: 'topic' must be a non-empty string");
417
+ }
418
+ if (args.topic.length > config.maxPromptLength) {
419
+ throw new Error(
420
+ `Topic too long: ${args.topic.length} chars exceeds the ${config.maxPromptLength} char limit`,
421
+ );
422
+ }
423
+ if (
424
+ args.rounds != null &&
425
+ (!Number.isInteger(args.rounds) || args.rounds < 1 || args.rounds > CVP_MAX_ROUNDS)
426
+ ) {
427
+ throw new Error(
428
+ `Invalid arguments: 'rounds' must be an integer between 1 and ${CVP_MAX_ROUNDS}`,
429
+ );
430
+ }
431
+
432
+ const topic = args.topic.trim();
433
+ const numRounds = args.rounds ?? CVP_DEFAULT_ROUNDS;
434
+ const model = config.chatModel;
435
+
436
+ // Build conversation incrementally — Grok sees the full history each round.
437
+ const messages = [{ role: "system", content: CVP_SYSTEM_PROMPT }];
438
+
439
+ const roundResults = [];
440
+
441
+ for (let round = 1; round <= numRounds; round++) {
442
+ const userPrompt = cvpRoundPrompt(topic, round, numRounds);
443
+ messages.push({ role: "user", content: userPrompt });
444
+
445
+ const data = await xaiPost("/chat/completions", {
446
+ model,
447
+ messages,
448
+ temperature: 0.7,
449
+ });
450
+
451
+ const content = data?.choices?.[0]?.message?.content ?? "No response";
452
+ messages.push({ role: "assistant", content });
453
+ roundResults.push({ round, content });
454
+ }
455
+
456
+ const text = formatConsensusResult(topic, numRounds, roundResults, model);
457
+ return { content: [{ type: "text", text }] };
458
+ }
459
+
460
+ // -- Handler map -----------------------------------------------------------
461
+
462
+ return {
463
+ ask_grok: handleAskGrok,
464
+ generate_image: handleGenerateImage,
465
+ list_models: handleListModels,
466
+ grok_consensus: handleGrokConsensus,
467
+ };
468
+ }