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 +63 -5
- package/grok-mcp.mjs +42 -329
- package/grok-mcp.test.mjs +43 -2
- package/package.json +1 -1
- package/protocols/consensus-validation.md +105 -0
- package/src/tools.js +468 -0
package/README.md
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
# Grok MCP Server
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/askgrokmcp)
|
|
4
|
+
[](https://nodejs.org)
|
|
5
|
+
[](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**
|
|
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
|
|
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
|
|
7
|
+
* as tools for AI assistants like Claude Code. Provides four capabilities:
|
|
8
8
|
*
|
|
9
|
-
* - ask_grok:
|
|
10
|
-
* - generate_image:
|
|
11
|
-
* - list_models:
|
|
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
|
|
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.
|
|
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
|
-
// --
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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 () => ({
|
|
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("
|
|
200
|
-
assert.equal(
|
|
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
|
@@ -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
|
+
}
|