@pkwadsy/grok-mcp 1.2.0 → 1.3.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/dist/index.js +117 -46
- package/package.json +1 -1
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
|
|
33
|
+
}
|
|
34
|
+
return paths.map((p) => ({ path: resolve(p), startLine, endLine, force }));
|
|
35
|
+
}
|
|
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)}`);
|
|
26
92
|
}
|
|
27
|
-
return paths.map((p) => ({ path: resolve(p), startLine, endLine }));
|
|
28
93
|
}
|
|
29
|
-
|
|
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) {
|
|
@@ -75,7 +143,15 @@ server.tool("ask_grok", `Ask Grok a question. Grok is great for thinking, planni
|
|
|
75
143
|
files: z
|
|
76
144
|
.array(z.string())
|
|
77
145
|
.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.
|
|
146
|
+
.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.'),
|
|
147
|
+
max_files: z
|
|
148
|
+
.number()
|
|
149
|
+
.optional()
|
|
150
|
+
.describe("Override max file count (default 50). Useful when a glob legitimately matches many files"),
|
|
151
|
+
max_file_size: z
|
|
152
|
+
.number()
|
|
153
|
+
.optional()
|
|
154
|
+
.describe("Override max per-file size in KB (default 32). Applies to all files without :force suffix"),
|
|
79
155
|
system_prompt: z
|
|
80
156
|
.string()
|
|
81
157
|
.optional()
|
|
@@ -93,54 +169,21 @@ server.tool("ask_grok", `Ask Grok a question. Grok is great for thinking, planni
|
|
|
93
169
|
.boolean()
|
|
94
170
|
.optional()
|
|
95
171
|
.describe("Enable X/Twitter search to find and analyze posts"),
|
|
96
|
-
}, async ({ prompt, files, system_prompt, model, web_search, x_search }) => {
|
|
172
|
+
}, async ({ prompt, files, max_files, max_file_size, system_prompt, model, web_search, x_search }) => {
|
|
97
173
|
const messages = [];
|
|
98
174
|
if (system_prompt) {
|
|
99
175
|
messages.push({ role: "system", content: system_prompt });
|
|
100
176
|
}
|
|
101
177
|
let userContent = prompt;
|
|
102
178
|
if (files && files.length > 0) {
|
|
179
|
+
const result = resolveFiles(files, { maxFiles: max_files, maxFileSize: max_file_size });
|
|
180
|
+
if (!result.ok) {
|
|
181
|
+
return { isError: true, content: [{ type: "text", text: result.error }] };
|
|
182
|
+
}
|
|
103
183
|
const cwd = process.cwd();
|
|
104
184
|
const fileBlocks = [`Working directory: ${cwd}\n`];
|
|
105
|
-
const
|
|
106
|
-
|
|
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---`);
|
|
134
|
-
}
|
|
135
|
-
catch (err) {
|
|
136
|
-
errors.push(`${spec.path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
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
|
-
};
|
|
185
|
+
for (const f of result.files) {
|
|
186
|
+
fileBlocks.push(`--- ${f.path}${f.range} ---\n${f.content}\n---`);
|
|
144
187
|
}
|
|
145
188
|
userContent = `${fileBlocks.join("\n\n")}\n\n${prompt}`;
|
|
146
189
|
}
|
|
@@ -161,5 +204,33 @@ server.tool("ask_grok", `Ask Grok a question. Grok is great for thinking, planni
|
|
|
161
204
|
content: [{ type: "text", text: response }],
|
|
162
205
|
};
|
|
163
206
|
});
|
|
207
|
+
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()}`, {
|
|
208
|
+
files: z
|
|
209
|
+
.array(z.string())
|
|
210
|
+
.describe('Files to check. Same syntax as ask_grok: "path/to/file", "path/to/file:10-30", "src/**/*.ts", "large-file.ts:force"'),
|
|
211
|
+
max_files: z
|
|
212
|
+
.number()
|
|
213
|
+
.optional()
|
|
214
|
+
.describe("Override max file count (default 50)"),
|
|
215
|
+
max_file_size: z
|
|
216
|
+
.number()
|
|
217
|
+
.optional()
|
|
218
|
+
.describe("Override max per-file size in KB (default 32)"),
|
|
219
|
+
}, async ({ files, max_files, max_file_size }) => {
|
|
220
|
+
const result = resolveFiles(files, { maxFiles: max_files, maxFileSize: max_file_size });
|
|
221
|
+
if (!result.ok) {
|
|
222
|
+
return { isError: true, content: [{ type: "text", text: result.error }] };
|
|
223
|
+
}
|
|
224
|
+
const sorted = [...result.files].sort((a, b) => b.bytes - a.bytes);
|
|
225
|
+
const lines = [
|
|
226
|
+
`${result.files.length} file(s), ${Math.round(result.totalBytes / 1024)} KB total (limit ${MAX_TOTAL_BYTES / 1024} KB)`,
|
|
227
|
+
"",
|
|
228
|
+
...sorted.map((f) => {
|
|
229
|
+
const kb = (f.bytes / 1024).toFixed(1);
|
|
230
|
+
return ` ${kb} KB ${f.path}${f.range}`;
|
|
231
|
+
}),
|
|
232
|
+
];
|
|
233
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
234
|
+
});
|
|
164
235
|
const transport = new StdioServerTransport();
|
|
165
236
|
await server.connect(transport);
|