memory-journal-mcp 4.3.0 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/.dockerignore +131 -122
  2. package/.gitattributes +29 -0
  3. package/.github/workflows/docker-publish.yml +1 -1
  4. package/.github/workflows/lint-and-test.yml +1 -2
  5. package/.github/workflows/secrets-scanning.yml +0 -1
  6. package/.github/workflows/security-update.yml +6 -6
  7. package/.vscode/settings.json +17 -15
  8. package/CHANGELOG.md +1065 -11
  9. package/DOCKER_README.md +51 -33
  10. package/Dockerfile +14 -12
  11. package/README.md +68 -33
  12. package/SECURITY.md +225 -220
  13. package/dist/cli.js +7 -0
  14. package/dist/cli.js.map +1 -1
  15. package/dist/constants/ServerInstructions.d.ts +1 -1
  16. package/dist/constants/ServerInstructions.d.ts.map +1 -1
  17. package/dist/constants/ServerInstructions.js +70 -26
  18. package/dist/constants/ServerInstructions.js.map +1 -1
  19. package/dist/constants/icons.d.ts +2 -0
  20. package/dist/constants/icons.d.ts.map +1 -1
  21. package/dist/constants/icons.js +6 -0
  22. package/dist/constants/icons.js.map +1 -1
  23. package/dist/database/SqliteAdapter.d.ts +51 -10
  24. package/dist/database/SqliteAdapter.d.ts.map +1 -1
  25. package/dist/database/SqliteAdapter.js +143 -43
  26. package/dist/database/SqliteAdapter.js.map +1 -1
  27. package/dist/filtering/ToolFilter.d.ts +1 -1
  28. package/dist/filtering/ToolFilter.d.ts.map +1 -1
  29. package/dist/filtering/ToolFilter.js +7 -1
  30. package/dist/filtering/ToolFilter.js.map +1 -1
  31. package/dist/github/GitHubIntegration.d.ts +74 -2
  32. package/dist/github/GitHubIntegration.d.ts.map +1 -1
  33. package/dist/github/GitHubIntegration.js +508 -7
  34. package/dist/github/GitHubIntegration.js.map +1 -1
  35. package/dist/handlers/prompts/index.js +1 -0
  36. package/dist/handlers/prompts/index.js.map +1 -1
  37. package/dist/handlers/resources/index.d.ts.map +1 -1
  38. package/dist/handlers/resources/index.js +257 -13
  39. package/dist/handlers/resources/index.js.map +1 -1
  40. package/dist/handlers/tools/index.d.ts.map +1 -1
  41. package/dist/handlers/tools/index.js +595 -8
  42. package/dist/handlers/tools/index.js.map +1 -1
  43. package/dist/server/McpServer.d.ts +2 -0
  44. package/dist/server/McpServer.d.ts.map +1 -1
  45. package/dist/server/McpServer.js +69 -26
  46. package/dist/server/McpServer.js.map +1 -1
  47. package/dist/types/index.d.ts +97 -0
  48. package/dist/types/index.d.ts.map +1 -1
  49. package/dist/types/index.js.map +1 -1
  50. package/dist/utils/logger.d.ts +1 -0
  51. package/dist/utils/logger.d.ts.map +1 -1
  52. package/dist/utils/logger.js +8 -1
  53. package/dist/utils/logger.js.map +1 -1
  54. package/dist/utils/progress-utils.d.ts +18 -3
  55. package/dist/utils/progress-utils.d.ts.map +1 -1
  56. package/dist/utils/progress-utils.js.map +1 -1
  57. package/dist/utils/security-utils.d.ts +91 -0
  58. package/dist/utils/security-utils.d.ts.map +1 -0
  59. package/dist/utils/security-utils.js +184 -0
  60. package/dist/utils/security-utils.js.map +1 -0
  61. package/dist/vector/VectorSearchManager.d.ts +2 -1
  62. package/dist/vector/VectorSearchManager.d.ts.map +1 -1
  63. package/dist/vector/VectorSearchManager.js +100 -34
  64. package/dist/vector/VectorSearchManager.js.map +1 -1
  65. package/docker-compose.yml +46 -37
  66. package/mcp-config-example.json +0 -2
  67. package/package.json +21 -14
  68. package/releases/v4.3.1.md +69 -0
  69. package/releases/v4.4.0.md +120 -0
  70. package/server.json +3 -3
  71. package/src/cli.ts +11 -0
  72. package/src/constants/ServerInstructions.ts +70 -26
  73. package/src/constants/icons.ts +7 -0
  74. package/src/database/SqliteAdapter.ts +165 -44
  75. package/src/filtering/ToolFilter.ts +7 -1
  76. package/src/github/GitHubIntegration.ts +588 -8
  77. package/src/handlers/prompts/index.ts +1 -0
  78. package/src/handlers/resources/index.ts +318 -12
  79. package/src/handlers/tools/index.ts +686 -13
  80. package/src/server/McpServer.ts +79 -37
  81. package/src/types/index.ts +98 -0
  82. package/src/utils/logger.ts +10 -1
  83. package/src/utils/progress-utils.ts +17 -6
  84. package/src/utils/security-utils.ts +205 -0
  85. package/src/vector/VectorSearchManager.ts +110 -39
  86. package/tests/constants/icons.test.ts +102 -0
  87. package/tests/constants/server-instructions.test.ts +549 -0
  88. package/tests/database/sqlite-adapter.bench.ts +63 -0
  89. package/tests/database/sqlite-adapter.test.ts +555 -0
  90. package/tests/filtering/tool-filter.test.ts +266 -0
  91. package/tests/github/github-integration.test.ts +1024 -0
  92. package/tests/handlers/github-resource-handlers.test.ts +473 -0
  93. package/tests/handlers/github-tool-handlers.test.ts +556 -0
  94. package/tests/handlers/prompt-handlers.test.ts +91 -0
  95. package/tests/handlers/resource-handlers.test.ts +339 -0
  96. package/tests/handlers/tool-handlers.test.ts +497 -0
  97. package/tests/handlers/vector-tool-handlers.test.ts +238 -0
  98. package/tests/security/sql-injection.test.ts +347 -0
  99. package/tests/server/mcp-server.bench.ts +55 -0
  100. package/tests/server/mcp-server.test.ts +675 -0
  101. package/tests/utils/logger.test.ts +180 -0
  102. package/tests/utils/mcp-logger.test.ts +212 -0
  103. package/tests/utils/progress-utils.test.ts +156 -0
  104. package/tests/utils/security-utils.test.ts +82 -0
  105. package/tests/vector/vector-search-manager.test.ts +335 -0
  106. package/tests/vector/vector-search.bench.ts +53 -0
  107. package/vitest.config.ts +15 -0
  108. package/.github/workflows/DOCKER_DEPLOYMENT_SETUP.md +0 -387
  109. package/.github/workflows/dependabot-auto-merge.yml +0 -42
@@ -572,6 +572,7 @@ ${entrySummary}
572
572
  **For More Context:**
573
573
  - Full entries: \`memory://recent\` or \`get_entry_by_id(ID)\`
574
574
  - GitHub status: \`memory://github/status\`
575
+ - Repo insights: \`memory://github/insights\` (stars, traffic, clones)
575
576
  - Full health: \`memory://health\`
576
577
 
577
578
  Please confirm this context to the user in a concise, friendly format. Use a table if helpful.`,
@@ -7,6 +7,7 @@
7
7
  import type { SqliteAdapter } from '../../database/SqliteAdapter.js'
8
8
  import type { VectorSearchManager } from '../../vector/VectorSearchManager.js'
9
9
  import type { ToolFilterConfig } from '../../filtering/ToolFilter.js'
10
+ import { getAllToolNames } from '../../filtering/ToolFilter.js'
10
11
  import type { Tag, McpIcon } from '../../types/index.js'
11
12
  import type { GitHubIntegration } from '../../github/GitHubIntegration.js'
12
13
  import { generateInstructions, type InstructionLevel } from '../../constants/ServerInstructions.js'
@@ -17,6 +18,7 @@ import {
17
18
  ICON_GRAPH,
18
19
  ICON_HEALTH,
19
20
  ICON_GITHUB,
21
+ ICON_MILESTONE,
20
22
  ICON_STAR,
21
23
  ICON_TAG,
22
24
  ICON_TEAM,
@@ -192,8 +194,7 @@ function execQuery(
192
194
  * Get total tool count for health status
193
195
  */
194
196
  function getTotalToolCount(): number {
195
- // Import dynamically to avoid circular dependency
196
- return 33 // 6 core + 4 search + 2 analytics + 2 relationships + 1 export + 5 admin + 9 github + 4 backup
197
+ return getAllToolNames().length
197
198
  }
198
199
 
199
200
  /**
@@ -261,6 +262,13 @@ function getAllResourceDefinitions(): InternalResourceDef[] {
261
262
  ci: 'passing' | 'failing' | 'pending' | 'cancelled' | 'unknown'
262
263
  openIssues: number
263
264
  openPRs: number
265
+ milestones: { title: string; progress: string; dueOn: string | null }[]
266
+ insights?: {
267
+ stars: number | null
268
+ forks: number | null
269
+ clones14d?: number
270
+ views14d?: number
271
+ }
264
272
  } | null = null
265
273
 
266
274
  if (context.github) {
@@ -328,12 +336,75 @@ function getAllResourceDefinitions(): InternalResourceDef[] {
328
336
  // Counts unavailable
329
337
  }
330
338
 
339
+ // Get milestone summary for briefing
340
+ let milestones: {
341
+ title: string
342
+ progress: string
343
+ dueOn: string | null
344
+ }[] = []
345
+ try {
346
+ const msList = await context.github.getMilestones(
347
+ owner,
348
+ repo,
349
+ 'open',
350
+ 3
351
+ )
352
+ milestones = msList.map((m) => {
353
+ const total = m.closedIssues + m.openIssues
354
+ const pct =
355
+ total > 0 ? Math.round((m.closedIssues / total) * 100) : 0
356
+ return {
357
+ title: m.title,
358
+ progress: `${String(pct)}%`,
359
+ dueOn: m.dueOn,
360
+ }
361
+ })
362
+ } catch {
363
+ // Milestones unavailable
364
+ }
365
+
366
+ // Get repo insights (stars, forks, traffic)
367
+ let insights:
368
+ | {
369
+ stars: number | null
370
+ forks: number | null
371
+ clones14d?: number
372
+ views14d?: number
373
+ }
374
+ | undefined = undefined
375
+ try {
376
+ const repoStats = await context.github.getRepoStats(owner, repo)
377
+ if (repoStats) {
378
+ insights = {
379
+ stars: repoStats.stars ?? null,
380
+ forks: repoStats.forks ?? null,
381
+ }
382
+ // Traffic requires push access - may fail
383
+ try {
384
+ const trafficData = await context.github.getTrafficData(
385
+ owner,
386
+ repo
387
+ )
388
+ if (trafficData) {
389
+ insights.clones14d = trafficData.clones.total
390
+ insights.views14d = trafficData.views.total
391
+ }
392
+ } catch {
393
+ // Traffic data unavailable (requires push access)
394
+ }
395
+ }
396
+ } catch {
397
+ // Repo stats unavailable
398
+ }
399
+
331
400
  github = {
332
401
  repo: `${owner}/${repo}`,
333
402
  branch: repoInfo.branch ?? null,
334
403
  ci: ciStatus,
335
404
  openIssues,
336
405
  openPRs,
406
+ milestones,
407
+ insights,
337
408
  }
338
409
  }
339
410
  } catch {
@@ -356,6 +427,29 @@ function getAllResourceDefinitions(): InternalResourceDef[] {
356
427
  ? `#${latestEntries[0].id} (${latestEntries[0].type}): ${latestEntries[0].preview}`
357
428
  : 'No entries yet'
358
429
 
430
+ const milestoneRow =
431
+ github?.milestones && github.milestones.length > 0
432
+ ? `\n| **Milestones** | ${github.milestones.map((m) => `${m.title} (${m.progress}${m.dueOn ? `, due ${m.dueOn.split('T')[0] ?? ''}` : ''})`).join(', ')} |`
433
+ : ''
434
+
435
+ // Build insights row for userMessage
436
+ let insightsRow = ''
437
+ if (github?.insights) {
438
+ const parts: string[] = []
439
+ if (github.insights.stars !== null)
440
+ parts.push(`⭐ ${String(github.insights.stars)} stars`)
441
+ if (github.insights.forks !== null)
442
+ parts.push(`🍴 ${String(github.insights.forks)} forks`)
443
+ if (github.insights.clones14d !== undefined)
444
+ parts.push(`📦 ${String(github.insights.clones14d)} clones`)
445
+ if (github.insights.views14d !== undefined)
446
+ parts.push(`👁️ ${String(github.insights.views14d)} views`)
447
+ if (parts.length > 0) {
448
+ const trafficNote = github.insights.clones14d !== undefined ? ' (14d)' : ''
449
+ insightsRow = `\n| **Insights** | ${parts.join(' · ')}${trafficNote} |`
450
+ }
451
+ }
452
+
359
453
  return {
360
454
  data: {
361
455
  version: pkg.version,
@@ -377,11 +471,13 @@ function getAllResourceDefinitions(): InternalResourceDef[] {
377
471
  'memory://prs/{pr_number}/timeline',
378
472
  'memory://kanban/{project_number}',
379
473
  'memory://kanban/{project_number}/diagram',
474
+ 'memory://milestones/{number}',
380
475
  ],
381
476
  more: {
382
477
  fullHealth: 'memory://health',
383
478
  allRecent: 'memory://recent',
384
479
  githubStatus: 'memory://github/status',
480
+ repoInsights: 'memory://github/insights',
385
481
  contextBundle: 'get-context-bundle prompt',
386
482
  },
387
483
  // IMPORTANT: Agent should relay this message to the user
@@ -392,7 +488,7 @@ function getAllResourceDefinitions(): InternalResourceDef[] {
392
488
  | **Branch** | ${branchName} |
393
489
  | **CI Status** | ${ciStatus} |
394
490
  | **Journal** | ${totalEntries} entries |
395
- | **Latest** | ${latestPreview} |
491
+ | **Latest** | ${latestPreview} |${milestoneRow}${insightsRow}
396
492
 
397
493
  I have project memory access and will create entries for significant work.`,
398
494
  // Note for clients that don't auto-inject ServerInstructions
@@ -420,8 +516,9 @@ I have project memory access and will create entries for significant work.`,
420
516
  // because the MCP SDK performs exact URI matching before calling handlers.
421
517
  const level: InstructionLevel = 'full'
422
518
 
423
- // Get enabled tools from filter config or all tools
424
- const enabledTools = context.filterConfig?.enabledTools ?? new Set<string>()
519
+ // Get enabled tools from filter config, or fall back to all tool names
520
+ const allToolNames = new Set(getAllToolNames())
521
+ const enabledTools = context.filterConfig?.enabledTools ?? allToolNames
425
522
 
426
523
  // Get prompts for instruction generation
427
524
  const prompts = getPrompts().map((p) => {
@@ -437,9 +534,7 @@ I have project memory access and will create entries for significant work.`,
437
534
 
438
535
  // Generate instructions at requested level
439
536
  const instructions = generateInstructions(
440
- enabledTools.size > 0
441
- ? enabledTools
442
- : new Set(['create_entry', 'search_entries', 'get_recent_entries']),
537
+ enabledTools,
443
538
  resources,
444
539
  prompts,
445
540
  undefined, // No latest entry needed for instructions
@@ -483,21 +578,23 @@ I have project memory access and will create entries for significant work.`,
483
578
  priority: 0.7,
484
579
  },
485
580
  handler: (_uri: string, context: ResourceContext) => {
581
+ // Fetch ALL significant entries so importance sort runs on the full set
582
+ // (not just the 20 most recent). We then slice after sorting.
486
583
  const rows = execQuery(
487
584
  context.db,
488
585
  `
489
586
  SELECT * FROM memory_journal
490
587
  WHERE significance_type IS NOT NULL
491
588
  AND deleted_at IS NULL
492
- ORDER BY timestamp DESC
493
- LIMIT 20
494
589
  `
495
590
  )
496
591
  // Transform entries and calculate importance scores
497
592
  const entriesWithImportance: (Record<string, unknown> & { importance: number })[] =
498
593
  rows.map((row) => {
499
594
  const entry = transformEntryRow(row)
500
- const importance = context.db.calculateImportance(entry['id'] as number)
595
+ const { score: importance } = context.db.calculateImportance(
596
+ entry['id'] as number
597
+ )
501
598
  return { ...entry, importance }
502
599
  })
503
600
  // Sort by importance (highest first), then by timestamp (newest first) for ties
@@ -510,7 +607,9 @@ I have project memory access and will create entries for significant work.`,
510
607
  const bTime = new Date(b['timestamp'] as string).getTime()
511
608
  return bTime - aTime
512
609
  })
513
- return { entries: entriesWithImportance, count: entriesWithImportance.length }
610
+ // Slice to top 20 AFTER sorting (not before) to ensure correctness
611
+ const top20 = entriesWithImportance.slice(0, 20)
612
+ return { entries: top20, count: top20.length }
514
613
  },
515
614
  },
516
615
  {
@@ -1198,6 +1297,39 @@ I have project memory access and will create entries for significant work.`,
1198
1297
  // Kanban not available
1199
1298
  }
1200
1299
 
1300
+ // Get milestone summary
1301
+ let milestoneSummary:
1302
+ | {
1303
+ number: number
1304
+ title: string
1305
+ state: string
1306
+ openIssues: number
1307
+ closedIssues: number
1308
+ completionPercentage: number
1309
+ dueOn: string | null
1310
+ }[]
1311
+ | null = null
1312
+ try {
1313
+ const milestones = await context.github.getMilestones(owner, repo, 'open', 5)
1314
+ if (milestones.length > 0) {
1315
+ milestoneSummary = milestones.map((ms) => {
1316
+ const total = ms.openIssues + ms.closedIssues
1317
+ const pct = total > 0 ? Math.round((ms.closedIssues / total) * 100) : 0
1318
+ return {
1319
+ number: ms.number,
1320
+ title: ms.title,
1321
+ state: ms.state,
1322
+ openIssues: ms.openIssues,
1323
+ closedIssues: ms.closedIssues,
1324
+ completionPercentage: pct,
1325
+ dueOn: ms.dueOn,
1326
+ }
1327
+ })
1328
+ }
1329
+ } catch {
1330
+ // Milestones not available
1331
+ }
1332
+
1201
1333
  return {
1202
1334
  data: {
1203
1335
  repository: `${owner}/${repo}`,
@@ -1216,11 +1348,185 @@ I have project memory access and will create entries for significant work.`,
1216
1348
  items: openPrs,
1217
1349
  },
1218
1350
  kanbanSummary,
1351
+ milestones: milestoneSummary,
1352
+ },
1353
+ annotations: { lastModified },
1354
+ }
1355
+ },
1356
+ },
1357
+ // Repository insights resource
1358
+ {
1359
+ uri: 'memory://github/insights',
1360
+ name: 'Repository Insights',
1361
+ title: 'Repository Stars & Traffic Summary',
1362
+ description: 'Compact repo insights: stars, forks, 14-day traffic totals (~150 tokens)',
1363
+ mimeType: 'application/json',
1364
+ icons: [ICON_ANALYTICS],
1365
+ annotations: {
1366
+ audience: ['assistant'],
1367
+ priority: 0.4, // Lower than status — optional enrichment
1368
+ },
1369
+ handler: async (_uri: string, context: ResourceContext): Promise<ResourceResult> => {
1370
+ const lastModified = new Date().toISOString()
1371
+
1372
+ if (!context.github) {
1373
+ return {
1374
+ data: {
1375
+ error: 'GitHub integration not available',
1376
+ hint: 'Set GITHUB_TOKEN and GITHUB_REPO_PATH environment variables.',
1377
+ },
1378
+ annotations: { lastModified },
1379
+ }
1380
+ }
1381
+
1382
+ const repoInfo = await context.github.getRepoInfo()
1383
+ const owner = repoInfo.owner
1384
+ const repo = repoInfo.repo
1385
+
1386
+ if (!owner || !repo) {
1387
+ return {
1388
+ data: {
1389
+ error: 'Could not detect repository',
1390
+ hint: 'Set GITHUB_REPO_PATH to your git repository.',
1391
+ },
1392
+ annotations: { lastModified },
1393
+ }
1394
+ }
1395
+
1396
+ // Get repo stats (stars, forks)
1397
+ const stats = await context.github.getRepoStats(owner, repo)
1398
+
1399
+ // Get traffic data (clones, views) - may fail if token lacks push access
1400
+ let traffic: { clones14d: number; views14d: number } | null = null
1401
+ try {
1402
+ const trafficData = await context.github.getTrafficData(owner, repo)
1403
+ if (trafficData) {
1404
+ traffic = {
1405
+ clones14d: trafficData.clones.total,
1406
+ views14d: trafficData.views.total,
1407
+ }
1408
+ }
1409
+ } catch {
1410
+ // Traffic data requires push access - silently skip
1411
+ }
1412
+
1413
+ return {
1414
+ data: {
1415
+ repository: `${owner}/${repo}`,
1416
+ stars: stats?.stars ?? null,
1417
+ forks: stats?.forks ?? null,
1418
+ watchers: stats?.watchers ?? null,
1419
+ ...(traffic ?? {}),
1420
+ hint: !traffic
1421
+ ? 'Traffic data requires push access to the repository.'
1422
+ : undefined,
1219
1423
  },
1220
1424
  annotations: { lastModified },
1221
1425
  }
1222
1426
  },
1223
1427
  },
1428
+ // Milestone resources
1429
+ {
1430
+ uri: 'memory://github/milestones',
1431
+ name: 'GitHub Milestones',
1432
+ title: 'GitHub Repository Milestones',
1433
+ description:
1434
+ 'Open GitHub milestones with completion percentages, due dates, and issue counts',
1435
+ mimeType: 'application/json',
1436
+ icons: [ICON_MILESTONE],
1437
+ annotations: {
1438
+ audience: ['assistant'],
1439
+ priority: 0.6,
1440
+ },
1441
+ handler: async (_uri: string, context: ResourceContext) => {
1442
+ if (!context.github) {
1443
+ return {
1444
+ error: 'GitHub integration not available',
1445
+ hint: 'Set GITHUB_TOKEN and GITHUB_REPO_PATH environment variables.',
1446
+ }
1447
+ }
1448
+
1449
+ const repoInfo = await context.github.getRepoInfo()
1450
+ const owner = repoInfo.owner
1451
+ const repo = repoInfo.repo
1452
+
1453
+ if (!owner || !repo) {
1454
+ return {
1455
+ error: 'Could not detect repository',
1456
+ hint: 'Set GITHUB_REPO_PATH to your git repository.',
1457
+ }
1458
+ }
1459
+
1460
+ const milestones = await context.github.getMilestones(owner, repo, 'open', 20)
1461
+ const milestonesWithProgress = milestones.map((ms) => {
1462
+ const total = ms.openIssues + ms.closedIssues
1463
+ const completionPercentage =
1464
+ total > 0 ? Math.round((ms.closedIssues / total) * 100) : 0
1465
+ return { ...ms, completionPercentage }
1466
+ })
1467
+
1468
+ return {
1469
+ repository: `${owner}/${repo}`,
1470
+ milestones: milestonesWithProgress,
1471
+ count: milestonesWithProgress.length,
1472
+ hint: 'Use get_github_milestones tool for state filtering. Use memory://milestones/{number} for detail.',
1473
+ }
1474
+ },
1475
+ },
1476
+ {
1477
+ uri: 'memory://milestones/{number}',
1478
+ name: 'Milestone Detail',
1479
+ title: 'GitHub Milestone Detail',
1480
+ description:
1481
+ 'Detailed view of a single GitHub milestone with completion progress and issue counts. Use get_github_issues with the milestone filter for individual issue details.',
1482
+ mimeType: 'application/json',
1483
+ icons: [ICON_MILESTONE],
1484
+ annotations: {
1485
+ audience: ['assistant'],
1486
+ priority: 0.5,
1487
+ },
1488
+ handler: async (uri: string, context: ResourceContext) => {
1489
+ const match = /memory:\/\/milestones\/(\d+)/.exec(uri)
1490
+ const milestoneNumber = match?.[1] ? parseInt(match[1], 10) : null
1491
+
1492
+ if (milestoneNumber === null) {
1493
+ return { error: 'Invalid milestone number' }
1494
+ }
1495
+
1496
+ if (!context.github) {
1497
+ return {
1498
+ error: 'GitHub integration not available',
1499
+ hint: 'Set GITHUB_TOKEN and GITHUB_REPO_PATH environment variables.',
1500
+ }
1501
+ }
1502
+
1503
+ const repoInfo = await context.github.getRepoInfo()
1504
+ const owner = repoInfo.owner
1505
+ const repo = repoInfo.repo
1506
+
1507
+ if (!owner || !repo) {
1508
+ return {
1509
+ error: 'Could not detect repository',
1510
+ hint: 'Set GITHUB_REPO_PATH to your git repository.',
1511
+ }
1512
+ }
1513
+
1514
+ const milestone = await context.github.getMilestone(owner, repo, milestoneNumber)
1515
+ if (!milestone) {
1516
+ return { error: `Milestone #${String(milestoneNumber)} not found` }
1517
+ }
1518
+
1519
+ const total = milestone.openIssues + milestone.closedIssues
1520
+ const completionPercentage =
1521
+ total > 0 ? Math.round((milestone.closedIssues / total) * 100) : 0
1522
+
1523
+ return {
1524
+ repository: `${owner}/${repo}`,
1525
+ milestone: { ...milestone, completionPercentage },
1526
+ hint: 'Use get_github_issues tool to list issues associated with this milestone.',
1527
+ }
1528
+ },
1529
+ },
1224
1530
  // Kanban board resources (GitHub Projects v2)
1225
1531
  {
1226
1532
  uri: 'memory://kanban/{project_number}',