@oss-scout/mcp 0.2.0 → 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.
@@ -1,3 +1,3 @@
1
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import type { OssScout } from '@oss-scout/core';
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { OssScout } from "@oss-scout/core";
3
3
  export declare function registerResources(server: McpServer, scout: OssScout): void;
package/dist/resources.js CHANGED
@@ -1,26 +1,32 @@
1
1
  export function registerResources(server, scout) {
2
- server.resource('config', 'scout://config', { description: 'Current oss-scout preferences' }, async () => ({
3
- contents: [{
4
- uri: 'scout://config',
5
- mimeType: 'application/json',
2
+ server.resource("config", "scout://config", { description: "Current oss-scout preferences" }, async () => ({
3
+ contents: [
4
+ {
5
+ uri: "scout://config",
6
+ mimeType: "application/json",
6
7
  text: JSON.stringify(scout.getPreferences(), null, 2),
7
- }],
8
+ },
9
+ ],
8
10
  }));
9
- server.resource('results', 'scout://results', { description: 'Saved search results from previous runs' }, async () => ({
10
- contents: [{
11
- uri: 'scout://results',
12
- mimeType: 'application/json',
11
+ server.resource("results", "scout://results", { description: "Saved search results from previous runs" }, async () => ({
12
+ contents: [
13
+ {
14
+ uri: "scout://results",
15
+ mimeType: "application/json",
13
16
  text: JSON.stringify(scout.getSavedResults(), null, 2),
14
- }],
17
+ },
18
+ ],
15
19
  }));
16
- server.resource('scores', 'scout://scores', { description: 'Repository score data based on contribution history' }, async () => {
20
+ server.resource("scores", "scout://scores", { description: "Repository score data based on contribution history" }, async () => {
17
21
  const state = scout.getState();
18
22
  return {
19
- contents: [{
20
- uri: 'scout://scores',
21
- mimeType: 'application/json',
23
+ contents: [
24
+ {
25
+ uri: "scout://scores",
26
+ mimeType: "application/json",
22
27
  text: JSON.stringify(state.repoScores, null, 2),
23
- }],
28
+ },
29
+ ],
24
30
  };
25
31
  });
26
32
  }
package/dist/tools.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
- import type { OssScout } from '@oss-scout/core';
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { OssScout } from "@oss-scout/core";
3
3
  export declare function registerTools(server: McpServer, scout: OssScout): void;
package/dist/tools.js CHANGED
@@ -1,43 +1,204 @@
1
- import { z } from 'zod';
2
- import { SearchStrategySchema, ScoutPreferencesSchema } from '@oss-scout/core';
1
+ import { z } from "zod";
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
- server.tool('search', 'Search for open source issues matching your preferences', {
5
- maxResults: z.number().optional().describe('Maximum number of results to return (default 10)'),
6
- strategies: z.string().optional().describe('Comma-separated search strategies: merged, orgs, starred, broad, maintained, all'),
12
+ server.tool("search", "Search for open source issues matching your preferences", {
13
+ maxResults: z
14
+ .number()
15
+ .optional()
16
+ .describe("Maximum number of results to return (default 10)"),
17
+ strategies: z
18
+ .string()
19
+ .optional()
20
+ .describe("Comma-separated search strategies: merged, starred, broad, maintained, all"),
7
21
  }, async ({ maxResults, strategies }) => {
8
- const parsedStrategies = strategies
9
- ? strategies.split(',').map((s) => SearchStrategySchema.parse(s.trim()))
10
- : undefined;
11
- const result = await scout.search({
12
- maxResults: maxResults ?? 10,
13
- strategies: parsedStrategies,
14
- });
15
- return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
22
+ try {
23
+ const parsedStrategies = strategies
24
+ ? strategies
25
+ .split(",")
26
+ .map((s) => SearchStrategySchema.parse(s.trim()))
27
+ : undefined;
28
+ const result = await withTimeout(scout.search({
29
+ maxResults: maxResults ?? 10,
30
+ strategies: parsedStrategies,
31
+ }));
32
+ scout.saveResults(result.candidates);
33
+ await scout.checkpoint();
34
+ return {
35
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
36
+ };
37
+ }
38
+ catch (err) {
39
+ const msg = err instanceof Error ? err.message : String(err);
40
+ return {
41
+ content: [{ type: "text", text: `Error: ${msg}` }],
42
+ isError: true,
43
+ };
44
+ }
16
45
  });
17
- server.tool('vet', 'Vet a specific GitHub issue for contribution viability', {
18
- issueUrl: z.string().describe('Full GitHub issue URL (e.g. https://github.com/owner/repo/issues/123)'),
46
+ server.tool("vet", "Vet a specific GitHub issue for contribution viability", {
47
+ issueUrl: z
48
+ .string()
49
+ .describe("Full GitHub issue URL (e.g. https://github.com/owner/repo/issues/123)"),
19
50
  }, async ({ issueUrl }) => {
20
- const candidate = await scout.vetIssue(issueUrl);
21
- return { content: [{ type: 'text', text: JSON.stringify(candidate, null, 2) }] };
51
+ try {
52
+ const candidate = await withTimeout(scout.vetIssue(issueUrl));
53
+ return {
54
+ content: [{ type: "text", text: JSON.stringify(candidate, null, 2) }],
55
+ };
56
+ }
57
+ catch (err) {
58
+ const msg = err instanceof Error ? err.message : String(err);
59
+ return {
60
+ content: [{ type: "text", text: `Error: ${msg}` }],
61
+ isError: true,
62
+ };
63
+ }
22
64
  });
23
- server.tool('config', 'Show current oss-scout configuration preferences', async () => {
65
+ server.tool("config", "Show current oss-scout configuration preferences", async () => {
24
66
  const preferences = scout.getPreferences();
25
- return { content: [{ type: 'text', text: JSON.stringify(preferences, null, 2) }] };
67
+ return {
68
+ content: [{ type: "text", text: JSON.stringify(preferences, null, 2) }],
69
+ };
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
+ }
26
168
  });
27
169
  const VALID_KEYS = Object.keys(ScoutPreferencesSchema.shape);
28
170
  const ARRAY_KEYS = new Set([
29
- 'languages', 'labels', 'preferredOrgs', 'projectCategories',
30
- 'excludeRepos', 'aiPolicyBlocklist', 'scope', 'defaultStrategy',
171
+ "languages",
172
+ "labels",
173
+ "projectCategories",
174
+ "excludeRepos",
175
+ "excludeOrgs",
176
+ "aiPolicyBlocklist",
177
+ "scope",
178
+ "defaultStrategy",
179
+ ]);
180
+ const NUMBER_KEYS = new Set([
181
+ "minStars",
182
+ "maxIssueAgeDays",
183
+ "minRepoScoreThreshold",
31
184
  ]);
32
- const NUMBER_KEYS = new Set(['minStars', 'maxIssueAgeDays', 'minRepoScoreThreshold']);
33
- const BOOLEAN_KEYS = new Set(['includeDocIssues']);
34
- server.tool('config-set', 'Update an oss-scout configuration preference', {
35
- key: z.string().describe('Preference key to update (e.g. languages, minStars, excludeRepos)'),
36
- value: z.string().describe('New value — comma-separated for arrays, plain string for scalars'),
185
+ const BOOLEAN_KEYS = new Set(["includeDocIssues"]);
186
+ server.tool("config-set", "Update an oss-scout configuration preference", {
187
+ key: z
188
+ .string()
189
+ .describe("Preference key to update (e.g. languages, minStars, excludeRepos)"),
190
+ value: z
191
+ .string()
192
+ .describe("New value — comma-separated for arrays, plain string for scalars"),
37
193
  }, async ({ key, value }) => {
38
194
  if (!VALID_KEYS.includes(key)) {
39
195
  return {
40
- content: [{ type: 'text', text: `Unknown config key: "${key}". Valid keys: ${VALID_KEYS.join(', ')}` }],
196
+ content: [
197
+ {
198
+ type: "text",
199
+ text: `Unknown config key: "${key}". Valid keys: ${VALID_KEYS.join(", ")}`,
200
+ },
201
+ ],
41
202
  isError: true,
42
203
  };
43
204
  }
@@ -47,18 +208,26 @@ export function registerTools(server, scout) {
47
208
  try {
48
209
  parsed = JSON.parse(value);
49
210
  if (!Array.isArray(parsed)) {
50
- parsed = value.split(',').map((s) => s.trim()).filter(Boolean);
211
+ parsed = value
212
+ .split(",")
213
+ .map((s) => s.trim())
214
+ .filter(Boolean);
51
215
  }
52
216
  }
53
217
  catch {
54
- parsed = value.split(',').map((s) => s.trim()).filter(Boolean);
218
+ parsed = value
219
+ .split(",")
220
+ .map((s) => s.trim())
221
+ .filter(Boolean);
55
222
  }
56
223
  }
57
224
  else if (NUMBER_KEYS.has(key)) {
58
225
  const num = Number(value);
59
226
  if (isNaN(num)) {
60
227
  return {
61
- content: [{ type: 'text', text: `Invalid number for "${key}": "${value}"` }],
228
+ content: [
229
+ { type: "text", text: `Invalid number for "${key}": "${value}"` },
230
+ ],
62
231
  isError: true,
63
232
  };
64
233
  }
@@ -66,13 +235,18 @@ export function registerTools(server, scout) {
66
235
  }
67
236
  else if (BOOLEAN_KEYS.has(key)) {
68
237
  const lower = value.toLowerCase();
69
- if (lower === 'true' || lower === 'yes')
238
+ if (lower === "true" || lower === "yes")
70
239
  parsed = true;
71
- else if (lower === 'false' || lower === 'no')
240
+ else if (lower === "false" || lower === "no")
72
241
  parsed = false;
73
242
  else {
74
243
  return {
75
- content: [{ type: 'text', text: `Invalid boolean for "${key}": "${value}". Use true/false or yes/no.` }],
244
+ content: [
245
+ {
246
+ type: "text",
247
+ text: `Invalid boolean for "${key}": "${value}". Use true/false or yes/no.`,
248
+ },
249
+ ],
76
250
  isError: true,
77
251
  };
78
252
  }
@@ -86,14 +260,19 @@ export function registerTools(server, scout) {
86
260
  // Validate the full preferences object with Zod
87
261
  const result = ScoutPreferencesSchema.safeParse(candidate);
88
262
  if (!result.success) {
89
- const issues = result.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ');
263
+ const issues = result.error.issues
264
+ .map((i) => `${i.path.join(".")}: ${i.message}`)
265
+ .join("; ");
90
266
  return {
91
- content: [{ type: 'text', text: `Validation error: ${issues}` }],
267
+ content: [{ type: "text", text: `Validation error: ${issues}` }],
92
268
  isError: true,
93
269
  };
94
270
  }
95
271
  scout.updatePreferences({ [key]: parsed });
272
+ await scout.checkpoint();
96
273
  const updated = scout.getPreferences();
97
- return { content: [{ type: 'text', text: JSON.stringify(updated, null, 2) }] };
274
+ return {
275
+ content: [{ type: "text", text: JSON.stringify(updated, null, 2) }],
276
+ };
98
277
  });
99
278
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oss-scout/mcp",
3
- "version": "0.2.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
- "@oss-scout/core": "workspace:^",
30
- "zod": "^4.3.6"
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
+ }