@oss-scout/mcp 0.9.0 → 0.10.1

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/tools.js CHANGED
@@ -3,6 +3,25 @@ import { SearchStrategySchema, ISSUE_URL_PATTERN, validateGitHubUrl, validateUrl
3
3
  // Generous ceiling: even with inter-phase delays zeroed, vetting many issues
4
4
  // under the budget tracker's sliding-window pacing can take minutes (#143).
5
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
+ }
19
+ /**
20
+ * Rejects when `ms` elapses, but does NOT cancel the underlying work — the
21
+ * scout call keeps running (consuming API budget and mutating scout state)
22
+ * after the tool has returned its timeout error. Acceptable for a local
23
+ * stdio server; revisit with AbortSignal threading if it ever matters (#295).
24
+ */
6
25
  function withTimeout(promise, ms = TOOL_TIMEOUT_MS) {
7
26
  let timeoutId;
8
27
  const timer = new Promise((_, reject) => {
@@ -14,8 +33,11 @@ export function registerTools(server, scout) {
14
33
  server.tool("search", "Search for open source issues matching your preferences", {
15
34
  maxResults: z
16
35
  .number()
36
+ .int()
37
+ .min(1)
38
+ .max(50)
17
39
  .optional()
18
- .describe("Maximum number of results to return (default 10)"),
40
+ .describe("Maximum number of results to return (1-50, default 10)"),
19
41
  strategies: z
20
42
  .string()
21
43
  .optional()
@@ -28,24 +50,41 @@ export function registerTools(server, scout) {
28
50
  .array(z.string())
29
51
  .optional()
30
52
  .describe("Soft-boost ranking for candidates in one of these `owner/repo` slugs. Stronger weight than language match (#1244). Not a filter; only reorders."),
53
+ avoidRepos: z
54
+ .array(z.string())
55
+ .optional()
56
+ .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."),
57
+ boostIssueTypes: z
58
+ .array(z.string())
59
+ .optional()
60
+ .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.'),
31
61
  diversityRatio: z
32
62
  .number()
33
63
  .min(0)
34
64
  .max(1)
35
65
  .optional()
36
66
  .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, }) => {
67
+ }, async ({ maxResults, strategies, preferLanguages, preferRepos, avoidRepos, boostIssueTypes, diversityRatio, }) => {
38
68
  try {
69
+ // Validate each CSV entry with a readable error instead of letting a
70
+ // raw ZodError string reach the model (#295).
39
71
  const parsedStrategies = strategies
40
- ? strategies
41
- .split(",")
42
- .map((s) => SearchStrategySchema.parse(s.trim()))
72
+ ? strategies.split(",").map((s) => {
73
+ const trimmed = s.trim();
74
+ const parsed = SearchStrategySchema.safeParse(trimmed);
75
+ if (!parsed.success) {
76
+ throw new Error(`Invalid strategy "${trimmed}". Valid strategies: ${SearchStrategySchema.options.join(", ")}`);
77
+ }
78
+ return parsed.data;
79
+ })
43
80
  : undefined;
44
81
  const result = await withTimeout(scout.search({
45
82
  maxResults: maxResults ?? 10,
46
83
  strategies: parsedStrategies,
47
84
  preferLanguages,
48
85
  preferRepos,
86
+ avoidRepos,
87
+ boostIssueTypes,
49
88
  diversityRatio,
50
89
  // No fixed inter-phase sleeps in the request/response MCP context;
51
90
  // the budget tracker still paces the GitHub calls (#143)
@@ -53,9 +92,14 @@ export function registerTools(server, scout) {
53
92
  broadPhaseDelayMs: 0,
54
93
  }));
55
94
  scout.saveResults(result.candidates);
56
- await scout.checkpoint();
95
+ const persisted = await scout.checkpoint();
57
96
  return {
58
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
97
+ content: [
98
+ {
99
+ type: "text",
100
+ text: JSON.stringify(withPersistWarning(result, persisted), null, 2),
101
+ },
102
+ ],
59
103
  };
60
104
  }
61
105
  catch (err) {
@@ -69,8 +113,11 @@ export function registerTools(server, scout) {
69
113
  server.tool("scout-features", "Surface feature-scoped contribution opportunities in repos where you have 3+ merged PRs", {
70
114
  maxResults: z
71
115
  .number()
116
+ .int()
117
+ .min(1)
118
+ .max(50)
72
119
  .optional()
73
- .describe("Maximum number of results to return (default 10)"),
120
+ .describe("Maximum number of results to return (1-50, default 10)"),
74
121
  anchorThreshold: z
75
122
  .number()
76
123
  .int()
@@ -92,9 +139,14 @@ export function registerTools(server, scout) {
92
139
  splitRatio,
93
140
  }));
94
141
  scout.saveResults([...result.quickWins, ...result.biggerBets]);
95
- await scout.checkpoint();
142
+ const persisted = await scout.checkpoint();
96
143
  return {
97
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
144
+ content: [
145
+ {
146
+ type: "text",
147
+ text: JSON.stringify(withPersistWarning(result, persisted), null, 2),
148
+ },
149
+ ],
98
150
  };
99
151
  }
100
152
  catch (err) {
@@ -192,6 +244,9 @@ export function registerTools(server, scout) {
192
244
  };
193
245
  }
194
246
  if (action === "remove") {
247
+ // Deliberately no URL validation, matching the CLI's runSkipRemove:
248
+ // entries stored before skip-add validation existed may be junk,
249
+ // and exact-match removal is the only way to clean them up.
195
250
  const wasPresent = scout
196
251
  .getSkippedIssues()
197
252
  .some((s) => s.url === issueUrl);
@@ -279,10 +334,15 @@ export function registerTools(server, scout) {
279
334
  scout.updatePreferences({
280
335
  [key]: validated[key],
281
336
  });
282
- await scout.checkpoint();
337
+ const persisted = await scout.checkpoint();
283
338
  const updated = scout.getPreferences();
284
339
  return {
285
- content: [{ type: "text", text: JSON.stringify(updated, null, 2) }],
340
+ content: [
341
+ {
342
+ type: "text",
343
+ text: JSON.stringify(withPersistWarning(updated, persisted), null, 2),
344
+ },
345
+ ],
286
346
  };
287
347
  });
288
348
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oss-scout/mcp",
3
- "version": "0.9.0",
3
+ "version": "0.10.1",
4
4
  "description": "MCP server for oss-scout — exposes issue discovery and vetting as MCP tools",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,8 +8,8 @@
8
8
  },
9
9
  "exports": {
10
10
  ".": {
11
- "import": "./dist/index.js",
12
- "types": "./dist/index.d.ts"
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
13
  }
14
14
  },
15
15
  "files": [
@@ -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.0.0"
21
+ "@oss-scout/core": "^1.5.1"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/node": "^25.5.0",