@probelabs/probe 0.6.0-rc56

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.
@@ -0,0 +1,499 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
+ import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
5
+ import { exec } from 'child_process';
6
+ import { promisify } from 'util';
7
+ import path from 'path';
8
+ import fs from 'fs-extra';
9
+ import { fileURLToPath } from 'url';
10
+ // Import from parent package
11
+ import { search, query, extract } from '../index.js';
12
+ // Parse command-line arguments
13
+ function parseArgs() {
14
+ const args = process.argv.slice(2);
15
+ const config = {};
16
+ for (let i = 0; i < args.length; i++) {
17
+ if ((args[i] === '--timeout' || args[i] === '-t') && i + 1 < args.length) {
18
+ const timeout = parseInt(args[i + 1], 10);
19
+ if (!isNaN(timeout) && timeout > 0) {
20
+ config.timeout = timeout;
21
+ console.error(`Timeout set to ${timeout} seconds`);
22
+ }
23
+ else {
24
+ console.error(`Invalid timeout value: ${args[i + 1]}. Using default.`);
25
+ }
26
+ i++; // Skip the next argument
27
+ }
28
+ else if (args[i] === '--help' || args[i] === '-h') {
29
+ console.log(`
30
+ Probe MCP Server
31
+
32
+ Usage:
33
+ probe mcp [options]
34
+
35
+ Options:
36
+ --timeout, -t <seconds> Set timeout for search operations (default: 30)
37
+ --help, -h Show this help message
38
+ `);
39
+ process.exit(0);
40
+ }
41
+ }
42
+ return config;
43
+ }
44
+ const cliConfig = parseArgs();
45
+ const execAsync = promisify(exec);
46
+ // Get the package.json to determine the version
47
+ const __filename = fileURLToPath(import.meta.url);
48
+ const __dirname = path.dirname(__filename);
49
+ // Try multiple possible locations for package.json
50
+ let packageVersion = '0.0.0';
51
+ const possiblePaths = [
52
+ path.resolve(__dirname, '..', 'package.json'), // When installed from npm: build/../package.json
53
+ path.resolve(__dirname, '..', '..', 'package.json') // In development: src/../package.json
54
+ ];
55
+ for (const packageJsonPath of possiblePaths) {
56
+ try {
57
+ if (fs.existsSync(packageJsonPath)) {
58
+ console.log(`Found package.json at: ${packageJsonPath}`);
59
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
60
+ if (packageJson.version) {
61
+ packageVersion = packageJson.version;
62
+ console.log(`Using version from package.json: ${packageVersion}`);
63
+ break;
64
+ }
65
+ }
66
+ }
67
+ catch (error) {
68
+ console.error(`Error reading package.json at ${packageJsonPath}:`, error);
69
+ }
70
+ }
71
+ // If we still have 0.0.0, try to get version from npm package
72
+ if (packageVersion === '0.0.0') {
73
+ try {
74
+ // Try to get version from the package name itself
75
+ const result = await execAsync('npm list -g @probelabs/probe --json');
76
+ const npmList = JSON.parse(result.stdout);
77
+ if (npmList.dependencies && npmList.dependencies['@probelabs/probe']) {
78
+ packageVersion = npmList.dependencies['@probelabs/probe'].version;
79
+ console.log(`Using version from npm list: ${packageVersion}`);
80
+ }
81
+ }
82
+ catch (error) {
83
+ console.error('Error getting version from npm:', error);
84
+ }
85
+ }
86
+ // Get the path to the bin directory
87
+ const binDir = path.resolve(__dirname, '..', 'bin');
88
+ console.log(`Bin directory: ${binDir}`);
89
+ class ProbeServer {
90
+ constructor(timeout = 30) {
91
+ this.defaultTimeout = timeout;
92
+ this.server = new Server({
93
+ name: '@probelabs/probe',
94
+ version: packageVersion,
95
+ }, {
96
+ capabilities: {
97
+ tools: {},
98
+ },
99
+ });
100
+ this.setupToolHandlers();
101
+ // Error handling
102
+ this.server.onerror = (error) => console.error('[MCP Error]', error);
103
+ process.on('SIGINT', async () => {
104
+ await this.server.close();
105
+ process.exit(0);
106
+ });
107
+ }
108
+ setupToolHandlers() {
109
+ // Use the tool descriptions defined at the top of the file
110
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
111
+ tools: [
112
+ {
113
+ name: 'search_code',
114
+ description: "Search code in the repository using ElasticSearch. Use this tool first for any code-related questions.",
115
+ inputSchema: {
116
+ type: 'object',
117
+ properties: {
118
+ path: {
119
+ type: 'string',
120
+ description: 'Absolute path to the directory to search in (e.g., "/Users/username/projects/myproject").',
121
+ },
122
+ query: {
123
+ type: 'string',
124
+ description: 'Elastic search query. Supports logical operators (AND, OR, NOT), and grouping with parentheses. Examples: "config", "(term1 OR term2) AND term3". Use quotes for exact matches, like function or type names.',
125
+ },
126
+ filesOnly: {
127
+ type: 'boolean',
128
+ description: 'Skip AST parsing and just output unique files',
129
+ },
130
+ ignore: {
131
+ type: 'array',
132
+ items: { type: 'string' },
133
+ description: 'Custom patterns to ignore (in addition to .gitignore and common patterns)'
134
+ },
135
+ excludeFilenames: {
136
+ type: 'boolean',
137
+ description: 'Exclude filenames from being used for matching'
138
+ },
139
+ exact: {
140
+ type: 'boolean',
141
+ description: 'Perform exact search without tokenization (case-insensitive)'
142
+ },
143
+ allowTests: {
144
+ type: 'boolean',
145
+ description: 'Allow test files and test code blocks in results (disabled by default)'
146
+ },
147
+ session: {
148
+ type: 'string',
149
+ description: 'Session identifier for caching. Set to "new" if unknown, or want to reset cache. Re-use session ID returned from previous searches',
150
+ default: "new",
151
+ },
152
+ timeout: {
153
+ type: 'number',
154
+ description: 'Timeout for the search operation in seconds (default: 30)',
155
+ },
156
+ noGitignore: {
157
+ type: 'boolean',
158
+ description: 'Skip .gitignore files (will use PROBE_NO_GITIGNORE environment variable if not set)',
159
+ }
160
+ },
161
+ required: ['path', 'query']
162
+ },
163
+ },
164
+ {
165
+ name: 'query_code',
166
+ description: "Search code using ast-grep structural pattern matching. Use this tool to find specific code structures like functions, classes, or methods.",
167
+ inputSchema: {
168
+ type: 'object',
169
+ properties: {
170
+ path: {
171
+ type: 'string',
172
+ description: 'Absolute path to the directory to search in (e.g., "/Users/username/projects/myproject").',
173
+ },
174
+ pattern: {
175
+ type: 'string',
176
+ description: 'The ast-grep pattern to search for. Examples: "fn $NAME($$$PARAMS) $$$BODY" for Rust functions, "def $NAME($$$PARAMS): $$$BODY" for Python functions.',
177
+ },
178
+ language: {
179
+ type: 'string',
180
+ description: 'The programming language to search in. If not specified, the tool will try to infer the language from file extensions. Supported languages: rust, javascript, typescript, python, go, c, cpp, java, ruby, php, swift, csharp.',
181
+ },
182
+ ignore: {
183
+ type: 'array',
184
+ items: { type: 'string' },
185
+ description: 'Custom patterns to ignore (in addition to common patterns)',
186
+ },
187
+ maxResults: {
188
+ type: 'number',
189
+ description: 'Maximum number of results to return'
190
+ },
191
+ format: {
192
+ type: 'string',
193
+ enum: ['markdown', 'plain', 'json', 'color'],
194
+ description: 'Output format for the query results'
195
+ },
196
+ timeout: {
197
+ type: 'number',
198
+ description: 'Timeout for the query operation in seconds (default: 30)',
199
+ },
200
+ noGitignore: {
201
+ type: 'boolean',
202
+ description: 'Skip .gitignore files (will use PROBE_NO_GITIGNORE environment variable if not set)',
203
+ }
204
+ },
205
+ required: ['path', 'pattern']
206
+ },
207
+ },
208
+ {
209
+ name: 'extract_code',
210
+ description: "Extract code blocks from files based on line number, or symbol name. Fetch full file when line number is not provided.",
211
+ inputSchema: {
212
+ type: 'object',
213
+ properties: {
214
+ path: {
215
+ type: 'string',
216
+ description: 'Absolute path to the directory to search in (e.g., "/Users/username/projects/myproject").',
217
+ },
218
+ files: {
219
+ type: 'array',
220
+ items: { type: 'string' },
221
+ description: 'Files and lines or sybmbols to extract from: /path/to/file.rs:10, /path/to/file.rs#func_name Path should be absolute.',
222
+ },
223
+ allowTests: {
224
+ type: 'boolean',
225
+ description: 'Allow test files and test code blocks in results (disabled by default)',
226
+ },
227
+ contextLines: {
228
+ type: 'number',
229
+ description: 'Number of context lines to include before and after the extracted block when AST parsing fails to find a suitable node',
230
+ default: 0
231
+ },
232
+ format: {
233
+ type: 'string',
234
+ enum: ['markdown', 'plain', 'json'],
235
+ description: 'Output format for the extracted code',
236
+ default: 'markdown'
237
+ },
238
+ timeout: {
239
+ type: 'number',
240
+ description: 'Timeout for the extract operation in seconds (default: 30)',
241
+ },
242
+ noGitignore: {
243
+ type: 'boolean',
244
+ description: 'Skip .gitignore files (will use PROBE_NO_GITIGNORE environment variable if not set)',
245
+ }
246
+ },
247
+ required: ['path', 'files'],
248
+ },
249
+ },
250
+ ],
251
+ }));
252
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
253
+ if (request.params.name !== 'search_code' && request.params.name !== 'query_code' && request.params.name !== 'extract_code' &&
254
+ request.params.name !== 'probe' && request.params.name !== 'query' && request.params.name !== 'extract') {
255
+ throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
256
+ }
257
+ try {
258
+ let result;
259
+ // Log the incoming request for debugging
260
+ console.error(`Received request for tool: ${request.params.name}`);
261
+ console.error(`Request arguments: ${JSON.stringify(request.params.arguments)}`);
262
+ // Handle both new tool names and legacy tool names
263
+ if (request.params.name === 'search_code' || request.params.name === 'probe') {
264
+ // Ensure arguments is an object
265
+ if (!request.params.arguments || typeof request.params.arguments !== 'object') {
266
+ throw new Error("Arguments must be an object");
267
+ }
268
+ const args = request.params.arguments;
269
+ // Validate required fields
270
+ if (!args.path) {
271
+ throw new Error("Path is required in arguments");
272
+ }
273
+ if (!args.query) {
274
+ throw new Error("Query is required in arguments");
275
+ }
276
+ result = await this.executeCodeSearch(args);
277
+ }
278
+ else if (request.params.name === 'query_code' || request.params.name === 'query') {
279
+ const args = request.params.arguments;
280
+ result = await this.executeCodeQuery(args);
281
+ }
282
+ else { // extract_code or extract
283
+ const args = request.params.arguments;
284
+ result = await this.executeCodeExtract(args);
285
+ }
286
+ return {
287
+ content: [
288
+ {
289
+ type: 'text',
290
+ text: result,
291
+ },
292
+ ],
293
+ };
294
+ }
295
+ catch (error) {
296
+ console.error(`Error executing ${request.params.name}:`, error);
297
+ return {
298
+ content: [
299
+ {
300
+ type: 'text',
301
+ text: `Error executing ${request.params.name}: ${error instanceof Error ? error.message : String(error)}`,
302
+ },
303
+ ],
304
+ isError: true,
305
+ };
306
+ }
307
+ });
308
+ }
309
+ async executeCodeSearch(args) {
310
+ try {
311
+ // Ensure path is included in the options and is a non-empty string
312
+ if (!args.path || typeof args.path !== 'string' || args.path.trim() === '') {
313
+ throw new Error("Path is required and must be a non-empty string");
314
+ }
315
+ // Ensure query is included in the options
316
+ if (!args.query) {
317
+ throw new Error("Query is required");
318
+ }
319
+ // Log the arguments we received for debugging
320
+ console.error(`Received search arguments: path=${args.path}, query=${JSON.stringify(args.query)}`);
321
+ // Create a clean options object with only the essential properties first
322
+ const options = {
323
+ path: args.path.trim(), // Ensure path is trimmed
324
+ query: args.query
325
+ };
326
+ // Add optional parameters only if they exist
327
+ if (args.filesOnly !== undefined)
328
+ options.filesOnly = args.filesOnly;
329
+ if (args.ignore !== undefined)
330
+ options.ignore = args.ignore;
331
+ if (args.excludeFilenames !== undefined)
332
+ options.excludeFilenames = args.excludeFilenames;
333
+ if (args.exact !== undefined)
334
+ options.exact = args.exact;
335
+ if (args.maxResults !== undefined)
336
+ options.maxResults = args.maxResults;
337
+ if (args.maxTokens !== undefined)
338
+ options.maxTokens = args.maxTokens;
339
+ if (args.allowTests !== undefined)
340
+ options.allowTests = args.allowTests;
341
+ // Use noGitignore from args, or fall back to PROBE_NO_GITIGNORE environment variable
342
+ if (args.noGitignore !== undefined) {
343
+ options.noGitignore = args.noGitignore;
344
+ }
345
+ else if (process.env.PROBE_NO_GITIGNORE) {
346
+ options.noGitignore = process.env.PROBE_NO_GITIGNORE === 'true';
347
+ }
348
+ if (args.session !== undefined && args.session.trim() !== '') {
349
+ options.session = args.session;
350
+ }
351
+ else {
352
+ options.session = "new";
353
+ }
354
+ // Use timeout from args, or fall back to instance default
355
+ if (args.timeout !== undefined) {
356
+ options.timeout = args.timeout;
357
+ }
358
+ else if (this.defaultTimeout !== undefined) {
359
+ options.timeout = this.defaultTimeout;
360
+ }
361
+ console.error("Executing search with options:", JSON.stringify(options, null, 2));
362
+ // Double-check that path is still in the options object
363
+ if (!options.path) {
364
+ console.error("Path is missing from options object after construction");
365
+ throw new Error("Path is missing from options object");
366
+ }
367
+ try {
368
+ // Call search with the options object
369
+ const result = await search(options);
370
+ return result;
371
+ }
372
+ catch (searchError) {
373
+ console.error("Search function error:", searchError);
374
+ throw new Error(`Search function error: ${searchError.message || String(searchError)}`);
375
+ }
376
+ }
377
+ catch (error) {
378
+ console.error('Error executing code search:', error);
379
+ throw new McpError('MethodNotFound', `Error executing code search: ${error.message || String(error)}`);
380
+ }
381
+ }
382
+ async executeCodeQuery(args) {
383
+ try {
384
+ // Validate required parameters
385
+ if (!args.path) {
386
+ throw new Error("Path is required");
387
+ }
388
+ if (!args.pattern) {
389
+ throw new Error("Pattern is required");
390
+ }
391
+ // Create a single options object with both pattern and path
392
+ const options = {
393
+ path: args.path,
394
+ pattern: args.pattern,
395
+ language: args.language,
396
+ ignore: args.ignore,
397
+ allowTests: args.allowTests,
398
+ maxResults: args.maxResults,
399
+ format: args.format,
400
+ timeout: args.timeout || this.defaultTimeout
401
+ };
402
+ // Use noGitignore from args, or fall back to PROBE_NO_GITIGNORE environment variable
403
+ if (args.noGitignore !== undefined) {
404
+ options.noGitignore = args.noGitignore;
405
+ }
406
+ else if (process.env.PROBE_NO_GITIGNORE) {
407
+ options.noGitignore = process.env.PROBE_NO_GITIGNORE === 'true';
408
+ }
409
+ console.log("Executing query with options:", JSON.stringify({
410
+ path: options.path,
411
+ pattern: options.pattern
412
+ }));
413
+ const result = await query(options);
414
+ return result;
415
+ }
416
+ catch (error) {
417
+ console.error('Error executing code query:', error);
418
+ throw new McpError('MethodNotFound', `Error executing code query: ${error.message || String(error)}`);
419
+ }
420
+ }
421
+ async executeCodeExtract(args) {
422
+ try {
423
+ // Validate required parameters
424
+ if (!args.path) {
425
+ throw new Error("Path is required");
426
+ }
427
+ if (!args.files || !Array.isArray(args.files) || args.files.length === 0) {
428
+ throw new Error("Files array is required and must not be empty");
429
+ }
430
+ // Create a single options object with files and other parameters
431
+ const options = {
432
+ files: args.files,
433
+ path: args.path,
434
+ allowTests: args.allowTests,
435
+ contextLines: args.contextLines,
436
+ format: args.format,
437
+ timeout: args.timeout || this.defaultTimeout
438
+ };
439
+ // Use noGitignore from args, or fall back to PROBE_NO_GITIGNORE environment variable
440
+ if (args.noGitignore !== undefined) {
441
+ options.noGitignore = args.noGitignore;
442
+ }
443
+ else if (process.env.PROBE_NO_GITIGNORE) {
444
+ options.noGitignore = process.env.PROBE_NO_GITIGNORE === 'true';
445
+ }
446
+ // Call extract with the complete options object
447
+ try {
448
+ // Track request size for token usage
449
+ const requestSize = JSON.stringify(args).length;
450
+ const requestTokens = Math.ceil(requestSize / 4); // Approximate token count
451
+ // Execute the extract command
452
+ const result = await extract(options);
453
+ // Parse the result to extract token information if available
454
+ let responseTokens = 0;
455
+ let totalTokens = 0;
456
+ // Try to extract token information from the result
457
+ if (typeof result === 'string') {
458
+ const tokenMatch = result.match(/Total tokens returned: (\d+)/);
459
+ if (tokenMatch && tokenMatch[1]) {
460
+ responseTokens = parseInt(tokenMatch[1], 10);
461
+ totalTokens = requestTokens + responseTokens;
462
+ }
463
+ // Remove spinner debug output lines
464
+ const cleanedLines = result.split('\n').filter(line => !line.match(/^⠙|^⠹|^⠧|^⠇|^⠏/) &&
465
+ !line.includes('Thinking...Extract:') &&
466
+ !line.includes('Extract results:'));
467
+ // Add token usage information if not already present
468
+ if (!result.includes('Token Usage:')) {
469
+ cleanedLines.push('');
470
+ cleanedLines.push('Token Usage:');
471
+ cleanedLines.push(` Request tokens: ${requestTokens}`);
472
+ cleanedLines.push(` Response tokens: ${responseTokens}`);
473
+ cleanedLines.push(` Total tokens: ${totalTokens}`);
474
+ }
475
+ return cleanedLines.join('\n');
476
+ }
477
+ return result;
478
+ }
479
+ catch (error) {
480
+ console.error(`Error extracting:`, error);
481
+ return `Error extracting: ${error.message || String(error)}`;
482
+ }
483
+ }
484
+ catch (error) {
485
+ console.error('Error executing code extract:', error);
486
+ throw new McpError('MethodNotFound', `Error executing code extract: ${error.message || String(error)}`);
487
+ }
488
+ }
489
+ async run() {
490
+ // The @probelabs/probe package now handles binary path management internally
491
+ // We don't need to verify or download the binary in the MCP server anymore
492
+ // Just connect the server to the transport
493
+ const transport = new StdioServerTransport();
494
+ await this.server.connect(transport);
495
+ console.error('Probe MCP server running on stdio');
496
+ }
497
+ }
498
+ const server = new ProbeServer(cliConfig.timeout);
499
+ server.run().catch(console.error);