octocode-mcp 2.3.6 → 2.3.7

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 +483 -232
  2. package/package.json +1 -1
package/build/index.js CHANGED
@@ -107,9 +107,9 @@ function escapeShellArg(arg, shellType, isGitHubQuery // Flag to indicate if thi
107
107
  const isWindows = platform() === 'win32';
108
108
  shellType = isWindows ? 'cmd' : 'unix';
109
109
  }
110
- // Special handling for GitHub search queries
110
+ // Special handling for GitHub search queries to preserve AND logic
111
111
  if (isGitHubQuery) {
112
- // If the argument already contains quotes, preserve them
112
+ // If the argument already contains quotes, preserve them for exact phrases
113
113
  if (arg.includes('"')) {
114
114
  // For Unix-like shells, wrap the entire argument in single quotes
115
115
  if (shellType === 'unix') {
@@ -122,6 +122,13 @@ function escapeShellArg(arg, shellType, isGitHubQuery // Flag to indicate if thi
122
122
  // For PowerShell
123
123
  return `'${arg.replace(/'/g, "''")}'`;
124
124
  }
125
+ // For space-separated terms (AND search), minimize escaping
126
+ if (arg.includes(' ') && shellType === 'unix') {
127
+ // Only escape if contains dangerous shell characters
128
+ if (!/[;&|<>$`\\]/.test(arg)) {
129
+ return `"${arg}"`;
130
+ }
131
+ }
125
132
  }
126
133
  switch (shellType) {
127
134
  case 'powershell':
@@ -158,16 +165,28 @@ function escapeWindowsCmdArg(arg) {
158
165
  }
159
166
  /**
160
167
  * Escape arguments for Unix shells with special handling for GitHub CLI queries
168
+ * Preserves AND search logic by not over-escaping space-separated terms
161
169
  */
162
170
  function escapeUnixShellArg(arg, isGitHubQuery) {
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
171
+ // For GitHub search queries, we need to preserve AND logic
172
+ if (isGitHubQuery) {
173
+ // If the query contains quotes, preserve them for exact phrase matching
166
174
  if (arg.includes('"')) {
175
+ // Use single quotes to wrap the entire query while preserving internal quotes
176
+ return `'${arg.replace(/'/g, "'\"'\"'")}'`;
177
+ }
178
+ // For space-separated terms (AND search), only escape if absolutely necessary
179
+ // GitHub CLI expects space-separated terms for AND logic
180
+ if (arg.includes(' ') && !/[;&|<>$`\\]/.test(arg)) {
181
+ // Only wrap in quotes if it contains shell metacharacters beyond spaces
182
+ return `"${arg}"`;
183
+ }
184
+ // For single terms or terms with special chars, escape normally
185
+ if (/[;&|<>$`\\]/.test(arg)) {
167
186
  return `'${arg.replace(/'/g, "'\"'\"'")}'`;
168
187
  }
169
- // Add double quotes for terms that need them
170
- return `"${arg}"`;
188
+ // Simple terms don't need escaping
189
+ return arg;
171
190
  }
172
191
  // Standard Unix shell escaping for other arguments
173
192
  if (/[^a-zA-Z0-9\-_./=@:]/.test(arg)) {
@@ -313,61 +332,6 @@ async function executeCommand(fullCommand, type, options = {}, shellConfig) {
313
332
  }
314
333
  }
315
334
 
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
-
371
335
  function createResult(options) {
372
336
  const { data, error } = options;
373
337
  if (error) {
@@ -387,7 +351,6 @@ function createResult(options) {
387
351
  };
388
352
  }
389
353
  catch (jsonError) {
390
- logger.error('JSON serialization failed:', jsonError);
391
354
  return {
392
355
  content: [
393
356
  {
@@ -783,11 +746,13 @@ async function viewRepositoryStructure(params) {
783
746
  let usedBranch = branch;
784
747
  let lastError = null;
785
748
  let attemptCount = 0;
786
- const maxAttempts = 3; // Prevent infinite loops
749
+ const maxAttempts = branchesToTry.length;
750
+ const triedBranches = [];
787
751
  for (const tryBranch of branchesToTry) {
788
752
  if (attemptCount >= maxAttempts)
789
753
  break;
790
754
  attemptCount++;
755
+ triedBranches.push(tryBranch);
791
756
  try {
792
757
  const apiPath = `/repos/${owner}/${repo}/contents/${cleanPath}?ref=${tryBranch}`;
793
758
  const result = await executeGitHubCommand('api', [apiPath], {
@@ -806,33 +771,52 @@ async function viewRepositoryStructure(params) {
806
771
  }
807
772
  catch (error) {
808
773
  lastError = error instanceof Error ? error : new Error(String(error));
809
- // Try next branch
810
774
  continue;
811
775
  }
812
776
  }
813
777
  if (items.length === 0) {
814
- // Use the most descriptive error message
778
+ // Check repository existence only after content fetch fails
779
+ const repoCheckResult = await executeGitHubCommand('api', [`/repos/${owner}/${repo}`], {
780
+ cache: false,
781
+ });
782
+ if (repoCheckResult.isError) {
783
+ const repoErrorMsg = repoCheckResult.content[0].text;
784
+ if (repoErrorMsg.includes('404')) {
785
+ return createResult({
786
+ error: `Repository "${owner}/${repo}" not found. It might have been deleted, renamed, or made private. Use github_search_code to find current location.`,
787
+ });
788
+ }
789
+ else if (repoErrorMsg.includes('403')) {
790
+ return createResult({
791
+ error: `Repository "${owner}/${repo}" exists but access is denied. Repository might be private or archived. Use api_status_check to verify permissions.`,
792
+ });
793
+ }
794
+ }
815
795
  const errorMsg = lastError?.message || 'Unknown error';
816
796
  if (errorMsg.includes('404') || errorMsg.includes('Not Found')) {
817
797
  if (path) {
798
+ const searchSuggestion = await suggestPathSearchFallback(owner, path);
818
799
  return createResult({
819
- error: `Path "${path}" not found. Verify the path or use github_search_code to find files`,
800
+ error: `Path "${path}" not found in any branch (tried: ${triedBranches.join(', ')}).${searchSuggestion}`,
820
801
  });
821
802
  }
822
803
  else {
823
804
  return createResult({
824
- error: `Repository not found: ${owner}/${repo}. Check spelling and case sensitivity`,
805
+ error: `Repository "${owner}/${repo}" structure not accessible. Repository might be empty, private, or you might not have sufficient permissions. Use github_search_code with owner="${owner}" to find accessible repositories.`,
825
806
  });
826
807
  }
827
808
  }
828
809
  else if (errorMsg.includes('403') || errorMsg.includes('Forbidden')) {
829
810
  return createResult({
830
- error: `Access denied to ${owner}/${repo}. Repository may be private - use api_status_check`,
811
+ error: `Access denied to "${owner}/${repo}". Repository exists but might be private/archived. Use api_status_check to verify permissions, or github_search_code with owner="${owner}" to find accessible repositories.`,
831
812
  });
832
813
  }
833
814
  else {
815
+ const searchSuggestion = path
816
+ ? await suggestPathSearchFallback(owner, path)
817
+ : '';
834
818
  return createResult({
835
- error: `Failed to access ${owner}/${repo}. Check network connection`,
819
+ error: `Failed to access "${owner}/${repo}": ${errorMsg}. Check network connection and repository permissions.${searchSuggestion}`,
836
820
  });
837
821
  }
838
822
  }
@@ -877,17 +861,19 @@ async function viewRepositoryStructure(params) {
877
861
  });
878
862
  }
879
863
  catch (error) {
864
+ const errorMessage = error instanceof Error ? error.message : String(error);
880
865
  return createResult({
881
- error: `Failed to access repository. ${error}. Verify repository name and authentication`,
866
+ error: `Failed to access repository "${owner}/${repo}": ${errorMessage}. Verify repository name, permissions, and network connection.`,
882
867
  });
883
868
  }
884
869
  });
885
870
  }
886
871
  /**
887
872
  * Smart branch detection with automatic fallback to common branch names.
873
+ * Now includes more comprehensive branch detection and better error handling.
888
874
  */
889
875
  async function getSmartBranchFallback(owner, repo, requestedBranch) {
890
- const branches = [requestedBranch];
876
+ const branches = new Set([requestedBranch]);
891
877
  try {
892
878
  // Try to get repository info to find default branch
893
879
  const repoInfoResult = await executeGitHubCommand('api', [`/repos/${owner}/${repo}`], {
@@ -897,22 +883,48 @@ async function getSmartBranchFallback(owner, repo, requestedBranch) {
897
883
  const execResult = JSON.parse(repoInfoResult.content[0].text);
898
884
  const repoData = execResult.result;
899
885
  const defaultBranch = repoData.default_branch;
900
- if (defaultBranch && !branches.includes(defaultBranch)) {
901
- branches.push(defaultBranch);
886
+ if (defaultBranch) {
887
+ branches.add(defaultBranch);
902
888
  }
903
889
  }
904
890
  }
905
891
  catch {
906
892
  // If we can't get repo info, proceed with standard fallbacks
907
893
  }
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);
913
- }
894
+ // Add only main/master as fallback branches
895
+ const commonBranches = ['main', 'master'];
896
+ commonBranches.forEach(branch => branches.add(branch));
897
+ // Convert Set back to array, with requested branch first
898
+ const branchesArray = Array.from(branches);
899
+ branchesArray.sort((a, b) => {
900
+ if (a === requestedBranch)
901
+ return -1;
902
+ if (b === requestedBranch)
903
+ return 1;
904
+ return 0;
914
905
  });
915
- return branches;
906
+ return branchesArray;
907
+ }
908
+ // Helper function to suggest path search strategy
909
+ async function suggestPathSearchFallback(owner, path) {
910
+ try {
911
+ // Extract last path segment and try to find in same organization
912
+ const pathSegment = path.split('/').pop() || path;
913
+ const searchResult = await executeGitHubCommand('api', [
914
+ `/search/code?q=${encodeURIComponent(pathSegment)}+in:path+org:${owner}`,
915
+ ], { cache: false });
916
+ if (!searchResult.isError) {
917
+ const results = JSON.parse(searchResult.content[0].text);
918
+ if (results.total_count > 0) {
919
+ const firstMatch = results.items[0];
920
+ return ` Directory might be in ${firstMatch.repository.full_name}. Try these searches:\n1. github_search_code with query="${pathSegment}" owner="${owner}"\n2. github_search_code with query="path:${path}" owner="${owner}"`;
921
+ }
922
+ }
923
+ }
924
+ catch {
925
+ // Fallback to generic message if search fails
926
+ }
927
+ return ` Try these searches:\n1. github_search_code with query="${path.split('/').pop()}" owner="${owner}"\n2. github_search_code with query="path:${path}"`;
916
928
  }
917
929
 
918
930
  const GITHUB_GET_FILE_CONTENT_TOOL_NAME = 'githubGetFileContent';
@@ -969,18 +981,39 @@ async function fetchGitHubFileContent(params) {
969
981
  const { owner, repo, branch, filePath } = params;
970
982
  try {
971
983
  // Try to fetch file content directly
972
- const apiPath = `/repos/${owner}/${repo}/contents/${filePath}?ref=${branch}`;
984
+ const apiPath = `/repos/${owner}/${repo}/contents/${filePath}`;
973
985
  const result = await executeGitHubCommand('api', [apiPath], {
974
986
  cache: false,
975
987
  });
976
988
  if (result.isError) {
977
989
  const errorMsg = result.content[0].text;
990
+ // Check repository existence only after content fetch fails
991
+ const repoCheckResult = await executeGitHubCommand('api', [`/repos/${owner}/${repo}`], {
992
+ cache: false,
993
+ });
994
+ if (repoCheckResult.isError) {
995
+ const repoErrorMsg = repoCheckResult.content[0].text;
996
+ if (repoErrorMsg.includes('404')) {
997
+ return createResult({
998
+ error: `Repository "${owner}/${repo}" not found. It might have been deleted, renamed, or made private. Use github_search_code to find current location.`,
999
+ });
1000
+ }
1001
+ else if (repoErrorMsg.includes('403')) {
1002
+ return createResult({
1003
+ error: `Repository "${owner}/${repo}" exists but access is denied. Repository might be private or archived. Use api_status_check to verify permissions.`,
1004
+ });
1005
+ }
1006
+ }
978
1007
  // Try fallback branches if original branch fails
979
1008
  if (errorMsg.includes('404') &&
980
1009
  branch !== 'main' &&
981
1010
  branch !== 'master') {
982
1011
  const fallbackBranches = ['main', 'master'];
1012
+ const triedBranches = [branch];
983
1013
  for (const fallbackBranch of fallbackBranches) {
1014
+ if (triedBranches.includes(fallbackBranch))
1015
+ continue;
1016
+ triedBranches.push(fallbackBranch);
984
1017
  const fallbackPath = `/repos/${owner}/${repo}/contents/${filePath}?ref=${fallbackBranch}`;
985
1018
  const fallbackResult = await executeGitHubCommand('api', [fallbackPath], {
986
1019
  cache: false,
@@ -989,27 +1022,32 @@ async function fetchGitHubFileContent(params) {
989
1022
  return await processFileContent(fallbackResult, owner, repo, fallbackBranch, filePath);
990
1023
  }
991
1024
  }
1025
+ return createResult({
1026
+ error: `File not found in any common branches (tried: ${triedBranches.join(', ')}). File might have been moved or deleted. Use github_search_code to find current location.`,
1027
+ });
992
1028
  }
993
- // Handle common errors
1029
+ // Handle common errors with more context
994
1030
  if (errorMsg.includes('404')) {
1031
+ const searchSuggestion = await suggestCodeSearchFallback(owner, filePath);
995
1032
  return createResult({
996
- error: 'File not found. Use github_view_repo_structure to explore repository structure',
1033
+ error: `File "${filePath}" not found in branch "${branch}". Use github_view_repo_structure to verify path.${searchSuggestion}`,
997
1034
  });
998
1035
  }
999
1036
  else if (errorMsg.includes('403')) {
1000
1037
  return createResult({
1001
- error: 'Access denied. Repository may be private - use apiStatusCheck to verify',
1038
+ error: `Access denied to "${filePath}" in "${owner}/${repo}". Repository or file might be private/archived. Use api_status_check to verify permissions, or github_search_code with query="path:${filePath}" owner="${owner}" to find in accessible repositories.`,
1002
1039
  });
1003
1040
  }
1004
1041
  else if (errorMsg.includes('maxBuffer') ||
1005
1042
  errorMsg.includes('stdout maxBuffer length exceeded')) {
1006
1043
  return createResult({
1007
- error: 'File too large (>300KB). Use githubSearchCode to search for patterns within the file',
1044
+ error: `File "${filePath}" is too large (>300KB). Use github_search_code with query="path:${filePath}" to search within the file or download directly from GitHub.`,
1008
1045
  });
1009
1046
  }
1010
1047
  else {
1048
+ const searchSuggestion = await suggestCodeSearchFallback(owner, filePath);
1011
1049
  return createResult({
1012
- error: 'Failed to fetch file. Verify repository name and file path',
1050
+ error: `Failed to fetch "${filePath}". Error: ${errorMsg}. Verify repository name, branch, and file path.${searchSuggestion}`,
1013
1051
  });
1014
1052
  }
1015
1053
  }
@@ -1017,15 +1055,14 @@ async function fetchGitHubFileContent(params) {
1017
1055
  }
1018
1056
  catch (error) {
1019
1057
  const errorMessage = error.message;
1020
- // Handle maxBuffer errors that escape the main try-catch
1021
1058
  if (errorMessage.includes('maxBuffer') ||
1022
1059
  errorMessage.includes('stdout maxBuffer length exceeded')) {
1023
1060
  return createResult({
1024
- error: 'File too large (>300KB). Use github_search_code to search for patterns within the file',
1061
+ error: `File "${filePath}" is too large (>300KB). Use github_search_code to search within the file or download directly from GitHub.`,
1025
1062
  });
1026
1063
  }
1027
1064
  return createResult({
1028
- error: 'Unexpected error. Check network connection and try again',
1065
+ error: `Unexpected error fetching "${filePath}": ${errorMessage}. Check network connection and try again.`,
1029
1066
  });
1030
1067
  }
1031
1068
  });
@@ -1088,9 +1125,39 @@ async function processFileContent(result, owner, repo, branch, filePath) {
1088
1125
  },
1089
1126
  });
1090
1127
  }
1128
+ // Helper function to suggest code search strategy
1129
+ async function suggestCodeSearchFallback(owner, filePath) {
1130
+ try {
1131
+ // Extract filename and try to find in same organization
1132
+ const fileName = filePath.split('/').pop() || filePath;
1133
+ const searchResult = await executeGitHubCommand('api', [`/search/code?q=${encodeURIComponent(fileName)}+in:path+org:${owner}`], { cache: false });
1134
+ if (!searchResult.isError) {
1135
+ const results = JSON.parse(searchResult.content[0].text);
1136
+ if (results.total_count > 0) {
1137
+ const firstMatch = results.items[0];
1138
+ return ` File might be in ${firstMatch.repository.full_name}. Try these searches:\n1. github_search_code with query="${fileName}" owner="${owner}"\n2. github_search_code with query="path:${filePath}" owner="${owner}"`;
1139
+ }
1140
+ }
1141
+ }
1142
+ catch {
1143
+ // Fallback to generic message if search fails
1144
+ }
1145
+ return ` Try these searches:\n1. github_search_code with query="${filePath.split('/').pop()}" owner="${owner}"\n2. github_search_code with query="path:${filePath}"`;
1146
+ }
1091
1147
 
1092
1148
  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).`;
1149
+ const DESCRIPTION$6 = `Search code across GitHub repositories using GitHub's code search API.
1150
+
1151
+ Search Syntax (all terms must be present with AND logic):
1152
+ - Multiple words: react lifecycle → finds files with BOTH "react" AND "lifecycle" (separate words)
1153
+ - Exact phrases: "error handling" → finds exact phrase "error handling" (quoted phrase)
1154
+ - Mixed: "async function" timeout → finds exact phrase "async function" AND word "timeout"
1155
+ - Complex: useState useEffect "custom hook" → finds "useState" AND "useEffect" AND exact phrase "custom hook"
1156
+ - Advanced: authentication authorization "jwt token" → finds "authentication" AND "authorization" AND exact phrase "jwt token"
1157
+ - Framework: "React.Component" "componentDidMount" → finds both exact phrases in same file
1158
+
1159
+ Key difference: Quotes create exact phrases, no quotes = individual words (all must be present).
1160
+ Start with 1-2 terms, add more to narrow results. Use filters for precision.`;
1094
1161
  function registerGitHubSearchCodeTool(server) {
1095
1162
  server.registerTool(GITHUB_SEARCH_CODE_TOOL_NAME, {
1096
1163
  description: DESCRIPTION$6,
@@ -1098,31 +1165,36 @@ function registerGitHubSearchCodeTool(server) {
1098
1165
  query: z
1099
1166
  .string()
1100
1167
  .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.'),
1168
+ .describe('Search query with AND logic between terms. Multiple words require ALL to be present. Use quotes for exact phrases. Examples: react lifecycle (both words), "error handling" (exact phrase), async await "try catch" (words + phrase), "function definition" variable scope (phrase + words)'),
1102
1169
  language: z
1103
1170
  .string()
1104
1171
  .optional()
1105
- .describe('Language filter (javascript, python, etc). Use only when needed.'),
1172
+ .describe('Programming language filter (javascript, python, typescript, go, etc). Narrows search to specific language files. Use for language-specific searches.'),
1106
1173
  owner: z
1107
1174
  .union([z.string(), z.array(z.string())])
1108
1175
  .optional()
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.'),
1176
+ .describe('Repository owner or organization name(s) to search within. Format: "microsoft" or ["facebook", "google"]. Drastically narrows search scope. Do NOT use owner/repo format.'),
1177
+ repo: z
1178
+ .union([z.string(), z.array(z.string())])
1179
+ .optional()
1180
+ .describe('Filter on specific repository(ies). Format: "owner/repo" or ["microsoft/vscode", "facebook/react"]. Use for repository-specific searches.'),
1110
1181
  filename: z
1111
1182
  .string()
1112
1183
  .optional()
1113
- .describe('Specific filename to search. Use for targeted searches.'),
1184
+ .describe('Target specific filename or pattern. Examples: "package.json", "*.test.js", "Dockerfile". Use for file-specific searches.'),
1114
1185
  extension: z
1115
1186
  .string()
1116
1187
  .optional()
1117
- .describe('File extension (.js, .py, etc). Alternative to language filter.'),
1188
+ .describe('File extension filter (js, py, ts, go, etc). Alternative to language parameter. Examples: "js", "py", "tsx".'),
1118
1189
  match: z
1119
1190
  .union([z.enum(['file', 'path']), z.array(z.enum(['file', 'path']))])
1120
1191
  .optional()
1121
- .describe('Search scope: "file" for content, "path" for filenames. Default: file content.'),
1192
+ .describe('Search scope: "file" for file content (default), "path" for filenames/paths, or ["file", "path"] for both. Controls where to search for terms.'),
1122
1193
  size: z
1123
1194
  .string()
1195
+ .regex(/^(>=?\d+|<=?\d+|\d+\.\.\d+|\d+)$/, 'Invalid size format. Use: ">10", ">=5", "<100", "<=50", "10..100", or exact number "50"')
1124
1196
  .optional()
1125
- .describe('File size in KB. Format: >10, <100, or 10..50'),
1197
+ .describe('File size filter in KB. Format: ">10" (larger than), ">=5" (at least), "<100" (smaller than), "<=50" (at most), "10..50" (range), "25" (exact). Examples from docs: ">10", "10..50", "<100"'),
1126
1198
  limit: z
1127
1199
  .number()
1128
1200
  .int()
@@ -1130,7 +1202,7 @@ function registerGitHubSearchCodeTool(server) {
1130
1202
  .max(100)
1131
1203
  .optional()
1132
1204
  .default(30)
1133
- .describe('Results limit (1-100). Default: 30'),
1205
+ .describe('Maximum number of results to return (1-100). Default: 30. Higher values may increase response time.'),
1134
1206
  },
1135
1207
  annotations: {
1136
1208
  title: 'GitHub Code Search - Smart & Efficient',
@@ -1262,32 +1334,18 @@ function extractSingleRepository$1(items) {
1262
1334
  }
1263
1335
  /**
1264
1336
  * 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`).
1337
+ * Preserves quoted phrases and supports both AND search and exact phrase matching.
1270
1338
  */
1271
1339
  function buildGitHubCliArgs(params) {
1272
1340
  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 => {
1341
+ // Parse query to preserve quoted phrases and extract qualifiers
1342
+ const { searchQuery, extractedQualifiers } = parseSearchQuery(params.query);
1343
+ // Add the main search query if present
1344
+ if (searchQuery) {
1345
+ args.push(searchQuery);
1346
+ }
1347
+ // Add extracted qualifiers from the query
1348
+ extractedQualifiers.forEach(qualifier => {
1291
1349
  args.push(qualifier);
1292
1350
  });
1293
1351
  // Add explicit parameters as qualifiers
@@ -1300,6 +1358,10 @@ function buildGitHubCliArgs(params) {
1300
1358
  const owners = Array.isArray(params.owner) ? params.owner : [params.owner];
1301
1359
  owners.forEach(owner => args.push(`org:${owner}`));
1302
1360
  }
1361
+ if (params.repo && !params.query.includes('repo:')) {
1362
+ const repos = Array.isArray(params.repo) ? params.repo : [params.repo];
1363
+ repos.forEach(repo => args.push(`repo:${repo}`));
1364
+ }
1303
1365
  if (params.filename && !params.query.includes('filename:')) {
1304
1366
  args.push(`filename:${params.filename}`);
1305
1367
  }
@@ -1382,6 +1444,42 @@ function validateSearchParameters(params) {
1382
1444
  // }
1383
1445
  return null; // No validation errors
1384
1446
  }
1447
+ /**
1448
+ * Parse search query to preserve quoted phrases and extract qualifiers.
1449
+ * Handles:
1450
+ * - Quoted phrases: "error handling" -> kept as single unit
1451
+ * - Multiple terms: react lifecycle -> both terms for AND search
1452
+ * - Mixed: "error handling" debug -> phrase + term
1453
+ * - Qualifiers: language:javascript -> extracted separately
1454
+ */
1455
+ function parseSearchQuery(query) {
1456
+ const qualifiers = [];
1457
+ const searchTerms = [];
1458
+ // Regular expression to match quoted strings or individual words/qualifiers
1459
+ const tokenRegex = /"([^"]+)"|([^\s]+)/g;
1460
+ let match;
1461
+ while ((match = tokenRegex.exec(query)) !== null) {
1462
+ const token = match[1] || match[2]; // match[1] is quoted content, match[2] is unquoted
1463
+ // Check if it's a qualifier (contains : but not inside quotes)
1464
+ if (!match[1] && token.includes(':') && /^[a-zA-Z]+:/.test(token)) {
1465
+ qualifiers.push(token);
1466
+ }
1467
+ else {
1468
+ // It's a search term (either quoted or unquoted)
1469
+ if (match[1]) {
1470
+ // Preserve quotes for exact phrase search
1471
+ searchTerms.push(`"${token}"`);
1472
+ }
1473
+ else {
1474
+ searchTerms.push(token);
1475
+ }
1476
+ }
1477
+ }
1478
+ return {
1479
+ searchQuery: searchTerms.join(' '),
1480
+ extractedQualifiers: qualifiers,
1481
+ };
1482
+ }
1385
1483
 
1386
1484
  const GITHUB_SEARCH_COMMITS_TOOL_NAME = 'githubSearchCommits';
1387
1485
  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).`;
@@ -1407,24 +1505,30 @@ function registerGitHubSearchCommitsTool(server) {
1407
1505
  .string()
1408
1506
  .optional()
1409
1507
  .describe('GitHub username of commit author'),
1410
- authorName: z
1508
+ 'author-name': z
1411
1509
  .string()
1412
1510
  .optional()
1413
1511
  .describe('Full name of commit author'),
1414
- authorEmail: z.string().optional().describe('Email of commit author'),
1512
+ 'author-email': z
1513
+ .string()
1514
+ .optional()
1515
+ .describe('Email of commit author'),
1415
1516
  // Committer filters
1416
1517
  committer: z
1417
1518
  .string()
1418
1519
  .optional()
1419
1520
  .describe('GitHub username of committer'),
1420
- committerName: z.string().optional().describe('Full name of committer'),
1421
- committerEmail: z.string().optional().describe('Email of committer'),
1521
+ 'committer-name': z
1522
+ .string()
1523
+ .optional()
1524
+ .describe('Full name of committer'),
1525
+ 'committer-email': z.string().optional().describe('Email of committer'),
1422
1526
  // Date filters
1423
- authorDate: z
1527
+ 'author-date': z
1424
1528
  .string()
1425
1529
  .optional()
1426
1530
  .describe('When authored. Format: >2020-01-01, <2023-12-31, 2020-01-01..2023-12-31'),
1427
- committerDate: z
1531
+ 'committer-date': z
1428
1532
  .string()
1429
1533
  .optional()
1430
1534
  .describe('When committed. Format: >2020-01-01, <2023-12-31, 2020-01-01..2023-12-31'),
@@ -1603,22 +1707,22 @@ function buildGitHubCommitCliArgs(params) {
1603
1707
  // Author filters
1604
1708
  if (params.author)
1605
1709
  args.push(`--author=${params.author}`);
1606
- if (params.authorName)
1607
- args.push(`--author-name=${params.authorName}`);
1608
- if (params.authorEmail)
1609
- args.push(`--author-email=${params.authorEmail}`);
1710
+ if (params['author-name'])
1711
+ args.push(`--author-name=${params['author-name']}`);
1712
+ if (params['author-email'])
1713
+ args.push(`--author-email=${params['author-email']}`);
1610
1714
  // Committer filters
1611
1715
  if (params.committer)
1612
1716
  args.push(`--committer=${params.committer}`);
1613
- if (params.committerName)
1614
- args.push(`--committer-name=${params.committerName}`);
1615
- if (params.committerEmail)
1616
- args.push(`--committer-email=${params.committerEmail}`);
1717
+ if (params['committer-name'])
1718
+ args.push(`--committer-name=${params['committer-name']}`);
1719
+ if (params['committer-email'])
1720
+ args.push(`--committer-email=${params['committer-email']}`);
1617
1721
  // Date filters
1618
- if (params.authorDate)
1619
- args.push(`--author-date=${params.authorDate}`);
1620
- if (params.committerDate)
1621
- args.push(`--committer-date=${params.committerDate}`);
1722
+ if (params['author-date'])
1723
+ args.push(`--author-date=${params['author-date']}`);
1724
+ if (params['committer-date'])
1725
+ args.push(`--committer-date=${params['committer-date']}`);
1622
1726
  // Hash filters
1623
1727
  if (params.hash)
1624
1728
  args.push(`--hash=${params.hash}`);
@@ -1628,7 +1732,7 @@ function buildGitHubCommitCliArgs(params) {
1628
1732
  args.push(`--tree=${params.tree}`);
1629
1733
  // State filters
1630
1734
  if (params.merge !== undefined)
1631
- args.push(`--merge=${params.merge}`);
1735
+ args.push(`--merge`);
1632
1736
  // Visibility
1633
1737
  if (params.visibility)
1634
1738
  args.push(`--visibility=${params.visibility}`);
@@ -1678,33 +1782,45 @@ function registerSearchGitHubIssuesTool(server) {
1678
1782
  .describe('GitHub username of issue creator'),
1679
1783
  closed: z
1680
1784
  .string()
1785
+ .regex(/^(>=?\d{4}-\d{2}-\d{2}|<=?\d{4}-\d{2}-\d{2}|\d{4}-\d{2}-\d{2}\.\.\d{4}-\d{2}-\d{2}|\d{4}-\d{2}-\d{2})$/, 'Invalid date format. Use: ">2020-01-01", ">=2020-01-01", "<2023-12-31", "<=2023-12-31", "2020-01-01..2023-12-31", or exact date "2023-01-01"')
1681
1786
  .optional()
1682
- .describe('When closed. Format: >2020-01-01'),
1787
+ .describe('When closed. Format: ">2020-01-01" (after), ">=2020-01-01" (on or after), "<2023-12-31" (before), "2020-01-01..2023-12-31" (range)'),
1683
1788
  commenter: z
1684
1789
  .string()
1685
1790
  .optional()
1686
1791
  .describe('User who commented on issue'),
1687
1792
  comments: z
1688
- .number()
1793
+ .union([
1794
+ z.number().int().min(0),
1795
+ z
1796
+ .string()
1797
+ .regex(/^(>=?\d+|<=?\d+|\d+\.\.\d+|\d+)$/, 'Invalid format. Use: ">10", ">=5", "<20", "<=15", "5..20", or exact number "10"'),
1798
+ ])
1689
1799
  .optional()
1690
- .describe('Comment count. Format: >10, <5, 5..10'),
1800
+ .describe('Comment count filter. Format: ">10" (many comments), ">=5" (at least 5), "<5" (few comments), "5..10" (range), "10" (exact)'),
1691
1801
  created: z
1692
1802
  .string()
1803
+ .regex(/^(>=?\d{4}-\d{2}-\d{2}|<=?\d{4}-\d{2}-\d{2}|\d{4}-\d{2}-\d{2}\.\.\d{4}-\d{2}-\d{2}|\d{4}-\d{2}-\d{2})$/, 'Invalid date format. Use: ">2020-01-01", ">=2020-01-01", "<2023-12-31", "<=2023-12-31", "2020-01-01..2023-12-31", or exact date "2023-01-01"')
1693
1804
  .optional()
1694
- .describe('When created. Format: >2020-01-01'),
1695
- includePrs: z
1805
+ .describe('When created. Format: ">2020-01-01" (after), ">=2020-01-01" (on or after), "<2023-12-31" (before), "2020-01-01..2023-12-31" (range)'),
1806
+ 'include-prs': z
1696
1807
  .boolean()
1697
1808
  .optional()
1698
1809
  .describe('Include pull requests. Default: false'),
1699
1810
  interactions: z
1700
- .number()
1811
+ .union([
1812
+ z.number().int().min(0),
1813
+ z
1814
+ .string()
1815
+ .regex(/^(>=?\d+|<=?\d+|\d+\.\.\d+|\d+)$/, 'Invalid format. Use: ">100", ">=50", "<200", "<=150", "50..200", or exact number "100"'),
1816
+ ])
1701
1817
  .optional()
1702
- .describe('Total interactions (reactions + comments)'),
1818
+ .describe('Total interactions (reactions + comments) filter. Format: ">100" (highly engaged), ">=50" (moderately engaged), "<20" (low engagement), "50..200" (range)'),
1703
1819
  involves: z.string().optional().describe('User involved in any way'),
1704
- labels: z
1705
- .string()
1820
+ label: z
1821
+ .union([z.string(), z.array(z.string())])
1706
1822
  .optional()
1707
- .describe('Label names (bug, feature, etc.)'),
1823
+ .describe('Label names (bug, feature, etc.). Can be single string or array.'),
1708
1824
  language: z.string().optional().describe('Repository language'),
1709
1825
  locked: z.boolean().optional().describe('Conversation locked status'),
1710
1826
  match: z
@@ -1713,27 +1829,39 @@ function registerSearchGitHubIssuesTool(server) {
1713
1829
  .describe('Search scope. Default: title and body'),
1714
1830
  mentions: z.string().optional().describe('Issues mentioning this user'),
1715
1831
  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
1832
+ 'no-assignee': z
1833
+ .boolean()
1834
+ .optional()
1835
+ .describe('Issues without assignee'),
1836
+ 'no-label': z.boolean().optional().describe('Issues without labels'),
1837
+ 'no-milestone': z
1719
1838
  .boolean()
1720
1839
  .optional()
1721
1840
  .describe('Issues without milestone'),
1722
- noProject: z.boolean().optional().describe('Issues not in projects'),
1841
+ 'no-project': z.boolean().optional().describe('Issues not in projects'),
1723
1842
  project: z.string().optional().describe('Project board number'),
1724
1843
  reactions: z
1725
- .number()
1844
+ .union([
1845
+ z.number().int().min(0),
1846
+ z
1847
+ .string()
1848
+ .regex(/^(>=?\d+|<=?\d+|\d+\.\.\d+|\d+)$/, 'Invalid format. Use: ">10", ">=5", "<50", "<=25", "5..50", or exact number "10"'),
1849
+ ])
1726
1850
  .optional()
1727
- .describe('Reaction count. Format: >10'),
1851
+ .describe('Reaction count filter. Format: ">10" (popular), ">=5" (some reactions), "<50" (moderate), "5..50" (range), "10" (exact)'),
1728
1852
  state: z
1729
1853
  .enum(['open', 'closed'])
1730
1854
  .optional()
1731
1855
  .describe('Issue state. Default: all'),
1732
- teamMentions: z.string().optional().describe('Team mentioned in issue'),
1856
+ 'team-mentions': z
1857
+ .string()
1858
+ .optional()
1859
+ .describe('Team mentioned in issue (@org/team-name)'),
1733
1860
  updated: z
1734
1861
  .string()
1862
+ .regex(/^(>=?\d{4}-\d{2}-\d{2}|<=?\d{4}-\d{2}-\d{2}|\d{4}-\d{2}-\d{2}\.\.\d{4}-\d{2}-\d{2}|\d{4}-\d{2}-\d{2})$/, 'Invalid date format. Use: ">2020-01-01", ">=2020-01-01", "<2023-12-31", "<=2023-12-31", "2020-01-01..2023-12-31", or exact date "2023-01-01"')
1735
1863
  .optional()
1736
- .describe('When updated. Format: >2020-01-01'),
1864
+ .describe('When updated. Format: ">2020-01-01" (after), ">=2020-01-01" (on or after), "<2023-12-31" (before), "2020-01-01..2023-12-31" (range)'),
1737
1865
  visibility: z
1738
1866
  .enum(['public', 'private', 'internal'])
1739
1867
  .optional()
@@ -1889,23 +2017,40 @@ function buildGitHubIssuesAPICommand(params) {
1889
2017
  queryParts.push(`${key}:${value}`);
1890
2018
  });
1891
2019
  // Special qualifiers - handle labels carefully
1892
- if (params.labels) {
1893
- queryParts.push(`label:"${params.labels}"`);
2020
+ if (params.label) {
2021
+ const labels = Array.isArray(params.label) ? params.label : [params.label];
2022
+ labels.forEach(label => queryParts.push(`label:"${label}"`));
1894
2023
  }
1895
2024
  if (params.milestone)
1896
2025
  queryParts.push(`milestone:"${params.milestone}"`);
1897
- if (params.noAssignee)
2026
+ if (params['no-assignee'])
1898
2027
  queryParts.push('no:assignee');
1899
- if (params.noLabel)
2028
+ if (params['no-label'])
1900
2029
  queryParts.push('no:label');
1901
- if (params.noMilestone)
2030
+ if (params['no-milestone'])
1902
2031
  queryParts.push('no:milestone');
2032
+ if (params['no-project'])
2033
+ queryParts.push('no:project');
1903
2034
  if (params.archived !== undefined)
1904
2035
  queryParts.push(`archived:${params.archived}`);
1905
2036
  if (params.locked)
1906
2037
  queryParts.push('is:locked');
1907
2038
  if (params.visibility)
1908
2039
  queryParts.push(`is:${params.visibility}`);
2040
+ if (params['include-prs'])
2041
+ queryParts.push('is:pr');
2042
+ if (params['team-mentions'])
2043
+ queryParts.push(`team:${params['team-mentions']}`);
2044
+ if (params.project)
2045
+ queryParts.push(`project:${params.project}`);
2046
+ if (params.app)
2047
+ queryParts.push(`app:${params.app}`);
2048
+ if (params.comments)
2049
+ queryParts.push(`comments:${params.comments}`);
2050
+ if (params.interactions)
2051
+ queryParts.push(`interactions:${params.interactions}`);
2052
+ if (params.reactions)
2053
+ queryParts.push(`reactions:${params.reactions}`);
1909
2054
  // Extract qualifiers from original query and add them if not already set by params
1910
2055
  if (baseQuery.includes('is:') && !params.state) {
1911
2056
  const isMatch = baseQuery.match(/\bis:(open|closed)\b/i);
@@ -1913,7 +2058,7 @@ function buildGitHubIssuesAPICommand(params) {
1913
2058
  queryParts.push(`state:${isMatch[1].toLowerCase()}`);
1914
2059
  }
1915
2060
  }
1916
- if (baseQuery.includes('label:') && !params.labels) {
2061
+ if (baseQuery.includes('label:') && !params.label) {
1917
2062
  const labelMatch = baseQuery.match(/\blabel:("[^"]*"|[^\s]+)/i);
1918
2063
  if (labelMatch) {
1919
2064
  const labelValue = labelMatch[1].replace(/"/g, '');
@@ -1960,8 +2105,11 @@ function registerSearchGitHubPullRequestsTool(server) {
1960
2105
  mentions: z.string().optional().describe('PRs mentioning this user'),
1961
2106
  commenter: z.string().optional().describe('User who commented on PR'),
1962
2107
  involves: z.string().optional().describe('User involved in any way'),
1963
- reviewedBy: z.string().optional().describe('User who reviewed the PR'),
1964
- reviewRequested: z
2108
+ 'reviewed-by': z
2109
+ .string()
2110
+ .optional()
2111
+ .describe('User who reviewed the PR'),
2112
+ 'review-requested': z
1965
2113
  .string()
1966
2114
  .optional()
1967
2115
  .describe('User/team requested for review'),
@@ -1983,7 +2131,7 @@ function registerSearchGitHubPullRequestsTool(server) {
1983
2131
  .string()
1984
2132
  .optional()
1985
2133
  .describe('When updated. Format: >2020-01-01'),
1986
- mergedAt: z
2134
+ 'merged-at': z
1987
2135
  .string()
1988
2136
  .optional()
1989
2137
  .describe('When merged. Format: >2020-01-01'),
@@ -2004,6 +2152,49 @@ function registerSearchGitHubPullRequestsTool(server) {
2004
2152
  .enum(['none', 'required', 'approved', 'changes_requested'])
2005
2153
  .optional()
2006
2154
  .describe('Review status filter'),
2155
+ app: z.string().optional().describe('GitHub App that created the PR'),
2156
+ archived: z
2157
+ .boolean()
2158
+ .optional()
2159
+ .describe('Include archived repositories'),
2160
+ comments: z
2161
+ .number()
2162
+ .optional()
2163
+ .describe('Comment count filter. Format: >10, <5, 5..10'),
2164
+ interactions: z
2165
+ .number()
2166
+ .optional()
2167
+ .describe('Total interactions (reactions + comments)'),
2168
+ 'team-mentions': z
2169
+ .string()
2170
+ .optional()
2171
+ .describe('Team mentioned in PR (@org/team-name)'),
2172
+ reactions: z
2173
+ .number()
2174
+ .optional()
2175
+ .describe('Reaction count filter. Format: >10'),
2176
+ locked: z.boolean().optional().describe('Conversation locked status'),
2177
+ 'no-assignee': z.boolean().optional().describe('PRs without assignee'),
2178
+ 'no-label': z.boolean().optional().describe('PRs without labels'),
2179
+ 'no-milestone': z
2180
+ .boolean()
2181
+ .optional()
2182
+ .describe('PRs without milestone'),
2183
+ 'no-project': z.boolean().optional().describe('PRs not in projects'),
2184
+ label: z
2185
+ .union([z.string(), z.array(z.string())])
2186
+ .optional()
2187
+ .describe('Label names. Can be single string or array.'),
2188
+ milestone: z.string().optional().describe('Milestone title'),
2189
+ project: z.string().optional().describe('Project board owner/number'),
2190
+ visibility: z
2191
+ .enum(['public', 'private', 'internal'])
2192
+ .optional()
2193
+ .describe('Repository visibility'),
2194
+ match: z
2195
+ .enum(['title', 'body', 'comments'])
2196
+ .optional()
2197
+ .describe('Search scope. Default: title and body'),
2007
2198
  limit: z
2008
2199
  .number()
2009
2200
  .int()
@@ -2138,16 +2329,16 @@ function buildGitHubPullRequestsAPICommand(params) {
2138
2329
  queryParts.push(`${key}:${value}`);
2139
2330
  });
2140
2331
  // Special qualifiers
2141
- if (params.reviewedBy)
2142
- queryParts.push(`reviewed-by:${params.reviewedBy}`);
2143
- if (params.reviewRequested)
2144
- queryParts.push(`review-requested:${params.reviewRequested}`);
2332
+ if (params['reviewed-by'])
2333
+ queryParts.push(`reviewed-by:${params['reviewed-by']}`);
2334
+ if (params['review-requested'])
2335
+ queryParts.push(`review-requested:${params['review-requested']}`);
2145
2336
  if (params.head)
2146
2337
  queryParts.push(`head:${params.head}`);
2147
2338
  if (params.base)
2148
2339
  queryParts.push(`base:${params.base}`);
2149
- if (params.mergedAt)
2150
- queryParts.push(`merged:${params.mergedAt}`);
2340
+ if (params['merged-at'])
2341
+ queryParts.push(`merged:${params['merged-at']}`);
2151
2342
  if (params.draft !== undefined)
2152
2343
  queryParts.push(`draft:${params.draft}`);
2153
2344
  if (params.checks)
@@ -2156,6 +2347,39 @@ function buildGitHubPullRequestsAPICommand(params) {
2156
2347
  queryParts.push(`is:${params.merged ? 'merged' : 'unmerged'}`);
2157
2348
  if (params.review)
2158
2349
  queryParts.push(`review:${params.review}`);
2350
+ // Additional parameters
2351
+ if (params.app)
2352
+ queryParts.push(`app:${params.app}`);
2353
+ if (params.archived !== undefined)
2354
+ queryParts.push(`archived:${params.archived}`);
2355
+ if (params.comments)
2356
+ queryParts.push(`comments:${params.comments}`);
2357
+ if (params.interactions)
2358
+ queryParts.push(`interactions:${params.interactions}`);
2359
+ if (params.reactions)
2360
+ queryParts.push(`reactions:${params.reactions}`);
2361
+ if (params.locked)
2362
+ queryParts.push('is:locked');
2363
+ if (params.visibility)
2364
+ queryParts.push(`is:${params.visibility}`);
2365
+ if (params['team-mentions'])
2366
+ queryParts.push(`team:${params['team-mentions']}`);
2367
+ if (params['no-assignee'])
2368
+ queryParts.push('no:assignee');
2369
+ if (params['no-label'])
2370
+ queryParts.push('no:label');
2371
+ if (params['no-milestone'])
2372
+ queryParts.push('no:milestone');
2373
+ if (params['no-project'])
2374
+ queryParts.push('no:project');
2375
+ if (params.label) {
2376
+ const labels = Array.isArray(params.label) ? params.label : [params.label];
2377
+ labels.forEach(label => queryParts.push(`label:"${label}"`));
2378
+ }
2379
+ if (params.milestone)
2380
+ queryParts.push(`milestone:"${params.milestone}"`);
2381
+ if (params.project)
2382
+ queryParts.push(`project:${params.project}`);
2159
2383
  // Add type qualifier to search only pull requests
2160
2384
  queryParts.push('type:pr');
2161
2385
  const query = queryParts.filter(Boolean).join(' ');
@@ -2189,7 +2413,22 @@ function buildGitHubPullRequestsAPICommand(params) {
2189
2413
  * TIP: Use limit parameter instead of adding more filters
2190
2414
  */
2191
2415
  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).`;
2416
+ const DESCRIPTION$2 = `Search GitHub repositories using GitHub's repository search API with comprehensive filtering.
2417
+
2418
+ Search Logic (AND operation for multiple terms):
2419
+ - Multiple words: "react typescript" → repositories containing BOTH "react" AND "typescript"
2420
+ - Exact phrases: "web framework" → repositories with exact phrase "web framework"
2421
+ - Mixed: "machine learning" python → exact phrase "machine learning" AND word "python"
2422
+ - Filter combinations: language:javascript stars:>1000 → JavaScript repos with 1000+ stars
2423
+
2424
+ Supported Filters: language, stars (ranges), topics, owner/org, license, dates (created/updated),
2425
+ size, forks, community metrics (good-first-issues, help-wanted), archived status, visibility.
2426
+
2427
+ Examples:
2428
+ - Popular projects: stars:">1000" language:typescript
2429
+ - Beginner-friendly: good-first-issues:">5" stars:"100..5000"
2430
+ - Recent quality: created:">2023-01-01" stars:">500"
2431
+ - Organization repos: owner:microsoft language:python`;
2193
2432
  /**
2194
2433
  * Extract owner/repo information from various query formats
2195
2434
  */
@@ -2227,65 +2466,83 @@ function registerSearchGitHubReposTool(server) {
2227
2466
  query: z
2228
2467
  .string()
2229
2468
  .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.'),
2469
+ .describe('Search query with AND logic between terms. Multiple words require ALL to be present. Use quotes for exact phrases. Examples: react typescript (both words), "web framework" (exact phrase), "machine learning" python (phrase + word).'),
2231
2470
  // CORE FILTERS (GitHub CLI flags)
2232
2471
  owner: z
2233
2472
  .union([z.string(), z.array(z.string())])
2234
2473
  .optional()
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.'),
2474
+ .describe('Repository owner or organization name(s). Format: "microsoft" or ["facebook", "google"]. Do NOT use owner/repo format. Dramatically narrows search scope to specific organizations.'),
2236
2475
  language: z
2237
2476
  .string()
2238
2477
  .optional()
2239
- .describe('Programming language filter. Use when results need refinement.'),
2478
+ .describe('Programming language filter (javascript, python, typescript, go, etc). Filters repositories by primary language. Essential for language-specific searches.'),
2240
2479
  stars: z
2241
2480
  .union([
2242
2481
  z.number().int().min(0),
2243
- z.string().regex(/^(>=?\d+|<=?\d+|\d+\.\.\d+|\d+)$/),
2482
+ z
2483
+ .string()
2484
+ .regex(/^(>=?\d+|<=?\d+|\d+\.\.\d+|\d+)$/, 'Invalid format. Use: ">1000", ">=500", "<100", "<=50", "10..100", or exact number "50"'),
2244
2485
  ])
2245
2486
  .optional()
2246
- .describe('Stars filter. Supports ranges and thresholds.'),
2487
+ .describe('Star count filter. Format: ">1000" (more than), ">=500" (more than or equal), "<100" (less than), "<=50" (less than or equal), "100..1000" (range), "500" (exact). Examples from docs: ">1000", "100..5000", "<100"'),
2247
2488
  topic: z
2248
2489
  .union([z.string(), z.array(z.string())])
2249
2490
  .optional()
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
2254
- .number()
2491
+ .describe('Repository topics filter. Examples: "machine-learning", ["react", "typescript"]. Topics use kebab-case format (machine-learning, web-development).'),
2492
+ forks: z
2493
+ .union([
2494
+ z.number().int().min(0),
2495
+ z
2496
+ .string()
2497
+ .regex(/^(>=?\d+|<=?\d+|\d+\.\.\d+|\d+)$/, 'Invalid format. Use: ">100", ">=50", "<10", "<=5", "10..100", or exact number "5"'),
2498
+ ])
2255
2499
  .optional()
2256
- .describe('Filter by number of topics (indicates documentation quality).'),
2500
+ .describe('Fork count filter. Format: ">100" (more than), ">=50" (more than or equal), "<10" (less than), "<=5" (less than or equal), "10..100" (range), "5" (exact). Examples from docs: ">100", "10..100", "<10"'),
2501
+ // Match CLI parameter name exactly
2502
+ 'number-topics': z
2503
+ .union([
2504
+ z.number().int().min(0),
2505
+ z
2506
+ .string()
2507
+ .regex(/^(>=?\d+|<=?\d+|\d+\.\.\d+|\d+)$/, 'Invalid format. Use: ">5", ">=3", "<10", "<=2", "3..10", or exact number "5"'),
2508
+ ])
2509
+ .optional()
2510
+ .describe('Number of topics filter. Format: ">5" (many topics), ">=3" (at least 3), "<10" (few topics), "1..3" (range), "5" (exact). Well-documented projects typically have 3-10 topics.'),
2257
2511
  // QUALITY & STATE FILTERS
2258
2512
  license: z
2259
2513
  .union([z.string(), z.array(z.string())])
2260
2514
  .optional()
2261
- .describe('License filter. Works well as array ["mit", "apache-2.0"].'),
2515
+ .describe('License filter. Examples: "mit", "apache-2.0", ["mit", "apache-2.0"]. Common licenses: mit, apache-2.0, gpl-3.0, bsd-3-clause.'),
2262
2516
  archived: z
2263
2517
  .boolean()
2264
2518
  .optional()
2265
- .describe('Filter archived repositories (true/false).'),
2266
- includeForks: z
2519
+ .describe('Archive status filter. false (active repos only), true (archived repos only). Use false to find actively maintained projects.'),
2520
+ 'include-forks': z
2267
2521
  .enum(['false', 'true', 'only'])
2268
2522
  .optional()
2269
- .describe('Include forks: false (exclude), true (include), only (forks only).'),
2523
+ .describe('Fork inclusion. "false" (exclude forks), "true" (include forks), "only" (forks only). Use "false" for original projects.'),
2270
2524
  visibility: z
2271
2525
  .enum(['public', 'private', 'internal'])
2272
2526
  .optional()
2273
- .describe('Repository visibility filter.'),
2527
+ .describe('Repository visibility. "public" (open source), "private" (private repos you have access to), "internal" (organization internal).'),
2274
2528
  // DATE & SIZE FILTERS
2275
2529
  created: z
2276
2530
  .string()
2531
+ .regex(/^(>=?\d{4}-\d{2}-\d{2}|<=?\d{4}-\d{2}-\d{2}|\d{4}-\d{2}-\d{2}\.\.\d{4}-\d{2}-\d{2}|\d{4}-\d{2}-\d{2})$/, 'Invalid date format. Use: ">2020-01-01", ">=2020-01-01", "<2023-12-31", "<=2023-12-31", "2020-01-01..2023-12-31", or exact date "2023-01-01"')
2277
2532
  .optional()
2278
- .describe('Created date filter. Format: ">2020-01-01", "<2023-12-31".'),
2533
+ .describe('Repository creation date filter. Format: ">2020-01-01" (after), ">=2020-01-01" (on or after), "<2023-12-31" (before), "<=2023-12-31" (on or before), "2020-01-01..2023-12-31" (range), "2023-01-01" (exact). Examples from docs: ">2020-01-01", "2023-01-01..2023-12-31"'),
2279
2534
  updated: z
2280
2535
  .string()
2536
+ .regex(/^(>=?\d{4}-\d{2}-\d{2}|<=?\d{4}-\d{2}-\d{2}|\d{4}-\d{2}-\d{2}\.\.\d{4}-\d{2}-\d{2}|\d{4}-\d{2}-\d{2})$/, 'Invalid date format. Use: ">2024-01-01", ">=2024-01-01", "<2022-01-01", "<=2022-01-01", "2023-01-01..2024-12-31", or exact date "2024-01-01"')
2281
2537
  .optional()
2282
- .describe('Updated date filter. Good for finding active projects.'),
2538
+ .describe('Last updated date filter. Format: ">2024-01-01" (recently updated), ">=2024-01-01" (on or after), "<2022-01-01" (not recently updated), "2023-01-01..2024-12-31" (range). Essential for finding actively maintained projects. Examples from docs: ">2024-01-01", "2022-01-01..2023-12-31"'),
2283
2539
  size: z
2284
2540
  .string()
2541
+ .regex(/^(>=?\d+|<=?\d+|\d+\.\.\d+|\d+)$/, 'Invalid size format. Use: ">1000", ">=500", "<100", "<=50", "100..1000", or exact number "500"')
2285
2542
  .optional()
2286
- .describe('Repository size filter in KB. Format: ">1000", "<500".'),
2543
+ .describe('Repository size filter in KB. Format: ">1000" (large projects), ">=500" (medium-large), "<100" (small projects), "<=50" (tiny), "100..1000" (medium range), "500" (exact). Examples from docs: ">1000", "100..1000", "<100"'),
2287
2544
  // COMMUNITY FILTERS - Match CLI parameter names exactly
2288
- goodFirstIssues: z
2545
+ 'good-first-issues': z
2289
2546
  .union([
2290
2547
  z.number().int().min(0),
2291
2548
  z
@@ -2293,8 +2550,8 @@ function registerSearchGitHubReposTool(server) {
2293
2550
  .regex(/^(>=?\d+|<=?\d+|\d+\.\.\d+|\d+)$/, 'Invalid format. Use: number, ">5", ">=10", "<20", "<=15", or "5..20"'),
2294
2551
  ])
2295
2552
  .optional()
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
2553
+ .describe('Good first issues count. Format: ">5" (many beginner issues), "1..10" (some beginner issues). Perfect for finding beginner-friendly open source projects.'),
2554
+ 'help-wanted-issues': z
2298
2555
  .union([
2299
2556
  z.number().int().min(0),
2300
2557
  z
@@ -2302,13 +2559,21 @@ function registerSearchGitHubReposTool(server) {
2302
2559
  .regex(/^(>=?\d+|<=?\d+|\d+\.\.\d+|\d+)$/, 'Invalid format. Use: number, ">5", ">=10", "<20", "<=15", or "5..20"'),
2303
2560
  ])
2304
2561
  .optional()
2305
- .describe('Help wanted issues count. Good for finding projects needing contributors.'),
2306
- followers: z.number().optional().describe('Followers count filter.'),
2562
+ .describe('Help wanted issues count. Format: ">10" (many help wanted), "1..5" (some help wanted). Great for finding projects actively seeking contributors.'),
2563
+ followers: z
2564
+ .union([
2565
+ z.number().int().min(0),
2566
+ z
2567
+ .string()
2568
+ .regex(/^(>=?\d+|<=?\d+|\d+\.\.\d+|\d+)$/, 'Invalid format. Use: ">1000", ">=500", "<100", "<=50", "100..1000", or exact number "500"'),
2569
+ ])
2570
+ .optional()
2571
+ .describe('Repository owner followers count. Format: ">1000" (popular developers), ">=500" (established developers), "<100" (smaller developers), "100..1000" (range). Indicates developer/org reputation.'),
2307
2572
  // SEARCH SCOPE
2308
2573
  match: z
2309
2574
  .enum(['name', 'description', 'readme'])
2310
2575
  .optional()
2311
- .describe('Search scope: name, description, or readme content.'),
2576
+ .describe('Search scope. "name" (repository names only), "description" (descriptions only), "readme" (README content). Default searches all fields.'),
2312
2577
  // SORTING & LIMITS - Match CLI defaults exactly
2313
2578
  sort: z
2314
2579
  .enum([
@@ -2320,12 +2585,12 @@ function registerSearchGitHubReposTool(server) {
2320
2585
  ])
2321
2586
  .optional()
2322
2587
  .default('best-match')
2323
- .describe('Sort criteria for results.'),
2588
+ .describe('Sort criteria. "stars" (popularity), "updated" (recent activity), "forks" (community engagement), "help-wanted-issues" (contribution opportunities), "best-match" (relevance).'),
2324
2589
  order: z
2325
2590
  .enum(['asc', 'desc'])
2326
2591
  .optional()
2327
2592
  .default('desc')
2328
- .describe('Sort order direction.'),
2593
+ .describe('Sort order direction. "desc" (descending, highest first), "asc" (ascending, lowest first). Default is descending for most useful results.'),
2329
2594
  limit: z
2330
2595
  .number()
2331
2596
  .int()
@@ -2333,7 +2598,7 @@ function registerSearchGitHubReposTool(server) {
2333
2598
  .max(100)
2334
2599
  .optional()
2335
2600
  .default(30)
2336
- .describe('Maximum results to return (1-100). Default: 30'),
2601
+ .describe('Maximum number of repositories to return (1-100). Default: 30. Higher values may increase response time.'),
2337
2602
  },
2338
2603
  annotations: {
2339
2604
  title: 'GitHub Repository Search',
@@ -2534,13 +2799,13 @@ function buildGitHubReposSearchCommand(params) {
2534
2799
  // CORE FILTERS
2535
2800
  addArg('owner', 'owner', !hasEmbeddedQualifiers);
2536
2801
  addArg('language', 'language', !hasEmbeddedQualifiers);
2537
- addArg('forks', 'forks', !hasEmbeddedQualifiers);
2802
+ addArg('forks', 'forks', !hasEmbeddedQualifiers, value => typeof value === 'number' ? value.toString() : value.trim());
2538
2803
  addArg('topic', 'topic', !hasEmbeddedQualifiers);
2539
- addArg('numberOfTopics', 'number-topics');
2804
+ addArg('number-topics', 'number-topics', true, value => typeof value === 'number' ? value.toString() : value.trim());
2540
2805
  addArg('stars', 'stars', !hasEmbeddedQualifiers, value => typeof value === 'number' ? value.toString() : value.trim());
2541
2806
  // QUALITY & STATE FILTERS
2542
2807
  addArg('archived', 'archived');
2543
- addArg('includeForks', 'include-forks');
2808
+ addArg('include-forks', 'include-forks');
2544
2809
  addArg('visibility', 'visibility');
2545
2810
  addArg('license', 'license');
2546
2811
  // DATE & SIZE FILTERS
@@ -2548,9 +2813,9 @@ function buildGitHubReposSearchCommand(params) {
2548
2813
  addArg('updated', 'updated');
2549
2814
  addArg('size', 'size');
2550
2815
  // 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');
2816
+ addArg('good-first-issues', 'good-first-issues', true, value => typeof value === 'number' ? value.toString() : value);
2817
+ addArg('help-wanted-issues', 'help-wanted-issues', true, value => typeof value === 'number' ? value.toString() : value);
2818
+ addArg('followers', 'followers', true, value => typeof value === 'number' ? value.toString() : value.trim());
2554
2819
  // SEARCH SCOPE
2555
2820
  addArg('match', 'match');
2556
2821
  // SORTING AND LIMITS
@@ -2667,7 +2932,6 @@ function parseNpmSearchOutput(output) {
2667
2932
  return packages.map(normalizePackage);
2668
2933
  }
2669
2934
  catch (error) {
2670
- logger.warn('Failed to parse NPM search results:', error);
2671
2935
  return [];
2672
2936
  }
2673
2937
  }
@@ -2996,39 +3260,31 @@ function registerAllTools(server) {
2996
3260
  },
2997
3261
  { name: NPM_VIEW_PACKAGE_TOOL_NAME, fn: registerNpmViewPackageTool },
2998
3262
  ];
2999
- logger.info(`Registering ${toolRegistrations.length} tools...`);
3000
3263
  let successCount = 0;
3001
3264
  for (const tool of toolRegistrations) {
3002
3265
  try {
3003
- logger.debug(`Registering tool: ${tool.name}`);
3004
3266
  tool.fn(server);
3005
3267
  successCount++;
3006
- logger.info(`✓ Successfully registered: ${tool.name}`);
3007
3268
  }
3008
3269
  catch (error) {
3009
- logger.error(`✗ Failed to register ${tool.name}:`, error);
3010
3270
  // Continue with other tools instead of failing completely
3011
3271
  }
3012
3272
  }
3013
3273
  if (successCount === 0) {
3014
3274
  throw new Error('No tools were successfully registered');
3015
3275
  }
3016
- logger.info(`All tools registration completed - ${successCount}/${toolRegistrations.length} successful`);
3017
3276
  }
3018
3277
  async function startServer() {
3019
3278
  try {
3020
- logger.info('Creating MCP server...');
3021
3279
  const server = new McpServer(SERVER_CONFIG);
3022
3280
  registerAllTools(server);
3023
3281
  const transport = new StdioServerTransport();
3024
3282
  await server.connect(transport);
3025
- logger.info('=== Server Connected Successfully ===');
3026
3283
  // Ensure all buffered output is sent
3027
3284
  process.stdout.uncork();
3028
3285
  process.stderr.uncork();
3029
- const gracefulShutdown = async (signal) => {
3286
+ const gracefulShutdown = async () => {
3030
3287
  try {
3031
- logger.info(`Received ${signal}, shutting down gracefully...`);
3032
3288
  clearAllCache();
3033
3289
  // Give server time to close properly
3034
3290
  await Promise.race([
@@ -3038,35 +3294,30 @@ async function startServer() {
3038
3294
  process.exit(0);
3039
3295
  }
3040
3296
  catch (error) {
3041
- logger.error('Error during shutdown:', error);
3042
3297
  process.exit(1);
3043
3298
  }
3044
3299
  };
3045
3300
  // Handle process signals
3046
- process.on('SIGINT', () => gracefulShutdown('SIGINT'));
3047
- process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
3301
+ process.on('SIGINT', gracefulShutdown);
3302
+ process.on('SIGTERM', gracefulShutdown);
3048
3303
  // Handle stdin close (important for MCP)
3049
3304
  process.stdin.on('close', async () => {
3050
- await gracefulShutdown('STDIN_CLOSE');
3305
+ await gracefulShutdown();
3051
3306
  });
3052
3307
  // Handle uncaught errors
3053
- process.on('uncaughtException', error => {
3054
- logger.error('Uncaught exception:', error);
3055
- gracefulShutdown('UNCAUGHT_EXCEPTION');
3308
+ process.on('uncaughtException', () => {
3309
+ gracefulShutdown();
3056
3310
  });
3057
- process.on('unhandledRejection', (reason, promise) => {
3058
- logger.error('Unhandled rejection at:', promise, 'reason:', reason);
3059
- gracefulShutdown('UNHANDLED_REJECTION');
3311
+ process.on('unhandledRejection', () => {
3312
+ gracefulShutdown();
3060
3313
  });
3061
3314
  // Keep process alive
3062
3315
  process.stdin.resume();
3063
3316
  }
3064
3317
  catch (error) {
3065
- logger.error('Error details:', error);
3066
3318
  process.exit(1);
3067
3319
  }
3068
3320
  }
3069
- startServer().catch(error => {
3070
- logger.error('Error:', error);
3321
+ startServer().catch(() => {
3071
3322
  process.exit(1);
3072
3323
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "octocode-mcp",
3
- "version": "2.3.6",
3
+ "version": "2.3.7",
4
4
  "description": "Model Context Protocol (MCP) server for advanced GitHub repository analysis, code discovery, and npm package exploration. Provides AI assistants with powerful tools to search, analyze, and understand codebases across GitHub and npm ecosystems.",
5
5
  "author": "Guy Bary <guybary@gmail.com>",
6
6
  "homepage": "https://github.com/bgauryy/octocode-mcp#readme",