@oss-scout/mcp 0.6.0 → 0.8.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.
@@ -37542,6 +37542,32 @@ async function searchWithChunkedLabels(octokit, labels, reservedOps, buildQuery,
37542
37542
  }
37543
37543
  return allItems;
37544
37544
  }
37545
+ function buildLanguageVariants(languages, isAnyLanguage, hasLabels) {
37546
+ if (isAnyLanguage || languages.length === 0)
37547
+ return [""];
37548
+ if (languages.length === 1)
37549
+ return [`language:${languages[0]}`];
37550
+ if (!hasLabels)
37551
+ return [languages.map((l) => `language:${l}`).join(" ")];
37552
+ return languages.map((l) => `language:${l}`);
37553
+ }
37554
+ async function searchAcrossLanguagesAndLabels(octokit, languages, isAnyLanguage, labels, buildBaseQuery, perPage) {
37555
+ const langVariants = buildLanguageVariants(languages, isAnyLanguage, labels.length > 0);
37556
+ const seenUrls = /* @__PURE__ */ new Set();
37557
+ const allItems = [];
37558
+ for (let i = 0; i < langVariants.length; i++) {
37559
+ if (i > 0)
37560
+ await sleep(INTER_QUERY_DELAY_MS);
37561
+ const items = await searchWithChunkedLabels(octokit, labels, 0, (labelQ) => `${buildBaseQuery(langVariants[i])} ${labelQ}`.replace(/ +/g, " ").trim(), perPage);
37562
+ for (const item of items) {
37563
+ if (!seenUrls.has(item.html_url)) {
37564
+ seenUrls.add(item.html_url);
37565
+ allItems.push(item);
37566
+ }
37567
+ }
37568
+ }
37569
+ return allItems;
37570
+ }
37545
37571
  async function filterVetAndScore(vetter, items, filterIssues, excludedRepoSets, remainingNeeded, minStars, phaseLabel) {
37546
37572
  const spamRepos = detectLabelFarmingRepos(items);
37547
37573
  if (spamRepos.size > 0) {
@@ -37574,6 +37600,72 @@ async function filterVetAndScore(vetter, items, filterIssues, excludedRepoSets,
37574
37600
  return { candidates: starFiltered, allVetFailed, rateLimitHit };
37575
37601
  }
37576
37602
 
37603
+ // ../core/dist/core/personalization.js
37604
+ var REPO_BOOST = 20;
37605
+ var LANGUAGE_BOOST = 10;
37606
+ function annotateBoost(candidates, preferLanguages, preferRepos) {
37607
+ const langSet = new Set((preferLanguages ?? []).map((l) => l.trim().toLowerCase()).filter(Boolean));
37608
+ const repoSet = new Set((preferRepos ?? []).map((r) => r.trim()).filter(Boolean));
37609
+ if (langSet.size === 0 && repoSet.size === 0)
37610
+ return;
37611
+ for (const c of candidates) {
37612
+ let score = 0;
37613
+ const reasons = [];
37614
+ if (repoSet.size > 0 && repoSet.has(c.issue.repo)) {
37615
+ score += REPO_BOOST;
37616
+ reasons.push(`repo affinity: ${c.issue.repo}`);
37617
+ }
37618
+ const lang = c.projectHealth.language;
37619
+ if (langSet.size > 0 && lang && langSet.has(lang.toLowerCase())) {
37620
+ score += LANGUAGE_BOOST;
37621
+ reasons.push(`language match: ${lang}`);
37622
+ }
37623
+ if (score > 0) {
37624
+ c.boostScore = score;
37625
+ c.boostReasons = reasons;
37626
+ }
37627
+ }
37628
+ }
37629
+ function applyDiversityRatio(candidates, maxResults, diversityRatio) {
37630
+ if (maxResults <= 0)
37631
+ return [];
37632
+ const ratio = Math.max(0, Math.min(1, diversityRatio));
37633
+ if (ratio === 0)
37634
+ return candidates.slice(0, maxResults);
37635
+ const diversityReserve = Math.min(Math.floor(maxResults * ratio), maxResults);
37636
+ if (diversityReserve === 0)
37637
+ return candidates.slice(0, maxResults);
37638
+ const mainBudget = maxResults - diversityReserve;
37639
+ const picks = [];
37640
+ const seen = /* @__PURE__ */ new Set();
37641
+ for (const c of candidates) {
37642
+ if (picks.length >= mainBudget)
37643
+ break;
37644
+ picks.push(c);
37645
+ seen.add(c.issue.url);
37646
+ }
37647
+ for (const c of candidates) {
37648
+ if (picks.length >= maxResults)
37649
+ break;
37650
+ if (seen.has(c.issue.url))
37651
+ continue;
37652
+ if (c.boostScore && c.boostScore > 0)
37653
+ continue;
37654
+ c.diversitySlot = true;
37655
+ picks.push(c);
37656
+ seen.add(c.issue.url);
37657
+ }
37658
+ for (const c of candidates) {
37659
+ if (picks.length >= maxResults)
37660
+ break;
37661
+ if (seen.has(c.issue.url))
37662
+ continue;
37663
+ picks.push(c);
37664
+ seen.add(c.issue.url);
37665
+ }
37666
+ return picks;
37667
+ }
37668
+
37577
37669
  // ../core/dist/core/issue-discovery.js
37578
37670
  var MODULE10 = "issue-discovery";
37579
37671
  var LOW_BUDGET_THRESHOLD = 20;
@@ -37629,7 +37721,7 @@ async function runPhase1(octokit, vetter, repos, labels, maxResults, filterIssue
37629
37721
  rateLimitHit
37630
37722
  };
37631
37723
  }
37632
- async function runPhase2(octokit, vetter, scopes, labels, configLabels, baseQualifiers, maxResults, minStars, phase0RepoSet, starredRepoSet, existingCandidates, filterIssues) {
37724
+ async function runPhase2(octokit, vetter, scopes, labels, configLabels, languages, isAnyLanguage, maxResults, minStars, phase0RepoSet, starredRepoSet, existingCandidates, filterIssues) {
37633
37725
  info(MODULE10, "Phase 2: General issue search...");
37634
37726
  const seenRepos = new Set(existingCandidates.map((c) => c.issue.repo));
37635
37727
  const tierLabelGroups = [];
@@ -37656,7 +37748,7 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, baseQual
37656
37748
  let rateLimitHit = false;
37657
37749
  for (const { tier, tierLabels } of tierLabelGroups) {
37658
37750
  try {
37659
- const allItems = await searchWithChunkedLabels(octokit, tierLabels, 0, (labelQ) => `${baseQualifiers} ${labelQ}`.replace(/ +/g, " ").trim(), budgetPerTier * 3);
37751
+ const allItems = await searchAcrossLanguagesAndLabels(octokit, languages, isAnyLanguage, tierLabels, (langQ) => `is:issue is:open ${langQ} no:assignee`.replace(/ +/g, " ").trim(), budgetPerTier * 3);
37660
37752
  info(MODULE10, `Phase 2 [${tier}]: processing ${allItems.length} items...`);
37661
37753
  const { candidates: tierCandidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, allItems, filterIssues, [phase0RepoSet, starredRepoSet, seenRepos], budgetPerTier, minStars, `Phase 2 [${tier}]`);
37662
37754
  tierResults.push(tierCandidates);
@@ -37850,7 +37942,6 @@ var IssueDiscovery = class {
37850
37942
  const lowScoringRepos = new Set(this.deriveLowScoringRepos(config2.minRepoScoreThreshold));
37851
37943
  const isAnyLanguage = languages.some((l) => l.toLowerCase() === "any");
37852
37944
  const langQuery = isAnyLanguage ? "" : languages.map((l) => `language:${l}`).join(" ");
37853
- const baseQualifiers = `is:issue is:open ${langQuery} no:assignee`.replace(/ +/g, " ").trim();
37854
37945
  const aiBlocklisted = new Set(config2.aiPolicyBlocklist);
37855
37946
  if (aiBlocklisted.size > 0) {
37856
37947
  debug(MODULE10, `[AI_POLICY_FILTER] Filtering issues from ${aiBlocklisted.size} blocklisted repo(s): ${[...aiBlocklisted].join(", ")}`);
@@ -37922,7 +38013,7 @@ var IssueDiscovery = class {
37922
38013
  info(MODULE10, `Skipping broad phase delay: no results from previous phases, proceeding immediately`);
37923
38014
  }
37924
38015
  const remaining = maxResults - allCandidates.length;
37925
- const result = await runPhase2(this.octokit, this.vetter, scopes, labels, config2.labels, baseQualifiers, remaining, minStars, phase0RepoSet, starredRepoSet, allCandidates, filterIssues);
38016
+ const result = await runPhase2(this.octokit, this.vetter, scopes, labels, config2.labels, languages, isAnyLanguage, remaining, minStars, phase0RepoSet, starredRepoSet, allCandidates, filterIssues);
37926
38017
  allCandidates.push(...result.candidates);
37927
38018
  phaseErrors["2"] = result.error;
37928
38019
  if (result.rateLimitHit)
@@ -37967,6 +38058,7 @@ var IssueDiscovery = class {
37967
38058
  if (rateLimitHitDuringSearch || phasesSkippedForBudget) {
37968
38059
  this.rateLimitWarning = `Search results may be incomplete: GitHub API rate limits were hit during search.${budgetNote} Found ${allCandidates.length} candidate${allCandidates.length === 1 ? "" : "s"} but some search phases were limited. Try again after the rate limit resets for complete results.`;
37969
38060
  }
38061
+ annotateBoost(allCandidates, options.preferLanguages, options.preferRepos);
37970
38062
  allCandidates.sort((a, b) => {
37971
38063
  const priorityOrder = {
37972
38064
  merged_pr: 0,
@@ -37980,11 +38072,15 @@ var IssueDiscovery = class {
37980
38072
  const recDiff = recommendationOrder[a.recommendation] - recommendationOrder[b.recommendation];
37981
38073
  if (recDiff !== 0)
37982
38074
  return recDiff;
38075
+ const boostDiff = (b.boostScore ?? 0) - (a.boostScore ?? 0);
38076
+ if (boostDiff !== 0)
38077
+ return boostDiff;
37983
38078
  return b.viabilityScore - a.viabilityScore;
37984
38079
  });
37985
38080
  const capped = applyPerRepoCap(allCandidates, 2);
37986
- info(MODULE10, `Search complete: ${tracker.getTotalCalls()} Search API calls used, ${capped.length} candidates returned`);
37987
- return { candidates: capped.slice(0, maxResults), strategiesUsed };
38081
+ const finalPicks = applyDiversityRatio(capped, maxResults, options.diversityRatio ?? 0);
38082
+ info(MODULE10, `Search complete: ${tracker.getTotalCalls()} Search API calls used, ${finalPicks.length} candidates returned`);
38083
+ return { candidates: finalPicks, strategiesUsed };
37988
38084
  }
37989
38085
  /**
37990
38086
  * Vet a specific issue for claimability and project health.
@@ -38812,7 +38908,10 @@ var OssScout = class {
38812
38908
  const { candidates, strategiesUsed } = await discovery.searchIssues({
38813
38909
  maxResults: options?.maxResults,
38814
38910
  strategies: options?.strategies,
38815
- skippedUrls
38911
+ skippedUrls,
38912
+ preferLanguages: options?.preferLanguages,
38913
+ preferRepos: options?.preferRepos,
38914
+ diversityRatio: options?.diversityRatio
38816
38915
  });
38817
38916
  this.state.lastSearchAt = (/* @__PURE__ */ new Date()).toISOString();
38818
38917
  this.dirty = true;
@@ -39271,15 +39370,33 @@ function registerTools(server, scout) {
39271
39370
  maxResults: external_exports3.number().optional().describe("Maximum number of results to return (default 10)"),
39272
39371
  strategies: external_exports3.string().optional().describe(
39273
39372
  "Comma-separated search strategies: merged, starred, broad, maintained, all"
39373
+ ),
39374
+ preferLanguages: external_exports3.array(external_exports3.string()).optional().describe(
39375
+ "Soft-boost ranking for candidates whose repo language matches one of these (case-insensitive). Personalization tier between recommendation and viabilityScore (#1244). Not a filter; only reorders."
39376
+ ),
39377
+ preferRepos: external_exports3.array(external_exports3.string()).optional().describe(
39378
+ "Soft-boost ranking for candidates in one of these `owner/repo` slugs. Stronger weight than language match (#1244). Not a filter; only reorders."
39379
+ ),
39380
+ diversityRatio: external_exports3.number().min(0).max(1).optional().describe(
39381
+ "Diversity counterweight (#1244): fraction of result slots reserved for candidates that matched NEITHER preference list. Counters echo-chamber bias when boosts accumulate over time. 0 disables, 1 reserves every slot for diversity. Default 0."
39274
39382
  )
39275
39383
  },
39276
- async ({ maxResults, strategies }) => {
39384
+ async ({
39385
+ maxResults,
39386
+ strategies,
39387
+ preferLanguages,
39388
+ preferRepos,
39389
+ diversityRatio
39390
+ }) => {
39277
39391
  try {
39278
39392
  const parsedStrategies = strategies ? strategies.split(",").map((s) => SearchStrategySchema.parse(s.trim())) : void 0;
39279
39393
  const result = await withTimeout(
39280
39394
  scout.search({
39281
39395
  maxResults: maxResults ?? 10,
39282
- strategies: parsedStrategies
39396
+ strategies: parsedStrategies,
39397
+ preferLanguages,
39398
+ preferRepos,
39399
+ diversityRatio
39283
39400
  })
39284
39401
  );
39285
39402
  scout.saveResults(result.candidates);
package/dist/tools.js CHANGED
@@ -18,7 +18,21 @@ export function registerTools(server, scout) {
18
18
  .string()
19
19
  .optional()
20
20
  .describe("Comma-separated search strategies: merged, starred, broad, maintained, all"),
21
- }, async ({ maxResults, strategies }) => {
21
+ preferLanguages: z
22
+ .array(z.string())
23
+ .optional()
24
+ .describe("Soft-boost ranking for candidates whose repo language matches one of these (case-insensitive). Personalization tier between recommendation and viabilityScore (#1244). Not a filter; only reorders."),
25
+ preferRepos: z
26
+ .array(z.string())
27
+ .optional()
28
+ .describe("Soft-boost ranking for candidates in one of these `owner/repo` slugs. Stronger weight than language match (#1244). Not a filter; only reorders."),
29
+ diversityRatio: z
30
+ .number()
31
+ .min(0)
32
+ .max(1)
33
+ .optional()
34
+ .describe("Diversity counterweight (#1244): fraction of result slots reserved for candidates that matched NEITHER preference list. Counters echo-chamber bias when boosts accumulate over time. 0 disables, 1 reserves every slot for diversity. Default 0."),
35
+ }, async ({ maxResults, strategies, preferLanguages, preferRepos, diversityRatio, }) => {
22
36
  try {
23
37
  const parsedStrategies = strategies
24
38
  ? strategies
@@ -28,6 +42,9 @@ export function registerTools(server, scout) {
28
42
  const result = await withTimeout(scout.search({
29
43
  maxResults: maxResults ?? 10,
30
44
  strategies: parsedStrategies,
45
+ preferLanguages,
46
+ preferRepos,
47
+ diversityRatio,
31
48
  }));
32
49
  scout.saveResults(result.candidates);
33
50
  await scout.checkpoint();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oss-scout/mcp",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "MCP server for oss-scout — exposes issue discovery and vetting as MCP tools",
5
5
  "type": "module",
6
6
  "bin": {
@@ -18,7 +18,7 @@
18
18
  "dependencies": {
19
19
  "@modelcontextprotocol/sdk": "^1.27.1",
20
20
  "zod": "^4.3.6",
21
- "@oss-scout/core": "^0.9.0"
21
+ "@oss-scout/core": "^0.11.0"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/node": "^25.5.0",