@pkwadsy/grok-mcp 1.2.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.
Files changed (3) hide show
  1. package/README.md +46 -14
  2. package/dist/index.js +151 -58
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -31,22 +31,55 @@ Add to your project's `.mcp.json`:
31
31
  }
32
32
  ```
33
33
 
34
- ## Tool: `ask_grok`
34
+ ## Tools
35
35
 
36
- Single tool with options for different use cases.
36
+ ### `ask_grok`
37
37
 
38
- ### Parameters
38
+ Ask Grok a question with optional file context, web search, and X/Twitter search.
39
39
 
40
40
  | Parameter | Type | Required | Description |
41
41
  |-----------|------|----------|-------------|
42
42
  | `prompt` | string | yes | The question or task for Grok |
43
- | `files` | array | no | Files to include in context (path, optional start_line/end_line) |
43
+ | `files` | string[] | no | Files to include in context (see file syntax below) |
44
+ | `max_files` | number | no | Override max file count (default 50) |
45
+ | `max_file_size` | number | no | Override max per-file size in KB (default 32) |
44
46
  | `system_prompt` | string | no | Custom system prompt |
45
47
  | `model` | string | no | Model to use (default: `grok-4.20-multi-agent`) |
46
48
  | `web_search` | boolean | no | Web search, enabled by default |
47
49
  | `x_search` | boolean | no | Enable X/Twitter search |
48
50
 
49
- ### Available Models
51
+ ### `check_files`
52
+
53
+ Dry-run file resolution. Validates all files and shows sizes without calling Grok. If `check_files` passes, `ask_grok` will too.
54
+
55
+ | Parameter | Type | Required | Description |
56
+ |-----------|------|----------|-------------|
57
+ | `files` | string[] | yes | Files to check (same syntax as `ask_grok`) |
58
+ | `max_files` | number | no | Override max file count (default 50) |
59
+ | `max_file_size` | number | no | Override max per-file size in KB (default 32) |
60
+
61
+ ### File syntax
62
+
63
+ Files are passed as an array of strings with compact syntax:
64
+
65
+ | Syntax | Description |
66
+ |--------|-------------|
67
+ | `"src/index.ts"` | Whole file |
68
+ | `"src/index.ts:10-30"` | Lines 10 to 30 |
69
+ | `"src/index.ts:10"` | Just line 10 |
70
+ | `"src/**/*.ts"` | Glob pattern |
71
+ | `"large-file.ts:force"` | Bypass per-file size limit |
72
+ | `"large-file.ts:1-100:force"` | Combine line range with force |
73
+
74
+ ### Safety limits
75
+
76
+ | Limit | Default | Override |
77
+ |-------|---------|----------|
78
+ | Files per call | 50 | `max_files` param |
79
+ | Per-file size | 32 KB | `max_file_size` param or `:force` suffix |
80
+ | Total context | 256 KB | Hard cap, not overridable |
81
+
82
+ ### Available models
50
83
 
51
84
  - `grok-4.20-multi-agent` — multi-agent mode, great for architecture and planning (default)
52
85
  - `grok-4.20-reasoning` — flagship reasoning
@@ -56,21 +89,15 @@ Single tool with options for different use cases.
56
89
 
57
90
  ### Examples
58
91
 
59
- **Ask a question:**
60
- ```
61
- prompt: "What are the trade-offs between microservices and monoliths?"
62
- ```
63
-
64
- **Deep architecture planning (multi-agent):**
92
+ **Ask with file context:**
65
93
  ```
66
- prompt: "Design a system architecture for a real-time collaborative editor"
67
- model: "grok-4.20-multi-agent"
94
+ prompt: "Review this code for bugs"
95
+ files: ["src/index.ts", "src/utils.ts:20-50"]
68
96
  ```
69
97
 
70
98
  **Search the web:**
71
99
  ```
72
100
  prompt: "What happened in tech news today?"
73
- web_search: true
74
101
  ```
75
102
 
76
103
  **Search X/Twitter:**
@@ -79,6 +106,11 @@ prompt: "What are people saying about the new React release?"
79
106
  x_search: true
80
107
  ```
81
108
 
109
+ **Check files before asking:**
110
+ ```
111
+ files: ["src/**/*.ts"]
112
+ ```
113
+
82
114
  ## License
83
115
 
84
116
  MIT
package/dist/index.js CHANGED
@@ -5,8 +5,15 @@ import { globSync, readFileSync } from "node:fs";
5
5
  import { resolve } from "node:path";
6
6
  import { z } from "zod";
7
7
  function parseFileArg(arg) {
8
+ // Strip :force suffix first
9
+ let force = false;
10
+ let input = arg;
11
+ if (input.endsWith(":force")) {
12
+ force = true;
13
+ input = input.slice(0, -6);
14
+ }
8
15
  // "path/to/file:10-30" or "path/to/file:10" or "path/to/file" or "src/**/*.ts"
9
- const match = arg.match(/^(.+?):(\d+)(?:-(\d+))?$/);
16
+ const match = input.match(/^(.+?):(\d+)(?:-(\d+))?$/);
10
17
  let pattern;
11
18
  let startLine;
12
19
  let endLine;
@@ -16,17 +23,78 @@ function parseFileArg(arg) {
16
23
  endLine = match[3] ? parseInt(match[3], 10) : startLine;
17
24
  }
18
25
  else {
19
- pattern = arg;
26
+ pattern = input;
20
27
  }
21
28
  const resolved = resolve(pattern);
22
29
  if (/[*?[\]]/.test(pattern)) {
23
30
  const paths = globSync(pattern).sort();
24
31
  if (paths.length === 0) {
25
- return [{ path: pattern }]; // will fail at read time with a clear error
32
+ return [{ path: pattern, force }]; // will fail at read time with a clear error
26
33
  }
27
- return paths.map((p) => ({ path: resolve(p), startLine, endLine }));
34
+ return paths.map((p) => ({ path: resolve(p), startLine, endLine, force }));
28
35
  }
29
- return [{ path: resolved, startLine, endLine }];
36
+ return [{ path: resolved, startLine, endLine, force }];
37
+ }
38
+ const DEFAULT_MAX_FILES = 50;
39
+ const MAX_TOTAL_BYTES = 256 * 1024; // 256 KB hard cap
40
+ const DEFAULT_MAX_SINGLE_BYTES = 32 * 1024; // 32 KB
41
+ function resolveFiles(fileArgs, opts) {
42
+ const maxFiles = opts?.maxFiles ?? DEFAULT_MAX_FILES;
43
+ const maxSingleBytes = opts?.maxFileSize ? opts.maxFileSize * 1024 : DEFAULT_MAX_SINGLE_BYTES;
44
+ const errors = [];
45
+ const resolved = [];
46
+ const specs = fileArgs.flatMap(parseFileArg);
47
+ if (specs.length > maxFiles) {
48
+ return { ok: false, error: `Too many files: ${specs.length} resolved (limit ${maxFiles}). This usually means a glob matched more than intended (e.g. node_modules). Use a more specific pattern.` };
49
+ }
50
+ let totalBytes = 0;
51
+ for (const spec of specs) {
52
+ try {
53
+ const raw = readFileSync(spec.path, "utf-8");
54
+ if (!spec.force && raw.length > maxSingleBytes && !spec.startLine && !spec.endLine) {
55
+ const kb = Math.round(raw.length / 1024);
56
+ errors.push(`${spec.path}: file is ${kb} KB (limit ${maxSingleBytes / 1024} KB per file). Use ":force" to override, or line ranges to include only the relevant part, e.g. "${spec.path}:1-100"`);
57
+ continue;
58
+ }
59
+ const allLines = raw.split("\n");
60
+ const totalLines = allLines.length;
61
+ if (spec.startLine && spec.startLine > totalLines) {
62
+ errors.push(`${spec.path}: line ${spec.startLine} is past end of file (${totalLines} lines)`);
63
+ continue;
64
+ }
65
+ if (spec.endLine && spec.endLine > totalLines) {
66
+ errors.push(`${spec.path}: line ${spec.endLine} is past end of file (${totalLines} lines)`);
67
+ continue;
68
+ }
69
+ if (spec.startLine && spec.endLine && spec.startLine > spec.endLine) {
70
+ errors.push(`${spec.path}: start line ${spec.startLine} is after end line ${spec.endLine}`);
71
+ continue;
72
+ }
73
+ const start = spec.startLine ? spec.startLine - 1 : 0;
74
+ const end = spec.endLine ? spec.endLine : totalLines;
75
+ const sliced = allLines.slice(start, end);
76
+ const numbered = sliced
77
+ .map((line, i) => `${start + i + 1}\t${line}`)
78
+ .join("\n");
79
+ totalBytes += numbered.length;
80
+ if (totalBytes > MAX_TOTAL_BYTES) {
81
+ const kb = Math.round(totalBytes / 1024);
82
+ errors.push(`Total file context is ${kb} KB (hard limit ${MAX_TOTAL_BYTES / 1024} KB). Include fewer files or use line ranges to narrow down.`);
83
+ break;
84
+ }
85
+ const range = spec.startLine || spec.endLine
86
+ ? `:${spec.startLine ?? 1}-${spec.endLine ?? totalLines}`
87
+ : "";
88
+ resolved.push({ path: spec.path, range, content: numbered, bytes: numbered.length });
89
+ }
90
+ catch (err) {
91
+ errors.push(`${spec.path}: ${err instanceof Error ? err.message : String(err)}`);
92
+ }
93
+ }
94
+ if (errors.length > 0) {
95
+ return { ok: false, error: `File context error:\n${errors.join("\n")}\n\nFix and try again.` };
96
+ }
97
+ return { ok: true, files: resolved, totalBytes };
30
98
  }
31
99
  const apiKey = process.env.XAI_API_KEY;
32
100
  if (!apiKey) {
@@ -41,8 +109,16 @@ async function callGrok(options) {
41
109
  const model = options.model ?? "grok-4.20-multi-agent";
42
110
  const body = {
43
111
  model,
44
- input: options.messages,
112
+ input: options.messages.map((m) => ({
113
+ role: m.role,
114
+ content: m.content,
115
+ })),
116
+ store: true,
117
+ include: ["reasoning.encrypted_content"],
45
118
  };
119
+ if (options.previousResponseId) {
120
+ body.previous_response_id = options.previousResponseId;
121
+ }
46
122
  if (options.tools && options.tools.length > 0) {
47
123
  body.tools = options.tools;
48
124
  }
@@ -63,19 +139,31 @@ async function callGrok(options) {
63
139
  if (item.type === "message" && item.content) {
64
140
  for (const block of item.content) {
65
141
  if (block.type === "output_text" && block.text) {
66
- return block.text;
142
+ return { text: block.text, responseId: data.id ?? undefined };
67
143
  }
68
144
  }
69
145
  }
70
146
  }
71
147
  throw new Error("No text content in Grok response");
72
148
  }
73
- server.tool("ask_grok", `Ask Grok a question. Grok is great for thinking, planning, architecture, and real-time search via web and X/Twitter. Use web_search for current information from the internet. Use x_search to find and analyze posts on X/Twitter. IMPORTANT: Grok has no context about your conversation or codebase. Always include all relevant context directly in the prompt — file contents, error messages, architecture details, constraints, and goals. The more context you provide, the better Grok's response will be. Do not assume Grok knows anything about the current project. Use the files parameter to automatically include file contents with line numbers — this is preferred over pasting code into the prompt. File paths are resolved relative to the server working directory: ${process.cwd()}`, {
149
+ server.tool("ask_grok", `Ask Grok a question. Grok is great for thinking, planning, architecture, and real-time search via web and X/Twitter. Use web_search for current information from the internet. Use x_search to find and analyze posts on X/Twitter. IMPORTANT: Grok has no context about your conversation or codebase. Always include all relevant context directly in the prompt — file contents, error messages, architecture details, constraints, and goals. The more context you provide, the better Grok's response will be. Do not assume Grok knows anything about the current project. Use the files parameter to automatically include file contents with line numbers — this is preferred over pasting code into the prompt. File paths are resolved relative to the server working directory: ${process.cwd()}. Responses include a response_id — pass it back as previous_response_id to continue a conversation without resending context.`, {
74
150
  prompt: z.string().describe("The question or task for Grok. Include all relevant context — constraints, background, and goals — since Grok has no access to your conversation or files. Use the files parameter to attach source code rather than pasting it inline"),
151
+ previous_response_id: z
152
+ .string()
153
+ .optional()
154
+ .describe("Response ID from a previous ask_grok call. Continues the conversation — Grok remembers all prior context so you don't need to resend files or repeat background. Not supported by multi-agent model (beta limitation)"),
75
155
  files: z
76
156
  .array(z.string())
77
157
  .optional()
78
- .describe('Files to include in context. Compact syntax: "path/to/file" (whole file), "path/to/file:10-30" (lines 10-30), "path/to/file:10" (just line 10), "src/**/*.ts" (glob pattern). Paths resolve relative to server cwd. All files are sent to Grok with line numbers.'),
158
+ .describe('Files to include in context. Compact syntax: "path/to/file" (whole file), "path/to/file:10-30" (lines 10-30), "path/to/file:10" (just line 10), "src/**/*.ts" (glob pattern), "large-file.ts:force" (bypass per-file size limit). Paths resolve relative to server cwd.'),
159
+ max_files: z
160
+ .number()
161
+ .optional()
162
+ .describe("Override max file count (default 50). Useful when a glob legitimately matches many files"),
163
+ max_file_size: z
164
+ .number()
165
+ .optional()
166
+ .describe("Override max per-file size in KB (default 32). Applies to all files without :force suffix"),
79
167
  system_prompt: z
80
168
  .string()
81
169
  .optional()
@@ -93,58 +181,31 @@ server.tool("ask_grok", `Ask Grok a question. Grok is great for thinking, planni
93
181
  .boolean()
94
182
  .optional()
95
183
  .describe("Enable X/Twitter search to find and analyze posts"),
96
- }, async ({ prompt, files, system_prompt, model, web_search, x_search }) => {
184
+ }, async ({ prompt, previous_response_id, files, max_files, max_file_size, system_prompt, model, web_search, x_search }) => {
97
185
  const messages = [];
98
- if (system_prompt) {
99
- messages.push({ role: "system", content: system_prompt });
100
- }
101
- let userContent = prompt;
102
- if (files && files.length > 0) {
103
- const cwd = process.cwd();
104
- const fileBlocks = [`Working directory: ${cwd}\n`];
105
- const errors = [];
106
- const specs = files.flatMap(parseFileArg);
107
- for (const spec of specs) {
108
- try {
109
- const raw = readFileSync(spec.path, "utf-8");
110
- const allLines = raw.split("\n");
111
- const totalLines = allLines.length;
112
- if (spec.startLine && spec.startLine > totalLines) {
113
- errors.push(`${spec.path}: line ${spec.startLine} is past end of file (${totalLines} lines)`);
114
- continue;
115
- }
116
- if (spec.endLine && spec.endLine > totalLines) {
117
- errors.push(`${spec.path}: line ${spec.endLine} is past end of file (${totalLines} lines)`);
118
- continue;
119
- }
120
- if (spec.startLine && spec.endLine && spec.startLine > spec.endLine) {
121
- errors.push(`${spec.path}: start line ${spec.startLine} is after end line ${spec.endLine}`);
122
- continue;
123
- }
124
- const start = spec.startLine ? spec.startLine - 1 : 0;
125
- const end = spec.endLine ? spec.endLine : totalLines;
126
- const sliced = allLines.slice(start, end);
127
- const numbered = sliced
128
- .map((line, i) => `${start + i + 1}\t${line}`)
129
- .join("\n");
130
- const range = spec.startLine || spec.endLine
131
- ? `:${spec.startLine ?? 1}-${spec.endLine ?? totalLines}`
132
- : "";
133
- fileBlocks.push(`--- ${spec.path}${range} ---\n${numbered}\n---`);
186
+ // On continuations, Grok already has prior context — only send the new prompt
187
+ if (previous_response_id) {
188
+ messages.push({ role: "user", content: prompt });
189
+ }
190
+ else {
191
+ if (system_prompt) {
192
+ messages.push({ role: "system", content: system_prompt });
193
+ }
194
+ let userContent = prompt;
195
+ if (files && files.length > 0) {
196
+ const result = resolveFiles(files, { maxFiles: max_files, maxFileSize: max_file_size });
197
+ if (!result.ok) {
198
+ return { isError: true, content: [{ type: "text", text: result.error }] };
134
199
  }
135
- catch (err) {
136
- errors.push(`${spec.path}: ${err instanceof Error ? err.message : String(err)}`);
200
+ const cwd = process.cwd();
201
+ const fileBlocks = [`Working directory: ${cwd}\n`];
202
+ for (const f of result.files) {
203
+ fileBlocks.push(`--- ${f.path}${f.range} ---\n${f.content}\n---`);
137
204
  }
205
+ userContent = `${fileBlocks.join("\n\n")}\n\n${prompt}`;
138
206
  }
139
- if (errors.length > 0) {
140
- return {
141
- isError: true,
142
- content: [{ type: "text", text: `Failed to read ${errors.length} file(s):\n${errors.join("\n")}\n\nFix the paths and try again.` }],
143
- };
144
- }
145
- userContent = `${fileBlocks.join("\n\n")}\n\n${prompt}`;
207
+ messages.push({ role: "user", content: userContent });
146
208
  }
147
- messages.push({ role: "user", content: userContent });
148
209
  const tools = [];
149
210
  if (web_search !== false) {
150
211
  tools.push({ type: "web_search" });
@@ -152,14 +213,46 @@ server.tool("ask_grok", `Ask Grok a question. Grok is great for thinking, planni
152
213
  if (x_search) {
153
214
  tools.push({ type: "x_search" });
154
215
  }
155
- const response = await callGrok({
216
+ const result = await callGrok({
156
217
  messages,
157
218
  model,
158
219
  tools: tools.length > 0 ? tools : undefined,
220
+ previousResponseId: previous_response_id,
159
221
  });
222
+ const text = result.responseId
223
+ ? `${result.text}\n\n---\nresponse_id: ${result.responseId}`
224
+ : result.text;
160
225
  return {
161
- content: [{ type: "text", text: response }],
226
+ content: [{ type: "text", text }],
162
227
  };
163
228
  });
229
+ server.tool("check_files", `Dry-run file resolution. Use this before ask_grok to verify files will resolve correctly and check context size. Uses the same validation as ask_grok — if check_files passes, ask_grok will too. File paths resolve relative to: ${process.cwd()}`, {
230
+ files: z
231
+ .array(z.string())
232
+ .describe('Files to check. Same syntax as ask_grok: "path/to/file", "path/to/file:10-30", "src/**/*.ts", "large-file.ts:force"'),
233
+ max_files: z
234
+ .number()
235
+ .optional()
236
+ .describe("Override max file count (default 50)"),
237
+ max_file_size: z
238
+ .number()
239
+ .optional()
240
+ .describe("Override max per-file size in KB (default 32)"),
241
+ }, async ({ files, max_files, max_file_size }) => {
242
+ const result = resolveFiles(files, { maxFiles: max_files, maxFileSize: max_file_size });
243
+ if (!result.ok) {
244
+ return { isError: true, content: [{ type: "text", text: result.error }] };
245
+ }
246
+ const sorted = [...result.files].sort((a, b) => b.bytes - a.bytes);
247
+ const lines = [
248
+ `${result.files.length} file(s), ${Math.round(result.totalBytes / 1024)} KB total (limit ${MAX_TOTAL_BYTES / 1024} KB)`,
249
+ "",
250
+ ...sorted.map((f) => {
251
+ const kb = (f.bytes / 1024).toFixed(1);
252
+ return ` ${kb} KB ${f.path}${f.range}`;
253
+ }),
254
+ ];
255
+ return { content: [{ type: "text", text: lines.join("\n") }] };
256
+ });
164
257
  const transport = new StdioServerTransport();
165
258
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pkwadsy/grok-mcp",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "MCP server that wraps the xAI Grok API — delegate thinking, planning, and search to Grok",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",