freshcontext-mcp 0.3.16 → 0.3.18
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/.env.example +3 -0
- package/LICENSE +21 -0
- package/NOTICE.md +17 -0
- package/README.md +395 -296
- package/SECURITY.md +34 -0
- package/TRADEMARKS.md +9 -0
- package/dist/adapters/arxiv.js +92 -48
- package/dist/adapters/finance.js +87 -101
- package/dist/adapters/gdelt.js +1 -1
- package/dist/adapters/gebiz.js +1 -1
- package/dist/adapters/hackernews.js +59 -29
- package/dist/adapters/productHunt.js +8 -4
- package/dist/adapters/registry.js +232 -0
- package/dist/adapters/repoSearch.js +1 -1
- package/dist/adapters/secFilings.js +1 -1
- package/dist/core/decay.js +61 -0
- package/dist/core/decision.js +176 -0
- package/dist/core/envelope.js +59 -0
- package/dist/core/explain.js +28 -0
- package/dist/core/guards.js +17 -0
- package/dist/core/index.js +11 -0
- package/dist/core/pipeline.js +101 -0
- package/dist/core/provenance.js +73 -0
- package/dist/core/rank.js +84 -0
- package/dist/core/signal.js +101 -0
- package/dist/core/sourceProfiles.js +126 -0
- package/dist/core/types.js +1 -0
- package/dist/core/utility.js +90 -0
- package/dist/rest/handler.js +126 -0
- package/dist/security.js +1 -1
- package/dist/server.js +10 -10
- package/dist/tools/freshnessStamp.js +1 -117
- package/dist/types.js +0 -1
- package/docs/API_DESIGN.md +434 -0
- package/docs/CODEX_MCP_USAGE.md +116 -0
- package/docs/CORE_API.md +224 -0
- package/docs/DEPENDENCY_DILIGENCE.md +63 -0
- package/docs/HA_PRI_V2_DESIGN.md +279 -0
- package/docs/OPERATIONAL_DEMO_RUNBOOK.md +458 -0
- package/docs/RELEASE_INTEGRITY.md +53 -0
- package/docs/RELEASE_NOTES.md +38 -0
- package/docs/SIGNAL_CONTRACT.md +89 -0
- package/docs/SOURCE_PROFILES.md +427 -0
- package/freshcontext.schema.json +103 -103
- package/package-script-guard.mjs +140 -0
- package/package.json +92 -52
- package/server.json +27 -28
- package/.github/workflows/publish.yml +0 -32
- package/RESEARCH.md +0 -487
- package/RISKS.md +0 -137
- package/cleanup.ps1 +0 -99
- package/demo/README.md +0 -70
- package/demo/data.json +0 -88
- package/demo/generate.mjs +0 -199
- package/demo/index.html +0 -513
- package/demo/logo-export.html +0 -61
- package/demo/logo.svg +0 -23
- package/dist/apify.js +0 -133
- package/freshcontext-validate.js +0 -196
- package/time-check.ps1 +0 -46
package/SECURITY.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
FreshContext currently supports the active `freshcontext-mcp@0.3.x` package line.
|
|
6
|
+
|
|
7
|
+
Please use the latest published `0.3.x` release when reporting a vulnerability, and include the exact package version, repository, transport, and environment involved.
|
|
8
|
+
|
|
9
|
+
## Reporting A Vulnerability
|
|
10
|
+
|
|
11
|
+
FreshContext accepts responsible security reports by email:
|
|
12
|
+
|
|
13
|
+
- gimmanuel73@gmail.com
|
|
14
|
+
|
|
15
|
+
Please do not post secrets, tokens, private logs, customer data, exploit payloads, or sensitive operational details in public GitHub issues.
|
|
16
|
+
|
|
17
|
+
For a useful report, include:
|
|
18
|
+
|
|
19
|
+
- affected repository or package
|
|
20
|
+
- affected version or commit
|
|
21
|
+
- reproduction steps
|
|
22
|
+
- expected and observed behavior
|
|
23
|
+
- security impact
|
|
24
|
+
- whether the issue affects local MCP usage, hosted Worker usage, examples, docs, packaging, or another surface
|
|
25
|
+
|
|
26
|
+
Public GitHub issues are fine for non-sensitive bugs, documentation mistakes, stale claims, build failures, and feature requests.
|
|
27
|
+
|
|
28
|
+
## Scope Notes
|
|
29
|
+
|
|
30
|
+
FreshContext does not currently offer a formal bug bounty program.
|
|
31
|
+
|
|
32
|
+
Please do not send live production tokens, private Cloudflare logs, npm tokens, GitHub tokens, MCP registry tokens, customer data, or private account data. If a report requires sensitive evidence, describe the issue first by email so a safer exchange path can be agreed.
|
|
33
|
+
|
|
34
|
+
This policy does not make claims of certification, compliance, guaranteed response time, or security warranty.
|
package/TRADEMARKS.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# FreshContext Brand and Trademark Notice
|
|
2
|
+
|
|
3
|
+
FreshContext is the product and project name used by the project owner for this software, documentation, examples, and related public materials.
|
|
4
|
+
|
|
5
|
+
No mark registration claim is made in this repository. Any future trademark filing, transfer, assignment, or licensing question should be reviewed separately.
|
|
6
|
+
|
|
7
|
+
Third-party names, marks, services, and platforms, including Cloudflare, npm, GitHub, Model Context Protocol, MCP, Apify, Claude, OpenAI, Anthropic, and other referenced ecosystems, belong to their respective owners.
|
|
8
|
+
|
|
9
|
+
Use of third-party names in this repository is descriptive, interoperability-oriented, or reference-oriented. It does not imply endorsement, certification, sponsorship, partnership, or official affiliation unless explicitly stated by the relevant third party.
|
package/dist/adapters/arxiv.js
CHANGED
|
@@ -1,66 +1,110 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
?
|
|
11
|
-
|
|
1
|
+
const USER_AGENT = "freshcontext-mcp/0.1.7 (https://github.com/PrinceGabriel-lgtm/freshcontext-mcp)";
|
|
2
|
+
const DEFAULT_ARXIV_SIGNAL_SCORE = 0.8;
|
|
3
|
+
function buildArxivApiUrl(input, maxResults = 10) {
|
|
4
|
+
const trimmed = input.trim();
|
|
5
|
+
const safeMaxResults = Number.isFinite(maxResults)
|
|
6
|
+
? Math.max(1, Math.min(Math.trunc(maxResults), 50))
|
|
7
|
+
: 10;
|
|
8
|
+
return trimmed.startsWith("http")
|
|
9
|
+
? trimmed
|
|
10
|
+
: `https://export.arxiv.org/api/query?search_query=all:${encodeURIComponent(trimmed)}&start=0&max_results=${safeMaxResults}&sortBy=relevance&sortOrder=descending`;
|
|
11
|
+
}
|
|
12
|
+
async function fetchArxivXml(apiUrl) {
|
|
12
13
|
const res = await fetch(apiUrl, {
|
|
13
|
-
headers: { "User-Agent":
|
|
14
|
+
headers: { "User-Agent": USER_AGENT },
|
|
14
15
|
});
|
|
15
16
|
if (!res.ok)
|
|
16
17
|
throw new Error(`arXiv API error: ${res.status} ${res.statusText}`);
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
18
|
+
return res.text();
|
|
19
|
+
}
|
|
20
|
+
function getTag(block, tag) {
|
|
21
|
+
const m = block.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`, "i"));
|
|
22
|
+
return m ? m[1].trim().replace(/\s+/g, " ") : "";
|
|
23
|
+
}
|
|
24
|
+
function getAttr(block, tag, attr) {
|
|
25
|
+
const m = block.match(new RegExp(`<${tag}[^>]*${attr}="([^"]*)"`, "i"));
|
|
26
|
+
return m ? m[1].trim() : "";
|
|
27
|
+
}
|
|
28
|
+
function normalizeArxivUrl(id) {
|
|
29
|
+
return id.replace("http://arxiv.org/abs/", "https://arxiv.org/abs/");
|
|
30
|
+
}
|
|
31
|
+
function parseArxivEntries(xml) {
|
|
32
|
+
return [...xml.matchAll(/<entry>([\s\S]*?)<\/entry>/g)].map((match) => {
|
|
32
33
|
const block = match[1];
|
|
33
|
-
const title = getTag(block, "title").replace(/\n/g, " ");
|
|
34
|
-
const summary = getTag(block, "summary").slice(0, 300).replace(/\n/g, " ");
|
|
35
|
-
const published = getTag(block, "published").slice(0, 10); // YYYY-MM-DD
|
|
36
|
-
const updated = getTag(block, "updated").slice(0, 10);
|
|
37
|
-
const id = getTag(block, "id").replace("http://arxiv.org/abs/", "https://arxiv.org/abs/");
|
|
38
|
-
// Authors — can be multiple
|
|
39
34
|
const authorMatches = [...block.matchAll(/<author>([\s\S]*?)<\/author>/g)];
|
|
40
35
|
const authors = authorMatches
|
|
41
36
|
.map(a => getTag(a[1], "name"))
|
|
42
37
|
.filter(Boolean)
|
|
43
|
-
.slice(0, 4)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
`Link: ${id}`,
|
|
55
|
-
].filter(Boolean).join("\n");
|
|
38
|
+
.slice(0, 4);
|
|
39
|
+
return {
|
|
40
|
+
title: getTag(block, "title").replace(/\n/g, " "),
|
|
41
|
+
summary: getTag(block, "summary").replace(/\n/g, " "),
|
|
42
|
+
published: getTag(block, "published"),
|
|
43
|
+
updated: getTag(block, "updated"),
|
|
44
|
+
id: normalizeArxivUrl(getTag(block, "id")),
|
|
45
|
+
authors,
|
|
46
|
+
category: getAttr(block, "arxiv:primary_category", "term") ||
|
|
47
|
+
getAttr(block, "category", "term"),
|
|
48
|
+
};
|
|
56
49
|
});
|
|
50
|
+
}
|
|
51
|
+
function formatArxivEntry(entry, index) {
|
|
52
|
+
const published = entry.published.slice(0, 10);
|
|
53
|
+
const updated = entry.updated.slice(0, 10);
|
|
54
|
+
const authors = entry.authors.join(", ");
|
|
55
|
+
const summary = entry.summary.slice(0, 300);
|
|
56
|
+
return [
|
|
57
|
+
`[${index + 1}] ${entry.title}`,
|
|
58
|
+
`Authors: ${authors || "Unknown"}`,
|
|
59
|
+
`Published: ${published}${updated !== published ? ` (updated ${updated})` : ""}`,
|
|
60
|
+
entry.category ? `Category: ${entry.category}` : null,
|
|
61
|
+
`Abstract: ${summary}\u00e2\u20ac\u00a6`,
|
|
62
|
+
`Link: ${entry.id}`,
|
|
63
|
+
].filter(Boolean).join("\n");
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* arXiv adapter uses the official arXiv API.
|
|
67
|
+
* Accepts a search query or a direct arXiv API URL.
|
|
68
|
+
* Docs: https://arxiv.org/help/api/user-manual
|
|
69
|
+
*/
|
|
70
|
+
export async function arxivAdapter(options) {
|
|
71
|
+
const input = options.url.trim();
|
|
72
|
+
const apiUrl = buildArxivApiUrl(input);
|
|
73
|
+
const xml = await fetchArxivXml(apiUrl);
|
|
74
|
+
const entries = parseArxivEntries(xml);
|
|
75
|
+
if (!entries.length) {
|
|
76
|
+
return { raw: "No results found for this query.", content_date: null, freshness_confidence: "low" };
|
|
77
|
+
}
|
|
78
|
+
const papers = entries.map(formatArxivEntry);
|
|
57
79
|
const raw = papers.join("\n\n").slice(0, options.maxLength ?? 6000);
|
|
58
|
-
// Most recent publication date
|
|
59
80
|
const dates = entries
|
|
60
|
-
.map(
|
|
81
|
+
.map(entry => entry.published.slice(0, 10))
|
|
61
82
|
.filter(Boolean)
|
|
62
83
|
.sort()
|
|
63
84
|
.reverse();
|
|
64
85
|
const content_date = dates[0] ?? null;
|
|
65
86
|
return { raw, content_date, freshness_confidence: content_date ? "high" : "medium" };
|
|
66
87
|
}
|
|
88
|
+
export async function searchArxivSignals(input) {
|
|
89
|
+
const query = input.query.trim();
|
|
90
|
+
const apiUrl = buildArxivApiUrl(query, input.maxResults);
|
|
91
|
+
const xml = await fetchArxivXml(apiUrl);
|
|
92
|
+
const entries = parseArxivEntries(xml);
|
|
93
|
+
const retrievedAt = input.retrievedAt ?? new Date().toISOString();
|
|
94
|
+
const semanticScore = input.semanticScore ?? DEFAULT_ARXIV_SIGNAL_SCORE;
|
|
95
|
+
return entries.map((entry) => ({
|
|
96
|
+
title: entry.title,
|
|
97
|
+
content: entry.summary,
|
|
98
|
+
source: entry.id,
|
|
99
|
+
source_type: "arxiv",
|
|
100
|
+
published_at: entry.published || null,
|
|
101
|
+
retrieved_at: retrievedAt,
|
|
102
|
+
semantic_score: semanticScore,
|
|
103
|
+
metadata: {
|
|
104
|
+
authors: entry.authors,
|
|
105
|
+
category: entry.category || null,
|
|
106
|
+
updated_at: entry.updated || null,
|
|
107
|
+
query,
|
|
108
|
+
},
|
|
109
|
+
}));
|
|
110
|
+
}
|
package/dist/adapters/finance.js
CHANGED
|
@@ -1,117 +1,103 @@
|
|
|
1
|
-
function
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
if (cap >= 1e6)
|
|
9
|
-
return `$${(cap / 1e6).toFixed(2)}M`;
|
|
10
|
-
return `$${cap.toLocaleString()}`;
|
|
1
|
+
function toStooqSymbol(ticker) {
|
|
2
|
+
const clean = ticker.trim().toUpperCase().replace(/[^A-Z0-9.^=-]/g, "");
|
|
3
|
+
if (!clean)
|
|
4
|
+
throw new Error("Ticker cannot be empty");
|
|
5
|
+
if (clean.includes(".") || clean.startsWith("^") || clean.includes("="))
|
|
6
|
+
return clean;
|
|
7
|
+
return `${clean}.US`;
|
|
11
8
|
}
|
|
12
|
-
function
|
|
13
|
-
if (
|
|
14
|
-
return
|
|
15
|
-
const
|
|
16
|
-
return
|
|
9
|
+
function toNumber(value) {
|
|
10
|
+
if (value === undefined || value === "N/D")
|
|
11
|
+
return null;
|
|
12
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
13
|
+
return Number.isFinite(n) ? n : null;
|
|
14
|
+
}
|
|
15
|
+
function normalizeQuoteTimestamp(date, time) {
|
|
16
|
+
if (!date || date === "N/D")
|
|
17
|
+
throw new Error("Quote date unavailable");
|
|
18
|
+
const clock = time && time !== "N/D" ? time : "00:00:00";
|
|
19
|
+
const parsed = new Date(`${date}T${clock}Z`);
|
|
20
|
+
if (Number.isNaN(parsed.getTime()))
|
|
21
|
+
throw new Error(`Invalid quote timestamp: ${date} ${time}`);
|
|
22
|
+
return parsed.toISOString();
|
|
23
|
+
}
|
|
24
|
+
function formatNumber(value, prefix = "") {
|
|
25
|
+
const n = toNumber(value);
|
|
26
|
+
return n === null ? "N/A" : `${prefix}${n.toLocaleString(undefined, { maximumFractionDigits: 4 })}`;
|
|
27
|
+
}
|
|
28
|
+
async function fetchStooqQuote(ticker) {
|
|
29
|
+
const stooqSymbol = toStooqSymbol(ticker);
|
|
30
|
+
const url = `https://stooq.com/q/l/?s=${encodeURIComponent(stooqSymbol.toLowerCase())}&f=sd2t2ohlcv&h&e=json`;
|
|
31
|
+
const res = await fetch(url, {
|
|
32
|
+
headers: {
|
|
33
|
+
"User-Agent": "freshcontext-mcp/0.3.17",
|
|
34
|
+
"Accept": "application/json",
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
if (!res.ok)
|
|
38
|
+
throw new Error(`Stooq quote API error: ${res.status}`);
|
|
39
|
+
const data = await res.json();
|
|
40
|
+
const quote = data.symbols?.[0];
|
|
41
|
+
if (!quote)
|
|
42
|
+
throw new Error(`No quote returned for ${ticker}`);
|
|
43
|
+
if (quote.close === "N/D" || quote.date === "N/D") {
|
|
44
|
+
throw new Error(`No Stooq quote data found for ${ticker}`);
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
requested: ticker,
|
|
48
|
+
stooqSymbol,
|
|
49
|
+
quote,
|
|
50
|
+
timestamp: normalizeQuoteTimestamp(quote.date, quote.time),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function formatQuote(result) {
|
|
54
|
+
const q = result.quote;
|
|
55
|
+
return [
|
|
56
|
+
`${result.requested.toUpperCase()} — ${q.symbol}`,
|
|
57
|
+
`source: stooq`,
|
|
58
|
+
`Quote timestamp: ${result.timestamp}`,
|
|
59
|
+
"",
|
|
60
|
+
`Close: ${formatNumber(q.close, "$")}`,
|
|
61
|
+
`Open: ${formatNumber(q.open, "$")}`,
|
|
62
|
+
`High: ${formatNumber(q.high, "$")}`,
|
|
63
|
+
`Low: ${formatNumber(q.low, "$")}`,
|
|
64
|
+
`Volume: ${formatNumber(q.volume)}`,
|
|
65
|
+
].join("\n");
|
|
17
66
|
}
|
|
18
67
|
export async function financeAdapter(options) {
|
|
19
68
|
const input = options.url.trim();
|
|
20
|
-
// Support comma-separated tickers
|
|
21
69
|
const rawTickers = input
|
|
22
70
|
.split(",")
|
|
23
|
-
.map((t) => t.trim()
|
|
71
|
+
.map((t) => t.trim())
|
|
24
72
|
.filter(Boolean)
|
|
25
|
-
.slice(0, 5);
|
|
26
|
-
|
|
27
|
-
|
|
73
|
+
.slice(0, 5);
|
|
74
|
+
if (!rawTickers.length)
|
|
75
|
+
throw new Error("At least one ticker is required");
|
|
76
|
+
const successes = [];
|
|
77
|
+
const failures = [];
|
|
28
78
|
for (const ticker of rawTickers) {
|
|
29
79
|
try {
|
|
30
|
-
|
|
31
|
-
if (quoteData) {
|
|
32
|
-
results.push(formatQuote(quoteData));
|
|
33
|
-
if (quoteData.regularMarketTime) {
|
|
34
|
-
latestTimestamp = Math.max(latestTimestamp ?? 0, quoteData.regularMarketTime);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
80
|
+
successes.push(await fetchStooqQuote(ticker));
|
|
37
81
|
}
|
|
38
82
|
catch (err) {
|
|
39
|
-
|
|
83
|
+
failures.push(`[${ticker.toUpperCase()}] ${err instanceof Error ? err.message : String(err)}`);
|
|
40
84
|
}
|
|
41
85
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
? new Date(latestTimestamp * 1000).toISOString()
|
|
45
|
-
: new Date().toISOString();
|
|
46
|
-
return { raw, content_date, freshness_confidence: "high" };
|
|
47
|
-
}
|
|
48
|
-
async function fetchQuote(ticker) {
|
|
49
|
-
// v7 quote endpoint — public, no auth
|
|
50
|
-
const quoteUrl = `https://query1.finance.yahoo.com/v7/finance/quote?symbols=${encodeURIComponent(ticker)}&fields=shortName,longName,regularMarketPrice,regularMarketChange,regularMarketChangePercent,marketCap,regularMarketVolume,fiftyTwoWeekHigh,fiftyTwoWeekLow,trailingPE,dividendYield,currency,exchangeName,regularMarketTime`;
|
|
51
|
-
const quoteRes = await fetch(quoteUrl, {
|
|
52
|
-
headers: {
|
|
53
|
-
"User-Agent": "Mozilla/5.0 (compatible; freshcontext-mcp/0.1.5)",
|
|
54
|
-
"Accept": "application/json",
|
|
55
|
-
},
|
|
56
|
-
});
|
|
57
|
-
if (!quoteRes.ok)
|
|
58
|
-
throw new Error(`Yahoo Finance API error: ${quoteRes.status}`);
|
|
59
|
-
const quoteJson = await quoteRes.json();
|
|
60
|
-
const quote = quoteJson?.quoteResponse?.result?.[0];
|
|
61
|
-
if (!quote)
|
|
62
|
-
throw new Error(`No data found for ticker: ${ticker}`);
|
|
63
|
-
// Optionally fetch company summary (v11 quoteSummary)
|
|
64
|
-
try {
|
|
65
|
-
const summaryUrl = `https://query1.finance.yahoo.com/v11/finance/quoteSummary/${encodeURIComponent(ticker)}?modules=assetProfile`;
|
|
66
|
-
const summaryRes = await fetch(summaryUrl, {
|
|
67
|
-
headers: { "User-Agent": "Mozilla/5.0 (compatible; freshcontext-mcp/0.1.5)" },
|
|
68
|
-
});
|
|
69
|
-
if (summaryRes.ok) {
|
|
70
|
-
const summaryJson = await summaryRes.json();
|
|
71
|
-
const profile = summaryJson?.quoteSummary?.result?.[0]?.assetProfile;
|
|
72
|
-
if (profile) {
|
|
73
|
-
Object.assign(quote, {
|
|
74
|
-
longBusinessSummary: profile.longBusinessSummary,
|
|
75
|
-
sector: profile.sector,
|
|
76
|
-
industry: profile.industry,
|
|
77
|
-
fullTimeEmployees: profile.fullTimeEmployees,
|
|
78
|
-
website: profile.website,
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
catch {
|
|
84
|
-
// Summary is optional — continue without it
|
|
85
|
-
}
|
|
86
|
-
return quote;
|
|
87
|
-
}
|
|
88
|
-
function formatQuote(q) {
|
|
89
|
-
const lines = [
|
|
90
|
-
`${q.symbol} — ${q.longName ?? q.shortName ?? "Unknown"}`,
|
|
91
|
-
`Exchange: ${q.exchangeName ?? "N/A"} · Currency: ${q.currency ?? "USD"}`,
|
|
92
|
-
"",
|
|
93
|
-
`Price: ${q.regularMarketPrice !== undefined ? `$${q.regularMarketPrice.toFixed(2)}` : "N/A"}`,
|
|
94
|
-
`Change: ${formatChange(q.regularMarketChange, q.regularMarketChangePercent)}`,
|
|
95
|
-
`Market Cap: ${formatMarketCap(q.marketCap)}`,
|
|
96
|
-
`Volume: ${q.regularMarketVolume?.toLocaleString() ?? "N/A"}`,
|
|
97
|
-
`52w High: ${q.fiftyTwoWeekHigh !== undefined ? `$${q.fiftyTwoWeekHigh.toFixed(2)}` : "N/A"}`,
|
|
98
|
-
`52w Low: ${q.fiftyTwoWeekLow !== undefined ? `$${q.fiftyTwoWeekLow.toFixed(2)}` : "N/A"}`,
|
|
99
|
-
`P/E Ratio: ${q.trailingPE !== undefined ? q.trailingPE.toFixed(2) : "N/A"}`,
|
|
100
|
-
`Div Yield: ${q.dividendYield !== undefined ? `${(q.dividendYield * 100).toFixed(2)}%` : "N/A"}`,
|
|
101
|
-
];
|
|
102
|
-
if (q.sector || q.industry) {
|
|
103
|
-
lines.push("");
|
|
104
|
-
if (q.sector)
|
|
105
|
-
lines.push(`Sector: ${q.sector}`);
|
|
106
|
-
if (q.industry)
|
|
107
|
-
lines.push(`Industry: ${q.industry}`);
|
|
108
|
-
if (q.fullTimeEmployees)
|
|
109
|
-
lines.push(`Employees: ${q.fullTimeEmployees.toLocaleString()}`);
|
|
110
|
-
if (q.website)
|
|
111
|
-
lines.push(`Website: ${q.website}`);
|
|
86
|
+
if (!successes.length) {
|
|
87
|
+
throw new Error(`Finance quote lookup failed for all tickers via source=stooq. ${failures.join("; ")}`);
|
|
112
88
|
}
|
|
113
|
-
|
|
114
|
-
|
|
89
|
+
const sections = successes.map(formatQuote);
|
|
90
|
+
if (failures.length) {
|
|
91
|
+
sections.push(["Partial failures:", ...failures.map((f) => `- ${f}`)].join("\n"));
|
|
115
92
|
}
|
|
116
|
-
|
|
93
|
+
const raw = sections.join("\n\n-----------------------------\n\n").slice(0, options.maxLength ?? 5000);
|
|
94
|
+
const content_date = successes
|
|
95
|
+
.map((s) => s.timestamp)
|
|
96
|
+
.sort()
|
|
97
|
+
.reverse()[0] ?? null;
|
|
98
|
+
return {
|
|
99
|
+
raw,
|
|
100
|
+
content_date,
|
|
101
|
+
freshness_confidence: failures.length ? "medium" : "high",
|
|
102
|
+
};
|
|
117
103
|
}
|
package/dist/adapters/gdelt.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
const HEADERS = {
|
|
13
13
|
"Accept": "application/json",
|
|
14
|
-
"User-Agent": "freshcontext-mcp/
|
|
14
|
+
"User-Agent": "freshcontext-mcp/0.3.17 (https://github.com/PrinceGabriel-lgtm/freshcontext-mcp)",
|
|
15
15
|
};
|
|
16
16
|
function parseGdeltDate(raw) {
|
|
17
17
|
if (!raw)
|
package/dist/adapters/gebiz.js
CHANGED
|
@@ -20,7 +20,7 @@ const DATASET_ID = "d_acde1106003906a75c3fa052592f2fcb";
|
|
|
20
20
|
const BASE_URL = "https://data.gov.sg/api/action/datastore_search";
|
|
21
21
|
const HEADERS = {
|
|
22
22
|
"Accept": "application/json",
|
|
23
|
-
"User-Agent": "freshcontext-mcp/
|
|
23
|
+
"User-Agent": "freshcontext-mcp/0.3.17 (https://github.com/PrinceGabriel-lgtm/freshcontext-mcp)",
|
|
24
24
|
};
|
|
25
25
|
function formatDate(raw) {
|
|
26
26
|
if (!raw)
|
|
@@ -1,9 +1,29 @@
|
|
|
1
1
|
import { chromium } from "playwright";
|
|
2
2
|
import { validateUrl } from "../security.js";
|
|
3
|
+
function isUrl(input) {
|
|
4
|
+
try {
|
|
5
|
+
new URL(input);
|
|
6
|
+
return true;
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function normalizeHnDate(raw) {
|
|
13
|
+
if (!raw)
|
|
14
|
+
return null;
|
|
15
|
+
const match = raw.match(/\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?\b/);
|
|
16
|
+
if (!match)
|
|
17
|
+
return null;
|
|
18
|
+
const isoLike = match[0].endsWith("Z") ? match[0] : `${match[0]}Z`;
|
|
19
|
+
const parsed = new Date(isoLike);
|
|
20
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
|
|
21
|
+
}
|
|
3
22
|
export async function hackerNewsAdapter(options) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
23
|
+
const input = options.url.trim();
|
|
24
|
+
if (!input)
|
|
25
|
+
throw new Error("HN URL or search query is required");
|
|
26
|
+
const url = isUrl(input) ? validateUrl(input, "hackernews") : `hn-search:${input}`;
|
|
7
27
|
if (url.includes("hn.algolia.com/api/") || url.startsWith("hn-search:")) {
|
|
8
28
|
const query = url.startsWith("hn-search:")
|
|
9
29
|
? url.replace("hn-search:", "").trim()
|
|
@@ -20,45 +40,55 @@ export async function hackerNewsAdapter(options) {
|
|
|
20
40
|
`[${i + 1}] ${r.title ?? "Untitled"}`,
|
|
21
41
|
`URL: ${r.url ?? `https://news.ycombinator.com/item?id=${r.objectID}`}`,
|
|
22
42
|
`Score: ${r.points} points | ${r.num_comments} comments`,
|
|
23
|
-
`Author: ${r.author} | Posted: ${r.created_at}`,
|
|
43
|
+
`Author: ${r.author} | Posted: ${normalizeHnDate(r.created_at) ?? r.created_at}`,
|
|
24
44
|
].join("\n"))
|
|
25
45
|
.join("\n\n")
|
|
26
46
|
.slice(0, options.maxLength ?? 4000);
|
|
27
|
-
const newest = data.hits
|
|
47
|
+
const newest = data.hits
|
|
48
|
+
.map((r) => normalizeHnDate(r.created_at))
|
|
49
|
+
.filter((d) => Boolean(d))
|
|
50
|
+
.sort()
|
|
51
|
+
.reverse()[0] ?? null;
|
|
28
52
|
return { raw, content_date: newest, freshness_confidence: newest ? "high" : "medium" };
|
|
29
53
|
}
|
|
30
|
-
// Default: browser-based scrape for HN front page or search pages
|
|
31
54
|
const browser = await chromium.launch({ headless: true });
|
|
32
55
|
const page = await browser.newPage();
|
|
33
56
|
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 20000 });
|
|
34
|
-
const data = await page.evaluate(`(function() {
|
|
35
|
-
var items = Array.from(document.querySelectorAll('.athing')).slice(0, 20);
|
|
36
|
-
var results = items.map(function(el) {
|
|
37
|
-
var titleLineEl = el.querySelector('.titleline > a');
|
|
38
|
-
var title = titleLineEl ? titleLineEl.textContent.trim() : null;
|
|
39
|
-
var link = titleLineEl ? titleLineEl.getAttribute('href') : null;
|
|
40
|
-
var subtext = el.nextElementSibling;
|
|
41
|
-
var scoreEl = subtext ? subtext.querySelector('.score') : null;
|
|
42
|
-
var score = scoreEl ? scoreEl.textContent.trim() : null;
|
|
43
|
-
var ageEl = subtext ? subtext.querySelector('.age') : null;
|
|
44
|
-
var age = ageEl ? ageEl.getAttribute('title') : null;
|
|
45
|
-
var anchors = subtext ? subtext.querySelectorAll('a') : [];
|
|
46
|
-
var commentLink = anchors.length > 0 ? anchors[anchors.length - 1].textContent.trim() : null;
|
|
47
|
-
return { title: title, link: link, score: score, age: age, commentLink: commentLink };
|
|
48
|
-
});
|
|
49
|
-
return results;
|
|
57
|
+
const data = await page.evaluate(`(function() {
|
|
58
|
+
var items = Array.from(document.querySelectorAll('.athing')).slice(0, 20);
|
|
59
|
+
var results = items.map(function(el) {
|
|
60
|
+
var titleLineEl = el.querySelector('.titleline > a');
|
|
61
|
+
var title = titleLineEl ? titleLineEl.textContent.trim() : null;
|
|
62
|
+
var link = titleLineEl ? titleLineEl.getAttribute('href') : null;
|
|
63
|
+
var subtext = el.nextElementSibling;
|
|
64
|
+
var scoreEl = subtext ? subtext.querySelector('.score') : null;
|
|
65
|
+
var score = scoreEl ? scoreEl.textContent.trim() : null;
|
|
66
|
+
var ageEl = subtext ? subtext.querySelector('.age') : null;
|
|
67
|
+
var age = ageEl ? ageEl.getAttribute('title') : null;
|
|
68
|
+
var anchors = subtext ? subtext.querySelectorAll('a') : [];
|
|
69
|
+
var commentLink = anchors.length > 0 ? anchors[anchors.length - 1].textContent.trim() : null;
|
|
70
|
+
return { title: title, link: link, score: score, age: age, commentLink: commentLink };
|
|
71
|
+
});
|
|
72
|
+
return results;
|
|
50
73
|
})()`);
|
|
51
74
|
await browser.close();
|
|
52
75
|
const typedData = data;
|
|
53
76
|
const raw = typedData
|
|
54
|
-
.map((r, i) =>
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
77
|
+
.map((r, i) => {
|
|
78
|
+
const date = normalizeHnDate(r.age);
|
|
79
|
+
return [
|
|
80
|
+
`[${i + 1}] ${r.title ?? "Untitled"}`,
|
|
81
|
+
`URL: ${r.link ?? "N/A"}`,
|
|
82
|
+
`Score: ${r.score ?? "N/A"} | ${r.commentLink ?? ""}`,
|
|
83
|
+
`Posted: ${date ?? "unknown"}`,
|
|
84
|
+
].join("\n");
|
|
85
|
+
})
|
|
60
86
|
.join("\n\n");
|
|
61
|
-
const newestDate = typedData
|
|
87
|
+
const newestDate = typedData
|
|
88
|
+
.map((r) => normalizeHnDate(r.age))
|
|
89
|
+
.filter((d) => Boolean(d))
|
|
90
|
+
.sort()
|
|
91
|
+
.reverse()[0] ?? null;
|
|
62
92
|
return {
|
|
63
93
|
raw,
|
|
64
94
|
content_date: newestDate,
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
import { validateUrl } from "../security.js";
|
|
1
2
|
export async function productHuntAdapter(options) {
|
|
2
|
-
|
|
3
|
+
const token = process.env.PH_TOKEN?.trim() || process.env.PRODUCTHUNT_TOKEN?.trim();
|
|
4
|
+
if (!token)
|
|
5
|
+
return scrapeProductHunt(options);
|
|
6
|
+
// Product Hunt GraphQL API requires a bearer token. Keep it in env/secrets,
|
|
7
|
+
// never in source.
|
|
3
8
|
const query = options.url.startsWith("http")
|
|
4
9
|
? null
|
|
5
10
|
: options.url;
|
|
@@ -28,8 +33,7 @@ export async function productHuntAdapter(options) {
|
|
|
28
33
|
method: "POST",
|
|
29
34
|
headers: {
|
|
30
35
|
"Content-Type": "application/json",
|
|
31
|
-
|
|
32
|
-
"Authorization": "Bearer irgTzMNAz-S-p1P8H5pFCxzU4TEF7GIJZ8vZZi0gLJg",
|
|
36
|
+
"Authorization": `Bearer ${token}`,
|
|
33
37
|
},
|
|
34
38
|
body: JSON.stringify({ query: gql }),
|
|
35
39
|
});
|
|
@@ -68,7 +72,7 @@ export async function productHuntAdapter(options) {
|
|
|
68
72
|
async function scrapeProductHunt(options) {
|
|
69
73
|
const { chromium } = await import("playwright");
|
|
70
74
|
const url = options.url.startsWith("http")
|
|
71
|
-
? options.url
|
|
75
|
+
? validateUrl(options.url, "productHunt")
|
|
72
76
|
: `https://www.producthunt.com/search?q=${encodeURIComponent(options.url)}`;
|
|
73
77
|
const browser = await chromium.launch({ headless: true });
|
|
74
78
|
const page = await browser.newPage();
|