@oss-scout/mcp 0.7.0 → 0.9.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 +1306 -577
- package/dist/tools.js +59 -103
- package/package.json +3 -3
package/dist/tools.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
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;
|
|
4
6
|
function withTimeout(promise, ms = TOOL_TIMEOUT_MS) {
|
|
5
7
|
let timeoutId;
|
|
6
8
|
const timer = new Promise((_, reject) => {
|
|
@@ -26,7 +28,13 @@ export function registerTools(server, scout) {
|
|
|
26
28
|
.array(z.string())
|
|
27
29
|
.optional()
|
|
28
30
|
.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
|
-
|
|
31
|
+
diversityRatio: z
|
|
32
|
+
.number()
|
|
33
|
+
.min(0)
|
|
34
|
+
.max(1)
|
|
35
|
+
.optional()
|
|
36
|
+
.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."),
|
|
37
|
+
}, async ({ maxResults, strategies, preferLanguages, preferRepos, diversityRatio, }) => {
|
|
30
38
|
try {
|
|
31
39
|
const parsedStrategies = strategies
|
|
32
40
|
? strategies
|
|
@@ -38,6 +46,11 @@ export function registerTools(server, scout) {
|
|
|
38
46
|
strategies: parsedStrategies,
|
|
39
47
|
preferLanguages,
|
|
40
48
|
preferRepos,
|
|
49
|
+
diversityRatio,
|
|
50
|
+
// No fixed inter-phase sleeps in the request/response MCP context;
|
|
51
|
+
// the budget tracker still paces the GitHub calls (#143)
|
|
52
|
+
interPhaseDelayMs: 0,
|
|
53
|
+
broadPhaseDelayMs: 0,
|
|
41
54
|
}));
|
|
42
55
|
scout.saveResults(result.candidates);
|
|
43
56
|
await scout.checkpoint();
|
|
@@ -117,6 +130,21 @@ export function registerTools(server, scout) {
|
|
|
117
130
|
content: [{ type: "text", text: JSON.stringify(preferences, null, 2) }],
|
|
118
131
|
};
|
|
119
132
|
});
|
|
133
|
+
server.tool("sync", "Reconcile tracked open PRs against GitHub — mark merged/closed and recompute repo scores. Cheap; good for a daily startup call.", async () => {
|
|
134
|
+
try {
|
|
135
|
+
const result = await withTimeout(scout.syncOpenPRs());
|
|
136
|
+
return {
|
|
137
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
142
|
+
return {
|
|
143
|
+
content: [{ type: "text", text: `Error: ${msg}` }],
|
|
144
|
+
isError: true,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
});
|
|
120
148
|
server.tool("skip", "Skip, unskip, list, or clear skipped issues. Skipped issues are excluded from future searches and auto-expire after 90 days.", {
|
|
121
149
|
action: z
|
|
122
150
|
.enum(["add", "remove", "list", "clear"])
|
|
@@ -149,7 +177,9 @@ export function registerTools(server, scout) {
|
|
|
149
177
|
const count = scout.getSkippedIssues().length;
|
|
150
178
|
scout.clearSkippedIssues();
|
|
151
179
|
const synced = await scout.checkpoint();
|
|
152
|
-
const syncNote = synced
|
|
180
|
+
const syncNote = synced
|
|
181
|
+
? ""
|
|
182
|
+
: " (warning: failed to persist to disk)";
|
|
153
183
|
return {
|
|
154
184
|
content: [
|
|
155
185
|
{
|
|
@@ -167,7 +197,9 @@ export function registerTools(server, scout) {
|
|
|
167
197
|
.some((s) => s.url === issueUrl);
|
|
168
198
|
scout.unskipIssue(issueUrl);
|
|
169
199
|
const synced = await scout.checkpoint();
|
|
170
|
-
const syncNote = synced
|
|
200
|
+
const syncNote = synced
|
|
201
|
+
? ""
|
|
202
|
+
: " (warning: failed to persist to disk)";
|
|
171
203
|
return {
|
|
172
204
|
content: [
|
|
173
205
|
{
|
|
@@ -180,6 +212,11 @@ export function registerTools(server, scout) {
|
|
|
180
212
|
};
|
|
181
213
|
}
|
|
182
214
|
// action === "add"
|
|
215
|
+
// Same validation as the CLI's runSkip: skip matching is exact-URL,
|
|
216
|
+
// so junk or near-miss URLs would be stored but never match anything.
|
|
217
|
+
// The throw is caught below and surfaced as isError.
|
|
218
|
+
validateUrl(issueUrl);
|
|
219
|
+
validateGitHubUrl(issueUrl, ISSUE_URL_PATTERN, "issue");
|
|
183
220
|
const alreadySkipped = scout
|
|
184
221
|
.getSkippedIssues()
|
|
185
222
|
.some((s) => s.url === issueUrl);
|
|
@@ -202,7 +239,7 @@ export function registerTools(server, scout) {
|
|
|
202
239
|
: undefined;
|
|
203
240
|
scout.skipIssue(issueUrl, metadata);
|
|
204
241
|
const synced = await scout.checkpoint();
|
|
205
|
-
const syncNote = synced ? "" : " (
|
|
242
|
+
const syncNote = synced ? "" : " (warning: failed to persist to disk)";
|
|
206
243
|
return {
|
|
207
244
|
content: [{ type: "text", text: `Skipped: ${issueUrl}${syncNote}` }],
|
|
208
245
|
};
|
|
@@ -215,114 +252,33 @@ export function registerTools(server, scout) {
|
|
|
215
252
|
};
|
|
216
253
|
}
|
|
217
254
|
});
|
|
218
|
-
const VALID_KEYS = Object.keys(ScoutPreferencesSchema.shape);
|
|
219
|
-
const ARRAY_KEYS = new Set([
|
|
220
|
-
"languages",
|
|
221
|
-
"labels",
|
|
222
|
-
"projectCategories",
|
|
223
|
-
"excludeRepos",
|
|
224
|
-
"excludeOrgs",
|
|
225
|
-
"aiPolicyBlocklist",
|
|
226
|
-
"scope",
|
|
227
|
-
"defaultStrategy",
|
|
228
|
-
]);
|
|
229
|
-
const NUMBER_KEYS = new Set([
|
|
230
|
-
"minStars",
|
|
231
|
-
"maxIssueAgeDays",
|
|
232
|
-
"minRepoScoreThreshold",
|
|
233
|
-
"interPhaseDelayMs",
|
|
234
|
-
"broadPhaseDelayMs",
|
|
235
|
-
"skipBroadWhenSufficientResults",
|
|
236
|
-
"featuresAnchorThreshold",
|
|
237
|
-
"featuresSplitRatio",
|
|
238
|
-
]);
|
|
239
|
-
const BOOLEAN_KEYS = new Set(["includeDocIssues"]);
|
|
240
255
|
server.tool("config-set", "Update an oss-scout configuration preference", {
|
|
241
256
|
key: z
|
|
242
257
|
.string()
|
|
243
|
-
.describe(
|
|
258
|
+
.describe(`Preference key to update. One of: ${SORTED_PREFERENCE_KEYS.join(", ")}`),
|
|
244
259
|
value: z
|
|
245
260
|
.string()
|
|
246
|
-
.describe(
|
|
261
|
+
.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.'),
|
|
247
262
|
}, async ({ key, value }) => {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
},
|
|
255
|
-
],
|
|
256
|
-
isError: true,
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
let parsed;
|
|
260
|
-
if (ARRAY_KEYS.has(key)) {
|
|
261
|
-
// Accept comma-separated strings → arrays
|
|
262
|
-
try {
|
|
263
|
-
parsed = JSON.parse(value);
|
|
264
|
-
if (!Array.isArray(parsed)) {
|
|
265
|
-
parsed = value
|
|
266
|
-
.split(",")
|
|
267
|
-
.map((s) => s.trim())
|
|
268
|
-
.filter(Boolean);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
catch {
|
|
272
|
-
parsed = value
|
|
273
|
-
.split(",")
|
|
274
|
-
.map((s) => s.trim())
|
|
275
|
-
.filter(Boolean);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
else if (NUMBER_KEYS.has(key)) {
|
|
279
|
-
const num = Number(value);
|
|
280
|
-
if (isNaN(num)) {
|
|
281
|
-
return {
|
|
282
|
-
content: [
|
|
283
|
-
{ type: "text", text: `Invalid number for "${key}": "${value}"` },
|
|
284
|
-
],
|
|
285
|
-
isError: true,
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
parsed = num;
|
|
289
|
-
}
|
|
290
|
-
else if (BOOLEAN_KEYS.has(key)) {
|
|
291
|
-
const lower = value.toLowerCase();
|
|
292
|
-
if (lower === "true" || lower === "yes")
|
|
293
|
-
parsed = true;
|
|
294
|
-
else if (lower === "false" || lower === "no")
|
|
295
|
-
parsed = false;
|
|
296
|
-
else {
|
|
297
|
-
return {
|
|
298
|
-
content: [
|
|
299
|
-
{
|
|
300
|
-
type: "text",
|
|
301
|
-
text: `Invalid boolean for "${key}": "${value}". Use true/false or yes/no.`,
|
|
302
|
-
},
|
|
303
|
-
],
|
|
304
|
-
isError: true,
|
|
305
|
-
};
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
else {
|
|
309
|
-
// String or enum fields — pass through as-is
|
|
310
|
-
parsed = value;
|
|
263
|
+
// Parse + validate via the shared field map so the CLI and MCP stay in
|
|
264
|
+
// lockstep, including the +/- array syntax and the scope special case
|
|
265
|
+
// (#153). applyPreferenceField throws ValidationError on a bad key/value.
|
|
266
|
+
let validated;
|
|
267
|
+
try {
|
|
268
|
+
validated = applyPreferenceField(scout.getPreferences(), key, value);
|
|
311
269
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
// Validate the full preferences object with Zod
|
|
315
|
-
const result = ScoutPreferencesSchema.safeParse(candidate);
|
|
316
|
-
if (!result.success) {
|
|
317
|
-
const issues = result.error.issues
|
|
318
|
-
.map((i) => `${i.path.join(".")}: ${i.message}`)
|
|
319
|
-
.join("; ");
|
|
270
|
+
catch (err) {
|
|
271
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
320
272
|
return {
|
|
321
|
-
content: [{ type: "text", text:
|
|
273
|
+
content: [{ type: "text", text: msg }],
|
|
322
274
|
isError: true,
|
|
323
275
|
};
|
|
324
276
|
}
|
|
325
|
-
|
|
277
|
+
// Apply only the changed key; validation above ran against the full
|
|
278
|
+
// merged object so cross-field rules still hold.
|
|
279
|
+
scout.updatePreferences({
|
|
280
|
+
[key]: validated[key],
|
|
281
|
+
});
|
|
326
282
|
await scout.checkpoint();
|
|
327
283
|
const updated = scout.getPreferences();
|
|
328
284
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oss-scout/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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.
|
|
21
|
+
"@oss-scout/core": "^1.0.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",
|