@@ -163,6 +163,8 @@ interface WorkflowRunRaw {
163163 created_at : string ;
164164 updated_at : string ;
165165 run_started_at : string ;
166+ // BUG-007: completed_at is present in GitHub API response but was missing from the interface.
167+ completed_at : string | null ;
166168 run_attempt : number ;
167169 display_title : string ;
168170 actor : { login : string } | null ;
@@ -187,7 +189,7 @@ function mapWorkflowRun(raw: WorkflowRunRaw, repoFullName: string): WorkflowRun
187189 repoFullName,
188190 isPrRun : Array . isArray ( raw . pull_requests ) && raw . pull_requests . length > 0 ,
189191 runStartedAt : raw . run_started_at ?? raw . created_at ,
190- completedAt : null ,
192+ completedAt : raw . completed_at ?? null ,
191193 runAttempt : raw . run_attempt ?? 1 ,
192194 displayTitle : raw . display_title ?? raw . name ?? "" ,
193195 actorLogin : raw . actor ?. login ?? "" ,
@@ -207,12 +209,11 @@ export class OctokitDataSource implements DataSource {
207209
208210 private async getLogin ( ) : Promise < string > {
209211 if ( this . _login ) return this . _login ;
210- try {
211- const { data } = await this . octokit . request ( "GET /user" ) ;
212- this . _login = ( data as { login : string } ) . login ;
213- } catch {
214- this . _login = "" ;
215- }
212+ // BUG-006: Throw if login cannot be determined to prevent empty `involves:` query strings.
213+ const { data } = await this . octokit . request ( "GET /user" ) ;
214+ const login = ( data as { login : string } ) . login ;
215+ if ( ! login ) throw new Error ( "Could not determine authenticated user login from GET /user" ) ;
216+ this . _login = login ;
216217 return this . _login ;
217218 }
218219
@@ -221,21 +222,38 @@ export class OctokitDataSource implements DataSource {
221222 const repos = resolveRepos ( repo ) ;
222223 const results : PullRequest [ ] = [ ] ;
223224
224- for ( const r of repos ) {
225- const q = `is:pr+is:open+involves:${ login } +repo:${ r . owner } /${ r . name } ` ;
226- try {
227- const { data } = await this . octokit . request ( "GET /search/issues" , {
228- q,
229- per_page : 100 ,
230- } ) ;
231- const items = ( data as { items : SearchItem [ ] } ) . items ?? [ ] ;
232- for ( const item of items ) {
233- if ( item . pull_request !== undefined ) {
234- results . push ( mapSearchItemToPR ( item , r . fullName ) ) ;
225+ // PERF-001: Batch repos into groups of 20 to avoid N+1 REST calls.
226+ // GitHub search supports multiple repo: qualifiers in a single query.
227+ const BATCH_SIZE = 20 ;
228+ const batches : RepoRef [ ] [ ] = [ ] ;
229+ for ( let i = 0 ; i < repos . length ; i += BATCH_SIZE ) {
230+ batches . push ( repos . slice ( i , i + BATCH_SIZE ) ) ;
231+ }
232+
233+ const batchResults = await Promise . allSettled (
234+ batches . map ( ( batch ) => {
235+ const repoFilter = batch . map ( ( r ) => `repo:${ r . owner } /${ r . name } ` ) . join ( "+" ) ;
236+ const q = `is:pr+is:open+involves:${ login } +${ repoFilter } ` ;
237+ return this . octokit . request ( "GET /search/issues" , { q, per_page : 100 } ) . then ( ( { data } ) => {
238+ const items = ( data as { items : SearchItem [ ] } ) . items ?? [ ] ;
239+ const prs : PullRequest [ ] = [ ] ;
240+ for ( const item of items ) {
241+ if ( item . pull_request !== undefined ) {
242+ // Derive repo from repository_url (last two segments: owner/name)
243+ const repoFullName = item . repository_url . replace ( "https://api.github.com/repos/" , "" ) ;
244+ prs . push ( mapSearchItemToPR ( item , repoFullName ) ) ;
245+ }
235246 }
236- }
237- } catch ( err ) {
238- console . error ( `[mcp] getOpenPRs error for ${ r . fullName } :` , err instanceof Error ? err . message : String ( err ) ) ;
247+ return prs ;
248+ } ) ;
249+ } )
250+ ) ;
251+
252+ for ( const settled of batchResults ) {
253+ if ( settled . status === "fulfilled" ) {
254+ results . push ( ...settled . value ) ;
255+ } else {
256+ console . error ( "[mcp] getOpenPRs batch error:" , settled . reason instanceof Error ? settled . reason . message : String ( settled . reason ) ) ;
239257 }
240258 }
241259
@@ -259,22 +277,37 @@ export class OctokitDataSource implements DataSource {
259277 const repos = resolveRepos ( repo ) ;
260278 const results : Issue [ ] = [ ] ;
261279
262- for ( const r of repos ) {
263- const q = `is:issue+is:open+involves:${ login } +repo:${ r . owner } /${ r . name } ` ;
264- try {
265- const { data } = await this . octokit . request ( "GET /search/issues" , {
266- q,
267- per_page : 100 ,
268- } ) ;
269- const items = ( data as { items : SearchItem [ ] } ) . items ?? [ ] ;
270- for ( const item of items ) {
271- // Filter out PRs from issue search
272- if ( item . pull_request === undefined ) {
273- results . push ( mapSearchItemToIssue ( item , r . fullName ) ) ;
280+ // PERF-002: Batch repos into groups of 20 to avoid N+1 REST calls.
281+ const BATCH_SIZE = 20 ;
282+ const batches : RepoRef [ ] [ ] = [ ] ;
283+ for ( let i = 0 ; i < repos . length ; i += BATCH_SIZE ) {
284+ batches . push ( repos . slice ( i , i + BATCH_SIZE ) ) ;
285+ }
286+
287+ const batchResults = await Promise . allSettled (
288+ batches . map ( ( batch ) => {
289+ const repoFilter = batch . map ( ( r ) => `repo:${ r . owner } /${ r . name } ` ) . join ( "+" ) ;
290+ const q = `is:issue+is:open+involves:${ login } +${ repoFilter } ` ;
291+ return this . octokit . request ( "GET /search/issues" , { q, per_page : 100 } ) . then ( ( { data } ) => {
292+ const items = ( data as { items : SearchItem [ ] } ) . items ?? [ ] ;
293+ const issues : Issue [ ] = [ ] ;
294+ for ( const item of items ) {
295+ // Filter out PRs from issue search
296+ if ( item . pull_request === undefined ) {
297+ const repoFullName = item . repository_url . replace ( "https://api.github.com/repos/" , "" ) ;
298+ issues . push ( mapSearchItemToIssue ( item , repoFullName ) ) ;
299+ }
274300 }
275- }
276- } catch ( err ) {
277- console . error ( `[mcp] getOpenIssues error for ${ r . fullName } :` , err instanceof Error ? err . message : String ( err ) ) ;
301+ return issues ;
302+ } ) ;
303+ } )
304+ ) ;
305+
306+ for ( const settled of batchResults ) {
307+ if ( settled . status === "fulfilled" ) {
308+ results . push ( ...settled . value ) ;
309+ } else {
310+ console . error ( "[mcp] getOpenIssues batch error:" , settled . reason instanceof Error ? settled . reason . message : String ( settled . reason ) ) ;
278311 }
279312 }
280313
@@ -283,22 +316,32 @@ export class OctokitDataSource implements DataSource {
283316
284317 async getFailingActions ( repo ?: string ) : Promise < WorkflowRun [ ] > {
285318 const repos = resolveRepos ( repo ) ;
286- const results : WorkflowRun [ ] = [ ] ;
287319
288- for ( const r of repos ) {
289- for ( const status of [ "in_progress" , "failure" ] as const ) {
290- try {
291- const { data } = await this . octokit . request (
292- "GET /repos/{owner}/{repo}/actions/runs" ,
293- { owner : r . owner , repo : r . name , status, per_page : 20 }
294- ) ;
320+ // PERF-003: Collect all {repo, status} pairs and run them in parallel.
321+ const pairs = repos . flatMap ( ( r ) =>
322+ ( [ "in_progress" , "failure" ] as const ) . map ( ( status ) => ( { r, status } ) )
323+ ) ;
324+
325+ const settled = await Promise . allSettled (
326+ pairs . map ( ( { r, status } ) =>
327+ this . octokit . request (
328+ "GET /repos/{owner}/{repo}/actions/runs" ,
329+ { owner : r . owner , repo : r . name , status, per_page : 20 }
330+ ) . then ( ( { data } ) => {
295331 const runs = ( data as { workflow_runs : WorkflowRunRaw [ ] } ) . workflow_runs ?? [ ] ;
296- for ( const run of runs ) {
297- results . push ( mapWorkflowRun ( run , r . fullName ) ) ;
298- }
299- } catch ( err ) {
300- console . error ( `[mcp] getFailingActions error for ${ r . fullName } (${ status } ):` , err instanceof Error ? err . message : String ( err ) ) ;
301- }
332+ return runs . map ( ( run ) => mapWorkflowRun ( run , r . fullName ) ) ;
333+ } )
334+ )
335+ ) ;
336+
337+ const results : WorkflowRun [ ] = [ ] ;
338+ for ( let i = 0 ; i < settled . length ; i ++ ) {
339+ const result = settled [ i ] ;
340+ if ( result . status === "fulfilled" ) {
341+ results . push ( ...result . value ) ;
342+ } else {
343+ const { r, status } = pairs [ i ] ;
344+ console . error ( `[mcp] getFailingActions error for ${ r . fullName } (${ status } ):` , result . reason instanceof Error ? result . reason . message : String ( result . reason ) ) ;
302345 }
303346 }
304347
@@ -420,16 +463,19 @@ export class OctokitDataSource implements DataSource {
420463 console . error ( "[mcp] getDashboardSummary review count error:" , err instanceof Error ? err . message : String ( err ) ) ;
421464 }
422465
423- // Failing runs: count across all repos
424- for ( const r of repos ) {
425- try {
426- const { data : runData } = await this . octokit . request (
466+ // Failing runs: count across all repos (BUG-008: use total_count, not repo presence).
467+ // Run in parallel with Promise.allSettled for performance (PERF-003).
468+ const failingRunResults = await Promise . allSettled (
469+ repos . map ( ( r ) =>
470+ this . octokit . request (
427471 "GET /repos/{owner}/{repo}/actions/runs" ,
428472 { owner : r . owner , repo : r . name , status : "failure" , per_page : 5 }
429- ) ;
430- failingRunCount += ( runData as { total_count : number } ) . total_count > 0 ? 1 : 0 ;
431- } catch {
432- // best-effort
473+ )
474+ )
475+ ) ;
476+ for ( const settled of failingRunResults ) {
477+ if ( settled . status === "fulfilled" ) {
478+ failingRunCount += ( settled . value . data as { total_count : number } ) . total_count ;
433479 }
434480 }
435481
@@ -472,11 +518,18 @@ export class WebSocketDataSource implements DataSource {
472518 }
473519
474520 async getRateLimit ( ) : Promise < RateLimitInfo > {
475- const raw = await sendRelayRequest ( METHODS . GET_RATE_LIMIT , { } ) as { limit : number ; remaining : number ; resetAt : string } ;
521+ // BUG-002: SPA relay returns { core: {...}, graphql: {...} } — unwrap the core property.
522+ const raw = await sendRelayRequest ( METHODS . GET_RATE_LIMIT , { } ) as {
523+ core ?: { limit : number ; remaining : number ; resetAt : string } ;
524+ limit ?: number ;
525+ remaining ?: number ;
526+ resetAt ?: string ;
527+ } ;
528+ const core = raw . core ?? ( raw as { limit : number ; remaining : number ; resetAt : string } ) ;
476529 return {
477- limit : raw . limit ,
478- remaining : raw . remaining ,
479- resetAt : new Date ( raw . resetAt ) ,
530+ limit : core . limit ,
531+ remaining : core . remaining ,
532+ resetAt : new Date ( core . resetAt ) ,
480533 } ;
481534 }
482535
0 commit comments