@oss-scout/mcp 0.6.0 → 0.7.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,33 @@ 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
+
37577
37630
  // ../core/dist/core/issue-discovery.js
37578
37631
  var MODULE10 = "issue-discovery";
37579
37632
  var LOW_BUDGET_THRESHOLD = 20;
@@ -37629,7 +37682,7 @@ async function runPhase1(octokit, vetter, repos, labels, maxResults, filterIssue
37629
37682
  rateLimitHit
37630
37683
  };
37631
37684
  }
37632
- async function runPhase2(octokit, vetter, scopes, labels, configLabels, baseQualifiers, maxResults, minStars, phase0RepoSet, starredRepoSet, existingCandidates, filterIssues) {
37685
+ async function runPhase2(octokit, vetter, scopes, labels, configLabels, languages, isAnyLanguage, maxResults, minStars, phase0RepoSet, starredRepoSet, existingCandidates, filterIssues) {
37633
37686
  info(MODULE10, "Phase 2: General issue search...");
37634
37687
  const seenRepos = new Set(existingCandidates.map((c) => c.issue.repo));
37635
37688
  const tierLabelGroups = [];
@@ -37656,7 +37709,7 @@ async function runPhase2(octokit, vetter, scopes, labels, configLabels, baseQual
37656
37709
  let rateLimitHit = false;
37657
37710
  for (const { tier, tierLabels } of tierLabelGroups) {
37658
37711
  try {
37659
- const allItems = await searchWithChunkedLabels(octokit, tierLabels, 0, (labelQ) => `${baseQualifiers} ${labelQ}`.replace(/ +/g, " ").trim(), budgetPerTier * 3);
37712
+ const allItems = await searchAcrossLanguagesAndLabels(octokit, languages, isAnyLanguage, tierLabels, (langQ) => `is:issue is:open ${langQ} no:assignee`.replace(/ +/g, " ").trim(), budgetPerTier * 3);
37660
37713
  info(MODULE10, `Phase 2 [${tier}]: processing ${allItems.length} items...`);
37661
37714
  const { candidates: tierCandidates, allVetFailed, rateLimitHit: vetRateLimitHit } = await filterVetAndScore(vetter, allItems, filterIssues, [phase0RepoSet, starredRepoSet, seenRepos], budgetPerTier, minStars, `Phase 2 [${tier}]`);
37662
37715
  tierResults.push(tierCandidates);
@@ -37850,7 +37903,6 @@ var IssueDiscovery = class {
37850
37903
  const lowScoringRepos = new Set(this.deriveLowScoringRepos(config2.minRepoScoreThreshold));
37851
37904
  const isAnyLanguage = languages.some((l) => l.toLowerCase() === "any");
37852
37905
  const langQuery = isAnyLanguage ? "" : languages.map((l) => `language:${l}`).join(" ");
37853
- const baseQualifiers = `is:issue is:open ${langQuery} no:assignee`.replace(/ +/g, " ").trim();
37854
37906
  const aiBlocklisted = new Set(config2.aiPolicyBlocklist);
37855
37907
  if (aiBlocklisted.size > 0) {
37856
37908
  debug(MODULE10, `[AI_POLICY_FILTER] Filtering issues from ${aiBlocklisted.size} blocklisted repo(s): ${[...aiBlocklisted].join(", ")}`);
@@ -37922,7 +37974,7 @@ var IssueDiscovery = class {
37922
37974
  info(MODULE10, `Skipping broad phase delay: no results from previous phases, proceeding immediately`);
37923
37975
  }
37924
37976
  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);
37977
+ const result = await runPhase2(this.octokit, this.vetter, scopes, labels, config2.labels, languages, isAnyLanguage, remaining, minStars, phase0RepoSet, starredRepoSet, allCandidates, filterIssues);
37926
37978
  allCandidates.push(...result.candidates);
37927
37979
  phaseErrors["2"] = result.error;
37928
37980
  if (result.rateLimitHit)
@@ -37967,6 +38019,7 @@ var IssueDiscovery = class {
37967
38019
  if (rateLimitHitDuringSearch || phasesSkippedForBudget) {
37968
38020
  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
38021
  }
38022
+ annotateBoost(allCandidates, options.preferLanguages, options.preferRepos);
37970
38023
  allCandidates.sort((a, b) => {
37971
38024
  const priorityOrder = {
37972
38025
  merged_pr: 0,
@@ -37980,6 +38033,9 @@ var IssueDiscovery = class {
37980
38033
  const recDiff = recommendationOrder[a.recommendation] - recommendationOrder[b.recommendation];
37981
38034
  if (recDiff !== 0)
37982
38035
  return recDiff;
38036
+ const boostDiff = (b.boostScore ?? 0) - (a.boostScore ?? 0);
38037
+ if (boostDiff !== 0)
38038
+ return boostDiff;
37983
38039
  return b.viabilityScore - a.viabilityScore;
37984
38040
  });
37985
38041
  const capped = applyPerRepoCap(allCandidates, 2);
@@ -38812,7 +38868,9 @@ var OssScout = class {
38812
38868
  const { candidates, strategiesUsed } = await discovery.searchIssues({
38813
38869
  maxResults: options?.maxResults,
38814
38870
  strategies: options?.strategies,
38815
- skippedUrls
38871
+ skippedUrls,
38872
+ preferLanguages: options?.preferLanguages,
38873
+ preferRepos: options?.preferRepos
38816
38874
  });
38817
38875
  this.state.lastSearchAt = (/* @__PURE__ */ new Date()).toISOString();
38818
38876
  this.dirty = true;
@@ -39271,15 +39329,23 @@ function registerTools(server, scout) {
39271
39329
  maxResults: external_exports3.number().optional().describe("Maximum number of results to return (default 10)"),
39272
39330
  strategies: external_exports3.string().optional().describe(
39273
39331
  "Comma-separated search strategies: merged, starred, broad, maintained, all"
39332
+ ),
39333
+ preferLanguages: external_exports3.array(external_exports3.string()).optional().describe(
39334
+ "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."
39335
+ ),
39336
+ preferRepos: external_exports3.array(external_exports3.string()).optional().describe(
39337
+ "Soft-boost ranking for candidates in one of these `owner/repo` slugs. Stronger weight than language match (#1244). Not a filter; only reorders."
39274
39338
  )
39275
39339
  },
39276
- async ({ maxResults, strategies }) => {
39340
+ async ({ maxResults, strategies, preferLanguages, preferRepos }) => {
39277
39341
  try {
39278
39342
  const parsedStrategies = strategies ? strategies.split(",").map((s) => SearchStrategySchema.parse(s.trim())) : void 0;
39279
39343
  const result = await withTimeout(
39280
39344
  scout.search({
39281
39345
  maxResults: maxResults ?? 10,
39282
- strategies: parsedStrategies
39346
+ strategies: parsedStrategies,
39347
+ preferLanguages,
39348
+ preferRepos
39283
39349
  })
39284
39350
  );
39285
39351
  scout.saveResults(result.candidates);
package/dist/tools.js CHANGED
@@ -18,7 +18,15 @@ 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
+ }, async ({ maxResults, strategies, preferLanguages, preferRepos }) => {
22
30
  try {
23
31
  const parsedStrategies = strategies
24
32
  ? strategies
@@ -28,6 +36,8 @@ export function registerTools(server, scout) {
28
36
  const result = await withTimeout(scout.search({
29
37
  maxResults: maxResults ?? 10,
30
38
  strategies: parsedStrategies,
39
+ preferLanguages,
40
+ preferRepos,
31
41
  }));
32
42
  scout.saveResults(result.candidates);
33
43
  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.7.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.10.0"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/node": "^25.5.0",