@oss-scout/mcp 0.8.0 → 0.10.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/dist/bin.d.ts +2 -0
- package/dist/bin.js +11 -0
- package/dist/index.d.ts +26 -1
- package/dist/index.js +35 -14
- package/dist/mcp-server.bundle.cjs +1412 -662
- package/dist/tools.js +96 -109
- package/package.json +3 -3
package/dist/tools.js
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { SearchStrategySchema,
|
|
3
|
-
|
|
2
|
+
import { SearchStrategySchema, ISSUE_URL_PATTERN, validateGitHubUrl, validateUrl, applyPreferenceField, SORTED_PREFERENCE_KEYS, } from "@oss-scout/core";
|
|
3
|
+
// Generous ceiling: even with inter-phase delays zeroed, vetting many issues
|
|
4
|
+
// under the budget tracker's sliding-window pacing can take minutes (#143).
|
|
5
|
+
const TOOL_TIMEOUT_MS = 300000;
|
|
6
|
+
// Surfaced on the JSON envelope when scout.checkpoint() returns false (#212).
|
|
7
|
+
// The skip tool reports this inline because it returns a short status line;
|
|
8
|
+
// the JSON tools (search, scout-features, config-set) carry it as a top-level
|
|
9
|
+
// field so a persist failure isn't silently swallowed.
|
|
10
|
+
const PERSIST_WARNING = "Checkpoint failed: changes were applied in memory but could not be persisted. They may be lost when the server restarts.";
|
|
11
|
+
/**
|
|
12
|
+
* Serialize a tool result, attaching a top-level `persistWarning` field when
|
|
13
|
+
* the preceding checkpoint failed (#212). Spreads onto the existing object so
|
|
14
|
+
* the happy-path payload is byte-for-byte unchanged.
|
|
15
|
+
*/
|
|
16
|
+
function withPersistWarning(payload, persisted) {
|
|
17
|
+
return persisted ? payload : { ...payload, persistWarning: PERSIST_WARNING };
|
|
18
|
+
}
|
|
4
19
|
function withTimeout(promise, ms = TOOL_TIMEOUT_MS) {
|
|
5
20
|
let timeoutId;
|
|
6
21
|
const timer = new Promise((_, reject) => {
|
|
@@ -26,13 +41,21 @@ export function registerTools(server, scout) {
|
|
|
26
41
|
.array(z.string())
|
|
27
42
|
.optional()
|
|
28
43
|
.describe("Soft-boost ranking for candidates in one of these `owner/repo` slugs. Stronger weight than language match (#1244). Not a filter; only reorders."),
|
|
44
|
+
avoidRepos: z
|
|
45
|
+
.array(z.string())
|
|
46
|
+
.optional()
|
|
47
|
+
.describe("Soft PENALTY (milder than excludeRepos) for candidates in one of these `owner/repo` slugs (#168). Pushes them down the ranking but does not remove them; a strong boost can still outweigh it."),
|
|
48
|
+
boostIssueTypes: z
|
|
49
|
+
.array(z.string())
|
|
50
|
+
.optional()
|
|
51
|
+
.describe('Soft-boost ranking for candidates whose issue labels match one of these types, case-insensitive (e.g. "bug", "good first issue") (#168). Not a filter; only reorders.'),
|
|
29
52
|
diversityRatio: z
|
|
30
53
|
.number()
|
|
31
54
|
.min(0)
|
|
32
55
|
.max(1)
|
|
33
56
|
.optional()
|
|
34
57
|
.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, }) => {
|
|
58
|
+
}, async ({ maxResults, strategies, preferLanguages, preferRepos, avoidRepos, boostIssueTypes, diversityRatio, }) => {
|
|
36
59
|
try {
|
|
37
60
|
const parsedStrategies = strategies
|
|
38
61
|
? strategies
|
|
@@ -44,12 +67,23 @@ export function registerTools(server, scout) {
|
|
|
44
67
|
strategies: parsedStrategies,
|
|
45
68
|
preferLanguages,
|
|
46
69
|
preferRepos,
|
|
70
|
+
avoidRepos,
|
|
71
|
+
boostIssueTypes,
|
|
47
72
|
diversityRatio,
|
|
73
|
+
// No fixed inter-phase sleeps in the request/response MCP context;
|
|
74
|
+
// the budget tracker still paces the GitHub calls (#143)
|
|
75
|
+
interPhaseDelayMs: 0,
|
|
76
|
+
broadPhaseDelayMs: 0,
|
|
48
77
|
}));
|
|
49
78
|
scout.saveResults(result.candidates);
|
|
50
|
-
await scout.checkpoint();
|
|
79
|
+
const persisted = await scout.checkpoint();
|
|
51
80
|
return {
|
|
52
|
-
content: [
|
|
81
|
+
content: [
|
|
82
|
+
{
|
|
83
|
+
type: "text",
|
|
84
|
+
text: JSON.stringify(withPersistWarning(result, persisted), null, 2),
|
|
85
|
+
},
|
|
86
|
+
],
|
|
53
87
|
};
|
|
54
88
|
}
|
|
55
89
|
catch (err) {
|
|
@@ -86,9 +120,14 @@ export function registerTools(server, scout) {
|
|
|
86
120
|
splitRatio,
|
|
87
121
|
}));
|
|
88
122
|
scout.saveResults([...result.quickWins, ...result.biggerBets]);
|
|
89
|
-
await scout.checkpoint();
|
|
123
|
+
const persisted = await scout.checkpoint();
|
|
90
124
|
return {
|
|
91
|
-
content: [
|
|
125
|
+
content: [
|
|
126
|
+
{
|
|
127
|
+
type: "text",
|
|
128
|
+
text: JSON.stringify(withPersistWarning(result, persisted), null, 2),
|
|
129
|
+
},
|
|
130
|
+
],
|
|
92
131
|
};
|
|
93
132
|
}
|
|
94
133
|
catch (err) {
|
|
@@ -124,6 +163,21 @@ export function registerTools(server, scout) {
|
|
|
124
163
|
content: [{ type: "text", text: JSON.stringify(preferences, null, 2) }],
|
|
125
164
|
};
|
|
126
165
|
});
|
|
166
|
+
server.tool("sync", "Reconcile tracked open PRs against GitHub — mark merged/closed and recompute repo scores. Cheap; good for a daily startup call.", async () => {
|
|
167
|
+
try {
|
|
168
|
+
const result = await withTimeout(scout.syncOpenPRs());
|
|
169
|
+
return {
|
|
170
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
175
|
+
return {
|
|
176
|
+
content: [{ type: "text", text: `Error: ${msg}` }],
|
|
177
|
+
isError: true,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
});
|
|
127
181
|
server.tool("skip", "Skip, unskip, list, or clear skipped issues. Skipped issues are excluded from future searches and auto-expire after 90 days.", {
|
|
128
182
|
action: z
|
|
129
183
|
.enum(["add", "remove", "list", "clear"])
|
|
@@ -156,7 +210,9 @@ export function registerTools(server, scout) {
|
|
|
156
210
|
const count = scout.getSkippedIssues().length;
|
|
157
211
|
scout.clearSkippedIssues();
|
|
158
212
|
const synced = await scout.checkpoint();
|
|
159
|
-
const syncNote = synced
|
|
213
|
+
const syncNote = synced
|
|
214
|
+
? ""
|
|
215
|
+
: " (warning: failed to persist to disk)";
|
|
160
216
|
return {
|
|
161
217
|
content: [
|
|
162
218
|
{
|
|
@@ -174,7 +230,9 @@ export function registerTools(server, scout) {
|
|
|
174
230
|
.some((s) => s.url === issueUrl);
|
|
175
231
|
scout.unskipIssue(issueUrl);
|
|
176
232
|
const synced = await scout.checkpoint();
|
|
177
|
-
const syncNote = synced
|
|
233
|
+
const syncNote = synced
|
|
234
|
+
? ""
|
|
235
|
+
: " (warning: failed to persist to disk)";
|
|
178
236
|
return {
|
|
179
237
|
content: [
|
|
180
238
|
{
|
|
@@ -187,6 +245,11 @@ export function registerTools(server, scout) {
|
|
|
187
245
|
};
|
|
188
246
|
}
|
|
189
247
|
// action === "add"
|
|
248
|
+
// Same validation as the CLI's runSkip: skip matching is exact-URL,
|
|
249
|
+
// so junk or near-miss URLs would be stored but never match anything.
|
|
250
|
+
// The throw is caught below and surfaced as isError.
|
|
251
|
+
validateUrl(issueUrl);
|
|
252
|
+
validateGitHubUrl(issueUrl, ISSUE_URL_PATTERN, "issue");
|
|
190
253
|
const alreadySkipped = scout
|
|
191
254
|
.getSkippedIssues()
|
|
192
255
|
.some((s) => s.url === issueUrl);
|
|
@@ -209,7 +272,7 @@ export function registerTools(server, scout) {
|
|
|
209
272
|
: undefined;
|
|
210
273
|
scout.skipIssue(issueUrl, metadata);
|
|
211
274
|
const synced = await scout.checkpoint();
|
|
212
|
-
const syncNote = synced ? "" : " (
|
|
275
|
+
const syncNote = synced ? "" : " (warning: failed to persist to disk)";
|
|
213
276
|
return {
|
|
214
277
|
content: [{ type: "text", text: `Skipped: ${issueUrl}${syncNote}` }],
|
|
215
278
|
};
|
|
@@ -222,118 +285,42 @@ export function registerTools(server, scout) {
|
|
|
222
285
|
};
|
|
223
286
|
}
|
|
224
287
|
});
|
|
225
|
-
const VALID_KEYS = Object.keys(ScoutPreferencesSchema.shape);
|
|
226
|
-
const ARRAY_KEYS = new Set([
|
|
227
|
-
"languages",
|
|
228
|
-
"labels",
|
|
229
|
-
"projectCategories",
|
|
230
|
-
"excludeRepos",
|
|
231
|
-
"excludeOrgs",
|
|
232
|
-
"aiPolicyBlocklist",
|
|
233
|
-
"scope",
|
|
234
|
-
"defaultStrategy",
|
|
235
|
-
]);
|
|
236
|
-
const NUMBER_KEYS = new Set([
|
|
237
|
-
"minStars",
|
|
238
|
-
"maxIssueAgeDays",
|
|
239
|
-
"minRepoScoreThreshold",
|
|
240
|
-
"interPhaseDelayMs",
|
|
241
|
-
"broadPhaseDelayMs",
|
|
242
|
-
"skipBroadWhenSufficientResults",
|
|
243
|
-
"featuresAnchorThreshold",
|
|
244
|
-
"featuresSplitRatio",
|
|
245
|
-
]);
|
|
246
|
-
const BOOLEAN_KEYS = new Set(["includeDocIssues"]);
|
|
247
288
|
server.tool("config-set", "Update an oss-scout configuration preference", {
|
|
248
289
|
key: z
|
|
249
290
|
.string()
|
|
250
|
-
.describe(
|
|
291
|
+
.describe(`Preference key to update. One of: ${SORTED_PREFERENCE_KEYS.join(", ")}`),
|
|
251
292
|
value: z
|
|
252
293
|
.string()
|
|
253
|
-
.describe(
|
|
294
|
+
.describe('New value. Comma-separated for arrays; prefix with + to append or - to remove items (e.g. "+spam/repo"). Plain string/number/boolean for scalars.'),
|
|
254
295
|
}, async ({ key, value }) => {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
},
|
|
262
|
-
],
|
|
263
|
-
isError: true,
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
let parsed;
|
|
267
|
-
if (ARRAY_KEYS.has(key)) {
|
|
268
|
-
// Accept comma-separated strings → arrays
|
|
269
|
-
try {
|
|
270
|
-
parsed = JSON.parse(value);
|
|
271
|
-
if (!Array.isArray(parsed)) {
|
|
272
|
-
parsed = value
|
|
273
|
-
.split(",")
|
|
274
|
-
.map((s) => s.trim())
|
|
275
|
-
.filter(Boolean);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
catch {
|
|
279
|
-
parsed = value
|
|
280
|
-
.split(",")
|
|
281
|
-
.map((s) => s.trim())
|
|
282
|
-
.filter(Boolean);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
else if (NUMBER_KEYS.has(key)) {
|
|
286
|
-
const num = Number(value);
|
|
287
|
-
if (isNaN(num)) {
|
|
288
|
-
return {
|
|
289
|
-
content: [
|
|
290
|
-
{ type: "text", text: `Invalid number for "${key}": "${value}"` },
|
|
291
|
-
],
|
|
292
|
-
isError: true,
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
parsed = num;
|
|
296
|
-
}
|
|
297
|
-
else if (BOOLEAN_KEYS.has(key)) {
|
|
298
|
-
const lower = value.toLowerCase();
|
|
299
|
-
if (lower === "true" || lower === "yes")
|
|
300
|
-
parsed = true;
|
|
301
|
-
else if (lower === "false" || lower === "no")
|
|
302
|
-
parsed = false;
|
|
303
|
-
else {
|
|
304
|
-
return {
|
|
305
|
-
content: [
|
|
306
|
-
{
|
|
307
|
-
type: "text",
|
|
308
|
-
text: `Invalid boolean for "${key}": "${value}". Use true/false or yes/no.`,
|
|
309
|
-
},
|
|
310
|
-
],
|
|
311
|
-
isError: true,
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
else {
|
|
316
|
-
// String or enum fields — pass through as-is
|
|
317
|
-
parsed = value;
|
|
296
|
+
// Parse + validate via the shared field map so the CLI and MCP stay in
|
|
297
|
+
// lockstep, including the +/- array syntax and the scope special case
|
|
298
|
+
// (#153). applyPreferenceField throws ValidationError on a bad key/value.
|
|
299
|
+
let validated;
|
|
300
|
+
try {
|
|
301
|
+
validated = applyPreferenceField(scout.getPreferences(), key, value);
|
|
318
302
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
// Validate the full preferences object with Zod
|
|
322
|
-
const result = ScoutPreferencesSchema.safeParse(candidate);
|
|
323
|
-
if (!result.success) {
|
|
324
|
-
const issues = result.error.issues
|
|
325
|
-
.map((i) => `${i.path.join(".")}: ${i.message}`)
|
|
326
|
-
.join("; ");
|
|
303
|
+
catch (err) {
|
|
304
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
327
305
|
return {
|
|
328
|
-
content: [{ type: "text", text:
|
|
306
|
+
content: [{ type: "text", text: msg }],
|
|
329
307
|
isError: true,
|
|
330
308
|
};
|
|
331
309
|
}
|
|
332
|
-
|
|
333
|
-
|
|
310
|
+
// Apply only the changed key; validation above ran against the full
|
|
311
|
+
// merged object so cross-field rules still hold.
|
|
312
|
+
scout.updatePreferences({
|
|
313
|
+
[key]: validated[key],
|
|
314
|
+
});
|
|
315
|
+
const persisted = await scout.checkpoint();
|
|
334
316
|
const updated = scout.getPreferences();
|
|
335
317
|
return {
|
|
336
|
-
content: [
|
|
318
|
+
content: [
|
|
319
|
+
{
|
|
320
|
+
type: "text",
|
|
321
|
+
text: JSON.stringify(withPersistWarning(updated, persisted), null, 2),
|
|
322
|
+
},
|
|
323
|
+
],
|
|
337
324
|
};
|
|
338
325
|
});
|
|
339
326
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oss-scout/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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": "^
|
|
21
|
+
"@oss-scout/core": "^1.1.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@types/node": "^25.5.0",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
"scripts": {
|
|
48
48
|
"build": "tsc",
|
|
49
|
-
"bundle": "esbuild src/
|
|
49
|
+
"bundle": "esbuild src/bin.ts --bundle --platform=node --target=node20 --format=cjs --outfile=dist/mcp-server.bundle.cjs",
|
|
50
50
|
"test": "vitest run",
|
|
51
51
|
"test:watch": "vitest",
|
|
52
52
|
"test:coverage": "vitest run --coverage",
|