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