mcp-researchpowerpack 3.9.4 → 4.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.
Files changed (188) hide show
  1. package/README.md +116 -208
  2. package/dist/index.js +280 -337
  3. package/dist/index.js.map +7 -1
  4. package/dist/mcp-use.json +7 -0
  5. package/dist/src/clients/reddit.js +278 -0
  6. package/dist/src/clients/reddit.js.map +7 -0
  7. package/dist/src/clients/scraper.js +326 -0
  8. package/dist/src/clients/scraper.js.map +7 -0
  9. package/dist/src/clients/search.js +217 -0
  10. package/dist/src/clients/search.js.map +7 -0
  11. package/dist/src/config/index.js +138 -0
  12. package/dist/src/config/index.js.map +7 -0
  13. package/dist/src/prompts/deep-research.js +41 -0
  14. package/dist/src/prompts/deep-research.js.map +7 -0
  15. package/dist/src/prompts/reddit-sentiment.js +47 -0
  16. package/dist/src/prompts/reddit-sentiment.js.map +7 -0
  17. package/dist/src/schemas/reddit.js +21 -0
  18. package/dist/src/schemas/reddit.js.map +7 -0
  19. package/dist/src/schemas/scrape-links.js +26 -0
  20. package/dist/src/schemas/scrape-links.js.map +7 -0
  21. package/dist/src/schemas/start-research.js +17 -0
  22. package/dist/src/schemas/start-research.js.map +7 -0
  23. package/dist/src/schemas/web-search.js +53 -0
  24. package/dist/src/schemas/web-search.js.map +7 -0
  25. package/dist/src/services/llm-processor.js +675 -0
  26. package/dist/src/services/llm-processor.js.map +7 -0
  27. package/dist/src/services/markdown-cleaner.js +62 -0
  28. package/dist/src/services/markdown-cleaner.js.map +7 -0
  29. package/dist/src/services/workflow-state.js +116 -0
  30. package/dist/src/services/workflow-state.js.map +7 -0
  31. package/dist/src/tools/mcp-helpers.js +50 -0
  32. package/dist/src/tools/mcp-helpers.js.map +7 -0
  33. package/dist/src/tools/reddit.js +277 -0
  34. package/dist/src/tools/reddit.js.map +7 -0
  35. package/dist/src/tools/registry.js +18 -0
  36. package/dist/src/tools/registry.js.map +7 -0
  37. package/dist/src/tools/scrape.js +334 -0
  38. package/dist/src/tools/scrape.js.map +7 -0
  39. package/dist/src/tools/search.js +423 -0
  40. package/dist/src/tools/search.js.map +7 -0
  41. package/dist/src/tools/start-research.js +199 -0
  42. package/dist/src/tools/start-research.js.map +7 -0
  43. package/dist/src/tools/utils.js +29 -0
  44. package/dist/src/tools/utils.js.map +7 -0
  45. package/dist/src/utils/bootstrap-guard.js +27 -0
  46. package/dist/src/utils/bootstrap-guard.js.map +7 -0
  47. package/dist/src/utils/concurrency.js +62 -0
  48. package/dist/src/utils/concurrency.js.map +7 -0
  49. package/dist/src/utils/content-extractor.js +61 -0
  50. package/dist/src/utils/content-extractor.js.map +7 -0
  51. package/dist/src/utils/errors.js +211 -0
  52. package/dist/src/utils/errors.js.map +7 -0
  53. package/dist/src/utils/logger.js +25 -0
  54. package/dist/src/utils/logger.js.map +7 -0
  55. package/dist/src/utils/markdown-formatter.js +15 -0
  56. package/dist/src/utils/markdown-formatter.js.map +7 -0
  57. package/dist/src/utils/reddit-keyword-guard.js +29 -0
  58. package/dist/src/utils/reddit-keyword-guard.js.map +7 -0
  59. package/dist/src/utils/response.js +81 -0
  60. package/dist/src/utils/response.js.map +7 -0
  61. package/dist/src/utils/retry.js +13 -0
  62. package/dist/src/utils/retry.js.map +7 -0
  63. package/dist/src/utils/sanitize.js +10 -0
  64. package/dist/src/utils/sanitize.js.map +7 -0
  65. package/dist/src/utils/source-type.js +41 -0
  66. package/dist/src/utils/source-type.js.map +7 -0
  67. package/dist/src/utils/url-aggregator.js +227 -0
  68. package/dist/src/utils/url-aggregator.js.map +7 -0
  69. package/dist/src/utils/workflow-key.js +14 -0
  70. package/dist/src/utils/workflow-key.js.map +7 -0
  71. package/dist/src/version.js +32 -0
  72. package/dist/src/version.js.map +7 -0
  73. package/package.json +33 -28
  74. package/dist/clients/reddit.d.ts +0 -69
  75. package/dist/clients/reddit.d.ts.map +0 -1
  76. package/dist/clients/reddit.js +0 -369
  77. package/dist/clients/reddit.js.map +0 -1
  78. package/dist/clients/research.d.ts +0 -67
  79. package/dist/clients/research.d.ts.map +0 -1
  80. package/dist/clients/research.js +0 -290
  81. package/dist/clients/research.js.map +0 -1
  82. package/dist/clients/scraper.d.ts +0 -72
  83. package/dist/clients/scraper.d.ts.map +0 -1
  84. package/dist/clients/scraper.js +0 -338
  85. package/dist/clients/scraper.js.map +0 -1
  86. package/dist/clients/search.d.ts +0 -57
  87. package/dist/clients/search.d.ts.map +0 -1
  88. package/dist/clients/search.js +0 -223
  89. package/dist/clients/search.js.map +0 -1
  90. package/dist/config/index.d.ts +0 -78
  91. package/dist/config/index.d.ts.map +0 -1
  92. package/dist/config/index.js +0 -201
  93. package/dist/config/index.js.map +0 -1
  94. package/dist/config/loader.d.ts +0 -40
  95. package/dist/config/loader.d.ts.map +0 -1
  96. package/dist/config/loader.js +0 -322
  97. package/dist/config/loader.js.map +0 -1
  98. package/dist/config/types.d.ts +0 -81
  99. package/dist/config/types.d.ts.map +0 -1
  100. package/dist/config/types.js +0 -6
  101. package/dist/config/types.js.map +0 -1
  102. package/dist/config/yaml/tools.yaml +0 -146
  103. package/dist/index.d.ts +0 -7
  104. package/dist/index.d.ts.map +0 -1
  105. package/dist/schemas/deep-research.d.ts +0 -64
  106. package/dist/schemas/deep-research.d.ts.map +0 -1
  107. package/dist/schemas/deep-research.js +0 -224
  108. package/dist/schemas/deep-research.js.map +0 -1
  109. package/dist/schemas/scrape-links.d.ts +0 -32
  110. package/dist/schemas/scrape-links.d.ts.map +0 -1
  111. package/dist/schemas/scrape-links.js +0 -34
  112. package/dist/schemas/scrape-links.js.map +0 -1
  113. package/dist/schemas/web-search.d.ts +0 -22
  114. package/dist/schemas/web-search.d.ts.map +0 -1
  115. package/dist/schemas/web-search.js +0 -21
  116. package/dist/schemas/web-search.js.map +0 -1
  117. package/dist/services/file-attachment.d.ts +0 -30
  118. package/dist/services/file-attachment.d.ts.map +0 -1
  119. package/dist/services/file-attachment.js +0 -207
  120. package/dist/services/file-attachment.js.map +0 -1
  121. package/dist/services/llm-processor.d.ts +0 -29
  122. package/dist/services/llm-processor.d.ts.map +0 -1
  123. package/dist/services/llm-processor.js +0 -244
  124. package/dist/services/llm-processor.js.map +0 -1
  125. package/dist/services/markdown-cleaner.d.ts +0 -8
  126. package/dist/services/markdown-cleaner.d.ts.map +0 -1
  127. package/dist/services/markdown-cleaner.js +0 -74
  128. package/dist/services/markdown-cleaner.js.map +0 -1
  129. package/dist/tools/definitions.d.ts +0 -16
  130. package/dist/tools/definitions.d.ts.map +0 -1
  131. package/dist/tools/definitions.js +0 -17
  132. package/dist/tools/definitions.js.map +0 -1
  133. package/dist/tools/reddit.d.ts +0 -14
  134. package/dist/tools/reddit.d.ts.map +0 -1
  135. package/dist/tools/reddit.js +0 -265
  136. package/dist/tools/reddit.js.map +0 -1
  137. package/dist/tools/registry.d.ts +0 -71
  138. package/dist/tools/registry.d.ts.map +0 -1
  139. package/dist/tools/registry.js +0 -252
  140. package/dist/tools/registry.js.map +0 -1
  141. package/dist/tools/research.d.ts +0 -14
  142. package/dist/tools/research.d.ts.map +0 -1
  143. package/dist/tools/research.js +0 -196
  144. package/dist/tools/research.js.map +0 -1
  145. package/dist/tools/scrape.d.ts +0 -14
  146. package/dist/tools/scrape.d.ts.map +0 -1
  147. package/dist/tools/scrape.js +0 -234
  148. package/dist/tools/scrape.js.map +0 -1
  149. package/dist/tools/search.d.ts +0 -10
  150. package/dist/tools/search.d.ts.map +0 -1
  151. package/dist/tools/search.js +0 -158
  152. package/dist/tools/search.js.map +0 -1
  153. package/dist/tools/utils.d.ts +0 -105
  154. package/dist/tools/utils.d.ts.map +0 -1
  155. package/dist/tools/utils.js +0 -159
  156. package/dist/tools/utils.js.map +0 -1
  157. package/dist/utils/concurrency.d.ts +0 -28
  158. package/dist/utils/concurrency.d.ts.map +0 -1
  159. package/dist/utils/concurrency.js +0 -92
  160. package/dist/utils/concurrency.js.map +0 -1
  161. package/dist/utils/errors.d.ts +0 -95
  162. package/dist/utils/errors.d.ts.map +0 -1
  163. package/dist/utils/errors.js +0 -390
  164. package/dist/utils/errors.js.map +0 -1
  165. package/dist/utils/logger.d.ts +0 -39
  166. package/dist/utils/logger.d.ts.map +0 -1
  167. package/dist/utils/logger.js +0 -57
  168. package/dist/utils/logger.js.map +0 -1
  169. package/dist/utils/markdown-formatter.d.ts +0 -5
  170. package/dist/utils/markdown-formatter.d.ts.map +0 -1
  171. package/dist/utils/markdown-formatter.js +0 -15
  172. package/dist/utils/markdown-formatter.js.map +0 -1
  173. package/dist/utils/response.d.ts +0 -93
  174. package/dist/utils/response.d.ts.map +0 -1
  175. package/dist/utils/response.js +0 -170
  176. package/dist/utils/response.js.map +0 -1
  177. package/dist/utils/retry.d.ts +0 -43
  178. package/dist/utils/retry.d.ts.map +0 -1
  179. package/dist/utils/retry.js +0 -57
  180. package/dist/utils/retry.js.map +0 -1
  181. package/dist/utils/url-aggregator.d.ts +0 -90
  182. package/dist/utils/url-aggregator.d.ts.map +0 -1
  183. package/dist/utils/url-aggregator.js +0 -538
  184. package/dist/utils/url-aggregator.js.map +0 -1
  185. package/dist/version.d.ts +0 -29
  186. package/dist/version.d.ts.map +0 -1
  187. package/dist/version.js +0 -55
  188. package/dist/version.js.map +0 -1
@@ -0,0 +1,423 @@
1
+ import { getCapabilities, getMissingEnvMessage } from "../config/index.js";
2
+ import {
3
+ webSearchOutputSchema,
4
+ webSearchParamsSchema
5
+ } from "../schemas/web-search.js";
6
+ import { SearchClient } from "../clients/search.js";
7
+ import {
8
+ aggregateAndRank,
9
+ generateUnifiedOutput
10
+ } from "../utils/url-aggregator.js";
11
+ import {
12
+ createLLMProcessor,
13
+ classifySearchResults,
14
+ suggestRefineQueriesForRawMode
15
+ } from "../services/llm-processor.js";
16
+ import { classifyError } from "../utils/errors.js";
17
+ import { classifySourceByUrl } from "../utils/source-type.js";
18
+ import {
19
+ mcpLog,
20
+ formatError,
21
+ formatDuration
22
+ } from "./utils.js";
23
+ import {
24
+ createToolReporter,
25
+ NOOP_REPORTER,
26
+ toolFailure,
27
+ toolSuccess,
28
+ toToolResponse
29
+ } from "./mcp-helpers.js";
30
+ import { requireBootstrap } from "../utils/bootstrap-guard.js";
31
+ import { redditKeywordGuard } from "../utils/reddit-keyword-guard.js";
32
+ import { sanitizeSuggestion } from "../utils/sanitize.js";
33
+ const REDDIT_POST_PERMALINK = /\/r\/[^/]+\/comments\/[a-z0-9]+\//i;
34
+ const REDDIT_HOST = /(?:^|\.)reddit\.com$/i;
35
+ function decorateQueriesForScope(queries, scope) {
36
+ if (scope === "web") return queries;
37
+ const reddited = queries.map(
38
+ (q) => /\bsite:reddit\.com\b/i.test(q) ? q : `${q} site:reddit.com`
39
+ );
40
+ return scope === "reddit" ? reddited : [...queries, ...reddited];
41
+ }
42
+ async function executeSearches(queries) {
43
+ const client = new SearchClient();
44
+ return client.searchMultiple(queries);
45
+ }
46
+ function filterScopedSearches(response, scope) {
47
+ if (scope === "web") return response;
48
+ const filtered = response.searches.map((search) => ({
49
+ ...search,
50
+ results: search.results.filter((r) => {
51
+ let host;
52
+ try {
53
+ host = new URL(r.link).hostname;
54
+ } catch {
55
+ return true;
56
+ }
57
+ if (!REDDIT_HOST.test(host)) return scope !== "reddit";
58
+ return REDDIT_POST_PERMALINK.test(r.link);
59
+ })
60
+ }));
61
+ return { ...response, searches: filtered };
62
+ }
63
+ function processResults(response) {
64
+ const aggregation = aggregateAndRank(response.searches, 5);
65
+ return { aggregation };
66
+ }
67
+ function buildRawOutput(queries, aggregation, searches, verbose = false) {
68
+ return generateUnifiedOutput(
69
+ aggregation.rankedUrls,
70
+ queries,
71
+ searches,
72
+ aggregation.totalUniqueUrls,
73
+ aggregation.frequencyThreshold,
74
+ aggregation.thresholdNote,
75
+ verbose
76
+ );
77
+ }
78
+ function buildSignalsSection(aggregation, searches, totalQueries) {
79
+ const coverageCount = searches.filter((search) => search.results.length >= 3).length;
80
+ const lowYield = searches.filter((search) => search.results.length <= 1).map((search) => `"${search.query}"`);
81
+ const consensusCount = aggregation.rankedUrls.filter((url) => url.isConsensus).length;
82
+ const lines = [
83
+ "**Signals**",
84
+ `- Coverage: ${coverageCount}/${totalQueries} queries returned \u22653 results`,
85
+ `- Consensus URLs: ${consensusCount}`
86
+ ];
87
+ if (lowYield.length > 0) {
88
+ lines.push(`- Low-yield: ${lowYield.join(", ")}`);
89
+ }
90
+ return lines.join("\n");
91
+ }
92
+ function buildSuggestedFollowUpsSection(refineQueries) {
93
+ if (!refineQueries || refineQueries.length === 0) {
94
+ return "";
95
+ }
96
+ const lines = ["## Suggested follow-up searches", ""];
97
+ for (const item of refineQueries) {
98
+ const query = sanitizeSuggestion(item.query ?? "");
99
+ if (!query) continue;
100
+ const rationale = sanitizeSuggestion(item.rationale ?? "");
101
+ const gapTag = typeof item.gap_id === "number" ? ` _(closes gap [${item.gap_id}])_` : item.gap_description ? ` _(${sanitizeSuggestion(item.gap_description)})_` : "";
102
+ lines.push(
103
+ rationale ? `- ${query} \u2014 ${rationale}${gapTag}` : `- ${query}${gapTag}`
104
+ );
105
+ }
106
+ return lines.length === 2 ? "" : lines.join("\n");
107
+ }
108
+ function appendSignalsAndFollowUps(markdown, signalsSection, refineQueries, options = {}) {
109
+ const includeSignals = options.includeSignals ?? false;
110
+ const sections = [markdown];
111
+ if (includeSignals && signalsSection) {
112
+ sections.push("", "---", signalsSection);
113
+ }
114
+ const followUps = buildSuggestedFollowUpsSection(refineQueries);
115
+ if (followUps) {
116
+ sections.push("", followUps);
117
+ }
118
+ return sections.join("\n");
119
+ }
120
+ function buildClassifiedOutput(classification, aggregation, extract, searches, totalQueries, verbose = false) {
121
+ const rankedUrls = aggregation.rankedUrls;
122
+ const entryByRank = new Map(classification.results.map((r) => [r.rank, r]));
123
+ const tiers = {
124
+ high: [],
125
+ maybe: [],
126
+ other: []
127
+ };
128
+ for (const url of rankedUrls) {
129
+ const entry = entryByRank.get(url.rank);
130
+ const tier = entry?.tier;
131
+ if (tier === "HIGHLY_RELEVANT") {
132
+ tiers.high.push(url);
133
+ } else if (tier === "MAYBE_RELEVANT") {
134
+ tiers.maybe.push(url);
135
+ } else {
136
+ tiers.other.push(url);
137
+ }
138
+ }
139
+ const lines = [];
140
+ lines.push(`## ${classification.title}`);
141
+ lines.push(`> Looking for: ${extract}`);
142
+ lines.push(`> ${totalQueries} queries \u2192 ${rankedUrls.length} URLs \u2192 ${tiers.high.length} highly relevant, ${tiers.maybe.length} possibly relevant`);
143
+ if (classification.confidence) {
144
+ const confReason = classification.confidence_reason ? ` \u2014 ${classification.confidence_reason}` : "";
145
+ lines.push(`> Confidence: \`${classification.confidence}\`${confReason}`);
146
+ }
147
+ lines.push("");
148
+ lines.push(`**Summary:** ${classification.synthesis}`);
149
+ lines.push("");
150
+ const renderRichRow = (url) => {
151
+ const entry = entryByRank.get(url.rank);
152
+ const coveragePct = Math.round(url.coverageRatio * 100);
153
+ const seenIn = `${url.frequency}/${totalQueries} (${coveragePct}%)`;
154
+ const sourceType = entry?.source_type ? `\`${entry.source_type}\`` : "\u2014";
155
+ const reason = entry?.reason ? entry.reason.replace(/\|/g, "\\|") : "\u2014";
156
+ return `| ${url.rank} | [${url.title}](${url.url}) | ${sourceType} | ${seenIn} | ${reason} |`;
157
+ };
158
+ if (tiers.high.length > 0) {
159
+ lines.push(`### Highly Relevant (${tiers.high.length})`);
160
+ lines.push("| # | URL | Source | Seen in | Why |");
161
+ lines.push("|---|-----|--------|---------|-----|");
162
+ for (const url of tiers.high) lines.push(renderRichRow(url));
163
+ lines.push("");
164
+ }
165
+ if (tiers.maybe.length > 0) {
166
+ lines.push(`### Maybe Relevant (${tiers.maybe.length})`);
167
+ lines.push("| # | URL | Source | Seen in | Why |");
168
+ lines.push("|---|-----|--------|---------|-----|");
169
+ for (const url of tiers.maybe) lines.push(renderRichRow(url));
170
+ lines.push("");
171
+ }
172
+ if (tiers.other.length > 0) {
173
+ lines.push(`### Other Results (${tiers.other.length})`);
174
+ lines.push("| # | URL | Source | Score | Queries |");
175
+ lines.push("|---|-----|--------|-------|---------|");
176
+ for (const url of tiers.other) {
177
+ const entry = entryByRank.get(url.rank);
178
+ const queryList = url.queries.map((q) => `"${q}"`).join(", ");
179
+ const sourceType = entry?.source_type ? `\`${entry.source_type}\`` : "\u2014";
180
+ let domain;
181
+ try {
182
+ domain = new URL(url.url).hostname.replace(/^www\./, "");
183
+ } catch {
184
+ domain = url.url;
185
+ }
186
+ lines.push(`| ${url.rank} | ${domain} | ${sourceType} | ${url.score.toFixed(1)} | ${queryList} |`);
187
+ }
188
+ lines.push("");
189
+ }
190
+ if (verbose) {
191
+ lines.push(buildSignalsSection(aggregation, searches, totalQueries));
192
+ }
193
+ if (classification.gaps && classification.gaps.length > 0) {
194
+ lines.push("");
195
+ lines.push("## Gaps");
196
+ for (const gap of classification.gaps) {
197
+ lines.push(`- **[${gap.id}]** ${gap.description}`);
198
+ }
199
+ }
200
+ const followUps = buildSuggestedFollowUpsSection(classification.refine_queries);
201
+ if (followUps) {
202
+ lines.push("");
203
+ lines.push(followUps);
204
+ }
205
+ return lines.join("\n");
206
+ }
207
+ function buildMetadata(aggregation, executionTime, totalQueries, searches, llmClassified, scope, llmError) {
208
+ const coverageSummary = searches.map((s) => {
209
+ let topDomain;
210
+ const topResult = s.results[0];
211
+ if (topResult) {
212
+ try {
213
+ topDomain = new URL(topResult.link).hostname.replace(/^www\./, "");
214
+ } catch {
215
+ }
216
+ }
217
+ return { query: s.query, result_count: s.results.length, top_url: topDomain };
218
+ });
219
+ const lowYieldQueries = searches.filter((s) => s.results.length <= 1).map((s) => s.query);
220
+ return {
221
+ total_items: totalQueries,
222
+ successful: aggregation.rankedUrls.length,
223
+ failed: totalQueries - searches.filter((s) => s.results.length > 0).length,
224
+ execution_time_ms: executionTime,
225
+ llm_classified: llmClassified,
226
+ scope,
227
+ ...llmError ? { llm_error: llmError } : {},
228
+ coverage_summary: coverageSummary,
229
+ ...lowYieldQueries.length > 0 ? { low_yield_queries: lowYieldQueries } : {}
230
+ };
231
+ }
232
+ function buildStructuredResults(aggregation, llmTagsByRank) {
233
+ return aggregation.rankedUrls.map((row) => {
234
+ const llmTag = llmTagsByRank?.get(row.rank);
235
+ const heuristic = classifySourceByUrl(row.url);
236
+ return {
237
+ rank: row.rank,
238
+ url: row.url,
239
+ title: row.title,
240
+ snippet: row.snippet,
241
+ source_type: llmTag ?? heuristic,
242
+ score: Number(row.score.toFixed(2)),
243
+ seen_in: row.frequency,
244
+ best_position: row.bestPosition
245
+ };
246
+ });
247
+ }
248
+ function buildWebSearchError(error, params, startTime) {
249
+ const structuredError = classifyError(error);
250
+ const executionTime = Date.now() - startTime;
251
+ mcpLog("error", `web-search: ${structuredError.message}`, "search");
252
+ const errorContent = formatError({
253
+ code: structuredError.code,
254
+ message: structuredError.message,
255
+ retryable: structuredError.retryable,
256
+ toolName: "web-search",
257
+ howToFix: ["Verify SERPER_API_KEY is set correctly"],
258
+ alternatives: [
259
+ 'web-search(queries=["topic recommendations"], extract="...", scope: "reddit") \u2014 Reddit-only post permalinks via the same backend',
260
+ 'scrape-links(urls=[...], extract="...") \u2014 if you have URLs from prior steps, scrape them now'
261
+ ]
262
+ });
263
+ return toolFailure(
264
+ `${errorContent}
265
+
266
+ Execution time: ${formatDuration(executionTime)}
267
+ Queries: ${params.queries.length}`
268
+ );
269
+ }
270
+ async function handleWebSearch(params, reporter = NOOP_REPORTER) {
271
+ const startTime = Date.now();
272
+ try {
273
+ const effectiveQueries = decorateQueriesForScope(params.queries, params.scope);
274
+ if (params.scope !== "web") {
275
+ mcpLog("info", `Searching scope=${params.scope}: ${params.queries.length} input queries \u2192 ${effectiveQueries.length} dispatched`, "search");
276
+ } else {
277
+ mcpLog("info", `Searching for ${params.queries.length} query/queries`, "search");
278
+ }
279
+ await reporter.log("info", `Searching for ${effectiveQueries.length} query/queries (scope=${params.scope})`);
280
+ await reporter.progress(15, 100, "Submitting search queries");
281
+ const rawResponse = await executeSearches(effectiveQueries);
282
+ const response = filterScopedSearches(rawResponse, params.scope);
283
+ await reporter.progress(50, 100, "Collected search results");
284
+ const { aggregation } = processResults(response);
285
+ await reporter.log(
286
+ "info",
287
+ `Collected ${aggregation.totalUniqueUrls} unique URLs across ${response.totalQueries} queries`
288
+ );
289
+ const useRaw = params.raw;
290
+ const llmProcessor = createLLMProcessor();
291
+ let markdown;
292
+ let llmClassified = false;
293
+ let llmError;
294
+ if (useRaw || !llmProcessor) {
295
+ if (!useRaw && !llmProcessor) {
296
+ llmError = "LLM unavailable (LLM_EXTRACTION_API_KEY not set). Falling back to raw output.";
297
+ mcpLog("warning", llmError, "search");
298
+ await reporter.log("warning", "llm_classifier_unreachable: planner not configured; raw ranked list returned");
299
+ }
300
+ let rawRefineQueries;
301
+ if (useRaw && llmProcessor) {
302
+ const refineResult = await suggestRefineQueriesForRawMode(
303
+ aggregation.rankedUrls,
304
+ params.extract,
305
+ params.queries,
306
+ llmProcessor
307
+ );
308
+ rawRefineQueries = refineResult.result;
309
+ }
310
+ markdown = appendSignalsAndFollowUps(
311
+ buildRawOutput(params.queries, aggregation, response.searches, params.verbose),
312
+ buildSignalsSection(aggregation, response.searches, response.totalQueries),
313
+ rawRefineQueries,
314
+ { includeSignals: params.verbose }
315
+ );
316
+ await reporter.progress(80, 100, "Ranking search results");
317
+ } else {
318
+ await reporter.progress(65, 100, "Classifying results by relevance");
319
+ const classification = await classifySearchResults(
320
+ aggregation.rankedUrls,
321
+ params.extract,
322
+ response.totalQueries,
323
+ llmProcessor,
324
+ params.queries
325
+ );
326
+ if (classification.result) {
327
+ markdown = buildClassifiedOutput(
328
+ classification.result,
329
+ aggregation,
330
+ params.extract,
331
+ response.searches,
332
+ response.totalQueries,
333
+ params.verbose
334
+ );
335
+ llmClassified = true;
336
+ await reporter.progress(85, 100, "Formatted classified results");
337
+ } else {
338
+ llmError = classification.error ?? "Unknown classification error";
339
+ mcpLog("warning", `Classification failed, falling back to raw: ${llmError}`, "search");
340
+ await reporter.log("warning", `llm_classifier_unreachable: ${llmError}`);
341
+ markdown = appendSignalsAndFollowUps(
342
+ buildRawOutput(params.queries, aggregation, response.searches, params.verbose),
343
+ buildSignalsSection(aggregation, response.searches, response.totalQueries),
344
+ void 0,
345
+ { includeSignals: params.verbose }
346
+ );
347
+ await reporter.progress(85, 100, "Classification failed, using raw output");
348
+ }
349
+ }
350
+ const executionTime = Date.now() - startTime;
351
+ const metadata = buildMetadata(
352
+ aggregation,
353
+ executionTime,
354
+ response.totalQueries,
355
+ response.searches,
356
+ llmClassified,
357
+ params.scope,
358
+ llmError
359
+ );
360
+ const llmTagsByRank = /* @__PURE__ */ new Map();
361
+ const results = buildStructuredResults(aggregation, llmTagsByRank);
362
+ mcpLog("info", `Search completed: ${aggregation.rankedUrls.length} URLs, classified=${llmClassified}`, "search");
363
+ await reporter.log("info", `Search completed with ${aggregation.rankedUrls.length} URLs (classified: ${llmClassified})`);
364
+ const footer = `
365
+ ---
366
+ *${formatDuration(executionTime)} | ${aggregation.totalUniqueUrls} unique URLs${llmClassified ? " | LLM classified" : ""}*`;
367
+ const fullMarkdown = markdown + footer;
368
+ return toolSuccess(fullMarkdown, { content: fullMarkdown, results, metadata });
369
+ } catch (error) {
370
+ return buildWebSearchError(error, params, startTime);
371
+ }
372
+ }
373
+ function registerWebSearchTool(server) {
374
+ server.tool(
375
+ {
376
+ name: "web-search",
377
+ title: "Web Search",
378
+ description: "Fan out many Google searches in parallel (no hard cap), aggregate and deduplicate results, then classify each URL against your extract goal. Returns a tiered Markdown report: HIGHLY_RELEVANT / MAYBE_RELEVANT / OTHER with source_type and a per-result reason; plus a grounded synthesis with rank citations, a domain-independence confidence, a `## Gaps` list of what the current results do not answer, and `## Suggested follow-up searches` tied to gap ids (non-paraphrases of your prior queries). Think of `queries` as concept groups \u2014 diverse facets, not paraphrases. Set `raw=true` to skip classification and get an unclassified ranked list.",
379
+ schema: webSearchParamsSchema,
380
+ outputSchema: webSearchOutputSchema,
381
+ annotations: {
382
+ readOnlyHint: true,
383
+ idempotentHint: true,
384
+ destructiveHint: false,
385
+ openWorldHint: true,
386
+ // Non-standard precondition hint. mcp-use's ToolAnnotations type
387
+ // does not expose `experimental` natively (the SDK schema uses
388
+ // $strip) but the runtime forwards the whole annotations object
389
+ // verbatim. Cast keeps TS happy. See:
390
+ // docs/code-review/context/04-session-and-workflow-state.md
391
+ // mcp-revisions/contract-fixes/03-precondition-annotation-on-gated-tools.md
392
+ ...{ experimental: { requires: ["start-research"] } }
393
+ },
394
+ _meta: { requires: ["start-research"] }
395
+ },
396
+ async (args, ctx) => {
397
+ if (!getCapabilities().search) {
398
+ return toToolResponse(toolFailure(getMissingEnvMessage("search")));
399
+ }
400
+ const guard = await requireBootstrap(ctx);
401
+ if (guard) {
402
+ return guard;
403
+ }
404
+ if (args.scope === "web") {
405
+ const redditGuard = await redditKeywordGuard(ctx, args.queries);
406
+ if (redditGuard) {
407
+ return redditGuard;
408
+ }
409
+ }
410
+ const reporter = createToolReporter(ctx, "web-search");
411
+ const result = await handleWebSearch(args, reporter);
412
+ await reporter.progress(100, 100, result.isError ? "Search failed" : "Search complete");
413
+ return toToolResponse(result);
414
+ }
415
+ );
416
+ }
417
+ export {
418
+ appendSignalsAndFollowUps,
419
+ buildSuggestedFollowUpsSection,
420
+ handleWebSearch,
421
+ registerWebSearchTool
422
+ };
423
+ //# sourceMappingURL=search.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/tools/search.ts"],
4
+ "sourcesContent": ["/**\n * Web Search Tool Handler\n * NEVER throws - always returns structured response for graceful degradation\n */\n\nimport type { MCPServer } from 'mcp-use/server';\n\nimport { getCapabilities, getMissingEnvMessage } from '../config/index.js';\nimport {\n webSearchOutputSchema,\n webSearchParamsSchema,\n type WebSearchParams,\n type WebSearchOutput,\n} from '../schemas/web-search.js';\nimport { SearchClient } from '../clients/search.js';\nimport {\n aggregateAndRank,\n generateUnifiedOutput,\n} from '../utils/url-aggregator.js';\nimport {\n createLLMProcessor,\n classifySearchResults,\n suggestRefineQueriesForRawMode,\n type ClassificationResult,\n type RefineQuerySuggestion,\n} from '../services/llm-processor.js';\nimport { classifyError } from '../utils/errors.js';\nimport { classifySourceByUrl } from '../utils/source-type.js';\nimport {\n mcpLog,\n formatError,\n formatDuration,\n} from './utils.js';\nimport {\n createToolReporter,\n NOOP_REPORTER,\n toolFailure,\n toolSuccess,\n toToolResponse,\n type ToolExecutionResult,\n type ToolReporter,\n} from './mcp-helpers.js';\nimport { requireBootstrap } from '../utils/bootstrap-guard.js';\nimport { redditKeywordGuard } from '../utils/reddit-keyword-guard.js';\nimport { sanitizeSuggestion } from '../utils/sanitize.js';\n\n// --- Internal types ---\n\ninterface SearchAggregation {\n readonly rankedUrls: ReturnType<typeof aggregateAndRank>['rankedUrls'];\n readonly totalUniqueUrls: number;\n readonly frequencyThreshold: number;\n readonly thresholdNote?: string;\n}\n\ninterface SearchResponse {\n searches: Parameters<typeof aggregateAndRank>[0];\n totalQueries: number;\n}\n\n// --- Helpers ---\n\n/** Reddit post permalink: /r/{sub}/comments/{id}/ \u2014 drops subreddit\n * homepages, /rising, /new, /top, etc. so only post URLs reach the agent.\n * See mcp-revisions/tool-surface/02-extend-web-search-with-reddit-scope.md. */\nconst REDDIT_POST_PERMALINK = /\\/r\\/[^/]+\\/comments\\/[a-z0-9]+\\//i;\nconst REDDIT_HOST = /(?:^|\\.)reddit\\.com$/i;\n\nfunction decorateQueriesForScope(queries: string[], scope: 'web' | 'reddit' | 'both'): string[] {\n if (scope === 'web') return queries;\n const reddited = queries.map((q) =>\n /\\bsite:reddit\\.com\\b/i.test(q) ? q : `${q} site:reddit.com`,\n );\n return scope === 'reddit' ? reddited : [...queries, ...reddited];\n}\n\nasync function executeSearches(queries: string[]): Promise<SearchResponse> {\n const client = new SearchClient();\n return client.searchMultiple(queries);\n}\n\nfunction filterScopedSearches(\n response: SearchResponse,\n scope: 'web' | 'reddit' | 'both',\n): SearchResponse {\n if (scope === 'web') return response;\n const filtered = response.searches.map((search) => ({\n ...search,\n results: search.results.filter((r) => {\n let host: string;\n try { host = new URL(r.link).hostname; } catch { return true; }\n // Non-reddit URLs pass through; reddit URLs must be post permalinks.\n if (!REDDIT_HOST.test(host)) return scope !== 'reddit';\n return REDDIT_POST_PERMALINK.test(r.link);\n }),\n }));\n return { ...response, searches: filtered };\n}\n\nfunction processResults(response: SearchResponse): {\n aggregation: SearchAggregation;\n} {\n const aggregation = aggregateAndRank(response.searches, 5);\n return { aggregation };\n}\n\n// --- Raw output (traditional unified ranked list) ---\n\nfunction buildRawOutput(\n queries: string[],\n aggregation: SearchAggregation,\n searches: SearchResponse['searches'],\n verbose: boolean = false,\n): string {\n return generateUnifiedOutput(\n aggregation.rankedUrls, queries, searches,\n aggregation.totalUniqueUrls,\n aggregation.frequencyThreshold, aggregation.thresholdNote,\n verbose,\n );\n}\n\nfunction buildSignalsSection(\n aggregation: SearchAggregation,\n searches: SearchResponse['searches'],\n totalQueries: number,\n): string {\n const coverageCount = searches.filter((search) => search.results.length >= 3).length;\n const lowYield = searches\n .filter((search) => search.results.length <= 1)\n .map((search) => `\"${search.query}\"`);\n const consensusCount = aggregation.rankedUrls.filter((url) => url.isConsensus).length;\n\n const lines = [\n '**Signals**',\n `- Coverage: ${coverageCount}/${totalQueries} queries returned \u22653 results`,\n `- Consensus URLs: ${consensusCount}`,\n ];\n\n if (lowYield.length > 0) {\n lines.push(`- Low-yield: ${lowYield.join(', ')}`);\n }\n\n return lines.join('\\n');\n}\n\nexport function buildSuggestedFollowUpsSection(\n refineQueries: Array<{ query: string; rationale?: string; gap_id?: number; gap_description?: string }> | undefined,\n): string {\n if (!refineQueries || refineQueries.length === 0) {\n return '';\n }\n\n const lines = ['## Suggested follow-up searches', ''];\n\n for (const item of refineQueries) {\n const query = sanitizeSuggestion(item.query ?? '');\n if (!query) continue;\n const rationale = sanitizeSuggestion(item.rationale ?? '');\n const gapTag = typeof item.gap_id === 'number'\n ? ` _(closes gap [${item.gap_id}])_`\n : item.gap_description\n ? ` _(${sanitizeSuggestion(item.gap_description)})_`\n : '';\n lines.push(rationale\n ? `- ${query} \u2014 ${rationale}${gapTag}`\n : `- ${query}${gapTag}`,\n );\n }\n\n return lines.length === 2 ? '' : lines.join('\\n');\n}\n\nexport function appendSignalsAndFollowUps(\n markdown: string,\n signalsSection: string,\n refineQueries: RefineQuerySuggestion[] | undefined,\n options: { includeSignals?: boolean } = {},\n): string {\n const includeSignals = options.includeSignals ?? false;\n const sections = [markdown];\n if (includeSignals && signalsSection) {\n sections.push('', '---', signalsSection);\n }\n const followUps = buildSuggestedFollowUpsSection(refineQueries);\n if (followUps) {\n sections.push('', followUps);\n }\n return sections.join('\\n');\n}\n\n// --- Classified output (3-tier LLM-classified table) ---\n\nfunction buildClassifiedOutput(\n classification: ClassificationResult,\n aggregation: SearchAggregation,\n extract: string,\n searches: SearchResponse['searches'],\n totalQueries: number,\n verbose: boolean = false,\n): string {\n const rankedUrls = aggregation.rankedUrls;\n\n // Build tier \u2192 entries mapping (keep url data alongside classifier metadata)\n const entryByRank = new Map(classification.results.map((r) => [r.rank, r]));\n\n const tiers = {\n high: [] as typeof rankedUrls,\n maybe: [] as typeof rankedUrls,\n other: [] as typeof rankedUrls,\n };\n\n for (const url of rankedUrls) {\n const entry = entryByRank.get(url.rank);\n const tier = entry?.tier;\n if (tier === 'HIGHLY_RELEVANT') {\n tiers.high.push(url);\n } else if (tier === 'MAYBE_RELEVANT') {\n tiers.maybe.push(url);\n } else {\n tiers.other.push(url);\n }\n }\n\n const lines: string[] = [];\n\n // Header with generated title, synthesis, and confidence\n lines.push(`## ${classification.title}`);\n lines.push(`> Looking for: ${extract}`);\n lines.push(`> ${totalQueries} queries \u2192 ${rankedUrls.length} URLs \u2192 ${tiers.high.length} highly relevant, ${tiers.maybe.length} possibly relevant`);\n if (classification.confidence) {\n const confReason = classification.confidence_reason ? ` \u2014 ${classification.confidence_reason}` : '';\n lines.push(`> Confidence: \\`${classification.confidence}\\`${confReason}`);\n }\n lines.push('');\n lines.push(`**Summary:** ${classification.synthesis}`);\n lines.push('');\n\n // Helper: render one row with optional source_type + reason\n const renderRichRow = (url: typeof rankedUrls[number]): string => {\n const entry = entryByRank.get(url.rank);\n const coveragePct = Math.round(url.coverageRatio * 100);\n const seenIn = `${url.frequency}/${totalQueries} (${coveragePct}%)`;\n const sourceType = entry?.source_type ? `\\`${entry.source_type}\\`` : '\u2014';\n const reason = entry?.reason ? entry.reason.replace(/\\|/g, '\\\\|') : '\u2014';\n return `| ${url.rank} | [${url.title}](${url.url}) | ${sourceType} | ${seenIn} | ${reason} |`;\n };\n\n // Highly Relevant tier\n if (tiers.high.length > 0) {\n lines.push(`### Highly Relevant (${tiers.high.length})`);\n lines.push('| # | URL | Source | Seen in | Why |');\n lines.push('|---|-----|--------|---------|-----|');\n for (const url of tiers.high) lines.push(renderRichRow(url));\n lines.push('');\n }\n\n // Maybe Relevant tier\n if (tiers.maybe.length > 0) {\n lines.push(`### Maybe Relevant (${tiers.maybe.length})`);\n lines.push('| # | URL | Source | Seen in | Why |');\n lines.push('|---|-----|--------|---------|-----|');\n for (const url of tiers.maybe) lines.push(renderRichRow(url));\n lines.push('');\n }\n\n // Other tier \u2014 with query attribution\n if (tiers.other.length > 0) {\n lines.push(`### Other Results (${tiers.other.length})`);\n lines.push('| # | URL | Source | Score | Queries |');\n lines.push('|---|-----|--------|-------|---------|');\n for (const url of tiers.other) {\n const entry = entryByRank.get(url.rank);\n const queryList = url.queries.map((q) => `\"${q}\"`).join(', ');\n const sourceType = entry?.source_type ? `\\`${entry.source_type}\\`` : '\u2014';\n let domain: string;\n try {\n domain = new URL(url.url).hostname.replace(/^www\\./, '');\n } catch {\n domain = url.url;\n }\n lines.push(`| ${url.rank} | ${domain} | ${sourceType} | ${url.score.toFixed(1)} | ${queryList} |`);\n }\n lines.push('');\n }\n\n // Signals block is gated behind verbose \u2014 it duplicates info already\n // present in the per-row metadata for callers who care.\n // See: docs/code-review/context/05-output-formatting-patterns.md.\n if (verbose) {\n lines.push(buildSignalsSection(aggregation, searches, totalQueries));\n }\n\n // Gaps section \u2014 what the current results don't answer\n if (classification.gaps && classification.gaps.length > 0) {\n lines.push('');\n lines.push('## Gaps');\n for (const gap of classification.gaps) {\n lines.push(`- **[${gap.id}]** ${gap.description}`);\n }\n }\n\n const followUps = buildSuggestedFollowUpsSection(classification.refine_queries);\n if (followUps) {\n lines.push('');\n lines.push(followUps);\n }\n\n return lines.join('\\n');\n}\n\n// --- Metadata builder ---\n\nfunction buildMetadata(\n aggregation: SearchAggregation,\n executionTime: number,\n totalQueries: number,\n searches: SearchResponse['searches'],\n llmClassified: boolean,\n scope: 'web' | 'reddit' | 'both',\n llmError?: string,\n) {\n const coverageSummary = searches.map(s => {\n let topDomain: string | undefined;\n const topResult = s.results[0];\n if (topResult) {\n try { topDomain = new URL(topResult.link).hostname.replace(/^www\\./, ''); } catch { /* ignore */ }\n }\n return { query: s.query, result_count: s.results.length, top_url: topDomain };\n });\n const lowYieldQueries = searches\n .filter(s => s.results.length <= 1)\n .map(s => s.query);\n\n return {\n total_items: totalQueries,\n successful: aggregation.rankedUrls.length,\n failed: totalQueries - searches.filter(s => s.results.length > 0).length,\n execution_time_ms: executionTime,\n llm_classified: llmClassified,\n scope,\n ...(llmError ? { llm_error: llmError } : {}),\n coverage_summary: coverageSummary,\n ...(lowYieldQueries.length > 0 ? { low_yield_queries: lowYieldQueries } : {}),\n };\n}\n\nfunction buildStructuredResults(\n aggregation: SearchAggregation,\n llmTagsByRank?: Map<number, string>,\n): Array<{\n rank: number;\n url: string;\n title: string;\n snippet: string;\n source_type: 'reddit' | 'github' | 'docs' | 'blog' | 'paper' | 'qa' | 'cve' | 'news' | 'video' | 'web';\n score: number;\n seen_in: number;\n best_position: number;\n}> {\n return aggregation.rankedUrls.map((row) => {\n // LLM tag wins when present; heuristic is the always-on fallback. See:\n // mcp-revisions/output-shaping/06-source-type-tagging-without-llm.md.\n const llmTag = llmTagsByRank?.get(row.rank);\n const heuristic = classifySourceByUrl(row.url);\n return {\n rank: row.rank,\n url: row.url,\n title: row.title,\n snippet: row.snippet,\n source_type: ((llmTag as typeof heuristic) ?? heuristic),\n score: Number(row.score.toFixed(2)),\n seen_in: row.frequency,\n best_position: row.bestPosition,\n };\n });\n}\n\n// --- Error builder ---\n\nfunction buildWebSearchError(\n error: unknown,\n params: WebSearchParams,\n startTime: number,\n): ToolExecutionResult<WebSearchOutput> {\n const structuredError = classifyError(error);\n const executionTime = Date.now() - startTime;\n\n mcpLog('error', `web-search: ${structuredError.message}`, 'search');\n\n const errorContent = formatError({\n code: structuredError.code,\n message: structuredError.message,\n retryable: structuredError.retryable,\n toolName: 'web-search',\n howToFix: ['Verify SERPER_API_KEY is set correctly'],\n alternatives: [\n 'web-search(queries=[\"topic recommendations\"], extract=\"...\", scope: \"reddit\") \u2014 Reddit-only post permalinks via the same backend',\n 'scrape-links(urls=[...], extract=\"...\") \u2014 if you have URLs from prior steps, scrape them now',\n ],\n });\n\n return toolFailure(\n `${errorContent}\\n\\nExecution time: ${formatDuration(executionTime)}\\nQueries: ${params.queries.length}`,\n );\n}\n\n// --- Main handler ---\n\nexport async function handleWebSearch(\n params: WebSearchParams,\n reporter: ToolReporter = NOOP_REPORTER,\n): Promise<ToolExecutionResult<WebSearchOutput>> {\n const startTime = Date.now();\n\n try {\n const effectiveQueries = decorateQueriesForScope(params.queries, params.scope);\n if (params.scope !== 'web') {\n mcpLog('info', `Searching scope=${params.scope}: ${params.queries.length} input queries \u2192 ${effectiveQueries.length} dispatched`, 'search');\n } else {\n mcpLog('info', `Searching for ${params.queries.length} query/queries`, 'search');\n }\n await reporter.log('info', `Searching for ${effectiveQueries.length} query/queries (scope=${params.scope})`);\n await reporter.progress(15, 100, 'Submitting search queries');\n\n const rawResponse = await executeSearches(effectiveQueries);\n const response = filterScopedSearches(rawResponse, params.scope);\n await reporter.progress(50, 100, 'Collected search results');\n\n const { aggregation } = processResults(response);\n await reporter.log(\n 'info',\n `Collected ${aggregation.totalUniqueUrls} unique URLs across ${response.totalQueries} queries`,\n );\n\n // Decide: raw output or LLM classification\n const useRaw = params.raw;\n const llmProcessor = createLLMProcessor();\n\n let markdown: string;\n let llmClassified = false;\n let llmError: string | undefined;\n\n if (useRaw || !llmProcessor) {\n // Raw path: traditional unified ranked list\n if (!useRaw && !llmProcessor) {\n llmError = 'LLM unavailable (LLM_EXTRACTION_API_KEY not set). Falling back to raw output.';\n mcpLog('warning', llmError, 'search');\n // mcp-revisions/llm-degradation/01: surface degraded mode to the client.\n await reporter.log('warning', 'llm_classifier_unreachable: planner not configured; raw ranked list returned');\n }\n let rawRefineQueries: RefineQuerySuggestion[] | undefined;\n if (useRaw && llmProcessor) {\n const refineResult = await suggestRefineQueriesForRawMode(\n aggregation.rankedUrls,\n params.extract,\n params.queries,\n llmProcessor,\n );\n rawRefineQueries = refineResult.result;\n }\n markdown = appendSignalsAndFollowUps(\n buildRawOutput(params.queries, aggregation, response.searches, params.verbose),\n buildSignalsSection(aggregation, response.searches, response.totalQueries),\n rawRefineQueries,\n { includeSignals: params.verbose },\n );\n await reporter.progress(80, 100, 'Ranking search results');\n } else {\n // LLM classification path\n await reporter.progress(65, 100, 'Classifying results by relevance');\n const classification = await classifySearchResults(\n aggregation.rankedUrls,\n params.extract,\n response.totalQueries,\n llmProcessor,\n params.queries,\n );\n\n if (classification.result) {\n markdown = buildClassifiedOutput(\n classification.result, aggregation, params.extract, response.searches, response.totalQueries, params.verbose,\n );\n llmClassified = true;\n await reporter.progress(85, 100, 'Formatted classified results');\n } else {\n // Classification failed \u2014 fall back to raw\n llmError = classification.error ?? 'Unknown classification error';\n mcpLog('warning', `Classification failed, falling back to raw: ${llmError}`, 'search');\n // mcp-revisions/llm-degradation/01: surface degraded mode to the client.\n await reporter.log('warning', `llm_classifier_unreachable: ${llmError}`);\n markdown = appendSignalsAndFollowUps(\n buildRawOutput(params.queries, aggregation, response.searches, params.verbose),\n buildSignalsSection(aggregation, response.searches, response.totalQueries),\n undefined,\n { includeSignals: params.verbose },\n );\n await reporter.progress(85, 100, 'Classification failed, using raw output');\n }\n }\n\n const executionTime = Date.now() - startTime;\n const metadata = buildMetadata(\n aggregation, executionTime, response.totalQueries, response.searches, llmClassified, params.scope, llmError,\n );\n\n // Build per-row structured results so capability-aware clients can\n // index into `structuredContent.results` rather than regex-scrape the\n // markdown table. The LLM tag wins when present; heuristic is the\n // always-on fallback.\n const llmTagsByRank = new Map<number, string>();\n // (When classification succeeds the source_type per-row is populated\n // inside buildClassifiedOutput via the entry.source_type field \u2014 but\n // we don't have a direct handle on it here without a refactor. The\n // heuristic alone covers the structuredContent shape correctly; the\n // LLM-tagged variant remains in the markdown body.)\n const results = buildStructuredResults(aggregation, llmTagsByRank);\n\n mcpLog('info', `Search completed: ${aggregation.rankedUrls.length} URLs, classified=${llmClassified}`, 'search');\n await reporter.log('info', `Search completed with ${aggregation.rankedUrls.length} URLs (classified: ${llmClassified})`);\n\n const footer = `\\n---\\n*${formatDuration(executionTime)} | ${aggregation.totalUniqueUrls} unique URLs${llmClassified ? ' | LLM classified' : ''}*`;\n const fullMarkdown = markdown + footer;\n\n return toolSuccess(fullMarkdown, { content: fullMarkdown, results, metadata });\n } catch (error) {\n return buildWebSearchError(error, params, startTime);\n }\n}\n\nexport function registerWebSearchTool(server: MCPServer): void {\n server.tool(\n {\n name: 'web-search',\n title: 'Web Search',\n description:\n 'Fan out many Google searches in parallel (no hard cap), aggregate and deduplicate results, then classify each URL against your extract goal. Returns a tiered Markdown report: HIGHLY_RELEVANT / MAYBE_RELEVANT / OTHER with source_type and a per-result reason; plus a grounded synthesis with rank citations, a domain-independence confidence, a `## Gaps` list of what the current results do not answer, and `## Suggested follow-up searches` tied to gap ids (non-paraphrases of your prior queries). Think of `queries` as concept groups \u2014 diverse facets, not paraphrases. Set `raw=true` to skip classification and get an unclassified ranked list.',\n schema: webSearchParamsSchema,\n outputSchema: webSearchOutputSchema,\n annotations: {\n readOnlyHint: true,\n idempotentHint: true,\n destructiveHint: false,\n openWorldHint: true,\n // Non-standard precondition hint. mcp-use's ToolAnnotations type\n // does not expose `experimental` natively (the SDK schema uses\n // $strip) but the runtime forwards the whole annotations object\n // verbatim. Cast keeps TS happy. See:\n // docs/code-review/context/04-session-and-workflow-state.md\n // mcp-revisions/contract-fixes/03-precondition-annotation-on-gated-tools.md\n ...({ experimental: { requires: ['start-research'] } } as Record<string, unknown>),\n },\n _meta: { requires: ['start-research'] },\n },\n async (args, ctx) => {\n if (!getCapabilities().search) {\n return toToolResponse(toolFailure(getMissingEnvMessage('search')));\n }\n\n const guard = await requireBootstrap(ctx);\n if (guard) {\n return guard;\n }\n\n // The keyword guard is only relevant for scope=web \u2014 when the caller\n // explicitly asked for the reddit scope, \"reddit\" in the query is\n // intentional context, not a tool-choice mistake.\n if (args.scope === 'web') {\n const redditGuard = await redditKeywordGuard(ctx, args.queries);\n if (redditGuard) {\n return redditGuard;\n }\n }\n\n const reporter = createToolReporter(ctx, 'web-search');\n const result = await handleWebSearch(args, reporter);\n\n await reporter.progress(100, 100, result.isError ? 'Search failed' : 'Search complete');\n return toToolResponse(result);\n },\n );\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }