octocode-mcp 2.3.5 → 2.3.6

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/build/index.js +1836 -2048
  2. package/package.json +2 -2
package/build/index.js CHANGED
@@ -8,44 +8,12 @@ import NodeCache from 'node-cache';
8
8
  import crypto from 'crypto';
9
9
  import z from 'zod';
10
10
 
11
- const PROMPT_SYSTEM_PROMPT = `Expert code research assistant for GitHub/NPM ecosystems (public/private). Use powerful semantic search for efficient code discovery.
12
-
13
- CORE TOOLS:
14
- GitHub: Code, repos, issues, PRs, commits - supports boolean logic (AND/OR/NOT) and exact phrases
15
- NPM: Package search (fuzzy only, no boolean) + metadata (repo URLs, exports, dependencies)
16
- API Status: Check connectivity + user's GitHub organizations for private access
17
-
18
- SEARCH STRATEGIES:
19
- GitHub Code/Issues/PRs/Commits:
20
- OR (broad): "useState OR setState" - finds alternatives
21
- AND (precise): "react AND hooks" - requires both terms
22
- NOT (filter): "auth NOT test" - excludes noise
23
- Quotes (exact): "useEffect cleanup" - literal phrases
24
- Combine with filters: language, path, owner for focus
25
-
26
- NPM Search:
27
- Space-separated keywords only: "react state management"
28
- No boolean operators supported
29
- Use npm_view_package for direct package metadata → GitHub repo URL
30
-
31
- OPTIMIZATION:
32
- Check user's GitHub orgs for private repo access
33
- npm_view_package gives repo URL instantly - avoid GitHub repo search
34
- Use targeted searches, avoid redundant tool calls
35
- Combine tools strategically: NPM → GitHub file content
36
- Discovery: comprehensive multi-tool approach
37
- Direct queries: quick targeted approach
38
-
39
- CLI Help using CLI (use when needed to check failures)
40
- gh <command> --help
41
- npm <command> --help
42
-
43
- Always provide code snippets and documentation references.`;
44
-
45
11
  const VERSION = 'v1';
46
12
  const cache = new NodeCache({
47
13
  stdTTL: 86400, // 24 hour cache
48
14
  checkperiod: 3600, // Check for expired keys every 1 hour
15
+ maxKeys: 1000, // Limit cache to 1000 entries to prevent unbounded growth
16
+ deleteOnExpire: true, // Automatically delete expired keys
49
17
  });
50
18
  function generateCacheKey(prefix, params) {
51
19
  const paramString = JSON.stringify(params, Object.keys(params).sort());
@@ -66,6 +34,9 @@ async function withCache(cacheKey, operation) {
66
34
  }
67
35
  return result;
68
36
  }
37
+ function clearAllCache() {
38
+ cache.flushAll();
39
+ }
69
40
 
70
41
  const safeExecAsync = promisify(exec);
71
42
  // Allowed command prefixes - this prevents shell injection by restricting to safe commands
@@ -85,10 +56,9 @@ function createSuccessResult(data) {
85
56
  };
86
57
  }
87
58
  function createErrorResult(message, error) {
59
+ const errorMessage = error instanceof Error ? error.message : String(error);
88
60
  return {
89
- content: [
90
- { type: 'text', text: `${message}: ${error.message}` },
91
- ],
61
+ content: [{ type: 'text', text: `${message}: ${errorMessage}` }],
92
62
  isError: true,
93
63
  };
94
64
  }
@@ -128,23 +98,31 @@ function getShellConfig(preferredWindowsShell) {
128
98
  };
129
99
  }
130
100
  /**
131
- * Checks if a query contains GitHub search boolean operators
101
+ * Escape shell arguments with improved GitHub CLI query handling
132
102
  */
133
- function hasGitHubBooleanOperators(query) {
134
- // Check for GitHub boolean operators (must be uppercase)
135
- return (/\b(AND|OR|NOT)\b/.test(query) ||
136
- /\s+(AND|OR|NOT)\s+/.test(query) ||
137
- /"[^"]*\s+(AND|OR|NOT)\s+[^"]*"/.test(query));
138
- }
139
- /**
140
- * Escape shell arguments with improved GitHub CLI boolean query handling
141
- */
142
- function escapeShellArg(arg, shellType, isGitHubQuery) {
103
+ function escapeShellArg(arg, shellType, isGitHubQuery // Flag to indicate if this is the main GitHub search query argument
104
+ ) {
143
105
  // Auto-detect shell type if not provided
144
106
  if (!shellType) {
145
107
  const isWindows = platform() === 'win32';
146
108
  shellType = isWindows ? 'cmd' : 'unix';
147
109
  }
110
+ // Special handling for GitHub search queries
111
+ if (isGitHubQuery) {
112
+ // If the argument already contains quotes, preserve them
113
+ if (arg.includes('"')) {
114
+ // For Unix-like shells, wrap the entire argument in single quotes
115
+ if (shellType === 'unix') {
116
+ return `'${arg.replace(/'/g, "'\"'\"'")}'`;
117
+ }
118
+ // For Windows CMD
119
+ if (shellType === 'cmd') {
120
+ return `"${arg.replace(/"/g, '""')}"`;
121
+ }
122
+ // For PowerShell
123
+ return `'${arg.replace(/'/g, "''")}'`;
124
+ }
125
+ }
148
126
  switch (shellType) {
149
127
  case 'powershell':
150
128
  return escapePowerShellArg(arg);
@@ -182,25 +160,17 @@ function escapeWindowsCmdArg(arg) {
182
160
  * Escape arguments for Unix shells with special handling for GitHub CLI queries
183
161
  */
184
162
  function escapeUnixShellArg(arg, isGitHubQuery) {
185
- // Special handling for GitHub CLI search queries to preserve boolean logic
186
- if (isGitHubQuery && hasGitHubBooleanOperators(arg)) {
187
- // For boolean queries with OR/AND/NOT, use double quotes to preserve operators
188
- // GitHub CLI handles boolean logic properly when quoted
189
- return `"${arg.replace(/"/g, '\\"')}"`;
190
- }
191
- // For GitHub queries with spaces but no boolean operators, quote them
192
- if (isGitHubQuery &&
193
- /\s/.test(arg) &&
194
- !arg.startsWith('"') &&
195
- !arg.endsWith('"')) {
196
- return `"${arg.replace(/"/g, '\\"')}"`;
197
- }
198
- // For already quoted GitHub queries, pass through with escaped internal quotes
199
- if (isGitHubQuery && arg.startsWith('"') && arg.endsWith('"')) {
200
- return arg.replace(/\\"/g, '\\\\"'); // Escape already escaped quotes
163
+ // If it's a GitHub search query with special characters or spaces
164
+ if (isGitHubQuery && (arg.includes(' ') || /[:"']/g.test(arg))) {
165
+ // Preserve existing quotes if present
166
+ if (arg.includes('"')) {
167
+ return `'${arg.replace(/'/g, "'\"'\"'")}'`;
168
+ }
169
+ // Add double quotes for terms that need them
170
+ return `"${arg}"`;
201
171
  }
202
172
  // Standard Unix shell escaping for other arguments
203
- if (/[^\w\-._/:=@]/.test(arg)) {
173
+ if (/[^a-zA-Z0-9\-_./=@:]/.test(arg)) {
204
174
  return `'${arg.replace(/'/g, "'\"'\"'")}'`;
205
175
  }
206
176
  return arg;
@@ -241,12 +211,21 @@ async function executeGitHubCommand(command, args = [], options = {}) {
241
211
  // Get shell configuration
242
212
  const shellConfig = getShellConfig(options.windowsShell);
243
213
  // Build command with validated prefix and properly escaped arguments
244
- // First argument is typically the search query for GitHub CLI search commands
214
+ // For GitHub search commands, qualifiers like "language:typescript" should not be escaped
215
+ // Only the main query term (if it contains spaces) needs escaping
245
216
  const escapedArgs = args.map((arg, index) => {
246
- const isFirstArg = index === 0;
247
- // Detect if this is a search query (first non-subcommand argument)
248
- const isSearchQuery = command === 'search' && isFirstArg && !arg.startsWith('--');
249
- return escapeShellArg(arg, shellConfig.type, isSearchQuery);
217
+ const isMainQueryArgument = command === 'search' && index === 1;
218
+ const isGitHubQualifier = command === 'search' &&
219
+ index > 1 &&
220
+ (arg.includes(':') || arg.startsWith('(')) &&
221
+ !arg.startsWith('--');
222
+ // Don't escape GitHub search qualifiers - they need to be passed as-is
223
+ // This includes qualifiers like "language:typescript", "user:microsoft", "org:microsoft"
224
+ // and complex expressions like "(user:microsoft OR org:microsoft)"
225
+ if (isGitHubQualifier) {
226
+ return arg;
227
+ }
228
+ return escapeShellArg(arg, shellConfig.type, isMainQueryArgument);
250
229
  });
251
230
  const fullCommand = `gh ${command} ${escapedArgs.join(' ')}`;
252
231
  const executeGhCommand = () => executeCommand(fullCommand, 'github', options, shellConfig);
@@ -307,9 +286,17 @@ async function executeCommand(fullCommand, type, options = {}, shellConfig) {
307
286
  const errorType = type === 'npm' ? 'NPM command error' : 'GitHub CLI command error';
308
287
  return createErrorResult(errorType, new Error(stderr));
309
288
  }
289
+ // Try to parse stdout as JSON, fallback to string if not possible
290
+ let parsedResult = stdout;
291
+ try {
292
+ parsedResult = JSON.parse(stdout);
293
+ }
294
+ catch {
295
+ // Not JSON, keep as string
296
+ }
310
297
  return createSuccessResult({
311
298
  command: fullCommand,
312
- result: stdout,
299
+ result: parsedResult,
313
300
  timestamp: new Date().toISOString(),
314
301
  type,
315
302
  platform: platform(),
@@ -326,37 +313,91 @@ async function executeCommand(fullCommand, type, options = {}, shellConfig) {
326
313
  }
327
314
  }
328
315
 
316
+ var LogLevel;
317
+ (function (LogLevel) {
318
+ LogLevel["ERROR"] = "ERROR";
319
+ LogLevel["WARN"] = "WARN";
320
+ LogLevel["INFO"] = "INFO";
321
+ LogLevel["DEBUG"] = "DEBUG";
322
+ })(LogLevel || (LogLevel = {}));
323
+ class Logger {
324
+ appName;
325
+ timestamp;
326
+ constructor(appName = 'octocode-mcp', timestamp = true) {
327
+ this.appName = appName;
328
+ this.timestamp = timestamp;
329
+ }
330
+ formatMessage(level, message, ...args) {
331
+ const timestamp = this.timestamp ? `[${new Date().toISOString()}] ` : '';
332
+ const prefix = `${timestamp}[${this.appName}] [${level}]`;
333
+ const formattedArgs = args.length > 0
334
+ ? ' ' +
335
+ args
336
+ .map(arg => {
337
+ if (arg instanceof Error) {
338
+ return `${arg.message}\n${arg.stack}`;
339
+ }
340
+ return typeof arg === 'object'
341
+ ? JSON.stringify(arg, null, 2)
342
+ : String(arg);
343
+ })
344
+ .join(' ')
345
+ : '';
346
+ return `${prefix} ${message}${formattedArgs}`;
347
+ }
348
+ error(message, ...args) {
349
+ // eslint-disable-next-line no-console
350
+ console.error(this.formatMessage(LogLevel.ERROR, message, ...args));
351
+ }
352
+ warn(message, ...args) {
353
+ // eslint-disable-next-line no-console
354
+ console.warn(this.formatMessage(LogLevel.WARN, message, ...args));
355
+ }
356
+ info(message, ...args) {
357
+ // eslint-disable-next-line no-console
358
+ console.log(this.formatMessage(LogLevel.INFO, message, ...args));
359
+ }
360
+ debug(message, ...args) {
361
+ if (process.env.DEBUG === 'true' ||
362
+ process.env.NODE_ENV === 'development') {
363
+ // eslint-disable-next-line no-console
364
+ console.log(this.formatMessage(LogLevel.DEBUG, message, ...args));
365
+ }
366
+ }
367
+ }
368
+ // Singleton instance
369
+ const logger = new Logger();
370
+
329
371
  function createResult(options) {
330
- const { data, error, suggestions, cli_command } = options;
372
+ const { data, error } = options;
331
373
  if (error) {
332
374
  const errorMessage = typeof error === 'string'
333
375
  ? error
334
376
  : error.message || 'Unknown error';
335
- // Build error response with optional CLI command
336
- const errorResponse = { error: errorMessage };
337
- if (suggestions) {
338
- errorResponse.suggestions = suggestions;
339
- }
340
- if (cli_command) {
341
- errorResponse.cli_command = cli_command;
342
- }
377
+ const errorResponse = errorMessage;
343
378
  return {
344
- content: [{ type: 'text', text: JSON.stringify(errorResponse, null, 2) }],
379
+ content: [{ type: 'text', text: errorResponse }],
345
380
  isError: true,
346
381
  };
347
382
  }
348
- // For successful responses, include cli_command if provided (for debugging)
349
- if (cli_command && data && typeof data === 'object') {
350
- const dataWithCli = { ...data, cli_command };
383
+ try {
351
384
  return {
352
- content: [{ type: 'text', text: JSON.stringify(dataWithCli, null, 2) }],
385
+ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
353
386
  isError: false,
354
387
  };
355
388
  }
356
- return {
357
- content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
358
- isError: false,
359
- };
389
+ catch (jsonError) {
390
+ logger.error('JSON serialization failed:', jsonError);
391
+ return {
392
+ content: [
393
+ {
394
+ type: 'text',
395
+ text: `JSON serialization failed: ${jsonError instanceof Error ? jsonError.message : 'Unknown error'}`,
396
+ },
397
+ ],
398
+ isError: true,
399
+ };
400
+ }
360
401
  }
361
402
  /**
362
403
  * Convert ISO timestamp to DDMMYYYY format
@@ -417,10 +458,149 @@ function optimizeTextMatch(fragment, maxLength = 100) {
417
458
  return truncated + '…';
418
459
  }
419
460
 
420
- const TOOL_NAME$9 = 'api_status_check';
421
- const DESCRIPTION$9 = `Get GitHub organizations list and check CLI authentication status. Use when searching private repos or when CLI tools fail.`;
461
+ /**
462
+ * Constants for error messages and suggestions across MCP tools
463
+ * Centralized management of error handling for better consistency
464
+ */
465
+ const ERROR_MESSAGES = {
466
+ // Authentication errors
467
+ AUTHENTICATION_REQUIRED: 'GitHub authentication required - run api_status_check tool',
468
+ // Rate limit errors
469
+ RATE_LIMIT_EXCEEDED: 'Rate limit exceeded - use specific filters (owner, language) or wait',
470
+ RATE_LIMIT_SIMPLE: 'Rate limit exceeded - wait or add filters',
471
+ // No results errors
472
+ NO_RESULTS_FOUND: 'No results found - try simpler query or different filters',
473
+ NO_REPOSITORIES_FOUND: 'No repositories found - try simpler query or different filters',
474
+ NO_COMMITS_FOUND: 'No commits found - try simpler query or different filters',
475
+ NO_ISSUES_FOUND: 'No issues found - try simpler query or different filters',
476
+ NO_PULL_REQUESTS_FOUND: 'No pull requests found - try simpler query or different filters',
477
+ NO_PACKAGES_FOUND: 'No packages found',
478
+ // Connection/network errors
479
+ SEARCH_FAILED: 'Search failed - check connection or simplify query',
480
+ REPOSITORY_SEARCH_FAILED: 'Repository search failed - check connection or query',
481
+ COMMIT_SEARCH_FAILED: 'Commit search failed',
482
+ ISSUE_SEARCH_FAILED: 'Issue search failed - check auth or simplify query',
483
+ PR_SEARCH_FAILED: 'PR search failed - check access and query syntax',
484
+ PACKAGE_SEARCH_FAILED: 'Package search failed - try different keywords',
485
+ // GitHub CLI errors
486
+ CLI_INVALID_RESPONSE: 'GitHub CLI invalid response - check "gh version" and update',
487
+ // Timeout errors
488
+ SEARCH_TIMEOUT: 'Search timed out - add filters (language, owner) or use specific terms',
489
+ // Query validation errors
490
+ QUERY_TOO_LONG: 'Query too long (max 256 chars) - simplify search terms',
491
+ QUERY_REQUIRED: 'Query required - provide search keywords',
492
+ EMPTY_QUERY: 'Empty query - try "useState", "authentication", or language:python',
493
+ QUERY_TOO_LONG_1000: 'Query too long (max 1000 chars) - use key terms like "error handling"',
494
+ REPO_OR_OWNER_NOT_FOUND: 'Repository/owner not found - check spelling, visibility, and permissions',
495
+ // Query syntax errors
496
+ INVALID_QUERY_SYNTAX: 'Invalid syntax - Boolean operators not supported, use quotes for phrases',
497
+ // Size/format validation errors
498
+ INVALID_SIZE_FORMAT: 'Invalid size format - use >N, <N, or N..M without quotes',
499
+ INVALID_SEARCH_SCOPE: 'Invalid scope - use "file" for content, "path" for filenames',
500
+ // API Status check errors
501
+ API_STATUS_CHECK_FAILED: 'API Status Check Failed',
502
+ };
503
+ const SUGGESTIONS = {
504
+ // Code search suggestions
505
+ CODE_SEARCH_NO_RESULTS: `No results found. Try this progression:
506
+ 1. Remove ALL filters and search with just one keyword
507
+ 2. Break into several queries with minimal query and filters
508
+ 3. Use api_status_check to verify access to private repos`,
509
+ // Repository search suggestions
510
+ REPO_SEARCH_PRIMARY_FILTER: 'Start with simple query (1-2 words), then add filters based on results',
511
+ // General search suggestions
512
+ SIMPLIFY_QUERY: `Try this search progression:
513
+ 1. Single keyword with NO filters
514
+ 2. Add ONE filter at a time
515
+ 3. Try synonyms and related terms
516
+ 4. Search broader categories`,
517
+ // Issue/PR search suggestions
518
+ PROVIDE_KEYWORDS: 'Start with simple keywords, then refine based on results',
519
+ PROVIDE_PR_KEYWORDS: 'Begin with basic terms, analyze results, then add filters',
520
+ // Package search suggestions
521
+ DIFFERENT_KEYWORDS: `Try multiple approaches:
522
+ 1. Single functional terms: "auth", "react"
523
+ 2. Break compound words: "authlib" → "auth"
524
+ 3. Search by use case: "user login" vs "authentication"
525
+ 4. Try category terms: "framework", "tool", "library"`,
526
+ };
527
+ // Helper function to get error message with context-specific suggestions
528
+ function getErrorWithSuggestion(options) {
529
+ const { baseError, suggestion } = options;
530
+ const errors = Array.isArray(baseError) ? baseError : [baseError];
531
+ const suggestions = Array.isArray(suggestion)
532
+ ? suggestion
533
+ : suggestion
534
+ ? [suggestion]
535
+ : [];
536
+ let result = errors.join('\n');
537
+ if (suggestions.length > 0) {
538
+ result += '\n\nSuggestion: ' + suggestions.join('\n');
539
+ }
540
+ return result;
541
+ }
542
+ // Common error handling patterns
543
+ function createAuthenticationError() {
544
+ return ERROR_MESSAGES.AUTHENTICATION_REQUIRED;
545
+ }
546
+ function createRateLimitError(detailed = true) {
547
+ return detailed
548
+ ? ERROR_MESSAGES.RATE_LIMIT_EXCEEDED
549
+ : ERROR_MESSAGES.RATE_LIMIT_SIMPLE;
550
+ }
551
+ function createNoResultsError(type = 'code') {
552
+ switch (type) {
553
+ case 'repositories':
554
+ return ERROR_MESSAGES.NO_REPOSITORIES_FOUND;
555
+ case 'commits':
556
+ return ERROR_MESSAGES.NO_COMMITS_FOUND;
557
+ case 'issues':
558
+ return ERROR_MESSAGES.NO_ISSUES_FOUND;
559
+ case 'pull_requests':
560
+ return ERROR_MESSAGES.NO_PULL_REQUESTS_FOUND;
561
+ case 'packages':
562
+ return ERROR_MESSAGES.NO_PACKAGES_FOUND;
563
+ default:
564
+ return ERROR_MESSAGES.NO_RESULTS_FOUND;
565
+ }
566
+ }
567
+ function createSearchFailedError(type = 'code') {
568
+ switch (type) {
569
+ case 'repositories':
570
+ return ERROR_MESSAGES.REPOSITORY_SEARCH_FAILED;
571
+ case 'commits':
572
+ return ERROR_MESSAGES.COMMIT_SEARCH_FAILED;
573
+ case 'issues':
574
+ return ERROR_MESSAGES.ISSUE_SEARCH_FAILED;
575
+ case 'pull_requests':
576
+ return ERROR_MESSAGES.PR_SEARCH_FAILED;
577
+ case 'packages':
578
+ return ERROR_MESSAGES.PACKAGE_SEARCH_FAILED;
579
+ default:
580
+ return ERROR_MESSAGES.SEARCH_FAILED;
581
+ }
582
+ }
583
+
584
+ const API_STATUS_CHECK_TOOL_NAME = 'apiStatusCheck';
585
+ const DESCRIPTION$9 = `Check GitHub and NPM authentication status. Returns connected status and GitHub organizations for accessing private repositories. No parameters required.`;
586
+ // Helper function to parse execution results with proper typing
587
+ function parseExecResult(result) {
588
+ if (!result.isError && result.content?.[0]?.text) {
589
+ try {
590
+ const textContent = result.content[0].text;
591
+ if (typeof textContent === 'string') {
592
+ const parsed = JSON.parse(textContent);
593
+ return typeof parsed === 'object' && parsed !== null ? parsed : null;
594
+ }
595
+ }
596
+ catch (e) {
597
+ return null;
598
+ }
599
+ }
600
+ return null;
601
+ }
422
602
  function registerApiStatusCheckTool(server) {
423
- server.registerTool(TOOL_NAME$9, {
603
+ server.registerTool(API_STATUS_CHECK_TOOL_NAME, {
424
604
  description: DESCRIPTION$9,
425
605
  inputSchema: {},
426
606
  annotations: {
@@ -440,32 +620,22 @@ function registerApiStatusCheckTool(server) {
440
620
  try {
441
621
  const authResult = await executeGitHubCommand('auth', ['status']);
442
622
  if (!authResult.isError) {
443
- let authData;
444
- try {
445
- authData = JSON.parse(authResult.content[0].text);
446
- }
447
- catch (parseError) {
448
- // JSON parsing error - this is unexpected, propagate it
449
- throw new Error(`GitHub auth response JSON parsing failed: ${parseError.message}`);
450
- }
451
- const isAuthenticated = authData.result?.includes('Logged in') ||
452
- authData.result?.includes('github.com');
623
+ const execResult = parseExecResult(authResult);
624
+ const isAuthenticated = typeof execResult?.result === 'string'
625
+ ? execResult.result.includes('Logged in') ||
626
+ execResult.result.includes('github.com')
627
+ : false;
453
628
  if (isAuthenticated) {
454
629
  githubConnected = true;
455
630
  // Get user organizations using direct GitHub CLI command
456
631
  try {
457
632
  const orgsResult = await executeGitHubCommand('org', ['list', '--limit=50'], { cache: false });
458
- if (!orgsResult.isError) {
459
- let execResult;
460
- try {
461
- execResult = JSON.parse(orgsResult.content[0].text);
462
- }
463
- catch (parseError) {
464
- // JSON parsing error for organizations - treat as no orgs available
465
- execResult = { result: '' };
466
- }
467
- const output = execResult.result;
468
- // Parse organizations into clean array
633
+ const orgsExecResult = parseExecResult(orgsResult);
634
+ const output = typeof orgsExecResult?.result === 'string'
635
+ ? orgsExecResult.result
636
+ : '';
637
+ // Parse organizations into clean array
638
+ if (typeof output === 'string') {
469
639
  organizations = output
470
640
  .split('\n')
471
641
  .map((org) => org.trim())
@@ -500,23 +670,12 @@ function registerApiStatusCheckTool(server) {
500
670
  if (!npmResult.isError) {
501
671
  npmConnected = true;
502
672
  // Get registry info
503
- try {
504
- const registryResult = await executeNpmCommand('config', ['get', 'registry'], { timeout: 3000 });
505
- if (!registryResult.isError) {
506
- let registryData;
507
- try {
508
- registryData = JSON.parse(registryResult.content[0].text);
509
- }
510
- catch (parseError) {
511
- // JSON parsing error for registry - use default
512
- registryData = { result: 'https://registry.npmjs.org/' };
513
- }
514
- registry = registryData.result.trim();
515
- }
516
- }
517
- catch {
518
- registry = 'https://registry.npmjs.org/'; // default fallback
519
- }
673
+ const registryResult = await executeNpmCommand('config', ['get', 'registry'], { timeout: 3000 });
674
+ const registryExecResult = parseExecResult(registryResult);
675
+ registry =
676
+ typeof registryExecResult?.result === 'string'
677
+ ? registryExecResult.result.trim()
678
+ : 'https://registry.npmjs.org/'; // default fallback
520
679
  }
521
680
  }
522
681
  catch (error) {
@@ -529,120 +688,66 @@ function registerApiStatusCheckTool(server) {
529
688
  }
530
689
  npmConnected = false;
531
690
  }
532
- // Build structured response object
533
- const loginStatus = {
534
- login: {
535
- github: {
536
- connected: githubConnected,
537
- user_organizations: organizations,
538
- },
539
- npm: {
540
- connected: npmConnected,
541
- registry: registry || 'https://registry.npmjs.org/',
691
+ return createResult({
692
+ data: {
693
+ login: {
694
+ github: {
695
+ connected: githubConnected,
696
+ user_organizations: organizations,
697
+ },
698
+ npm: {
699
+ connected: npmConnected,
700
+ registry: registry || 'https://registry.npmjs.org/',
701
+ },
702
+ hints: [
703
+ 'Use user organizations to search private repositories when requested - verify access by checking query and repository structure',
704
+ ],
542
705
  },
543
- hints: [
544
- 'use user organizations: to search on private repositories in case the user asked about private repo - check by query nd structure',
545
- ],
546
706
  },
547
- };
548
- return createResult({ data: loginStatus });
707
+ });
549
708
  }
550
709
  catch (error) {
551
710
  return createResult({
552
- error: `API Status Check Failed\nError: ${error instanceof Error ? error.message : 'Unknown error'}\n\nThis usually indicates a system configuration issue. Please verify GitHub CLI and NPM are properly installed.`,
711
+ error: `${ERROR_MESSAGES.API_STATUS_CHECK_FAILED}\nError: ${error instanceof Error ? error.message : 'Unknown error'}\n\nThis usually indicates a system configuration issue. Please verify GitHub CLI and NPM are properly installed.`,
553
712
  });
554
713
  }
555
714
  });
556
715
  }
557
716
 
558
- const TOOL_NAME$8 = 'github_search_code';
559
- const DESCRIPTION$8 = `Search code across GitHub repositories with powerful GitHub search syntax and advanced filtering.
560
-
561
- BOOLEAN LOGIC (MOST POWERFUL):
562
- - "useState OR useEffect" - Find either hook
563
- - "useState AND useEffect" - Find both hooks together
564
- - "authentication AND (jwt OR oauth)" - Complex logic combinations
565
- - "NOT deprecated" - Exclude deprecated code
566
-
567
- EMBEDDED QUALIFIERS:
568
- - "useState language:javascript filename:*.jsx" - Hook in React files
569
- - "authentication language:python path:*/security/*" - Security code in Python
570
- - "docker OR kubernetes language:yaml extension:yml" - Container configs
571
-
572
- TRADITIONAL FILTERS (ALSO SUPPORTED):
573
- - language: "javascript", owner: "microsoft", filename: "package.json"
574
-
575
- PROVEN PATTERNS: "authentication" → +language → +owner → +filename
576
- KEY TIPS: language filter = 90% speed boost, boolean operators work perfectly with filters`;
577
- function registerGitHubSearchCodeTool(server) {
578
- server.registerTool(TOOL_NAME$8, {
717
+ const GITHUB_VIEW_REPO_STRUCTURE_TOOL_NAME = 'githubViewRepoStructure';
718
+ const DESCRIPTION$8 = `Explore repository structure and navigate directories. Auto-detects branches and provides file/folder listings with size information. Parameters: owner (required - GitHub username/org), repo (required - repository name), branch (required), path (optional).`;
719
+ function registerViewRepositoryStructureTool(server) {
720
+ server.registerTool(GITHUB_VIEW_REPO_STRUCTURE_TOOL_NAME, {
579
721
  description: DESCRIPTION$8,
580
722
  inputSchema: {
581
- query: z
582
- .string()
583
- .min(1)
584
- .describe(`Search query with GitHub syntax. BOOLEAN LOGIC: "useState OR useEffect", "authentication AND jwt", "NOT deprecated". EMBEDDED QUALIFIERS: "useState language:javascript", "docker path:*/config/*". EXACT PHRASES: "error handling".
585
-
586
- POWERFUL EXAMPLES: "useState OR useEffect language:javascript", "authentication AND (jwt OR oauth)", "docker OR kubernetes language:yaml", "NOT deprecated language:python"
587
- RULES: Boolean operators MUST be uppercase (AND, OR, NOT). Combines perfectly with traditional filters.`),
588
- language: z
589
- .string()
590
- .optional()
591
- .describe(`MOST EFFECTIVE FILTER - 90% speed boost! Essential for popular languages.
592
-
593
- POPULAR: javascript, typescript, python, java, go, rust, php, ruby, swift, kotlin, dart
594
- SYSTEMS: c, cpp, assembly, shell, dockerfile, yaml`),
595
723
  owner: z
596
- .union([z.string(), z.array(z.string())])
597
- .optional()
598
- .describe(`HIGH IMPACT - Reduces search space by 95%+
599
-
600
- EXAMPLES: "microsoft", "google", "facebook" or ["microsoft", "google"]
601
- POPULAR: microsoft, google, facebook, amazon, apache, hashicorp, kubernetes`),
602
- filename: z
603
724
  .string()
604
- .optional()
605
- .describe(`SURGICAL PRECISION for configs and special files
606
-
607
- TARGETS: "package.json", "Dockerfile", "webpack.config.js", ".eslintrc", "README.md"
608
- STRATEGY: filename:package.json + "react typescript"`),
725
+ .min(1)
726
+ .max(100)
727
+ .regex(/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/, 'Invalid GitHub username/org format')
728
+ .describe(`Repository owner/org name (e.g., 'microsoft', 'google', NOT 'microsoft/vscode')`),
609
729
  repo: z
610
- .union([z.string(), z.array(z.string())])
611
- .optional()
612
- .describe(`PRECISE TARGETING for specific repositories
613
-
614
- FORMAT: "facebook/react", "microsoft/vscode" or ["facebook/react", "vuejs/vue"]
615
- USE: Deep dive analysis of specific projects`),
616
- extension: z
617
- .string()
618
- .optional()
619
- .describe(`FILE TYPE PRECISION - More specific than language filter
620
-
621
- POPULAR: js, ts, jsx, tsx, py, java, go, rs, rb, php, cs, sh, yml, json, md
622
- USE: extension:tsx (React TypeScript only), extension:dockerfile`),
623
- match: z
624
- .union([z.enum(['file', 'path']), z.array(z.enum(['file', 'path']))])
625
- .optional()
626
- .describe(`SEARCH SCOPE: "file" (content), "path" (filenames), ["file", "path"] (both)
627
-
628
- EXAMPLES: match:"path" + "test" (find test files), match:"file" + "useState"`),
629
- size: z
630
730
  .string()
631
- .optional()
632
- .describe(`FILE SIZE FILTER: ">100" (>100KB), "<50" (<50KB), "10..100" (range)
633
-
634
- STRATEGY: "<200" (avoid huge files), ">20" (substantial code), "<10" (configs)`),
635
- limit: z
636
- .number()
637
- .int()
638
731
  .min(1)
639
732
  .max(100)
733
+ .regex(/^[a-zA-Z0-9._-]+$/, 'Invalid repository name format')
734
+ .describe('Repository name (case-sensitive)'),
735
+ branch: z
736
+ .string()
737
+ .min(1)
738
+ .max(255)
739
+ .regex(/^[^\s]+$/, 'Branch name cannot contain spaces')
740
+ .describe('Branch name. Falls back to default branch if not found'),
741
+ path: z
742
+ .string()
640
743
  .optional()
641
- .default(30)
642
- .describe(`RESULTS: 10-20 (quick), 30 (default), 50-100 (comprehensive). Use filters over high limits.`),
744
+ .default('')
745
+ .refine(path => !path.includes('..'), 'Path traversal not allowed')
746
+ .refine(path => path.length <= 500, 'Path too long')
747
+ .describe('Directory path within repository. Leave empty for root.'),
643
748
  },
644
749
  annotations: {
645
- title: 'GitHub Code Search - Smart & Efficient',
750
+ title: 'GitHub Repository Explorer',
646
751
  readOnlyHint: true,
647
752
  destructiveHint: false,
648
753
  idempotentHint: true,
@@ -650,604 +755,170 @@ STRATEGY: "<200" (avoid huge files), ">20" (substantial code), "<10" (configs)`)
650
755
  },
651
756
  }, async (args) => {
652
757
  try {
653
- // Validate parameter combinations
654
- const validationError = validateSearchParameters(args);
655
- if (validationError) {
656
- return createResult({ error: validationError });
657
- }
658
- const result = await searchGitHubCode(args);
659
- if (result.isError) {
660
- return result;
661
- }
662
- const execResult = JSON.parse(result.content[0].text);
663
- const codeResults = JSON.parse(execResult.result);
664
- // GitHub CLI returns a direct array, not an object with total_count and items
665
- const items = Array.isArray(codeResults) ? codeResults : [];
666
- // Smart handling for no results - provide actionable suggestions
667
- if (items.length === 0) {
668
- return handleNoResults(args, execResult.command);
669
- }
670
- // Transform to optimized format
671
- const optimizedResult = transformToOptimizedFormat$1(items, args);
672
- return createResult({ data: optimizedResult });
758
+ const result = await viewRepositoryStructure(args);
759
+ return result;
673
760
  }
674
761
  catch (error) {
675
- const errorMessage = error.message || '';
676
- // Handle JSON parsing errors
677
- if (errorMessage.includes('JSON')) {
678
- return createResult({
679
- error: 'GitHub CLI returned invalid response - check if GitHub CLI is up to date with "gh version" and try again',
680
- });
681
- }
762
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
682
763
  return createResult({
683
- error: 'GitHub code search failed',
684
- suggestions: [
685
- 'Try simpler queries without NOT operators',
686
- 'Use filters (language, owner, filename) instead of complex boolean',
687
- 'Check authentication with api_status_check',
688
- ],
764
+ error: `Failed to explore repository. ${errorMessage}. Verify repository exists and is accessible`,
689
765
  });
690
766
  }
691
767
  });
692
768
  }
693
769
  /**
694
- * Smart handler for no results - provides actionable suggestions
770
+ * Views the structure of a GitHub repository at a specific path.
771
+ * Optimized for code analysis workflows with smart defaults and clear errors.
695
772
  */
696
- function handleNoResults(params, cliCommand) {
697
- const suggestions = generateSmartSuggestions(params);
698
- const fallbackQueries = generateFallbackQueries(params);
699
- return createResult({
700
- data: {
701
- items: [],
702
- total_count: 0,
703
- smart_suggestions: {
704
- message: "No results found. Here are smart strategies to find what you're looking for:",
705
- suggestions,
706
- fallback_queries: fallbackQueries,
707
- next_steps: [
708
- 'Try one of the fallback queries above',
709
- 'Remove some filters to broaden the search',
710
- 'Use different keywords or synonyms',
711
- 'Check spelling and try variations',
712
- ],
713
- },
714
- metadata: {
715
- has_filters: !!(params.language ||
716
- params.owner ||
717
- params.filename ||
718
- params.extension),
719
- search_scope: params.match
720
- ? Array.isArray(params.match)
721
- ? params.match.join(',')
722
- : params.match
723
- : 'file',
724
- },
725
- cli_command: cliCommand,
726
- },
773
+ async function viewRepositoryStructure(params) {
774
+ const cacheKey = generateCacheKey('gh-repo-structure', params);
775
+ return withCache(cacheKey, async () => {
776
+ const { owner, repo, branch, path = '' } = params;
777
+ try {
778
+ // Clean up path
779
+ const cleanPath = path.startsWith('/') ? path.substring(1) : path;
780
+ // Try the requested branch first, then fallback to main/master
781
+ const branchesToTry = await getSmartBranchFallback(owner, repo, branch);
782
+ let items = [];
783
+ let usedBranch = branch;
784
+ let lastError = null;
785
+ let attemptCount = 0;
786
+ const maxAttempts = 3; // Prevent infinite loops
787
+ for (const tryBranch of branchesToTry) {
788
+ if (attemptCount >= maxAttempts)
789
+ break;
790
+ attemptCount++;
791
+ try {
792
+ const apiPath = `/repos/${owner}/${repo}/contents/${cleanPath}?ref=${tryBranch}`;
793
+ const result = await executeGitHubCommand('api', [apiPath], {
794
+ cache: false,
795
+ });
796
+ if (!result.isError) {
797
+ const execResult = JSON.parse(result.content[0].text);
798
+ const apiItems = execResult.result;
799
+ items = Array.isArray(apiItems) ? apiItems : [apiItems];
800
+ usedBranch = tryBranch;
801
+ break;
802
+ }
803
+ else {
804
+ lastError = new Error(result.content[0].text);
805
+ }
806
+ }
807
+ catch (error) {
808
+ lastError = error instanceof Error ? error : new Error(String(error));
809
+ // Try next branch
810
+ continue;
811
+ }
812
+ }
813
+ if (items.length === 0) {
814
+ // Use the most descriptive error message
815
+ const errorMsg = lastError?.message || 'Unknown error';
816
+ if (errorMsg.includes('404') || errorMsg.includes('Not Found')) {
817
+ if (path) {
818
+ return createResult({
819
+ error: `Path "${path}" not found. Verify the path or use github_search_code to find files`,
820
+ });
821
+ }
822
+ else {
823
+ return createResult({
824
+ error: `Repository not found: ${owner}/${repo}. Check spelling and case sensitivity`,
825
+ });
826
+ }
827
+ }
828
+ else if (errorMsg.includes('403') || errorMsg.includes('Forbidden')) {
829
+ return createResult({
830
+ error: `Access denied to ${owner}/${repo}. Repository may be private - use api_status_check`,
831
+ });
832
+ }
833
+ else {
834
+ return createResult({
835
+ error: `Failed to access ${owner}/${repo}. Check network connection`,
836
+ });
837
+ }
838
+ }
839
+ // Limit total items to 100 for efficiency
840
+ const limitedItems = items.slice(0, 100);
841
+ // Sort: directories first, then alphabetically
842
+ limitedItems.sort((a, b) => {
843
+ if (a.type !== b.type) {
844
+ return a.type === 'dir' ? -1 : 1;
845
+ }
846
+ return a.name.localeCompare(b.name);
847
+ });
848
+ // Create simplified, token-efficient structure
849
+ const files = limitedItems
850
+ .filter(item => item.type === 'file')
851
+ .map(item => ({
852
+ name: item.name,
853
+ size: item.size,
854
+ url: item.path, // Use path for fetching
855
+ }));
856
+ const folders = limitedItems
857
+ .filter(item => item.type === 'dir')
858
+ .map(item => ({
859
+ name: item.name,
860
+ url: item.path, // Use path for browsing
861
+ }));
862
+ return createResult({
863
+ data: {
864
+ repository: `${owner}/${repo}`,
865
+ branch: usedBranch,
866
+ path: cleanPath || '/',
867
+ githubBasePath: `https://api.github.com/repos/${owner}/${repo}/contents/`,
868
+ files: {
869
+ count: files.length,
870
+ files: files,
871
+ },
872
+ folders: {
873
+ count: folders.length,
874
+ folders: folders,
875
+ },
876
+ },
877
+ });
878
+ }
879
+ catch (error) {
880
+ return createResult({
881
+ error: `Failed to access repository. ${error}. Verify repository name and authentication`,
882
+ });
883
+ }
727
884
  });
728
885
  }
729
886
  /**
730
- * Generate smart suggestions based on current search parameters
887
+ * Smart branch detection with automatic fallback to common branch names.
731
888
  */
732
- function generateSmartSuggestions(params) {
733
- const suggestions = [];
734
- // Query-specific suggestions
735
- if (params.query.includes('"')) {
736
- suggestions.push('Remove quotes to search for individual terms instead of exact phrase');
737
- }
738
- if (params.query.includes(' AND ') || params.query.includes(' OR ')) {
739
- suggestions.push('Try simpler queries without boolean operators');
740
- }
741
- if (params.query.length > 50) {
742
- suggestions.push('Simplify query - shorter terms often work better');
743
- }
744
- // Filter-specific suggestions
745
- if (!params.language) {
746
- suggestions.push('Add language filter - this improves results by 90%');
747
- }
748
- if (params.language && !params.owner && !params.filename) {
749
- suggestions.push('Add owner filter to target specific organizations');
750
- }
751
- if (params.owner && params.repo) {
752
- suggestions.push('Remove repo filter and search across all repos in the organization');
753
- }
754
- if (params.filename) {
755
- suggestions.push('Remove filename filter to search all file types');
756
- }
757
- if (params.extension && params.language) {
758
- suggestions.push('Try removing extension filter (language filter might be sufficient)');
759
- }
760
- // Always provide generic fallbacks
761
- if (suggestions.length === 0) {
762
- suggestions.push('Try a broader search with fewer filters');
763
- suggestions.push('Use more common/general terms');
764
- suggestions.push('Check for typos in query or filter values');
765
- }
766
- return suggestions.slice(0, 5); // Limit to top 5 suggestions
767
- }
768
- /**
769
- * Generate fallback queries that are likely to succeed
770
- */
771
- function generateFallbackQueries(params) {
772
- const fallbacks = [];
773
- // Extract key terms from original query
774
- const keyTerms = extractKeyTerms(params.query);
775
- // Fallback 1: Simplify query, keep language if present
776
- if (keyTerms.length > 1) {
777
- const simplifiedQuery = keyTerms[0];
778
- fallbacks.push({
779
- query: simplifiedQuery,
780
- description: `Search for just "${simplifiedQuery}"${params.language ? ` in ${params.language}` : ''}`,
781
- rationale: 'Single term searches often yield better results',
782
- });
783
- }
784
- // Fallback 2: Remove filters but keep core query
785
- if (params.language || params.owner || params.filename || params.extension) {
786
- fallbacks.push({
787
- query: keyTerms.join(' '),
788
- description: `Search "${keyTerms.join(' ')}" without filters`,
789
- rationale: 'Removes restrictive filters for broader results',
790
- });
791
- }
792
- // Fallback 3: Language-specific popular search
793
- if (params.language) {
794
- const popularTerms = getPopularTermsForLanguage(params.language);
795
- if (popularTerms.length > 0) {
796
- fallbacks.push({
797
- query: popularTerms[0],
798
- description: `Popular ${params.language} pattern: "${popularTerms[0]}"`,
799
- rationale: `Common patterns in ${params.language} development`,
800
- });
801
- }
802
- }
803
- // Fallback 4: Broader category search
804
- const category = inferCategory(params.query);
805
- if (category) {
806
- fallbacks.push({
807
- query: category,
808
- description: `Broader search: "${category}"`,
809
- rationale: 'Searches the general category for related patterns',
889
+ async function getSmartBranchFallback(owner, repo, requestedBranch) {
890
+ const branches = [requestedBranch];
891
+ try {
892
+ // Try to get repository info to find default branch
893
+ const repoInfoResult = await executeGitHubCommand('api', [`/repos/${owner}/${repo}`], {
894
+ cache: false,
810
895
  });
811
- }
812
- return fallbacks.slice(0, 4);
813
- }
814
- /**
815
- * Extract key terms from search query
816
- */
817
- function extractKeyTerms(query) {
818
- // Remove quotes, boolean operators, and special characters
819
- const cleaned = query
820
- .replace(/["']/g, '')
821
- .replace(/\b(AND|OR|NOT)\b/gi, '')
822
- .replace(/[^\w\s]/g, ' ')
823
- .replace(/\s+/g, ' ')
824
- .trim();
825
- return cleaned.split(' ').filter(term => term.length > 2);
826
- }
827
- /**
828
- * Get popular search terms for specific programming languages
829
- */
830
- function getPopularTermsForLanguage(language) {
831
- const popularTerms = {
832
- javascript: [
833
- 'useState',
834
- 'async await',
835
- 'fetch api',
836
- 'event listener',
837
- 'promise',
838
- ],
839
- typescript: [
840
- 'interface',
841
- 'generic type',
842
- 'type guard',
843
- 'enum',
844
- 'namespace',
845
- ],
846
- python: ['def function', 'import', 'class method', 'lambda', 'decorator'],
847
- java: [
848
- 'public class',
849
- 'static method',
850
- 'interface',
851
- 'annotation',
852
- 'spring',
853
- ],
854
- go: ['func main', 'interface', 'goroutine', 'channel', 'struct'],
855
- rust: ['fn main', 'impl', 'trait', 'enum', 'match'],
856
- cpp: ['class', 'template', 'namespace', 'virtual', 'std::'],
857
- c: ['int main', 'struct', 'malloc', 'pointer', 'header'],
858
- php: ['function', 'class', 'namespace', 'interface', 'trait'],
859
- ruby: ['def', 'class', 'module', 'block', 'gem'],
860
- swift: ['func', 'class', 'protocol', 'extension', 'guard'],
861
- kotlin: ['fun', 'class', 'interface', 'data class', 'coroutine'],
862
- dart: ['class', 'function', 'widget', 'async', 'future'],
863
- shell: ['function', 'if then', 'for loop', 'variable', 'script'],
864
- yaml: ['version', 'name', 'build', 'deploy', 'config'],
865
- dockerfile: ['FROM', 'RUN', 'COPY', 'EXPOSE', 'CMD'],
866
- };
867
- return popularTerms[language.toLowerCase()] || [];
868
- }
869
- /**
870
- * Infer search category from query terms
871
- */
872
- function inferCategory(query) {
873
- const categories = {
874
- authentication: [
875
- 'auth',
876
- 'login',
877
- 'oauth',
878
- 'jwt',
879
- 'token',
880
- 'passport',
881
- 'session',
882
- ],
883
- database: ['db', 'sql', 'query', 'model', 'schema', 'migration', 'orm'],
884
- api: ['rest', 'graphql', 'endpoint', 'route', 'request', 'response'],
885
- testing: ['test', 'spec', 'mock', 'assert', 'unit', 'integration'],
886
- config: ['config', 'setting', 'env', 'environment', 'setup'],
887
- ui: ['component', 'button', 'form', 'modal', 'layout', 'style'],
888
- error: ['error', 'exception', 'try', 'catch', 'throw', 'handle'],
889
- performance: ['cache', 'optimization', 'memory', 'speed', 'performance'],
890
- security: ['security', 'encrypt', 'hash', 'validate', 'sanitize'],
891
- };
892
- const queryLower = query.toLowerCase();
893
- for (const [category, keywords] of Object.entries(categories)) {
894
- if (keywords.some(keyword => queryLower.includes(keyword))) {
895
- return category;
896
- }
897
- }
898
- return null;
899
- }
900
- /**
901
- * Transform GitHub CLI response to optimized format with enhanced metadata
902
- */
903
- function transformToOptimizedFormat$1(items, params) {
904
- const searchEfficiency = calculateSearchEfficiency(params);
905
- // Extract repository info if single repo search
906
- const singleRepo = extractSingleRepository$1(items);
907
- const optimizedItems = items.map(item => ({
908
- path: item.path,
909
- matches: item.textMatches.map(match => ({
910
- context: optimizeTextMatch(match.fragment, 120), // Increased context for better understanding
911
- positions: match.matches.map(m => m.indices),
912
- })),
913
- url: singleRepo ? item.path : simplifyGitHubUrl(item.url),
914
- }));
915
- const result = {
916
- items: optimizedItems,
917
- total_count: items.length,
918
- };
919
- // Add repository info if single repo
920
- if (singleRepo) {
921
- result.repository = {
922
- name: singleRepo.nameWithOwner,
923
- url: simplifyRepoUrl(singleRepo.url),
924
- };
925
- }
926
- // Enhanced metadata with search efficiency and tips
927
- result.metadata = {
928
- has_filters: !!(params.language ||
929
- params.owner ||
930
- params.filename ||
931
- params.extension),
932
- search_scope: params.match
933
- ? Array.isArray(params.match)
934
- ? params.match.join(',')
935
- : params.match
936
- : 'file',
937
- search_efficiency: searchEfficiency,
938
- };
939
- // Add performance tips for low-efficiency searches
940
- if (searchEfficiency.score < 7) {
941
- result.metadata.performance_tips = generatePerformanceTips(params);
942
- }
943
- return result;
944
- }
945
- /**
946
- * Calculate search efficiency score and provide insights
947
- */
948
- function calculateSearchEfficiency(params) {
949
- let score = 5; // Base score
950
- const factors = [];
951
- const recommendations = [];
952
- // Language filter is huge efficiency boost
953
- if (params.language) {
954
- score += 3;
955
- factors.push('Language filter (+3)');
956
- }
957
- else {
958
- recommendations.push('Add language filter for 90% performance boost');
959
- }
960
- // Owner filter provides good targeting
961
- if (params.owner) {
962
- score += 2;
963
- factors.push('Owner filter (+2)');
964
- }
965
- // Filename filter is very efficient
966
- if (params.filename) {
967
- score += 2;
968
- factors.push('Filename filter (+2)');
969
- }
970
- // Extension filter adds precision
971
- if (params.extension) {
972
- score += 1;
973
- factors.push('Extension filter (+1)');
974
- }
975
- // Repository filter is highly targeted
976
- if (params.repo) {
977
- score += 2;
978
- factors.push('Repository filter (+2)');
979
- }
980
- // Size filter helps avoid huge files
981
- if (params.size) {
982
- score += 1;
983
- factors.push('Size filter (+1)');
984
- }
985
- // Boolean operators are now properly supported
986
- if (params.query.includes(' OR ')) {
987
- // OR is powerful when used correctly
988
- if (params.language || params.owner) {
989
- score += 1;
990
- factors.push('OR with filters (+1)');
991
- }
992
- else {
993
- factors.push('OR operator (neutral)');
994
- recommendations.push('Add language or owner filter with OR for better targeting');
995
- }
996
- }
997
- if (params.query.includes(' AND ')) {
998
- score += 1;
999
- factors.push('AND operator (+1)');
1000
- }
1001
- if (params.query.includes(' NOT ')) {
1002
- // NOT can be useful but less efficient
1003
- factors.push('NOT operator (neutral)');
1004
- recommendations.push('Consider positive filters alongside NOT for better results');
1005
- }
1006
- if (params.query.length > 100) {
1007
- score -= 1;
1008
- factors.push('Long query (-1)');
1009
- recommendations.push('Simplify query for better results');
1010
- }
1011
- return {
1012
- score: Math.max(1, Math.min(10, score)),
1013
- factors,
1014
- recommendations,
1015
- };
1016
- }
1017
- /**
1018
- * Generate performance tips for inefficient searches
1019
- */
1020
- function generatePerformanceTips(params) {
1021
- const tips = [];
1022
- if (!params.language) {
1023
- tips.push('Add language filter - single biggest performance boost');
1024
- }
1025
- if (!params.owner && !params.repo) {
1026
- tips.push('Add owner filter to target specific organizations');
1027
- }
1028
- if (params.query.includes(' OR ') && !params.language && !params.owner) {
1029
- tips.push('Add language or owner filter with OR for better targeting');
1030
- }
1031
- if (params.query.includes(' NOT ')) {
1032
- tips.push('Combine NOT with positive filters (language, filename, etc.) for best results');
1033
- }
1034
- if (!params.filename && !params.extension) {
1035
- tips.push('Add filename or extension filter for file-type precision');
1036
- }
1037
- return tips.slice(0, 3);
1038
- }
1039
- /**
1040
- * Extract single repository if all results are from same repo
1041
- */
1042
- function extractSingleRepository$1(items) {
1043
- if (items.length === 0)
1044
- return null;
1045
- const firstRepo = items[0].repository;
1046
- const allSameRepo = items.every(item => item.repository.nameWithOwner === firstRepo.nameWithOwner);
1047
- return allSameRepo ? firstRepo : null;
1048
- }
1049
- /**
1050
- * Build command line arguments for GitHub CLI with improved parameter handling
1051
- * Ensures exact string search capability with proper quote and escape handling
1052
- */
1053
- function buildGitHubCliArgs(params) {
1054
- const args = ['code'];
1055
- // Handle exact string search - preserve quotes and special characters
1056
- const searchQuery = params.query;
1057
- // For exact string searches (quoted strings), preserve the quotes
1058
- // For special characters and escape sequences, pass them through
1059
- // GitHub CLI will handle the proper escaping to the GitHub API
1060
- if (searchQuery.includes('"') || searchQuery.includes("'")) {
1061
- // Already has quotes - exact string search
1062
- args.push(searchQuery);
1063
- }
1064
- else if (searchQuery.includes('\\') ||
1065
- /[<>{}[\]()&|;$`!*?~]/.test(searchQuery)) {
1066
- // Contains special characters that might need exact matching
1067
- // Let GitHub CLI handle the escaping - don't double-escape
1068
- args.push(searchQuery);
1069
- }
1070
- else {
1071
- // Regular search query
1072
- args.push(searchQuery);
1073
- }
1074
- // Add filters in order of effectiveness for better CLI performance
1075
- if (params.language) {
1076
- args.push(`--language=${params.language}`);
1077
- }
1078
- if (params.filename) {
1079
- args.push(`--filename=${params.filename}`);
1080
- }
1081
- if (params.extension) {
1082
- args.push(`--extension=${params.extension}`);
1083
- }
1084
- if (params.size) {
1085
- args.push(`--size=${params.size}`);
1086
- }
1087
- // Always add limit
1088
- if (params.limit) {
1089
- args.push(`--limit=${params.limit}`);
1090
- }
1091
- // Handle match parameter
1092
- if (params.match) {
1093
- const matchValues = Array.isArray(params.match)
1094
- ? params.match
1095
- : [params.match];
1096
- // Use the first match type when multiple are provided
1097
- const matchValue = matchValues[0];
1098
- args.push(`--match=${matchValue}`);
1099
- }
1100
- // Handle owner parameter - can be string or array
1101
- if (params.owner && !params.repo) {
1102
- const ownerValues = Array.isArray(params.owner)
1103
- ? params.owner
1104
- : [params.owner];
1105
- ownerValues.forEach(owner => args.push(`--owner=${owner}`));
1106
- }
1107
- // Handle repository filters with improved validation
1108
- if (params.repo) {
1109
- const repos = Array.isArray(params.repo) ? params.repo : [params.repo];
1110
- if (params.owner) {
1111
- const owners = Array.isArray(params.owner)
1112
- ? params.owner
1113
- : [params.owner];
1114
- // Create repo filters for each owner/repo combination
1115
- owners.forEach(owner => {
1116
- repos.forEach(repo => {
1117
- // Handle both "owner/repo" format and just "repo" format
1118
- if (repo.includes('/')) {
1119
- args.push(`--repo=${repo}`);
1120
- }
1121
- else {
1122
- args.push(`--repo=${owner}/${repo}`);
1123
- }
1124
- });
1125
- });
1126
- }
1127
- else {
1128
- // Handle repo without owner (must be in owner/repo format)
1129
- repos.forEach(repo => {
1130
- args.push(`--repo=${repo}`);
1131
- });
1132
- }
1133
- }
1134
- // JSON output with all available fields
1135
- args.push('--json=repository,path,textMatches,sha,url');
1136
- return args;
1137
- }
1138
- async function searchGitHubCode(params) {
1139
- const cacheKey = generateCacheKey('gh-code', params);
1140
- return withCache(cacheKey, async () => {
1141
- try {
1142
- const args = buildGitHubCliArgs(params);
1143
- const result = await executeGitHubCommand('search', args, {
1144
- cache: false,
1145
- });
1146
- return result;
1147
- }
1148
- catch (error) {
1149
- const errorMessage = error.message || '';
1150
- // Parse specific GitHub CLI error types
1151
- if (errorMessage.includes('authentication')) {
1152
- return createResult({
1153
- error: 'GitHub authentication required - run api_status_check tool',
1154
- });
1155
- }
1156
- if (errorMessage.includes('rate limit')) {
1157
- return createResult({
1158
- error: 'GitHub rate limit exceeded - use more specific filters or wait',
1159
- suggestions: [
1160
- 'Add language filter to reduce search scope',
1161
- 'Use owner filter to target specific organizations',
1162
- 'Add filename filter for precision targeting',
1163
- 'Wait a few minutes and try again',
1164
- ],
1165
- });
1166
- }
1167
- if (errorMessage.includes('validation failed') ||
1168
- errorMessage.includes('Invalid query')) {
1169
- return createResult({
1170
- error: 'Invalid query syntax. GitHub legacy search has limitations.',
1171
- suggestions: [
1172
- 'Remove NOT operators (use positive filters instead)',
1173
- 'Simplify OR logic to separate searches',
1174
- 'Use quotes for exact phrases: "error handling"',
1175
- 'Try: language:python + "function definition" instead of complex boolean',
1176
- ],
1177
- });
1178
- }
1179
- if (errorMessage.includes('repository not found') ||
1180
- errorMessage.includes('owner not found')) {
1181
- return createResult({
1182
- error: 'Repository or owner not found',
1183
- suggestions: [
1184
- 'Verify exact owner/repository names (case-sensitive)',
1185
- 'Check if repository is private and you have access',
1186
- 'Remove repo filter to search across all repositories',
1187
- 'Use owner filter instead of specific repo for broader search',
1188
- ],
1189
- });
1190
- }
1191
- if (errorMessage.includes('timeout')) {
1192
- return createResult({
1193
- error: 'Search timeout - query too broad',
1194
- suggestions: [
1195
- 'Add language filter to narrow scope',
1196
- 'Use owner or repo filters for targeting',
1197
- 'Simplify query terms',
1198
- 'Add filename filter for specific file types',
1199
- ],
1200
- });
896
+ if (!repoInfoResult.isError) {
897
+ const execResult = JSON.parse(repoInfoResult.content[0].text);
898
+ const repoData = execResult.result;
899
+ const defaultBranch = repoData.default_branch;
900
+ if (defaultBranch && !branches.includes(defaultBranch)) {
901
+ branches.push(defaultBranch);
1201
902
  }
1202
- // Generic fallback with helpful guidance
1203
- return createResult({
1204
- error: 'Code search failed',
1205
- suggestions: [
1206
- 'Check GitHub CLI authentication with: gh auth status',
1207
- 'Simplify query and add language filter',
1208
- 'Try owner filter for targeted search',
1209
- 'Use api_status_check tool to verify setup',
1210
- ],
1211
- });
1212
903
  }
1213
- });
1214
- }
1215
- /**
1216
- * Enhanced validation with helpful suggestions
1217
- * Supports exact string search with quotes and special characters
1218
- */
1219
- function validateSearchParameters(params) {
1220
- // Query validation
1221
- if (!params.query.trim()) {
1222
- return 'Empty query. Try: "useState", "authentication", "docker setup", or use filters like language:python';
1223
904
  }
1224
- if (params.query.length > 1000) {
1225
- return 'Query too long (max 1000 chars). Simplify to key terms like "error handling" instead of full sentences.';
905
+ catch {
906
+ // If we can't get repo info, proceed with standard fallbacks
1226
907
  }
1227
- // Repository validation - allow owner/repo format in repo field
1228
- if (params.repo && !params.owner) {
1229
- const repoValues = Array.isArray(params.repo) ? params.repo : [params.repo];
1230
- const hasOwnerFormat = repoValues.every(repo => repo.includes('/'));
1231
- if (!hasOwnerFormat) {
1232
- return 'Repository format error. Use "owner/repo" format like "facebook/react" or provide both owner and repo parameters.';
908
+ // Add common branch names if not already included
909
+ const commonBranches = ['main', 'master', 'develop', 'dev'];
910
+ commonBranches.forEach(branch => {
911
+ if (!branches.includes(branch)) {
912
+ branches.push(branch);
1233
913
  }
1234
- }
1235
- // Support exact string searches with quotes and special characters
1236
- // Remove overly restrictive validation for escape characters
1237
- // GitHub CLI and API can handle special characters properly
1238
- // Boolean operator validation with suggestions
1239
- const invalidBooleans = params.query.match(/\b(and|or|not)\b/g);
1240
- if (invalidBooleans) {
1241
- const corrected = invalidBooleans.map(op => op.toUpperCase()).join(', ');
1242
- return `Boolean operators must be uppercase: ${corrected}. Example: "react OR vue" not "react or vue"`;
1243
- }
1244
- return null; // No validation errors
914
+ });
915
+ return branches;
1245
916
  }
1246
917
 
1247
- const TOOL_NAME$7 = 'github_get_file_content';
1248
- const DESCRIPTION$7 = `Read file content with exact path verification. Smart branch fallbacks and size limits. Use github_get_contents first to verify file existence.`;
918
+ const GITHUB_GET_FILE_CONTENT_TOOL_NAME = 'githubGetFileContent';
919
+ const DESCRIPTION$7 = `Fetch file content from GitHub repositories. Use ${GITHUB_VIEW_REPO_STRUCTURE_TOOL_NAME} first to explore repository structure and find exact file paths. Supports automatic branch fallback (main/master) and handles files up to 300KB. Parameters: owner (required - GitHub username/org), repo (required - repository name), branch (required), filePath (required).`;
1249
920
  function registerFetchGitHubFileContentTool(server) {
1250
- server.registerTool(TOOL_NAME$7, {
921
+ server.registerTool(GITHUB_GET_FILE_CONTENT_TOOL_NAME, {
1251
922
  description: DESCRIPTION$7,
1252
923
  inputSchema: {
1253
924
  owner: z
@@ -1255,26 +926,26 @@ function registerFetchGitHubFileContentTool(server) {
1255
926
  .min(1)
1256
927
  .max(100)
1257
928
  .regex(/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/)
1258
- .describe(`Repository owner/organization`),
929
+ .describe(`Repository owner/org name (e.g., 'microsoft', 'google', NOT 'microsoft/vscode')`),
1259
930
  repo: z
1260
931
  .string()
1261
932
  .min(1)
1262
933
  .max(100)
1263
934
  .regex(/^[a-zA-Z0-9._-]+$/)
1264
- .describe(`Repository name. Case-sensitive.`),
935
+ .describe(`Repository name only (e.g., 'vscode', 'react', NOT 'microsoft/vscode')`),
1265
936
  branch: z
1266
937
  .string()
1267
938
  .min(1)
1268
939
  .max(255)
1269
940
  .regex(/^[^\s]+$/)
1270
- .describe(`Branch name. Auto-fallback to common branches if not found.`),
941
+ .describe(`Branch name. Falls back to main/master if not found`),
1271
942
  filePath: z
1272
943
  .string()
1273
944
  .min(1)
1274
- .describe(`File path from repository root. Use github_get_contents to explore structure.`),
945
+ .describe(`Exact file path from repo root (e.g., src/index.js, README.md)`),
1275
946
  },
1276
947
  annotations: {
1277
- title: 'GitHub File Content Reader',
948
+ title: 'GitHub File Content - Direct Access',
1278
949
  readOnlyHint: true,
1279
950
  destructiveHint: false,
1280
951
  idempotentHint: true,
@@ -1287,7 +958,7 @@ function registerFetchGitHubFileContentTool(server) {
1287
958
  }
1288
959
  catch (error) {
1289
960
  return createResult({
1290
- error: 'File fetch failed - verify path or try github_get_contents first',
961
+ error: 'Failed to fetch file. Verify path with github_get_contents first',
1291
962
  });
1292
963
  }
1293
964
  });
@@ -1322,27 +993,23 @@ async function fetchGitHubFileContent(params) {
1322
993
  // Handle common errors
1323
994
  if (errorMsg.includes('404')) {
1324
995
  return createResult({
1325
- error: 'File not found - verify path with github_get_contents',
1326
- cli_command: `gh api "/repos/${owner}/${repo}/contents/${filePath}?ref=${branch}"`,
996
+ error: 'File not found. Use github_view_repo_structure to explore repository structure',
1327
997
  });
1328
998
  }
1329
999
  else if (errorMsg.includes('403')) {
1330
1000
  return createResult({
1331
- error: 'Access denied - repository may be private',
1332
- cli_command: `gh api "/repos/${owner}/${repo}/contents/${filePath}?ref=${branch}"`,
1001
+ error: 'Access denied. Repository may be private - use apiStatusCheck to verify',
1333
1002
  });
1334
1003
  }
1335
1004
  else if (errorMsg.includes('maxBuffer') ||
1336
1005
  errorMsg.includes('stdout maxBuffer length exceeded')) {
1337
1006
  return createResult({
1338
- error: 'File too large (>300KB) - use github_search_code for patterns instead',
1339
- cli_command: `gh api "/repos/${owner}/${repo}/contents/${filePath}?ref=${branch}"`,
1007
+ error: 'File too large (>300KB). Use githubSearchCode to search for patterns within the file',
1340
1008
  });
1341
1009
  }
1342
1010
  else {
1343
1011
  return createResult({
1344
- error: 'Fetch failed - check repository and file path',
1345
- cli_command: `gh api "/repos/${owner}/${repo}/contents/${filePath}?ref=${branch}"`,
1012
+ error: 'Failed to fetch file. Verify repository name and file path',
1346
1013
  });
1347
1014
  }
1348
1015
  }
@@ -1354,11 +1021,11 @@ async function fetchGitHubFileContent(params) {
1354
1021
  if (errorMessage.includes('maxBuffer') ||
1355
1022
  errorMessage.includes('stdout maxBuffer length exceeded')) {
1356
1023
  return createResult({
1357
- error: 'File too large (>300KB) - use github_search_code for patterns instead',
1024
+ error: 'File too large (>300KB). Use github_search_code to search for patterns within the file',
1358
1025
  });
1359
1026
  }
1360
1027
  return createResult({
1361
- error: 'Unexpected error during file fetch - check connection',
1028
+ error: 'Unexpected error. Check network connection and try again',
1362
1029
  });
1363
1030
  }
1364
1031
  });
@@ -1366,28 +1033,33 @@ async function fetchGitHubFileContent(params) {
1366
1033
  async function processFileContent(result, owner, repo, branch, filePath) {
1367
1034
  // Extract the actual content from the exec result
1368
1035
  const execResult = JSON.parse(result.content[0].text);
1369
- const fileData = JSON.parse(execResult.result);
1036
+ const fileData = execResult.result;
1370
1037
  // Check if it's a directory
1371
1038
  if (Array.isArray(fileData)) {
1372
1039
  return createResult({
1373
- error: 'Path is directory - use github_get_contents instead',
1040
+ error: 'Path is a directory. Use github_view_repo_structure to list directory contents',
1374
1041
  });
1375
1042
  }
1376
- const fileSize = fileData.size || 0;
1043
+ const fileSize = typeof fileData.size === 'number' ? fileData.size : 0;
1377
1044
  const MAX_FILE_SIZE = 300 * 1024; // 300KB limit for better performance and reliability
1378
1045
  // Check file size with helpful message
1379
1046
  if (fileSize > MAX_FILE_SIZE) {
1380
1047
  const fileSizeKB = Math.round(fileSize / 1024);
1381
1048
  const maxSizeKB = Math.round(MAX_FILE_SIZE / 1024);
1382
1049
  return createResult({
1383
- error: `File too large (${fileSizeKB}KB > ${maxSizeKB}KB) - use github_search_code for patterns`,
1050
+ error: `File too large (${fileSizeKB}KB > ${maxSizeKB}KB). Use githubSearchCode to search within the file`,
1051
+ });
1052
+ }
1053
+ // Get and decode content with validation
1054
+ if (!fileData.content) {
1055
+ return createResult({
1056
+ error: 'File is empty - no content to display',
1384
1057
  });
1385
1058
  }
1386
- // Get and decode content
1387
- const base64Content = fileData.content?.replace(/\s/g, ''); // Remove all whitespace
1059
+ const base64Content = fileData.content.replace(/\s/g, ''); // Remove all whitespace
1388
1060
  if (!base64Content) {
1389
1061
  return createResult({
1390
- error: 'Empty file - no content to display',
1062
+ error: 'File is empty - no content to display',
1391
1063
  });
1392
1064
  }
1393
1065
  let decodedContent;
@@ -1396,215 +1068,61 @@ async function processFileContent(result, owner, repo, branch, filePath) {
1396
1068
  // Simple binary check - look for null bytes
1397
1069
  if (buffer.indexOf(0) !== -1) {
1398
1070
  return createResult({
1399
- error: 'Binary file detected - cannot display as text',
1071
+ error: 'Binary file detected. Cannot display as text - download directly from GitHub',
1400
1072
  });
1401
1073
  }
1402
1074
  decodedContent = buffer.toString('utf-8');
1403
1075
  }
1404
1076
  catch (decodeError) {
1405
1077
  return createResult({
1406
- error: 'Decode failed - file encoding not supported',
1078
+ error: 'Failed to decode file. Encoding may not be supported (expected UTF-8)',
1407
1079
  });
1408
1080
  }
1409
- // Return simplified response
1410
- const response = {
1411
- filePath,
1412
- owner,
1413
- repo,
1414
- branch,
1415
- content: decodedContent,
1416
- metadata: {
1417
- size: fileSize,
1418
- lines: decodedContent.split('\n').length,
1419
- encoding: 'utf-8',
1081
+ return createResult({
1082
+ data: {
1083
+ filePath,
1084
+ owner,
1085
+ repo,
1086
+ branch,
1087
+ content: decodedContent,
1420
1088
  },
1421
- };
1422
- return createResult({ data: response });
1089
+ });
1423
1090
  }
1424
1091
 
1425
- /**
1426
- * GitHub Repository Search Tool
1427
- *
1428
- * MOST EFFECTIVE PATTERNS (based on testing):
1429
- *
1430
- * 1. Quality Discovery:
1431
- * { topic: ["react", "typescript"], stars: "1000..5000", limit: 10 }
1432
- *
1433
- * 2. Organization Research:
1434
- * { owner: ["microsoft", "google"], language: "python", limit: 10 }
1435
- *
1436
- * 3. Beginner Projects:
1437
- * { goodFirstIssues: ">=5", stars: "100..5000", limit: 10 }
1438
- *
1439
- * 4. Recent Quality:
1440
- * { stars: ">1000", created: ">2023-01-01", limit: 10 }
1441
- *
1442
- * AVOID: OR queries + language filter, 5+ filters, multi-word OR
1443
- * TIP: Use limit parameter instead of adding more filters
1444
- */
1445
- const TOOL_NAME$6 = 'github_search_repositories';
1446
- const DESCRIPTION$6 = `Search GitHub repositories with powerful GitHub search syntax and advanced filtering.
1447
-
1448
- EMBEDDED QUALIFIERS (MOST POWERFUL):
1449
- - "vue OR react stars:>1000 language:javascript" - OR logic with constraints
1450
- - "typescript AND framework stars:100..5000" - AND logic with star range
1451
- - "repo:facebook/react OR repo:vuejs/vue" - Multiple specific repositories
1452
- - "org:microsoft OR org:google language:typescript" - Multiple organizations
1453
- - "topic:react topic:typescript stars:>500" - Multiple topics with quality filter
1454
-
1455
- TRADITIONAL FILTERS (ALSO SUPPORTED):
1456
- - owner: ["microsoft", "google"] - Multiple owners as array
1457
- - topic: ["react", "typescript"] - Multiple topics as array
1458
- - stars: "1000..5000" - Range or threshold filtering
1459
-
1460
- BEST PRACTICES:
1461
- - Use embedded qualifiers for complex queries with OR/AND logic
1462
- - Use traditional filters for simple, clean parameter-based searches
1463
- - Combine both approaches: "vue OR react" + language:"javascript" + stars:">1000"`;
1464
- /**
1465
- * Extract owner/repo information from various query formats
1466
- */
1467
- function extractOwnerRepoFromQuery(query) {
1468
- let cleanedQuery = query;
1469
- let extractedOwner;
1470
- let extractedRepo;
1471
- // Pattern 1: GitHub URLs (https://github.com/owner/repo)
1472
- const githubUrlMatch = query.match(/github\.com\/([^\\s]+)\/([^\\s]+)/i);
1473
- if (githubUrlMatch) {
1474
- extractedOwner = githubUrlMatch[1];
1475
- extractedRepo = githubUrlMatch[2];
1476
- cleanedQuery = query
1477
- .replace(/https?:\/\/github\.com\/[^\\s]+\/[^\\s]+/gi, '')
1478
- .trim();
1479
- }
1480
- // Pattern 2: owner/repo format in query
1481
- const ownerRepoMatch = query.match(/\b([a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\/([a-zA-Z0-9][a-zA-Z0-9\-.]*[a-zA-Z0-9])\b/);
1482
- if (ownerRepoMatch && !extractedOwner) {
1483
- extractedOwner = ownerRepoMatch[1];
1484
- extractedRepo = ownerRepoMatch[2];
1485
- cleanedQuery = query.replace(ownerRepoMatch[0], '').trim();
1486
- }
1487
- // Pattern 3: NPM package-like references (@scope/package)
1488
- const scopedPackageMatch = query.match(/@([a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\/([a-zA-Z0-9][a-zA-Z0-9\-.]*[a-zA-Z0-9])/);
1489
- if (scopedPackageMatch && !extractedOwner) {
1490
- extractedOwner = scopedPackageMatch[1];
1491
- extractedRepo = scopedPackageMatch[2];
1492
- cleanedQuery = query.replace(scopedPackageMatch[0], '').trim();
1493
- }
1494
- return {
1495
- extractedOwner,
1496
- extractedRepo,
1497
- cleanedQuery: cleanedQuery || query,
1498
- };
1499
- }
1500
- function registerSearchGitHubReposTool(server) {
1501
- server.registerTool(TOOL_NAME$6, {
1092
+ const GITHUB_SEARCH_CODE_TOOL_NAME = 'githubSearchCode';
1093
+ const DESCRIPTION$6 = `Search code across GitHub repositories. Start with simple 1-2 word queries, then refine with filters like language, owner, or filename. Supports exact phrase matching with quotes. Parameters: query (required), language (optional), owner (optional - GitHub username/org, NOT owner/repo), filename (optional), extension (optional), match (optional), size (optional), limit (optional).`;
1094
+ function registerGitHubSearchCodeTool(server) {
1095
+ server.registerTool(GITHUB_SEARCH_CODE_TOOL_NAME, {
1502
1096
  description: DESCRIPTION$6,
1503
1097
  inputSchema: {
1504
1098
  query: z
1505
1099
  .string()
1506
- .optional()
1507
- .describe('Search query with GitHub search syntax. POWERFUL EXAMPLES: "vue OR react stars:>1000", "typescript AND framework stars:100..5000", "repo:facebook/react OR repo:vuejs/vue", "org:microsoft language:typescript", "topic:react topic:typescript stars:>500". SUPPORTS: OR/AND logic, embedded qualifiers (stars:, language:, org:, repo:, topic:, etc.), exact repository targeting. COMBINES with traditional filters for maximum flexibility.'),
1508
- // CORE FILTERS (GitHub CLI flags)
1509
- owner: z
1510
- .union([z.string(), z.array(z.string())])
1511
- .optional()
1512
- .describe('Repository owner/organization. HIGHLY EFFECTIVE as array ["microsoft", "google"]. FIXED: Now supports multiple owners with comma separation. Best for targeted research.'),
1100
+ .min(1)
1101
+ .describe('Search terms. START SIMPLE: Use 1-2 words first (e.g., "useState", "auth"). Add more terms only after seeing initial results.'),
1513
1102
  language: z
1514
1103
  .string()
1515
1104
  .optional()
1516
- .describe('Programming language filter. CAUTION: Restrictive with other filters. Use alone or with stars/owner only.'),
1517
- stars: z
1518
- .union([
1519
- z.number().int().min(0),
1520
- z
1521
- .string()
1522
- .regex(/^(>=?\d+|<=?\d+|\d+\.\.\d+|\d+)$/, 'Invalid stars format. Use: number, ">100", ">=50", "<200", "<=100", or "10..100"'),
1523
- ])
1524
- .optional()
1525
- .describe('Stars filter. MOST EFFECTIVE: ranges "1000..5000", thresholds ">1000". Excellent for quality filtering.'),
1526
- topic: z
1527
- .union([z.string(), z.array(z.string())])
1528
- .optional()
1529
- .describe('Topics filter. BEST PATTERN: arrays ["react", "typescript"]. FIXED: Now supports comma-separated topics. Preferred over OR queries. Combines well with stars.'),
1530
- forks: z.number().optional().describe('Number of forks filter.'),
1531
- // UPDATED: Match CLI parameter name exactly
1532
- numberOfTopics: z
1533
- .number()
1534
- .optional()
1535
- .describe('Filter by number of topics (indicates documentation quality).'),
1536
- // QUALITY & STATE FILTERS
1537
- license: z
1105
+ .describe('Language filter (javascript, python, etc). Use only when needed.'),
1106
+ owner: z
1538
1107
  .union([z.string(), z.array(z.string())])
1539
1108
  .optional()
1540
- .describe('License filter. Works well as array ["mit", "apache-2.0"].'),
1541
- archived: z
1542
- .boolean()
1109
+ .describe('Repository owner/org (for organization-specific searches). Format: username or org-name only, NOT owner/repo. Use this to search within a specific organization.'),
1110
+ filename: z
1111
+ .string()
1543
1112
  .optional()
1544
- .describe('Filter archived repositories (true/false).'),
1545
- includeForks: z
1546
- .enum(['false', 'true', 'only'])
1113
+ .describe('Specific filename to search. Use for targeted searches.'),
1114
+ extension: z
1115
+ .string()
1547
1116
  .optional()
1548
- .describe('Include forks: false (exclude), true (include), only (forks only).'),
1549
- visibility: z
1550
- .enum(['public', 'private', 'internal'])
1117
+ .describe('File extension (.js, .py, etc). Alternative to language filter.'),
1118
+ match: z
1119
+ .union([z.enum(['file', 'path']), z.array(z.enum(['file', 'path']))])
1551
1120
  .optional()
1552
- .describe('Repository visibility filter.'),
1553
- // DATE & SIZE FILTERS
1554
- created: z
1121
+ .describe('Search scope: "file" for content, "path" for filenames. Default: file content.'),
1122
+ size: z
1555
1123
  .string()
1556
1124
  .optional()
1557
- .describe('Created date filter. Format: ">2020-01-01", "<2023-12-31".'),
1558
- updated: z
1559
- .string()
1560
- .optional()
1561
- .describe('Updated date filter. Good for finding active projects.'),
1562
- size: z
1563
- .string()
1564
- .optional()
1565
- .describe('Repository size filter in KB. Format: ">1000", "<500".'),
1566
- // COMMUNITY FILTERS - Match CLI parameter names exactly
1567
- goodFirstIssues: z
1568
- .union([
1569
- z.number().int().min(0),
1570
- z
1571
- .string()
1572
- .regex(/^(>=?\d+|<=?\d+|\d+\.\.\d+|\d+)$/, 'Invalid format. Use: number, ">5", ">=10", "<20", "<=15", or "5..20"'),
1573
- ])
1574
- .optional()
1575
- .describe('Good first issues count. WORKING: Filter for beginner-friendly projects. EXCELLENT when combined with stars "100..5000" for quality beginner projects.'),
1576
- helpWantedIssues: z
1577
- .union([
1578
- z.number().int().min(0),
1579
- z
1580
- .string()
1581
- .regex(/^(>=?\d+|<=?\d+|\d+\.\.\d+|\d+)$/, 'Invalid format. Use: number, ">5", ">=10", "<20", "<=15", or "5..20"'),
1582
- ])
1583
- .optional()
1584
- .describe('Help wanted issues count. Good for finding projects needing contributors.'),
1585
- followers: z.number().optional().describe('Followers count filter.'),
1586
- // SEARCH SCOPE
1587
- match: z
1588
- .enum(['name', 'description', 'readme'])
1589
- .optional()
1590
- .describe('Search scope: name, description, or readme content.'),
1591
- // SORTING & LIMITS - Match CLI defaults exactly
1592
- sort: z
1593
- .enum([
1594
- 'forks',
1595
- 'help-wanted-issues',
1596
- 'stars',
1597
- 'updated',
1598
- 'best-match',
1599
- ])
1600
- .optional()
1601
- .default('best-match')
1602
- .describe('Sort by: stars, updated, forks, help-wanted-issues, best-match.'),
1603
- order: z
1604
- .enum(['asc', 'desc'])
1605
- .optional()
1606
- .default('desc')
1607
- .describe('Sort order: asc or desc.'),
1125
+ .describe('File size in KB. Format: >10, <100, or 10..50'),
1608
1126
  limit: z
1609
1127
  .number()
1610
1128
  .int()
@@ -1612,10 +1130,10 @@ function registerSearchGitHubReposTool(server) {
1612
1130
  .max(100)
1613
1131
  .optional()
1614
1132
  .default(30)
1615
- .describe('Results limit (1-100). PREFER increasing limit over adding filters.'),
1133
+ .describe('Results limit (1-100). Default: 30'),
1616
1134
  },
1617
1135
  annotations: {
1618
- title: 'GitHub Repository Search',
1136
+ title: 'GitHub Code Search - Smart & Efficient',
1619
1137
  readOnlyHint: true,
1620
1138
  destructiveHint: false,
1621
1139
  idempotentHint: true,
@@ -1623,325 +1141,307 @@ function registerSearchGitHubReposTool(server) {
1623
1141
  },
1624
1142
  }, async (args) => {
1625
1143
  try {
1626
- // Extract owner/repo from query if present
1627
- const queryInfo = args.query
1628
- ? extractOwnerRepoFromQuery(args.query)
1629
- : {
1630
- cleanedQuery: '',
1631
- extractedOwner: undefined,
1632
- extractedRepo: undefined,
1633
- };
1634
- // Merge extracted owner with explicit owner parameter
1635
- let finalOwner = args.owner;
1636
- if (queryInfo.extractedOwner && !finalOwner) {
1637
- finalOwner = queryInfo.extractedOwner;
1144
+ // Validate parameter combinations
1145
+ const validationError = validateSearchParameters(args);
1146
+ if (validationError) {
1147
+ return createResult({ error: validationError });
1638
1148
  }
1639
- // Update parameters with extracted information
1640
- const enhancedArgs = {
1641
- ...args,
1642
- query: queryInfo.cleanedQuery || args.query,
1643
- owner: finalOwner,
1644
- };
1645
- // Enhanced validation logic for primary filters
1646
- const hasPrimaryFilter = enhancedArgs.query?.trim() ||
1647
- enhancedArgs.owner ||
1648
- enhancedArgs.language ||
1649
- enhancedArgs.topic ||
1650
- enhancedArgs.stars ||
1651
- enhancedArgs.forks;
1652
- if (!hasPrimaryFilter) {
1653
- return createResult({
1654
- error: 'Requires query or primary filter (owner, language, stars, topic, forks). You can also use owner/repo format like "microsoft/vscode" in the query.',
1655
- });
1149
+ const result = await searchGitHubCode(args);
1150
+ if (result.isError) {
1151
+ return result;
1656
1152
  }
1657
- // First attempt: Search with current parameters
1658
- const result = await searchGitHubRepos(enhancedArgs);
1659
- // Fallback for private repositories: If no results and owner is specified, try with private visibility
1660
- if (!result.isError) {
1661
- const resultData = JSON.parse(result.content[0].text);
1662
- if (resultData.total === 0 &&
1663
- enhancedArgs.owner &&
1664
- !enhancedArgs.visibility) {
1665
- // Try searching with private visibility for organization repos
1666
- const privateSearchArgs = {
1667
- ...enhancedArgs,
1668
- visibility: 'private',
1669
- };
1670
- const privateResult = await searchGitHubRepos(privateSearchArgs);
1671
- if (!privateResult.isError) {
1672
- const privateData = JSON.parse(privateResult.content[0].text);
1673
- if (privateData.total > 0) {
1674
- // Return private results with note
1675
- return createResult({
1676
- data: {
1677
- ...privateData,
1678
- note: 'Found results in private repositories within the specified organization.',
1679
- },
1680
- });
1681
- }
1682
- }
1153
+ const execResult = JSON.parse(result.content[0].text);
1154
+ const codeResults = execResult.result;
1155
+ // GitHub CLI returns a direct array, not an object with total_count and items
1156
+ const items = Array.isArray(codeResults) ? codeResults : [];
1157
+ // Smart handling for no results - provide actionable suggestions
1158
+ if (items.length === 0) {
1159
+ // Provide progressive search guidance based on current parameters
1160
+ let specificSuggestion = SUGGESTIONS.CODE_SEARCH_NO_RESULTS;
1161
+ // If filters were used, suggest removing them first
1162
+ if (args.language || args.owner || args.filename || args.extension) {
1163
+ specificSuggestion = SUGGESTIONS.CODE_SEARCH_NO_RESULTS;
1683
1164
  }
1165
+ return createResult({
1166
+ error: getErrorWithSuggestion({
1167
+ baseError: createNoResultsError('code'),
1168
+ suggestion: specificSuggestion,
1169
+ }),
1170
+ });
1684
1171
  }
1685
- return result;
1172
+ // Transform to optimized format
1173
+ const optimizedResult = transformToOptimizedFormat$1(items);
1174
+ return createResult({ data: optimizedResult });
1686
1175
  }
1687
1176
  catch (error) {
1688
- return createResult({
1689
- error: 'Repository search failed - verify connection or simplify query',
1690
- });
1177
+ const errorMessage = error.message || '';
1178
+ return handleSearchError(errorMessage);
1691
1179
  }
1692
1180
  });
1693
1181
  }
1694
- async function searchGitHubRepos(params) {
1695
- const cacheKey = generateCacheKey('gh-repos', params);
1182
+ /**
1183
+ * Handles various search errors and returns a formatted CallToolResult.
1184
+ */
1185
+ function handleSearchError(errorMessage) {
1186
+ // Common GitHub search errors with helpful suggestions
1187
+ if (errorMessage.includes('JSON')) {
1188
+ return createResult({
1189
+ error: ERROR_MESSAGES.CLI_INVALID_RESPONSE,
1190
+ });
1191
+ }
1192
+ if (errorMessage.includes('authentication')) {
1193
+ return createResult({
1194
+ error: createAuthenticationError(),
1195
+ });
1196
+ }
1197
+ if (errorMessage.includes('rate limit')) {
1198
+ return createResult({
1199
+ error: createRateLimitError(true),
1200
+ });
1201
+ }
1202
+ if (errorMessage.includes('timed out')) {
1203
+ return createResult({
1204
+ error: ERROR_MESSAGES.SEARCH_TIMEOUT,
1205
+ });
1206
+ }
1207
+ if (errorMessage.includes('validation failed') ||
1208
+ errorMessage.includes('Invalid query')) {
1209
+ return createResult({
1210
+ error: ERROR_MESSAGES.INVALID_QUERY_SYNTAX,
1211
+ });
1212
+ }
1213
+ if (errorMessage.includes('repository not found') ||
1214
+ errorMessage.includes('owner not found')) {
1215
+ return createResult({
1216
+ error: ERROR_MESSAGES.REPO_OR_OWNER_NOT_FOUND,
1217
+ });
1218
+ }
1219
+ // Generic fallback with guidance
1220
+ return createResult({
1221
+ error: `Code search failed: ${errorMessage}\n${SUGGESTIONS.SIMPLIFY_QUERY}`,
1222
+ });
1223
+ }
1224
+ /**
1225
+ * Transform GitHub CLI response to optimized format with enhanced metadata
1226
+ */
1227
+ function transformToOptimizedFormat$1(items) {
1228
+ // Extract repository info if single repo search
1229
+ const singleRepo = extractSingleRepository$1(items);
1230
+ const optimizedItems = items.map(item => ({
1231
+ path: item.path,
1232
+ matches: item.textMatches?.map(match => ({
1233
+ context: optimizeTextMatch(match.fragment, 120), // Increased context for better understanding
1234
+ positions: match.matches?.map(m => Array.isArray(m.indices) && m.indices.length >= 2
1235
+ ? [m.indices[0], m.indices[1]]
1236
+ : [0, 0]) || [],
1237
+ })) || [],
1238
+ url: singleRepo ? item.path : simplifyGitHubUrl(item.url),
1239
+ }));
1240
+ const result = {
1241
+ items: optimizedItems,
1242
+ total_count: items.length,
1243
+ };
1244
+ // Add repository info if single repo
1245
+ if (singleRepo) {
1246
+ result.repository = {
1247
+ name: singleRepo.nameWithOwner,
1248
+ url: simplifyRepoUrl(singleRepo.url),
1249
+ };
1250
+ }
1251
+ return result;
1252
+ }
1253
+ /**
1254
+ * Extract single repository if all results are from same repo
1255
+ */
1256
+ function extractSingleRepository$1(items) {
1257
+ if (items.length === 0)
1258
+ return null;
1259
+ const firstRepo = items[0].repository;
1260
+ const allSameRepo = items.every(item => item.repository.nameWithOwner === firstRepo.nameWithOwner);
1261
+ return allSameRepo ? firstRepo : null;
1262
+ }
1263
+ /**
1264
+ * Build command line arguments for GitHub CLI with improved parameter handling.
1265
+ * Ensures exact string search capability with proper quote and escape handling.
1266
+ *
1267
+ * This function is refactored to correctly distinguish between search qualifiers
1268
+ * (like `language` and `extension`), which will be passed as separate arguments
1269
+ * to `gh search`, and command-line flags (like `--size` and `--limit`).
1270
+ */
1271
+ function buildGitHubCliArgs(params) {
1272
+ const args = ['code'];
1273
+ // Extract qualifiers from the query
1274
+ const queryParts = params.query.trim().split(/\s+/);
1275
+ const searchTerms = [];
1276
+ const qualifiers = [];
1277
+ queryParts.forEach(part => {
1278
+ if (part.includes(':')) {
1279
+ qualifiers.push(part);
1280
+ }
1281
+ else {
1282
+ searchTerms.push(part);
1283
+ }
1284
+ });
1285
+ // Add search terms if any
1286
+ if (searchTerms.length > 0) {
1287
+ args.push(searchTerms.join(' '));
1288
+ }
1289
+ // Add extracted qualifiers
1290
+ qualifiers.forEach(qualifier => {
1291
+ args.push(qualifier);
1292
+ });
1293
+ // Add explicit parameters as qualifiers
1294
+ if (params.language && !params.query.includes('language:')) {
1295
+ args.push(`language:${params.language}`);
1296
+ }
1297
+ if (params.owner &&
1298
+ !params.query.includes('org:') &&
1299
+ !params.query.includes('user:')) {
1300
+ const owners = Array.isArray(params.owner) ? params.owner : [params.owner];
1301
+ owners.forEach(owner => args.push(`org:${owner}`));
1302
+ }
1303
+ if (params.filename && !params.query.includes('filename:')) {
1304
+ args.push(`filename:${params.filename}`);
1305
+ }
1306
+ if (params.extension && !params.query.includes('extension:')) {
1307
+ args.push(`extension:${params.extension}`);
1308
+ }
1309
+ if (params.size && !params.query.includes('size:')) {
1310
+ args.push(`size:${params.size}`);
1311
+ }
1312
+ // Handle match parameter
1313
+ if (params.match) {
1314
+ const matches = Array.isArray(params.match) ? params.match : [params.match];
1315
+ args.push(`in:${matches.join(',')}`);
1316
+ }
1317
+ // Add limit
1318
+ if (params.limit) {
1319
+ args.push(`--limit=${params.limit}`);
1320
+ }
1321
+ // Add JSON output format
1322
+ args.push('--json=repository,path,textMatches,sha,url');
1323
+ return args;
1324
+ }
1325
+ async function searchGitHubCode(params) {
1326
+ const cacheKey = generateCacheKey('gh-code', params);
1696
1327
  return withCache(cacheKey, async () => {
1697
1328
  try {
1698
- const { command, args } = buildGitHubReposSearchCommand(params);
1699
- const result = await executeGitHubCommand(command, args, {
1329
+ const args = buildGitHubCliArgs(params);
1330
+ const result = await executeGitHubCommand('search', args, {
1700
1331
  cache: false,
1701
1332
  });
1702
- if (result.isError) {
1703
- return result;
1704
- }
1705
- // Extract the actual content from the exec result
1706
- const execResult = JSON.parse(result.content[0].text);
1707
- const rawContent = execResult.result;
1708
- // Parse JSON results and provide structured analysis
1709
- let repositories = [];
1710
- const analysis = {
1711
- totalFound: 0,
1712
- languages: new Set(),
1713
- avgStars: 0,
1714
- recentlyUpdated: 0,
1715
- topStarred: [],
1716
- };
1717
- // Parse JSON response from GitHub CLI
1718
- repositories = JSON.parse(rawContent);
1719
- if (Array.isArray(repositories) && repositories.length > 0) {
1720
- analysis.totalFound = repositories.length;
1721
- // Analyze repository data
1722
- let totalStars = 0;
1723
- const now = new Date();
1724
- const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
1725
- repositories.forEach(repo => {
1726
- // Collect languages
1727
- if (repo.language) {
1728
- analysis.languages.add(repo.language);
1729
- }
1730
- // Calculate average stars (use correct field name)
1731
- if (repo.stargazersCount) {
1732
- totalStars += repo.stargazersCount;
1733
- }
1734
- // Count recently updated repositories (use correct field name)
1735
- if (repo.updatedAt) {
1736
- const updatedDate = new Date(repo.updatedAt);
1737
- if (updatedDate > thirtyDaysAgo) {
1738
- analysis.recentlyUpdated++;
1739
- }
1740
- }
1741
- });
1742
- analysis.avgStars =
1743
- repositories.length > 0
1744
- ? Math.round(totalStars / repositories.length)
1745
- : 0;
1746
- // Get all repositories with comprehensive data
1747
- analysis.topStarred = repositories.map(repo => ({
1748
- name: repo.fullName || repo.name,
1749
- stars: repo.stargazersCount || 0,
1750
- description: repo.description || 'No description',
1751
- language: repo.language || 'Unknown',
1752
- url: repo.url,
1753
- forks: repo.forksCount || 0,
1754
- isPrivate: repo.isPrivate || false,
1755
- isArchived: repo.isArchived || false,
1756
- isFork: repo.isFork || false,
1757
- topics: [], // GitHub CLI search repos doesn't provide topics in JSON output
1758
- license: repo.license?.name || null,
1759
- hasIssues: repo.hasIssues || false,
1760
- openIssuesCount: repo.openIssuesCount || 0,
1761
- createdAt: toDDMMYYYY(repo.createdAt),
1762
- updatedAt: toDDMMYYYY(repo.updatedAt),
1763
- visibility: repo.visibility || 'public',
1764
- owner: repo.owner?.login || repo.owner,
1765
- }));
1766
- }
1767
- return createResult({
1768
- data: {
1769
- total_count: analysis.totalFound,
1770
- ...(analysis.totalFound > 0
1771
- ? {
1772
- repositories: analysis.topStarred,
1773
- summary: {
1774
- languages: Array.from(analysis.languages).slice(0, 10),
1775
- avgStars: analysis.avgStars,
1776
- recentlyUpdated: analysis.recentlyUpdated,
1777
- },
1778
- }
1779
- : {
1780
- repositories: [],
1781
- cli_command: execResult.command, // Only on no results
1782
- }),
1783
- },
1784
- });
1333
+ return result;
1785
1334
  }
1786
1335
  catch (error) {
1787
- return createResult({
1788
- error: 'Repository search failed - verify connection or simplify query',
1789
- });
1336
+ const errorMessage = error.message || '';
1337
+ return handleSearchError(errorMessage); // Delegating error handling
1790
1338
  }
1791
1339
  });
1792
1340
  }
1793
- function buildGitHubReposSearchCommand(params) {
1794
- // Build query following GitHub CLI patterns
1795
- const query = params.query?.trim() || '';
1796
- const args = ['repos'];
1797
- // Detect embedded qualifiers in query to avoid conflicts and optimize
1798
- const hasEmbeddedQualifiers = query &&
1799
- /\b(stars|language|org|repo|topic|user|created|updated|size|license|archived|fork|good-first-issues|help-wanted-issues):/i.test(query);
1800
- // Handle exact string search - preserve quotes and special characters
1801
- if (query) {
1802
- // For exact repository name searches (quoted strings), preserve the quotes
1803
- // For special characters, pass them through - GitHub CLI handles escaping
1804
- // Support searches like "microsoft/vscode", "@types/node", etc.
1805
- args.push(query);
1341
+ /**
1342
+ * Enhanced validation with helpful suggestions
1343
+ */
1344
+ function validateSearchParameters(params) {
1345
+ // Query validation
1346
+ if (!params.query.trim()) {
1347
+ return ERROR_MESSAGES.EMPTY_QUERY;
1806
1348
  }
1807
- // Add JSON output with specific fields for structured data parsing
1808
- // Note: 'topics' field is not available in GitHub CLI search repos JSON output
1809
- args.push('--json=name,fullName,description,language,stargazersCount,forksCount,updatedAt,createdAt,url,owner,isPrivate,license,hasIssues,openIssuesCount,isArchived,isFork,visibility');
1810
- // SMART FILTER HANDLING - Avoid conflicts with embedded qualifiers
1811
- // Only add CLI flags if not already specified in query to prevent conflicts
1812
- // CORE FILTERS - Handle arrays properly
1813
- if (params.owner && !hasEmbeddedQualifiers) {
1814
- // GitHub CLI supports multiple owners with comma separation: --owner=owner1,owner2
1815
- const owners = Array.isArray(params.owner) ? params.owner : [params.owner];
1816
- args.push(`--owner=${owners.join(',')}`);
1817
- }
1818
- if (params.language && !hasEmbeddedQualifiers)
1819
- args.push(`--language=${params.language}`);
1820
- if (params.forks !== undefined && !hasEmbeddedQualifiers)
1821
- args.push(`--forks=${params.forks}`);
1822
- // Handle topic as string or array - GitHub CLI expects comma-separated topics
1823
- if (params.topic && !hasEmbeddedQualifiers) {
1824
- const topics = Array.isArray(params.topic) ? params.topic : [params.topic];
1825
- args.push(`--topic=${topics.join(',')}`);
1826
- }
1827
- if (params.numberOfTopics !== undefined)
1828
- args.push(`--number-topics=${params.numberOfTopics}`);
1829
- // Handle stars as number or string - avoid conflicts with embedded qualifiers
1830
- if (params.stars !== undefined && !hasEmbeddedQualifiers) {
1831
- const starsValue = typeof params.stars === 'number'
1832
- ? params.stars.toString()
1833
- : params.stars.trim();
1834
- // Validate numeric patterns for string values
1835
- if (typeof params.stars === 'number' ||
1836
- /^(\d+|>\d+|<\d+|\d+\.\.\d+|>=\d+|<=\d+)$/.test(starsValue)) {
1837
- args.push(`--stars=${starsValue}`);
1349
+ if (params.query.length > 1000) {
1350
+ return ERROR_MESSAGES.QUERY_TOO_LONG_1000;
1351
+ }
1352
+ // Repository validation - ensure owner is provided correctly
1353
+ if (params.owner &&
1354
+ typeof params.owner === 'string' &&
1355
+ params.owner.includes('/')) {
1356
+ return 'Owner parameter should contain only the username/org name, not owner/repo format. For repository-specific searches, use org: or user: qualifiers in the query.';
1357
+ }
1358
+ if (Array.isArray(params.owner)) {
1359
+ const hasSlashFormat = params.owner.some(owner => owner.includes('/'));
1360
+ if (hasSlashFormat) {
1361
+ return 'Owner parameter should contain only usernames/org names, not owner/repo format. For repository-specific searches, use repo: qualifier in the query.';
1838
1362
  }
1839
1363
  }
1840
- // QUALITY & STATE FILTERS
1841
- if (params.archived !== undefined)
1842
- args.push(`--archived=${params.archived}`);
1843
- if (params.includeForks)
1844
- args.push(`--include-forks=${params.includeForks}`);
1845
- if (params.visibility)
1846
- args.push(`--visibility=${params.visibility}`);
1847
- // Handle license as string or array
1848
- if (params.license) {
1849
- const licenses = Array.isArray(params.license)
1850
- ? params.license
1851
- : [params.license];
1852
- args.push(`--license=${licenses.join(',')}`);
1364
+ // Add validation for file size limit
1365
+ if (params.size) {
1366
+ if (!/^([<>]\d+|\d+\.\.\d+)$/.test(params.size)) {
1367
+ return ERROR_MESSAGES.INVALID_SIZE_FORMAT;
1368
+ }
1853
1369
  }
1854
- // DATE & SIZE FILTERS
1855
- if (params.created)
1856
- args.push(`--created=${params.created}`);
1857
- if (params.updated)
1858
- args.push(`--updated=${params.updated}`);
1859
- if (params.size)
1860
- args.push(`--size=${params.size}`);
1861
- // COMMUNITY FILTERS - handle both number and string
1862
- if (params.goodFirstIssues) {
1863
- const value = typeof params.goodFirstIssues === 'number'
1864
- ? params.goodFirstIssues.toString()
1865
- : params.goodFirstIssues;
1866
- args.push(`--good-first-issues=${value}`);
1867
- }
1868
- if (params.helpWantedIssues) {
1869
- const value = typeof params.helpWantedIssues === 'number'
1870
- ? params.helpWantedIssues.toString()
1871
- : params.helpWantedIssues;
1872
- args.push(`--help-wanted-issues=${value}`);
1873
- }
1874
- if (params.followers !== undefined)
1875
- args.push(`--followers=${params.followers}`);
1876
- // SEARCH SCOPE
1877
- if (params.match)
1878
- args.push(`--match=${params.match}`);
1879
- // SORTING AND LIMITS
1880
- if (params.limit)
1881
- args.push(`--limit=${params.limit}`);
1882
- if (params.order)
1883
- args.push(`--order=${params.order}`);
1884
- // Use best-match as default, only specify sort if different from default
1885
- const sortBy = params.sort || 'best-match';
1886
- if (sortBy !== 'best-match') {
1887
- args.push(`--sort=${sortBy}`);
1370
+ // Validate search scope
1371
+ if (params.match) {
1372
+ const validScopes = ['file', 'path'];
1373
+ const scopes = Array.isArray(params.match) ? params.match : [params.match];
1374
+ if (!scopes.every(scope => validScopes.includes(scope))) {
1375
+ return ERROR_MESSAGES.INVALID_SEARCH_SCOPE;
1376
+ }
1888
1377
  }
1889
- return { command: 'search', args };
1378
+ // Note about repository limitations (This is a note, not a hard error)
1379
+ // This return statement was returning null before, so it shouldn't be an issue
1380
+ // if (params.repo || params.owner) {
1381
+ // return null; // Return warning about repository limitations
1382
+ // }
1383
+ return null; // No validation errors
1890
1384
  }
1891
1385
 
1892
- const TOOL_NAME$5 = 'github_search_commits';
1893
- const DESCRIPTION$5 = `Search commit history effectively with GitHub's commit search. Use simple, specific terms for best results. Complex boolean queries may return limited results - try individual keywords instead.`;
1386
+ const GITHUB_SEARCH_COMMITS_TOOL_NAME = 'githubSearchCommits';
1387
+ const DESCRIPTION$5 = `Search commit history across GitHub repositories. Supports filtering by repository, author, date ranges, and commit attributes. Parameters: query (optional), owner (optional - GitHub username/org, NOT owner/repo), repo (optional - repository name, use with owner for specific repo), author (optional), authorName (optional), authorEmail (optional), committer (optional), committerName (optional), committerEmail (optional), authorDate (optional), committerDate (optional), hash (optional), parent (optional), tree (optional), merge (optional), visibility (optional), limit (optional), sort (optional), order (optional).`;
1894
1388
  function registerGitHubSearchCommitsTool(server) {
1895
- server.registerTool(TOOL_NAME$5, {
1389
+ server.registerTool(GITHUB_SEARCH_COMMITS_TOOL_NAME, {
1896
1390
  description: DESCRIPTION$5,
1897
1391
  inputSchema: {
1898
1392
  query: z
1899
1393
  .string()
1900
1394
  .optional()
1901
- .describe('Search query with boolean logic. Boolean: "fix AND bug", exact phrases: "initial commit", advanced syntax: "author:john OR committer:jane".'),
1395
+ .describe('Search terms. Start simple: "bug fix", "refactor". Use quotes for exact phrases.'),
1902
1396
  // Repository filters
1903
1397
  owner: z
1904
1398
  .string()
1905
1399
  .optional()
1906
- .describe('Repository owner/organization. Leave empty for global search.'),
1400
+ .describe('Repository owner/org name only (e.g., "microsoft", "google", NOT "microsoft/vscode"). Use with repo parameter for repository-specific searches.'),
1907
1401
  repo: z
1908
1402
  .string()
1909
1403
  .optional()
1910
- .describe('Repository name. Do exploratory search without repo filter first'),
1404
+ .describe('Repository name only (e.g., "vscode", "react", NOT "owner/repo"). Must be used together with owner parameter.'),
1911
1405
  // Author filters
1912
- author: z.string().optional().describe('Filter by commit author'),
1913
- authorName: z.string().optional().describe('Filter by author name'),
1914
- authorEmail: z.string().optional().describe('Filter by author email'),
1915
- // Committer filters
1916
- committer: z.string().optional().describe('Filter by committer'),
1917
- committerName: z
1406
+ author: z
1407
+ .string()
1408
+ .optional()
1409
+ .describe('GitHub username of commit author'),
1410
+ authorName: z
1918
1411
  .string()
1919
1412
  .optional()
1920
- .describe('Filter by committer name'),
1921
- committerEmail: z
1413
+ .describe('Full name of commit author'),
1414
+ authorEmail: z.string().optional().describe('Email of commit author'),
1415
+ // Committer filters
1416
+ committer: z
1922
1417
  .string()
1923
1418
  .optional()
1924
- .describe('Filter by committer email'),
1419
+ .describe('GitHub username of committer'),
1420
+ committerName: z.string().optional().describe('Full name of committer'),
1421
+ committerEmail: z.string().optional().describe('Email of committer'),
1925
1422
  // Date filters
1926
1423
  authorDate: z
1927
1424
  .string()
1928
1425
  .optional()
1929
- .describe('Filter by authored date (format: >2020-01-01, <2023-12-31)'),
1426
+ .describe('When authored. Format: >2020-01-01, <2023-12-31, 2020-01-01..2023-12-31'),
1930
1427
  committerDate: z
1931
1428
  .string()
1932
1429
  .optional()
1933
- .describe('Filter by committed date (format: >2020-01-01, <2023-12-31)'),
1430
+ .describe('When committed. Format: >2020-01-01, <2023-12-31, 2020-01-01..2023-12-31'),
1934
1431
  // Hash filters
1935
- hash: z.string().optional().describe('Filter by commit hash'),
1936
- parent: z.string().optional().describe('Filter by parent hash'),
1937
- tree: z.string().optional().describe('Filter by tree hash'),
1432
+ hash: z.string().optional().describe('Commit SHA (full or partial)'),
1433
+ parent: z.string().optional().describe('Parent commit SHA'),
1434
+ tree: z.string().optional().describe('Tree SHA'),
1938
1435
  // State filters
1939
- merge: z.boolean().optional().describe('Filter merge commits'),
1436
+ merge: z
1437
+ .boolean()
1438
+ .optional()
1439
+ .describe('Only merge commits (true) or exclude them (false)'),
1940
1440
  // Visibility
1941
1441
  visibility: z
1942
1442
  .enum(['public', 'private', 'internal'])
1943
1443
  .optional()
1944
- .describe('Filter by repository visibility'),
1444
+ .describe('Repository visibility filter'),
1945
1445
  // Pagination and sorting
1946
1446
  limit: z
1947
1447
  .number()
@@ -1950,19 +1450,19 @@ function registerGitHubSearchCommitsTool(server) {
1950
1450
  .max(50)
1951
1451
  .optional()
1952
1452
  .default(25)
1953
- .describe('Maximum results (default: 25, max: 50)'),
1453
+ .describe('Results limit (1-50). Default: 25'),
1954
1454
  sort: z
1955
1455
  .enum(['author-date', 'committer-date'])
1956
1456
  .optional()
1957
- .describe('Sort criteria (default: relevance)'),
1457
+ .describe('Sort by date. Default: best match'),
1958
1458
  order: z
1959
1459
  .enum(['asc', 'desc'])
1960
1460
  .optional()
1961
1461
  .default('desc')
1962
- .describe('Order (default: desc)'),
1462
+ .describe('Sort order. Default: desc'),
1963
1463
  },
1964
1464
  annotations: {
1965
- title: 'GitHub Commit Search',
1465
+ title: 'GitHub Commit Search - Smart History Analysis',
1966
1466
  readOnlyHint: true,
1967
1467
  destructiveHint: false,
1968
1468
  idempotentHint: true,
@@ -1975,28 +1475,13 @@ function registerGitHubSearchCommitsTool(server) {
1975
1475
  return result;
1976
1476
  }
1977
1477
  const execResult = JSON.parse(result.content[0].text);
1978
- const commits = JSON.parse(execResult.result);
1478
+ const commits = execResult.result;
1979
1479
  // GitHub CLI returns a direct array
1980
1480
  const items = Array.isArray(commits) ? commits : [];
1981
- // Enhanced handling for no results - provide fallback suggestions
1481
+ // Smart handling for no results - provide actionable suggestions
1982
1482
  if (items.length === 0) {
1983
1483
  return createResult({
1984
- data: {
1985
- commits: [],
1986
- total_count: 0,
1987
- cli_command: execResult.command,
1988
- suggestions: {
1989
- message: 'No commits found. GitHub commit search is limited compared to code/issue search.',
1990
- fallback_strategies: [
1991
- 'Try simpler, shorter queries (single keywords work better)',
1992
- "Use broader terms like 'fix' instead of 'fix useState bug'",
1993
- 'Search by author: add author filter for specific contributors',
1994
- 'Use date ranges: add authorDate or committerDate filters',
1995
- 'Try github_search_code tool for finding code patterns instead',
1996
- ],
1997
- alternative_queries: generateCommitSearchAlternatives(args.query),
1998
- },
1999
- },
1484
+ error: createNoResultsError('commits'),
2000
1485
  });
2001
1486
  }
2002
1487
  // Transform to optimized format
@@ -2007,21 +1492,16 @@ function registerGitHubSearchCommitsTool(server) {
2007
1492
  const errorMessage = error.message || '';
2008
1493
  if (errorMessage.includes('authentication')) {
2009
1494
  return createResult({
2010
- error: 'GitHub authentication required - run api_status_check tool',
1495
+ error: createAuthenticationError(),
2011
1496
  });
2012
1497
  }
2013
1498
  if (errorMessage.includes('rate limit')) {
2014
1499
  return createResult({
2015
- error: 'GitHub rate limit exceeded - try more specific filters',
1500
+ error: createRateLimitError(false),
2016
1501
  });
2017
1502
  }
2018
1503
  return createResult({
2019
- error: 'Commit search failed',
2020
- suggestions: [
2021
- 'Check authentication with api_status_check',
2022
- 'Use more specific date ranges or author filters',
2023
- 'Try simpler boolean queries',
2024
- ],
1504
+ error: createSearchFailedError('commits'),
2025
1505
  });
2026
1506
  }
2027
1507
  });
@@ -2032,14 +1512,12 @@ function registerGitHubSearchCommitsTool(server) {
2032
1512
  function transformCommitsToOptimizedFormat(items, _params) {
2033
1513
  // Extract repository info if single repo search
2034
1514
  const singleRepo = extractSingleRepository(items);
2035
- // Get unique authors for metadata
2036
- const uniqueAuthors = new Set(items.map(item => item.commit?.author?.name || item.author?.login || 'Unknown')).size;
2037
1515
  const optimizedCommits = items
2038
1516
  .map(item => ({
2039
1517
  sha: item.sha,
2040
- message: getCommitTitle(item.commit?.message || ''),
2041
- author: item.commit?.author?.name || item.author?.login || 'Unknown',
2042
- date: toDDMMYYYY(item.commit?.author?.date || ''),
1518
+ message: getCommitTitle(item.commit?.message ?? ''),
1519
+ author: item.commit?.author?.name ?? item.author?.login ?? 'Unknown',
1520
+ date: toDDMMYYYY(item.commit?.author?.date ?? ''),
2043
1521
  repository: singleRepo
2044
1522
  ? undefined
2045
1523
  : simplifyRepoUrl(item.repository?.url || ''),
@@ -2068,13 +1546,6 @@ function transformCommitsToOptimizedFormat(items, _params) {
2068
1546
  description: singleRepo.description,
2069
1547
  };
2070
1548
  }
2071
- // Add metadata for insights
2072
- if (items.length > 1) {
2073
- result.metadata = {
2074
- timeframe: getTimeframe(items),
2075
- unique_authors: uniqueAuthors,
2076
- };
2077
- }
2078
1549
  return result;
2079
1550
  }
2080
1551
  /**
@@ -2087,58 +1558,6 @@ function extractSingleRepository(items) {
2087
1558
  const allSameRepo = items.every(item => item.repository.fullName === firstRepo.fullName);
2088
1559
  return allSameRepo ? firstRepo : null;
2089
1560
  }
2090
- /**
2091
- * Calculate timeframe of commits
2092
- */
2093
- function getTimeframe(items) {
2094
- if (items.length === 0)
2095
- return '';
2096
- const dates = items.map(item => new Date(item.commit?.author?.date || ''));
2097
- const oldest = new Date(Math.min(...dates.map(d => d.getTime())));
2098
- const newest = new Date(Math.max(...dates.map(d => d.getTime())));
2099
- const diffMs = newest.getTime() - oldest.getTime();
2100
- const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
2101
- if (diffDays === 0)
2102
- return 'same day';
2103
- if (diffDays < 7)
2104
- return `${diffDays} days`;
2105
- if (diffDays < 30)
2106
- return `${Math.floor(diffDays / 7)} weeks`;
2107
- if (diffDays < 365)
2108
- return `${Math.floor(diffDays / 30)} months`;
2109
- return `${Math.floor(diffDays / 365)} years`;
2110
- }
2111
- /**
2112
- * Generate alternative commit search queries when original query fails
2113
- */
2114
- function generateCommitSearchAlternatives(originalQuery) {
2115
- if (!originalQuery) {
2116
- return [
2117
- { query: 'fix', reason: 'Search for general fixes' },
2118
- { query: 'bug', reason: 'Search for bug-related commits' },
2119
- { query: 'refactor', reason: 'Search for refactoring commits' },
2120
- ];
2121
- }
2122
- const alternatives = [];
2123
- const query = originalQuery.toLowerCase();
2124
- // Extract key terms and create simpler alternatives
2125
- if (query.includes('fix') && query.includes('bug')) {
2126
- alternatives.push({ query: 'fix', reason: 'Broader search for all fixes' }, { query: 'bug', reason: 'Search for bug-related commits' });
2127
- }
2128
- else if (query.includes(' ')) {
2129
- // Multi-word query - suggest individual terms
2130
- const words = query.split(' ').filter(w => w.length > 2);
2131
- words.slice(0, 2).forEach(word => {
2132
- alternatives.push({
2133
- query: word,
2134
- reason: `Single keyword search for '${word}'`,
2135
- });
2136
- });
2137
- }
2138
- // Always suggest some common commit patterns
2139
- alternatives.push({ query: 'feat', reason: 'Search for feature commits' }, { query: 'docs', reason: 'Search for documentation updates' });
2140
- return alternatives.slice(0, 4); // Limit to 4 suggestions
2141
- }
2142
1561
  async function searchGitHubCommits(params) {
2143
1562
  const cacheKey = generateCacheKey('gh-commits', params);
2144
1563
  return withCache(cacheKey, async () => {
@@ -2153,16 +1572,16 @@ async function searchGitHubCommits(params) {
2153
1572
  const errorMessage = error.message || '';
2154
1573
  if (errorMessage.includes('authentication')) {
2155
1574
  return createResult({
2156
- error: 'GitHub authentication required',
1575
+ error: createAuthenticationError(),
2157
1576
  });
2158
1577
  }
2159
1578
  if (errorMessage.includes('rate limit')) {
2160
1579
  return createResult({
2161
- error: 'GitHub rate limit exceeded',
1580
+ error: createRateLimitError(false),
2162
1581
  });
2163
1582
  }
2164
1583
  return createResult({
2165
- error: 'Commit search execution failed',
1584
+ error: createSearchFailedError('commits'),
2166
1585
  });
2167
1586
  }
2168
1587
  });
@@ -2225,85 +1644,133 @@ function buildGitHubCommitCliArgs(params) {
2225
1644
  return args;
2226
1645
  }
2227
1646
 
2228
- // TODO: add PR commeents. e.g, gh pr view <PR_NUMBER_OR_URL_OR_BRANCH> --comments
2229
- const TOOL_NAME$4 = 'github_search_pull_requests';
2230
- const DESCRIPTION$4 = `Find pull requests and implementations with detailed metadata. Discover feature implementations, code review patterns, and development workflows.`;
2231
- function registerSearchGitHubPullRequestsTool(server) {
2232
- server.registerTool(TOOL_NAME$4, {
1647
+ const GITHUB_SEARCH_ISSUES_TOOL_NAME = 'githubSearchIssues';
1648
+ const DESCRIPTION$4 = `Search GitHub issues for bug discovery and feature analysis. Supports filtering by state, labels, assignee, dates, and more. Parameters: query (required), owner (optional - GitHub username/org, NOT owner/repo), repo (optional - repository name, use with owner for specific repo), app (optional), archived (optional), assignee (optional), author (optional), closed (optional), commenter (optional), comments (optional), created (optional), includePrs (optional), interactions (optional), involves (optional), labels (optional), language (optional), locked (optional), match (optional), mentions (optional), milestone (optional), noAssignee (optional), noLabel (optional), noMilestone (optional), noProject (optional), project (optional), reactions (optional), state (optional), teamMentions (optional), updated (optional), visibility (optional), sort (optional), order (optional), limit (optional).`;
1649
+ function registerSearchGitHubIssuesTool(server) {
1650
+ server.registerTool(GITHUB_SEARCH_ISSUES_TOOL_NAME, {
2233
1651
  description: DESCRIPTION$4,
2234
1652
  inputSchema: {
2235
1653
  query: z
2236
1654
  .string()
2237
1655
  .min(1, 'Search query is required and cannot be empty')
2238
- .describe('Search query with GitHub syntax. Boolean: "fix AND bug", exact phrases: "initial commit", qualifiers: "is:merged review:approved".'),
2239
- owner: z.string().optional().describe('Repository owner/organization'),
2240
- repo: z.string().optional().describe('Repository name'),
2241
- author: z.string().optional().describe('Filter by pull request author'),
2242
- assignee: z.string().optional().describe('Filter by assignee'),
2243
- mentions: z.string().optional().describe('Filter by user mentions'),
2244
- commenter: z.string().optional().describe('Filter by comments by user'),
2245
- involves: z.string().optional().describe('Filter by user involvement'),
2246
- reviewedBy: z
1656
+ .describe('Search terms. Start simple: "error", "crash". Use quotes for exact phrases.'),
1657
+ owner: z
2247
1658
  .string()
1659
+ .min(1)
2248
1660
  .optional()
2249
- .describe('Filter by user who reviewed'),
2250
- reviewRequested: z
1661
+ .describe('Repository owner/org name only (e.g., "microsoft", "google", NOT "microsoft/vscode"). Use with repo parameter for repository-specific searches.'),
1662
+ repo: z
2251
1663
  .string()
2252
1664
  .optional()
2253
- .describe('Filter by user or team requested to review'),
2254
- state: z
2255
- .enum(['open', 'closed'])
1665
+ .describe('Repository name only (e.g., "vscode", "react", NOT "owner/repo"). Must be used together with owner parameter.'),
1666
+ app: z
1667
+ .string()
2256
1668
  .optional()
2257
- .describe('Filter by state'),
2258
- head: z.string().optional().describe('Filter by head branch name'),
2259
- base: z.string().optional().describe('Filter by base branch name'),
2260
- language: z.string().optional().describe('Filter by coding language'),
2261
- created: z.string().optional().describe('Filter by created date'),
2262
- updated: z.string().optional().describe('Filter by last updated date'),
2263
- mergedAt: z.string().optional().describe('Filter by merged date'),
2264
- closed: z.string().optional().describe('Filter by closed date'),
2265
- draft: z.boolean().optional().describe('Filter by draft state'),
2266
- checks: z
2267
- .enum(['pending', 'success', 'failure'])
1669
+ .describe('GitHub App that created the issue'),
1670
+ archived: z
1671
+ .boolean()
2268
1672
  .optional()
2269
- .describe('Filter based on status of the checks'),
2270
- merged: z.boolean().optional().describe('Filter based on merged state'),
2271
- review: z
2272
- .enum(['none', 'required', 'approved', 'changes_requested'])
1673
+ .describe('Include archived repositories'),
1674
+ assignee: z.string().optional().describe('GitHub username of assignee'),
1675
+ author: z
1676
+ .string()
2273
1677
  .optional()
2274
- .describe('Filter based on review status'),
2275
- limit: z
1678
+ .describe('GitHub username of issue creator'),
1679
+ closed: z
1680
+ .string()
1681
+ .optional()
1682
+ .describe('When closed. Format: >2020-01-01'),
1683
+ commenter: z
1684
+ .string()
1685
+ .optional()
1686
+ .describe('User who commented on issue'),
1687
+ comments: z
2276
1688
  .number()
2277
- .int()
2278
- .min(1)
2279
- .max(50)
2280
1689
  .optional()
2281
- .default(25)
2282
- .describe('Maximum results (default: 25, max: 50)'),
1690
+ .describe('Comment count. Format: >10, <5, 5..10'),
1691
+ created: z
1692
+ .string()
1693
+ .optional()
1694
+ .describe('When created. Format: >2020-01-01'),
1695
+ includePrs: z
1696
+ .boolean()
1697
+ .optional()
1698
+ .describe('Include pull requests. Default: false'),
1699
+ interactions: z
1700
+ .number()
1701
+ .optional()
1702
+ .describe('Total interactions (reactions + comments)'),
1703
+ involves: z.string().optional().describe('User involved in any way'),
1704
+ labels: z
1705
+ .string()
1706
+ .optional()
1707
+ .describe('Label names (bug, feature, etc.)'),
1708
+ language: z.string().optional().describe('Repository language'),
1709
+ locked: z.boolean().optional().describe('Conversation locked status'),
1710
+ match: z
1711
+ .enum(['title', 'body', 'comments'])
1712
+ .optional()
1713
+ .describe('Search scope. Default: title and body'),
1714
+ mentions: z.string().optional().describe('Issues mentioning this user'),
1715
+ milestone: z.string().optional().describe('Milestone name'),
1716
+ noAssignee: z.boolean().optional().describe('Issues without assignee'),
1717
+ noLabel: z.boolean().optional().describe('Issues without labels'),
1718
+ noMilestone: z
1719
+ .boolean()
1720
+ .optional()
1721
+ .describe('Issues without milestone'),
1722
+ noProject: z.boolean().optional().describe('Issues not in projects'),
1723
+ project: z.string().optional().describe('Project board number'),
1724
+ reactions: z
1725
+ .number()
1726
+ .optional()
1727
+ .describe('Reaction count. Format: >10'),
1728
+ state: z
1729
+ .enum(['open', 'closed'])
1730
+ .optional()
1731
+ .describe('Issue state. Default: all'),
1732
+ teamMentions: z.string().optional().describe('Team mentioned in issue'),
1733
+ updated: z
1734
+ .string()
1735
+ .optional()
1736
+ .describe('When updated. Format: >2020-01-01'),
1737
+ visibility: z
1738
+ .enum(['public', 'private', 'internal'])
1739
+ .optional()
1740
+ .describe('Repository visibility'),
2283
1741
  sort: z
2284
1742
  .enum([
2285
1743
  'comments',
1744
+ 'created',
1745
+ 'interactions',
2286
1746
  'reactions',
2287
1747
  'reactions-+1',
2288
1748
  'reactions--1',
2289
- 'reactions-smile',
2290
- 'reactions-thinking_face',
2291
1749
  'reactions-heart',
1750
+ 'reactions-smile',
2292
1751
  'reactions-tada',
2293
- 'interactions',
2294
- 'created',
1752
+ 'reactions-thinking_face',
2295
1753
  'updated',
1754
+ 'best-match',
2296
1755
  ])
2297
1756
  .optional()
2298
- .describe('Sort criteria'),
1757
+ .describe('Sort by activity or reactions. Default: best match'),
2299
1758
  order: z
2300
1759
  .enum(['asc', 'desc'])
2301
1760
  .optional()
2302
1761
  .default('desc')
2303
- .describe('Order (default: desc)'),
1762
+ .describe('Sort order. Default: desc'),
1763
+ limit: z
1764
+ .number()
1765
+ .int()
1766
+ .min(1)
1767
+ .max(50)
1768
+ .optional()
1769
+ .default(25)
1770
+ .describe('Results limit (1-50). Default: 25'),
2304
1771
  },
2305
1772
  annotations: {
2306
- title: 'GitHub Pull Requests Search',
1773
+ title: 'GitHub Issues Search - Bug & Feature Discovery',
2307
1774
  readOnlyHint: true,
2308
1775
  destructiveHint: false,
2309
1776
  idempotentHint: true,
@@ -2312,750 +1779,912 @@ function registerSearchGitHubPullRequestsTool(server) {
2312
1779
  }, async (args) => {
2313
1780
  if (!args.query?.trim()) {
2314
1781
  return createResult({
2315
- error: 'Search query is required and cannot be empty - provide keywords to search for pull requests',
1782
+ error: `${ERROR_MESSAGES.QUERY_REQUIRED} ${SUGGESTIONS.PROVIDE_KEYWORDS}`,
2316
1783
  });
2317
1784
  }
2318
1785
  if (args.query.length > 256) {
2319
1786
  return createResult({
2320
- error: 'Search query is too long. Please limit to 256 characters or less - simplify your search terms',
1787
+ error: ERROR_MESSAGES.QUERY_TOO_LONG,
2321
1788
  });
2322
1789
  }
2323
1790
  try {
2324
- return await searchGitHubPullRequests(args);
1791
+ return await searchGitHubIssues(args);
2325
1792
  }
2326
1793
  catch (error) {
1794
+ const errorMessage = error instanceof Error ? error.message : '';
1795
+ if (errorMessage.includes('authentication')) {
1796
+ return createResult({
1797
+ error: createAuthenticationError(),
1798
+ });
1799
+ }
1800
+ if (errorMessage.includes('rate limit')) {
1801
+ return createResult({
1802
+ error: createRateLimitError(false),
1803
+ });
1804
+ }
1805
+ // Generic fallback
2327
1806
  return createResult({
2328
- error: 'GitHub pull requests search failed - verify repository access and query syntax',
1807
+ error: createSearchFailedError('issues'),
2329
1808
  });
2330
1809
  }
2331
1810
  });
2332
1811
  }
2333
- async function searchGitHubPullRequests(params) {
2334
- const cacheKey = generateCacheKey('gh-prs', params);
1812
+ async function searchGitHubIssues(params) {
1813
+ const cacheKey = generateCacheKey('gh-issues', params);
2335
1814
  return withCache(cacheKey, async () => {
2336
- const { command, args } = buildGitHubPullRequestsAPICommand(params);
1815
+ const { command, args } = buildGitHubIssuesAPICommand(params);
2337
1816
  const result = await executeGitHubCommand(command, args, { cache: false });
2338
1817
  if (result.isError) {
2339
1818
  return result;
2340
1819
  }
2341
1820
  const execResult = JSON.parse(result.content[0].text);
2342
- const apiResponse = JSON.parse(execResult.result);
2343
- const pullRequests = apiResponse.items || [];
2344
- const cleanPRs = pullRequests.map((pr) => {
2345
- const result = {
2346
- number: pr.number,
2347
- title: pr.title,
2348
- state: pr.state,
2349
- author: pr.user?.login || '',
2350
- repository: pr.repository_url?.split('/').slice(-2).join('/') || 'unknown',
2351
- labels: pr.labels?.map(l => l.name) || [],
2352
- created_at: toDDMMYYYY(pr.created_at),
2353
- updated_at: toDDMMYYYY(pr.updated_at),
2354
- url: pr.html_url,
2355
- comments: pr.comments,
2356
- reactions: pr.reactions?.total_count || 0,
2357
- draft: pr.draft,
2358
- };
2359
- // Only include optional fields if they have values
2360
- if (pr.merged_at)
2361
- result.merged_at = pr.merged_at;
2362
- if (pr.closed_at)
2363
- result.closed_at = toDDMMYYYY(pr.closed_at);
2364
- if (pr.head?.ref)
2365
- result.head = pr.head.ref;
2366
- if (pr.base?.ref)
2367
- result.base = pr.base.ref;
2368
- return result;
2369
- });
1821
+ const apiResponse = execResult.result;
1822
+ const issues = apiResponse.items || [];
1823
+ const cleanIssues = issues.map((issue) => ({
1824
+ number: issue.number,
1825
+ title: issue.title,
1826
+ state: issue.state,
1827
+ author: issue.user?.login || '',
1828
+ repository: issue.repository_url?.split('/').slice(-2).join('/') || 'unknown',
1829
+ labels: issue.labels?.map(l => l.name) || [],
1830
+ created_at: toDDMMYYYY(issue.created_at),
1831
+ updated_at: toDDMMYYYY(issue.updated_at),
1832
+ url: issue.html_url,
1833
+ comments: issue.comments,
1834
+ reactions: issue.reactions?.total_count || 0,
1835
+ }));
2370
1836
  const searchResult = {
2371
- results: cleanPRs,
2372
- total_count: apiResponse.total_count || cleanPRs.length,
2373
- metadata: {
2374
- incomplete_results: apiResponse.incomplete_results || false,
2375
- },
1837
+ results: cleanIssues,
2376
1838
  };
2377
1839
  return createResult({ data: searchResult });
2378
1840
  });
2379
1841
  }
2380
- function buildGitHubPullRequestsAPICommand(params) {
2381
- const queryParts = [params.query?.trim() || ''];
2382
- // Repository/organization qualifiers
1842
+ function buildGitHubIssuesAPICommand(params) {
1843
+ const queryParts = [];
1844
+ // Start with the base query, but filter out qualifiers that will be added separately
1845
+ const baseQuery = params.query?.trim() || '';
1846
+ // Extract and remove qualifiers from the main query to avoid conflicts
1847
+ const qualifierPatterns = [
1848
+ /\bis:(open|closed)\b/gi,
1849
+ /\blabel:("[^"]*"|[^\s]+)/gi,
1850
+ /\bcreated:([^\s]+)/gi,
1851
+ /\bupdated:([^\s]+)/gi,
1852
+ /\bauthor:([^\s]+)/gi,
1853
+ /\bassignee:([^\s]+)/gi,
1854
+ /\bstate:(open|closed)/gi,
1855
+ /\brepo:([^\s]+)/gi,
1856
+ /\borg:([^\s]+)/gi,
1857
+ ];
1858
+ // Remove extracted qualifiers from base query
1859
+ let cleanQuery = baseQuery;
1860
+ qualifierPatterns.forEach(pattern => {
1861
+ cleanQuery = cleanQuery.replace(pattern, '').trim();
1862
+ });
1863
+ // Add the cleaned query if it has content
1864
+ if (cleanQuery) {
1865
+ queryParts.push(cleanQuery);
1866
+ }
1867
+ // Repository/organization qualifiers - prioritize function params over query
2383
1868
  if (params.owner && params.repo) {
2384
1869
  queryParts.push(`repo:${params.owner}/${params.repo}`);
2385
1870
  }
2386
1871
  else if (params.owner) {
2387
1872
  queryParts.push(`org:${params.owner}`);
2388
1873
  }
2389
- // Build search qualifiers from params
1874
+ // Build search qualifiers from function parameters (these take precedence)
2390
1875
  const qualifiers = {
2391
1876
  author: params.author,
2392
1877
  assignee: params.assignee,
2393
1878
  mentions: params.mentions,
2394
1879
  commenter: params.commenter,
2395
1880
  involves: params.involves,
1881
+ language: params.language,
2396
1882
  state: params.state,
2397
1883
  created: params.created,
2398
1884
  updated: params.updated,
2399
1885
  closed: params.closed,
2400
- language: params.language,
2401
1886
  };
2402
1887
  Object.entries(qualifiers).forEach(([key, value]) => {
2403
1888
  if (value)
2404
1889
  queryParts.push(`${key}:${value}`);
2405
1890
  });
2406
- // Special qualifiers
2407
- if (params.reviewedBy)
2408
- queryParts.push(`reviewed-by:${params.reviewedBy}`);
2409
- if (params.reviewRequested)
2410
- queryParts.push(`review-requested:${params.reviewRequested}`);
2411
- if (params.head)
2412
- queryParts.push(`head:${params.head}`);
2413
- if (params.base)
2414
- queryParts.push(`base:${params.base}`);
2415
- if (params.mergedAt)
2416
- queryParts.push(`merged:${params.mergedAt}`);
2417
- if (params.draft !== undefined)
2418
- queryParts.push(`draft:${params.draft}`);
2419
- if (params.checks)
2420
- queryParts.push(`status:${params.checks}`);
2421
- if (params.merged !== undefined)
2422
- queryParts.push(`is:${params.merged ? 'merged' : 'unmerged'}`);
2423
- if (params.review)
2424
- queryParts.push(`review:${params.review}`);
2425
- // Add type qualifier to search only pull requests
2426
- queryParts.push('type:pr');
2427
- const query = queryParts.filter(Boolean).join(' ');
2428
- const limit = Math.min(params.limit || 25, 100);
2429
- let apiPath = `search/issues?q=${encodeURIComponent(query)}&per_page=${limit}`;
2430
- if (params.sort)
2431
- apiPath += `&sort=${params.sort}`;
2432
- if (params.order)
2433
- apiPath += `&order=${params.order}`;
2434
- return { command: 'api', args: [apiPath] };
2435
- }
2436
-
2437
- const TOOL_NAME$3 = 'npm_package_search';
2438
- const DESCRIPTION$3 = `Search npm packages by keywords using fuzzy matching. Use space-separated keywords like "react hooks" or "cli typescript". No boolean operators supported.`;
2439
- const MAX_DESCRIPTION_LENGTH = 100;
2440
- const MAX_KEYWORDS = 10;
2441
- function registerNpmSearchTool(server) {
2442
- server.registerTool(TOOL_NAME$3, {
2443
- description: DESCRIPTION$3,
2444
- inputSchema: {
2445
- queries: z
2446
- .union([z.string(), z.array(z.string())])
2447
- .describe('Package names or keywords to search for. Use simple space-separated keywords like "react hooks" or "typescript cli" for fuzzy matching.'),
2448
- searchlimit: z
2449
- .number()
2450
- .int()
2451
- .min(1)
2452
- .max(50)
2453
- .optional()
2454
- .default(20)
2455
- .describe('Max results per query (default: 20, max: 50)'),
2456
- },
2457
- annotations: {
2458
- title: 'NPM Package Search',
2459
- readOnlyHint: true,
2460
- destructiveHint: false,
2461
- idempotentHint: true,
2462
- openWorldHint: true,
2463
- },
2464
- }, async (args) => {
2465
- try {
2466
- const queries = Array.isArray(args.queries)
2467
- ? args.queries
2468
- : [args.queries];
2469
- const searchLimit = args.searchlimit || 20;
2470
- const allPackages = [];
2471
- // Search for each query term
2472
- for (const query of queries) {
2473
- const result = await executeNpmCommand('search', [query, `--searchlimit=${searchLimit}`, '--json'], { cache: true });
2474
- if (!result.isError && result.content?.[0]?.text) {
2475
- const packages = parseNpmSearchOutput(result.content[0].text);
2476
- allPackages.push(...packages);
2477
- }
2478
- }
2479
- const deduplicatedPackages = deduplicatePackages(allPackages);
2480
- if (deduplicatedPackages.length > 0) {
2481
- return createResult({
2482
- data: {
2483
- total_count: deduplicatedPackages.length,
2484
- results: deduplicatedPackages,
2485
- },
2486
- });
2487
- }
2488
- return createResult({
2489
- error: 'No packages found',
2490
- cli_command: `npm search ${Array.isArray(args.queries) ? args.queries.join(' ') : args.queries}`,
2491
- });
2492
- }
2493
- catch (error) {
2494
- return createResult({
2495
- error: 'Package search failed - check terms or try different keywords',
2496
- cli_command: `npm search ${Array.isArray(args.queries) ? args.queries.join(' ') : args.queries}`,
2497
- });
2498
- }
2499
- });
2500
- }
2501
- function deduplicatePackages(packages) {
2502
- const seen = new Set();
2503
- return packages.filter(pkg => {
2504
- if (seen.has(pkg.name))
2505
- return false;
2506
- seen.add(pkg.name);
2507
- return true;
2508
- });
2509
- }
2510
- function normalizePackage(pkg) {
2511
- const description = pkg.description || null;
2512
- const truncatedDescription = description && description.length > MAX_DESCRIPTION_LENGTH
2513
- ? description.substring(0, MAX_DESCRIPTION_LENGTH) + '...'
2514
- : description;
2515
- const keywords = pkg.keywords || [];
2516
- const limitedKeywords = keywords.slice(0, MAX_KEYWORDS);
2517
- return {
2518
- name: pkg.name || '',
2519
- version: pkg.version || '',
2520
- description: truncatedDescription,
2521
- keywords: limitedKeywords,
2522
- repository: pkg.links?.repository || pkg.repository?.url || null,
2523
- };
2524
- }
2525
- function parseNpmSearchOutput(output) {
2526
- try {
2527
- const wrapper = JSON.parse(output);
2528
- const commandResult = typeof wrapper.result === 'string'
2529
- ? JSON.parse(wrapper.result)
2530
- : wrapper.result;
2531
- let packages = [];
2532
- // Handle different npm search output formats
2533
- if (Array.isArray(commandResult)) {
2534
- packages = commandResult;
2535
- }
2536
- else if (commandResult?.objects && Array.isArray(commandResult.objects)) {
2537
- packages = commandResult.objects.map((obj) => obj.package || obj);
1891
+ // Special qualifiers - handle labels carefully
1892
+ if (params.labels) {
1893
+ queryParts.push(`label:"${params.labels}"`);
1894
+ }
1895
+ if (params.milestone)
1896
+ queryParts.push(`milestone:"${params.milestone}"`);
1897
+ if (params.noAssignee)
1898
+ queryParts.push('no:assignee');
1899
+ if (params.noLabel)
1900
+ queryParts.push('no:label');
1901
+ if (params.noMilestone)
1902
+ queryParts.push('no:milestone');
1903
+ if (params.archived !== undefined)
1904
+ queryParts.push(`archived:${params.archived}`);
1905
+ if (params.locked)
1906
+ queryParts.push('is:locked');
1907
+ if (params.visibility)
1908
+ queryParts.push(`is:${params.visibility}`);
1909
+ // Extract qualifiers from original query and add them if not already set by params
1910
+ if (baseQuery.includes('is:') && !params.state) {
1911
+ const isMatch = baseQuery.match(/\bis:(open|closed)\b/i);
1912
+ if (isMatch && !queryParts.some(part => part.startsWith('state:'))) {
1913
+ queryParts.push(`state:${isMatch[1].toLowerCase()}`);
2538
1914
  }
2539
- else if (commandResult?.results && Array.isArray(commandResult.results)) {
2540
- packages = commandResult.results;
1915
+ }
1916
+ if (baseQuery.includes('label:') && !params.labels) {
1917
+ const labelMatch = baseQuery.match(/\blabel:("[^"]*"|[^\s]+)/i);
1918
+ if (labelMatch) {
1919
+ const labelValue = labelMatch[1].replace(/"/g, '');
1920
+ queryParts.push(`label:"${labelValue}"`);
2541
1921
  }
2542
- return packages.map(normalizePackage);
2543
1922
  }
2544
- catch {
2545
- return [];
1923
+ if (baseQuery.includes('created:') && !params.created) {
1924
+ const createdMatch = baseQuery.match(/\bcreated:([^\s]+)/i);
1925
+ if (createdMatch) {
1926
+ queryParts.push(`created:${createdMatch[1]}`);
1927
+ }
2546
1928
  }
1929
+ const query = queryParts.filter(Boolean).join(' ');
1930
+ const limit = Math.min(params.limit || 25, 100);
1931
+ let apiPath = `search/issues?q=${encodeURIComponent(query)}&per_page=${limit}`;
1932
+ if (params.sort)
1933
+ apiPath += `&sort=${params.sort}`;
1934
+ if (params.order)
1935
+ apiPath += `&order=${params.order}`;
1936
+ return { command: 'api', args: [apiPath] };
2547
1937
  }
2548
1938
 
2549
- const TOOL_NAME$2 = 'github_get_contents';
2550
- const DESCRIPTION$2 = `Browse repository structure and verify file existence. Smart branch detection with fallbacks. Use before fetching files to understand organization.`;
2551
- function registerViewRepositoryStructureTool(server) {
2552
- server.registerTool(TOOL_NAME$2, {
2553
- description: DESCRIPTION$2,
1939
+ // TODO: add PR commeents. e.g, gh pr view <PR_NUMBER_OR_URL_OR_BRANCH> --comments
1940
+ const GITHUB_SEARCH_PULL_REQUESTS_TOOL_NAME = 'githubSearchPullRequests';
1941
+ const DESCRIPTION$3 = `Search pull requests for implementation discovery and code review analysis. Supports filtering by state, review status, branches, and more. Parameters: query (required), owner (optional - GitHub username/org, NOT owner/repo), repo (optional - repository name, use with owner for specific repo), author (optional), assignee (optional), mentions (optional), commenter (optional), involves (optional), reviewedBy (optional), reviewRequested (optional), state (optional), head (optional), base (optional), language (optional), created (optional), updated (optional), mergedAt (optional), closed (optional), draft (optional), checks (optional), merged (optional), review (optional), limit (optional), sort (optional), order (optional).`;
1942
+ function registerSearchGitHubPullRequestsTool(server) {
1943
+ server.registerTool(GITHUB_SEARCH_PULL_REQUESTS_TOOL_NAME, {
1944
+ description: DESCRIPTION$3,
2554
1945
  inputSchema: {
1946
+ query: z
1947
+ .string()
1948
+ .min(1, 'Search query is required and cannot be empty')
1949
+ .describe('Search terms. Start simple: "refactor", "optimization". Use quotes for exact phrases.'),
2555
1950
  owner: z
2556
1951
  .string()
2557
- .min(1)
2558
- .max(100)
2559
- .regex(/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/, 'Invalid GitHub username/org format')
2560
- .describe(`Repository owner/organization.`),
1952
+ .optional()
1953
+ .describe('Repository owner/org name only (e.g., "microsoft", "google", NOT "microsoft/vscode"). Use with repo parameter for repository-specific searches.'),
2561
1954
  repo: z
2562
1955
  .string()
2563
- .min(1)
2564
- .max(100)
2565
- .regex(/^[a-zA-Z0-9._-]+$/, 'Invalid repository name format')
2566
- .describe('Repository name. Case-sensitive.'),
2567
- branch: z
1956
+ .optional()
1957
+ .describe('Repository name only (e.g., "vscode", "react", NOT "owner/repo"). Must be used together with owner parameter.'),
1958
+ author: z.string().optional().describe('GitHub username of PR author'),
1959
+ assignee: z.string().optional().describe('GitHub username of assignee'),
1960
+ mentions: z.string().optional().describe('PRs mentioning this user'),
1961
+ commenter: z.string().optional().describe('User who commented on PR'),
1962
+ involves: z.string().optional().describe('User involved in any way'),
1963
+ reviewedBy: z.string().optional().describe('User who reviewed the PR'),
1964
+ reviewRequested: z
2568
1965
  .string()
2569
- .min(1)
2570
- .max(255)
2571
- .regex(/^[^\s]+$/, 'Branch name cannot contain spaces')
2572
- .describe('Target branch name. Auto-detects default if not found.'),
2573
- path: z
1966
+ .optional()
1967
+ .describe('User/team requested for review'),
1968
+ state: z
1969
+ .enum(['open', 'closed'])
1970
+ .optional()
1971
+ .describe('PR state. Default: all'),
1972
+ head: z.string().optional().describe('Source branch name'),
1973
+ base: z
2574
1974
  .string()
2575
1975
  .optional()
2576
- .default('')
2577
- .refine(path => !path.includes('..'), 'Path traversal not allowed')
2578
- .refine(path => path.length <= 500, 'Path too long')
2579
- .describe('Directory path within repository. Leave empty for root.'),
1976
+ .describe('Target branch name (main, develop, etc.)'),
1977
+ language: z.string().optional().describe('Repository language'),
1978
+ created: z
1979
+ .string()
1980
+ .optional()
1981
+ .describe('When created. Format: >2020-01-01'),
1982
+ updated: z
1983
+ .string()
1984
+ .optional()
1985
+ .describe('When updated. Format: >2020-01-01'),
1986
+ mergedAt: z
1987
+ .string()
1988
+ .optional()
1989
+ .describe('When merged. Format: >2020-01-01'),
1990
+ closed: z
1991
+ .string()
1992
+ .optional()
1993
+ .describe('When closed. Format: >2020-01-01'),
1994
+ draft: z.boolean().optional().describe('Draft PR status'),
1995
+ checks: z
1996
+ .enum(['pending', 'success', 'failure'])
1997
+ .optional()
1998
+ .describe('CI/CD check status'),
1999
+ merged: z
2000
+ .boolean()
2001
+ .optional()
2002
+ .describe('Only merged PRs (true) or unmerged (false)'),
2003
+ review: z
2004
+ .enum(['none', 'required', 'approved', 'changes_requested'])
2005
+ .optional()
2006
+ .describe('Review status filter'),
2007
+ limit: z
2008
+ .number()
2009
+ .int()
2010
+ .min(1)
2011
+ .max(50)
2012
+ .optional()
2013
+ .default(25)
2014
+ .describe('Results limit (1-50). Default: 25'),
2015
+ sort: z
2016
+ .enum([
2017
+ 'comments',
2018
+ 'reactions',
2019
+ 'reactions-+1',
2020
+ 'reactions--1',
2021
+ 'reactions-smile',
2022
+ 'reactions-thinking_face',
2023
+ 'reactions-heart',
2024
+ 'reactions-tada',
2025
+ 'interactions',
2026
+ 'created',
2027
+ 'updated',
2028
+ ])
2029
+ .optional()
2030
+ .describe('Sort by activity or reactions. Default: best match'),
2031
+ order: z
2032
+ .enum(['asc', 'desc'])
2033
+ .optional()
2034
+ .default('desc')
2035
+ .describe('Sort order. Default: desc'),
2580
2036
  },
2581
2037
  annotations: {
2582
- title: 'GitHub Repository Contents',
2038
+ title: 'GitHub PR Search - Implementation Discovery',
2583
2039
  readOnlyHint: true,
2584
2040
  destructiveHint: false,
2585
2041
  idempotentHint: true,
2586
2042
  openWorldHint: true,
2587
2043
  },
2588
2044
  }, async (args) => {
2045
+ if (!args.query?.trim()) {
2046
+ return createResult({
2047
+ error: `${ERROR_MESSAGES.QUERY_REQUIRED} ${SUGGESTIONS.PROVIDE_PR_KEYWORDS}`,
2048
+ });
2049
+ }
2050
+ if (args.query.length > 256) {
2051
+ return createResult({
2052
+ error: ERROR_MESSAGES.QUERY_TOO_LONG,
2053
+ });
2054
+ }
2589
2055
  try {
2590
- const result = await viewRepositoryStructure(args);
2591
- return result;
2056
+ return await searchGitHubPullRequests(args);
2592
2057
  }
2593
2058
  catch (error) {
2594
- const errorMessage = error instanceof Error ? error.message : 'Unknown error';
2595
2059
  return createResult({
2596
- error: `Repository exploration failed: ${errorMessage} - verify access and permissions`,
2060
+ error: createSearchFailedError('pull_requests'),
2597
2061
  });
2598
2062
  }
2599
2063
  });
2600
2064
  }
2601
- /**
2602
- * Views the structure of a GitHub repository at a specific path.
2603
- *
2604
- * Features:
2605
- * - Smart branch detection: fetches repository default branch automatically
2606
- * - Intelligent fallback: tries requested -> default -> common branches
2607
- * - Input validation: prevents path traversal and validates GitHub naming
2608
- * - Clear error context: provides descriptive error messages
2609
- * - Efficient caching: avoids redundant API calls
2610
- * - Rich metadata: includes all GitHub API fields (sha, urls, links, etc.)
2611
- */
2612
- async function viewRepositoryStructure(params) {
2613
- const cacheKey = generateCacheKey('gh-repo-structure', params);
2065
+ async function searchGitHubPullRequests(params) {
2066
+ const cacheKey = generateCacheKey('gh-prs', params);
2614
2067
  return withCache(cacheKey, async () => {
2615
- const { owner, repo, branch, path = '' } = params;
2616
- try {
2617
- // Clean up path
2618
- const cleanPath = path.startsWith('/') ? path.substring(1) : path;
2619
- // Try the requested branch first, then fallback to main/master
2620
- const branchesToTry = await getSmartBranchFallback(owner, repo, branch);
2621
- let items = [];
2622
- let usedBranch = branch;
2623
- let lastError = null;
2624
- for (const tryBranch of branchesToTry) {
2625
- try {
2626
- const apiPath = `/repos/${owner}/${repo}/contents/${cleanPath}?ref=${tryBranch}`;
2627
- const result = await executeGitHubCommand('api', [apiPath], {
2628
- cache: false,
2629
- });
2630
- if (!result.isError) {
2631
- const execResult = JSON.parse(result.content[0].text);
2632
- const apiItems = JSON.parse(execResult.result);
2633
- items = Array.isArray(apiItems) ? apiItems : [apiItems];
2634
- usedBranch = tryBranch;
2635
- break;
2636
- }
2637
- else {
2638
- lastError = new Error(result.content[0].text);
2639
- }
2640
- }
2641
- catch (error) {
2642
- lastError = error instanceof Error ? error : new Error(String(error));
2643
- // Try next branch
2644
- continue;
2645
- }
2646
- }
2647
- if (items.length === 0) {
2648
- // Use the most descriptive error message
2649
- const errorMsg = lastError?.message || 'Unknown error';
2650
- if (errorMsg.includes('404') || errorMsg.includes('Not Found')) {
2651
- if (path) {
2652
- return createResult({
2653
- error: `Path "${path}" not found - verify path or use code search`,
2654
- });
2655
- }
2656
- else {
2657
- return createResult({
2658
- error: `Repository not found: ${owner}/${repo} - verify names`,
2659
- });
2660
- }
2661
- }
2662
- else if (errorMsg.includes('403') || errorMsg.includes('Forbidden')) {
2663
- return createResult({
2664
- error: `Access denied to ${owner}/${repo} - check permissions`,
2665
- });
2666
- }
2667
- else {
2668
- return createResult({
2669
- error: `Access failed: ${owner}/${repo} - check connection`,
2670
- });
2671
- }
2672
- }
2673
- // Limit total items to 100 for efficiency
2674
- const limitedItems = items.slice(0, 100);
2675
- // Sort: directories first, then alphabetically
2676
- limitedItems.sort((a, b) => {
2677
- if (a.type !== b.type) {
2678
- return a.type === 'dir' ? -1 : 1;
2679
- }
2680
- return a.name.localeCompare(b.name);
2681
- });
2682
- // Create simplified, token-efficient structure
2683
- const files = limitedItems
2684
- .filter(item => item.type === 'file')
2685
- .map(item => ({
2686
- name: item.name,
2687
- size: item.size,
2688
- url: item.path, // Use path for fetching
2689
- }));
2690
- const folders = limitedItems
2691
- .filter(item => item.type === 'dir')
2692
- .map(item => ({
2693
- name: item.name,
2694
- url: item.path, // Use path for browsing
2695
- }));
2696
- // Simplified result structure - token efficient
2697
- const result = {
2698
- repository: `${owner}/${repo}`,
2699
- branch: usedBranch,
2700
- path: cleanPath || '/',
2701
- githubBasePath: `https://api.github.com/repos/${owner}/${repo}/contents/`,
2702
- files: {
2703
- count: files.length,
2704
- files: files,
2705
- },
2706
- folders: {
2707
- count: folders.length,
2708
- folders: folders,
2709
- },
2710
- ...((usedBranch !== branch || limitedItems.length === 100) && {
2711
- metadata: {
2712
- ...(usedBranch !== branch && {
2713
- branchFallback: {
2714
- requested: branch,
2715
- used: usedBranch,
2716
- },
2717
- }),
2718
- ...(limitedItems.length === 100 && {
2719
- truncated: true,
2720
- }),
2721
- },
2722
- }),
2723
- };
2724
- return createResult({ data: result });
2068
+ const { command, args } = buildGitHubPullRequestsAPICommand(params);
2069
+ const result = await executeGitHubCommand(command, args, { cache: false });
2070
+ if (result.isError) {
2071
+ return result;
2725
2072
  }
2726
- catch (error) {
2073
+ const execResult = JSON.parse(result.content[0].text);
2074
+ const apiResponse = execResult.result;
2075
+ const pullRequests = apiResponse.items || [];
2076
+ if (pullRequests.length === 0) {
2727
2077
  return createResult({
2728
- error: `Repository access failed - verify repository and authentication: ${error}`,
2078
+ error: createNoResultsError('pull_requests'),
2729
2079
  });
2730
2080
  }
2081
+ const cleanPRs = pullRequests.map((pr) => {
2082
+ const result = {
2083
+ number: pr.number,
2084
+ title: pr.title,
2085
+ state: pr.state,
2086
+ author: pr.user?.login || '',
2087
+ repository: pr.repository_url?.split('/').slice(-2).join('/') || 'unknown',
2088
+ labels: pr.labels?.map(l => l.name) || [],
2089
+ created_at: toDDMMYYYY(pr.created_at),
2090
+ updated_at: toDDMMYYYY(pr.updated_at),
2091
+ url: pr.html_url,
2092
+ comments: pr.comments,
2093
+ reactions: pr.reactions?.total_count || 0,
2094
+ draft: pr.draft,
2095
+ };
2096
+ // Only include optional fields if they have values
2097
+ if (pr.merged_at)
2098
+ result.merged_at = pr.merged_at;
2099
+ if (pr.closed_at)
2100
+ result.closed_at = toDDMMYYYY(pr.closed_at);
2101
+ if (pr.head?.ref)
2102
+ result.head = pr.head.ref;
2103
+ if (pr.base?.ref)
2104
+ result.base = pr.base.ref;
2105
+ return result;
2106
+ });
2107
+ const searchResult = {
2108
+ results: cleanPRs,
2109
+ total_count: apiResponse.total_count || cleanPRs.length,
2110
+ };
2111
+ return createResult({ data: searchResult });
2731
2112
  });
2732
2113
  }
2114
+ function buildGitHubPullRequestsAPICommand(params) {
2115
+ const queryParts = [params.query?.trim() || ''];
2116
+ // Repository/organization qualifiers
2117
+ if (params.owner && params.repo) {
2118
+ queryParts.push(`repo:${params.owner}/${params.repo}`);
2119
+ }
2120
+ else if (params.owner) {
2121
+ queryParts.push(`org:${params.owner}`);
2122
+ }
2123
+ // Build search qualifiers from params
2124
+ const qualifiers = {
2125
+ author: params.author,
2126
+ assignee: params.assignee,
2127
+ mentions: params.mentions,
2128
+ commenter: params.commenter,
2129
+ involves: params.involves,
2130
+ state: params.state,
2131
+ created: params.created,
2132
+ updated: params.updated,
2133
+ closed: params.closed,
2134
+ language: params.language,
2135
+ };
2136
+ Object.entries(qualifiers).forEach(([key, value]) => {
2137
+ if (value)
2138
+ queryParts.push(`${key}:${value}`);
2139
+ });
2140
+ // Special qualifiers
2141
+ if (params.reviewedBy)
2142
+ queryParts.push(`reviewed-by:${params.reviewedBy}`);
2143
+ if (params.reviewRequested)
2144
+ queryParts.push(`review-requested:${params.reviewRequested}`);
2145
+ if (params.head)
2146
+ queryParts.push(`head:${params.head}`);
2147
+ if (params.base)
2148
+ queryParts.push(`base:${params.base}`);
2149
+ if (params.mergedAt)
2150
+ queryParts.push(`merged:${params.mergedAt}`);
2151
+ if (params.draft !== undefined)
2152
+ queryParts.push(`draft:${params.draft}`);
2153
+ if (params.checks)
2154
+ queryParts.push(`status:${params.checks}`);
2155
+ if (params.merged !== undefined)
2156
+ queryParts.push(`is:${params.merged ? 'merged' : 'unmerged'}`);
2157
+ if (params.review)
2158
+ queryParts.push(`review:${params.review}`);
2159
+ // Add type qualifier to search only pull requests
2160
+ queryParts.push('type:pr');
2161
+ const query = queryParts.filter(Boolean).join(' ');
2162
+ const limit = Math.min(params.limit || 25, 100);
2163
+ let apiPath = `search/issues?q=${encodeURIComponent(query)}&per_page=${limit}`;
2164
+ if (params.sort)
2165
+ apiPath += `&sort=${params.sort}`;
2166
+ if (params.order)
2167
+ apiPath += `&order=${params.order}`;
2168
+ return { command: 'api', args: [apiPath] };
2169
+ }
2170
+
2171
+ /**
2172
+ * GitHub Repository Search Tool
2173
+ *
2174
+ * MOST EFFECTIVE PATTERNS (based on testing):
2175
+ *
2176
+ * 1. Quality Discovery:
2177
+ * { topic: ["react", "typescript"], stars: "1000..5000", limit: 10 }
2178
+ *
2179
+ * 2. Organization Research:
2180
+ * { owner: ["microsoft", "google"], language: "python", limit: 10 }
2181
+ *
2182
+ * 3. Beginner Projects:
2183
+ * { goodFirstIssues: ">=5", stars: "100..5000", limit: 10 }
2184
+ *
2185
+ * 4. Recent Quality:
2186
+ * { stars: ">1000", created: ">2023-01-01", limit: 10 }
2187
+ *
2188
+ * AVOID: OR queries + language filter, 5+ filters, multi-word OR
2189
+ * TIP: Use limit parameter instead of adding more filters
2190
+ */
2191
+ const GITHUB_SEARCH_REPOSITORIES_TOOL_NAME = 'githubSearchRepositories';
2192
+ const DESCRIPTION$2 = `Discover GitHub repositories with smart filtering. Supports language, stars, topics, ownership, dates, and community metrics. Parameters: query (optional), owner (optional - GitHub username/org, NOT owner/repo), language (optional), stars (optional), topic (optional), forks (optional), numberOfTopics (optional), license (optional), archived (optional), includeForks (optional), visibility (optional), created (optional), updated (optional), size (optional), goodFirstIssues (optional), helpWantedIssues (optional), followers (optional), match (optional), sort (optional), order (optional), limit (optional).`;
2733
2193
  /**
2734
- * Intelligently determines the best branches to try for a repository.
2735
- * Attempts to fetch the default branch first, then falls back to common branches.
2194
+ * Extract owner/repo information from various query formats
2736
2195
  */
2737
- async function getSmartBranchFallback(owner, repo, requestedBranch) {
2738
- const branches = [requestedBranch];
2739
- try {
2740
- // Try to get repository info to find default branch
2741
- const repoInfoResult = await executeGitHubCommand('api', [`/repos/${owner}/${repo}`], {
2742
- cache: false,
2743
- });
2744
- if (!repoInfoResult.isError) {
2745
- const repoData = JSON.parse(JSON.parse(repoInfoResult.content[0].text).result);
2746
- const defaultBranch = repoData.default_branch;
2747
- if (defaultBranch && !branches.includes(defaultBranch)) {
2748
- branches.push(defaultBranch);
2749
- }
2196
+ function extractOwnerRepoFromQuery(query) {
2197
+ let cleanedQuery = query;
2198
+ let extractedOwner;
2199
+ let extractedRepo;
2200
+ const patterns = [
2201
+ // Pattern 1: GitHub URLs (https://github.com/owner/repo)
2202
+ /github\.com\/([^\\s]+)\/([^\\s]+)/i,
2203
+ // Pattern 2: owner/repo format in query
2204
+ /\b([a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\/([a-zA-Z0-9][a-zA-Z0-9\-.]*[a-zA-Z0-9])\b/,
2205
+ // Pattern 3: NPM package-like references (@scope/package)
2206
+ /@([a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\/([a-zA-Z0-9][a-zA-Z0-9\-.]*[a-zA-Z0-9])/,
2207
+ ];
2208
+ for (const pattern of patterns) {
2209
+ const match = cleanedQuery.match(pattern);
2210
+ if (match) {
2211
+ extractedOwner = match[1];
2212
+ extractedRepo = match[2];
2213
+ cleanedQuery = cleanedQuery.replace(match[0], '').trim();
2214
+ break; // Stop after the first successful match
2750
2215
  }
2751
2216
  }
2752
- catch {
2753
- // If we can't get repo info, proceed with standard fallbacks
2754
- }
2755
- // Add common branch names if not already included
2756
- const commonBranches = ['main', 'master', 'develop', 'dev'];
2757
- commonBranches.forEach(branch => {
2758
- if (!branches.includes(branch)) {
2759
- branches.push(branch);
2760
- }
2761
- });
2762
- return branches;
2217
+ return {
2218
+ extractedOwner,
2219
+ extractedRepo,
2220
+ cleanedQuery: cleanedQuery || query, // Ensure original query is returned if cleaned is empty
2221
+ };
2763
2222
  }
2764
-
2765
- const TOOL_NAME$1 = 'github_search_issues';
2766
- const DESCRIPTION$1 = `Find GitHub issues with rich metadata. Discover pain points, feature requests, and bug patterns with boolean logic and GitHub qualifiers.`;
2767
- function registerSearchGitHubIssuesTool(server) {
2768
- server.registerTool(TOOL_NAME$1, {
2769
- description: DESCRIPTION$1,
2223
+ function registerSearchGitHubReposTool(server) {
2224
+ server.registerTool(GITHUB_SEARCH_REPOSITORIES_TOOL_NAME, {
2225
+ description: DESCRIPTION$2,
2770
2226
  inputSchema: {
2771
2227
  query: z
2772
2228
  .string()
2773
- .min(1, 'Search query is required and cannot be empty')
2774
- .describe('Search query with GitHub syntax. Boolean: "bug AND crash", exact phrases: "memory leak", qualifiers: "is:open label:bug".'),
2229
+ .optional()
2230
+ .describe('Search query. START SIMPLE: Use 1-2 words with NO filters first (e.g., "react", "auth"). Add qualifiers only after initial search.'),
2231
+ // CORE FILTERS (GitHub CLI flags)
2775
2232
  owner: z
2776
- .string()
2777
- .min(1)
2233
+ .union([z.string(), z.array(z.string())])
2778
2234
  .optional()
2779
- .describe('Repository owner/organization. Leave empty for global search.'),
2780
- repo: z
2235
+ .describe('Repository owner or organization name only (e.g., "microsoft", "google", NOT "microsoft/vscode"). For private repos, use organizations from api_status_check (user_organizations). Can be a single value or array.'),
2236
+ language: z
2781
2237
  .string()
2782
2238
  .optional()
2783
- .describe('Repository name. Do exploratory search without repo filter first'),
2784
- app: z.string().optional().describe('Filter by GitHub App author'),
2785
- archived: z
2786
- .boolean()
2239
+ .describe('Programming language filter. Use when results need refinement.'),
2240
+ stars: z
2241
+ .union([
2242
+ z.number().int().min(0),
2243
+ z.string().regex(/^(>=?\d+|<=?\d+|\d+\.\.\d+|\d+)$/),
2244
+ ])
2787
2245
  .optional()
2788
- .describe('Filter by repository archived state'),
2789
- assignee: z.string().optional().describe('Filter by assignee'),
2790
- author: z.string().optional().describe('Filter by issue author'),
2791
- closed: z.string().optional().describe('Filter by closed date'),
2792
- commenter: z
2793
- .string()
2246
+ .describe('Stars filter. Supports ranges and thresholds.'),
2247
+ topic: z
2248
+ .union([z.string(), z.array(z.string())])
2794
2249
  .optional()
2795
- .describe('Filter by user who commented'),
2796
- comments: z
2250
+ .describe('Topics filter. Can be a single value or array.'),
2251
+ forks: z.number().optional().describe('Number of forks filter.'),
2252
+ // UPDATED: Match CLI parameter name exactly
2253
+ numberOfTopics: z
2797
2254
  .number()
2798
2255
  .optional()
2799
- .describe('Filter by number of comments'),
2800
- created: z.string().optional().describe('Filter by created date'),
2801
- includePrs: z
2256
+ .describe('Filter by number of topics (indicates documentation quality).'),
2257
+ // QUALITY & STATE FILTERS
2258
+ license: z
2259
+ .union([z.string(), z.array(z.string())])
2260
+ .optional()
2261
+ .describe('License filter. Works well as array ["mit", "apache-2.0"].'),
2262
+ archived: z
2802
2263
  .boolean()
2803
2264
  .optional()
2804
- .describe('Include pull requests in results'),
2805
- interactions: z
2806
- .number()
2265
+ .describe('Filter archived repositories (true/false).'),
2266
+ includeForks: z
2267
+ .enum(['false', 'true', 'only'])
2807
2268
  .optional()
2808
- .describe('Filter by reactions and comments count'),
2809
- involves: z.string().optional().describe('Filter by user involvement'),
2810
- labels: z.string().optional().describe('Filter by labels'),
2811
- language: z.string().optional().describe('Filter by coding language'),
2812
- locked: z
2813
- .boolean()
2269
+ .describe('Include forks: false (exclude), true (include), only (forks only).'),
2270
+ visibility: z
2271
+ .enum(['public', 'private', 'internal'])
2814
2272
  .optional()
2815
- .describe('Filter by locked conversation status'),
2816
- match: z
2817
- .enum(['title', 'body', 'comments'])
2273
+ .describe('Repository visibility filter.'),
2274
+ // DATE & SIZE FILTERS
2275
+ created: z
2276
+ .string()
2818
2277
  .optional()
2819
- .describe('Restrict search to specific field'),
2820
- mentions: z.string().optional().describe('Filter by user mentions'),
2821
- milestone: z.string().optional().describe('Filter by milestone title'),
2822
- noAssignee: z
2823
- .boolean()
2278
+ .describe('Created date filter. Format: ">2020-01-01", "<2023-12-31".'),
2279
+ updated: z
2280
+ .string()
2824
2281
  .optional()
2825
- .describe('Filter by missing assignee'),
2826
- noLabel: z.boolean().optional().describe('Filter by missing label'),
2827
- noMilestone: z
2828
- .boolean()
2282
+ .describe('Updated date filter. Good for finding active projects.'),
2283
+ size: z
2284
+ .string()
2829
2285
  .optional()
2830
- .describe('Filter by missing milestone'),
2831
- noProject: z.boolean().optional().describe('Filter by missing project'),
2832
- project: z.string().optional().describe('Filter by project board'),
2833
- reactions: z.number().optional().describe('Filter by reactions count'),
2834
- state: z
2835
- .enum(['open', 'closed'])
2286
+ .describe('Repository size filter in KB. Format: ">1000", "<500".'),
2287
+ // COMMUNITY FILTERS - Match CLI parameter names exactly
2288
+ goodFirstIssues: z
2289
+ .union([
2290
+ z.number().int().min(0),
2291
+ z
2292
+ .string()
2293
+ .regex(/^(>=?\d+|<=?\d+|\d+\.\.\d+|\d+)$/, 'Invalid format. Use: number, ">5", ">=10", "<20", "<=15", or "5..20"'),
2294
+ ])
2836
2295
  .optional()
2837
- .describe('Filter by issue state'),
2838
- teamMentions: z.string().optional().describe('Filter by team mentions'),
2839
- updated: z.string().optional().describe('Filter by last updated date'),
2840
- visibility: z
2841
- .enum(['public', 'private', 'internal'])
2296
+ .describe('Good first issues count. WORKING: Filter for beginner-friendly projects. EXCELLENT when combined with stars "100..5000" for quality beginner projects.'),
2297
+ helpWantedIssues: z
2298
+ .union([
2299
+ z.number().int().min(0),
2300
+ z
2301
+ .string()
2302
+ .regex(/^(>=?\d+|<=?\d+|\d+\.\.\d+|\d+)$/, 'Invalid format. Use: number, ">5", ">=10", "<20", "<=15", or "5..20"'),
2303
+ ])
2304
+ .optional()
2305
+ .describe('Help wanted issues count. Good for finding projects needing contributors.'),
2306
+ followers: z.number().optional().describe('Followers count filter.'),
2307
+ // SEARCH SCOPE
2308
+ match: z
2309
+ .enum(['name', 'description', 'readme'])
2842
2310
  .optional()
2843
- .describe('Filter by repository visibility'),
2311
+ .describe('Search scope: name, description, or readme content.'),
2312
+ // SORTING & LIMITS - Match CLI defaults exactly
2844
2313
  sort: z
2845
2314
  .enum([
2846
- 'comments',
2847
- 'created',
2848
- 'interactions',
2849
- 'reactions',
2850
- 'reactions-+1',
2851
- 'reactions--1',
2852
- 'reactions-heart',
2853
- 'reactions-smile',
2854
- 'reactions-tada',
2855
- 'reactions-thinking_face',
2315
+ 'forks',
2316
+ 'help-wanted-issues',
2317
+ 'stars',
2856
2318
  'updated',
2857
2319
  'best-match',
2858
2320
  ])
2859
2321
  .optional()
2860
- .describe('Sort criteria'),
2322
+ .default('best-match')
2323
+ .describe('Sort criteria for results.'),
2861
2324
  order: z
2862
2325
  .enum(['asc', 'desc'])
2863
2326
  .optional()
2864
2327
  .default('desc')
2865
- .describe('Order (default: desc)'),
2328
+ .describe('Sort order direction.'),
2866
2329
  limit: z
2330
+ .number()
2331
+ .int()
2332
+ .min(1)
2333
+ .max(100)
2334
+ .optional()
2335
+ .default(30)
2336
+ .describe('Maximum results to return (1-100). Default: 30'),
2337
+ },
2338
+ annotations: {
2339
+ title: 'GitHub Repository Search',
2340
+ readOnlyHint: true,
2341
+ destructiveHint: false,
2342
+ idempotentHint: true,
2343
+ openWorldHint: true,
2344
+ },
2345
+ }, async (args) => {
2346
+ try {
2347
+ // Extract owner/repo from query if present
2348
+ const queryInfo = args.query
2349
+ ? extractOwnerRepoFromQuery(args.query)
2350
+ : {
2351
+ cleanedQuery: '',
2352
+ extractedOwner: undefined,
2353
+ extractedRepo: undefined,
2354
+ };
2355
+ // Merge extracted owner with explicit owner parameter
2356
+ let finalOwner = args.owner;
2357
+ if (queryInfo.extractedOwner && !finalOwner) {
2358
+ finalOwner = queryInfo.extractedOwner;
2359
+ }
2360
+ // Update parameters with extracted information
2361
+ const enhancedArgs = {
2362
+ ...args,
2363
+ query: queryInfo.cleanedQuery || args.query,
2364
+ owner: finalOwner,
2365
+ };
2366
+ // Enhanced validation logic for primary filters
2367
+ const hasPrimaryFilter = enhancedArgs.query?.trim() ||
2368
+ enhancedArgs.owner ||
2369
+ enhancedArgs.language ||
2370
+ enhancedArgs.topic ||
2371
+ enhancedArgs.stars ||
2372
+ enhancedArgs.forks;
2373
+ if (!hasPrimaryFilter) {
2374
+ return createResult({
2375
+ error: SUGGESTIONS.REPO_SEARCH_PRIMARY_FILTER,
2376
+ });
2377
+ }
2378
+ // First attempt: Search with current parameters
2379
+ const result = await searchGitHubRepos(enhancedArgs);
2380
+ // Fallback for private repositories: If no results and owner is specified, try with private visibility
2381
+ if (!result.isError) {
2382
+ const resultData = JSON.parse(result.content[0].text);
2383
+ if (resultData.total === 0 &&
2384
+ enhancedArgs.owner &&
2385
+ !enhancedArgs.visibility) {
2386
+ // Try searching with private visibility for organization repos
2387
+ const privateSearchArgs = {
2388
+ ...enhancedArgs,
2389
+ visibility: 'private',
2390
+ };
2391
+ const privateResult = await searchGitHubRepos(privateSearchArgs);
2392
+ if (!privateResult.isError) {
2393
+ const privateData = JSON.parse(privateResult.content[0].text);
2394
+ if (privateData.total > 0) {
2395
+ // Return private results with note
2396
+ return createResult({
2397
+ data: {
2398
+ ...privateData,
2399
+ note: 'Found results in private repositories within the specified organization.',
2400
+ },
2401
+ });
2402
+ }
2403
+ }
2404
+ }
2405
+ }
2406
+ return result;
2407
+ }
2408
+ catch (error) {
2409
+ return createResult({
2410
+ error: createSearchFailedError('repositories'),
2411
+ });
2412
+ }
2413
+ });
2414
+ }
2415
+ async function searchGitHubRepos(params) {
2416
+ const cacheKey = generateCacheKey('gh-repos', params);
2417
+ return withCache(cacheKey, async () => {
2418
+ try {
2419
+ const { command, args } = buildGitHubReposSearchCommand(params);
2420
+ const result = await executeGitHubCommand(command, args, {
2421
+ cache: false,
2422
+ });
2423
+ if (result.isError) {
2424
+ return result;
2425
+ }
2426
+ const execResult = JSON.parse(result.content[0].text);
2427
+ const repositories = execResult.result;
2428
+ if (!Array.isArray(repositories) || repositories.length === 0) {
2429
+ return createResult({
2430
+ error: createNoResultsError('repositories'),
2431
+ });
2432
+ }
2433
+ const analysis = {
2434
+ totalFound: 0,
2435
+ languages: new Set(),
2436
+ avgStars: 0,
2437
+ recentlyUpdated: 0,
2438
+ topStarred: [],
2439
+ };
2440
+ analysis.totalFound = repositories.length;
2441
+ // Analyze repository data
2442
+ let totalStars = 0;
2443
+ const now = new Date();
2444
+ const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
2445
+ repositories.forEach(repo => {
2446
+ // Collect languages
2447
+ if (repo.language) {
2448
+ analysis.languages.add(repo.language);
2449
+ }
2450
+ // Calculate average stars (use correct field name)
2451
+ if (typeof repo.stargazersCount === 'number') {
2452
+ totalStars += repo.stargazersCount;
2453
+ }
2454
+ // Count recently updated repositories (use correct field name)
2455
+ if (repo.updatedAt) {
2456
+ const updatedDate = new Date(repo.updatedAt);
2457
+ if (!isNaN(updatedDate.getTime()) && updatedDate > thirtyDaysAgo) {
2458
+ analysis.recentlyUpdated++;
2459
+ }
2460
+ }
2461
+ });
2462
+ analysis.avgStars =
2463
+ repositories.length > 0
2464
+ ? Math.round(totalStars / repositories.length)
2465
+ : 0;
2466
+ // Get all repositories with comprehensive data
2467
+ analysis.topStarred = repositories.map(repo => ({
2468
+ name: repo.fullName || repo.name,
2469
+ stars: repo.stargazersCount || 0,
2470
+ description: repo.description || 'No description',
2471
+ language: repo.language || 'Unknown',
2472
+ url: repo.url,
2473
+ forks: repo.forksCount || 0,
2474
+ isPrivate: repo.isPrivate || false,
2475
+ isArchived: repo.isArchived || false,
2476
+ isFork: repo.isFork || false,
2477
+ topics: [], // GitHub CLI search repos doesn't provide topics in JSON output
2478
+ license: repo.license?.name || null,
2479
+ hasIssues: repo.hasIssues || false,
2480
+ openIssuesCount: repo.openIssuesCount || 0,
2481
+ createdAt: toDDMMYYYY(repo.createdAt),
2482
+ updatedAt: toDDMMYYYY(repo.updatedAt),
2483
+ visibility: repo.visibility || 'public',
2484
+ owner: repo.owner?.login || repo.owner,
2485
+ }));
2486
+ return createResult({
2487
+ data: {
2488
+ total_count: analysis.totalFound,
2489
+ ...(analysis.totalFound > 0
2490
+ ? {
2491
+ repositories: analysis.topStarred,
2492
+ summary: {
2493
+ languages: Array.from(analysis.languages).slice(0, 10),
2494
+ avgStars: analysis.avgStars,
2495
+ recentlyUpdated: analysis.recentlyUpdated,
2496
+ },
2497
+ }
2498
+ : {
2499
+ repositories: [],
2500
+ }),
2501
+ },
2502
+ });
2503
+ }
2504
+ catch (error) {
2505
+ return createResult({
2506
+ error: createSearchFailedError('repositories'),
2507
+ });
2508
+ }
2509
+ });
2510
+ }
2511
+ function buildGitHubReposSearchCommand(params) {
2512
+ const query = params.query?.trim() || '';
2513
+ const args = ['repos'];
2514
+ const hasEmbeddedQualifiers = query &&
2515
+ /\b(stars|language|org|repo|topic|user|created|updated|size|license|archived|fork|good-first-issues|help-wanted-issues):/i.test(query);
2516
+ if (query) {
2517
+ args.push(query);
2518
+ }
2519
+ args.push('--json=name,fullName,description,language,stargazersCount,forksCount,updatedAt,createdAt,url,owner,isPrivate,license,hasIssues,openIssuesCount,isArchived,isFork,visibility');
2520
+ const addArg = (paramName, cliFlag, condition = true, formatter) => {
2521
+ const value = params[paramName];
2522
+ if (value !== undefined && condition) {
2523
+ if (Array.isArray(value)) {
2524
+ args.push(`--${cliFlag}=${value.join(',')}`);
2525
+ }
2526
+ else if (formatter) {
2527
+ args.push(`--${cliFlag}=${formatter(value)}`);
2528
+ }
2529
+ else {
2530
+ args.push(`--${cliFlag}=${value.toString()}`);
2531
+ }
2532
+ }
2533
+ };
2534
+ // CORE FILTERS
2535
+ addArg('owner', 'owner', !hasEmbeddedQualifiers);
2536
+ addArg('language', 'language', !hasEmbeddedQualifiers);
2537
+ addArg('forks', 'forks', !hasEmbeddedQualifiers);
2538
+ addArg('topic', 'topic', !hasEmbeddedQualifiers);
2539
+ addArg('numberOfTopics', 'number-topics');
2540
+ addArg('stars', 'stars', !hasEmbeddedQualifiers, value => typeof value === 'number' ? value.toString() : value.trim());
2541
+ // QUALITY & STATE FILTERS
2542
+ addArg('archived', 'archived');
2543
+ addArg('includeForks', 'include-forks');
2544
+ addArg('visibility', 'visibility');
2545
+ addArg('license', 'license');
2546
+ // DATE & SIZE FILTERS
2547
+ addArg('created', 'created');
2548
+ addArg('updated', 'updated');
2549
+ addArg('size', 'size');
2550
+ // COMMUNITY FILTERS
2551
+ addArg('goodFirstIssues', 'good-first-issues', true, value => typeof value === 'number' ? value.toString() : value);
2552
+ addArg('helpWantedIssues', 'help-wanted-issues', true, value => typeof value === 'number' ? value.toString() : value);
2553
+ addArg('followers', 'followers');
2554
+ // SEARCH SCOPE
2555
+ addArg('match', 'match');
2556
+ // SORTING AND LIMITS
2557
+ addArg('limit', 'limit');
2558
+ addArg('order', 'order');
2559
+ const sortBy = params.sort || 'best-match';
2560
+ if (sortBy !== 'best-match') {
2561
+ args.push(`--sort=${sortBy}`);
2562
+ }
2563
+ return { command: 'search', args };
2564
+ }
2565
+
2566
+ const NPM_PACKAGE_SEARCH_TOOL_NAME = 'npmPackageSearch';
2567
+ const DESCRIPTION$1 = `Search NPM packages with fuzzy matching. Supports multiple search terms and aggregates results. Use functional keywords like "react hooks", "auth", or "testing". Parameters: queries (required, string or array), searchLimit (optional).`;
2568
+ const MAX_DESCRIPTION_LENGTH = 100;
2569
+ const MAX_KEYWORDS = 10;
2570
+ function registerNpmSearchTool(server) {
2571
+ server.registerTool(NPM_PACKAGE_SEARCH_TOOL_NAME, {
2572
+ description: DESCRIPTION$1,
2573
+ inputSchema: {
2574
+ queries: z
2575
+ .union([z.string(), z.array(z.string())])
2576
+ .describe('Search terms for packages. Use functionality keywords: "react hooks", "cli tool", "testing"'),
2577
+ searchLimit: z
2867
2578
  .number()
2868
2579
  .int()
2869
2580
  .min(1)
2870
2581
  .max(50)
2871
2582
  .optional()
2872
- .default(25)
2873
- .describe('Maximum results (default: 25, max: 50)'),
2583
+ .default(20)
2584
+ .describe('Results limit per query (1-50). Default: 20'),
2874
2585
  },
2875
2586
  annotations: {
2876
- title: 'GitHub Issues Search',
2587
+ title: 'NPM Package Search',
2877
2588
  readOnlyHint: true,
2878
2589
  destructiveHint: false,
2879
2590
  idempotentHint: true,
2880
2591
  openWorldHint: true,
2881
2592
  },
2882
2593
  }, async (args) => {
2883
- if (!args.query?.trim()) {
2884
- return createResult({
2885
- error: 'Search query is required and cannot be empty - provide keywords to search for issues',
2886
- });
2887
- }
2888
- if (args.query.length > 256) {
2889
- return createResult({
2890
- error: 'Search query is too long. Please limit to 256 characters or less - simplify your search terms',
2891
- });
2892
- }
2893
2594
  try {
2894
- return await searchGitHubIssues(args);
2895
- }
2896
- catch (error) {
2897
- const errorMessage = error instanceof Error ? error.message : '';
2898
- if (errorMessage.includes('authentication')) {
2899
- return createResult({
2900
- error: 'GitHub authentication required - run api_status_check tool',
2901
- });
2595
+ const queries = Array.isArray(args.queries)
2596
+ ? args.queries
2597
+ : [args.queries];
2598
+ const searchLimit = args.searchLimit || 20;
2599
+ const allPackages = [];
2600
+ // Search for each query term
2601
+ for (const query of queries) {
2602
+ const result = await executeNpmCommand('search', [query, `--searchlimit=${searchLimit}`, '--json'], { cache: true });
2603
+ if (!result.isError && result.content?.[0]?.text) {
2604
+ const packages = parseNpmSearchOutput(result.content[0].text);
2605
+ allPackages.push(...packages);
2606
+ }
2902
2607
  }
2903
- if (errorMessage.includes('rate limit')) {
2608
+ const deduplicatedPackages = deduplicatePackages(allPackages);
2609
+ if (deduplicatedPackages.length > 0) {
2904
2610
  return createResult({
2905
- error: 'GitHub rate limit exceeded - wait or use specific filters',
2611
+ data: {
2612
+ total_count: deduplicatedPackages.length,
2613
+ results: deduplicatedPackages,
2614
+ },
2906
2615
  });
2907
2616
  }
2908
- // Generic fallback
2909
2617
  return createResult({
2910
- error: 'GitHub issue search failed - check authentication or simplify query',
2618
+ error: createNoResultsError('packages'),
2911
2619
  });
2912
2620
  }
2913
- });
2914
- }
2915
- async function searchGitHubIssues(params) {
2916
- const cacheKey = generateCacheKey('gh-issues', params);
2917
- return withCache(cacheKey, async () => {
2918
- const { command, args } = buildGitHubIssuesAPICommand(params);
2919
- const result = await executeGitHubCommand(command, args, { cache: false });
2920
- if (result.isError) {
2921
- return result;
2621
+ catch (error) {
2622
+ return createResult({
2623
+ error: createSearchFailedError('packages'),
2624
+ });
2922
2625
  }
2923
- const execResult = JSON.parse(result.content[0].text);
2924
- const apiResponse = JSON.parse(execResult.result);
2925
- const issues = apiResponse.items || [];
2926
- const cleanIssues = issues.map((issue) => ({
2927
- number: issue.number,
2928
- title: issue.title,
2929
- state: issue.state,
2930
- author: issue.user?.login || '',
2931
- repository: issue.repository_url?.split('/').slice(-2).join('/') || 'unknown',
2932
- labels: issue.labels?.map(l => l.name) || [],
2933
- created_at: toDDMMYYYY(issue.created_at),
2934
- updated_at: toDDMMYYYY(issue.updated_at),
2935
- url: issue.html_url,
2936
- comments: issue.comments,
2937
- reactions: issue.reactions?.total_count || 0,
2938
- }));
2939
- const searchResult = {
2940
- results: cleanIssues,
2941
- total_count: apiResponse.total_count || cleanIssues.length,
2942
- metadata: {
2943
- incomplete_results: apiResponse.incomplete_results || false,
2944
- },
2945
- };
2946
- return createResult({ data: searchResult });
2947
2626
  });
2948
2627
  }
2949
- function buildGitHubIssuesAPICommand(params) {
2950
- const queryParts = [];
2951
- // Start with the base query, but filter out qualifiers that will be added separately
2952
- const baseQuery = params.query?.trim() || '';
2953
- // Extract and remove qualifiers from the main query to avoid conflicts
2954
- const qualifierPatterns = [
2955
- /\bis:(open|closed)\b/gi,
2956
- /\blabel:("[^"]*"|[^\s]+)/gi,
2957
- /\bcreated:([^\s]+)/gi,
2958
- /\bupdated:([^\s]+)/gi,
2959
- /\bauthor:([^\s]+)/gi,
2960
- /\bassignee:([^\s]+)/gi,
2961
- /\bstate:(open|closed)/gi,
2962
- /\brepo:([^\s]+)/gi,
2963
- /\borg:([^\s]+)/gi,
2964
- ];
2965
- // Remove extracted qualifiers from base query
2966
- let cleanQuery = baseQuery;
2967
- qualifierPatterns.forEach(pattern => {
2968
- cleanQuery = cleanQuery.replace(pattern, '').trim();
2628
+ function deduplicatePackages(packages) {
2629
+ const seen = new Set();
2630
+ return packages.filter(pkg => {
2631
+ if (seen.has(pkg.name))
2632
+ return false;
2633
+ seen.add(pkg.name);
2634
+ return true;
2969
2635
  });
2970
- // Add the cleaned query if it has content
2971
- if (cleanQuery) {
2972
- queryParts.push(cleanQuery);
2973
- }
2974
- // Repository/organization qualifiers - prioritize function params over query
2975
- if (params.owner && params.repo) {
2976
- queryParts.push(`repo:${params.owner}/${params.repo}`);
2977
- }
2978
- else if (params.owner) {
2979
- queryParts.push(`org:${params.owner}`);
2980
- }
2981
- // Build search qualifiers from function parameters (these take precedence)
2982
- const qualifiers = {
2983
- author: params.author,
2984
- assignee: params.assignee,
2985
- mentions: params.mentions,
2986
- commenter: params.commenter,
2987
- involves: params.involves,
2988
- language: params.language,
2989
- state: params.state,
2990
- created: params.created,
2991
- updated: params.updated,
2992
- closed: params.closed,
2636
+ }
2637
+ function normalizePackage(pkg) {
2638
+ const description = pkg.description || null;
2639
+ const truncatedDescription = description && description.length > MAX_DESCRIPTION_LENGTH
2640
+ ? description.substring(0, MAX_DESCRIPTION_LENGTH) + '...'
2641
+ : description;
2642
+ const keywords = pkg.keywords || [];
2643
+ const limitedKeywords = keywords.slice(0, MAX_KEYWORDS);
2644
+ return {
2645
+ name: pkg.name || '',
2646
+ version: pkg.version || '',
2647
+ description: truncatedDescription,
2648
+ keywords: limitedKeywords,
2649
+ repository: pkg.links?.repository || pkg.repository?.url || null,
2993
2650
  };
2994
- Object.entries(qualifiers).forEach(([key, value]) => {
2995
- if (value)
2996
- queryParts.push(`${key}:${value}`);
2997
- });
2998
- // Special qualifiers - handle labels carefully
2999
- if (params.labels) {
3000
- queryParts.push(`label:"${params.labels}"`);
3001
- }
3002
- if (params.milestone)
3003
- queryParts.push(`milestone:"${params.milestone}"`);
3004
- if (params.noAssignee)
3005
- queryParts.push('no:assignee');
3006
- if (params.noLabel)
3007
- queryParts.push('no:label');
3008
- if (params.noMilestone)
3009
- queryParts.push('no:milestone');
3010
- if (params.archived !== undefined)
3011
- queryParts.push(`archived:${params.archived}`);
3012
- if (params.locked)
3013
- queryParts.push('is:locked');
3014
- if (params.visibility)
3015
- queryParts.push(`is:${params.visibility}`);
3016
- // Extract qualifiers from original query and add them if not already set by params
3017
- if (baseQuery.includes('is:') && !params.state) {
3018
- const isMatch = baseQuery.match(/\bis:(open|closed)\b/i);
3019
- if (isMatch && !queryParts.some(part => part.startsWith('state:'))) {
3020
- queryParts.push(`state:${isMatch[1].toLowerCase()}`);
2651
+ }
2652
+ function parseNpmSearchOutput(output) {
2653
+ try {
2654
+ const wrapper = JSON.parse(output);
2655
+ const commandResult = wrapper.result;
2656
+ let packages = [];
2657
+ // Handle different npm search output formats
2658
+ if (Array.isArray(commandResult)) {
2659
+ packages = commandResult;
3021
2660
  }
3022
- }
3023
- if (baseQuery.includes('label:') && !params.labels) {
3024
- const labelMatch = baseQuery.match(/\blabel:("[^"]*"|[^\s]+)/i);
3025
- if (labelMatch) {
3026
- const labelValue = labelMatch[1].replace(/"/g, '');
3027
- queryParts.push(`label:"${labelValue}"`);
2661
+ else if (commandResult?.objects && Array.isArray(commandResult.objects)) {
2662
+ packages = commandResult.objects.map((obj) => obj.package || obj);
3028
2663
  }
3029
- }
3030
- if (baseQuery.includes('created:') && !params.created) {
3031
- const createdMatch = baseQuery.match(/\bcreated:([^\s]+)/i);
3032
- if (createdMatch) {
3033
- queryParts.push(`created:${createdMatch[1]}`);
2664
+ else if (commandResult?.results && Array.isArray(commandResult.results)) {
2665
+ packages = commandResult.results;
3034
2666
  }
2667
+ return packages.map(normalizePackage);
2668
+ }
2669
+ catch (error) {
2670
+ logger.warn('Failed to parse NPM search results:', error);
2671
+ return [];
3035
2672
  }
3036
- const query = queryParts.filter(Boolean).join(' ');
3037
- const limit = Math.min(params.limit || 25, 100);
3038
- let apiPath = `search/issues?q=${encodeURIComponent(query)}&per_page=${limit}`;
3039
- if (params.sort)
3040
- apiPath += `&sort=${params.sort}`;
3041
- if (params.order)
3042
- apiPath += `&order=${params.order}`;
3043
- return { command: 'api', args: [apiPath] };
3044
2673
  }
3045
2674
 
3046
- const TOOL_NAME = 'npm_view_package';
3047
- const DESCRIPTION = `Get comprehensive NPM package metadata efficiently. Returns repository URL, exports, dependencies, and version history. Essential for finding package source code and understanding project structure.`;
2675
+ const NPM_VIEW_PACKAGE_TOOL_NAME = 'npmViewPackage';
2676
+ const DESCRIPTION = `View detailed NPM package information including repository URL, exports, version history, dependencies, and download stats. Returns optimized metadata for code navigation. Parameters: packageName (required).`;
3048
2677
  function registerNpmViewPackageTool(server) {
3049
- server.registerTool(TOOL_NAME, {
2678
+ server.registerTool(NPM_VIEW_PACKAGE_TOOL_NAME, {
3050
2679
  description: DESCRIPTION,
3051
2680
  inputSchema: {
3052
2681
  packageName: z
3053
2682
  .string()
3054
2683
  .min(1)
3055
- .describe('NPM package name to analyze. Returns complete package context including exports (critical for GitHub file discovery), repository URL, dependencies, and version history.'),
2684
+ .describe('NPM package name (e.g., "react", "express", "@types/node")'),
3056
2685
  },
3057
2686
  annotations: {
3058
- title: 'NPM Package Metadata',
2687
+ title: 'NPM Package Analyzer',
3059
2688
  readOnlyHint: true,
3060
2689
  destructiveHint: false,
3061
2690
  idempotentHint: true,
@@ -3068,7 +2697,7 @@ function registerNpmViewPackageTool(server) {
3068
2697
  return result;
3069
2698
  }
3070
2699
  const execResult = JSON.parse(result.content[0].text);
3071
- const packageData = JSON.parse(execResult.result);
2700
+ const packageData = execResult.result;
3072
2701
  // Transform to optimized format
3073
2702
  const optimizedResult = transformToOptimizedFormat(packageData);
3074
2703
  return createResult({ data: optimizedResult });
@@ -3077,30 +2706,22 @@ function registerNpmViewPackageTool(server) {
3077
2706
  const errorMessage = error.message || '';
3078
2707
  if (errorMessage.includes('not found')) {
3079
2708
  return createResult({
3080
- error: 'Package not found - verify package name spelling',
3081
- cli_command: `npm view ${args.packageName} --json`,
2709
+ error: 'Package not found. Check spelling and use exact package name from npm',
3082
2710
  });
3083
2711
  }
3084
2712
  if (errorMessage.includes('network')) {
3085
2713
  return createResult({
3086
- error: 'Network error - check internet connection',
3087
- cli_command: `npm view ${args.packageName} --json`,
2714
+ error: 'Network error. Check internet connection and try again',
3088
2715
  });
3089
2716
  }
3090
2717
  return createResult({
3091
- error: 'NPM package lookup failed',
3092
- cli_command: `npm view ${args.packageName} --json`,
3093
- suggestions: [
3094
- 'Verify package name is correct',
3095
- 'Check if package exists on npmjs.com',
3096
- 'Try again in a moment',
3097
- ],
2718
+ error: 'Failed to fetch package information. Try again or check npm status',
3098
2719
  });
3099
2720
  }
3100
2721
  });
3101
2722
  }
3102
2723
  /**
3103
- * Transform NPM CLI response to optimized format
2724
+ * Transform NPM CLI response to optimized format for code analysis
3104
2725
  */
3105
2726
  function transformToOptimizedFormat(packageData) {
3106
2727
  // Extract repository URL and simplify
@@ -3112,7 +2733,9 @@ function transformToOptimizedFormat(packageData) {
3112
2733
  : undefined;
3113
2734
  // Get version timestamps from time object and limit to last 5
3114
2735
  const timeData = packageData.time || {};
3115
- const versionList = packageData.versions || [];
2736
+ const versionList = Array.isArray(packageData.versions)
2737
+ ? packageData.versions
2738
+ : [];
3116
2739
  const recentVersions = versionList.slice(-5).map((version) => ({
3117
2740
  version,
3118
2741
  date: timeData[version] ? toDDMMYYYY(timeData[version]) : 'Unknown',
@@ -3139,7 +2762,7 @@ function transformToOptimizedFormat(packageData) {
3139
2762
  return result;
3140
2763
  }
3141
2764
  /**
3142
- * Simplify exports object to essential entry points
2765
+ * Simplify exports to show only essential entry points for code navigation
3143
2766
  */
3144
2767
  function simplifyExports(exports) {
3145
2768
  if (typeof exports === 'string') {
@@ -3160,8 +2783,9 @@ function simplifyExports(exports) {
3160
2783
  simplified.main = mainExport.import;
3161
2784
  }
3162
2785
  }
3163
- // Extract types if available
3164
- if (exports['./types'] || exports['.']?.types) {
2786
+ // Extract types if available with safe property access
2787
+ if (exports['./types'] ||
2788
+ (exports['.'] && typeof exports['.'] === 'object' && exports['.'].types)) {
3165
2789
  simplified.types = exports['./types'] || exports['.'].types;
3166
2790
  }
3167
2791
  // Add a few other important exports (max 3 total)
@@ -3192,93 +2816,257 @@ async function viewNpmPackage(packageName) {
3192
2816
  const errorMessage = error.message || '';
3193
2817
  if (errorMessage.includes('404')) {
3194
2818
  return createResult({
3195
- error: 'Package not found on NPM registry',
2819
+ error: 'Package not found on NPM registry. Verify the exact package name',
3196
2820
  });
3197
2821
  }
3198
2822
  return createResult({
3199
- error: 'NPM command execution failed',
2823
+ error: 'Failed to execute NPM command. Check npm installation',
3200
2824
  });
3201
2825
  }
3202
2826
  });
3203
2827
  }
3204
2828
 
2829
+ const PROMPT_SYSTEM_PROMPT = `You are an expert code research assistant that knows how to understand users needs and search for the right information
2830
+ using gh cli for github and npm cli for packages.
2831
+
2832
+ CRITICAL SEARCH PRINCIPLES:
2833
+
2834
+ ## 1. PROGRESSIVE SEARCH STRATEGY (MOST IMPORTANT):
2835
+ a) START BROAD: Begin with simple, general terms (1-2 words max)
2836
+ b) ANALYZE RESULTS: Learn from what you find (repo names, owners, common patterns)
2837
+ c) REFINE GRADUALLY: Add filters only after understanding the landscape
2838
+ d) MULTIPLE ANGLES: Try different search terms if first approach yields no results
2839
+
2840
+ ## 2. SEARCH PROGRESSION EXAMPLES:
2841
+ - User asks about "React state management"
2842
+ 1st search: "state" or "redux" (BROAD)
2843
+ 2nd search: "react state" with language:javascript (REFINED)
2844
+ 3rd search: owner:facebook with specific terms (TARGETED)
2845
+
2846
+ - User asks about "authentication libraries"
2847
+ 1st search: "auth" or "authentication" (BROAD)
2848
+ 2nd search: Add language filter based on results
2849
+ 3rd search: Focus on specific owners/topics found
2850
+
2851
+ ## 3. HANDLING NO RESULTS:
2852
+ - NEVER give up after one failed search
2853
+ - Try progressively BROADER terms
2854
+ - Remove ALL filters and try core keywords
2855
+ - Search for related concepts (e.g., "auth" → "login" → "session")
2856
+ - Check different owners/organizations
2857
+ - For code search: try searching in popular repos first
2858
+
2859
+ ## 4. SMART FILTER USAGE:
2860
+ - NO FILTERS on first search (unless user specifies)
2861
+ - Add ONE filter at a time based on results
2862
+ - Common progression: query → +language → +stars → +owner
2863
+ - Reserve complex filters for final refinement
2864
+
2865
+ ## 5. TOOL SYNERGY:
2866
+ - Use repository search to find relevant repos FIRST
2867
+ - Then use code search within those repos
2868
+ - Check npm packages for JavaScript/TypeScript queries
2869
+ - Verify access with api_status_check for private repos
2870
+
2871
+ ## 6. RESEARCH BEST PRACTICES:
2872
+ - Conduct COMPREHENSIVE research with multiple searches
2873
+ - Learn from each search to improve the next
2874
+ - Provide context about your search strategy
2875
+ - Always verify technical details with actual code
2876
+
2877
+ ## 7. COMPLEX ANALYSIS PATTERNS:
2878
+
2879
+ ### Multi-Framework Comparison (e.g., React vs Vue):
2880
+ 1. Search repos separately: "react", then "vue"
2881
+ 2. Find core implementation files: "scheduler" in React, "reactivity" in Vue
2882
+ 3. Search specific features: "concurrent", "fiber", "proxy"
2883
+ 4. Compare similar functionalities across repos
2884
+
2885
+ ### Architecture Analysis (e.g., State Management):
2886
+ 1. Start broad: "state" or "store"
2887
+ 2. Identify major libraries: Redux, Zustand, Jotai
2888
+ 3. Search implementation patterns: "reducer", "atom", "selector"
2889
+ 4. Analyze each approach separately, then compare
2890
+
2891
+ ### Evolution Tracking (e.g., Feature History):
2892
+ 1. Use commit search with broad terms first
2893
+ 2. Narrow by date ranges progressively
2894
+ 3. Track changes in specific files over time
2895
+ 4. Identify key contributors and their patterns
2896
+
2897
+ ### Performance Analysis:
2898
+ 1. Search for benchmarks: "benchmark", "perf", "performance"
2899
+ 2. Look for optimization commits: "optimize", "faster", "improve"
2900
+ 3. Find profiling code: "profile", "measure", "timing"
2901
+ 4. Compare implementation strategies
2902
+
2903
+ ## 8. CROSS-REPOSITORY INTELLIGENCE:
2904
+ - When comparing frameworks, search EACH separately first
2905
+ - Build mental model of each codebase structure
2906
+ - Use discovered patterns to refine searches
2907
+ - Connect findings across repositories for insights
2908
+
2909
+ ## 9. TOOL-SPECIFIC BEST PRACTICES:
2910
+
2911
+ ### ${API_STATUS_CHECK_TOOL_NAME}:
2912
+ - Run FIRST when dealing with private repositories
2913
+ - Use organizations list to scope searches
2914
+ - Verify authentication before extensive searches
2915
+
2916
+ ### ${GITHUB_SEARCH_REPOSITORIES_TOOL_NAME}:
2917
+ - Start with topic/language, add stars/forks filters later
2918
+ - Use date ranges for trending analysis
2919
+ - Combine multiple searches for comprehensive discovery
2920
+
2921
+ ### ${GITHUB_SEARCH_CODE_TOOL_NAME}:
2922
+ - Begin with function/class names, not full signatures
2923
+ - Use extension filters for targeted searches
2924
+ - Try partial matches before exact phrases
2925
+
2926
+ ### ${GITHUB_VIEW_REPO_STRUCTURE_TOOL_NAME}:
2927
+ - Navigate from root, then drill down
2928
+ - Use for understanding project organization
2929
+ - Check common paths: src/, lib/, packages/
2930
+
2931
+ ### ${GITHUB_GET_FILE_CONTENT_TOOL_NAME}:
2932
+ - Verify file paths with repo structure first
2933
+ - Use for implementation details and documentation
2934
+ - Remember 300KB limit for large files
2935
+
2936
+ ### ${GITHUB_SEARCH_COMMITS_TOOL_NAME}:
2937
+ - Search by feature keywords, not commit hashes
2938
+ - Use author filter for contributor analysis
2939
+ - Date ranges help track feature evolution
2940
+
2941
+ ### ${GITHUB_SEARCH_ISSUES_TOOL_NAME} & ${GITHUB_SEARCH_PULL_REQUESTS_TOOL_NAME}:
2942
+ - Search for problem descriptions, not solutions
2943
+ - Use state filters progressively
2944
+ - Labels reveal project categorization
2945
+
2946
+ ### ${NPM_PACKAGE_SEARCH_TOOL_NAME}:
2947
+ - Use functional terms: "router", "validator", "parser"
2948
+ - Search multiple related terms in parallel
2949
+ - Aggregate results for comprehensive view
2950
+
2951
+ ### ${NPM_VIEW_PACKAGE_TOOL_NAME}:
2952
+ - Check repository field for source code access
2953
+ - Review exports for API understanding
2954
+ - Use version history to gauge stability
2955
+
2956
+ ## 10. CHAIN OF THOUGHT OPTIMIZATION:
2957
+ - Plan search sequence before executing
2958
+ - Document reasoning for each search refinement
2959
+ - Build knowledge progressively, don't jump to specifics
2960
+ - Validate findings with multiple sources
2961
+ `;
2962
+
3205
2963
  const SERVER_CONFIG = {
3206
2964
  name: 'octocode-mcp',
3207
2965
  version: '1.0.0',
3208
- description: `Comprehensive code analysis assistant: Deep exploration and understanding of complex implementations in GitHub repositories and npm packages.
3209
- Specialized in architectural analysis, algorithm explanations, and complete technical documentation.`,
2966
+ description: PROMPT_SYSTEM_PROMPT,
3210
2967
  };
3211
2968
  function registerAllTools(server) {
3212
2969
  const toolRegistrations = [
3213
- { name: 'ApiStatusCheck', fn: registerApiStatusCheckTool },
3214
- { name: 'GitHubSearchCode', fn: registerGitHubSearchCodeTool },
2970
+ { name: API_STATUS_CHECK_TOOL_NAME, fn: registerApiStatusCheckTool },
2971
+ { name: GITHUB_SEARCH_CODE_TOOL_NAME, fn: registerGitHubSearchCodeTool },
3215
2972
  {
3216
- name: 'FetchGitHubFileContent',
2973
+ name: GITHUB_GET_FILE_CONTENT_TOOL_NAME,
3217
2974
  fn: registerFetchGitHubFileContentTool,
3218
2975
  },
3219
- { name: 'SearchGitHubRepos', fn: registerSearchGitHubReposTool },
3220
- { name: 'SearchGitHubCommits', fn: registerGitHubSearchCommitsTool },
3221
2976
  {
3222
- name: 'SearchGitHubPullRequests',
2977
+ name: GITHUB_SEARCH_REPOSITORIES_TOOL_NAME,
2978
+ fn: registerSearchGitHubReposTool,
2979
+ },
2980
+ {
2981
+ name: GITHUB_SEARCH_COMMITS_TOOL_NAME,
2982
+ fn: registerGitHubSearchCommitsTool,
2983
+ },
2984
+ {
2985
+ name: GITHUB_SEARCH_PULL_REQUESTS_TOOL_NAME,
3223
2986
  fn: registerSearchGitHubPullRequestsTool,
3224
2987
  },
3225
- { name: 'NpmSearch', fn: registerNpmSearchTool },
2988
+ { name: NPM_PACKAGE_SEARCH_TOOL_NAME, fn: registerNpmSearchTool },
3226
2989
  {
3227
- name: 'ViewRepositoryStructure',
2990
+ name: GITHUB_VIEW_REPO_STRUCTURE_TOOL_NAME,
3228
2991
  fn: registerViewRepositoryStructureTool,
3229
2992
  },
3230
- { name: 'SearchGitHubIssues', fn: registerSearchGitHubIssuesTool },
3231
- { name: 'NpmViewPackage', fn: registerNpmViewPackageTool },
2993
+ {
2994
+ name: GITHUB_SEARCH_ISSUES_TOOL_NAME,
2995
+ fn: registerSearchGitHubIssuesTool,
2996
+ },
2997
+ { name: NPM_VIEW_PACKAGE_TOOL_NAME, fn: registerNpmViewPackageTool },
3232
2998
  ];
2999
+ logger.info(`Registering ${toolRegistrations.length} tools...`);
3000
+ let successCount = 0;
3233
3001
  for (const tool of toolRegistrations) {
3234
3002
  try {
3003
+ logger.debug(`Registering tool: ${tool.name}`);
3235
3004
  tool.fn(server);
3005
+ successCount++;
3006
+ logger.info(`✓ Successfully registered: ${tool.name}`);
3236
3007
  }
3237
3008
  catch (error) {
3238
- // ignore
3009
+ logger.error(`✗ Failed to register ${tool.name}:`, error);
3010
+ // Continue with other tools instead of failing completely
3239
3011
  }
3240
3012
  }
3013
+ if (successCount === 0) {
3014
+ throw new Error('No tools were successfully registered');
3015
+ }
3016
+ logger.info(`All tools registration completed - ${successCount}/${toolRegistrations.length} successful`);
3241
3017
  }
3242
3018
  async function startServer() {
3243
3019
  try {
3244
- const server = new McpServer(SERVER_CONFIG, {
3245
- capabilities: {
3246
- tools: {},
3247
- resources: {},
3248
- prompts: {},
3249
- },
3250
- instructions: `
3251
- ${PROMPT_SYSTEM_PROMPT}
3252
- `,
3253
- });
3020
+ logger.info('Creating MCP server...');
3021
+ const server = new McpServer(SERVER_CONFIG);
3254
3022
  registerAllTools(server);
3255
3023
  const transport = new StdioServerTransport();
3256
3024
  await server.connect(transport);
3257
- const gracefulShutdown = async (_signal) => {
3025
+ logger.info('=== Server Connected Successfully ===');
3026
+ // Ensure all buffered output is sent
3027
+ process.stdout.uncork();
3028
+ process.stderr.uncork();
3029
+ const gracefulShutdown = async (signal) => {
3258
3030
  try {
3259
- await server.close();
3031
+ logger.info(`Received ${signal}, shutting down gracefully...`);
3032
+ clearAllCache();
3033
+ // Give server time to close properly
3034
+ await Promise.race([
3035
+ server.close(),
3036
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Server close timeout')), 5000)),
3037
+ ]);
3260
3038
  process.exit(0);
3261
3039
  }
3262
3040
  catch (error) {
3041
+ logger.error('Error during shutdown:', error);
3263
3042
  process.exit(1);
3264
3043
  }
3265
3044
  };
3045
+ // Handle process signals
3266
3046
  process.on('SIGINT', () => gracefulShutdown('SIGINT'));
3267
3047
  process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
3048
+ // Handle stdin close (important for MCP)
3268
3049
  process.stdin.on('close', async () => {
3269
3050
  await gracefulShutdown('STDIN_CLOSE');
3270
3051
  });
3271
- process.on('uncaughtException', () => {
3052
+ // Handle uncaught errors
3053
+ process.on('uncaughtException', error => {
3054
+ logger.error('Uncaught exception:', error);
3272
3055
  gracefulShutdown('UNCAUGHT_EXCEPTION');
3273
3056
  });
3274
- process.on('unhandledRejection', () => {
3057
+ process.on('unhandledRejection', (reason, promise) => {
3058
+ logger.error('Unhandled rejection at:', promise, 'reason:', reason);
3275
3059
  gracefulShutdown('UNHANDLED_REJECTION');
3276
3060
  });
3061
+ // Keep process alive
3062
+ process.stdin.resume();
3277
3063
  }
3278
3064
  catch (error) {
3065
+ logger.error('Error details:', error);
3279
3066
  process.exit(1);
3280
3067
  }
3281
3068
  }
3282
- startServer().catch(() => {
3069
+ startServer().catch(error => {
3070
+ logger.error('Error:', error);
3283
3071
  process.exit(1);
3284
3072
  });