@pkwadsy/grok-mcp 1.1.1 → 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.
Files changed (2) hide show
  1. package/dist/index.js +139 -46
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,8 +1,101 @@
1
1
  #!/usr/bin/env node
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { readFile } from "node:fs/promises";
4
+ import { globSync, readFileSync } from "node:fs";
5
+ import { resolve } from "node:path";
5
6
  import { z } from "zod";
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
+ }
15
+ // "path/to/file:10-30" or "path/to/file:10" or "path/to/file" or "src/**/*.ts"
16
+ const match = input.match(/^(.+?):(\d+)(?:-(\d+))?$/);
17
+ let pattern;
18
+ let startLine;
19
+ let endLine;
20
+ if (match) {
21
+ pattern = match[1];
22
+ startLine = parseInt(match[2], 10);
23
+ endLine = match[3] ? parseInt(match[3], 10) : startLine;
24
+ }
25
+ else {
26
+ pattern = input;
27
+ }
28
+ const resolved = resolve(pattern);
29
+ if (/[*?[\]]/.test(pattern)) {
30
+ const paths = globSync(pattern).sort();
31
+ if (paths.length === 0) {
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)}`);
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 };
98
+ }
6
99
  const apiKey = process.env.XAI_API_KEY;
7
100
  if (!apiKey) {
8
101
  console.error("XAI_API_KEY environment variable is required");
@@ -48,13 +141,17 @@ async function callGrok(options) {
48
141
  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()}`, {
49
142
  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"),
50
143
  files: z
51
- .array(z.object({
52
- path: z.string().describe("Absolute path to the file"),
53
- start_line: z.number().optional().describe("First line to include (1-based, inclusive)"),
54
- end_line: z.number().optional().describe("Last line to include (1-based, inclusive)"),
55
- }))
144
+ .array(z.string())
145
+ .optional()
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()
56
149
  .optional()
57
- .describe("Files to read and include in the context sent to Grok. Each file is included with its path and line numbers. Use start_line/end_line to include only a specific range"),
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"),
58
155
  system_prompt: z
59
156
  .string()
60
157
  .optional()
@@ -72,53 +169,21 @@ server.tool("ask_grok", `Ask Grok a question. Grok is great for thinking, planni
72
169
  .boolean()
73
170
  .optional()
74
171
  .describe("Enable X/Twitter search to find and analyze posts"),
75
- }, 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 }) => {
76
173
  const messages = [];
77
174
  if (system_prompt) {
78
175
  messages.push({ role: "system", content: system_prompt });
79
176
  }
80
177
  let userContent = prompt;
81
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
+ }
82
183
  const cwd = process.cwd();
83
184
  const fileBlocks = [`Working directory: ${cwd}\n`];
84
- const errors = [];
85
- for (const file of files) {
86
- try {
87
- const raw = await readFile(file.path, "utf-8");
88
- const allLines = raw.split("\n");
89
- const totalLines = allLines.length;
90
- if (file.start_line && file.start_line > totalLines) {
91
- errors.push(`${file.path}: start_line ${file.start_line} is past end of file (${totalLines} lines)`);
92
- continue;
93
- }
94
- if (file.end_line && file.end_line > totalLines) {
95
- errors.push(`${file.path}: end_line ${file.end_line} is past end of file (${totalLines} lines)`);
96
- continue;
97
- }
98
- if (file.start_line && file.end_line && file.start_line > file.end_line) {
99
- errors.push(`${file.path}: start_line ${file.start_line} is after end_line ${file.end_line}`);
100
- continue;
101
- }
102
- const start = file.start_line ? file.start_line - 1 : 0;
103
- const end = file.end_line ? file.end_line : totalLines;
104
- const sliced = allLines.slice(start, end);
105
- const numbered = sliced
106
- .map((line, i) => `${start + i + 1}\t${line}`)
107
- .join("\n");
108
- const range = file.start_line || file.end_line
109
- ? `:${file.start_line ?? 1}-${file.end_line ?? allLines.length}`
110
- : "";
111
- fileBlocks.push(`--- ${file.path}${range} ---\n${numbered}\n---`);
112
- }
113
- catch (err) {
114
- errors.push(`${file.path}: ${err instanceof Error ? err.message : String(err)}`);
115
- }
116
- }
117
- if (errors.length > 0) {
118
- return {
119
- isError: true,
120
- content: [{ type: "text", text: `Failed to read ${errors.length} file(s):\n${errors.join("\n")}\n\nFix the paths and try again.` }],
121
- };
185
+ for (const f of result.files) {
186
+ fileBlocks.push(`--- ${f.path}${f.range} ---\n${f.content}\n---`);
122
187
  }
123
188
  userContent = `${fileBlocks.join("\n\n")}\n\n${prompt}`;
124
189
  }
@@ -139,5 +204,33 @@ server.tool("ask_grok", `Ask Grok a question. Grok is great for thinking, planni
139
204
  content: [{ type: "text", text: response }],
140
205
  };
141
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
+ });
142
235
  const transport = new StdioServerTransport();
143
236
  await server.connect(transport);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pkwadsy/grok-mcp",
3
- "version": "1.1.1",
3
+ "version": "1.3.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",