@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.
- package/README.md +46 -14
- package/dist/index.js +151 -58
- 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
|
-
##
|
|
34
|
+
## Tools
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
### `ask_grok`
|
|
37
37
|
|
|
38
|
-
|
|
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` |
|
|
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
|
-
###
|
|
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
|
|
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: "
|
|
67
|
-
|
|
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 =
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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);
|