@oss-scout/mcp 0.10.1 → 0.10.2

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/dist/index.d.ts CHANGED
@@ -12,7 +12,7 @@ export { registerResources } from "./resources.js";
12
12
  /** Server identity, kept in one place for the bin and any embedding host. */
13
13
  export declare const SERVER_INFO: {
14
14
  readonly name: "oss-scout-mcp";
15
- readonly version: "0.10.1";
15
+ readonly version: "0.10.2";
16
16
  };
17
17
  /**
18
18
  * Build a fully-wired MCP server (tools + resources) around a scout.
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ export { registerResources } from "./resources.js";
15
15
  /** Server identity, kept in one place for the bin and any embedding host. */
16
16
  export const SERVER_INFO = {
17
17
  name: "oss-scout-mcp",
18
- version: "0.10.1", // x-release-please-version
18
+ version: "0.10.2", // x-release-please-version
19
19
  };
20
20
  /**
21
21
  * Build a fully-wired MCP server (tools + resources) around a scout.
@@ -37684,7 +37684,11 @@ async function triageWithSLM(input, options) {
37684
37684
  format: SLM_TRIAGE_SCHEMA,
37685
37685
  options: { temperature: 0.1 }
37686
37686
  }),
37687
- signal: AbortSignal.timeout(timeoutMs)
37687
+ signal: AbortSignal.timeout(timeoutMs),
37688
+ // No redirect following: an allowed private host could otherwise
37689
+ // 307/308-redirect the issue-text POST body to a public origin (#310).
37690
+ // Ollama never redirects, so this costs nothing.
37691
+ redirect: "error"
37688
37692
  });
37689
37693
  } catch {
37690
37694
  return null;
@@ -38079,6 +38083,9 @@ var IssueVetter = class {
38079
38083
  let rateLimitFailures = 0;
38080
38084
  let attemptedCount = 0;
38081
38085
  let firstAuthError = null;
38086
+ let firstForbiddenError = null;
38087
+ let forbiddenCount = 0;
38088
+ const forbiddenRepos = /* @__PURE__ */ new Set();
38082
38089
  const uniqueUrls = [...new Set(urls)];
38083
38090
  const issueRefs = uniqueUrls.flatMap((url2) => {
38084
38091
  const p = parseGitHubUrl(url2);
@@ -38097,6 +38104,12 @@ var IssueVetter = class {
38097
38104
  break;
38098
38105
  if (firstAuthError)
38099
38106
  break;
38107
+ const parsedUrl = parseGitHubUrl(url2);
38108
+ const repoKey = parsedUrl ? `${parsedUrl.owner}/${parsedUrl.repo}` : null;
38109
+ if (repoKey && forbiddenRepos.has(repoKey)) {
38110
+ debug(MODULE10, `Skipping ${url2}: repo already returned 403`);
38111
+ continue;
38112
+ }
38100
38113
  attemptedCount++;
38101
38114
  const core = prefetchFor(url2);
38102
38115
  const task = this.vetIssue(url2, core ? { prefetched: core } : void 0).then((candidate) => {
@@ -38107,10 +38120,19 @@ var IssueVetter = class {
38107
38120
  candidates.push(candidate);
38108
38121
  }
38109
38122
  }).catch((error48) => {
38110
- if (isAuthError(error48)) {
38123
+ if (getHttpStatusCode(error48) === 401) {
38111
38124
  firstAuthError ??= error48;
38112
38125
  return;
38113
38126
  }
38127
+ if (isAuthError(error48)) {
38128
+ firstForbiddenError ??= error48;
38129
+ forbiddenCount++;
38130
+ failedVettingCount++;
38131
+ if (repoKey)
38132
+ forbiddenRepos.add(repoKey);
38133
+ warn(MODULE10, `Access forbidden for ${url2} (SAML/token scope?) \u2014 skipping this repo:`, errorMessage(error48));
38134
+ return;
38135
+ }
38114
38136
  failedVettingCount++;
38115
38137
  if (isRateLimitError(error48)) {
38116
38138
  rateLimitFailures++;
@@ -38129,6 +38151,9 @@ var IssueVetter = class {
38129
38151
  }
38130
38152
  throw firstAuthError;
38131
38153
  }
38154
+ if (firstForbiddenError && attemptedCount > 0 && forbiddenCount === attemptedCount) {
38155
+ throw firstForbiddenError;
38156
+ }
38132
38157
  const allFailed = failedVettingCount === attemptedCount && attemptedCount > 0;
38133
38158
  if (allFailed) {
38134
38159
  warn(MODULE10, `All ${attemptedCount} issue(s) failed vetting. This may indicate a systemic issue (rate limit, auth, network).`);
@@ -38806,6 +38831,7 @@ var IssueDiscovery = class {
38806
38831
  resetGraphQLSearchQueryCount();
38807
38832
  const tracker = this.budgetTracker;
38808
38833
  let searchBudget = CRITICAL_BUDGET_THRESHOLD - 1;
38834
+ let preflightFailed = false;
38809
38835
  try {
38810
38836
  const rateLimit = await checkRateLimit(this.githubToken);
38811
38837
  searchBudget = rateLimit.remaining;
@@ -38823,6 +38849,7 @@ var IssueDiscovery = class {
38823
38849
  } catch (error48) {
38824
38850
  if (getHttpStatusCode(error48) === 401)
38825
38851
  throw error48;
38852
+ preflightFailed = true;
38826
38853
  tracker.init(CRITICAL_BUDGET_THRESHOLD, new Date(Date.now() + 6e4).toISOString());
38827
38854
  warn(MODULE12, "Could not check rate limit \u2014 using conservative budget, skipping the starred phase:", errorMessage(error48));
38828
38855
  }
@@ -38926,7 +38953,7 @@ var IssueDiscovery = class {
38926
38953
  strategiesUsed.push("maintained");
38927
38954
  }
38928
38955
  const phasesSkippedForBudget = searchBudget < CRITICAL_BUDGET_THRESHOLD;
38929
- const budgetNote = phasesSkippedForBudget ? ` The starred-repo phase was skipped due to critically low API quota (${searchBudget} remaining); broad and maintained phases still ran on GraphQL.` : "";
38956
+ const budgetNote = phasesSkippedForBudget ? preflightFailed ? " The starred-repo phase was skipped as a precaution because the rate-limit check failed (quota unknown); broad and maintained phases still ran on GraphQL." : ` The starred-repo phase was skipped due to critically low API quota (${searchBudget} remaining); broad and maintained phases still ran on GraphQL.` : "";
38930
38957
  if (allCandidates.length === 0) {
38931
38958
  const errorDetails = [
38932
38959
  phaseErrors["0"] ? `Phase 0 (merged-PR repos): ${phaseErrors["0"]}` : null,
@@ -39034,10 +39061,17 @@ function parseRoadmapIssueRefs(content, owner, repo) {
39034
39061
  refs.add(n);
39035
39062
  }
39036
39063
  const fullRefStripPattern = /\b[\w.-]+\/[\w.-]+#\d+\b/gi;
39064
+ let inFence = false;
39037
39065
  for (const line of content.split("\n")) {
39066
+ if (/^\s*(```|~~~)/.test(line)) {
39067
+ inFence = !inFence;
39068
+ continue;
39069
+ }
39070
+ if (inFence)
39071
+ continue;
39038
39072
  if (/^\s*#+\s/.test(line))
39039
39073
  continue;
39040
- const stripped = line.replace(fullRefStripPattern, "");
39074
+ const stripped = line.replace(fullRefStripPattern, "").replace(/`[^`]*`/g, "");
39041
39075
  for (const m of stripped.matchAll(/(?:^|[^&\w])#(\d+)\b/g)) {
39042
39076
  const n = Number.parseInt(m[1], 10);
39043
39077
  if (n > 0)
@@ -39114,7 +39148,8 @@ function classifyHorizon(input) {
39114
39148
  function splitByHorizon(candidates, count, ratio = DEFAULT_SPLIT_RATIO) {
39115
39149
  const allQuick = candidates.filter((c) => c.horizon === "quick-win").sort((a, b) => b.viabilityScore - a.viabilityScore);
39116
39150
  const allBigger = candidates.filter((c) => c.horizon === "bigger-bet").sort((a, b) => b.viabilityScore - a.viabilityScore);
39117
- const targetQuick = Math.round(count * ratio);
39151
+ const clampedRatio = Math.max(0, Math.min(1, ratio));
39152
+ const targetQuick = Math.round(count * clampedRatio);
39118
39153
  const targetBigger = count - targetQuick;
39119
39154
  const quickTaken = Math.min(allQuick.length, targetQuick);
39120
39155
  const biggerTaken = Math.min(allBigger.length, targetBigger);
@@ -39168,6 +39203,17 @@ function detectWontfixNoContributor(input) {
39168
39203
  }
39169
39204
  var NO_ANCHORS_MESSAGE = "No anchor repos yet (need 3+ merged PRs in a repo). Try `scout search` to build relationships first.";
39170
39205
  var NO_RESULTS_MESSAGE = "No open feature opportunities in your anchor repos right now. Check back next week, or try `scout search` for fix-mode work.";
39206
+ function buildRepoExclusionFilter(opts) {
39207
+ const excluded = new Set([...opts.excludeRepos ?? [], ...opts.aiPolicyBlocklist ?? []].map((r) => r.toLowerCase()));
39208
+ const excludedOrgs = new Set((opts.excludeOrgs ?? []).map((o) => o.toLowerCase()));
39209
+ return (repoFullName) => {
39210
+ const lower = repoFullName.toLowerCase();
39211
+ if (excluded.has(lower))
39212
+ return false;
39213
+ const org = lower.split("/")[0];
39214
+ return !(org && excludedOrgs.has(org));
39215
+ };
39216
+ }
39171
39217
  function extractLabels(item) {
39172
39218
  if (!Array.isArray(item.labels))
39173
39219
  return [];
@@ -39213,7 +39259,8 @@ async function vetAndClassify(item, vetter, roadmapRefs) {
39213
39259
  return { ...candidate, horizon };
39214
39260
  }
39215
39261
  async function discoverFeatures(opts) {
39216
- const anchorRepos = resolveAnchorRepos(opts.repoScores, opts.anchorThreshold);
39262
+ const includeRepo = buildRepoExclusionFilter(opts);
39263
+ const anchorRepos = resolveAnchorRepos(opts.repoScores, opts.anchorThreshold).filter(includeRepo);
39217
39264
  if (anchorRepos.length === 0) {
39218
39265
  return {
39219
39266
  quickWins: [],
@@ -39320,7 +39367,11 @@ async function discoverFeaturesBroad(opts) {
39320
39367
  merged.push(raw);
39321
39368
  }
39322
39369
  }
39323
- items = merged.filter((it) => !it.pull_request && !it.assignee && isFeatureIssue(it)).slice(0, maxToVet);
39370
+ const includeRepo = buildRepoExclusionFilter(opts);
39371
+ items = merged.filter((it) => !it.pull_request && !it.assignee && isFeatureIssue(it)).filter((it) => {
39372
+ const repo = extractRepoFromUrl(it.html_url);
39373
+ return repo === null || includeRepo(repo);
39374
+ }).slice(0, maxToVet);
39324
39375
  } catch (err) {
39325
39376
  rethrowIfFatal(err);
39326
39377
  warn(MODULE13, `broad feature search failed: ${errorMessage(err)}`);
@@ -39654,6 +39705,13 @@ function mergeStates(local, remote) {
39654
39705
  const localPrefsWin = localPrefsTs !== void 0 && (remotePrefsTs === void 0 || localPrefsTs > remotePrefsTs);
39655
39706
  const preferences = localPrefsWin ? local.preferences : remote.preferences;
39656
39707
  const preferencesUpdatedAt = pickFresherTimestamp(localPrefsTs, remotePrefsTs);
39708
+ const mergedPRsFinal = unionByUrl(local.mergedPRs, remote.mergedPRs);
39709
+ const closedPRsFinal = unionByUrl(local.closedPRs, remote.closedPRs);
39710
+ const resolvedUrls = /* @__PURE__ */ new Set([
39711
+ ...mergedPRsFinal.map((p) => p.url),
39712
+ ...closedPRsFinal.map((p) => p.url)
39713
+ ]);
39714
+ const openPRsFinal = unionByUrl(local.openPRs ?? [], remote.openPRs ?? []).filter((p) => !resolvedUrls.has(p.url));
39657
39715
  return {
39658
39716
  ...local,
39659
39717
  ...remote,
@@ -39664,9 +39722,9 @@ function mergeStates(local, remote) {
39664
39722
  repoScores: mergeRepoScores(local.repoScores, remote.repoScores),
39665
39723
  starredRepos: mergeStarredRepos(local, remote),
39666
39724
  starredReposLastFetched: pickFresherTimestamp(local.starredReposLastFetched, remote.starredReposLastFetched),
39667
- mergedPRs: unionByUrl(local.mergedPRs, remote.mergedPRs),
39668
- closedPRs: unionByUrl(local.closedPRs, remote.closedPRs),
39669
- openPRs: unionByUrl(local.openPRs ?? [], remote.openPRs ?? []),
39725
+ mergedPRs: mergedPRsFinal,
39726
+ closedPRs: closedPRsFinal,
39727
+ openPRs: openPRsFinal,
39670
39728
  savedResults: savedResultsFinal,
39671
39729
  skippedIssues,
39672
39730
  lastSearchAt: pickFresherTimestamp(local.lastSearchAt, remote.lastSearchAt),
@@ -40038,12 +40096,16 @@ var OssScout = class {
40038
40096
  languages: this.state.preferences.languages,
40039
40097
  excludeRepos: this.state.preferences.excludeRepos,
40040
40098
  excludeOrgs: this.state.preferences.excludeOrgs,
40099
+ aiPolicyBlocklist: this.state.preferences.aiPolicyBlocklist,
40041
40100
  splitRatio: options?.splitRatio ?? this.state.preferences.featuresSplitRatio
40042
40101
  }) : await discoverFeatures({
40043
40102
  octokit,
40044
40103
  vetter,
40045
40104
  repoScores: this.state.repoScores ?? {},
40046
40105
  count,
40106
+ excludeRepos: this.state.preferences.excludeRepos,
40107
+ excludeOrgs: this.state.preferences.excludeOrgs,
40108
+ aiPolicyBlocklist: this.state.preferences.aiPolicyBlocklist,
40047
40109
  anchorThreshold: options?.anchorThreshold ?? this.state.preferences.featuresAnchorThreshold,
40048
40110
  splitRatio: options?.splitRatio ?? this.state.preferences.featuresSplitRatio
40049
40111
  });
@@ -40746,7 +40808,7 @@ function registerTools(server, scout) {
40746
40808
  "search",
40747
40809
  "Search for open source issues matching your preferences",
40748
40810
  {
40749
- maxResults: external_exports3.number().int().min(1).max(50).optional().describe("Maximum number of results to return (1-50, default 10)"),
40811
+ maxResults: external_exports3.number().int().min(1).max(100).optional().describe("Maximum number of results to return (1-100, default 10)"),
40750
40812
  strategies: external_exports3.string().optional().describe(
40751
40813
  "Comma-separated search strategies: merged, starred, broad, maintained, all"
40752
40814
  ),
@@ -41095,7 +41157,7 @@ function registerResources(server, scout) {
41095
41157
  // src/index.ts
41096
41158
  var SERVER_INFO = {
41097
41159
  name: "oss-scout-mcp",
41098
- version: "0.10.1"
41160
+ version: "0.10.2"
41099
41161
  // x-release-please-version
41100
41162
  };
41101
41163
  function createServer(scout) {
package/dist/tools.js CHANGED
@@ -35,9 +35,12 @@ export function registerTools(server, scout) {
35
35
  .number()
36
36
  .int()
37
37
  .min(1)
38
- .max(50)
38
+ // 100, not 50: the CLI's search count is unbounded, so a tight MCP
39
+ // cap made the two surfaces disagree for 1.5.0 clients while still
40
+ // needing SOME ceiling against absurd values (#317).
41
+ .max(100)
39
42
  .optional()
40
- .describe("Maximum number of results to return (1-50, default 10)"),
43
+ .describe("Maximum number of results to return (1-100, default 10)"),
41
44
  strategies: z
42
45
  .string()
43
46
  .optional()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oss-scout/mcp",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
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": "^1.5.1"
21
+ "@oss-scout/core": "^1.5.2"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/node": "^25.5.0",