@oss-scout/mcp 0.2.1 → 0.3.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.
- package/LICENSE +21 -0
- package/dist/index.js +1 -1
- package/dist/mcp-server.bundle.cjs +212 -75
- package/dist/tools.js +110 -5
- package/package.json +18 -13
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 John Costa
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ async function main() {
|
|
|
11
11
|
process.exit(1);
|
|
12
12
|
}
|
|
13
13
|
const scout = await createScout({ githubToken: token });
|
|
14
|
-
const server = new McpServer({ name: "
|
|
14
|
+
const server = new McpServer({ name: "oss-scout-mcp", version: "0.2.1" }, { capabilities: { resources: {}, tools: {} } });
|
|
15
15
|
registerTools(server, scout);
|
|
16
16
|
registerResources(server, scout);
|
|
17
17
|
const transport = new StdioServerTransport();
|
|
@@ -35704,7 +35704,6 @@ var IssueScopeSchema = external_exports3.enum([
|
|
|
35704
35704
|
]);
|
|
35705
35705
|
var SearchStrategySchema = external_exports3.enum([
|
|
35706
35706
|
"merged",
|
|
35707
|
-
"orgs",
|
|
35708
35707
|
"starred",
|
|
35709
35708
|
"broad",
|
|
35710
35709
|
"maintained",
|
|
@@ -35712,7 +35711,6 @@ var SearchStrategySchema = external_exports3.enum([
|
|
|
35712
35711
|
]);
|
|
35713
35712
|
var CONCRETE_STRATEGIES = [
|
|
35714
35713
|
"merged",
|
|
35715
|
-
"orgs",
|
|
35716
35714
|
"starred",
|
|
35717
35715
|
"broad",
|
|
35718
35716
|
"maintained"
|
|
@@ -35785,6 +35783,13 @@ var TrackedIssueSchema = external_exports3.object({
|
|
|
35785
35783
|
vetted: external_exports3.boolean(),
|
|
35786
35784
|
vettingResult: IssueVettingResultSchema.optional()
|
|
35787
35785
|
});
|
|
35786
|
+
var SkippedIssueSchema = external_exports3.object({
|
|
35787
|
+
url: external_exports3.string(),
|
|
35788
|
+
repo: external_exports3.string(),
|
|
35789
|
+
number: external_exports3.number(),
|
|
35790
|
+
title: external_exports3.string(),
|
|
35791
|
+
skippedAt: external_exports3.string()
|
|
35792
|
+
});
|
|
35788
35793
|
var SavedCandidateSchema = external_exports3.object({
|
|
35789
35794
|
issueUrl: external_exports3.string(),
|
|
35790
35795
|
repo: external_exports3.string(),
|
|
@@ -35801,13 +35806,12 @@ var SavedCandidateSchema = external_exports3.object({
|
|
|
35801
35806
|
var PersistenceModeSchema = external_exports3.enum(["local", "gist"]);
|
|
35802
35807
|
var ScoutPreferencesSchema = external_exports3.object({
|
|
35803
35808
|
githubUsername: external_exports3.string().default(""),
|
|
35804
|
-
languages: external_exports3.array(external_exports3.string()).default(["
|
|
35809
|
+
languages: external_exports3.array(external_exports3.string()).default(["any"]),
|
|
35805
35810
|
labels: external_exports3.array(external_exports3.string()).default(["good first issue", "help wanted"]),
|
|
35806
35811
|
scope: external_exports3.array(IssueScopeSchema).optional(),
|
|
35807
35812
|
excludeRepos: external_exports3.array(external_exports3.string()).default([]),
|
|
35808
35813
|
excludeOrgs: external_exports3.array(external_exports3.string()).default([]),
|
|
35809
35814
|
aiPolicyBlocklist: external_exports3.array(external_exports3.string()).default(["matplotlib/matplotlib"]),
|
|
35810
|
-
preferredOrgs: external_exports3.array(external_exports3.string()).default([]),
|
|
35811
35815
|
projectCategories: external_exports3.array(ProjectCategorySchema).default([]),
|
|
35812
35816
|
minStars: external_exports3.number().default(50),
|
|
35813
35817
|
maxIssueAgeDays: external_exports3.number().default(90),
|
|
@@ -35825,6 +35829,7 @@ var ScoutStateSchema = external_exports3.object({
|
|
|
35825
35829
|
mergedPRs: external_exports3.array(StoredMergedPRSchema).default([]),
|
|
35826
35830
|
closedPRs: external_exports3.array(StoredClosedPRSchema).default([]),
|
|
35827
35831
|
savedResults: external_exports3.array(SavedCandidateSchema).default([]),
|
|
35832
|
+
skippedIssues: external_exports3.array(SkippedIssueSchema).default([]),
|
|
35828
35833
|
lastSearchAt: external_exports3.string().optional(),
|
|
35829
35834
|
lastRunAt: external_exports3.string().default(() => (/* @__PURE__ */ new Date()).toISOString()),
|
|
35830
35835
|
gistId: external_exports3.string().optional()
|
|
@@ -36760,12 +36765,9 @@ var IssueVetter = class {
|
|
|
36760
36765
|
matchesPreferredCategory: matchesCategory
|
|
36761
36766
|
});
|
|
36762
36767
|
const starredRepos = this.stateReader.getStarredRepos();
|
|
36763
|
-
const preferredOrgs = this.stateReader.getPreferredOrgs();
|
|
36764
36768
|
let searchPriority = "normal";
|
|
36765
36769
|
if (effectiveMergedCount > 0) {
|
|
36766
36770
|
searchPriority = "merged_pr";
|
|
36767
|
-
} else if (preferredOrgs.some((o) => o.toLowerCase() === orgName?.toLowerCase())) {
|
|
36768
|
-
searchPriority = "preferred_org";
|
|
36769
36771
|
} else if (starredRepos.includes(repoFullName)) {
|
|
36770
36772
|
searchPriority = "starred";
|
|
36771
36773
|
}
|
|
@@ -37015,6 +37017,8 @@ function buildIssueFilter(config2) {
|
|
|
37015
37017
|
return false;
|
|
37016
37018
|
if (config2.lowScoringRepos.has(repoFullName))
|
|
37017
37019
|
return false;
|
|
37020
|
+
if (config2.skippedUrls.has(item.html_url))
|
|
37021
|
+
return false;
|
|
37018
37022
|
const updatedAt = new Date(item.updated_at);
|
|
37019
37023
|
const ageDays = daysBetween(updatedAt, config2.now);
|
|
37020
37024
|
if (ageDays > config2.maxAgeDays)
|
|
@@ -37035,38 +37039,6 @@ async function runPhase0(octokit, vetter, repos, baseQualifiers, maxResults, fil
|
|
|
37035
37039
|
rateLimitHit
|
|
37036
37040
|
};
|
|
37037
37041
|
}
|
|
37038
|
-
async function runPhase05(octokit, vetter, orgsToSearch, baseQualifiers, labels, maxResults, phase0RepoSet, filterIssues) {
|
|
37039
|
-
info(MODULE9, `Phase 0.5: Searching issues in ${orgsToSearch.length} preferred org(s)...`);
|
|
37040
|
-
const orgRepoFilter = orgsToSearch.map((org) => `org:${org}`).join(" OR ");
|
|
37041
|
-
const orgOps = orgsToSearch.length - 1;
|
|
37042
|
-
try {
|
|
37043
|
-
const allItems = await searchWithChunkedLabels(octokit, labels, orgOps, (labelQ) => `${baseQualifiers} ${labelQ} (${orgRepoFilter})`.replace(/ +/g, " ").trim(), maxResults * 3);
|
|
37044
|
-
if (allItems.length === 0) {
|
|
37045
|
-
return { candidates: [], error: null, rateLimitHit: false };
|
|
37046
|
-
}
|
|
37047
|
-
const filtered = filterIssues(allItems).filter((item) => {
|
|
37048
|
-
const repoFullName = extractRepoFromUrl(item.repository_url);
|
|
37049
|
-
if (!repoFullName)
|
|
37050
|
-
return false;
|
|
37051
|
-
return !phase0RepoSet.has(repoFullName);
|
|
37052
|
-
});
|
|
37053
|
-
const { candidates, allFailed: allVetFailed, rateLimitHit } = await vetter.vetIssuesParallel(filtered.slice(0, maxResults * 2).map((i) => i.html_url), maxResults, "preferred_org");
|
|
37054
|
-
info(MODULE9, `Found ${candidates.length} candidates from preferred orgs`);
|
|
37055
|
-
return {
|
|
37056
|
-
candidates,
|
|
37057
|
-
error: allVetFailed ? "All preferred org issue vetting failed" : null,
|
|
37058
|
-
rateLimitHit
|
|
37059
|
-
};
|
|
37060
|
-
} catch (error48) {
|
|
37061
|
-
const errMsg = errorMessage(error48);
|
|
37062
|
-
warn(MODULE9, `Error searching preferred orgs: ${errMsg}`);
|
|
37063
|
-
return {
|
|
37064
|
-
candidates: [],
|
|
37065
|
-
error: errMsg,
|
|
37066
|
-
rateLimitHit: isRateLimitError(error48)
|
|
37067
|
-
};
|
|
37068
|
-
}
|
|
37069
|
-
}
|
|
37070
37042
|
async function runPhase1(octokit, vetter, repos, baseQualifiers, labels, maxResults, filterIssues) {
|
|
37071
37043
|
info(MODULE9, `Phase 1: Searching issues in ${repos.length} starred repos...`);
|
|
37072
37044
|
const phase1Labels = labels.slice(0, 3);
|
|
@@ -37203,8 +37175,8 @@ var IssueDiscovery = class {
|
|
|
37203
37175
|
}
|
|
37204
37176
|
/**
|
|
37205
37177
|
* Search for issues matching our criteria.
|
|
37206
|
-
* Searches in priority order: merged-PR repos first (no label filter), then
|
|
37207
|
-
*
|
|
37178
|
+
* Searches in priority order: merged-PR repos first (no label filter), then starred
|
|
37179
|
+
* repos, then general search, then actively maintained repos.
|
|
37208
37180
|
* Filters out issues from low-scoring and excluded repos.
|
|
37209
37181
|
*
|
|
37210
37182
|
* @param options - Search configuration
|
|
@@ -37262,6 +37234,10 @@ var IssueDiscovery = class {
|
|
|
37262
37234
|
tracker.init(CRITICAL_BUDGET_THRESHOLD, new Date(Date.now() + 6e4).toISOString());
|
|
37263
37235
|
warn(MODULE9, "Could not check rate limit \u2014 using conservative budget, skipping heavy phases:", errorMessage(error48));
|
|
37264
37236
|
}
|
|
37237
|
+
if (searchBudget <= 0) {
|
|
37238
|
+
this.rateLimitWarning = "GitHub search API quota exhausted. Try again after the rate limit resets.";
|
|
37239
|
+
return { candidates: [], strategiesUsed: [] };
|
|
37240
|
+
}
|
|
37265
37241
|
const mergedPRRepos = this.stateReader.getReposWithMergedPRs();
|
|
37266
37242
|
const starredRepos = this.getStarredRepos();
|
|
37267
37243
|
const starredRepoSet = new Set(starredRepos);
|
|
@@ -37278,6 +37254,7 @@ var IssueDiscovery = class {
|
|
|
37278
37254
|
excludeOrgs: new Set((config2.excludeOrgs ?? []).map((o) => o.toLowerCase())),
|
|
37279
37255
|
aiBlocklisted,
|
|
37280
37256
|
lowScoringRepos,
|
|
37257
|
+
skippedUrls: options.skippedUrls ?? /* @__PURE__ */ new Set(),
|
|
37281
37258
|
maxAgeDays: config2.maxIssueAgeDays || 90,
|
|
37282
37259
|
now: /* @__PURE__ */ new Date(),
|
|
37283
37260
|
includeDocIssues: config2.includeDocIssues ?? true
|
|
@@ -37295,22 +37272,6 @@ var IssueDiscovery = class {
|
|
|
37295
37272
|
}
|
|
37296
37273
|
strategiesUsed.push("merged");
|
|
37297
37274
|
}
|
|
37298
|
-
const preferredOrgs = config2.preferredOrgs ?? [];
|
|
37299
|
-
if (allCandidates.length < maxResults && preferredOrgs.length > 0 && searchBudget >= CRITICAL_BUDGET_THRESHOLD && enabledStrategies.has("orgs")) {
|
|
37300
|
-
if (phase0Repos.length > 0)
|
|
37301
|
-
await sleep(INTER_PHASE_DELAY_MS);
|
|
37302
|
-
const phase0Orgs = new Set(phase0Repos.map((r) => r.split("/")[0]?.toLowerCase()));
|
|
37303
|
-
const orgsToSearch = preferredOrgs.filter((org) => !phase0Orgs.has(org.toLowerCase())).slice(0, 5);
|
|
37304
|
-
if (orgsToSearch.length > 0) {
|
|
37305
|
-
const remaining = maxResults - allCandidates.length;
|
|
37306
|
-
const result = await runPhase05(this.octokit, this.vetter, orgsToSearch, baseQualifiers, labels, remaining, phase0RepoSet, filterIssues);
|
|
37307
|
-
allCandidates.push(...result.candidates);
|
|
37308
|
-
phaseErrors["0.5"] = result.error;
|
|
37309
|
-
if (result.rateLimitHit)
|
|
37310
|
-
rateLimitHitDuringSearch = true;
|
|
37311
|
-
}
|
|
37312
|
-
strategiesUsed.push("orgs");
|
|
37313
|
-
}
|
|
37314
37275
|
if (allCandidates.length < maxResults && starredRepos.length > 0 && searchBudget >= CRITICAL_BUDGET_THRESHOLD && enabledStrategies.has("starred")) {
|
|
37315
37276
|
await sleep(INTER_PHASE_DELAY_MS);
|
|
37316
37277
|
const reposToSearch = starredRepos.filter((r) => !phase0RepoSet.has(r));
|
|
@@ -37356,7 +37317,6 @@ var IssueDiscovery = class {
|
|
|
37356
37317
|
if (allCandidates.length === 0) {
|
|
37357
37318
|
const errorDetails = [
|
|
37358
37319
|
phaseErrors["0"] ? `Phase 0 (merged-PR repos): ${phaseErrors["0"]}` : null,
|
|
37359
|
-
phaseErrors["0.5"] ? `Phase 0.5 (preferred orgs): ${phaseErrors["0.5"]}` : null,
|
|
37360
37320
|
phaseErrors["1"] ? `Phase 1 (starred repos): ${phaseErrors["1"]}` : null,
|
|
37361
37321
|
phaseErrors["2"] ? `Phase 2 (general): ${phaseErrors["2"]}` : null,
|
|
37362
37322
|
phaseErrors["3"] ? `Phase 3 (maintained repos): ${phaseErrors["3"]}` : null
|
|
@@ -37374,9 +37334,8 @@ var IssueDiscovery = class {
|
|
|
37374
37334
|
allCandidates.sort((a, b) => {
|
|
37375
37335
|
const priorityOrder = {
|
|
37376
37336
|
merged_pr: 0,
|
|
37377
|
-
|
|
37378
|
-
|
|
37379
|
-
normal: 3
|
|
37337
|
+
starred: 1,
|
|
37338
|
+
normal: 2
|
|
37380
37339
|
};
|
|
37381
37340
|
const priorityDiff = priorityOrder[a.searchPriority] - priorityOrder[b.searchPriority];
|
|
37382
37341
|
if (priorityDiff !== 0)
|
|
@@ -37462,11 +37421,16 @@ var GistStateStore = class {
|
|
|
37462
37421
|
warn(MODULE10, "No gist ID \u2014 cannot push");
|
|
37463
37422
|
return false;
|
|
37464
37423
|
}
|
|
37424
|
+
const json2 = JSON.stringify(state, null, 2);
|
|
37425
|
+
if (json2.length > 9e5) {
|
|
37426
|
+
warn(MODULE10, `State too large for gist (${Math.round(json2.length / 1024)}KB). Consider clearing old results with 'oss-scout results clear'.`);
|
|
37427
|
+
return false;
|
|
37428
|
+
}
|
|
37465
37429
|
try {
|
|
37466
37430
|
await this.octokit.gists.update({
|
|
37467
37431
|
gist_id: this.gistId,
|
|
37468
37432
|
files: {
|
|
37469
|
-
[GIST_FILENAME]: { content:
|
|
37433
|
+
[GIST_FILENAME]: { content: json2 }
|
|
37470
37434
|
}
|
|
37471
37435
|
});
|
|
37472
37436
|
debug(MODULE10, "State pushed to gist");
|
|
@@ -37639,6 +37603,7 @@ function mergeStates(local, remote) {
|
|
|
37639
37603
|
mergedPRs: unionByUrl(local.mergedPRs, remote.mergedPRs),
|
|
37640
37604
|
closedPRs: unionByUrl(local.closedPRs, remote.closedPRs),
|
|
37641
37605
|
savedResults: mergeSavedResults(local.savedResults ?? [], remote.savedResults ?? []),
|
|
37606
|
+
skippedIssues: mergeSkippedIssues(local.skippedIssues ?? [], remote.skippedIssues ?? []),
|
|
37642
37607
|
lastSearchAt: pickFresherTimestamp(local.lastSearchAt, remote.lastSearchAt),
|
|
37643
37608
|
lastRunAt: pickFresherTimestamp(local.lastRunAt, remote.lastRunAt) ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
37644
37609
|
gistId: remote.gistId ?? local.gistId
|
|
@@ -37689,6 +37654,18 @@ function mergeSavedResults(local, remote) {
|
|
|
37689
37654
|
}
|
|
37690
37655
|
return [...merged.values()];
|
|
37691
37656
|
}
|
|
37657
|
+
function mergeSkippedIssues(local, remote) {
|
|
37658
|
+
const merged = /* @__PURE__ */ new Map();
|
|
37659
|
+
for (const item of local)
|
|
37660
|
+
merged.set(item.url, item);
|
|
37661
|
+
for (const item of remote) {
|
|
37662
|
+
const existing = merged.get(item.url);
|
|
37663
|
+
if (!existing || item.skippedAt > existing.skippedAt) {
|
|
37664
|
+
merged.set(item.url, item);
|
|
37665
|
+
}
|
|
37666
|
+
}
|
|
37667
|
+
return [...merged.values()];
|
|
37668
|
+
}
|
|
37692
37669
|
function pickFresherTimestamp(a, b) {
|
|
37693
37670
|
if (!a)
|
|
37694
37671
|
return b;
|
|
@@ -37719,7 +37696,8 @@ function loadLocalState() {
|
|
|
37719
37696
|
const backupPath = `${statePath}.corrupt.${Date.now()}`;
|
|
37720
37697
|
fs4.copyFileSync(statePath, backupPath);
|
|
37721
37698
|
warn(MODULE11, `Corrupt state backed up to ${backupPath}`);
|
|
37722
|
-
} catch {
|
|
37699
|
+
} catch (backupErr) {
|
|
37700
|
+
warn(MODULE11, `Failed to back up corrupt state: ${errorMessage(backupErr)}`);
|
|
37723
37701
|
}
|
|
37724
37702
|
return ScoutStateSchema.parse({ version: 1 });
|
|
37725
37703
|
}
|
|
@@ -37799,12 +37777,16 @@ var OssScout = class {
|
|
|
37799
37777
|
// ── Search ──────────────────────────────────────────────────────────
|
|
37800
37778
|
/**
|
|
37801
37779
|
* Multi-strategy issue search. Returns scored, sorted candidates.
|
|
37780
|
+
* Automatically culls expired skip entries and filters skipped issues.
|
|
37802
37781
|
*/
|
|
37803
37782
|
async search(options) {
|
|
37783
|
+
this.cullExpiredSkips();
|
|
37784
|
+
const skippedUrls = new Set((this.state.skippedIssues ?? []).map((s) => s.url));
|
|
37804
37785
|
const discovery = new IssueDiscovery(this.githubToken, this.state.preferences, this);
|
|
37805
37786
|
const { candidates, strategiesUsed } = await discovery.searchIssues({
|
|
37806
37787
|
maxResults: options?.maxResults,
|
|
37807
|
-
strategies: options?.strategies
|
|
37788
|
+
strategies: options?.strategies,
|
|
37789
|
+
skippedUrls
|
|
37808
37790
|
});
|
|
37809
37791
|
this.state.lastSearchAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
37810
37792
|
this.dirty = true;
|
|
@@ -37905,9 +37887,6 @@ var OssScout = class {
|
|
|
37905
37887
|
getStarredRepos() {
|
|
37906
37888
|
return this.state.starredRepos;
|
|
37907
37889
|
}
|
|
37908
|
-
getPreferredOrgs() {
|
|
37909
|
-
return this.state.preferences.preferredOrgs;
|
|
37910
|
-
}
|
|
37911
37890
|
getProjectCategories() {
|
|
37912
37891
|
return this.state.preferences.projectCategories;
|
|
37913
37892
|
}
|
|
@@ -38038,6 +38017,69 @@ var OssScout = class {
|
|
|
38038
38017
|
this.state.savedResults = [];
|
|
38039
38018
|
this.dirty = true;
|
|
38040
38019
|
}
|
|
38020
|
+
// ── Skip List ───────────────────────────────────────────────────────
|
|
38021
|
+
/**
|
|
38022
|
+
* Skip an issue — excludes it from future searches. Auto-culled after 90 days.
|
|
38023
|
+
*/
|
|
38024
|
+
skipIssue(url2, metadata) {
|
|
38025
|
+
const existing = this.state.skippedIssues ?? [];
|
|
38026
|
+
if (existing.some((s) => s.url === url2))
|
|
38027
|
+
return;
|
|
38028
|
+
this.state.skippedIssues = [
|
|
38029
|
+
...existing,
|
|
38030
|
+
{
|
|
38031
|
+
url: url2,
|
|
38032
|
+
repo: metadata?.repo ?? "",
|
|
38033
|
+
number: metadata?.number ?? 0,
|
|
38034
|
+
title: metadata?.title ?? "",
|
|
38035
|
+
skippedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
38036
|
+
}
|
|
38037
|
+
];
|
|
38038
|
+
if (this.state.savedResults) {
|
|
38039
|
+
this.state.savedResults = this.state.savedResults.filter((r) => r.issueUrl !== url2);
|
|
38040
|
+
}
|
|
38041
|
+
this.dirty = true;
|
|
38042
|
+
}
|
|
38043
|
+
/**
|
|
38044
|
+
* Get all skipped issues.
|
|
38045
|
+
*/
|
|
38046
|
+
getSkippedIssues() {
|
|
38047
|
+
return this.state.skippedIssues ?? [];
|
|
38048
|
+
}
|
|
38049
|
+
/**
|
|
38050
|
+
* Remove a specific issue from the skip list.
|
|
38051
|
+
*/
|
|
38052
|
+
unskipIssue(url2) {
|
|
38053
|
+
this.state.skippedIssues = (this.state.skippedIssues ?? []).filter((s) => s.url !== url2);
|
|
38054
|
+
this.dirty = true;
|
|
38055
|
+
}
|
|
38056
|
+
/**
|
|
38057
|
+
* Clear all skipped issues.
|
|
38058
|
+
*/
|
|
38059
|
+
clearSkippedIssues() {
|
|
38060
|
+
this.state.skippedIssues = [];
|
|
38061
|
+
this.dirty = true;
|
|
38062
|
+
}
|
|
38063
|
+
/**
|
|
38064
|
+
* Remove skipped issues older than maxDays (default 90). Called automatically during search.
|
|
38065
|
+
* @returns The number of expired entries that were removed.
|
|
38066
|
+
*/
|
|
38067
|
+
cullExpiredSkips(maxDays = 90) {
|
|
38068
|
+
const cutoff = /* @__PURE__ */ new Date();
|
|
38069
|
+
cutoff.setDate(cutoff.getDate() - maxDays);
|
|
38070
|
+
const before = (this.state.skippedIssues ?? []).length;
|
|
38071
|
+
this.state.skippedIssues = (this.state.skippedIssues ?? []).filter((s) => {
|
|
38072
|
+
const d = new Date(s.skippedAt);
|
|
38073
|
+
if (isNaN(d.getTime())) {
|
|
38074
|
+
return true;
|
|
38075
|
+
}
|
|
38076
|
+
return d >= cutoff;
|
|
38077
|
+
});
|
|
38078
|
+
const culled = before - this.state.skippedIssues.length;
|
|
38079
|
+
if (culled > 0)
|
|
38080
|
+
this.dirty = true;
|
|
38081
|
+
return culled;
|
|
38082
|
+
}
|
|
38041
38083
|
// ── Persistence ─────────────────────────────────────────────────────
|
|
38042
38084
|
/**
|
|
38043
38085
|
* Check if state has uncommitted changes.
|
|
@@ -38097,6 +38139,17 @@ var OssScout = class {
|
|
|
38097
38139
|
};
|
|
38098
38140
|
|
|
38099
38141
|
// src/tools.ts
|
|
38142
|
+
var TOOL_TIMEOUT_MS = 6e4;
|
|
38143
|
+
function withTimeout(promise2, ms = TOOL_TIMEOUT_MS) {
|
|
38144
|
+
let timeoutId;
|
|
38145
|
+
const timer = new Promise((_, reject) => {
|
|
38146
|
+
timeoutId = setTimeout(
|
|
38147
|
+
() => reject(new Error(`Request timed out after ${ms / 1e3}s`)),
|
|
38148
|
+
ms
|
|
38149
|
+
);
|
|
38150
|
+
});
|
|
38151
|
+
return Promise.race([promise2, timer]).finally(() => clearTimeout(timeoutId));
|
|
38152
|
+
}
|
|
38100
38153
|
function registerTools(server, scout) {
|
|
38101
38154
|
server.tool(
|
|
38102
38155
|
"search",
|
|
@@ -38104,16 +38157,18 @@ function registerTools(server, scout) {
|
|
|
38104
38157
|
{
|
|
38105
38158
|
maxResults: external_exports3.number().optional().describe("Maximum number of results to return (default 10)"),
|
|
38106
38159
|
strategies: external_exports3.string().optional().describe(
|
|
38107
|
-
"Comma-separated search strategies: merged,
|
|
38160
|
+
"Comma-separated search strategies: merged, starred, broad, maintained, all"
|
|
38108
38161
|
)
|
|
38109
38162
|
},
|
|
38110
38163
|
async ({ maxResults, strategies }) => {
|
|
38111
38164
|
try {
|
|
38112
38165
|
const parsedStrategies = strategies ? strategies.split(",").map((s) => SearchStrategySchema.parse(s.trim())) : void 0;
|
|
38113
|
-
const result = await
|
|
38114
|
-
|
|
38115
|
-
|
|
38116
|
-
|
|
38166
|
+
const result = await withTimeout(
|
|
38167
|
+
scout.search({
|
|
38168
|
+
maxResults: maxResults ?? 10,
|
|
38169
|
+
strategies: parsedStrategies
|
|
38170
|
+
})
|
|
38171
|
+
);
|
|
38117
38172
|
scout.saveResults(result.candidates);
|
|
38118
38173
|
await scout.checkpoint();
|
|
38119
38174
|
return {
|
|
@@ -38138,7 +38193,7 @@ function registerTools(server, scout) {
|
|
|
38138
38193
|
},
|
|
38139
38194
|
async ({ issueUrl }) => {
|
|
38140
38195
|
try {
|
|
38141
|
-
const candidate = await scout.vetIssue(issueUrl);
|
|
38196
|
+
const candidate = await withTimeout(scout.vetIssue(issueUrl));
|
|
38142
38197
|
return {
|
|
38143
38198
|
content: [{ type: "text", text: JSON.stringify(candidate, null, 2) }]
|
|
38144
38199
|
};
|
|
@@ -38161,11 +38216,93 @@ function registerTools(server, scout) {
|
|
|
38161
38216
|
};
|
|
38162
38217
|
}
|
|
38163
38218
|
);
|
|
38219
|
+
server.tool(
|
|
38220
|
+
"skip",
|
|
38221
|
+
"Skip, unskip, list, or clear skipped issues. Skipped issues are excluded from future searches and auto-expire after 90 days.",
|
|
38222
|
+
{
|
|
38223
|
+
action: external_exports3.enum(["add", "remove", "list", "clear"]).default("add").describe("Action to perform (default: add)"),
|
|
38224
|
+
issueUrl: external_exports3.string().optional().describe("GitHub issue URL (required for add/remove actions)")
|
|
38225
|
+
},
|
|
38226
|
+
async ({ action, issueUrl }) => {
|
|
38227
|
+
try {
|
|
38228
|
+
if ((action === "add" || action === "remove") && !issueUrl) {
|
|
38229
|
+
return {
|
|
38230
|
+
content: [
|
|
38231
|
+
{
|
|
38232
|
+
type: "text",
|
|
38233
|
+
text: `Error: issueUrl is required for the "${action}" action.`
|
|
38234
|
+
}
|
|
38235
|
+
],
|
|
38236
|
+
isError: true
|
|
38237
|
+
};
|
|
38238
|
+
}
|
|
38239
|
+
if (action === "list") {
|
|
38240
|
+
const skipped = scout.getSkippedIssues();
|
|
38241
|
+
return {
|
|
38242
|
+
content: [{ type: "text", text: JSON.stringify(skipped, null, 2) }]
|
|
38243
|
+
};
|
|
38244
|
+
}
|
|
38245
|
+
if (action === "clear") {
|
|
38246
|
+
const count = scout.getSkippedIssues().length;
|
|
38247
|
+
scout.clearSkippedIssues();
|
|
38248
|
+
const synced2 = await scout.checkpoint();
|
|
38249
|
+
const syncNote2 = synced2 ? "" : " (saved locally, gist sync failed)";
|
|
38250
|
+
return {
|
|
38251
|
+
content: [
|
|
38252
|
+
{
|
|
38253
|
+
type: "text",
|
|
38254
|
+
text: count > 0 ? `Skip list cleared (${count} entries removed)${syncNote2}` : `Skip list already empty${syncNote2}`
|
|
38255
|
+
}
|
|
38256
|
+
]
|
|
38257
|
+
};
|
|
38258
|
+
}
|
|
38259
|
+
if (action === "remove") {
|
|
38260
|
+
const wasPresent = scout.getSkippedIssues().some((s) => s.url === issueUrl);
|
|
38261
|
+
scout.unskipIssue(issueUrl);
|
|
38262
|
+
const synced2 = await scout.checkpoint();
|
|
38263
|
+
const syncNote2 = synced2 ? "" : " (saved locally, gist sync failed)";
|
|
38264
|
+
return {
|
|
38265
|
+
content: [
|
|
38266
|
+
{
|
|
38267
|
+
type: "text",
|
|
38268
|
+
text: wasPresent ? `Removed from skip list: ${issueUrl}${syncNote2}` : `Not in skip list: ${issueUrl}`
|
|
38269
|
+
}
|
|
38270
|
+
]
|
|
38271
|
+
};
|
|
38272
|
+
}
|
|
38273
|
+
const alreadySkipped = scout.getSkippedIssues().some((s) => s.url === issueUrl);
|
|
38274
|
+
if (alreadySkipped) {
|
|
38275
|
+
return {
|
|
38276
|
+
content: [
|
|
38277
|
+
{ type: "text", text: `Already in skip list: ${issueUrl}` }
|
|
38278
|
+
]
|
|
38279
|
+
};
|
|
38280
|
+
}
|
|
38281
|
+
const saved = scout.getSavedResults().find((r) => r.issueUrl === issueUrl);
|
|
38282
|
+
const metadata = saved ? {
|
|
38283
|
+
repo: saved.repo,
|
|
38284
|
+
number: saved.number,
|
|
38285
|
+
title: saved.title
|
|
38286
|
+
} : void 0;
|
|
38287
|
+
scout.skipIssue(issueUrl, metadata);
|
|
38288
|
+
const synced = await scout.checkpoint();
|
|
38289
|
+
const syncNote = synced ? "" : " (saved locally, gist sync failed)";
|
|
38290
|
+
return {
|
|
38291
|
+
content: [{ type: "text", text: `Skipped: ${issueUrl}${syncNote}` }]
|
|
38292
|
+
};
|
|
38293
|
+
} catch (err) {
|
|
38294
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
38295
|
+
return {
|
|
38296
|
+
content: [{ type: "text", text: `Error: ${msg}` }],
|
|
38297
|
+
isError: true
|
|
38298
|
+
};
|
|
38299
|
+
}
|
|
38300
|
+
}
|
|
38301
|
+
);
|
|
38164
38302
|
const VALID_KEYS = Object.keys(ScoutPreferencesSchema.shape);
|
|
38165
38303
|
const ARRAY_KEYS = /* @__PURE__ */ new Set([
|
|
38166
38304
|
"languages",
|
|
38167
38305
|
"labels",
|
|
38168
|
-
"preferredOrgs",
|
|
38169
38306
|
"projectCategories",
|
|
38170
38307
|
"excludeRepos",
|
|
38171
38308
|
"excludeOrgs",
|
|
@@ -38321,7 +38458,7 @@ async function main() {
|
|
|
38321
38458
|
}
|
|
38322
38459
|
const scout = await createScout({ githubToken: token });
|
|
38323
38460
|
const server = new McpServer(
|
|
38324
|
-
{ name: "
|
|
38461
|
+
{ name: "oss-scout-mcp", version: "0.2.1" },
|
|
38325
38462
|
{ capabilities: { resources: {}, tools: {} } }
|
|
38326
38463
|
);
|
|
38327
38464
|
registerTools(server, scout);
|
package/dist/tools.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { SearchStrategySchema, ScoutPreferencesSchema } from "@oss-scout/core";
|
|
3
|
+
const TOOL_TIMEOUT_MS = 60000;
|
|
4
|
+
function withTimeout(promise, ms = TOOL_TIMEOUT_MS) {
|
|
5
|
+
let timeoutId;
|
|
6
|
+
const timer = new Promise((_, reject) => {
|
|
7
|
+
timeoutId = setTimeout(() => reject(new Error(`Request timed out after ${ms / 1000}s`)), ms);
|
|
8
|
+
});
|
|
9
|
+
return Promise.race([promise, timer]).finally(() => clearTimeout(timeoutId));
|
|
10
|
+
}
|
|
3
11
|
export function registerTools(server, scout) {
|
|
4
12
|
server.tool("search", "Search for open source issues matching your preferences", {
|
|
5
13
|
maxResults: z
|
|
@@ -9,7 +17,7 @@ export function registerTools(server, scout) {
|
|
|
9
17
|
strategies: z
|
|
10
18
|
.string()
|
|
11
19
|
.optional()
|
|
12
|
-
.describe("Comma-separated search strategies: merged,
|
|
20
|
+
.describe("Comma-separated search strategies: merged, starred, broad, maintained, all"),
|
|
13
21
|
}, async ({ maxResults, strategies }) => {
|
|
14
22
|
try {
|
|
15
23
|
const parsedStrategies = strategies
|
|
@@ -17,10 +25,10 @@ export function registerTools(server, scout) {
|
|
|
17
25
|
.split(",")
|
|
18
26
|
.map((s) => SearchStrategySchema.parse(s.trim()))
|
|
19
27
|
: undefined;
|
|
20
|
-
const result = await scout.search({
|
|
28
|
+
const result = await withTimeout(scout.search({
|
|
21
29
|
maxResults: maxResults ?? 10,
|
|
22
30
|
strategies: parsedStrategies,
|
|
23
|
-
});
|
|
31
|
+
}));
|
|
24
32
|
scout.saveResults(result.candidates);
|
|
25
33
|
await scout.checkpoint();
|
|
26
34
|
return {
|
|
@@ -41,7 +49,7 @@ export function registerTools(server, scout) {
|
|
|
41
49
|
.describe("Full GitHub issue URL (e.g. https://github.com/owner/repo/issues/123)"),
|
|
42
50
|
}, async ({ issueUrl }) => {
|
|
43
51
|
try {
|
|
44
|
-
const candidate = await scout.vetIssue(issueUrl);
|
|
52
|
+
const candidate = await withTimeout(scout.vetIssue(issueUrl));
|
|
45
53
|
return {
|
|
46
54
|
content: [{ type: "text", text: JSON.stringify(candidate, null, 2) }],
|
|
47
55
|
};
|
|
@@ -60,11 +68,108 @@ export function registerTools(server, scout) {
|
|
|
60
68
|
content: [{ type: "text", text: JSON.stringify(preferences, null, 2) }],
|
|
61
69
|
};
|
|
62
70
|
});
|
|
71
|
+
server.tool("skip", "Skip, unskip, list, or clear skipped issues. Skipped issues are excluded from future searches and auto-expire after 90 days.", {
|
|
72
|
+
action: z
|
|
73
|
+
.enum(["add", "remove", "list", "clear"])
|
|
74
|
+
.default("add")
|
|
75
|
+
.describe("Action to perform (default: add)"),
|
|
76
|
+
issueUrl: z
|
|
77
|
+
.string()
|
|
78
|
+
.optional()
|
|
79
|
+
.describe("GitHub issue URL (required for add/remove actions)"),
|
|
80
|
+
}, async ({ action, issueUrl }) => {
|
|
81
|
+
try {
|
|
82
|
+
if ((action === "add" || action === "remove") && !issueUrl) {
|
|
83
|
+
return {
|
|
84
|
+
content: [
|
|
85
|
+
{
|
|
86
|
+
type: "text",
|
|
87
|
+
text: `Error: issueUrl is required for the "${action}" action.`,
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
isError: true,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (action === "list") {
|
|
94
|
+
const skipped = scout.getSkippedIssues();
|
|
95
|
+
return {
|
|
96
|
+
content: [{ type: "text", text: JSON.stringify(skipped, null, 2) }],
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (action === "clear") {
|
|
100
|
+
const count = scout.getSkippedIssues().length;
|
|
101
|
+
scout.clearSkippedIssues();
|
|
102
|
+
const synced = await scout.checkpoint();
|
|
103
|
+
const syncNote = synced ? "" : " (saved locally, gist sync failed)";
|
|
104
|
+
return {
|
|
105
|
+
content: [
|
|
106
|
+
{
|
|
107
|
+
type: "text",
|
|
108
|
+
text: count > 0
|
|
109
|
+
? `Skip list cleared (${count} entries removed)${syncNote}`
|
|
110
|
+
: `Skip list already empty${syncNote}`,
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (action === "remove") {
|
|
116
|
+
const wasPresent = scout
|
|
117
|
+
.getSkippedIssues()
|
|
118
|
+
.some((s) => s.url === issueUrl);
|
|
119
|
+
scout.unskipIssue(issueUrl);
|
|
120
|
+
const synced = await scout.checkpoint();
|
|
121
|
+
const syncNote = synced ? "" : " (saved locally, gist sync failed)";
|
|
122
|
+
return {
|
|
123
|
+
content: [
|
|
124
|
+
{
|
|
125
|
+
type: "text",
|
|
126
|
+
text: wasPresent
|
|
127
|
+
? `Removed from skip list: ${issueUrl}${syncNote}`
|
|
128
|
+
: `Not in skip list: ${issueUrl}`,
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
// action === "add"
|
|
134
|
+
const alreadySkipped = scout
|
|
135
|
+
.getSkippedIssues()
|
|
136
|
+
.some((s) => s.url === issueUrl);
|
|
137
|
+
if (alreadySkipped) {
|
|
138
|
+
return {
|
|
139
|
+
content: [
|
|
140
|
+
{ type: "text", text: `Already in skip list: ${issueUrl}` },
|
|
141
|
+
],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const saved = scout
|
|
145
|
+
.getSavedResults()
|
|
146
|
+
.find((r) => r.issueUrl === issueUrl);
|
|
147
|
+
const metadata = saved
|
|
148
|
+
? {
|
|
149
|
+
repo: saved.repo,
|
|
150
|
+
number: saved.number,
|
|
151
|
+
title: saved.title,
|
|
152
|
+
}
|
|
153
|
+
: undefined;
|
|
154
|
+
scout.skipIssue(issueUrl, metadata);
|
|
155
|
+
const synced = await scout.checkpoint();
|
|
156
|
+
const syncNote = synced ? "" : " (saved locally, gist sync failed)";
|
|
157
|
+
return {
|
|
158
|
+
content: [{ type: "text", text: `Skipped: ${issueUrl}${syncNote}` }],
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
catch (err) {
|
|
162
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
163
|
+
return {
|
|
164
|
+
content: [{ type: "text", text: `Error: ${msg}` }],
|
|
165
|
+
isError: true,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
});
|
|
63
169
|
const VALID_KEYS = Object.keys(ScoutPreferencesSchema.shape);
|
|
64
170
|
const ARRAY_KEYS = new Set([
|
|
65
171
|
"languages",
|
|
66
172
|
"labels",
|
|
67
|
-
"preferredOrgs",
|
|
68
173
|
"projectCategories",
|
|
69
174
|
"excludeRepos",
|
|
70
175
|
"excludeOrgs",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oss-scout/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "MCP server for oss-scout — exposes issue discovery and vetting as MCP tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -15,19 +15,10 @@
|
|
|
15
15
|
"files": [
|
|
16
16
|
"dist/"
|
|
17
17
|
],
|
|
18
|
-
"scripts": {
|
|
19
|
-
"build": "tsc",
|
|
20
|
-
"bundle": "esbuild src/index.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist/mcp-server.bundle.cjs",
|
|
21
|
-
"test": "vitest run",
|
|
22
|
-
"test:watch": "vitest",
|
|
23
|
-
"test:coverage": "vitest run --coverage",
|
|
24
|
-
"typecheck": "tsc --noEmit",
|
|
25
|
-
"prepublishOnly": "pnpm run build && pnpm run bundle"
|
|
26
|
-
},
|
|
27
18
|
"dependencies": {
|
|
28
19
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
29
|
-
"
|
|
30
|
-
"
|
|
20
|
+
"zod": "^4.3.6",
|
|
21
|
+
"@oss-scout/core": "^0.3.0"
|
|
31
22
|
},
|
|
32
23
|
"devDependencies": {
|
|
33
24
|
"@types/node": "^25.5.0",
|
|
@@ -37,6 +28,12 @@
|
|
|
37
28
|
"@vitest/coverage-v8": "^4.1.0",
|
|
38
29
|
"vitest": "^4.1.0"
|
|
39
30
|
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"mcp",
|
|
33
|
+
"mcp-server",
|
|
34
|
+
"claude",
|
|
35
|
+
"issue-discovery"
|
|
36
|
+
],
|
|
40
37
|
"engines": {
|
|
41
38
|
"node": ">=20"
|
|
42
39
|
},
|
|
@@ -45,5 +42,13 @@
|
|
|
45
42
|
"type": "git",
|
|
46
43
|
"url": "git+https://github.com/costajohnt/oss-scout.git",
|
|
47
44
|
"directory": "packages/mcp-server"
|
|
45
|
+
},
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "tsc",
|
|
48
|
+
"bundle": "esbuild src/index.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist/mcp-server.bundle.cjs",
|
|
49
|
+
"test": "vitest run",
|
|
50
|
+
"test:watch": "vitest",
|
|
51
|
+
"test:coverage": "vitest run --coverage",
|
|
52
|
+
"typecheck": "tsc --noEmit"
|
|
48
53
|
}
|
|
49
|
-
}
|
|
54
|
+
}
|