octocode-mcp 2.3.15 → 2.3.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +463 -111
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -85,7 +85,14 @@ const ALLOWED_NPM_COMMANDS = [
85
85
  'whoami',
86
86
  ];
87
87
  // Allowed command prefixes - this prevents shell injection by restricting to safe commands
88
- const ALLOWED_GH_COMMANDS = ['search', 'api', 'auth', 'org', 'pr'];
88
+ const ALLOWED_GH_COMMANDS = [
89
+ 'search',
90
+ 'api',
91
+ 'auth',
92
+ 'org',
93
+ 'pr',
94
+ 'repo',
95
+ ];
89
96
  function createSuccessResult(data) {
90
97
  return {
91
98
  content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
@@ -247,10 +254,10 @@ async function executeGitHubCommand(command, args = [], options = {}) {
247
254
  // Get shell configuration
248
255
  const shellConfig = getShellConfig(options.windowsShell);
249
256
  // Build command with validated prefix and properly escaped arguments
250
- // For GitHub search commands, we need minimal escaping to avoid interfering with GitHub CLI
251
257
  const escapedArgs = args.map((arg, index) => {
252
258
  const isMainQueryArgument = command === 'search' && index === 1;
253
259
  const isCliFlag = arg.startsWith('--');
260
+ const isApiPath = command === 'api' && arg.startsWith('/');
254
261
  // CLI flags like --language=javascript, --repo=owner/repo need minimal escaping
255
262
  if (isCliFlag) {
256
263
  // Only escape CLI flags if they contain dangerous shell characters
@@ -259,6 +266,22 @@ async function executeGitHubCommand(command, args = [], options = {}) {
259
266
  }
260
267
  return arg;
261
268
  }
269
+ // API paths with query parameters need proper escaping
270
+ if (isApiPath) {
271
+ // Always quote API paths that contain query parameters or special characters
272
+ if (arg.includes('?') || arg.includes('&') || /[\s*?[\]{}]/.test(arg)) {
273
+ if (shellConfig.type === 'unix') {
274
+ return `'${arg.replace(/'/g, "'\"'\"'")}'`;
275
+ }
276
+ else if (shellConfig.type === 'cmd') {
277
+ return `"${arg.replace(/"/g, '""')}"`;
278
+ }
279
+ else {
280
+ return `'${arg.replace(/'/g, "''")}'`;
281
+ }
282
+ }
283
+ return arg;
284
+ }
262
285
  // For search queries, only escape if absolutely necessary for shell safety
263
286
  if (isMainQueryArgument) {
264
287
  // Only escape if the argument contains shell metacharacters that could be dangerous
@@ -294,6 +317,29 @@ async function executeGitHubCommand(command, args = [], options = {}) {
294
317
  }
295
318
  return executeGhCommand();
296
319
  }
320
+ /**
321
+ * Helper function to detect shell configuration errors vs real GitHub errors
322
+ */
323
+ function isShellConfigurationError(errorText) {
324
+ if (!errorText)
325
+ return false;
326
+ // Preserve GitHub API errors
327
+ if (errorText.includes('HTTP 404') ||
328
+ errorText.includes('HTTP 403') ||
329
+ errorText.includes('Not Found') ||
330
+ errorText.includes('API rate limit')) {
331
+ return false;
332
+ }
333
+ // Shell configuration conflicts to ignore
334
+ return (errorText.includes('head: illegal option') ||
335
+ errorText.includes('head: |: No such file or directory') ||
336
+ errorText.includes('head: cat: No such file or directory') ||
337
+ /^head:\s+/.test(errorText) ||
338
+ /^\s*head:\s+/.test(errorText) ||
339
+ (errorText.includes('No such file or directory') &&
340
+ !errorText.includes('HTTP') &&
341
+ !errorText.includes('gh:')));
342
+ }
297
343
  /**
298
344
  * Execute shell commands with improved environment handling and error detection
299
345
  */
@@ -329,13 +375,8 @@ async function executeCommand(fullCommand, type, options = {}, shellConfig) {
329
375
  : stderr &&
330
376
  !stderr.includes('Warning:') &&
331
377
  !stderr.includes('notice:') &&
332
- // Ignore shell configuration conflicts - common in development environments
333
- !stderr.includes('No such file or directory') &&
334
- !stderr.includes('head: illegal option') &&
335
- !stderr.includes('head: |: No such file or directory') &&
336
- !stderr.includes('head: cat: No such file or directory') &&
337
- !/^head:\s+/.test(stderr) && // Ignore all head command errors (shell conflicts)
338
- !/^\s*head:\s+/.test(stderr) && // Ignore head errors with leading whitespace
378
+ // Ignore shell configuration conflicts but preserve GitHub API errors
379
+ !isShellConfigurationError(stderr) &&
339
380
  stderr.trim() !== '';
340
381
  if (shouldTreatAsError) {
341
382
  const errorType = type === 'npm' ? 'NPM command error' : 'GitHub CLI command error';
@@ -929,10 +970,7 @@ function createToolSuggestion(currentTool, suggestedTools) {
929
970
  }
930
971
 
931
972
  const API_STATUS_CHECK_TOOL_NAME = 'apiStatusCheck';
932
- const DESCRIPTION$9 = `initial tool to verify user connections
933
-
934
- - Github: check gh login status and list available organizations.
935
- - Npm: check npm login status and list npm registry.`;
973
+ const DESCRIPTION$9 = `Check user status: GitHub/NPM connections, organizations, and current timestamp. Essential for understanding user's data access and API capabilities.`;
936
974
  // Helper function to parse execution results with proper typing
937
975
  function parseExecResult(result) {
938
976
  if (!result.isError && result.content?.[0]?.text) {
@@ -1053,6 +1091,7 @@ function registerApiStatusCheckTool(server) {
1053
1091
  }
1054
1092
  return createResult({
1055
1093
  data: {
1094
+ timestamp: new Date().toISOString(),
1056
1095
  login: {
1057
1096
  github: {
1058
1097
  connected: githubConnected,
@@ -1090,24 +1129,12 @@ const GITHUB_SEARCH_CODE_TOOL_NAME = 'githubSearchCode';
1090
1129
  const DESCRIPTION$8 = `Search code across GitHub repositories using GitHub's code search API via GitHub CLI.
1091
1130
 
1092
1131
  SEARCH STRATEGY FOR BEST RESULTS:
1093
-
1094
- EXACT vs TERMS (Choose ONE):
1095
- - exactQuery: Use for exact phrase matching
1096
- - queryTerms: Use minimal words for broader coverage
1097
-
1098
- TERM OPTIMIZATION:
1099
- - BEST: Single terms for maximum coverage
1100
- - GOOD: 2-3 minimal terms
1101
- - AVOID: Long phrases in queryTerms
1102
-
1103
- MULTI-SEARCH STRATEGY:
1132
+ - use 'exactQuery' for exact phrase matching
1133
+ - use 'queryTerms' for minimal words for broader coverage (recommended: 2-3 terms)
1134
+ - queryTerms can be exacts strings or words (e.g. term1, "term2 exact string"...)
1104
1135
  - Use separate searches for different aspects
1105
1136
  - Separate searches provide broader coverage than complex queries
1106
-
1107
- Filter Usage:
1108
- - Use filters to narrow scope: language, owner, repo, filename
1109
- - Combine filters strategically: language + owner for organization-wide searches
1110
- - Never use filters on exploratory searches - use to refine results`;
1137
+ - Use filters to narrow scope (never use filters on exploratory searches - use to refine results)`;
1111
1138
  function registerGitHubSearchCodeTool(server) {
1112
1139
  server.registerTool(GITHUB_SEARCH_CODE_TOOL_NAME, {
1113
1140
  description: DESCRIPTION$8,
@@ -111960,7 +111987,10 @@ async function fetchGitHubFileContent(params) {
111960
111987
  const { owner, repo, branch, filePath } = params;
111961
111988
  try {
111962
111989
  // Try to fetch file content directly
111963
- const apiPath = `/repos/${owner}/${repo}/contents/${filePath}?ref=${branch}`;
111990
+ const isCommitSha = branch.match(/^[0-9a-f]{40}$/);
111991
+ const apiPath = isCommitSha
111992
+ ? `/repos/${owner}/${repo}/contents/${filePath}?ref=${branch}` // Use contents API with ref for commit SHA
111993
+ : `/repos/${owner}/${repo}/contents/${filePath}?ref=${branch}`; // Use contents API for branches/tags
111964
111994
  const result = await executeGitHubCommand('api', [apiPath], {
111965
111995
  cache: false,
111966
111996
  });
@@ -112794,12 +112824,13 @@ function buildGitHubReposSearchCommand(params) {
112794
112824
  }
112795
112825
 
112796
112826
  const GITHUB_SEARCH_COMMITS_TOOL_NAME = 'githubSearchCommits';
112797
- const DESCRIPTION$5 = `Search commit history across GitHub repositories. Find commits by message, author, date, or repository. Supports advanced filtering for comprehensive commit analysis. Returns commit SHA, message, author, and date information.
112827
+ const DESCRIPTION$5 = `Search GitHub commits by message, author, or date. Returns SHAs for github_fetch_content (branch=SHA). Can fetch commit content changes (diffs/patches) when getChangesContent=true.
112798
112828
 
112799
- INTEGRATION WORKFLOW:
112800
- - Returned commit SHAs can be used directly with github fetch content (branch=SHA)
112801
- - Use SHA as 'branch' parameter to view files from specific commits
112802
- - Perfect for examining code changes and historical versions`;
112829
+ SEARCH STRATEGY FOR BEST RESULTS:
112830
+ - Use minimal search terms for broader coverage (2-3 words max)
112831
+ - Separate searches for different aspects vs complex queries
112832
+ - Use filters to narrow scope after getting initial results
112833
+ - getChangesContent=true only when analyzing actual code changes`;
112803
112834
  function registerGitHubSearchCommitsTool(server) {
112804
112835
  server.registerTool(GITHUB_SEARCH_COMMITS_TOOL_NAME, {
112805
112836
  description: DESCRIPTION$5,
@@ -112807,16 +112838,16 @@ function registerGitHubSearchCommitsTool(server) {
112807
112838
  query: z
112808
112839
  .string()
112809
112840
  .optional()
112810
- .describe('Search terms. Start simple: "bug fix", "refactor". Use quotes for exact phrases.'),
112841
+ .describe('Commit message search terms (keep minimal for broader results)'),
112811
112842
  // Repository filters
112812
112843
  owner: z
112813
112844
  .string()
112814
112845
  .optional()
112815
- .describe('Repository owner/organization name only (e.g., "facebook", "microsoft"). Do NOT include repository name. Must be used with repo parameter for repository-specific searches.'),
112846
+ .describe('Repository owner (use with repo param)'),
112816
112847
  repo: z
112817
112848
  .string()
112818
112849
  .optional()
112819
- .describe('Repository name only (e.g., "react", "vscode"). Do NOT include owner prefix. Must be used together with owner parameter.'),
112850
+ .describe('Repository name (use with owner param)'),
112820
112851
  // Author filters
112821
112852
  author: z
112822
112853
  .string()
@@ -112844,11 +112875,11 @@ function registerGitHubSearchCommitsTool(server) {
112844
112875
  'author-date': z
112845
112876
  .string()
112846
112877
  .optional()
112847
- .describe('When authored. Format: >2020-01-01, <2023-12-31, 2020-01-01..2023-12-31'),
112878
+ .describe('Filter by author date (e.g., >2020-01-01)'),
112848
112879
  'committer-date': z
112849
112880
  .string()
112850
112881
  .optional()
112851
- .describe('When committed. Format: >2020-01-01, <2023-12-31, 2020-01-01..2023-12-31'),
112882
+ .describe('Filter by commit date (e.g., >2020-01-01)'),
112852
112883
  // Hash filters
112853
112884
  hash: z.string().optional().describe('Commit SHA (full or partial)'),
112854
112885
  parent: z.string().optional().describe('Parent commit SHA'),
@@ -112871,19 +112902,24 @@ function registerGitHubSearchCommitsTool(server) {
112871
112902
  .max(50)
112872
112903
  .optional()
112873
112904
  .default(25)
112874
- .describe('Results limit (1-50). Default: 25'),
112905
+ .describe('Maximum number of results to fetch'),
112875
112906
  sort: z
112876
112907
  .enum(['author-date', 'committer-date'])
112877
112908
  .optional()
112878
- .describe('Sort by date. Default: best match'),
112909
+ .describe('Sort by date field'),
112879
112910
  order: z
112880
112911
  .enum(['asc', 'desc'])
112881
112912
  .optional()
112882
112913
  .default('desc')
112883
- .describe('Sort order. Default: desc'),
112914
+ .describe('Sort order'),
112915
+ getChangesContent: z
112916
+ .boolean()
112917
+ .optional()
112918
+ .default(false)
112919
+ .describe('Get actual code diffs - only use when analyzing changes, not for commit identification'),
112884
112920
  },
112885
112921
  annotations: {
112886
- title: 'GitHub Commit Search - Smart History Analysis',
112922
+ title: 'GitHub Commit Search - Smart & Effective',
112887
112923
  readOnlyHint: true,
112888
112924
  destructiveHint: false,
112889
112925
  idempotentHint: true,
@@ -112901,12 +112937,65 @@ function registerGitHubSearchCommitsTool(server) {
112901
112937
  const items = Array.isArray(commits) ? commits : [];
112902
112938
  // Smart handling for no results - provide actionable suggestions
112903
112939
  if (items.length === 0) {
112940
+ // Progressive simplification strategy based on current search complexity
112941
+ const simplificationSteps = [];
112942
+ let hasFilters = false;
112943
+ // Check for active filters
112944
+ const activeFilters = [];
112945
+ if (args.owner && args.repo)
112946
+ activeFilters.push('repo');
112947
+ if (args.author)
112948
+ activeFilters.push('author');
112949
+ if (args['author-email'])
112950
+ activeFilters.push('author-email');
112951
+ if (args.hash)
112952
+ activeFilters.push('hash');
112953
+ if (args['author-date'])
112954
+ activeFilters.push('date');
112955
+ if (args.visibility)
112956
+ activeFilters.push('visibility');
112957
+ hasFilters = activeFilters.length > 0;
112958
+ // Step 1: If complex query, simplify search terms
112959
+ if (args.query && args.query.trim().split(' ').length > 2) {
112960
+ const words = args.query.trim().split(' ');
112961
+ const simplified = words.slice(0, 1).join(' '); // Take first word only
112962
+ simplificationSteps.push(`Try simpler search: "${simplified}" instead of "${args.query}"`);
112963
+ }
112964
+ // Step 2: Remove filters progressively
112965
+ if (hasFilters) {
112966
+ if (activeFilters.length > 1) {
112967
+ simplificationSteps.push(`Remove some filters (currently: ${activeFilters.join(', ')}) and keep only the most important one`);
112968
+ }
112969
+ else {
112970
+ simplificationSteps.push(`Remove the ${activeFilters[0]} filter and search more broadly`);
112971
+ }
112972
+ }
112973
+ // Step 3: Alternative approaches
112974
+ if (!args.query && hasFilters) {
112975
+ simplificationSteps.push('Add basic search terms like "fix", "update", or "add" with your filters');
112976
+ }
112977
+ // Step 4: Ask for user guidance if no obvious simplification
112978
+ if (simplificationSteps.length === 0) {
112979
+ simplificationSteps.push("Try different keywords or ask the user to be more specific about what commits they're looking for");
112980
+ }
112904
112981
  return createResult({
112905
- error: createNoResultsError('commits'),
112982
+ error: `${createNoResultsError('commits')}
112983
+
112984
+ Try these simplified searches:
112985
+ ${simplificationSteps.map(step => `• ${step}`).join('\n')}
112986
+
112987
+ Or ask the user:
112988
+ • "What specific type of commits are you looking for?"
112989
+ • "Can you provide different keywords to search for?"
112990
+ • "Should I search in a specific repository instead?"
112991
+
112992
+ Alternative tools:
112993
+ • Use github_search_code for file-specific commits
112994
+ • Use github_search_repos to find repositories first`,
112906
112995
  });
112907
112996
  }
112908
112997
  // Transform to optimized format
112909
- const optimizedResult = transformCommitsToOptimizedFormat(items, args);
112998
+ const optimizedResult = await transformCommitsToOptimizedFormat(items, args);
112910
112999
  return createResult({ data: optimizedResult });
112911
113000
  }
112912
113001
  catch (error) {
@@ -112930,22 +113019,75 @@ function registerGitHubSearchCommitsTool(server) {
112930
113019
  /**
112931
113020
  * Transform GitHub CLI response to optimized format
112932
113021
  */
112933
- function transformCommitsToOptimizedFormat(items, _params) {
113022
+ async function transformCommitsToOptimizedFormat(items, params) {
112934
113023
  // Extract repository info if single repo search
112935
113024
  const singleRepo = extractSingleRepository(items);
113025
+ // Fetch diff information if requested and this is a repo-specific search
113026
+ const shouldFetchDiff = params.getChangesContent && params.owner && params.repo;
113027
+ const diffData = new Map();
113028
+ if (shouldFetchDiff && items.length > 0) {
113029
+ // Fetch diff info for each commit (limit to first 10 to avoid rate limits)
113030
+ const commitShas = items.slice(0, 10).map(item => item.sha);
113031
+ const diffPromises = commitShas.map(async (sha) => {
113032
+ try {
113033
+ const diffResult = await executeGitHubCommand('api', [`/repos/${params.owner}/${params.repo}/commits/${sha}`], { cache: false });
113034
+ if (!diffResult.isError) {
113035
+ const diffExecResult = JSON.parse(diffResult.content[0].text);
113036
+ return { sha, commitData: diffExecResult.result };
113037
+ }
113038
+ }
113039
+ catch (e) {
113040
+ // Ignore diff fetch errors
113041
+ }
113042
+ return { sha, commitData: null };
113043
+ });
113044
+ const diffResults = await Promise.all(diffPromises);
113045
+ diffResults.forEach(({ sha, commitData }) => {
113046
+ if (commitData) {
113047
+ diffData.set(sha, commitData);
113048
+ }
113049
+ });
113050
+ }
112936
113051
  const optimizedCommits = items
112937
- .map(item => ({
112938
- sha: item.sha,
112939
- message: getCommitTitle(item.commit?.message ?? ''),
112940
- author: item.commit?.author?.name ?? item.author?.login ?? 'Unknown',
112941
- date: toDDMMYYYY(item.commit?.author?.date ?? ''),
112942
- repository: singleRepo
112943
- ? undefined
112944
- : simplifyRepoUrl(item.repository?.url || ''),
112945
- url: singleRepo
112946
- ? item.sha
112947
- : `${simplifyRepoUrl(item.repository?.url || '')}@${item.sha}`,
112948
- }))
113052
+ .map(item => {
113053
+ const commitObj = {
113054
+ sha: item.sha, // Use as branch parameter in github_fetch_content
113055
+ message: getCommitTitle(item.commit?.message ?? ''),
113056
+ author: item.commit?.author?.name ?? item.author?.login ?? 'Unknown',
113057
+ date: toDDMMYYYY(item.commit?.author?.date ?? ''),
113058
+ repository: singleRepo
113059
+ ? undefined
113060
+ : simplifyRepoUrl(item.repository?.url || ''),
113061
+ url: singleRepo
113062
+ ? item.sha
113063
+ : `${simplifyRepoUrl(item.repository?.url || '')}@${item.sha}`,
113064
+ };
113065
+ // Add diff information if available
113066
+ if (shouldFetchDiff && diffData.has(item.sha)) {
113067
+ const commitData = diffData.get(item.sha);
113068
+ const files = commitData.files || [];
113069
+ commitObj.diff = {
113070
+ changed_files: files.length,
113071
+ additions: commitData.stats?.additions || 0,
113072
+ deletions: commitData.stats?.deletions || 0,
113073
+ total_changes: commitData.stats?.total || 0,
113074
+ files: files
113075
+ .map((f) => ({
113076
+ filename: f.filename,
113077
+ status: f.status,
113078
+ additions: f.additions,
113079
+ deletions: f.deletions,
113080
+ changes: f.changes,
113081
+ patch: f.patch
113082
+ ? f.patch.substring(0, 1000) +
113083
+ (f.patch.length > 1000 ? '...' : '')
113084
+ : undefined,
113085
+ }))
113086
+ .slice(0, 5), // Limit to 5 files per commit
113087
+ };
113088
+ }
113089
+ return commitObj;
113090
+ })
112949
113091
  .map(commit => {
112950
113092
  // Remove undefined fields
112951
113093
  const cleanCommit = {};
@@ -113067,13 +113209,13 @@ function buildGitHubCommitCliArgs(params) {
113067
113209
 
113068
113210
  // TODO: add PR commeents. e.g, gh pr view <PR_NUMBER_OR_URL_OR_BRANCH> --comments
113069
113211
  const GITHUB_SEARCH_PULL_REQUESTS_TOOL_NAME = 'githubSearchPullRequests';
113070
- const DESCRIPTION$4 = `Search GitHub pull requests for code changes, feature implementations, and bug fixes. Find PRs by keywords, state, author, review status, or repository. Returns PR number, title, state, branches, and review information for code review analysis.
113212
+ const DESCRIPTION$4 = `Search GitHub PRs by keywords, state, or author. Returns head/base SHAs for github_fetch_content (branch=SHA). Can fetch PR content changes (diffs/patches) when getChangesContent=true.
113071
113213
 
113072
- INTEGRATION WORKFLOW:
113073
- - Use PR head/base branch names with github_fetch_content to view PR code
113074
- - Repository-specific searches return commit SHAs (head_sha, base_sha) for direct use with github fetch content (branch=SHA)
113075
- - Combine with github_search_commits to find specific commit SHAs from PR
113076
- - Perfect for reviewing implementations and understanding changes`;
113214
+ SEARCH STRATEGY FOR BEST RESULTS:
113215
+ - Use minimal search terms for broader coverage (2-3 words max)
113216
+ - Separate searches for different aspects vs complex queries
113217
+ - Use filters to narrow scope after getting initial results
113218
+ - getChangesContent=true only when analyzing actual code changes`;
113077
113219
  function registerSearchGitHubPullRequestsTool(server) {
113078
113220
  server.registerTool(GITHUB_SEARCH_PULL_REQUESTS_TOOL_NAME, {
113079
113221
  description: DESCRIPTION$4,
@@ -113081,15 +113223,15 @@ function registerSearchGitHubPullRequestsTool(server) {
113081
113223
  query: z
113082
113224
  .string()
113083
113225
  .min(1, 'Search query is required and cannot be empty')
113084
- .describe('Search terms. Start simple: "refactor", "optimization". Use quotes for exact phrases.'),
113226
+ .describe('Search query for PR content (keep minimal for broader results)'),
113085
113227
  owner: z
113086
113228
  .string()
113087
113229
  .optional()
113088
- .describe('Repository owner/organization name only (e.g., "facebook", "microsoft"). Do NOT include repository name. Must be used with repo parameter for repository-specific searches.'),
113230
+ .describe('Repository owner (use with repo param)'),
113089
113231
  repo: z
113090
113232
  .string()
113091
113233
  .optional()
113092
- .describe('Repository name only (e.g., "react", "vscode"). Do NOT include owner prefix. Must be used together with owner parameter.'),
113234
+ .describe('Repository name (use with owner param)'),
113093
113235
  author: z.string().optional().describe('GitHub username of PR author'),
113094
113236
  assignee: z.string().optional().describe('GitHub username of assignee'),
113095
113237
  mentions: z.string().optional().describe('PRs mentioning this user'),
@@ -113106,48 +113248,45 @@ function registerSearchGitHubPullRequestsTool(server) {
113106
113248
  state: z
113107
113249
  .enum(['open', 'closed'])
113108
113250
  .optional()
113109
- .describe('PR state. Default: all'),
113110
- head: z.string().optional().describe('Source branch name'),
113111
- base: z.string().optional().describe('Target branch name'),
113251
+ .describe('Filter by state: open or closed'),
113252
+ head: z.string().optional().describe('Filter on head branch name'),
113253
+ base: z.string().optional().describe('Filter on base branch name'),
113112
113254
  language: z.string().optional().describe('Repository language'),
113113
113255
  created: z
113114
113256
  .string()
113115
113257
  .optional()
113116
- .describe('When created. Format: >2020-01-01'),
113258
+ .describe('Filter by created date (e.g., >2020-01-01)'),
113117
113259
  updated: z
113118
113260
  .string()
113119
113261
  .optional()
113120
- .describe('When updated. Format: >2020-01-01'),
113262
+ .describe('Filter by updated date (e.g., >2020-01-01)'),
113121
113263
  'merged-at': z
113122
113264
  .string()
113123
113265
  .optional()
113124
- .describe('When merged. Format: >2020-01-01'),
113266
+ .describe('Filter by merged date (e.g., >2020-01-01)'),
113125
113267
  closed: z
113126
113268
  .string()
113127
113269
  .optional()
113128
- .describe('When closed. Format: >2020-01-01'),
113129
- draft: z.boolean().optional().describe('Draft PR status'),
113270
+ .describe('Filter by closed date (e.g., >2020-01-01)'),
113271
+ draft: z.boolean().optional().describe('Filter by draft state'),
113130
113272
  checks: z
113131
113273
  .enum(['pending', 'success', 'failure'])
113132
113274
  .optional()
113133
- .describe('CI/CD check status'),
113134
- merged: z
113135
- .boolean()
113136
- .optional()
113137
- .describe('Only merged PRs (true) or unmerged (false)'),
113275
+ .describe('Filter by checks status'),
113276
+ merged: z.boolean().optional().describe('Filter by merged state'),
113138
113277
  review: z
113139
113278
  .enum(['none', 'required', 'approved', 'changes_requested'])
113140
113279
  .optional()
113141
- .describe('Review status filter'),
113142
- app: z.string().optional().describe('GitHub App that created the PR'),
113280
+ .describe('Filter by review status'),
113281
+ app: z.string().optional().describe('Filter by GitHub App author'),
113143
113282
  archived: z
113144
113283
  .boolean()
113145
113284
  .optional()
113146
- .describe('Include archived repositories'),
113285
+ .describe('Filter by repository archived state'),
113147
113286
  comments: z
113148
113287
  .number()
113149
113288
  .optional()
113150
- .describe('Comment count filter. Format: >10, <5, 5..10'),
113289
+ .describe('Filter by number of comments'),
113151
113290
  interactions: z
113152
113291
  .number()
113153
113292
  .optional()
@@ -113155,23 +113294,32 @@ function registerSearchGitHubPullRequestsTool(server) {
113155
113294
  'team-mentions': z
113156
113295
  .string()
113157
113296
  .optional()
113158
- .describe('Team mentioned in PR (@org/team-name)'),
113297
+ .describe('Filter by team mentions'),
113159
113298
  reactions: z
113160
113299
  .number()
113161
113300
  .optional()
113162
- .describe('Reaction count filter. Format: >10'),
113163
- locked: z.boolean().optional().describe('Conversation locked status'),
113164
- 'no-assignee': z.boolean().optional().describe('PRs without assignee'),
113165
- 'no-label': z.boolean().optional().describe('PRs without labels'),
113301
+ .describe('Filter by number of reactions'),
113302
+ locked: z
113303
+ .boolean()
113304
+ .optional()
113305
+ .describe('Filter by locked conversation status'),
113306
+ 'no-assignee': z
113307
+ .boolean()
113308
+ .optional()
113309
+ .describe('Filter by missing assignee'),
113310
+ 'no-label': z.boolean().optional().describe('Filter by missing label'),
113166
113311
  'no-milestone': z
113167
113312
  .boolean()
113168
113313
  .optional()
113169
- .describe('PRs without milestone'),
113170
- 'no-project': z.boolean().optional().describe('PRs not in projects'),
113314
+ .describe('Filter by missing milestone'),
113315
+ 'no-project': z
113316
+ .boolean()
113317
+ .optional()
113318
+ .describe('Filter by missing project'),
113171
113319
  label: z
113172
113320
  .union([z.string(), z.array(z.string())])
113173
113321
  .optional()
113174
- .describe('Label names. Can be single string or array.'),
113322
+ .describe('Filter by label'),
113175
113323
  milestone: z.string().optional().describe('Milestone title'),
113176
113324
  project: z.string().optional().describe('Project board owner/number'),
113177
113325
  visibility: z
@@ -113179,17 +113327,17 @@ function registerSearchGitHubPullRequestsTool(server) {
113179
113327
  .optional()
113180
113328
  .describe('Repository visibility'),
113181
113329
  match: z
113182
- .enum(['title', 'body', 'comments'])
113330
+ .array(z.enum(['title', 'body', 'comments']))
113183
113331
  .optional()
113184
- .describe('Search scope. Default: title and body'),
113332
+ .describe('Restrict search to specific fields'),
113185
113333
  limit: z
113186
113334
  .number()
113187
113335
  .int()
113188
113336
  .min(1)
113189
- .max(50)
113337
+ .max(100)
113190
113338
  .optional()
113191
- .default(25)
113192
- .describe('Results limit (1-50). Default: 25'),
113339
+ .default(30)
113340
+ .describe('Maximum number of results to fetch'),
113193
113341
  sort: z
113194
113342
  .enum([
113195
113343
  'comments',
@@ -113205,15 +113353,20 @@ function registerSearchGitHubPullRequestsTool(server) {
113205
113353
  'updated',
113206
113354
  ])
113207
113355
  .optional()
113208
- .describe('Sort by activity or reactions. Default: best match'),
113356
+ .describe('Sort fetched results'),
113209
113357
  order: z
113210
113358
  .enum(['asc', 'desc'])
113211
113359
  .optional()
113212
113360
  .default('desc')
113213
- .describe('Sort order. Default: desc'),
113361
+ .describe('Order of results (requires --sort)'),
113362
+ getChangesContent: z
113363
+ .boolean()
113364
+ .optional()
113365
+ .default(false)
113366
+ .describe('Get actual code diffs - only use when analyzing changes, not for PR identification'),
113214
113367
  },
113215
113368
  annotations: {
113216
- title: 'GitHub PR Search - Implementation Discovery',
113369
+ title: 'GitHub PR Search - Smart & Effective',
113217
113370
  readOnlyHint: true,
113218
113371
  destructiveHint: false,
113219
113372
  idempotentHint: true,
@@ -113246,6 +113399,52 @@ async function searchGitHubPullRequests(params) {
113246
113399
  const { command, args } = buildGitHubPullRequestsAPICommand(params);
113247
113400
  const result = await executeGitHubCommand(command, args, { cache: false });
113248
113401
  if (result.isError) {
113402
+ const errorMsg = result.content[0].text;
113403
+ // Enhanced error handling for repository-specific searches
113404
+ if (params.owner && params.repo) {
113405
+ // Handle 404 errors with repository and branch checking
113406
+ if (errorMsg.includes('404')) {
113407
+ // Single repository check to avoid duplicate API calls
113408
+ const repoCheckResult = await executeGitHubCommand('api', [`/repos/${params.owner}/${params.repo}`], { cache: false });
113409
+ if (repoCheckResult.isError) {
113410
+ // Repository doesn't exist
113411
+ return createResult({
113412
+ error: `Repository "${params.owner}/${params.repo}" not found. Use github_search_repositories to find the correct repository name.`,
113413
+ });
113414
+ }
113415
+ // Repository exists, check if it's a branch-related error
113416
+ if (params.head || params.base) {
113417
+ try {
113418
+ const repoData = JSON.parse(repoCheckResult.content[0].text);
113419
+ const defaultBranch = repoData.result?.default_branch || 'main';
113420
+ let branchSuggestion = '';
113421
+ if (params.head && params.head !== defaultBranch) {
113422
+ branchSuggestion = `\n\nNote: Branch "${params.head}" may not exist. Repository default branch is "${defaultBranch}".`;
113423
+ branchSuggestion += `\nTry searching without head branch filter or use head="${defaultBranch}".`;
113424
+ }
113425
+ if (params.base && params.base !== defaultBranch) {
113426
+ branchSuggestion += `\n\nNote: Branch "${params.base}" may not exist. Repository default branch is "${defaultBranch}".`;
113427
+ branchSuggestion += `\nTry searching without base branch filter or use base="${defaultBranch}".`;
113428
+ }
113429
+ if (branchSuggestion) {
113430
+ return createResult({
113431
+ error: createNoResultsError('pull_requests') + branchSuggestion,
113432
+ });
113433
+ }
113434
+ }
113435
+ catch (e) {
113436
+ // Continue with original error if parsing fails
113437
+ }
113438
+ }
113439
+ }
113440
+ // Handle rate limit errors
113441
+ if (errorMsg.includes('rate limit') ||
113442
+ errorMsg.includes('API rate limit')) {
113443
+ return createResult({
113444
+ error: ERROR_MESSAGES.RATE_LIMIT_EXCEEDED,
113445
+ });
113446
+ }
113447
+ }
113249
113448
  return result;
113250
113449
  }
113251
113450
  const execResult = JSON.parse(result.content[0].text);
@@ -113255,8 +113454,97 @@ async function searchGitHubPullRequests(params) {
113255
113454
  ? execResult.result
113256
113455
  : execResult.result?.items || [];
113257
113456
  if (pullRequests.length === 0) {
113457
+ // Progressive simplification strategy based on current search complexity
113458
+ const simplificationSteps = [];
113459
+ let hasFilters = false;
113460
+ // Check for active filters
113461
+ const activeFilters = [];
113462
+ if (params.owner && params.repo)
113463
+ activeFilters.push('repo');
113464
+ if (params.author)
113465
+ activeFilters.push('author');
113466
+ if (params.state)
113467
+ activeFilters.push('state');
113468
+ if (params.label)
113469
+ activeFilters.push('label');
113470
+ if (params.head)
113471
+ activeFilters.push('head-branch');
113472
+ if (params.base)
113473
+ activeFilters.push('base-branch');
113474
+ if (params.assignee)
113475
+ activeFilters.push('assignee');
113476
+ if (params.created)
113477
+ activeFilters.push('date');
113478
+ hasFilters = activeFilters.length > 0;
113479
+ // Step 1: If complex query, simplify search terms
113480
+ if (params.query && params.query.trim().split(' ').length > 2) {
113481
+ const words = params.query.trim().split(' ');
113482
+ const simplified = words.slice(0, 1).join(' '); // Take first word only
113483
+ simplificationSteps.push(`Try simpler search: "${simplified}" instead of "${params.query}"`);
113484
+ }
113485
+ // Step 2: Remove filters progressively
113486
+ if (hasFilters) {
113487
+ if (activeFilters.length > 2) {
113488
+ simplificationSteps.push(`Remove some filters (currently: ${activeFilters.join(', ')}) and keep only the most important ones`);
113489
+ }
113490
+ else if (activeFilters.length > 1) {
113491
+ simplificationSteps.push(`Remove one filter (currently: ${activeFilters.join(', ')}) to broaden search`);
113492
+ }
113493
+ else {
113494
+ simplificationSteps.push(`Remove the ${activeFilters[0]} filter and search more broadly`);
113495
+ }
113496
+ }
113497
+ // Step 3: Alternative approaches
113498
+ if (!params.query && hasFilters) {
113499
+ simplificationSteps.push('Add basic search terms like "fix", "feature", "bug", or "update" with your filters');
113500
+ }
113501
+ // Step 4: Repository-specific guidance
113502
+ if (!params.owner && !params.repo) {
113503
+ simplificationSteps.push('Try searching in a specific repository first using owner and repo parameters');
113504
+ }
113505
+ // Step 5: Ask for user guidance if no obvious simplification
113506
+ if (simplificationSteps.length === 0) {
113507
+ simplificationSteps.push("Try different keywords or ask the user to be more specific about what PRs they're looking for");
113508
+ }
113258
113509
  return createResult({
113259
- error: createNoResultsError('pull_requests'),
113510
+ error: `${createNoResultsError('pull_requests')}
113511
+
113512
+ Try these simplified searches:
113513
+ ${simplificationSteps.map(step => `• ${step}`).join('\n')}
113514
+
113515
+ Or ask the user:
113516
+ • "What specific type of pull requests are you looking for?"
113517
+ • "Can you provide different keywords to search for?"
113518
+ • "Should I search in a specific repository instead?"
113519
+ • "Are you looking for open or closed PRs?"
113520
+
113521
+ Alternative tools:
113522
+ • Use github_search_code for PR-related file changes
113523
+ • Use github_search_repos to find repositories first`,
113524
+ });
113525
+ }
113526
+ // Fetch diff information if requested and this is a repo-specific search
113527
+ const shouldFetchDiff = params.getChangesContent && params.owner && params.repo;
113528
+ const diffData = new Map();
113529
+ if (shouldFetchDiff && pullRequests.length > 0) {
113530
+ // Fetch diff info for each PR (limit to first 10 to avoid rate limits)
113531
+ const prNumbers = pullRequests.slice(0, 10).map((pr) => pr.number);
113532
+ const diffPromises = prNumbers.map(async (prNumber) => {
113533
+ try {
113534
+ const diffResult = await executeGitHubCommand('api', [`/repos/${params.owner}/${params.repo}/pulls/${prNumber}/files`], { cache: false });
113535
+ if (!diffResult.isError) {
113536
+ const diffExecResult = JSON.parse(diffResult.content[0].text);
113537
+ return { prNumber, files: diffExecResult.result };
113538
+ }
113539
+ }
113540
+ catch (e) {
113541
+ // Ignore diff fetch errors
113542
+ }
113543
+ return { prNumber, files: [] };
113544
+ });
113545
+ const diffResults = await Promise.all(diffPromises);
113546
+ diffResults.forEach(({ prNumber, files }) => {
113547
+ diffData.set(prNumber, files);
113260
113548
  });
113261
113549
  }
113262
113550
  const cleanPRs = pullRequests.map((pr) => {
@@ -113276,15 +113564,38 @@ async function searchGitHubPullRequests(params) {
113276
113564
  reactions: 0, // Not available in list format
113277
113565
  draft: pr.isDraft || false,
113278
113566
  };
113279
- // Add commit SHAs - this is the key enhancement!
113567
+ // Add commit SHAs for use with github_fetch_content
113568
+ // Use head_sha/base_sha as branch parameter to view PR files
113280
113569
  if (pr.headRefName)
113281
113570
  result.head = pr.headRefName;
113282
113571
  if (pr.headRefOid)
113283
- result.head_sha = pr.headRefOid;
113572
+ result.head_sha = pr.headRefOid; // Use as branch=SHA
113284
113573
  if (pr.baseRefName)
113285
113574
  result.base = pr.baseRefName;
113286
113575
  if (pr.baseRefOid)
113287
- result.base_sha = pr.baseRefOid;
113576
+ result.base_sha = pr.baseRefOid; // Use as branch=SHA
113577
+ // Add diff information if available
113578
+ if (shouldFetchDiff && diffData.has(pr.number)) {
113579
+ const files = diffData.get(pr.number);
113580
+ result.diff = {
113581
+ changed_files: files.length,
113582
+ additions: files.reduce((sum, f) => sum + (f.additions || 0), 0),
113583
+ deletions: files.reduce((sum, f) => sum + (f.deletions || 0), 0),
113584
+ files: files
113585
+ .map((f) => ({
113586
+ filename: f.filename,
113587
+ status: f.status,
113588
+ additions: f.additions,
113589
+ deletions: f.deletions,
113590
+ changes: f.changes,
113591
+ patch: f.patch
113592
+ ? f.patch.substring(0, 1000) +
113593
+ (f.patch.length > 1000 ? '...' : '')
113594
+ : undefined,
113595
+ }))
113596
+ .slice(0, 5), // Limit to 5 files per PR
113597
+ };
113598
+ }
113288
113599
  return result;
113289
113600
  }
113290
113601
  // Handle search API format
@@ -113311,6 +113622,28 @@ async function searchGitHubPullRequests(params) {
113311
113622
  result.head = pr.head.ref;
113312
113623
  if (pr.base?.ref)
113313
113624
  result.base = pr.base.ref;
113625
+ // Add diff information if available (search API format)
113626
+ if (shouldFetchDiff && diffData.has(pr.number)) {
113627
+ const files = diffData.get(pr.number);
113628
+ result.diff = {
113629
+ changed_files: files.length,
113630
+ additions: files.reduce((sum, f) => sum + (f.additions || 0), 0),
113631
+ deletions: files.reduce((sum, f) => sum + (f.deletions || 0), 0),
113632
+ files: files
113633
+ .map((f) => ({
113634
+ filename: f.filename,
113635
+ status: f.status,
113636
+ additions: f.additions,
113637
+ deletions: f.deletions,
113638
+ changes: f.changes,
113639
+ patch: f.patch
113640
+ ? f.patch.substring(0, 1000) +
113641
+ (f.patch.length > 1000 ? '...' : '')
113642
+ : undefined,
113643
+ }))
113644
+ .slice(0, 5), // Limit to 5 files per PR
113645
+ };
113646
+ }
113314
113647
  return result;
113315
113648
  });
113316
113649
  const searchResult = {
@@ -113319,11 +113652,25 @@ async function searchGitHubPullRequests(params) {
113319
113652
  ? cleanPRs.length
113320
113653
  : execResult.result?.total_count || cleanPRs.length,
113321
113654
  };
113322
- return createResult({ data: searchResult });
113655
+ // Add helpful context if filtering by branches that might not exist
113656
+ let additionalContext = '';
113657
+ if (cleanPRs.length === 0 &&
113658
+ params.owner &&
113659
+ params.repo &&
113660
+ (params.head || params.base)) {
113661
+ additionalContext =
113662
+ '\n\nNote: No PRs found with specified branch filters. Consider removing head/base filters or checking if branches exist.';
113663
+ }
113664
+ return createResult({
113665
+ data: searchResult,
113666
+ ...(additionalContext && { message: additionalContext }),
113667
+ });
113323
113668
  });
113324
113669
  }
113325
113670
  function buildGitHubPullRequestsAPICommand(params) {
113326
- // For repository-specific searches, use gh pr list to get commit SHAs
113671
+ // For repository-specific searches, use gh pr list instead of search API
113672
+ // This provides commit SHAs (head_sha, base_sha) which are essential for
113673
+ // integrating with github_fetch_content to view PR code at specific commits
113327
113674
  if (params.owner && params.repo) {
113328
113675
  return buildGitHubPullRequestsListCommand(params);
113329
113676
  }
@@ -113401,10 +113748,13 @@ function buildGitHubPullRequestsAPICommand(params) {
113401
113748
  queryParts.push(`milestone:"${params.milestone}"`);
113402
113749
  if (params.project)
113403
113750
  queryParts.push(`project:${params.project}`);
113751
+ if (params.match) {
113752
+ params.match.forEach(field => queryParts.push(`in:${field}`));
113753
+ }
113404
113754
  // Add type qualifier to search only pull requests
113405
113755
  queryParts.push('type:pr');
113406
113756
  const query = queryParts.filter(Boolean).join(' ');
113407
- const limit = Math.min(params.limit || 25, 100);
113757
+ const limit = Math.min(params.limit || 30, 100);
113408
113758
  let apiPath = `search/issues?q=${encodeURIComponent(query)}&per_page=${limit}`;
113409
113759
  if (params.sort)
113410
113760
  apiPath += `&sort=${params.sort}`;
@@ -113420,7 +113770,7 @@ function buildGitHubPullRequestsListCommand(params) {
113420
113770
  '--json',
113421
113771
  'number,title,headRefName,headRefOid,baseRefName,baseRefOid,state,author,labels,createdAt,updatedAt,url,comments,isDraft',
113422
113772
  '--limit',
113423
- String(Math.min(params.limit || 25, 100)),
113773
+ String(Math.min(params.limit || 30, 100)),
113424
113774
  ];
113425
113775
  // Add filters
113426
113776
  if (params.state) {
@@ -113756,8 +114106,10 @@ async function viewRepositoryStructure(params) {
113756
114106
  try {
113757
114107
  // Clean up path
113758
114108
  const cleanPath = path.startsWith('/') ? path.substring(1) : path;
113759
- // Try the requested branch first
113760
- const apiPath = `/repos/${owner}/${repo}/contents/${cleanPath}?ref=${branch}`;
114109
+ // Try the requested branch first - handle empty path correctly
114110
+ const apiPath = cleanPath
114111
+ ? `/repos/${owner}/${repo}/contents/${cleanPath}?ref=${branch}`
114112
+ : `/repos/${owner}/${repo}/contents?ref=${branch}`;
113761
114113
  const result = await executeGitHubCommand('api', [apiPath], {
113762
114114
  cache: false,
113763
114115
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "octocode-mcp",
3
- "version": "2.3.15",
3
+ "version": "2.3.16",
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://octocode.ai",