mcp-researchpowerpack 7.0.11 → 7.0.13
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/index.js +4662 -21
- package/dist/index.js.map +4 -4
- package/dist/mcp-use.json +2 -2
- package/dist/src/clients/jina.js +202 -16
- package/dist/src/clients/jina.js.map +3 -3
- package/dist/src/clients/kernel.js +254 -7
- package/dist/src/clients/kernel.js.map +4 -4
- package/dist/src/clients/reddit.js +326 -23
- package/dist/src/clients/reddit.js.map +4 -4
- package/dist/src/clients/scraper.js +345 -22
- package/dist/src/clients/scraper.js.map +4 -4
- package/dist/src/clients/search.js +316 -20
- package/dist/src/clients/search.js.map +4 -4
- package/dist/src/config/index.js +39 -10
- package/dist/src/config/index.js.map +3 -3
- package/dist/src/effect/errors.js +130 -5
- package/dist/src/effect/errors.js.map +3 -3
- package/dist/src/effect/runtime.js +1893 -4
- package/dist/src/effect/runtime.js.map +4 -4
- package/dist/src/effect/services.js +2124 -22
- package/dist/src/effect/services.js.map +4 -4
- package/dist/src/schemas/scrape-links.js +6 -5
- package/dist/src/schemas/scrape-links.js.map +1 -1
- package/dist/src/schemas/start-research.js +2 -1
- package/dist/src/schemas/start-research.js.map +1 -1
- package/dist/src/schemas/web-search.js +9 -8
- package/dist/src/schemas/web-search.js.map +1 -1
- package/dist/src/services/llm-processor.js +406 -25
- package/dist/src/services/llm-processor.js.map +4 -4
- package/dist/src/services/markdown-cleaner.js +6 -5
- package/dist/src/services/markdown-cleaner.js.map +1 -1
- package/dist/src/tools/mcp-helpers.js +2 -1
- package/dist/src/tools/mcp-helpers.js.map +1 -1
- package/dist/src/tools/registry.js +4629 -3
- package/dist/src/tools/registry.js.map +4 -4
- package/dist/src/tools/scrape.js +2610 -80
- package/dist/src/tools/scrape.js.map +4 -4
- package/dist/src/tools/search.js +2388 -59
- package/dist/src/tools/search.js.map +4 -4
- package/dist/src/tools/start-research.js +2030 -23
- package/dist/src/tools/start-research.js.map +4 -4
- package/dist/src/tools/utils.js +98 -7
- package/dist/src/tools/utils.js.map +3 -3
- package/dist/src/utils/concurrency.js +1 -0
- package/dist/src/utils/concurrency.js.map +1 -1
- package/dist/src/utils/content-extractor.js +27 -2
- package/dist/src/utils/content-extractor.js.map +3 -3
- package/dist/src/utils/content-quality.js +4 -3
- package/dist/src/utils/content-quality.js.map +1 -1
- package/dist/src/utils/errors.js +26 -3
- package/dist/src/utils/errors.js.map +3 -3
- package/dist/src/utils/logger.js +1 -0
- package/dist/src/utils/logger.js.map +1 -1
- package/dist/src/utils/markdown-formatter.js +1 -0
- package/dist/src/utils/markdown-formatter.js.map +1 -1
- package/dist/src/utils/query-relax.js +9 -8
- package/dist/src/utils/query-relax.js.map +1 -1
- package/dist/src/utils/response.js +3 -2
- package/dist/src/utils/response.js.map +1 -1
- package/dist/src/utils/retry.js +5 -4
- package/dist/src/utils/retry.js.map +1 -1
- package/dist/src/utils/sanitize.js +4 -3
- package/dist/src/utils/sanitize.js.map +1 -1
- package/dist/src/utils/source-type.js +4 -3
- package/dist/src/utils/source-type.js.map +1 -1
- package/dist/src/utils/url-aggregator.js +112 -11
- package/dist/src/utils/url-aggregator.js.map +3 -3
- package/dist/src/version.js +7 -6
- package/dist/src/version.js.map +1 -1
- package/package.json +3 -3
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
// src/utils/source-type.ts
|
|
2
|
+
var RULES = [
|
|
2
3
|
// Reddit post permalinks (subreddit homepages are filtered out upstream).
|
|
3
4
|
[/(?:^|\.)reddit\.com\//i, "reddit"],
|
|
4
5
|
[/(?:^|\.)github\.com\//i, "github"],
|
|
@@ -35,7 +36,7 @@ function classifySourceByUrl(url) {
|
|
|
35
36
|
}
|
|
36
37
|
return "web";
|
|
37
38
|
}
|
|
38
|
-
|
|
39
|
+
var DOCUMENT_PATH_SUFFIXES = [
|
|
39
40
|
".pdf",
|
|
40
41
|
".doc",
|
|
41
42
|
".docx",
|
|
@@ -56,7 +57,7 @@ function isDocumentUrl(url) {
|
|
|
56
57
|
}
|
|
57
58
|
return false;
|
|
58
59
|
}
|
|
59
|
-
|
|
60
|
+
var BINARY_CONTENT_TYPE_PREFIXES = [
|
|
60
61
|
"application/pdf",
|
|
61
62
|
"application/msword",
|
|
62
63
|
"application/vnd.openxmlformats-officedocument.",
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/utils/source-type.ts"],
|
|
4
4
|
"sourcesContent": ["/**\n * Hostname/path-heuristic source-type tagging. Works without the LLM\n * classifier so degraded-mode search responses still carry a\n * `source_type` field per result. When the LLM classifier IS available,\n * its tag wins (the classifier sees title + snippet as well, not just URL).\n *\n * See: mcp-revisions/output-shaping/06-source-type-tagging-without-llm.md.\n */\n\nexport type SourceType =\n | 'reddit'\n | 'github'\n | 'docs'\n | 'blog'\n | 'paper'\n | 'qa'\n | 'cve'\n | 'news'\n | 'video'\n | 'web';\n\nconst RULES: Array<[RegExp, SourceType]> = [\n // Reddit post permalinks (subreddit homepages are filtered out upstream).\n [/(?:^|\\.)reddit\\.com\\//i, 'reddit'],\n [/(?:^|\\.)github\\.com\\//i, 'github'],\n [/(?:^|\\.)gitlab\\.com\\//i, 'github'],\n // CVE-prefixed paths are unambiguous regardless of host.\n [/\\/CVE-\\d{4}-\\d+/i, 'cve'],\n [/(?:^|\\.)nvd\\.nist\\.gov\\//i, 'cve'],\n [/(?:^|\\.)stackoverflow\\.com\\//i, 'qa'],\n [/(?:^|\\.)stackexchange\\.com\\//i, 'qa'],\n [/(?:^|\\.)arxiv\\.org\\//i, 'paper'],\n [/(?:^|\\.)medium\\.com\\//i, 'blog'],\n [/(?:^|\\.)dev\\.to\\//i, 'blog'],\n [/(?:^|\\.)substack\\.com\\//i, 'blog'],\n // Docs subdomains and /docs/ paths.\n [/^(?:[a-z0-9-]+\\.)*docs\\./i, 'docs'],\n [/\\/docs\\//i, 'docs'],\n [/(?:^|\\.)readthedocs\\.io\\//i, 'docs'],\n // Video.\n [/(?:^|\\.)youtube\\.com\\/watch/i, 'video'],\n [/(?:^|\\.)youtu\\.be\\//i, 'video'],\n // News / engineering blogs (last so it doesn't capture vendor docs).\n [/(?:^|\\.)(?:news|blog|engineering)\\.[a-z0-9-]+\\.[a-z]{2,}\\//i, 'news'],\n];\n\nexport function classifySourceByUrl(url: string): SourceType {\n let candidate: string;\n try {\n const u = new URL(url);\n // Match against `host + pathname` so rules can use either or both.\n candidate = `${u.hostname}${u.pathname}`;\n } catch {\n candidate = url;\n }\n for (const [re, type] of RULES) {\n if (re.test(candidate)) return type;\n }\n return 'web';\n}\n\n// \u2500\u2500 Document-format detection (PDF / Office) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Scrape.do + Readability + Turndown assume HTML input. Binary document\n// formats need a markdown-extraction service (Jina Reader) instead. These two\n// helpers give the scrape pipeline both a pre-fetch gate (URL suffix) and a\n// post-fetch gate (response Content-Type header).\n\nconst DOCUMENT_PATH_SUFFIXES = [\n '.pdf',\n '.doc', '.docx',\n '.ppt', '.pptx',\n '.xls', '.xlsx',\n] as const;\n\n/**\n * Pre-fetch gate: does this URL's path end in a known binary-document suffix?\n * Case-insensitive. Trailing query strings / fragments are ignored \u2014 only the\n * pathname is inspected. Invalid URLs return false (handled upstream).\n */\nexport function isDocumentUrl(url: string): boolean {\n let pathname: string;\n try {\n pathname = new URL(url).pathname.toLowerCase();\n } catch {\n return false;\n }\n for (const suffix of DOCUMENT_PATH_SUFFIXES) {\n if (pathname.endsWith(suffix)) return true;\n }\n return false;\n}\n\nconst BINARY_CONTENT_TYPE_PREFIXES = [\n 'application/pdf',\n 'application/msword',\n 'application/vnd.openxmlformats-officedocument.',\n 'application/vnd.ms-excel',\n 'application/vnd.ms-powerpoint',\n 'application/octet-stream',\n] as const;\n\n/**\n * Post-fetch gate: does this Content-Type header indicate a binary document\n * that our HTML pipeline cannot decode? Returns false for HTML/JSON/plain text\n * and for unknown/missing content-types (the upstream pipeline can still try).\n */\nexport function isBinaryDocumentContentType(contentType: string | null | undefined): boolean {\n if (!contentType) return false;\n const lower = contentType.toLowerCase();\n for (const prefix of BINARY_CONTENT_TYPE_PREFIXES) {\n if (lower.startsWith(prefix)) return true;\n }\n return false;\n}\n"],
|
|
5
|
-
"mappings": "AAqBA,
|
|
5
|
+
"mappings": ";AAqBA,IAAM,QAAqC;AAAA;AAAA,EAEzC,CAAC,0BAA0B,QAAQ;AAAA,EACnC,CAAC,0BAA0B,QAAQ;AAAA,EACnC,CAAC,0BAA0B,QAAQ;AAAA;AAAA,EAEnC,CAAC,oBAAoB,KAAK;AAAA,EAC1B,CAAC,6BAA6B,KAAK;AAAA,EACnC,CAAC,iCAAiC,IAAI;AAAA,EACtC,CAAC,iCAAiC,IAAI;AAAA,EACtC,CAAC,yBAAyB,OAAO;AAAA,EACjC,CAAC,0BAA0B,MAAM;AAAA,EACjC,CAAC,sBAAsB,MAAM;AAAA,EAC7B,CAAC,4BAA4B,MAAM;AAAA;AAAA,EAEnC,CAAC,6BAA6B,MAAM;AAAA,EACpC,CAAC,aAAa,MAAM;AAAA,EACpB,CAAC,8BAA8B,MAAM;AAAA;AAAA,EAErC,CAAC,gCAAgC,OAAO;AAAA,EACxC,CAAC,wBAAwB,OAAO;AAAA;AAAA,EAEhC,CAAC,+DAA+D,MAAM;AACxE;AAEO,SAAS,oBAAoB,KAAyB;AAC3D,MAAI;AACJ,MAAI;AACF,UAAM,IAAI,IAAI,IAAI,GAAG;AAErB,gBAAY,GAAG,EAAE,QAAQ,GAAG,EAAE,QAAQ;AAAA,EACxC,QAAQ;AACN,gBAAY;AAAA,EACd;AACA,aAAW,CAAC,IAAI,IAAI,KAAK,OAAO;AAC9B,QAAI,GAAG,KAAK,SAAS,EAAG,QAAO;AAAA,EACjC;AACA,SAAO;AACT;AAQA,IAAM,yBAAyB;AAAA,EAC7B;AAAA,EACA;AAAA,EAAQ;AAAA,EACR;AAAA,EAAQ;AAAA,EACR;AAAA,EAAQ;AACV;AAOO,SAAS,cAAc,KAAsB;AAClD,MAAI;AACJ,MAAI;AACF,eAAW,IAAI,IAAI,GAAG,EAAE,SAAS,YAAY;AAAA,EAC/C,QAAQ;AACN,WAAO;AAAA,EACT;AACA,aAAW,UAAU,wBAAwB;AAC3C,QAAI,SAAS,SAAS,MAAM,EAAG,QAAO;AAAA,EACxC;AACA,SAAO;AACT;AAEA,IAAM,+BAA+B;AAAA,EACnC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAOO,SAAS,4BAA4B,aAAiD;AAC3F,MAAI,CAAC,YAAa,QAAO;AACzB,QAAM,QAAQ,YAAY,YAAY;AACtC,aAAW,UAAU,8BAA8B;AACjD,QAAI,MAAM,WAAW,MAAM,EAAG,QAAO;AAAA,EACvC;AACA,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,13 +1,114 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
// src/config/index.ts
|
|
2
|
+
import { Logger } from "mcp-use";
|
|
3
|
+
|
|
4
|
+
// src/version.ts
|
|
5
|
+
import { createRequire } from "module";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
import { dirname, join } from "path";
|
|
8
|
+
var DEFAULT_PACKAGE_INFO = {
|
|
9
|
+
version: "3.9.5",
|
|
10
|
+
name: "mcp-researchpowerpack-http",
|
|
11
|
+
description: "Research Powerpack MCP Server"
|
|
12
|
+
};
|
|
13
|
+
var packageJson = { ...DEFAULT_PACKAGE_INFO };
|
|
14
|
+
try {
|
|
15
|
+
if (typeof import.meta.url === "string" && import.meta.url.startsWith("file:")) {
|
|
16
|
+
const _require = createRequire(import.meta.url);
|
|
17
|
+
const _dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
try {
|
|
19
|
+
packageJson = _require(join(_dirname, "..", "package.json"));
|
|
20
|
+
} catch {
|
|
21
|
+
packageJson = _require(join(_dirname, "..", "..", "package.json"));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
}
|
|
26
|
+
var VERSION = packageJson.version;
|
|
27
|
+
var PACKAGE_NAME = packageJson.name;
|
|
28
|
+
var PACKAGE_DESCRIPTION = packageJson.description;
|
|
29
|
+
var USER_AGENT_VERSION = `${PACKAGE_NAME}/${VERSION}`;
|
|
30
|
+
|
|
31
|
+
// src/config/index.ts
|
|
32
|
+
function safeParseInt(value, defaultVal, min, max) {
|
|
33
|
+
const logger = Logger.get("config");
|
|
34
|
+
if (!value) {
|
|
35
|
+
return defaultVal;
|
|
36
|
+
}
|
|
37
|
+
const parsed = parseInt(value, 10);
|
|
38
|
+
if (isNaN(parsed)) {
|
|
39
|
+
logger.warn(`Invalid number "${value}", using default ${defaultVal}`);
|
|
40
|
+
return defaultVal;
|
|
41
|
+
}
|
|
42
|
+
if (parsed < min) {
|
|
43
|
+
logger.warn(`Value ${parsed} below minimum ${min}, clamping to ${min}`);
|
|
44
|
+
return min;
|
|
45
|
+
}
|
|
46
|
+
if (parsed > max) {
|
|
47
|
+
logger.warn(`Value ${parsed} above maximum ${max}, clamping to ${max}`);
|
|
48
|
+
return max;
|
|
49
|
+
}
|
|
50
|
+
return parsed;
|
|
51
|
+
}
|
|
52
|
+
var CONCURRENCY = {
|
|
53
|
+
SEARCH: safeParseInt(process.env.CONCURRENCY_SEARCH, 50, 1, 200),
|
|
54
|
+
SCRAPER: safeParseInt(process.env.CONCURRENCY_SCRAPER, 50, 1, 200),
|
|
55
|
+
JINA_READER: safeParseInt(process.env.CONCURRENCY_JINA_READER, 50, 1, 200),
|
|
56
|
+
REDDIT: safeParseInt(process.env.CONCURRENCY_REDDIT, 50, 1, 200),
|
|
57
|
+
LLM_EXTRACTION: safeParseInt(process.env.LLM_CONCURRENCY, 50, 1, 200),
|
|
58
|
+
KERNEL: safeParseInt(process.env.CONCURRENCY_KERNEL, 3, 1, 20)
|
|
59
|
+
};
|
|
60
|
+
var CTR_WEIGHTS = {
|
|
61
|
+
1: 100,
|
|
62
|
+
2: 60,
|
|
63
|
+
3: 48.89,
|
|
64
|
+
4: 33.33,
|
|
65
|
+
5: 28.89,
|
|
66
|
+
6: 26.44,
|
|
67
|
+
7: 24.44,
|
|
68
|
+
8: 17.78,
|
|
69
|
+
9: 13.33,
|
|
70
|
+
10: 12.56
|
|
71
|
+
};
|
|
72
|
+
var cachedLlmExtraction = null;
|
|
73
|
+
function getLlmExtraction() {
|
|
74
|
+
if (cachedLlmExtraction) return cachedLlmExtraction;
|
|
75
|
+
const apiKey = process.env.LLM_API_KEY?.trim() || "";
|
|
76
|
+
const baseUrl = process.env.LLM_BASE_URL?.trim();
|
|
77
|
+
const model = process.env.LLM_MODEL?.trim();
|
|
78
|
+
const fallbackModel = process.env.LLM_FALLBACK_MODEL?.trim() || "";
|
|
79
|
+
if (apiKey && !baseUrl) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
"LLM_BASE_URL is required when LLM_API_KEY is set. Set LLM_BASE_URL to your OpenAI-compatible endpoint."
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
if (apiKey && !model) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
"LLM_MODEL is required when LLM_API_KEY is set."
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
cachedLlmExtraction = {
|
|
90
|
+
API_KEY: apiKey,
|
|
91
|
+
BASE_URL: baseUrl || "",
|
|
92
|
+
MODEL: model || "",
|
|
93
|
+
FALLBACK_MODEL: fallbackModel
|
|
94
|
+
};
|
|
95
|
+
return cachedLlmExtraction;
|
|
96
|
+
}
|
|
97
|
+
var LLM_EXTRACTION = new Proxy({}, {
|
|
98
|
+
get(_target, prop) {
|
|
99
|
+
return getLlmExtraction()[prop];
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// src/utils/url-aggregator.ts
|
|
104
|
+
var MIN_BEYOND_TOP10_WEIGHT = 0;
|
|
105
|
+
var BEYOND_TOP10_DECAY = 0.5;
|
|
106
|
+
var BEYOND_TOP10_BASE = 10;
|
|
107
|
+
var DEFAULT_MIN_CONSENSUS_URLS = 5;
|
|
108
|
+
var HIGH_CONSENSUS_THRESHOLD = 4;
|
|
109
|
+
var MAX_ALT_SNIPPETS = 3;
|
|
110
|
+
var MAX_CONSISTENCY_PENALTY = 0.15;
|
|
111
|
+
var CONSISTENCY_STDDEV_SCALE = 5;
|
|
11
112
|
function computePositionStats(positions) {
|
|
12
113
|
if (positions.length <= 1) {
|
|
13
114
|
return { mean: positions[0] ?? 0, stdDev: 0, consistencyMultiplier: 1 };
|
|
@@ -104,7 +205,7 @@ function calculateWeightedScores(urls, consensusThreshold, totalQueries) {
|
|
|
104
205
|
consistencyMultiplier: stats.consistencyMultiplier
|
|
105
206
|
}));
|
|
106
207
|
}
|
|
107
|
-
|
|
208
|
+
var COVERAGE_TABLE_MAX_ROWS = 20;
|
|
108
209
|
function consistencyLabel(stdDev, frequency) {
|
|
109
210
|
if (frequency <= 1) return "n/a";
|
|
110
211
|
if (stdDev < 1.5) return "high";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../../../src/utils/url-aggregator.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * URL Aggregator Utility\n * Aggregates search results across multiple queries, calculates CTR-weighted scores,\n * and generates consensus-based rankings.\n */\n\nimport { CTR_WEIGHTS } from '../config/index.js';\nimport type { QuerySearchResult } from '../clients/search.js';\n\n/** Minimum frequency for web search consensus marking */\nconst WEB_CONSENSUS_THRESHOLD = 3 as const;\n\n\n/** Minimum weight assigned to positions beyond top 10 */\nconst MIN_BEYOND_TOP10_WEIGHT = 0 as const;\n\n/** Weight decay per position beyond top 10 */\nconst BEYOND_TOP10_DECAY = 0.5 as const;\n\n/** Base position for beyond-top-10 weight calculation */\nconst BEYOND_TOP10_BASE = 10 as const;\n\n/** Default minimum consensus URLs before lowering threshold (web search) */\nconst DEFAULT_MIN_CONSENSUS_URLS = 5 as const;\n\n/** High consensus frequency threshold for enhanced output labeling */\nconst HIGH_CONSENSUS_THRESHOLD = 4 as const;\n\n/** Maximum number of alternative snippets to retain per URL */\nconst MAX_ALT_SNIPPETS = 3 as const;\n\n/** Consistency penalty cap \u2014 bounds the impact of position variance */\nconst MAX_CONSISTENCY_PENALTY = 0.15 as const;\n\n/** Standard deviation normalizer \u2014 stdDev of 5+ gets full penalty */\nconst CONSISTENCY_STDDEV_SCALE = 5 as const;\n\n/**\n * Aggregated URL data structure\n */\ninterface AggregatedUrl {\n readonly url: string;\n title: string;\n snippet: string;\n readonly allSnippets: string[];\n frequency: number;\n readonly positions: number[];\n readonly queries: string[];\n bestPosition: number;\n totalScore: number;\n}\n\n/**\n * Compute position statistics for consistency scoring\n */\nfunction computePositionStats(positions: number[]): { mean: number; stdDev: number; consistencyMultiplier: number } {\n if (positions.length <= 1) {\n return { mean: positions[0] ?? 0, stdDev: 0, consistencyMultiplier: 1.0 };\n }\n const mean = positions.reduce((a, b) => a + b, 0) / positions.length;\n const variance = positions.reduce((sum, p) => sum + (p - mean) ** 2, 0) / (positions.length - 1);\n const stdDev = Math.sqrt(variance);\n const consistencyMultiplier = 1.0 - MAX_CONSISTENCY_PENALTY * Math.min(stdDev / CONSISTENCY_STDDEV_SCALE, 1.0);\n return { mean, stdDev, consistencyMultiplier };\n}\n\n/**\n * Ranked URL with normalized score and enriched signals\n */\ninterface RankedUrl {\n readonly url: string;\n readonly title: string;\n readonly snippet: string;\n readonly allSnippets: string[];\n readonly rank: number;\n readonly score: number;\n readonly frequency: number;\n readonly positions: number[];\n readonly queries: string[];\n readonly bestPosition: number;\n readonly isConsensus: boolean;\n readonly coverageRatio: number;\n readonly positionStdDev: number;\n readonly consistencyMultiplier: number;\n}\n\n/**\n * Aggregation result containing all processed data\n */\ninterface AggregationResult {\n readonly rankedUrls: RankedUrl[];\n readonly totalUniqueUrls: number;\n readonly totalQueries: number;\n readonly frequencyThreshold: number;\n readonly thresholdNote?: string;\n}\n\n/**\n * Get CTR weight for a position (1-10)\n * Positions beyond 10 get minimal weight\n */\nfunction getCtrWeight(position: number): number {\n if (position >= 1 && position <= 10) {\n return CTR_WEIGHTS[position] ?? 0;\n }\n // Positions beyond 10 get diminishing returns\n return Math.max(MIN_BEYOND_TOP10_WEIGHT, BEYOND_TOP10_BASE - (position - BEYOND_TOP10_BASE) * BEYOND_TOP10_DECAY);\n}\n\n/**\n * Aggregate results from multiple searches\n * Flattens all results, deduplicates by URL, and tracks frequency/positions\n */\nfunction aggregateResults(searches: QuerySearchResult[]): Map<string, AggregatedUrl> {\n const urlMap = new Map<string, AggregatedUrl>();\n\n for (const search of searches) {\n for (const result of search.results) {\n const normalizedUrl = normalizeUrl(result.link);\n const existing = urlMap.get(normalizedUrl);\n\n if (existing) {\n existing.frequency += 1;\n existing.positions.push(result.position);\n existing.queries.push(search.query);\n const prevBest = existing.bestPosition;\n existing.bestPosition = Math.min(existing.bestPosition, result.position);\n existing.totalScore += getCtrWeight(result.position);\n // Collect distinct snippets (up to MAX_ALT_SNIPPETS)\n if (\n result.snippet &&\n existing.allSnippets.length < MAX_ALT_SNIPPETS &&\n !existing.allSnippets.some(s => s === result.snippet)\n ) {\n existing.allSnippets.push(result.snippet);\n }\n // Keep best title/snippet (from highest ranking position)\n if (result.position < prevBest) {\n existing.title = result.title;\n existing.snippet = result.snippet;\n }\n } else {\n urlMap.set(normalizedUrl, {\n url: result.link,\n title: result.title,\n snippet: result.snippet,\n allSnippets: result.snippet ? [result.snippet] : [],\n frequency: 1,\n positions: [result.position],\n queries: [search.query],\n bestPosition: result.position,\n totalScore: getCtrWeight(result.position),\n });\n }\n }\n }\n\n return urlMap;\n}\n\n/**\n * Normalize URL for deduplication\n * Removes trailing slashes, www prefix, and normalizes protocol\n */\nfunction normalizeUrl(url: string): string {\n try {\n const parsed = new URL(url);\n let host = parsed.hostname.replace(/^www\\./, '');\n let path = parsed.pathname.replace(/\\/$/, '') || '/';\n return `${host}${path}${parsed.search}`.toLowerCase();\n } catch {\n return url.toLowerCase().replace(/\\/$/, '');\n }\n}\n\n/**\n * Count URLs meeting a frequency threshold\n */\nfunction countByFrequency(\n urlMap: Map<string, AggregatedUrl>,\n minFrequency: number\n): number {\n let count = 0;\n for (const url of urlMap.values()) {\n if (url.frequency >= minFrequency) count++;\n }\n return count;\n}\n\n/**\n * Calculate weighted scores with consistency multiplier, normalize to 100.0.\n * Returns ALL URLs sorted by composite score with rank assignments and consensus marking.\n */\nfunction calculateWeightedScores(urls: AggregatedUrl[], consensusThreshold: number, totalQueries: number): RankedUrl[] {\n if (urls.length === 0) return [];\n\n // Compute composite scores (base CTR \u00D7 consistency multiplier)\n const scored = urls.map(url => {\n const stats = computePositionStats(url.positions);\n const compositeScore = url.totalScore * stats.consistencyMultiplier;\n return { url, compositeScore, stats };\n });\n\n // Sort by composite score descending\n scored.sort((a, b) => b.compositeScore - a.compositeScore);\n\n // Find max for normalization\n const maxScore = scored[0]!.compositeScore;\n\n // Map to ranked URLs with all signals\n return scored.map(({ url, compositeScore, stats }, index) => ({\n url: url.url,\n title: url.title,\n snippet: url.snippet,\n allSnippets: url.allSnippets,\n rank: index + 1,\n score: maxScore > 0 ? (compositeScore / maxScore) * 100 : 0,\n frequency: url.frequency,\n positions: url.positions,\n queries: url.queries,\n bestPosition: url.bestPosition,\n isConsensus: url.frequency >= consensusThreshold,\n coverageRatio: totalQueries > 0 ? url.frequency / totalQueries : 0,\n positionStdDev: stats.stdDev,\n consistencyMultiplier: stats.consistencyMultiplier,\n }));\n}\n\n/** Maximum queries to show in the coverage table before collapsing */\nconst COVERAGE_TABLE_MAX_ROWS = 20 as const;\n\n/**\n * Consistency label based on position standard deviation\n */\nfunction consistencyLabel(stdDev: number, frequency: number): string {\n if (frequency <= 1) return 'n/a';\n if (stdDev < 1.5) return 'high';\n if (stdDev < 3.5) return 'medium';\n return 'variable';\n}\n\n/**\n * Generate a unified output where every URL appears exactly once.\n * Replaces the old generateEnhancedOutput + per-query section combo.\n */\nexport function generateUnifiedOutput(\n rankedUrls: RankedUrl[],\n allQueries: string[],\n queryResults: QuerySearchResult[],\n totalUniqueUrls: number,\n frequencyThreshold: number,\n thresholdNote?: string,\n verbose: boolean = false,\n): string {\n const lines: string[] = [];\n const consensusCount = rankedUrls.filter(u => u.isConsensus).length;\n\n // Header\n lines.push(`## Web Search Results (${allQueries.length} queries, ${totalUniqueUrls} unique URLs)`);\n lines.push('');\n if (thresholdNote) {\n lines.push(`> ${thresholdNote}`);\n lines.push('');\n }\n\n // Ranked URL list \u2014 every URL exactly once.\n //\n // Per-row metadata is gated:\n // - CONSENSUS labels only appear when the effective threshold is >1 (a\n // threshold of 1 means *every* row gets the label, so it carries no\n // signal). See: docs/code-review/context/02-current-tool-surface.md.\n // - The Score/Seen/Consistency line is suppressed for rows that were\n // seen in exactly one query in a multi-query call (Seen=1/N is common\n // and Consistency is always \"n/a\" in that case).\n // - Verbose mode restores both for callers that explicitly want them.\n const consensusActive = frequencyThreshold > 1;\n\n for (const url of rankedUrls) {\n const consensusTag = consensusActive && url.frequency >= HIGH_CONSENSUS_THRESHOLD\n ? ' CONSENSUS+++'\n : consensusActive && url.isConsensus\n ? ' CONSENSUS'\n : '';\n const coveragePct = Math.round(url.coverageRatio * 100);\n const consistency = consistencyLabel(url.positionStdDev, url.frequency);\n\n lines.push(`**${url.rank}. [${url.title}](${url.url})**${consensusTag}`);\n\n const showRowMetadata = verbose\n || (allQueries.length > 1 && url.frequency > 1)\n || allQueries.length === 1;\n if (showRowMetadata) {\n const parts = [\n `Score: ${url.score.toFixed(1)}`,\n `Seen in: ${url.frequency}/${allQueries.length} queries (${coveragePct}%)`,\n `Best pos: #${url.bestPosition}`,\n ];\n if (url.frequency > 1) {\n parts.push(`Consistency: ${consistency}`);\n }\n lines.push(parts.join(' | '));\n }\n if (url.queries.length > 1 || verbose) {\n lines.push(`Queries: ${url.queries.map(q => `\"${q}\"`).join(', ')}`);\n }\n lines.push(`> ${url.snippet}`);\n\n // Alt snippets (if multiple distinct snippets were collected)\n if (url.allSnippets.length > 1) {\n const alts = url.allSnippets\n .filter(s => s !== url.snippet)\n .slice(0, 3)\n .map(s => s.length > 100 ? s.slice(0, 97) + '...' : s);\n if (alts.length > 0) {\n lines.push(`Alt: ${alts.map(s => `\"${s}\"`).join(' | ')}`);\n }\n }\n\n lines.push('');\n }\n\n // Keyword coverage section\n lines.push('---');\n\n if (allQueries.length <= COVERAGE_TABLE_MAX_ROWS) {\n // Full table for \u226420 queries\n lines.push('### Query Coverage');\n lines.push('| Query | Results | Top URL | Top Pos |');\n lines.push('|---------|---------|---------|---------|');\n\n for (const search of queryResults) {\n const topResult = search.results[0];\n let topDomain = '';\n if (topResult) {\n try {\n topDomain = new URL(topResult.link).hostname.replace(/^www\\./, '');\n } catch {\n topDomain = topResult.link;\n }\n }\n lines.push(`| \"${search.query}\" | ${search.results.length} | ${topDomain || '\u2014'} | ${topResult ? `#${topResult.position}` : '\u2014'} |`);\n }\n lines.push('');\n } else {\n // Collapsed summary for >20 queries\n const goodCount = queryResults.filter(s => s.results.length >= 3).length;\n lines.push(`### Query Coverage: ${goodCount}/${allQueries.length} queries returned 3+ results`);\n lines.push('');\n }\n\n // Low-yield queries\n const lowYield = queryResults.filter(s => s.results.length <= 1);\n if (lowYield.length > 0) {\n lines.push(`**Low-yield queries** (0-1 results): ${lowYield.map(s => `\\`${s.query}\\``).join(', ')}`);\n lines.push('');\n }\n\n // Related searches (merged and deduplicated)\n const allRelated = new Set<string>();\n for (const search of queryResults) {\n if (search.related) {\n for (const r of search.related) {\n allRelated.add(r);\n }\n }\n }\n if (allRelated.size > 0) {\n const related = [...allRelated].slice(0, 10);\n lines.push(`**Related searches:** ${related.map(r => `\\`${r}\\``).join(', ')}`);\n lines.push('');\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Full aggregation pipeline \u2014 returns ALL URLs ranked by CTR score.\n * Determines a consensus threshold (\u22653, \u22652, or \u22651) for labeling, but never\n * drops URLs below the threshold. Every collected URL appears in the output.\n */\nexport function aggregateAndRank(\n searches: QuerySearchResult[],\n minConsensusUrls: number = DEFAULT_MIN_CONSENSUS_URLS\n): AggregationResult {\n const urlMap = aggregateResults(searches);\n const totalUniqueUrls = urlMap.size;\n const totalQueries = searches.length;\n\n // Determine consensus threshold for labeling (not filtering)\n const thresholds = [3, 2, 1];\n let usedThreshold = 1;\n let thresholdNote: string | undefined;\n\n for (const threshold of thresholds) {\n const count = countByFrequency(urlMap, threshold);\n if (count >= minConsensusUrls || threshold === 1) {\n usedThreshold = threshold;\n if (threshold < 3) {\n thresholdNote = `Note: Consensus threshold set to \u2265${threshold} due to result diversity.`;\n }\n break;\n }\n }\n\n // Rank ALL URLs, marking consensus based on determined threshold\n const allUrls = [...urlMap.values()];\n const rankedUrls = calculateWeightedScores(allUrls, usedThreshold, totalQueries);\n\n return {\n rankedUrls,\n totalUniqueUrls,\n totalQueries,\n frequencyThreshold: usedThreshold,\n thresholdNote,\n };\n}\n\n"],
|
|
5
|
-
"mappings": "
|
|
3
|
+
"sources": ["../../../src/config/index.ts", "../../../src/version.ts", "../../../src/utils/url-aggregator.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Consolidated configuration\n * All environment variables, constants, and LLM config in one place\n */\n\nimport { Logger } from 'mcp-use';\n\nimport { VERSION, PACKAGE_NAME, PACKAGE_DESCRIPTION } from '../version.js';\n\n// ============================================================================\n// Safe Integer Parsing Helper\n// ============================================================================\n\n/**\n * Safely parse an integer from environment variable with bounds checking\n */\nfunction safeParseInt(\n value: string | undefined,\n defaultVal: number,\n min: number,\n max: number\n): number {\n const logger = Logger.get('config');\n\n if (!value) {\n return defaultVal;\n }\n\n const parsed = parseInt(value, 10);\n\n if (isNaN(parsed)) {\n logger.warn(`Invalid number \"${value}\", using default ${defaultVal}`);\n return defaultVal;\n }\n\n if (parsed < min) {\n logger.warn(`Value ${parsed} below minimum ${min}, clamping to ${min}`);\n return min;\n }\n\n if (parsed > max) {\n logger.warn(`Value ${parsed} above maximum ${max}, clamping to ${max}`);\n return max;\n }\n\n return parsed;\n}\n\n\n// ============================================================================\n// Environment Parsing\n// ============================================================================\n\ninterface EnvConfig {\n SCRAPER_API_KEY: string;\n SEARCH_API_KEY: string | undefined;\n REDDIT_CLIENT_ID: string | undefined;\n REDDIT_CLIENT_SECRET: string | undefined;\n JINA_API_KEY: string | undefined;\n KERNEL_API_KEY: string | undefined;\n KERNEL_PROJECT: string | undefined;\n}\n\nlet cachedEnv: EnvConfig | null = null;\n\nexport function parseEnv(): EnvConfig {\n if (cachedEnv) return cachedEnv;\n cachedEnv = {\n SCRAPER_API_KEY: process.env.SCRAPEDO_API_KEY || '',\n SEARCH_API_KEY: process.env.SERPER_API_KEY || undefined,\n REDDIT_CLIENT_ID: process.env.REDDIT_CLIENT_ID || undefined,\n REDDIT_CLIENT_SECRET: process.env.REDDIT_CLIENT_SECRET || undefined,\n JINA_API_KEY: process.env.JINA_API_KEY || undefined,\n KERNEL_API_KEY: process.env.KERNEL_API_KEY || undefined,\n KERNEL_PROJECT: process.env.KERNEL_PROJECT || undefined,\n };\n return cachedEnv;\n}\n\n// ============================================================================\n// MCP Server Configuration\n// ============================================================================\n\nexport const SERVER = {\n NAME: PACKAGE_NAME,\n VERSION: VERSION,\n DESCRIPTION: PACKAGE_DESCRIPTION,\n} as const;\n\n// ============================================================================\n// Capability Detection (which features are available based on ENV)\n// ============================================================================\n\nexport interface Capabilities {\n reddit: boolean; // REDDIT_CLIENT_ID + REDDIT_CLIENT_SECRET\n search: boolean; // SERPER_API_KEY or JINA_API_KEY\n serperSearch: boolean; // SERPER_API_KEY\n jina: boolean; // JINA_API_KEY (Reader auth + Search)\n scraping: boolean; // SCRAPEDO_API_KEY\n kernel: boolean; // KERNEL_API_KEY\n llmExtraction: boolean; // LLM_API_KEY + LLM_BASE_URL + LLM_MODEL\n}\n\nexport function getCapabilities(): Capabilities {\n const env = parseEnv();\n return {\n reddit: !!(env.REDDIT_CLIENT_ID && env.REDDIT_CLIENT_SECRET),\n search: !!(env.SEARCH_API_KEY || env.JINA_API_KEY),\n serperSearch: !!env.SEARCH_API_KEY,\n jina: !!env.JINA_API_KEY,\n scraping: !!env.SCRAPER_API_KEY,\n kernel: !!env.KERNEL_API_KEY,\n llmExtraction: getLLMConfigStatus().configured,\n };\n}\n\nexport function getMissingEnvMessage(capability: keyof Capabilities): string {\n const messages: Record<keyof Capabilities, string> = {\n reddit: '\u274C **Reddit scraping unavailable.** Set `REDDIT_CLIENT_ID` and `REDDIT_CLIENT_SECRET` to enable threaded Reddit post fetching in `raw-scrape-links` and `smart-scrape-links`.\\n\\n\uD83D\uDC49 Create a Reddit app at: https://www.reddit.com/prefs/apps (select \"script\" type)',\n search: '\u274C **Search unavailable.** Set `SERPER_API_KEY` or `JINA_API_KEY` to enable `raw-web-search` and `smart-web-search`.\\n\\n\uD83D\uDC49 Serper provides Google SERPs; Jina Search is used as a fallback/search-only provider.',\n serperSearch: '\u274C **Serper search unavailable.** Set `SERPER_API_KEY` to enable Google-backed primary search.',\n jina: '\u274C **Jina unavailable.** Set `JINA_API_KEY` to enable Jina Search and authenticated Jina Reader requests.',\n scraping: '\u26A0\uFE0F **Scrape.do proxy fallback unavailable.** Set `SCRAPEDO_API_KEY` to enable Jina Reader retries through Scrape.do proxy mode.\\n\\n\uD83D\uDC49 Sign up at: https://scrape.do (1,000 free credits)',\n kernel: '\u274C **Kernel browser rendering unavailable.** Set `KERNEL_API_KEY` to enable optional browser-render fallback for raw/smart scraping.',\n llmExtraction: '\u26A0\uFE0F **AI extraction disabled.** Set `LLM_API_KEY`, `LLM_BASE_URL`, and `LLM_MODEL` to enable `smart-web-search` and `smart-scrape-links`.\\n\\nUse the raw tools when you need markdown without LLM processing.',\n };\n return messages[capability];\n}\n\n// ============================================================================\n// Concurrency Limits\n// ============================================================================\n\nexport const CONCURRENCY = {\n SEARCH: safeParseInt(process.env.CONCURRENCY_SEARCH, 50, 1, 200),\n SCRAPER: safeParseInt(process.env.CONCURRENCY_SCRAPER, 50, 1, 200),\n JINA_READER: safeParseInt(process.env.CONCURRENCY_JINA_READER, 50, 1, 200),\n REDDIT: safeParseInt(process.env.CONCURRENCY_REDDIT, 50, 1, 200),\n LLM_EXTRACTION: safeParseInt(process.env.LLM_CONCURRENCY, 50, 1, 200),\n KERNEL: safeParseInt(process.env.CONCURRENCY_KERNEL, 3, 1, 20),\n} as const;\n\nexport const SCRAPER = {\n BATCH_SIZE: 30,\n EXTRACTION_PREFIX: 'Extract from document only \u2014 never hallucinate or add external knowledge.',\n EXTRACTION_SUFFIX: 'First line = content, not preamble. No confirmation messages.',\n} as const;\n\n// ============================================================================\n// Reddit Configuration\n// ============================================================================\n\nexport const REDDIT = {\n BATCH_SIZE: 10,\n MAX_WORDS_PER_POST: 50_000,\n MAX_WORDS_TOTAL: 500_000,\n MIN_POSTS: 1,\n MAX_POSTS: 50,\n RETRY_COUNT: 5,\n RETRY_DELAYS: [2000, 4000, 8000, 16000, 32000] as const,\n} as const;\n\n// ============================================================================\n// CTR Weights for URL Ranking (inspired from CTR research)\n// ============================================================================\n\nexport const CTR_WEIGHTS: Record<number, number> = {\n 1: 100.00,\n 2: 60.00,\n 3: 48.89,\n 4: 33.33,\n 5: 28.89,\n 6: 26.44,\n 7: 24.44,\n 8: 17.78,\n 9: 13.33,\n 10: 12.56,\n} as const;\n\n// ============================================================================\n// LLM Configuration\n//\n// Required vars (all must be set together when LLM is enabled):\n// LLM_API_KEY \u2014 API key for the OpenAI-compatible endpoint\n// LLM_BASE_URL \u2014 endpoint base URL (e.g. https://server.up.railway.app/v1)\n// LLM_MODEL \u2014 primary model (e.g. gpt-5.4-mini)\n//\n// Optional:\n// LLM_FALLBACK_MODEL \u2014 model to use after primary exhausts all retries (e.g. gpt-5.4)\n// LLM_CONCURRENCY \u2014 parallel LLM calls (default: 50)\n//\n// Reasoning effort is always 'low' \u2014 not configurable.\n// ============================================================================\n\ninterface LlmExtractionConfig {\n readonly MODEL: string;\n readonly FALLBACK_MODEL: string;\n readonly BASE_URL: string;\n readonly API_KEY: string;\n}\n\nexport type LLMRequiredEnvVar = 'LLM_API_KEY' | 'LLM_BASE_URL' | 'LLM_MODEL';\n\nexport interface LLMConfigStatus {\n readonly configured: boolean;\n readonly apiKeyPresent: boolean;\n readonly baseUrlPresent: boolean;\n readonly modelPresent: boolean;\n readonly missingVars: readonly LLMRequiredEnvVar[];\n readonly error: string | null;\n}\n\nlet cachedLlmConfigStatus: LLMConfigStatus | null = null;\n\nexport function getLLMConfigStatus(): LLMConfigStatus {\n if (cachedLlmConfigStatus) return cachedLlmConfigStatus;\n\n const apiKeyPresent = !!process.env.LLM_API_KEY?.trim();\n const baseUrlPresent = !!process.env.LLM_BASE_URL?.trim();\n const modelPresent = !!process.env.LLM_MODEL?.trim();\n const missingVars: LLMRequiredEnvVar[] = [];\n\n if (!apiKeyPresent) missingVars.push('LLM_API_KEY');\n if (!baseUrlPresent) missingVars.push('LLM_BASE_URL');\n if (!modelPresent) missingVars.push('LLM_MODEL');\n\n const configured = missingVars.length === 0;\n cachedLlmConfigStatus = {\n configured,\n apiKeyPresent,\n baseUrlPresent,\n modelPresent,\n missingVars,\n error: configured\n ? null\n : `LLM disabled: missing ${missingVars.join(', ')}`,\n };\n return cachedLlmConfigStatus;\n}\n\n/**\n * Test-only \u2014 drop every env-derived cache so a test can mutate process.env\n * and re-read fresh values. Covers the LLM config caches AND the parseEnv()\n * cache (which holds SCRAPEDO_API_KEY, SERPER_API_KEY, REDDIT_CLIENT_*,\n * JINA_API_KEY, KERNEL_API_KEY, KERNEL_PROJECT). Tests that scrub non-LLM env vars must also see a clean\n * env on the next parseEnv() call, otherwise getCapabilities() returns\n * stale flags.\n */\nexport function _resetLLMConfigStatusForTests(): void {\n cachedLlmConfigStatus = null;\n cachedLlmExtraction = null;\n cachedEnv = null;\n}\n\nlet cachedLlmExtraction: LlmExtractionConfig | null = null;\n\nfunction getLlmExtraction(): LlmExtractionConfig {\n if (cachedLlmExtraction) return cachedLlmExtraction;\n\n const apiKey = process.env.LLM_API_KEY?.trim() || '';\n const baseUrl = process.env.LLM_BASE_URL?.trim();\n const model = process.env.LLM_MODEL?.trim();\n const fallbackModel = process.env.LLM_FALLBACK_MODEL?.trim() || '';\n\n if (apiKey && !baseUrl) {\n throw new Error(\n 'LLM_BASE_URL is required when LLM_API_KEY is set. ' +\n 'Set LLM_BASE_URL to your OpenAI-compatible endpoint.',\n );\n }\n if (apiKey && !model) {\n throw new Error(\n 'LLM_MODEL is required when LLM_API_KEY is set.',\n );\n }\n\n cachedLlmExtraction = {\n API_KEY: apiKey,\n BASE_URL: baseUrl || '',\n MODEL: model || '',\n FALLBACK_MODEL: fallbackModel,\n };\n return cachedLlmExtraction;\n}\n\nexport const LLM_EXTRACTION: LlmExtractionConfig = new Proxy({} as LlmExtractionConfig, {\n get(_target, prop: string) {\n return getLlmExtraction()[prop as keyof LlmExtractionConfig];\n },\n});\n", "/**\n * Version Module - Single Source of Truth\n * \n * This module reads package metadata from package.json at runtime and keeps a\n * fallback copy for environments where package.json cannot be resolved.\n * \n * Usage:\n * import { VERSION, PACKAGE_NAME } from './version.js';\n */\n\nimport { createRequire } from 'module';\nimport { fileURLToPath } from 'url';\nimport { dirname, join } from 'path';\n\n// Defaults used if package.json cannot be loaded at runtime.\nconst DEFAULT_PACKAGE_INFO = {\n version: '3.9.5',\n name: 'mcp-researchpowerpack-http',\n description: 'Research Powerpack MCP Server',\n} as const;\n\nlet packageJson: { version: string; name: string; description: string } = { ...DEFAULT_PACKAGE_INFO };\n\ntry {\n if (typeof import.meta.url === 'string' && import.meta.url.startsWith('file:')) {\n const _require = createRequire(import.meta.url);\n const _dirname = dirname(fileURLToPath(import.meta.url));\n try {\n packageJson = _require(join(_dirname, '..', 'package.json'));\n } catch {\n packageJson = _require(join(_dirname, '..', '..', 'package.json'));\n }\n }\n} catch {\n // Keep hardcoded defaults when package.json is unavailable\n}\n\n/**\n * Package version from package.json\n * This is the single source of truth for versioning\n */\nexport const VERSION: string = packageJson.version;\n\n/**\n * Package name from package.json\n */\nexport const PACKAGE_NAME: string = packageJson.name;\n\n/**\n * Package description from package.json\n */\nexport const PACKAGE_DESCRIPTION: string = packageJson.description;\n\n/**\n * Formatted version string for user agents and logging\n * Example: \"mcp-researchpowerpack-http/3.2.0\"\n */\nexport const USER_AGENT_VERSION: string = `${PACKAGE_NAME}/${VERSION}`;\n\n// VERSION_INFO removed - unused, individual exports sufficient\n", "/**\n * URL Aggregator Utility\n * Aggregates search results across multiple queries, calculates CTR-weighted scores,\n * and generates consensus-based rankings.\n */\n\nimport { CTR_WEIGHTS } from '../config/index.js';\nimport type { QuerySearchResult } from '../clients/search.js';\n\n/** Minimum frequency for web search consensus marking */\nconst WEB_CONSENSUS_THRESHOLD = 3 as const;\n\n\n/** Minimum weight assigned to positions beyond top 10 */\nconst MIN_BEYOND_TOP10_WEIGHT = 0 as const;\n\n/** Weight decay per position beyond top 10 */\nconst BEYOND_TOP10_DECAY = 0.5 as const;\n\n/** Base position for beyond-top-10 weight calculation */\nconst BEYOND_TOP10_BASE = 10 as const;\n\n/** Default minimum consensus URLs before lowering threshold (web search) */\nconst DEFAULT_MIN_CONSENSUS_URLS = 5 as const;\n\n/** High consensus frequency threshold for enhanced output labeling */\nconst HIGH_CONSENSUS_THRESHOLD = 4 as const;\n\n/** Maximum number of alternative snippets to retain per URL */\nconst MAX_ALT_SNIPPETS = 3 as const;\n\n/** Consistency penalty cap \u2014 bounds the impact of position variance */\nconst MAX_CONSISTENCY_PENALTY = 0.15 as const;\n\n/** Standard deviation normalizer \u2014 stdDev of 5+ gets full penalty */\nconst CONSISTENCY_STDDEV_SCALE = 5 as const;\n\n/**\n * Aggregated URL data structure\n */\ninterface AggregatedUrl {\n readonly url: string;\n title: string;\n snippet: string;\n readonly allSnippets: string[];\n frequency: number;\n readonly positions: number[];\n readonly queries: string[];\n bestPosition: number;\n totalScore: number;\n}\n\n/**\n * Compute position statistics for consistency scoring\n */\nfunction computePositionStats(positions: number[]): { mean: number; stdDev: number; consistencyMultiplier: number } {\n if (positions.length <= 1) {\n return { mean: positions[0] ?? 0, stdDev: 0, consistencyMultiplier: 1.0 };\n }\n const mean = positions.reduce((a, b) => a + b, 0) / positions.length;\n const variance = positions.reduce((sum, p) => sum + (p - mean) ** 2, 0) / (positions.length - 1);\n const stdDev = Math.sqrt(variance);\n const consistencyMultiplier = 1.0 - MAX_CONSISTENCY_PENALTY * Math.min(stdDev / CONSISTENCY_STDDEV_SCALE, 1.0);\n return { mean, stdDev, consistencyMultiplier };\n}\n\n/**\n * Ranked URL with normalized score and enriched signals\n */\ninterface RankedUrl {\n readonly url: string;\n readonly title: string;\n readonly snippet: string;\n readonly allSnippets: string[];\n readonly rank: number;\n readonly score: number;\n readonly frequency: number;\n readonly positions: number[];\n readonly queries: string[];\n readonly bestPosition: number;\n readonly isConsensus: boolean;\n readonly coverageRatio: number;\n readonly positionStdDev: number;\n readonly consistencyMultiplier: number;\n}\n\n/**\n * Aggregation result containing all processed data\n */\ninterface AggregationResult {\n readonly rankedUrls: RankedUrl[];\n readonly totalUniqueUrls: number;\n readonly totalQueries: number;\n readonly frequencyThreshold: number;\n readonly thresholdNote?: string;\n}\n\n/**\n * Get CTR weight for a position (1-10)\n * Positions beyond 10 get minimal weight\n */\nfunction getCtrWeight(position: number): number {\n if (position >= 1 && position <= 10) {\n return CTR_WEIGHTS[position] ?? 0;\n }\n // Positions beyond 10 get diminishing returns\n return Math.max(MIN_BEYOND_TOP10_WEIGHT, BEYOND_TOP10_BASE - (position - BEYOND_TOP10_BASE) * BEYOND_TOP10_DECAY);\n}\n\n/**\n * Aggregate results from multiple searches\n * Flattens all results, deduplicates by URL, and tracks frequency/positions\n */\nfunction aggregateResults(searches: QuerySearchResult[]): Map<string, AggregatedUrl> {\n const urlMap = new Map<string, AggregatedUrl>();\n\n for (const search of searches) {\n for (const result of search.results) {\n const normalizedUrl = normalizeUrl(result.link);\n const existing = urlMap.get(normalizedUrl);\n\n if (existing) {\n existing.frequency += 1;\n existing.positions.push(result.position);\n existing.queries.push(search.query);\n const prevBest = existing.bestPosition;\n existing.bestPosition = Math.min(existing.bestPosition, result.position);\n existing.totalScore += getCtrWeight(result.position);\n // Collect distinct snippets (up to MAX_ALT_SNIPPETS)\n if (\n result.snippet &&\n existing.allSnippets.length < MAX_ALT_SNIPPETS &&\n !existing.allSnippets.some(s => s === result.snippet)\n ) {\n existing.allSnippets.push(result.snippet);\n }\n // Keep best title/snippet (from highest ranking position)\n if (result.position < prevBest) {\n existing.title = result.title;\n existing.snippet = result.snippet;\n }\n } else {\n urlMap.set(normalizedUrl, {\n url: result.link,\n title: result.title,\n snippet: result.snippet,\n allSnippets: result.snippet ? [result.snippet] : [],\n frequency: 1,\n positions: [result.position],\n queries: [search.query],\n bestPosition: result.position,\n totalScore: getCtrWeight(result.position),\n });\n }\n }\n }\n\n return urlMap;\n}\n\n/**\n * Normalize URL for deduplication\n * Removes trailing slashes, www prefix, and normalizes protocol\n */\nfunction normalizeUrl(url: string): string {\n try {\n const parsed = new URL(url);\n let host = parsed.hostname.replace(/^www\\./, '');\n let path = parsed.pathname.replace(/\\/$/, '') || '/';\n return `${host}${path}${parsed.search}`.toLowerCase();\n } catch {\n return url.toLowerCase().replace(/\\/$/, '');\n }\n}\n\n/**\n * Count URLs meeting a frequency threshold\n */\nfunction countByFrequency(\n urlMap: Map<string, AggregatedUrl>,\n minFrequency: number\n): number {\n let count = 0;\n for (const url of urlMap.values()) {\n if (url.frequency >= minFrequency) count++;\n }\n return count;\n}\n\n/**\n * Calculate weighted scores with consistency multiplier, normalize to 100.0.\n * Returns ALL URLs sorted by composite score with rank assignments and consensus marking.\n */\nfunction calculateWeightedScores(urls: AggregatedUrl[], consensusThreshold: number, totalQueries: number): RankedUrl[] {\n if (urls.length === 0) return [];\n\n // Compute composite scores (base CTR \u00D7 consistency multiplier)\n const scored = urls.map(url => {\n const stats = computePositionStats(url.positions);\n const compositeScore = url.totalScore * stats.consistencyMultiplier;\n return { url, compositeScore, stats };\n });\n\n // Sort by composite score descending\n scored.sort((a, b) => b.compositeScore - a.compositeScore);\n\n // Find max for normalization\n const maxScore = scored[0]!.compositeScore;\n\n // Map to ranked URLs with all signals\n return scored.map(({ url, compositeScore, stats }, index) => ({\n url: url.url,\n title: url.title,\n snippet: url.snippet,\n allSnippets: url.allSnippets,\n rank: index + 1,\n score: maxScore > 0 ? (compositeScore / maxScore) * 100 : 0,\n frequency: url.frequency,\n positions: url.positions,\n queries: url.queries,\n bestPosition: url.bestPosition,\n isConsensus: url.frequency >= consensusThreshold,\n coverageRatio: totalQueries > 0 ? url.frequency / totalQueries : 0,\n positionStdDev: stats.stdDev,\n consistencyMultiplier: stats.consistencyMultiplier,\n }));\n}\n\n/** Maximum queries to show in the coverage table before collapsing */\nconst COVERAGE_TABLE_MAX_ROWS = 20 as const;\n\n/**\n * Consistency label based on position standard deviation\n */\nfunction consistencyLabel(stdDev: number, frequency: number): string {\n if (frequency <= 1) return 'n/a';\n if (stdDev < 1.5) return 'high';\n if (stdDev < 3.5) return 'medium';\n return 'variable';\n}\n\n/**\n * Generate a unified output where every URL appears exactly once.\n * Replaces the old generateEnhancedOutput + per-query section combo.\n */\nexport function generateUnifiedOutput(\n rankedUrls: RankedUrl[],\n allQueries: string[],\n queryResults: QuerySearchResult[],\n totalUniqueUrls: number,\n frequencyThreshold: number,\n thresholdNote?: string,\n verbose: boolean = false,\n): string {\n const lines: string[] = [];\n const consensusCount = rankedUrls.filter(u => u.isConsensus).length;\n\n // Header\n lines.push(`## Web Search Results (${allQueries.length} queries, ${totalUniqueUrls} unique URLs)`);\n lines.push('');\n if (thresholdNote) {\n lines.push(`> ${thresholdNote}`);\n lines.push('');\n }\n\n // Ranked URL list \u2014 every URL exactly once.\n //\n // Per-row metadata is gated:\n // - CONSENSUS labels only appear when the effective threshold is >1 (a\n // threshold of 1 means *every* row gets the label, so it carries no\n // signal). See: docs/code-review/context/02-current-tool-surface.md.\n // - The Score/Seen/Consistency line is suppressed for rows that were\n // seen in exactly one query in a multi-query call (Seen=1/N is common\n // and Consistency is always \"n/a\" in that case).\n // - Verbose mode restores both for callers that explicitly want them.\n const consensusActive = frequencyThreshold > 1;\n\n for (const url of rankedUrls) {\n const consensusTag = consensusActive && url.frequency >= HIGH_CONSENSUS_THRESHOLD\n ? ' CONSENSUS+++'\n : consensusActive && url.isConsensus\n ? ' CONSENSUS'\n : '';\n const coveragePct = Math.round(url.coverageRatio * 100);\n const consistency = consistencyLabel(url.positionStdDev, url.frequency);\n\n lines.push(`**${url.rank}. [${url.title}](${url.url})**${consensusTag}`);\n\n const showRowMetadata = verbose\n || (allQueries.length > 1 && url.frequency > 1)\n || allQueries.length === 1;\n if (showRowMetadata) {\n const parts = [\n `Score: ${url.score.toFixed(1)}`,\n `Seen in: ${url.frequency}/${allQueries.length} queries (${coveragePct}%)`,\n `Best pos: #${url.bestPosition}`,\n ];\n if (url.frequency > 1) {\n parts.push(`Consistency: ${consistency}`);\n }\n lines.push(parts.join(' | '));\n }\n if (url.queries.length > 1 || verbose) {\n lines.push(`Queries: ${url.queries.map(q => `\"${q}\"`).join(', ')}`);\n }\n lines.push(`> ${url.snippet}`);\n\n // Alt snippets (if multiple distinct snippets were collected)\n if (url.allSnippets.length > 1) {\n const alts = url.allSnippets\n .filter(s => s !== url.snippet)\n .slice(0, 3)\n .map(s => s.length > 100 ? s.slice(0, 97) + '...' : s);\n if (alts.length > 0) {\n lines.push(`Alt: ${alts.map(s => `\"${s}\"`).join(' | ')}`);\n }\n }\n\n lines.push('');\n }\n\n // Keyword coverage section\n lines.push('---');\n\n if (allQueries.length <= COVERAGE_TABLE_MAX_ROWS) {\n // Full table for \u226420 queries\n lines.push('### Query Coverage');\n lines.push('| Query | Results | Top URL | Top Pos |');\n lines.push('|---------|---------|---------|---------|');\n\n for (const search of queryResults) {\n const topResult = search.results[0];\n let topDomain = '';\n if (topResult) {\n try {\n topDomain = new URL(topResult.link).hostname.replace(/^www\\./, '');\n } catch {\n topDomain = topResult.link;\n }\n }\n lines.push(`| \"${search.query}\" | ${search.results.length} | ${topDomain || '\u2014'} | ${topResult ? `#${topResult.position}` : '\u2014'} |`);\n }\n lines.push('');\n } else {\n // Collapsed summary for >20 queries\n const goodCount = queryResults.filter(s => s.results.length >= 3).length;\n lines.push(`### Query Coverage: ${goodCount}/${allQueries.length} queries returned 3+ results`);\n lines.push('');\n }\n\n // Low-yield queries\n const lowYield = queryResults.filter(s => s.results.length <= 1);\n if (lowYield.length > 0) {\n lines.push(`**Low-yield queries** (0-1 results): ${lowYield.map(s => `\\`${s.query}\\``).join(', ')}`);\n lines.push('');\n }\n\n // Related searches (merged and deduplicated)\n const allRelated = new Set<string>();\n for (const search of queryResults) {\n if (search.related) {\n for (const r of search.related) {\n allRelated.add(r);\n }\n }\n }\n if (allRelated.size > 0) {\n const related = [...allRelated].slice(0, 10);\n lines.push(`**Related searches:** ${related.map(r => `\\`${r}\\``).join(', ')}`);\n lines.push('');\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Full aggregation pipeline \u2014 returns ALL URLs ranked by CTR score.\n * Determines a consensus threshold (\u22653, \u22652, or \u22651) for labeling, but never\n * drops URLs below the threshold. Every collected URL appears in the output.\n */\nexport function aggregateAndRank(\n searches: QuerySearchResult[],\n minConsensusUrls: number = DEFAULT_MIN_CONSENSUS_URLS\n): AggregationResult {\n const urlMap = aggregateResults(searches);\n const totalUniqueUrls = urlMap.size;\n const totalQueries = searches.length;\n\n // Determine consensus threshold for labeling (not filtering)\n const thresholds = [3, 2, 1];\n let usedThreshold = 1;\n let thresholdNote: string | undefined;\n\n for (const threshold of thresholds) {\n const count = countByFrequency(urlMap, threshold);\n if (count >= minConsensusUrls || threshold === 1) {\n usedThreshold = threshold;\n if (threshold < 3) {\n thresholdNote = `Note: Consensus threshold set to \u2265${threshold} due to result diversity.`;\n }\n break;\n }\n }\n\n // Rank ALL URLs, marking consensus based on determined threshold\n const allUrls = [...urlMap.values()];\n const rankedUrls = calculateWeightedScores(allUrls, usedThreshold, totalQueries);\n\n return {\n rankedUrls,\n totalUniqueUrls,\n totalQueries,\n frequencyThreshold: usedThreshold,\n thresholdNote,\n };\n}\n\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/src/version.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
+
// src/version.ts
|
|
1
2
|
import { createRequire } from "module";
|
|
2
3
|
import { fileURLToPath } from "url";
|
|
3
4
|
import { dirname, join } from "path";
|
|
4
|
-
|
|
5
|
+
var DEFAULT_PACKAGE_INFO = {
|
|
5
6
|
version: "3.9.5",
|
|
6
7
|
name: "mcp-researchpowerpack-http",
|
|
7
8
|
description: "Research Powerpack MCP Server"
|
|
8
9
|
};
|
|
9
|
-
|
|
10
|
+
var packageJson = { ...DEFAULT_PACKAGE_INFO };
|
|
10
11
|
try {
|
|
11
12
|
if (typeof import.meta.url === "string" && import.meta.url.startsWith("file:")) {
|
|
12
13
|
const _require = createRequire(import.meta.url);
|
|
@@ -19,10 +20,10 @@ try {
|
|
|
19
20
|
}
|
|
20
21
|
} catch {
|
|
21
22
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
var VERSION = packageJson.version;
|
|
24
|
+
var PACKAGE_NAME = packageJson.name;
|
|
25
|
+
var PACKAGE_DESCRIPTION = packageJson.description;
|
|
26
|
+
var USER_AGENT_VERSION = `${PACKAGE_NAME}/${VERSION}`;
|
|
26
27
|
export {
|
|
27
28
|
PACKAGE_DESCRIPTION,
|
|
28
29
|
PACKAGE_NAME,
|
package/dist/src/version.js.map
CHANGED
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/version.ts"],
|
|
4
4
|
"sourcesContent": ["/**\n * Version Module - Single Source of Truth\n * \n * This module reads package metadata from package.json at runtime and keeps a\n * fallback copy for environments where package.json cannot be resolved.\n * \n * Usage:\n * import { VERSION, PACKAGE_NAME } from './version.js';\n */\n\nimport { createRequire } from 'module';\nimport { fileURLToPath } from 'url';\nimport { dirname, join } from 'path';\n\n// Defaults used if package.json cannot be loaded at runtime.\nconst DEFAULT_PACKAGE_INFO = {\n version: '3.9.5',\n name: 'mcp-researchpowerpack-http',\n description: 'Research Powerpack MCP Server',\n} as const;\n\nlet packageJson: { version: string; name: string; description: string } = { ...DEFAULT_PACKAGE_INFO };\n\ntry {\n if (typeof import.meta.url === 'string' && import.meta.url.startsWith('file:')) {\n const _require = createRequire(import.meta.url);\n const _dirname = dirname(fileURLToPath(import.meta.url));\n try {\n packageJson = _require(join(_dirname, '..', 'package.json'));\n } catch {\n packageJson = _require(join(_dirname, '..', '..', 'package.json'));\n }\n }\n} catch {\n // Keep hardcoded defaults when package.json is unavailable\n}\n\n/**\n * Package version from package.json\n * This is the single source of truth for versioning\n */\nexport const VERSION: string = packageJson.version;\n\n/**\n * Package name from package.json\n */\nexport const PACKAGE_NAME: string = packageJson.name;\n\n/**\n * Package description from package.json\n */\nexport const PACKAGE_DESCRIPTION: string = packageJson.description;\n\n/**\n * Formatted version string for user agents and logging\n * Example: \"mcp-researchpowerpack-http/3.2.0\"\n */\nexport const USER_AGENT_VERSION: string = `${PACKAGE_NAME}/${VERSION}`;\n\n// VERSION_INFO removed - unused, individual exports sufficient\n"],
|
|
5
|
-
"mappings": "AAUA,SAAS,qBAAqB;AAC9B,SAAS,qBAAqB;AAC9B,SAAS,SAAS,YAAY;AAG9B,
|
|
5
|
+
"mappings": ";AAUA,SAAS,qBAAqB;AAC9B,SAAS,qBAAqB;AAC9B,SAAS,SAAS,YAAY;AAG9B,IAAM,uBAAuB;AAAA,EAC3B,SAAS;AAAA,EACT,MAAM;AAAA,EACN,aAAa;AACf;AAEA,IAAI,cAAsE,EAAE,GAAG,qBAAqB;AAEpG,IAAI;AACF,MAAI,OAAO,YAAY,QAAQ,YAAY,YAAY,IAAI,WAAW,OAAO,GAAG;AAC9E,UAAM,WAAW,cAAc,YAAY,GAAG;AAC9C,UAAM,WAAW,QAAQ,cAAc,YAAY,GAAG,CAAC;AACvD,QAAI;AACF,oBAAc,SAAS,KAAK,UAAU,MAAM,cAAc,CAAC;AAAA,IAC7D,QAAQ;AACN,oBAAc,SAAS,KAAK,UAAU,MAAM,MAAM,cAAc,CAAC;AAAA,IACnE;AAAA,EACF;AACF,QAAQ;AAER;AAMO,IAAM,UAAkB,YAAY;AAKpC,IAAM,eAAuB,YAAY;AAKzC,IAAM,sBAA8B,YAAY;AAMhD,IAAM,qBAA6B,GAAG,YAAY,IAAI,OAAO;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-researchpowerpack",
|
|
3
|
-
"version": "7.0.
|
|
3
|
+
"version": "7.0.13",
|
|
4
4
|
"description": "HTTP-first MCP research server: start-research plus raw/smart search and scrape tools — built on mcp-use.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"url": "https://github.com/yigitkonur/mcp-researchpowerpack/issues"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@mcp-use/cli": "
|
|
42
|
+
"@mcp-use/cli": "3.1.2",
|
|
43
43
|
"@mozilla/readability": "^0.6.0",
|
|
44
44
|
"@onkernel/sdk": "^0.52.0",
|
|
45
45
|
"@types/jsdom": "^28.0.1",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"@types/turndown": "^5.0.6",
|
|
48
48
|
"effect": "^3.21.2",
|
|
49
49
|
"jsdom": "^29.0.2",
|
|
50
|
-
"mcp-use": "1.
|
|
50
|
+
"mcp-use": "1.26.0",
|
|
51
51
|
"openai": "^4.77.0",
|
|
52
52
|
"p-map": "^7.0.3",
|
|
53
53
|
"react": "^19.0.0",
|