@oss-scout/mcp 0.8.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 +1258 -580
- package/dist/tools.js +51 -102
- 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) => {
|
|
@@ -45,6 +47,10 @@ export function registerTools(server, scout) {
|
|
|
45
47
|
preferLanguages,
|
|
46
48
|
preferRepos,
|
|
47
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,
|
|
48
54
|
}));
|
|
49
55
|
scout.saveResults(result.candidates);
|
|
50
56
|
await scout.checkpoint();
|
|
@@ -124,6 +130,21 @@ export function registerTools(server, scout) {
|
|
|
124
130
|
content: [{ type: "text", text: JSON.stringify(preferences, null, 2) }],
|
|
125
131
|
};
|
|
126
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
|
+
});
|
|
127
148
|
server.tool("skip", "Skip, unskip, list, or clear skipped issues. Skipped issues are excluded from future searches and auto-expire after 90 days.", {
|
|
128
149
|
action: z
|
|
129
150
|
.enum(["add", "remove", "list", "clear"])
|
|
@@ -156,7 +177,9 @@ export function registerTools(server, scout) {
|
|
|
156
177
|
const count = scout.getSkippedIssues().length;
|
|
157
178
|
scout.clearSkippedIssues();
|
|
158
179
|
const synced = await scout.checkpoint();
|
|
159
|
-
const syncNote = synced
|
|
180
|
+
const syncNote = synced
|
|
181
|
+
? ""
|
|
182
|
+
: " (warning: failed to persist to disk)";
|
|
160
183
|
return {
|
|
161
184
|
content: [
|
|
162
185
|
{
|
|
@@ -174,7 +197,9 @@ export function registerTools(server, scout) {
|
|
|
174
197
|
.some((s) => s.url === issueUrl);
|
|
175
198
|
scout.unskipIssue(issueUrl);
|
|
176
199
|
const synced = await scout.checkpoint();
|
|
177
|
-
const syncNote = synced
|
|
200
|
+
const syncNote = synced
|
|
201
|
+
? ""
|
|
202
|
+
: " (warning: failed to persist to disk)";
|
|
178
203
|
return {
|
|
179
204
|
content: [
|
|
180
205
|
{
|
|
@@ -187,6 +212,11 @@ export function registerTools(server, scout) {
|
|
|
187
212
|
};
|
|
188
213
|
}
|
|
189
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");
|
|
190
220
|
const alreadySkipped = scout
|
|
191
221
|
.getSkippedIssues()
|
|
192
222
|
.some((s) => s.url === issueUrl);
|
|
@@ -209,7 +239,7 @@ export function registerTools(server, scout) {
|
|
|
209
239
|
: undefined;
|
|
210
240
|
scout.skipIssue(issueUrl, metadata);
|
|
211
241
|
const synced = await scout.checkpoint();
|
|
212
|
-
const syncNote = synced ? "" : " (
|
|
242
|
+
const syncNote = synced ? "" : " (warning: failed to persist to disk)";
|
|
213
243
|
return {
|
|
214
244
|
content: [{ type: "text", text: `Skipped: ${issueUrl}${syncNote}` }],
|
|
215
245
|
};
|
|
@@ -222,114 +252,33 @@ export function registerTools(server, scout) {
|
|
|
222
252
|
};
|
|
223
253
|
}
|
|
224
254
|
});
|
|
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
255
|
server.tool("config-set", "Update an oss-scout configuration preference", {
|
|
248
256
|
key: z
|
|
249
257
|
.string()
|
|
250
|
-
.describe(
|
|
258
|
+
.describe(`Preference key to update. One of: ${SORTED_PREFERENCE_KEYS.join(", ")}`),
|
|
251
259
|
value: z
|
|
252
260
|
.string()
|
|
253
|
-
.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.'),
|
|
254
262
|
}, 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;
|
|
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);
|
|
318
269
|
}
|
|
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("; ");
|
|
270
|
+
catch (err) {
|
|
271
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
327
272
|
return {
|
|
328
|
-
content: [{ type: "text", text:
|
|
273
|
+
content: [{ type: "text", text: msg }],
|
|
329
274
|
isError: true,
|
|
330
275
|
};
|
|
331
276
|
}
|
|
332
|
-
|
|
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
|
+
});
|
|
333
282
|
await scout.checkpoint();
|
|
334
283
|
const updated = scout.getPreferences();
|
|
335
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",
|